diff --git a/.asf.yaml b/.asf.yaml new file mode 100644 index 00000000000..b1872d166bd --- /dev/null +++ b/.asf.yaml @@ -0,0 +1,53 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +github: + description: "Apache RocketMQ is a cloud native messaging and streaming platform, making it simple to build event-driven applications." + homepage: https://rocketmq.apache.org/ + labels: + - messaging + - streaming + - eventing + - cloud-native + - rocketmq + - java + - hacktoberfest + enabled_merge_buttons: + # Enable squash button + squash: true + # Disable merge button + merge: false + # Disable rebase button + rebase: false + protected_branches: + master: {} + develop: + required_pull_request_reviews: + dismiss_stale_reviews: true + require_code_owner_reviews: false + required_approving_review_count: 1 + required_status_checks: + contexts: + - misspell-check + - check-license + - maven-compile (ubuntu-latest, JDK-8) + - maven-compile (windows-latest, JDK-8) + - maven-compile (macos-latest, JDK-8) +notifications: + commits: commits@rocketmq.apache.org + issues: commits@rocketmq.apache.org + pullrequests: commits@rocketmq.apache.org + jobs: commits@rocketmq.apache.org + discussions: dev@rocketmq.apache.org diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 00000000000..6ebc08d0dc3 --- /dev/null +++ b/.bazelrc @@ -0,0 +1,69 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +startup --host_jvm_args=-Xmx2g + +run --color=yes + +build --color=yes +build --enable_platform_specific_config + +test --action_env=TEST_TMPDIR=/tmp + +test --experimental_strict_java_deps=warn +test --experimental_ui_max_stdouterr_bytes=10485760 +build --experimental_strict_java_deps=warn + +test --test_output=errors + + +# This .bazelrc file contains all of the flags required for the provided +# toolchain with Remote Build Execution. +# Note your WORKSPACE must contain an rbe_autoconfig target with +# name="rbe_default" to use these flags as-is. + +# Depending on how many machines are in the remote execution instance, setting +# this higher can make builds faster by allowing more jobs to run in parallel. +# Setting it too high can result in jobs that timeout, however, while waiting +# for a remote machine to execute them. +build:remote --jobs=150 + +build:remote --remote_executor=grpcs://remote.buildbuddy.io +build:remote --host_platform=@buildbuddy_toolchain//:platform +build:remote --platforms=@buildbuddy_toolchain//:platform +build:remote --extra_execution_platforms=@buildbuddy_toolchain//:platform +build:remote --crosstool_top=@buildbuddy_toolchain//:toolchain +build:remote --extra_toolchains=@buildbuddy_toolchain//:cc_toolchain +build:remote --javabase=@buildbuddy_toolchain//:javabase_jdk8 +build:remote --host_javabase=@buildbuddy_toolchain//:javabase_jdk8 +build:remote --java_toolchain=@buildbuddy_toolchain//:toolchain_jdk8 +build:remote --host_java_toolchain=@buildbuddy_toolchain//:toolchain_jdk8 +build:remote --define=EXECUTOR=remote + +# Enable remote execution so actions are performed on the remote systems. +build:remote --remote_executor=grpcs://remote.buildbuddy.io + +# Enforce stricter environment rules, which eliminates some non-hermetic +# behavior and therefore improves both the remote cache hit rate and the +# correctness and repeatability of the build. +build:remote --incompatible_strict_action_env=true + +# Set a higher timeout value, just in case. +build:remote --remote_timeout=3600 + +# Use a pre-configured account, such that we may have pull-request replacing pull-request-target +build:remote --remote_header=x-buildbuddy-api-key=FD819nUEY7WjvqmoufsU +test:remote --remote_header=x-buildbuddy-api-key=FD819nUEY7WjvqmoufsU diff --git a/.bazelversion b/.bazelversion new file mode 100644 index 00000000000..7cbea073bea --- /dev/null +++ b/.bazelversion @@ -0,0 +1 @@ +5.2.0 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index b0911cf32af..00000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,33 +0,0 @@ -The issue tracker is **ONLY** used for bug report and feature request. Keep in mind, please check whether there is an existing same report before your raise a new one. - -Alternately (especially if your communication is not a bug report), you can send mail to our [mailing lists](http://rocketmq.apache.org/about/contact/). We welcome any friendly suggestions, bug fixes, collaboration and other improvements. - -Please ensure that your bug report is clear and that it is complete. Otherwise, we may be unable to understand it or to reproduce it, either of which would prevent us from fixing the bug. We strongly recommend the report(bug report or feature request) could include some hints as the following: - -**BUG REPORT** - -1. Please describe the issue you observed: - -- What did you do (The steps to reproduce)? - -- What did you expect to see? - -- What did you see instead? - -2. Please tell us about your environment: - -3. Other information (e.g. detailed explanation, logs, related issues, suggestions how to fix, etc): - -**FEATURE REQUEST** - -1. Please describe the feature you are requesting. - -2. Provide any additional detail on your proposed use case for this feature. - -2. Indicate the importance of this issue to you (blocker, must-have, should-have, nice-to-have). Are you currently using any workarounds to address this issue? - -4. If there are some sub-tasks using -[] for each subtask and create a corresponding issue to map to the sub task: - -- [sub-task1-issue-number](example_sub_issue1_link_here): sub-task1 description here, -- [sub-task2-issue-number](example_sub_issue2_link_here): sub-task2 description here, -- ... diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000000..8f1cc71457d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,112 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +name: Bug Report +title: "[Bug] Bug title " +description: Create a report to help us identify any unintended flaws, errors, or faults. +body: + - type: checkboxes + attributes: + label: Before Creating the Bug Report + options: + - label: > + I found a bug, not just asking a question, which should be created in [GitHub Discussions](https://github.com/apache/rocketmq/discussions). + required: true + - label: > + I have searched the [GitHub Issues](https://github.com/apache/rocketmq/issues) and [GitHub Discussions](https://github.com/apache/rocketmq/discussions) of this repository and believe that this is not a duplicate. + required: true + - label: > + I have confirmed that this bug belongs to the current repository, not other repositories of RocketMQ. + required: true + + - type: textarea + attributes: + label: Runtime platform environment + description: Describe the runtime platform environment. + placeholder: > + OS: (e.g., "Ubuntu 20.04") + OS: (e.g., "Windows Server 2019") + validations: + required: true + + - type: textarea + attributes: + label: RocketMQ version + description: Describe the RocketMQ version. + placeholder: > + branch: (e.g develop|4.9.x) + version: (e.g. 5.1.0|4.9.5) + Git commit id: (e.g. c88b5cfa72e204962929eea105687647146112c6) + validations: + required: true + + - type: textarea + attributes: + label: JDK Version + description: Run or Compiler version. + placeholder: > + Compiler: (e.g., "Oracle JDK 11.0.17") + OS: (e.g., "Ubuntu 20.04") + Runtime (if different from JDK above): (e.g., "Oracle JRE 8u251") + OS (if different from OS compiled on): (e.g., "Windows Server 2019") + validations: + required: false + + - type: textarea + attributes: + label: Describe the Bug + description: Describe what happened. + placeholder: > + A clear and concise description of what the bug is. + validations: + required: true + + - type: textarea + attributes: + label: Steps to Reproduce + description: Describe the steps to reproduce the bug here. + placeholder: > + If possible, provide a recipe for reproducing the error. + validations: + required: true + + - type: textarea + attributes: + label: What Did You Expect to See? + description: You expect to see result. + placeholder: > + A clear and concise description of what you expected to see. + validations: + required: true + + - type: textarea + attributes: + label: What Did You See Instead? + description: You instead to see result. + placeholder: > + A clear and concise description of what you saw instead. + validations: + required: true + + - type: textarea + attributes: + label: Additional Context + description: Additional context. + placeholder: > + Add any other context about the problem here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..870c2b1d086 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,23 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +blank_issues_enabled: false +contact_links: + - name: Ask Question + url: https://github.com/apache/rocketmq/discussions + about: Please go to GitHub Disccusions to ask questions diff --git a/.github/ISSUE_TEMPLATE/doc.yml b/.github/ISSUE_TEMPLATE/doc.yml new file mode 100644 index 00000000000..e68928464a7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/doc.yml @@ -0,0 +1,55 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +name: Documentation Related +title: "[Doc] Documentation Related " +description: I find some issues related to the documentation. +labels: [ "module/doc" ] +body: + - type: checkboxes + attributes: + label: Search before creation + description: > + Please make sure to search in the [issues](https://github.com/apache/rocketmq/issues) + first to see whether the same issue was reported already. + options: + - label: > + I had searched in the [issues](https://github.com/apache/rocketmq/issues) and found + no similar issues. + required: true + + - type: textarea + attributes: + label: Documentation Related + description: Describe the suggestion about document. + placeholder: > + e.g There is a typo + validations: + required: true + + - type: checkboxes + attributes: + label: Are you willing to submit PR? + description: > + This is absolutely not required, but we are happy to guide you in the contribution process + especially if you already have a good understanding of how to implement the fix. + options: + - label: Yes I am willing to submit a PR! + + - type: markdown + attributes: + value: "Thanks for completing our form!" diff --git a/.github/ISSUE_TEMPLATE/enhancement_request.yml b/.github/ISSUE_TEMPLATE/enhancement_request.yml new file mode 100644 index 00000000000..cac503d17bd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement_request.yml @@ -0,0 +1,75 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +name: Enhancement Request +title: "[Enhancement] Enhancement title" +description: Suggest an enhancement for this project +labels: [ "type/enhancement" ] +body: + - type: checkboxes + attributes: + label: Before Creating the Enhancement Request + description: > + Most of issues should be classified as bug or feature request. An issue should be considered as an enhancement when it proposes improvements to + existing functionality or user experience, without necessarily introducing new features or fixing existing bugs. + options: + - label: > + I have confirmed that this should be classified as an enhancement rather than a bug/feature. + required: true + + - type: textarea + attributes: + label: Summary + placeholder: > + A clear and concise description of the enhancement you would like to see in the project. + validations: + required: true + + - type: textarea + attributes: + label: Motivation + placeholder: > + Explain why you believe this enhancement is necessary, and how it benefits the project and community. + Include any specific use cases that you have in mind. + validations: + required: true + + - type: textarea + attributes: + label: Describe the Solution You'd Like + placeholder: > + Describe the enhancement you propose, detailing the change and implementation steps involved. + If you have multiple solutions, please list them separately. + validations: + required: true + + - type: textarea + attributes: + label: Describe Alternatives You've Considered + placeholder: > + List any alternative enhancements or implementations you have considered, and explain why they may not be as effective or appropriate. + validations: + required: true + + - type: textarea + attributes: + label: Additional Context + placeholder: > + Add any relevant context, screenshots, prototypes, or other supplementary information to help illustrate the enhancement. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000000..8361b8aee92 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,58 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +name: Feature Request +title: "[Feature] New feature title" +description: Suggest an idea for this project. +labels: [ "type/new feature" ] +body: + - type: textarea + attributes: + label: Is Your Feature Request Related to a Problem? + description: Please Describe It. + placeholder: > + A clear and concise description of what the problem is. + validations: + required: true + + - type: textarea + attributes: + label: Describe the Solution You'd Like + description: Describe how you solved it. + placeholder: > + A clear and concise description of what you want to happen. + validations: + required: true + + - type: textarea + attributes: + label: Describe Alternatives You've Considered + description: Describe your solution + placeholder: > + A clear and concise description of any alternative solutions or features you've considered. + validations: + required: true + + - type: textarea + attributes: + label: Additional Context + description: Additional context. + placeholder: > + Add any other context about the problem here. + validations: + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index eb8cb83a63c..96bffa55a3f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,22 +1,15 @@ -Please do not create a Pull Request without creating an issue first. + -## What is the purpose of the change +### Which Issue(s) This PR Fixes -XXXXX + -## Brief changelog +Fixes #issue_id -XX +### Brief Description -## Verifying this change + -XXXX +### How Did You Test This Change? -Follow this checklist to help us incorporate your contribution quickly and easily: - -- [x] Make sure there is a Github issue filed for the change (usually before you start working on it). Trivial changes like typos do not require a Github issue. Your pull request should address just this issue, without pulling in other changes - one PR resolves one issue. -- [ ] Format the pull request title like `[ISSUE #123] Fix UnknownException when host config not exist`. Each commit in the pull request should have a meaningful subject line and body. -- [ ] Write a pull request description that is detailed enough to understand what the pull request does, how, and why. -- [ ] Write necessary unit-test to verify your logic correction, more mock a little better when cross module dependency exist. If the new feature or significant change is committed, please remember to add integration-test in [test module](https://github.com/apache/rocketmq/tree/master/test). -- [ ] Run `mvn -B clean apache-rat:check findbugs:findbugs checkstyle:checkstyle` to make sure basic checks pass. Run `mvn clean install -DskipITs` to make sure unit-test pass. Run `mvn clean test-compile failsafe:integration-test` to make sure integration-test pass. -- [ ] If this contribution is large, please file an [Apache Individual Contributor License Agreement](http://www.apache.org/licenses/#clas). + diff --git a/.github/asf-deploy-settings.xml b/.github/asf-deploy-settings.xml new file mode 100644 index 00000000000..246bf0973f7 --- /dev/null +++ b/.github/asf-deploy-settings.xml @@ -0,0 +1,38 @@ + + + + + + + + apache.snapshots.https + ${env.NEXUS_DEPLOY_USERNAME} + ${env.NEXUS_DEPLOY_PASSWORD} + + + 60 + + + + + + \ No newline at end of file diff --git a/.github/workflows/bazel.yml b/.github/workflows/bazel.yml new file mode 100644 index 00000000000..af674592bb9 --- /dev/null +++ b/.github/workflows/bazel.yml @@ -0,0 +1,22 @@ +name: Build and Run Tests by Bazel +on: + pull_request: + types: [opened, reopened, synchronize] + push: + branches: + - master + - develop + - bazel +jobs: + build: + name: "bazel-compile (${{ matrix.os }})" + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + steps: + - uses: actions/checkout@v2 + - name: Build + run: bazel build --config=remote //... + - name: Run Tests + run: bazel test --config=remote //... \ No newline at end of file diff --git a/.github/workflows/codeql_analysis.yml b/.github/workflows/codeql_analysis.yml new file mode 100644 index 00000000000..e5e8d323a29 --- /dev/null +++ b/.github/workflows/codeql_analysis.yml @@ -0,0 +1,32 @@ +name: CodeQL Analysis + +on: + pull_request: + types: [opened, reopened, synchronize] + push: + branches: + - master + +jobs: + CodeQL-Build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Cache Maven packages + uses: actions/cache@v3 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: java + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000000..81db2a656cb --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,24 @@ +name: Coverage +on: + pull_request: + types: [opened, reopened, synchronize] + push: + branches: [master, develop] +jobs: + calculate-coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Set up JDK 8 + uses: actions/setup-java@v2 + with: + java-version: "8" + distribution: "adopt" + cache: "maven" + - name: Generate coverage report + run: mvn -B test -T 2C --file pom.xml + - name: Upload to Codecov + uses: codecov/codecov-action@v3 + with: + fail_ci_if_error: true + verbose: true diff --git a/.github/workflows/license-checker.yaml b/.github/workflows/license-checker.yaml new file mode 100644 index 00000000000..259fdd7ffa7 --- /dev/null +++ b/.github/workflows/license-checker.yaml @@ -0,0 +1,34 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: License checker + +on: + pull_request: + branches: + - develop + - master + +jobs: + check-license: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check License Header + uses: apache/skywalking-eyes@v0.2.0 + with: + log: info + config: .licenserc.yaml diff --git a/.github/workflows/maven.yaml b/.github/workflows/maven.yaml new file mode 100644 index 00000000000..75bf91eb18f --- /dev/null +++ b/.github/workflows/maven.yaml @@ -0,0 +1,27 @@ +name: Build and Run Tests by Maven +on: + pull_request: + types: [opened, reopened, synchronize] + push: + branches: [master, develop, bazel] +jobs: + java_build: + name: "maven-compile (${{ matrix.os }}, JDK-${{ matrix.jdk }})" + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # see https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources + os: [ubuntu-latest, windows-latest, macos-latest] + jdk: [8] + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v2 + with: + java-version: ${{ matrix.jdk }} + distribution: "adopt" + cache: "maven" + - name: Build with Maven + run: mvn -B package --file pom.xml diff --git a/.github/workflows/misspell_check.yml b/.github/workflows/misspell_check.yml new file mode 100644 index 00000000000..81729e42a41 --- /dev/null +++ b/.github/workflows/misspell_check.yml @@ -0,0 +1,17 @@ +name: Misspell Check +on: + pull_request: + types: [opened, reopened, synchronize] + push: + branches: [master, develop] +jobs: + misspell-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Install misspell + run: | + curl -L -o ./install-misspell.sh https://git.io/misspell + sh ./install-misspell.sh + - name: Run misspell + run: find . -type f -print0 | xargs -0 bin/misspell -error -i transfered,derivate diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml new file mode 100644 index 00000000000..ef2db755d00 --- /dev/null +++ b/.github/workflows/pr-ci.yml @@ -0,0 +1,36 @@ +name: PR-CI + +on: + pull_request: + types: [opened, reopened, synchronize] + +jobs: + dist-tar: + name: Build distribution tar + runs-on: ubuntu-latest + timeout-minutes: 120 + steps: + - uses: actions/checkout@v3 + with: + submodules: true + - uses: actions/setup-java@v3 + with: + distribution: "temurin" + java-version: "8" + cache: "maven" + - name: Build distribution tar + run: | + mvn -Prelease-all -DskipTests -Dspotbugs.skip=true clean install -U + - uses: actions/upload-artifact@v3 + name: Upload distribution tar + with: + name: rocketmq + path: distribution/target/rocketmq*/rocketmq* + - name: Save PR number + run: | + mkdir -p ./pr + echo ${{ github.event.number }} > ./pr/NR + - uses: actions/upload-artifact@v2 + with: + name: pr + path: pr/ diff --git a/.github/workflows/pr-e2e-test.yml b/.github/workflows/pr-e2e-test.yml new file mode 100644 index 00000000000..d0371e31135 --- /dev/null +++ b/.github/workflows/pr-e2e-test.yml @@ -0,0 +1,257 @@ +name: E2E test for pull request + +# read-write repo token +# access to secrets +on: + workflow_run: + workflows: ["PR-CI"] + types: + - completed + +env: + DOCKER_REPO: apache/rocketmq-ci + +jobs: + docker: + runs-on: ubuntu-latest + if: > + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' + timeout-minutes: 30 + strategy: + matrix: + base-image: ["ubuntu"] + java-version: ["8"] + steps: + - name: 'Download artifact' + uses: actions/github-script@v3.1.0 + with: + script: | + var artifacts = await github.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{github.event.workflow_run.id }}, + }); + var matchArtifactRmq = artifacts.data.artifacts.filter((artifact) => { + return artifact.name == "rocketmq" + })[0]; + var download = await github.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifactRmq.id, + archive_format: 'zip', + }); + var fs = require('fs'); + fs.writeFileSync('${{github.workspace}}/rocketmq.zip', Buffer.from(download.data)); + - run: | + unzip rocketmq.zip + mkdir rocketmq + cp -r rocketmq-* rocketmq/ + ls + - uses: actions/checkout@v3 + with: + repository: apache/rocketmq-docker.git + ref: master + path: rocketmq-docker + - name: docker-login + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and save docker images + id: build-images + run: | + cd rocketmq-docker/image-build-ci + version=${{ github.event.pull_request.number || github.ref_name }}-$(uuidgen) + mkdir versionlist + touch versionlist/"${version}-`echo ${{ matrix.base-image }} | sed -e "s/:/-/g"`" + sh ./build-image-local.sh ${version} ${{ matrix.base-image }} ${{ matrix.java-version }} ${DOCKER_REPO} + - uses: actions/upload-artifact@v3 + name: Upload distribution tar + with: + name: versionlist + path: rocketmq-docker/image-build-ci/versionlist/* + + list-version: + if: always() + name: List version + needs: [docker] + runs-on: ubuntu-latest + timeout-minutes: 30 + outputs: + version-json: ${{ steps.show_versions.outputs.version-json }} + steps: + - uses: actions/download-artifact@v3 + name: Download versionlist + with: + name: versionlist + path: versionlist + - name: Show versions + id: show_versions + run: | + a=(`ls versionlist`) + printf '%s\n' "${a[@]}" | jq -R . | jq -s . + echo version-json=`printf '%s\n' "${a[@]}" | jq -R . | jq -s .` >> $GITHUB_OUTPUT + deploy: + if: ${{ success() }} + name: Deploy RocketMQ + needs: [list-version,docker] + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + matrix: + version: ${{ fromJSON(needs.list-version.outputs.version-json) }} + steps: + - uses: apache/rocketmq-test-tool@7d84d276ad7755b1dc5cf9657a7a9bff6ae6d288 + name: Deploy rocketmq + with: + action: "deploy" + ask-config: "${{ secrets.ASK_CONFIG_VIRGINA }}" + test-version: "${{ matrix.version }}" + chart-git: "https://ghproxy.com/https://github.com/apache/rocketmq-docker.git" + chart-branch: "master" + chart-path: "./rocketmq-k8s-helm" + job-id: ${{ strategy.job-index }} + helm-values: | + nameserver: + image: + repository: ${{env.DOCKER_REPO}} + tag: ${{ matrix.version }} + broker: + image: + repository: ${{env.DOCKER_REPO}} + tag: ${{ matrix.version }} + proxy: + image: + repository: ${{env.DOCKER_REPO}} + tag: ${{ matrix.version }} + test-e2e-grpc-java: + if: ${{ success() }} + name: Test E2E grpc java + needs: [list-version, deploy] + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + matrix: + version: ${{ fromJSON(needs.list-version.outputs.version-json) }} + steps: + - uses: apache/rocketmq-test-tool@7d84d276ad7755b1dc5cf9657a7a9bff6ae6d288 + name: e2e test + with: + action: "test" + ask-config: "${{ secrets.ASK_CONFIG_VIRGINA }}" + test-version: "${{ matrix.version }}" + test-code-git: "https://ghproxy.com/https://github.com/apache/rocketmq-e2e" + test-code-branch: "master" + test-code-path: java/e2e + test-cmd: "mvn -B test" + job-id: 0 + - name: Publish Test Report + uses: mikepenz/action-junit-report@v3 + if: always() # always run even if the previous step fails + with: + report_paths: '**/test_report/TEST-*.xml' + annotate_only: true + include_passed: true + detailed_summary: true + - uses: actions/upload-artifact@v3 + if: always() + name: Upload test log + with: + name: test-e2e-grpc-java-log.txt + path: testlog.txt + + test-e2e-golang: + if: ${{ success() }} + name: Test E2E golang + needs: [list-version, deploy] + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + matrix: + version: ${{ fromJSON(needs.list-version.outputs.version-json) }} + steps: + - uses: apache/rocketmq-test-tool@7d84d276ad7755b1dc5cf9657a7a9bff6ae6d288 + name: e2e test + with: + action: "test" + ask-config: "${{ secrets.ASK_CONFIG_VIRGINA }}" + test-version: "${{ matrix.version }}" + test-code-git: "https://ghproxy.com/https://github.com/apache/rocketmq-e2e" + test-code-branch: "master" + test-code-path: golang + test-cmd: | + cd ../common && mvn -Prelease -DskipTests clean package -U + cd ../rocketmq-admintools && source bin/env.sh + cd ../golang && go get -u github.com/apache/rocketmq-clients/golang && gotestsum --junitfile ./target/surefire-reports/TEST-report.xml ./mqgotest/... -timeout 2m -v + job-id: 0 + - name: Publish Test Report + uses: mikepenz/action-junit-report@v3 + if: always() # always run even if the previous step fails + with: + report_paths: '**/test_report/TEST-*.xml' + annotate_only: true + include_passed: true + detailed_summary: true + - uses: actions/upload-artifact@v3 + if: always() + name: Upload test log + with: + name: test-e2e-golang-log.txt + path: testlog.txt + + test-e2e-remoting-java: + if: ${{ success() }} + name: Test E2E remoting java + needs: [ list-version, deploy ] + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + matrix: + version: ${{ fromJSON(needs.list-version.outputs.version-json) }} + steps: + - uses: apache/rocketmq-test-tool@7d84d276ad7755b1dc5cf9657a7a9bff6ae6d288 + name: e2e test + with: + action: "test" + ask-config: "${{ secrets.ASK_CONFIG_VIRGINA }}" + test-version: "${{ matrix.version }}" + test-code-git: "https://ghproxy.com/https://github.com/apache/rocketmq-e2e" + test-code-branch: "master" + test-code-path: java/e2e-v4 + test-cmd: "mvn -B test" + job-id: 0 + - name: Publish Test Report + uses: mikepenz/action-junit-report@v3 + if: always() # always run even if the previous step fails + with: + report_paths: '**/test_report/TEST-*.xml' + annotate_only: true + include_passed: true + detailed_summary: true + - uses: actions/upload-artifact@v3 + if: always() + name: Upload test log + with: + name: test-e2e-remoting-java-log.txt + path: testlog.txt + + clean: + if: always() + name: Clean + needs: [list-version, test-e2e-grpc-java, test-e2e-golang, test-e2e-remoting-java] + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + matrix: + version: ${{ fromJSON(needs.list-version.outputs.version-json) }} + steps: + - uses: apache/rocketmq-test-tool@7d84d276ad7755b1dc5cf9657a7a9bff6ae6d288 + name: clean + with: + action: "clean" + ask-config: "${{ secrets.ASK_CONFIG_VIRGINA }}" + test-version: "${{ matrix.version }}" + job-id: ${{ strategy.job-index }} + diff --git a/.github/workflows/push-ci.yml b/.github/workflows/push-ci.yml new file mode 100644 index 00000000000..ad29a57c8a8 --- /dev/null +++ b/.github/workflows/push-ci.yml @@ -0,0 +1,262 @@ +name: PUSH-CI + +on: + push: + branches: [master, develop] + #schedule: + # - cron: "0 18 * * *" # TimeZone: UTC 0 + +concurrency: + group: rocketmq-${{ github.ref }} + +env: + MAVEN_OPTS: -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.httpconnectionManager.ttlSeconds=120 + DOCKER_REPO: apache/rocketmq-ci + +jobs: + dist-tar: + name: Build dist tar + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v3 + with: + submodules: true + - uses: actions/setup-java@v3 + with: + distribution: "temurin" + java-version: "8" + cache: "maven" + - name: Build distribution tar + run: | + mvn -Prelease-all -DskipTests -Dspotbugs.skip=true clean install -U + - uses: actions/upload-artifact@v3 + name: Upload distribution tar + with: + name: rocketmq + path: distribution/target/rocketmq*/rocketmq* + + docker: + if: ${{ success() }} + name: Docker images + needs: [dist-tar] + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + matrix: + base-image: ["ubuntu"] + java-version: ["8"] + steps: + - uses: actions/checkout@v3 + with: + repository: apache/rocketmq-docker.git + ref: master + path: rocketmq-docker + - uses: actions/download-artifact@v3 + name: Download distribution tar + with: + name: rocketmq + path: rocketmq + - name: docker-login + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and save docker images + id: build-images + run: | + cd rocketmq-docker/image-build-ci + version=${{ github.event.pull_request.number || github.ref_name }}-$(uuidgen) + mkdir versionlist + touch versionlist/"${version}-`echo ${{ matrix.base-image }} | sed -e "s/:/-/g"`" + sh ./build-image-local.sh ${version} ${{ matrix.base-image }} ${{ matrix.java-version }} ${DOCKER_REPO} + - uses: actions/upload-artifact@v3 + name: Upload distribution tar + with: + name: versionlist + path: rocketmq-docker/image-build-ci/versionlist/* + + + list-version: + if: always() + name: List version + needs: [docker] + runs-on: ubuntu-latest + timeout-minutes: 30 + outputs: + version-json: ${{ steps.show_versions.outputs.version-json }} + steps: + - uses: actions/download-artifact@v3 + name: Download versionlist + with: + name: versionlist + path: versionlist + - name: Show versions + id: show_versions + run: | + a=(`ls versionlist`) + printf '%s\n' "${a[@]}" | jq -R . | jq -s . + echo version-json=`printf '%s\n' "${a[@]}" | jq -R . | jq -s .` >> $GITHUB_OUTPUT + deploy: + if: ${{ success() }} + name: Deploy RocketMQ + needs: [list-version,docker] + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + matrix: + version: ${{ fromJSON(needs.list-version.outputs.version-json) }} + steps: + - uses: apache/rocketmq-test-tool@7d84d276ad7755b1dc5cf9657a7a9bff6ae6d288 + name: Deploy rocketmq + with: + action: "deploy" + ask-config: "${{ secrets.ASK_CONFIG_VIRGINA }}" + test-version: "${{ matrix.version }}" + chart-git: "https://ghproxy.com/https://github.com/apache/rocketmq-docker.git" + chart-branch: "master" + chart-path: "./rocketmq-k8s-helm" + job-id: ${{ strategy.job-index }} + helm-values: | + nameserver: + image: + repository: ${{env.DOCKER_REPO}} + tag: ${{ matrix.version }} + broker: + image: + repository: ${{env.DOCKER_REPO}} + tag: ${{ matrix.version }} + proxy: + image: + repository: ${{env.DOCKER_REPO}} + tag: ${{ matrix.version }} + test-e2e-grpc-java: + if: ${{ success() }} + name: Test E2E grpc java + needs: [list-version, deploy] + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + matrix: + version: ${{ fromJSON(needs.list-version.outputs.version-json) }} + steps: + - uses: apache/rocketmq-test-tool@7d84d276ad7755b1dc5cf9657a7a9bff6ae6d288 + name: e2e test + with: + action: "test" + ask-config: "${{ secrets.ASK_CONFIG_VIRGINA }}" + test-version: "${{ matrix.version }}" + test-code-git: "https://ghproxy.com/https://github.com/apache/rocketmq-e2e" + test-code-branch: "master" + test-code-path: java/e2e + test-cmd: "mvn -B test" + job-id: 0 + - name: Publish Test Report + uses: mikepenz/action-junit-report@v3 + if: always() # always run even if the previous step fails + with: + report_paths: '**/test_report/TEST-*.xml' + annotate_only: true + include_passed: true + detailed_summary: true + - uses: actions/upload-artifact@v3 + if: always() + name: Upload test log + with: + name: test-e2e-grpc-java-log.txt + path: testlog.txt + + test-e2e-golang: + if: ${{ success() }} + name: Test E2E golang + needs: [list-version, deploy] + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + matrix: + version: ${{ fromJSON(needs.list-version.outputs.version-json) }} + steps: + - uses: apache/rocketmq-test-tool@7d84d276ad7755b1dc5cf9657a7a9bff6ae6d288 + name: e2e test + with: + action: "test" + ask-config: "${{ secrets.ASK_CONFIG_VIRGINA }}" + test-version: "${{ matrix.version }}" + test-code-git: "https://ghproxy.com/https://github.com/apache/rocketmq-e2e" + test-code-branch: "master" + test-code-path: golang + test-cmd: | + cd ../common && mvn -Prelease -DskipTests clean package -U + cd ../rocketmq-admintools && source bin/env.sh + cd ../golang && go get -u github.com/apache/rocketmq-clients/golang && gotestsum --junitfile ./target/surefire-reports/TEST-report.xml ./mqgotest/... -timeout 2m -v + job-id: 0 + - name: Publish Test Report + uses: mikepenz/action-junit-report@v3 + if: always() # always run even if the previous step fails + with: + report_paths: '**/test_report/TEST-*.xml' + annotate_only: true + include_passed: true + detailed_summary: true + - uses: actions/upload-artifact@v3 + if: always() + name: Upload test log + with: + name: test-e2e-golang-log.txt + path: testlog.txt + + test-e2e-remoting-java: + if: ${{ success() }} + name: Test E2E remoting java + needs: [ list-version, deploy ] + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + matrix: + version: ${{ fromJSON(needs.list-version.outputs.version-json) }} + steps: + - uses: apache/rocketmq-test-tool@7d84d276ad7755b1dc5cf9657a7a9bff6ae6d288 + name: e2e test + with: + action: "test" + ask-config: "${{ secrets.ASK_CONFIG_VIRGINA }}" + test-version: "${{ matrix.version }}" + test-code-git: "https://ghproxy.com/https://github.com/apache/rocketmq-e2e" + test-code-branch: "master" + test-code-path: java/e2e-v4 + test-cmd: "mvn -B test" + job-id: 0 + - name: Publish Test Report + uses: mikepenz/action-junit-report@v3 + if: always() # always run even if the previous step fails + with: + report_paths: '**/test_report/TEST-*.xml' + annotate_only: true + include_passed: true + detailed_summary: true + - uses: actions/upload-artifact@v3 + if: always() + name: Upload test log + with: + name: test-e2e-remoting-java-log.txt + path: testlog.txt + + clean: + if: always() + name: Clean + needs: [list-version, test-e2e-grpc-java, test-e2e-golang, test-e2e-remoting-java] + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + matrix: + version: ${{ fromJSON(needs.list-version.outputs.version-json) }} + steps: + - uses: apache/rocketmq-test-tool@7d84d276ad7755b1dc5cf9657a7a9bff6ae6d288 + name: clean + with: + action: "clean" + ask-config: "${{ secrets.ASK_CONFIG_VIRGINA }}" + test-version: "${{ matrix.version }}" + job-id: ${{ strategy.job-index }} + diff --git a/.github/workflows/snapshot-automation.yml b/.github/workflows/snapshot-automation.yml new file mode 100644 index 00000000000..88f5f4e0ccb --- /dev/null +++ b/.github/workflows/snapshot-automation.yml @@ -0,0 +1,258 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Snapshot Daily Release Automation +on: + schedule: # schedule the job to run at 12 a.m. daily + - cron: "0 0 * * *" + workflow_dispatch: + inputs: + branch: + description: 'The branch to trigger the workflow, The default branch is "develop" when both branch and commit_id are empty' + required: false + commit_id: + description: 'The commit id to trigger the workflow. Do not set branch and commit_id together' + required: false + rocketmq_version: + description: 'Name of the SNAPSHOT version to be generated. The default version is "$VERSION-stable-SNAPSHOT"' + required: false + +env: + MAVEN_OPTS: -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.httpconnectionManager.ttlSeconds=120 + DOCKER_REPO: apache/rocketmq-ci + +jobs: + dist-tar: + name: Build dist tar + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout develop + if: github.event.inputs.branch == '' && github.event.inputs.commit_id == '' + uses: actions/checkout@v3 + with: + ref: develop + + - name: Checkout specific commit + if: github.event.inputs.branch == '' && github.event.inputs.commit_id != '' + uses: actions/checkout@v3 + with: + ref: ${{ github.event.inputs.commit_id }} + + - name: Checkout specific branch + if: github.event.inputs.branch != '' && github.event.inputs.commit_id == '' + uses: actions/checkout@v3 + with: + ref: ${{ github.event.inputs.branch }} + + - uses: actions/setup-java@v3 + with: + distribution: "temurin" + java-version: "8" + cache: "maven" + - name: Build distribution tar + env: + MAVEN_SETTINGS: ${{ github.workspace }}/.github/asf-deploy-settings.xml + run: | + mvn -Prelease-all -DskipTests -Dspotbugs.skip=true clean install -U + - uses: actions/upload-artifact@v3 + name: Upload distribution tar + with: + name: rocketmq + path: distribution/target/rocketmq*/rocketmq* + + docker-build: + if: ${{ success() }} + name: Docker images + needs: [ dist-tar ] + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + matrix: + base-image: [ "ubuntu" ] + java-version: [ "8" ] + steps: + - uses: actions/checkout@v3 + with: + repository: apache/rocketmq-docker.git + ref: master + path: rocketmq-docker + - uses: actions/download-artifact@v3 + name: Download distribution tar + with: + name: rocketmq + path: rocketmq + - name: docker-login + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and save docker images + id: build-images + run: | + cd rocketmq-docker/image-build-ci + version=${{ github.event.pull_request.number || github.ref_name }}-$(uuidgen) + mkdir versionlist + touch versionlist/"${version}-`echo ${{ matrix.base-image }} | sed -e "s/:/-/g"`" + sh ./build-image-local.sh ${version} ${{ matrix.base-image }} ${{ matrix.java-version }} ${DOCKER_REPO} + - uses: actions/upload-artifact@v3 + name: Upload distribution tar + with: + name: versionlist + path: rocketmq-docker/image-build-ci/versionlist/* + + list-version: + if: always() + name: List version + needs: [ docker-build ] + runs-on: ubuntu-latest + timeout-minutes: 30 + outputs: + version-json: ${{ steps.show_versions.outputs.version-json }} + steps: + - uses: actions/download-artifact@v3 + name: Download versionlist + with: + name: versionlist + path: versionlist + - name: Show versions + id: show_versions + run: | + a=(`ls versionlist`) + printf '%s\n' "${a[@]}" | jq -R . | jq -s . + echo version-json=`printf '%s\n' "${a[@]}" | jq -R . | jq -s .` >> $GITHUB_OUTPUT + + deploy-rocketmq: + if: ${{ success() }} + name: Deploy RocketMQ + needs: [ list-version,docker-build ] + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + matrix: + version: ${{ fromJSON(needs.list-version.outputs.version-json) }} + steps: + - uses: apache/rocketmq-test-tool@1a646589accad17070423eabf0f54925e52b0666 + name: Deploy rocketmq + with: + action: "deploy" + ask-config: "${{ secrets.ASK_CONFIG_VIRGINA }}" + test-version: "${{ matrix.version }}" + chart-git: "https://github.com/apache/rocketmq-docker.git" + chart-branch: "master" + chart-path: "./rocketmq-k8s-helm" + job-id: ${{ strategy.job-index }} + helm-values: | + nameserver: + image: + repository: ${{env.DOCKER_REPO}} + tag: ${{ matrix.version }} + broker: + image: + repository: ${{env.DOCKER_REPO}} + tag: ${{ matrix.version }} + proxy: + image: + repository: ${{env.DOCKER_REPO}} + tag: ${{ matrix.version }} + + java-grpc-e2e-test: + if: ${{ success() }} + name: E2E Test + needs: [ list-version, deploy-rocketmq ] + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + matrix: + version: ${{ fromJSON(needs.list-version.outputs.version-json) }} + steps: + - uses: apache/rocketmq-test-tool@1a646589accad17070423eabf0f54925e52b0666 + name: e2e test + with: + action: "test" + ask-config: "${{ secrets.ASK_CONFIG_VIRGINA }}" + test-version: "${{ matrix.version }}" + test-code-git: "https://github.com/apache/rocketmq-e2e.git" + test-code-branch: "master" + test-code-path: java/e2e + test-cmd: "mvn -B test" + job-id: ${{ strategy.job-index }} + - name: Publish Test Report + uses: mikepenz/action-junit-report@v3 + if: always() # always run even if the previous step fails + with: + report_paths: '**/test_report/TEST-*.xml' + annotate_only: true + include_passed: true + detailed_summary: true + - uses: actions/upload-artifact@v3 + if: always() + name: Upload test log + with: + name: testlog.txt + path: testlog.txt + + clean: + if: always() + name: Clean + needs: [ list-version, java-grpc-e2e-test ] + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + matrix: + version: ${{ fromJSON(needs.list-version.outputs.version-json) }} + steps: + - uses: apache/rocketmq-test-tool@1a646589accad17070423eabf0f54925e52b0666 + name: clean + with: + action: "clean" + ask-config: "${{ secrets.ASK_CONFIG_VIRGINA }}" + test-version: "${{ matrix.version }}" + job-id: ${{ strategy.job-index }} + + snapshot: + runs-on: ubuntu-latest + needs: [ java-grpc-e2e-test ] + env: + NEXUS_DEPLOY_USERNAME: ${{ secrets.NEXUS_USER }} + NEXUS_DEPLOY_PASSWORD: ${{ secrets.NEXUS_PW }} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + ref: develop + persist-credentials: false + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: 8 + distribution: "temurin" + cache: "maven" + - name: Update default pom version + if: github.event.inputs.rocketmq_version == '' + run: | + VERSION=$(mvn -q -Dexec.executable='echo' -Dexec.args='${project.version}' --non-recursive exec:exec) + VERSION=$(echo $VERSION | awk -F '-SNAPSHOT' '{print $1}') + VERSION=$VERSION-stable-SNAPSHOT + mvn versions:set -DnewVersion=$VERSION + - name: Update User-defined pom version + if: github.event.inputs.rocketmq_version != '' + run: | + mvn versions:set -DnewVersion=${{ github.event.inputs.rocketmq_version }} + - name: Deploy to ASF Snapshots Repository + timeout-minutes: 40 + run: mvn clean deploy -DskipTests=true --settings .github/asf-deploy-settings.xml diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000000..ca1a153e76f --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,32 @@ +name: Close Stale Issues/PRs + +permissions: + issues: write + pull-requests: write + +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * *" + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v5 + with: + operations-per-run: 128 + days-before-issue-stale: 365 + days-before-issue-close: 3 + exempt-issue-labels: "no stale" + stale-issue-label: "stale" + stale-issue-message: "This issue is stale because it has been open for 365 days with no activity. It will be closed in 3 days if no further activity occurs." + close-issue-message: "This issue was closed because it has been inactive for 3 days since being marked as stale." + remove-issue-stale-when-updated: true + days-before-pr-stale: 365 + days-before-pr-close: 3 + exempt-pr-labels: "no stale" + stale-pr-label: "stale" + stale-pr-message: "This PR is stale because it has been open for 365 days with no activity. It will be closed in 3 days if no further activity occurs. If you wish not to mark it as stale, please leave a comment in this PR." + close-pr-message: "This PR was closed because it has been inactive for 3 days since being marked as stale." + remove-pr-stale-when-updated: true diff --git a/.gitignore b/.gitignore index 80c6f569862..c20f4bf7685 100644 --- a/.gitignore +++ b/.gitignore @@ -4,10 +4,17 @@ .settings/ target/ devenv -*.log* +*.log.* *.iml .idea/ *.versionsBackup !NOTICE-BIN !LICENSE-BIN -.DS_Store \ No newline at end of file +.DS_Store +localbin +nohup.out +bazel-out +bazel-bin +bazel-rocketmq +bazel-testlogs +.vscode \ No newline at end of file diff --git a/.licenserc.yaml b/.licenserc.yaml new file mode 100644 index 00000000000..f74fb8c5dec --- /dev/null +++ b/.licenserc.yaml @@ -0,0 +1,50 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +header: + license: + spdx-id: Apache-2.0 + copyright-owner: Apache Software Foundation + + paths-ignore: + - '.gitignore' + - '.travis.yml' + - 'CONTRIBUTING.md' + - 'LICENSE' + - 'NOTICE' + - '**/*.md' + - 'BUILDING' + - '.github/**' + - '*/src/test/resources/certs/*' + - 'src/test/**/*.log' + - '*/src/test/resources/META-INF/service/*' + - '*/src/main/resources/META-INF/service/*' + - '*/src/test/resources/rmq-proxy-home/conf/rmq-proxy.json' + - '*/src/test/resources/mockito-extensions/*' + - '**/target/**' + - '**/*.iml' + - 'docs/**' + - 'localbin/**' + - 'distribution/LICENSE-BIN' + - 'distribution/NOTICE-BIN' + - 'distribution/conf/rmq-proxy.json' + - '.bazelversion' + - 'common/src/main/resources/META-INF/services/org.apache.rocketmq.logging.ch.qos.logback.classic.spi.Configurator' + + + comment: on-failure \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8f65c72f7d0..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,30 +0,0 @@ -notifications: - email: - recipients: - - dev@rocketmq.apache.org - on_success: change - on_failure: always - -language: java - -matrix: - include: - # On OSX, run with default JDK only. - # - os: osx - # On Linux, run with specific JDKs only. - - os: linux - env: CUSTOM_JDK="oraclejdk8" - -before_install: - - echo 'MAVEN_OPTS="$MAVEN_OPTS -Xmx1024m -XX:MaxPermSize=512m -XX:+BytecodeVerificationLocal"' >> ~/.mavenrc - - cat ~/.mavenrc - - if [ "$TRAVIS_OS_NAME" == "osx" ]; then export JAVA_HOME=$(/usr/libexec/java_home); fi - - if [ "$TRAVIS_OS_NAME" == "linux" ]; then jdk_switcher use "$CUSTOM_JDK"; fi - -script: - - travis_retry mvn -B clean apache-rat:check - - travis_retry mvn -B package jacoco:report coveralls:report - -after_success: - - mvn clean install -Pit-test - - mvn sonar:sonar -Psonar-apache diff --git a/BUILD.bazel b/BUILD.bazel new file mode 100644 index 00000000000..358527c3149 --- /dev/null +++ b/BUILD.bazel @@ -0,0 +1,48 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +load("@bazel_toolchains//rules/exec_properties:exec_properties.bzl", "create_rbe_exec_properties_dict") + +platform( + name = "custom_platform", + # Inherit from the platform target generated by 'rbe_configs_gen' assuming the generated configs + # were imported as a Bazel external repository named 'rbe_default'. If you extracted the + # generated configs elsewhere in your source repository, replace the following with the label + # to the 'platform' target in the generated configs. + parents = ["@rbe_default//config:platform"], + # Example custom execution property instructing RBE to use e2-standard-2 GCE VMs. + exec_properties = create_rbe_exec_properties_dict( + container_image = "ubuntu:latest", + ), +) + +java_library( + name = "test_deps", + visibility = ["//visibility:public"], + exports = [ + "@maven//:junit_junit", + "@maven//:org_assertj_assertj_core", + "@maven//:org_hamcrest_hamcrest_library", + "@maven//:org_mockito_mockito_core", + "@maven//:org_powermock_powermock_module_junit4", + "@maven//:org_powermock_powermock_api_mockito2", + "@maven//:org_hamcrest_hamcrest_core", + "@maven//:ch_qos_logback_logback_classic", + "@maven//:org_awaitility_awaitility", + "@maven//:org_openjdk_jmh_jmh_core", + "@maven//:org_openjdk_jmh_jmh_generator_annprocess", + ], +) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b7b133472c4..f3386e8c9d0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,28 +5,44 @@ We want to have high quality, well documented codes for each programming languag Nor is code the only way to contribute to the project. We strongly value documentation, integration with other project, and gladly accept improvements for these aspects. +Recommend reading: + * [Contributors Tech Guide](http://www.apache.org/dev/contributors) + * [Get involved!](http://www.apache.org/foundation/getinvolved.html) + ## Contributing code To submit a change for inclusion, please do the following: #### If the change is non-trivial please include some unit tests that cover the new functionality. -#### If you are introducing a completely new feature or API it is a good idea to start a wiki and get consensus on the basic design first. +#### If you are introducing a completely new feature or API it is a good idea to start a [RIP](https://github.com/apache/rocketmq/wiki/RocketMQ-Improvement-Proposal) and get consensus on the basic design first. #### It is our job to follow up on patches in a timely fashion. Nag us if we aren't doing our job (sometimes we drop things). +### Squash commits + +If your have a pull request on GitHub, and updated more than once, it's better to squash all commits. + +1. Identify how many commits you made since you began: ``git log``. +2. Squash these commits by N: ``git rebase -i HEAD~N`` . +3. Leave "pick" tag in the first line. +4. Change all other commits from "pick" to "fixup". +5. Then do "force push" to overwrite remote history: ``git push -u origin ROCKETMQ-9999 --force`` +6. All your changes are now in a single commit, that would be much better for review. + +More details of squash can be found at [stackoverflow](https://stackoverflow.com/questions/5189560/squash-my-last-x-commits-together-using-git). + ## Becoming a Committer We are always interested in adding new contributors. What we look for are series of contributions, good taste and ongoing interest in the project. If you are interested in becoming a committer, please let one of the existing committers know and they can help you walk through the process. Nowadays,we have several important contribution points: #### Wiki & JavaDoc -#### RocketMQ Console #### RocketMQ SDK(C++\.Net\Php\Python\Go\Node.js) -#### RocketMQ MySQL(Oracle\PostgreSQL\Redis\MongoDB\HBase\MSSQL) Replicator +#### RocketMQ Connectors ##### Prerequisite If you want to contribute the above listing points, you must abide our some prerequisites: -###### Readability - API must have Javadoc,some very important methods also must have javadoc +###### Readability - API must have Javadoc, some very important methods also must have javadoc ###### Testability - 80% above unit test coverage about main process ###### Maintainability - Comply with our [checkstyle spec](style/rmq_checkstyle.xml), and at least 3 month update frequency ###### Deployability - We encourage you to deploy into [maven repository](http://search.maven.org/) diff --git a/LICENSE b/LICENSE index 7f77f44e739..f49a4e16e68 100644 --- a/LICENSE +++ b/LICENSE @@ -15,7 +15,7 @@ "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, - "control" means (properties) the power, direct or indirect, to cause the + "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. diff --git a/NOTICE b/NOTICE index c218b26089b..85b0032cd80 100644 --- a/NOTICE +++ b/NOTICE @@ -1,5 +1,5 @@ Apache RocketMQ -Copyright 2016-2017 The Apache Software Foundation +Copyright 2016-2024 The Apache Software Foundation This product includes software developed at -The Apache Software Foundation (http://www.apache.org/). \ No newline at end of file +The Apache Software Foundation (http://www.apache.org/). diff --git a/README.md b/README.md index 3b9c54e559e..5aaa2ba73c3 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,250 @@ -## Apache RocketMQ [![Build Status](https://travis-ci.org/apache/rocketmq.svg?branch=master)](https://travis-ci.org/apache/rocketmq) [![Coverage Status](https://coveralls.io/repos/github/apache/rocketmq/badge.svg?branch=master)](https://coveralls.io/github/apache/rocketmq?branch=master) -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.apache.rocketmq/rocketmq-all/badge.svg)](http://search.maven.org/#search%7Cga%7C1%7Corg.apache.rocketmq) -[![GitHub release](https://img.shields.io/badge/release-download-orange.svg)](https://rocketmq.apache.org/dowloading/releases) -[![License](https://img.shields.io/badge/license-Apache%202-4EB1BA.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) +## Apache RocketMQ + +[![Build Status][maven-build-image]][maven-build-url] +[![CodeCov][codecov-image]][codecov-url] +[![Maven Central][maven-central-image]][maven-central-url] +[![Release][release-image]][release-url] +[![License][license-image]][license-url] +[![Average Time to Resolve An Issue][percentage-of-issues-still-open-image]][pencentage-of-issues-still-open-url] +[![Percentage of Issues Still Open][average-time-to-resolve-an-issue-image]][average-time-to-resolve-an-issue-url] +[![Twitter Follow][twitter-follow-image]][twitter-follow-url] **[Apache RocketMQ](https://rocketmq.apache.org) is a distributed messaging and streaming platform with low latency, high performance and reliability, trillion-level capacity and flexible scalability.** + It offers a variety of features: -* Pub/Sub messaging model -* Scheduled message delivery +* Messaging patterns including publish/subscribe, request/reply and streaming +* Financial grade transactional message +* Built-in fault tolerance and high availability configuration options base on [DLedger Controller](docs/en/controller/quick_start.md) +* Built-in message tracing capability, also support opentracing +* Versatile big-data and streaming ecosystem integration * Message retroactivity by time or offset -* Log hub for streaming -* Big data integration * Reliable FIFO and strict ordered messaging in the same queue -* Efficient pull&push consumption model +* Efficient pull and push consumption model * Million-level message accumulation capacity in a single queue -* Multiple messaging protocols like JMS and OpenMessaging +* Multiple messaging protocols like gRPC, MQTT, JMS and OpenMessaging * Flexible distributed scale-out deployment architecture * Lightning-fast batch message exchange system * Various message filter mechanics such as SQL and Tag * Docker images for isolated testing and cloud isolated clusters * Feature-rich administrative dashboard for configuration, metrics and monitoring +* Authentication and authorization +* Free open source connectors, for both sources and sinks +* Lightweight real-time computing +---------- ----------- +## Quick Start + +This paragraph guides you through steps of installing RocketMQ in different ways. +For local development and testing, only one instance will be created for each component. + +### Run RocketMQ locally + +RocketMQ runs on all major operating systems and requires only a Java JDK version 8 or higher to be installed. +To check, run `java -version`: +```shell +$ java -version +java version "1.8.0_121" +``` + +For Windows users, click [here](https://dist.apache.org/repos/dist/release/rocketmq/5.1.4/rocketmq-all-5.1.4-bin-release.zip) to download the 5.1.4 RocketMQ binary release, +unpack it to your local disk, such as `D:\rocketmq`. +For macOS and Linux users, execute following commands: + +```shell +# Download release from the Apache mirror +$ wget https://dist.apache.org/repos/dist/release/rocketmq/5.1.4/rocketmq-all-5.1.4-bin-release.zip + +# Unpack the release +$ unzip rocketmq-all-5.1.4-bin-release.zip +``` + +Prepare a terminal and change to the extracted `bin` directory: +```shell +$ cd rocketmq-all-5.1.4-bin-release/bin +``` + +**1) Start NameServer** + +NameServer will be listening at `0.0.0.0:9876`, make sure that the port is not used by others on the local machine, and then do as follows. + +For macOS and Linux users: +```shell +### start Name Server +$ nohup sh mqnamesrv & + +### check whether Name Server is successfully started +$ tail -f ~/logs/rocketmqlogs/namesrv.log +The Name Server boot success... +``` + +For Windows users, you need set environment variables first: +- From the desktop, right click the Computer icon. +- Choose Properties from the context menu. +- Click the Advanced system settings link. +- Click Environment Variables. +- Add Environment `ROCKETMQ_HOME="D:\rocketmq"`. + +Then change directory to rocketmq, type and run: +```shell +$ mqnamesrv.cmd +The Name Server boot success... +``` + +**2) Start Broker** + +For macOS and Linux users: +```shell +### start Broker +$ nohup sh bin/mqbroker -n localhost:9876 & + +### check whether Broker is successfully started, eg: Broker's IP is 192.168.1.2, Broker's name is broker-a +$ tail -f ~/logs/rocketmqlogs/broker.log +The broker[broker-a, 192.169.1.2:10911] boot success... +``` + +For Windows users: +```shell +$ mqbroker.cmd -n localhost:9876 +The broker[broker-a, 192.169.1.2:10911] boot success... +``` + +### Run RocketMQ in Docker + +You can run RocketMQ on your own machine within Docker containers, +`host` network will be used to expose listening port in the container. + +**1) Start NameServer** + +```shell +$ docker run -it --net=host apache/rocketmq ./mqnamesrv +``` +**2) Start Broker** + +```shell +$ docker run -it --net=host --mount source=/tmp/store,target=/home/rocketmq/store apache/rocketmq ./mqbroker -n localhost:9876 +``` + +### Run RocketMQ in Kubernetes + +You can also run a RocketMQ cluster within a Kubernetes cluster using [RocketMQ Operator](https://github.com/apache/rocketmq-operator). +Before your operations, make sure that `kubectl` and related kubeconfig file installed on your machine. + +**1) Install CRDs** +```shell +### install CRDs +$ git clone https://github.com/apache/rocketmq-operator +$ cd rocketmq-operator && make deploy + +### check whether CRDs is successfully installed +$ kubectl get crd | grep rocketmq.apache.org +brokers.rocketmq.apache.org 2022-05-12T09:23:18Z +consoles.rocketmq.apache.org 2022-05-12T09:23:19Z +nameservices.rocketmq.apache.org 2022-05-12T09:23:18Z +topictransfers.rocketmq.apache.org 2022-05-12T09:23:19Z + +### check whether operator is running +$ kubectl get pods | grep rocketmq-operator +rocketmq-operator-6f65c77c49-8hwmj 1/1 Running 0 93s +``` + +**2) Create Cluster Instance** +```shell +### create RocketMQ cluster resource +$ cd example && kubectl create -f rocketmq_v1alpha1_rocketmq_cluster.yaml + +### check whether cluster resources is running +$ kubectl get sts +NAME READY AGE +broker-0-master 1/1 107m +broker-0-replica-1 1/1 107m +name-service 1/1 107m +``` + +--- +## Apache RocketMQ Community +* [RocketMQ Streams](https://github.com/apache/rocketmq-streams): A lightweight stream computing engine based on Apache RocketMQ. +* [RocketMQ Flink](https://github.com/apache/rocketmq-flink): The Apache RocketMQ connector of Apache Flink that supports source and sink connector in data stream and Table. +* [RocketMQ APIs](https://github.com/apache/rocketmq-apis): RocketMQ protobuf protocol. +* [RocketMQ Clients](https://github.com/apache/rocketmq-clients): gRPC/protobuf-based RocketMQ clients. +* RocketMQ Remoting-based Clients + - [RocketMQ Client CPP](https://github.com/apache/rocketmq-client-cpp) + - [RocketMQ Client Go](https://github.com/apache/rocketmq-client-go) + - [RocketMQ Client Python](https://github.com/apache/rocketmq-client-python) + - [RocketMQ Client Nodejs](https://github.com/apache/rocketmq-client-nodejs) +* [RocketMQ Spring](https://github.com/apache/rocketmq-spring): A project which helps developers quickly integrate Apache RocketMQ with Spring Boot. +* [RocketMQ Exporter](https://github.com/apache/rocketmq-exporter): An Apache RocketMQ exporter for Prometheus. +* [RocketMQ Operator](https://github.com/apache/rocketmq-operator): Providing a way to run an Apache RocketMQ cluster on Kubernetes. +* [RocketMQ Docker](https://github.com/apache/rocketmq-docker): The Git repo of the Docker Image for Apache RocketMQ. +* [RocketMQ Dashboard](https://github.com/apache/rocketmq-dashboard): Operation and maintenance console of Apache RocketMQ. +* [RocketMQ Connect](https://github.com/apache/rocketmq-connect): A tool for scalably and reliably streaming data between Apache RocketMQ and other systems. +* [RocketMQ MQTT](https://github.com/apache/rocketmq-mqtt): A new MQTT protocol architecture model, based on which Apache RocketMQ can better support messages from terminals such as IoT devices and Mobile APP. +* [RocketMQ EventBridge](https://github.com/apache/rocketmq-eventbridge): EventBridge make it easier to build a event-driven application. +* [RocketMQ Incubating Community Projects](https://github.com/apache/rocketmq-externals): Icubator community projects of Apache RocketMQ, including [logappender](https://github.com/apache/rocketmq-externals/tree/master/logappender), [rocketmq-ansible](https://github.com/apache/rocketmq-externals/tree/master/rocketmq-ansible), [rocketmq-beats-integration](https://github.com/apache/rocketmq-externals/tree/master/rocketmq-beats-integration), [rocketmq-cloudevents-binding](https://github.com/apache/rocketmq-externals/tree/master/rocketmq-cloudevents-binding), etc. +* [RocketMQ Site](https://github.com/apache/rocketmq-site): The repository for Apache RocketMQ website. +* [RocketMQ E2E](https://github.com/apache/rocketmq-e2e): A project for testing Apache RocketMQ, including end-to-end, performance, compatibility tests. + + +---------- ## Learn it & Contact us * Mailing Lists: * Home: * Docs: -* Issues: , +* Issues: +* Rips: * Ask: - +* Slack: + ---------- -## Apache RocketMQ Community -* [RocketMQ Community Projects](https://github.com/apache/rocketmq-externals) ----------- ## Contributing -We always welcome new contributions, whether for trivial cleanups, big new features or other material rewards, more details see [here](http://rocketmq.apache.org/docs/how-to-contribute/) - +We always welcome new contributions, whether for trivial cleanups, [big new features](https://github.com/apache/rocketmq/wiki/RocketMQ-Improvement-Proposal) or other material rewards, more details see [here](http://rocketmq.apache.org/docs/how-to-contribute/). + ---------- ## License [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html) Copyright (C) Apache Software Foundation + + +---------- +## Export Control Notice +This distribution includes cryptographic software. The country in which you currently reside may have +restrictions on the import, possession, use, and/or re-export to another country, of encryption software. +BEFORE using any encryption software, please check your country's laws, regulations and policies concerning +the import, possession, or use, and re-export of encryption software, to see if this is permitted. See + for more information. + +The U.S. Government Department of Commerce, Bureau of Industry and Security (BIS), has classified this +software as Export Commodity Control Number (ECCN) 5D002.C.1, which includes information security software +using or performing cryptographic functions with asymmetric algorithms. The form and manner of this Apache +Software Foundation distribution makes it eligible for export under the License Exception ENC Technology +Software Unrestricted (TSU) exception (see the BIS Export Administration Regulations, Section 740.13) for +both object code and source code. + +The following provides more details on the included cryptographic software: + +This software uses Apache Commons Crypto (https://commons.apache.org/proper/commons-crypto/) to +support authentication, and encryption and decryption of data sent across the network between +services. + +[maven-build-image]: https://github.com/apache/rocketmq/actions/workflows/maven.yaml/badge.svg +[maven-build-url]: https://github.com/apache/rocketmq/actions/workflows/maven.yaml +[codecov-image]: https://codecov.io/gh/apache/rocketmq/branch/master/graph/badge.svg +[codecov-url]: https://codecov.io/gh/apache/rocketmq +[maven-central-image]: https://maven-badges.herokuapp.com/maven-central/org.apache.rocketmq/rocketmq-all/badge.svg +[maven-central-url]: http://search.maven.org/#search%7Cga%7C1%7Corg.apache.rocketmq +[release-image]: https://img.shields.io/badge/release-download-orange.svg +[release-url]: https://www.apache.org/licenses/LICENSE-2.0.html +[license-image]: https://img.shields.io/badge/license-Apache%202-4EB1BA.svg +[license-url]: https://www.apache.org/licenses/LICENSE-2.0.html +[average-time-to-resolve-an-issue-image]: http://isitmaintained.com/badge/resolution/apache/rocketmq.svg +[average-time-to-resolve-an-issue-url]: http://isitmaintained.com/project/apache/rocketmq +[percentage-of-issues-still-open-image]: http://isitmaintained.com/badge/open/apache/rocketmq.svg +[pencentage-of-issues-still-open-url]: http://isitmaintained.com/project/apache/rocketmq +[twitter-follow-image]: https://img.shields.io/twitter/follow/ApacheRocketMQ?style=social +[twitter-follow-url]: https://twitter.com/intent/follow?screen_name=ApacheRocketMQ diff --git a/WORKSPACE b/WORKSPACE new file mode 100644 index 00000000000..16f66b73255 --- /dev/null +++ b/WORKSPACE @@ -0,0 +1,140 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +RULES_JVM_EXTERNAL_TAG = "4.2" + +RULES_JVM_EXTERNAL_SHA = "cd1a77b7b02e8e008439ca76fd34f5b07aecb8c752961f9640dea15e9e5ba1ca" + +http_archive( + name = "rules_jvm_external", + sha256 = RULES_JVM_EXTERNAL_SHA, + strip_prefix = "rules_jvm_external-%s" % RULES_JVM_EXTERNAL_TAG, + url = "https://github.com/bazelbuild/rules_jvm_external/archive/%s.zip" % RULES_JVM_EXTERNAL_TAG, +) + +load("@rules_jvm_external//:repositories.bzl", "rules_jvm_external_deps") + +rules_jvm_external_deps() + +load("@rules_jvm_external//:setup.bzl", "rules_jvm_external_setup") + +rules_jvm_external_setup() + +load("@rules_jvm_external//:defs.bzl", "maven_install") + +maven_install( + artifacts = [ + "junit:junit:4.13.2", + "com.alibaba:fastjson:1.2.76", + "org.hamcrest:hamcrest-library:1.3", + "io.netty:netty-all:4.1.65.Final", + "org.assertj:assertj-core:3.22.0", + "org.mockito:mockito-core:3.10.0", + "org.powermock:powermock-module-junit4:2.0.9", + "org.powermock:powermock-api-mockito2:2.0.9", + "org.powermock:powermock-core:2.0.9", + "com.github.luben:zstd-jni:1.5.2-2", + "org.lz4:lz4-java:1.8.0", + "commons-validator:commons-validator:1.7", + "org.apache.commons:commons-lang3:3.12.0", + "org.hamcrest:hamcrest-core:1.3", + "io.openmessaging.storage:dledger:0.3.1", + "net.java.dev.jna:jna:4.2.2", + "ch.qos.logback:logback-classic:1.2.10", + "ch.qos.logback:logback-core:1.2.10", + "io.opentracing:opentracing-api:0.33.0", + "io.opentracing:opentracing-mock:0.33.0", + "commons-collections:commons-collections:3.2.2", + "org.awaitility:awaitility:4.1.0", + "commons-cli:commons-cli:1.5.0", + "com.google.guava:guava:31.0.1-jre", + "org.yaml:snakeyaml:1.30", + "commons-codec:commons-codec:1.13", + "commons-io:commons-io:2.7", + "com.google.truth:truth:0.30", + "org.bouncycastle:bcpkix-jdk15on:1.69", + "com.google.code.gson:gson:2.8.9", + "com.googlecode.concurrentlinkedhashmap:concurrentlinkedhashmap-lru:1.4.2", + "org.apache.rocketmq:rocketmq-proto:2.0.3", + "com.google.protobuf:protobuf-java:3.20.1", + "com.google.protobuf:protobuf-java-util:3.20.1", + "com.conversantmedia:disruptor:1.2.10", + "org.apache.tomcat:annotations-api:6.0.53", + "com.google.code.findbugs:jsr305:3.0.2", + "org.checkerframework:checker-qual:3.12.0", + "org.reflections:reflections:0.9.11", + "org.openjdk.jmh:jmh-core:1.19", + "org.openjdk.jmh:jmh-generator-annprocess:1.19", + "com.github.ben-manes.caffeine:caffeine:2.9.3", + "io.grpc:grpc-services:1.47.0", + "io.grpc:grpc-netty-shaded:1.47.0", + "io.grpc:grpc-context:1.47.0", + "io.grpc:grpc-stub:1.47.0", + "io.grpc:grpc-api:1.47.0", + "io.grpc:grpc-testing:1.47.0", + "org.springframework:spring-core:5.3.26", + "io.opentelemetry:opentelemetry-exporter-otlp:1.29.0", + "io.opentelemetry:opentelemetry-exporter-prometheus:1.29.0-alpha", + "io.opentelemetry:opentelemetry-exporter-logging:1.29.0", + "io.opentelemetry:opentelemetry-sdk:1.29.0", + "io.opentelemetry:opentelemetry-exporter-logging-otlp:1.29.0", + "com.squareup.okio:okio-jvm:3.0.0", + "io.opentelemetry:opentelemetry-api:1.29.0", + "io.opentelemetry:opentelemetry-sdk-metrics:1.29.0", + "io.opentelemetry:opentelemetry-sdk-common:1.29.0", + "io.github.aliyunmq:rocketmq-slf4j-api:1.0.0", + "io.github.aliyunmq:rocketmq-logback-classic:1.0.0", + "org.slf4j:jul-to-slf4j:2.0.6", + "org.jetbrains:annotations:23.1.0", + "io.github.aliyunmq:rocketmq-shaded-slf4j-api-bridge:1.0.0", + "software.amazon.awssdk:s3:2.20.29", + "com.fasterxml.jackson.core:jackson-databind:2.13.4.2", + "com.adobe.testing:s3mock-junit4:2.11.0", + "io.github.aliyunmq:rocketmq-grpc-netty-codec-haproxy:1.0.0", + "org.apache.rocketmq:rocketmq-rocksdb:1.0.2", + "com.alipay.sofa:jraft-core:1.3.14", + "com.alipay.sofa:hessian:3.3.6", + "io.netty:netty-tcnative-boringssl-static:2.0.48.Final", + ], + fetch_sources = True, + repositories = [ + # Private repositories are supported through HTTP Basic auth + "https://repo1.maven.org/maven2", + ], +) + +http_archive( + name = "io_buildbuddy_buildbuddy_toolchain", + sha256 = "b12273608db627eb14051eb75f8a2134590172cd69392086d392e25f3954ea6e", + strip_prefix = "buildbuddy-toolchain-8d5d18373adfca9d8e33b4812915abc9b132f1ee", + urls = ["https://github.com/buildbuddy-io/buildbuddy-toolchain/archive/8d5d18373adfca9d8e33b4812915abc9b132f1ee.tar.gz"], +) +load("@io_buildbuddy_buildbuddy_toolchain//:deps.bzl", "buildbuddy_deps") +buildbuddy_deps() +load("@io_buildbuddy_buildbuddy_toolchain//:rules.bzl", "buildbuddy") +buildbuddy(name = "buildbuddy_toolchain") + +http_archive( + name = "bazel_toolchains", + sha256 = "1adf5db506a7e3c465a26988514cfc3971af6d5b3c2218925cd6e71ee443fc3f", + strip_prefix = "bazel-toolchains-4.0.0", + urls = [ + "https://github.com/bazelbuild/bazel-toolchains/releases/download/4.0.0/bazel-toolchains-4.0.0.tar.gz", + "https://mirror.bazel.build/github.com/bazelbuild/bazel-toolchains/releases/download/4.0.0/bazel-toolchains-4.0.0.tar.gz", + ], +) diff --git a/acl/BUILD.bazel b/acl/BUILD.bazel new file mode 100644 index 00000000000..ac6ac65c779 --- /dev/null +++ b/acl/BUILD.bazel @@ -0,0 +1,75 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +load("//bazel:GenTestRules.bzl", "GenTestRules") + +java_library( + name = "acl", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//common", + "//remoting", + "//srvutil", + "@maven//:com_alibaba_fastjson", + "@maven//:com_github_luben_zstd_jni", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:commons_codec_commons_codec", + "@maven//:commons_validator_commons_validator", + "@maven//:io_netty_netty_all", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:org_apache_rocketmq_rocketmq_proto", + "@maven//:org_lz4_lz4_java", + "@maven//:org_yaml_snakeyaml", + "@maven//:io_github_aliyunmq_rocketmq_slf4j_api", + "@maven//:io_github_aliyunmq_rocketmq_logback_classic", + ], +) + +java_library( + name = "tests", + srcs = glob(["src/test/java/**/*.java"]), + resources = glob(["src/test/resources/**/*.yml"]), + visibility = ["//visibility:public"], + deps = [ + ":acl", + "//:test_deps", + "//common", + "//remoting", + "@maven//:com_alibaba_fastjson", + "@maven//:com_google_guava_guava", + "@maven//:commons_codec_commons_codec", + "@maven//:io_netty_netty_all", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:org_springframework_spring_core", + "@maven//:org_yaml_snakeyaml", + ], +) + +GenTestRules( + name = "GeneratedTestRules", + # The following tests are not hermetic. Fix them later. + exclude_tests = [ + ], + medium_tests = [ + "src/test/java/org/apache/rocketmq/acl/plain/PlainAccessValidatorTest", + ], + test_files = glob(["src/test/java/**/*Test.java"]), + deps = [ + ":tests", + ], +) diff --git a/acl/pom.xml b/acl/pom.xml new file mode 100644 index 00000000000..3da0cb82691 --- /dev/null +++ b/acl/pom.xml @@ -0,0 +1,90 @@ + + + 4.0.0 + + org.apache.rocketmq + rocketmq-all + 5.2.0 + + rocketmq-acl + rocketmq-acl ${project.version} + + + ${basedir}/.. + + + + + ${project.groupId} + rocketmq-proto + + + ${project.groupId} + rocketmq-remoting + + + ${project.groupId} + rocketmq-common + + + ${project.groupId} + rocketmq-srvutil + + + io.github.aliyunmq + rocketmq-slf4j-api + + + io.github.aliyunmq + rocketmq-logback-classic + + + org.yaml + snakeyaml + + + commons-codec + commons-codec + + + org.apache.commons + commons-lang3 + + + commons-validator + commons-validator + + + com.google.protobuf + protobuf-java-util + + + + org.springframework + spring-core + test + + + + + + + maven-surefire-plugin + ${maven-surefire-plugin.version} + + 1 + false + + + + + diff --git a/acl/src/main/java/org/apache/rocketmq/acl/AccessResource.java b/acl/src/main/java/org/apache/rocketmq/acl/AccessResource.java new file mode 100644 index 00000000000..e30febc5719 --- /dev/null +++ b/acl/src/main/java/org/apache/rocketmq/acl/AccessResource.java @@ -0,0 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.acl; + +public interface AccessResource { +} diff --git a/acl/src/main/java/org/apache/rocketmq/acl/AccessValidator.java b/acl/src/main/java/org/apache/rocketmq/acl/AccessValidator.java new file mode 100644 index 00000000000..315184c6150 --- /dev/null +++ b/acl/src/main/java/org/apache/rocketmq/acl/AccessValidator.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.acl; + +import com.google.protobuf.GeneratedMessageV3; +import java.util.List; +import java.util.Map; +import org.apache.rocketmq.acl.common.AuthenticationHeader; +import org.apache.rocketmq.common.AclConfig; +import org.apache.rocketmq.common.PlainAccessConfig; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public interface AccessValidator { + + /** + * Parse to get the AccessResource(user, resource, needed permission) + * + * @param request + * @param remoteAddr + * @return Plain access resource result,include access key,signature and some other access attributes. + */ + AccessResource parse(RemotingCommand request, String remoteAddr); + + /** + * Parse to get the AccessResource from gRPC protocol + * @param messageV3 + * @param header + * @return Plain access resource + */ + AccessResource parse(GeneratedMessageV3 messageV3, AuthenticationHeader header); + + /** + * Validate the access resource. + * + * @param accessResource + */ + void validate(AccessResource accessResource); + + /** + * Update the access resource config + * + * @param plainAccessConfig + * @return + */ + boolean updateAccessConfig(PlainAccessConfig plainAccessConfig); + + /** + * Delete the access resource config + * + * @return + */ + boolean deleteAccessConfig(String accessKey); + + /** + * Get the access resource config version information + * + * @return + */ + @Deprecated + String getAclConfigVersion(); + + /** + * Update globalWhiteRemoteAddresses in acl yaml config file + * + * @return + */ + boolean updateGlobalWhiteAddrsConfig(List globalWhiteAddrsList); + + boolean updateGlobalWhiteAddrsConfig(List globalWhiteAddrsList, String aclFileFullPath); + + /** + * get broker cluster acl config information + * + * @return + */ + AclConfig getAllAclConfig(); + + /** + * get all access resource config version information + * + * @return + */ + Map getAllAclConfigVersion(); +} diff --git a/acl/src/main/java/org/apache/rocketmq/acl/PermissionChecker.java b/acl/src/main/java/org/apache/rocketmq/acl/PermissionChecker.java new file mode 100644 index 00000000000..a38d3ec4787 --- /dev/null +++ b/acl/src/main/java/org/apache/rocketmq/acl/PermissionChecker.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.acl; + +public interface PermissionChecker { + void check(AccessResource checkedAccess, AccessResource ownedAccess); +} diff --git a/acl/src/main/java/org/apache/rocketmq/acl/common/AclClientRPCHook.java b/acl/src/main/java/org/apache/rocketmq/acl/common/AclClientRPCHook.java new file mode 100644 index 00000000000..294db4f069a --- /dev/null +++ b/acl/src/main/java/org/apache/rocketmq/acl/common/AclClientRPCHook.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.acl.common; + +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +import static org.apache.rocketmq.acl.common.SessionCredentials.ACCESS_KEY; +import static org.apache.rocketmq.acl.common.SessionCredentials.SECURITY_TOKEN; +import static org.apache.rocketmq.acl.common.SessionCredentials.SIGNATURE; + +public class AclClientRPCHook implements RPCHook { + private final SessionCredentials sessionCredentials; + + public AclClientRPCHook(SessionCredentials sessionCredentials) { + this.sessionCredentials = sessionCredentials; + } + + @Override + public void doBeforeRequest(String remoteAddr, RemotingCommand request) { + // Add AccessKey and SecurityToken into signature calculating. + request.addExtField(ACCESS_KEY, sessionCredentials.getAccessKey()); + // The SecurityToken value is unnecessary,user can choose this one. + if (sessionCredentials.getSecurityToken() != null) { + request.addExtField(SECURITY_TOKEN, sessionCredentials.getSecurityToken()); + } + byte[] total = AclUtils.combineRequestContent(request, parseRequestContent(request)); + String signature = AclUtils.calSignature(total, sessionCredentials.getSecretKey()); + request.addExtField(SIGNATURE, signature); + } + + @Override + public void doAfterResponse(String remoteAddr, RemotingCommand request, RemotingCommand response) { + + } + + protected SortedMap parseRequestContent(RemotingCommand request) { + request.makeCustomHeaderToNet(); + Map extFields = request.getExtFields(); + // Sort property + return new TreeMap<>(extFields); + } + + public SessionCredentials getSessionCredentials() { + return sessionCredentials; + } +} diff --git a/acl/src/main/java/org/apache/rocketmq/acl/common/AclConstants.java b/acl/src/main/java/org/apache/rocketmq/acl/common/AclConstants.java new file mode 100644 index 00000000000..d129c66d1c8 --- /dev/null +++ b/acl/src/main/java/org/apache/rocketmq/acl/common/AclConstants.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.acl.common; + +public class AclConstants { + + public static final String CONFIG_GLOBAL_WHITE_ADDRS = "globalWhiteRemoteAddresses"; + + public static final String CONFIG_ACCOUNTS = "accounts"; + + public static final String CONFIG_ACCESS_KEY = "accessKey"; + + public static final String CONFIG_SECRET_KEY = "secretKey"; + + public static final String CONFIG_WHITE_ADDR = "whiteRemoteAddress"; + + public static final String CONFIG_ADMIN_ROLE = "admin"; + + public static final String CONFIG_DEFAULT_TOPIC_PERM = "defaultTopicPerm"; + + public static final String CONFIG_DEFAULT_GROUP_PERM = "defaultGroupPerm"; + + public static final String CONFIG_TOPIC_PERMS = "topicPerms"; + + public static final String CONFIG_GROUP_PERMS = "groupPerms"; + + public static final String CONFIG_DATA_VERSION = "dataVersion"; + + public static final String CONFIG_COUNTER = "counter"; + + public static final String CONFIG_TIME_STAMP = "timestamp"; + + public static final String PUB = "PUB"; + + public static final String SUB = "SUB"; + + public static final String DENY = "DENY"; + + public static final String PUB_SUB = "PUB|SUB"; + + public static final String SUB_PUB = "SUB|PUB"; + + public static final int ACCESS_KEY_MIN_LENGTH = 6; + + public static final int SECRET_KEY_MIN_LENGTH = 6; +} diff --git a/acl/src/main/java/org/apache/rocketmq/acl/common/AclException.java b/acl/src/main/java/org/apache/rocketmq/acl/common/AclException.java new file mode 100644 index 00000000000..54579d48a7d --- /dev/null +++ b/acl/src/main/java/org/apache/rocketmq/acl/common/AclException.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.acl.common; + +public class AclException extends RuntimeException { + private static final long serialVersionUID = -7256002576788700354L; + + private String status; + private int code; + + public AclException(String status, int code) { + super(); + this.status = status; + this.code = code; + } + + public AclException(String status, int code, String message) { + super(message); + this.status = status; + this.code = code; + } + + public AclException(String message) { + super(message); + } + + public AclException(String message, Throwable throwable) { + super(message, throwable); + } + + public AclException(String status, int code, String message, Throwable throwable) { + super(message, throwable); + this.status = status; + this.code = code; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } +} diff --git a/acl/src/main/java/org/apache/rocketmq/acl/common/AclSigner.java b/acl/src/main/java/org/apache/rocketmq/acl/common/AclSigner.java new file mode 100644 index 00000000000..b4baa897225 --- /dev/null +++ b/acl/src/main/java/org/apache/rocketmq/acl/common/AclSigner.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.acl.common; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import org.apache.commons.codec.binary.Base64; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class AclSigner { + public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + public static final SigningAlgorithm DEFAULT_ALGORITHM = SigningAlgorithm.HmacSHA1; + private static final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_AUTHORIZE_LOGGER_NAME); + private static final int CAL_SIGNATURE_FAILED = 10015; + private static final String CAL_SIGNATURE_FAILED_MSG = "[%s:signature-failed] unable to calculate a request signature. error=%s"; + + public static String calSignature(String data, String key) throws AclException { + return calSignature(data, key, DEFAULT_ALGORITHM, DEFAULT_CHARSET); + } + + public static String calSignature(String data, String key, SigningAlgorithm algorithm, + Charset charset) throws AclException { + return signAndBase64Encode(data, key, algorithm, charset); + } + + private static String signAndBase64Encode(String data, String key, SigningAlgorithm algorithm, Charset charset) + throws AclException { + try { + byte[] signature = sign(data.getBytes(charset), key.getBytes(charset), algorithm); + return new String(Base64.encodeBase64(signature), DEFAULT_CHARSET); + } catch (Exception e) { + String message = String.format(CAL_SIGNATURE_FAILED_MSG, CAL_SIGNATURE_FAILED, e.getMessage()); + log.error(message, e); + throw new AclException("CAL_SIGNATURE_FAILED", CAL_SIGNATURE_FAILED, message, e); + } + } + + private static byte[] sign(byte[] data, byte[] key, SigningAlgorithm algorithm) throws AclException { + try { + Mac mac = Mac.getInstance(algorithm.toString()); + mac.init(new SecretKeySpec(key, algorithm.toString())); + return mac.doFinal(data); + } catch (Exception e) { + String message = String.format(CAL_SIGNATURE_FAILED_MSG, CAL_SIGNATURE_FAILED, e.getMessage()); + log.error(message, e); + throw new AclException("CAL_SIGNATURE_FAILED", CAL_SIGNATURE_FAILED, message, e); + } + } + + public static String calSignature(byte[] data, String key) throws AclException { + return calSignature(data, key, DEFAULT_ALGORITHM, DEFAULT_CHARSET); + } + + public static String calSignature(byte[] data, String key, SigningAlgorithm algorithm, + Charset charset) throws AclException { + return signAndBase64Encode(data, key, algorithm, charset); + } + + private static String signAndBase64Encode(byte[] data, String key, SigningAlgorithm algorithm, Charset charset) + throws AclException { + try { + byte[] signature = sign(data, key.getBytes(charset), algorithm); + return new String(Base64.encodeBase64(signature), DEFAULT_CHARSET); + } catch (Exception e) { + String message = String.format(CAL_SIGNATURE_FAILED_MSG, CAL_SIGNATURE_FAILED, e.getMessage()); + log.error(message, e); + throw new AclException("CAL_SIGNATURE_FAILED", CAL_SIGNATURE_FAILED, message, e); + } + } + +} diff --git a/acl/src/main/java/org/apache/rocketmq/acl/common/AclUtils.java b/acl/src/main/java/org/apache/rocketmq/acl/common/AclUtils.java new file mode 100644 index 00000000000..65f04f54339 --- /dev/null +++ b/acl/src/main/java/org/apache/rocketmq/acl/common/AclUtils.java @@ -0,0 +1,303 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.acl.common; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.util.Map; +import java.util.SortedMap; + +import com.alibaba.fastjson.JSONObject; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.yaml.snakeyaml.Yaml; + +import static org.apache.rocketmq.acl.common.SessionCredentials.CHARSET; + +public class AclUtils { + + private static final Logger log = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); + + public static byte[] combineRequestContent(RemotingCommand request, SortedMap fieldsMap) { + try { + StringBuilder sb = new StringBuilder(""); + for (Map.Entry entry : fieldsMap.entrySet()) { + if (!SessionCredentials.SIGNATURE.equals(entry.getKey())) { + sb.append(entry.getValue()); + } + } + + return AclUtils.combineBytes(sb.toString().getBytes(CHARSET), request.getBody()); + } catch (Exception e) { + throw new RuntimeException("Incompatible exception.", e); + } + } + + public static byte[] combineBytes(byte[] b1, byte[] b2) { + if (b1 == null || b1.length == 0) return b2; + if (b2 == null || b2.length == 0) return b1; + byte[] total = new byte[b1.length + b2.length]; + System.arraycopy(b1, 0, total, 0, b1.length); + System.arraycopy(b2, 0, total, b1.length, b2.length); + return total; + } + + public static String calSignature(byte[] data, String secretKey) { + String signature = AclSigner.calSignature(data, secretKey); + return signature; + } + + public static void IPv6AddressCheck(String netAddress) { + if (isAsterisk(netAddress) || isMinus(netAddress)) { + int asterisk = netAddress.indexOf("*"); + int minus = netAddress.indexOf("-"); +// '*' must be the end of netAddress if it exists + if (asterisk > -1 && asterisk != netAddress.length() - 1) { + throw new AclException(String.format("NetAddress examine scope Exception netAddress is %s", netAddress)); + } + +// format like "2::ac5:78:1-200:*" or "2::ac5:78:1-200" is legal + if (minus > -1) { + if (asterisk == -1) { + if (minus <= netAddress.lastIndexOf(":")) { + throw new AclException(String.format("NetAddress examine scope Exception netAddress is %s", netAddress)); + } + } else { + if (minus <= netAddress.lastIndexOf(":", netAddress.lastIndexOf(":") - 1)) { + throw new AclException(String.format("NetAddress examine scope Exception netAddress is %s", netAddress)); + } + } + } + } + } + + public static String v6ipProcess(String netAddress) { + int part; + String subAddress; + boolean isAsterisk = isAsterisk(netAddress); + boolean isMinus = isMinus(netAddress); + if (isAsterisk && isMinus) { + part = 6; + int lastColon = netAddress.lastIndexOf(':'); + int secondLastColon = netAddress.substring(0, lastColon).lastIndexOf(':'); + subAddress = netAddress.substring(0, secondLastColon); + } else if (!isAsterisk && !isMinus) { + part = 8; + subAddress = netAddress; + } else { + part = 7; + subAddress = netAddress.substring(0, netAddress.lastIndexOf(':')); + } + return expandIP(subAddress, part); + } + + public static void verify(String netAddress, int index) { + if (!AclUtils.isScope(netAddress, index)) { + throw new AclException(String.format("NetAddress examine scope Exception netAddress is %s", netAddress)); + } + } + + public static String[] getAddresses(String netAddress, String partialAddress) { + String[] parAddStrArray = StringUtils.split(partialAddress.substring(1, partialAddress.length() - 1), ","); + String address = netAddress.substring(0, netAddress.indexOf("{")); + String[] addressStrArray = new String[parAddStrArray.length]; + for (int i = 0; i < parAddStrArray.length; i++) { + addressStrArray[i] = address + parAddStrArray[i]; + } + return addressStrArray; + } + + public static boolean isScope(String netAddress, int index) { +// IPv6 Address + if (isColon(netAddress)) { + netAddress = expandIP(netAddress, 8); + String[] strArray = StringUtils.split(netAddress, ":"); + return isIPv6Scope(strArray, index); + } + + String[] strArray = StringUtils.split(netAddress, "."); + if (strArray.length != 4) { + return false; + } + return isScope(strArray, index); + + } + + public static boolean isScope(String[] num, int index) { + for (int i = 0; i < index; i++) { + if (!isScope(num[i])) { + return false; + } + } + return true; + } + + public static boolean isColon(String netAddress) { + return netAddress.indexOf(':') > -1; + } + + public static boolean isScope(String num) { + return isScope(Integer.parseInt(num.trim())); + } + + public static boolean isScope(int num) { + return num >= 0 && num <= 255; + } + + public static boolean isAsterisk(String asterisk) { + return asterisk.indexOf('*') > -1; + } + + public static boolean isComma(String colon) { + return colon.indexOf(',') > -1; + } + + public static boolean isMinus(String minus) { + return minus.indexOf('-') > -1; + + } + + public static boolean isIPv6Scope(String[] num, int index) { + for (int i = 0; i < index; i++) { + int value; + try { + value = Integer.parseInt(num[i], 16); + } catch (NumberFormatException e) { + return false; + } + if (!isIPv6Scope(value)) { + return false; + } + } + return true; + } + + public static boolean isIPv6Scope(int num) { + int min = Integer.parseInt("0", 16); + int max = Integer.parseInt("ffff", 16); + return num >= min && num <= max; + } + + public static String expandIP(String netAddress, int part) { + netAddress = netAddress.toUpperCase(); + // expand netAddress + int separatorCount = StringUtils.countMatches(netAddress, ":"); + int padCount = part - separatorCount; + if (padCount > 0) { + StringBuilder padStr = new StringBuilder(":"); + for (int i = 0; i < padCount; i++) { + padStr.append(":"); + } + netAddress = StringUtils.replace(netAddress, "::", padStr.toString()); + } + + // pad netAddress + String[] strArray = StringUtils.splitPreserveAllTokens(netAddress, ":"); + for (int i = 0; i < strArray.length; i++) { + if (strArray[i].length() < 4) { + strArray[i] = StringUtils.leftPad(strArray[i], 4, '0'); + } + } + + // output + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < strArray.length; i++) { + sb.append(strArray[i]); + if (i != strArray.length - 1) { + sb.append(":"); + } + } + return sb.toString(); + } + + public static T getYamlDataObject(String path, Class clazz) { + try (FileInputStream fis = new FileInputStream(path)) { + return getYamlDataObject(fis, clazz); + } catch (FileNotFoundException ignore) { + return null; + } catch (Exception e) { + throw new AclException(e.getMessage(), e); + } + } + + public static T getYamlDataObject(InputStream fis, Class clazz) { + Yaml yaml = new Yaml(); + try { + return yaml.loadAs(fis, clazz); + } catch (Exception e) { + throw new AclException(e.getMessage(), e); + } + } + + public static boolean writeDataObject(String path, Object dataMap) { + Yaml yaml = new Yaml(); + try (PrintWriter pw = new PrintWriter(path, "UTF-8")) { + String dumpAsMap = yaml.dumpAsMap(dataMap); + pw.print(dumpAsMap); + pw.flush(); + } catch (Exception e) { + throw new AclException(e.getMessage(), e); + } + return true; + } + + public static RPCHook getAclRPCHook(String fileName) { + JSONObject yamlDataObject; + try { + yamlDataObject = AclUtils.getYamlDataObject(fileName, + JSONObject.class); + } catch (Exception e) { + log.error("Convert yaml file to data object error, ", e); + return null; + } + return buildRpcHook(yamlDataObject); + } + + public static RPCHook getAclRPCHook(InputStream inputStream) { + JSONObject yamlDataObject = null; + try { + yamlDataObject = AclUtils.getYamlDataObject(inputStream, JSONObject.class); + } catch (Exception e) { + log.error("Convert yaml file to data object error, ", e); + return null; + } + return buildRpcHook(yamlDataObject); + } + + private static RPCHook buildRpcHook(JSONObject yamlDataObject) { + if (yamlDataObject == null || yamlDataObject.isEmpty()) { + log.warn("Failed to parse configuration to enable ACL."); + return null; + } + + String accessKey = yamlDataObject.getString(AclConstants.CONFIG_ACCESS_KEY); + String secretKey = yamlDataObject.getString(AclConstants.CONFIG_SECRET_KEY); + + if (StringUtils.isBlank(accessKey) || StringUtils.isBlank(secretKey)) { + log.warn("Failed to enable ACL. Either AccessKey or secretKey is blank"); + return null; + } + return new AclClientRPCHook(new SessionCredentials(accessKey, secretKey)); + } + +} diff --git a/acl/src/main/java/org/apache/rocketmq/acl/common/AuthenticationHeader.java b/acl/src/main/java/org/apache/rocketmq/acl/common/AuthenticationHeader.java new file mode 100644 index 00000000000..5b00c00c787 --- /dev/null +++ b/acl/src/main/java/org/apache/rocketmq/acl/common/AuthenticationHeader.java @@ -0,0 +1,237 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.acl.common; + +import com.google.common.base.MoreObjects; + +public class AuthenticationHeader { + private String remoteAddress; + private String tenantId; + private String namespace; + private String authorization; + private String datetime; + private String sessionToken; + private String requestId; + private String language; + private String clientVersion; + private String protocol; + private int requestCode; + + AuthenticationHeader(final String remoteAddress, final String tenantId, final String namespace, + final String authorization, final String datetime, final String sessionToken, final String requestId, + final String language, final String clientVersion, final String protocol, final int requestCode) { + this.remoteAddress = remoteAddress; + this.tenantId = tenantId; + this.namespace = namespace; + this.authorization = authorization; + this.datetime = datetime; + this.sessionToken = sessionToken; + this.requestId = requestId; + this.language = language; + this.clientVersion = clientVersion; + this.protocol = protocol; + this.requestCode = requestCode; + } + + public static class MetadataHeaderBuilder { + private String remoteAddress; + private String tenantId; + private String namespace; + private String authorization; + private String datetime; + private String sessionToken; + private String requestId; + private String language; + private String clientVersion; + private String protocol; + private int requestCode; + + MetadataHeaderBuilder() { + } + + public AuthenticationHeader.MetadataHeaderBuilder remoteAddress(final String remoteAddress) { + this.remoteAddress = remoteAddress; + return this; + } + + public AuthenticationHeader.MetadataHeaderBuilder tenantId(final String tenantId) { + this.tenantId = tenantId; + return this; + } + + public AuthenticationHeader.MetadataHeaderBuilder namespace(final String namespace) { + this.namespace = namespace; + return this; + } + + public AuthenticationHeader.MetadataHeaderBuilder authorization(final String authorization) { + this.authorization = authorization; + return this; + } + + public AuthenticationHeader.MetadataHeaderBuilder datetime(final String datetime) { + this.datetime = datetime; + return this; + } + + public AuthenticationHeader.MetadataHeaderBuilder sessionToken(final String sessionToken) { + this.sessionToken = sessionToken; + return this; + } + + public AuthenticationHeader.MetadataHeaderBuilder requestId(final String requestId) { + this.requestId = requestId; + return this; + } + + public AuthenticationHeader.MetadataHeaderBuilder language(final String language) { + this.language = language; + return this; + } + + public AuthenticationHeader.MetadataHeaderBuilder clientVersion(final String clientVersion) { + this.clientVersion = clientVersion; + return this; + } + + public AuthenticationHeader.MetadataHeaderBuilder protocol(final String protocol) { + this.protocol = protocol; + return this; + } + + public AuthenticationHeader.MetadataHeaderBuilder requestCode(final int requestCode) { + this.requestCode = requestCode; + return this; + } + + public AuthenticationHeader build() { + return new AuthenticationHeader(this.remoteAddress, this.tenantId, this.namespace, this.authorization, + this.datetime, this.sessionToken, this.requestId, this.language, this.clientVersion, this.protocol, + this.requestCode); + } + } + + public static AuthenticationHeader.MetadataHeaderBuilder builder() { + return new AuthenticationHeader.MetadataHeaderBuilder(); + } + + public String getRemoteAddress() { + return this.remoteAddress; + } + + public String getTenantId() { + return this.tenantId; + } + + public String getNamespace() { + return this.namespace; + } + + public String getAuthorization() { + return this.authorization; + } + + public String getDatetime() { + return this.datetime; + } + + public String getSessionToken() { + return this.sessionToken; + } + + public String getRequestId() { + return this.requestId; + } + + public String getLanguage() { + return this.language; + } + + public String getClientVersion() { + return this.clientVersion; + } + + public String getProtocol() { + return this.protocol; + } + + public int getRequestCode() { + return this.requestCode; + } + + public void setRemoteAddress(final String remoteAddress) { + this.remoteAddress = remoteAddress; + } + + public void setTenantId(final String tenantId) { + this.tenantId = tenantId; + } + + public void setNamespace(final String namespace) { + this.namespace = namespace; + } + + public void setAuthorization(final String authorization) { + this.authorization = authorization; + } + + public void setDatetime(final String datetime) { + this.datetime = datetime; + } + + public void setSessionToken(final String sessionToken) { + this.sessionToken = sessionToken; + } + + public void setRequestId(final String requestId) { + this.requestId = requestId; + } + + public void setLanguage(final String language) { + this.language = language; + } + + public void setClientVersion(final String clientVersion) { + this.clientVersion = clientVersion; + } + + public void setProtocol(final String protocol) { + this.protocol = protocol; + } + + public void setRequestCode(int requestCode) { + this.requestCode = requestCode; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("remoteAddress", remoteAddress) + .add("tenantId", tenantId) + .add("namespace", namespace) + .add("authorization", authorization) + .add("datetime", datetime) + .add("sessionToken", sessionToken) + .add("requestId", requestId) + .add("language", language) + .add("clientVersion", clientVersion) + .add("protocol", protocol) + .add("requestCode", requestCode) + .toString(); + } +} diff --git a/acl/src/main/java/org/apache/rocketmq/acl/common/AuthorizationHeader.java b/acl/src/main/java/org/apache/rocketmq/acl/common/AuthorizationHeader.java new file mode 100644 index 00000000000..eb75aa3bee9 --- /dev/null +++ b/acl/src/main/java/org/apache/rocketmq/acl/common/AuthorizationHeader.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.acl.common; + +import com.google.common.base.MoreObjects; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.binary.Hex; + +public class AuthorizationHeader { + private static final String HEADER_SEPARATOR = " "; + private static final String CREDENTIALS_SEPARATOR = "/"; + private static final int AUTH_HEADER_KV_LENGTH = 2; + private static final String CREDENTIAL = "Credential"; + private static final String SIGNED_HEADERS = "SignedHeaders"; + private static final String SIGNATURE = "Signature"; + private String method; + private String accessKey; + private String[] signedHeaders; + private String signature; + + /** + * Parse authorization from gRPC header. + * + * @param header gRPC header string. + * @throws Exception exception. + */ + public AuthorizationHeader(String header) throws DecoderException { + String[] result = header.split(HEADER_SEPARATOR, 2); + if (result.length != 2) { + throw new DecoderException("authorization header is incorrect"); + } + this.method = result[0]; + String[] keyValues = result[1].split(","); + for (String keyValue : keyValues) { + String[] kv = keyValue.trim().split("=", 2); + int kvLength = kv.length; + if (kv.length != AUTH_HEADER_KV_LENGTH) { + throw new DecoderException("authorization keyValues length is incorrect, actual length=" + kvLength); + } + String authItem = kv[0]; + if (CREDENTIAL.equals(authItem)) { + String[] credential = kv[1].split(CREDENTIALS_SEPARATOR); + int credentialActualLength = credential.length; + if (credentialActualLength == 0) { + throw new DecoderException("authorization credential length is incorrect, actual length=" + credentialActualLength); + } + this.accessKey = credential[0]; + continue; + } + if (SIGNED_HEADERS.equals(authItem)) { + this.signedHeaders = kv[1].split(";"); + continue; + } + if (SIGNATURE.equals(authItem)) { + this.signature = this.hexToBase64(kv[1]); + } + } + } + + public String hexToBase64(String input) throws DecoderException { + byte[] bytes = Hex.decodeHex(input); + return Base64.encodeBase64String(bytes); + } + + public String getMethod() { + return this.method; + } + + public String getAccessKey() { + return this.accessKey; + } + + public String[] getSignedHeaders() { + return this.signedHeaders; + } + + public String getSignature() { + return this.signature; + } + + public void setMethod(final String method) { + this.method = method; + } + + public void setAccessKey(final String accessKey) { + this.accessKey = accessKey; + } + + public void setSignedHeaders(final String[] signedHeaders) { + this.signedHeaders = signedHeaders; + } + + public void setSignature(final String signature) { + this.signature = signature; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("method", method) + .add("accessKey", accessKey) + .add("signedHeaders", signedHeaders) + .add("signature", signature) + .toString(); + } +} diff --git a/acl/src/main/java/org/apache/rocketmq/acl/common/Permission.java b/acl/src/main/java/org/apache/rocketmq/acl/common/Permission.java new file mode 100644 index 00000000000..38649b08327 --- /dev/null +++ b/acl/src/main/java/org/apache/rocketmq/acl/common/Permission.java @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.acl.common; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.acl.plain.PlainAccessResource; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +public class Permission { + + public static final byte DENY = 1; + public static final byte ANY = 1 << 1; + public static final byte PUB = 1 << 2; + public static final byte SUB = 1 << 3; + + public static final Set ADMIN_CODE = new HashSet<>(); + + static { + // UPDATE_AND_CREATE_TOPIC + ADMIN_CODE.add(RequestCode.UPDATE_AND_CREATE_TOPIC); + // UPDATE_BROKER_CONFIG + ADMIN_CODE.add(RequestCode.UPDATE_BROKER_CONFIG); + // DELETE_TOPIC_IN_BROKER + ADMIN_CODE.add(RequestCode.DELETE_TOPIC_IN_BROKER); + // UPDATE_AND_CREATE_SUBSCRIPTIONGROUP + ADMIN_CODE.add(RequestCode.UPDATE_AND_CREATE_SUBSCRIPTIONGROUP); + // DELETE_SUBSCRIPTIONGROUP + ADMIN_CODE.add(RequestCode.DELETE_SUBSCRIPTIONGROUP); + } + + public static boolean checkPermission(byte neededPerm, byte ownedPerm) { + if ((ownedPerm & DENY) > 0) { + return false; + } + if ((neededPerm & ANY) > 0) { + return (ownedPerm & PUB) > 0 || (ownedPerm & SUB) > 0; + } + return (neededPerm & ownedPerm) > 0; + } + + public static byte parsePermFromString(String permString) { + if (permString == null) { + return Permission.DENY; + } + switch (permString.trim()) { + case AclConstants.PUB: + return Permission.PUB; + case AclConstants.SUB: + return Permission.SUB; + case AclConstants.PUB_SUB: + case AclConstants.SUB_PUB: + return Permission.PUB | Permission.SUB; + case AclConstants.DENY: + return Permission.DENY; + default: + return Permission.DENY; + } + } + + public static void parseResourcePerms(PlainAccessResource plainAccessResource, Boolean isTopic, + List resources) { + if (resources == null || resources.isEmpty()) { + return; + } + for (String resource : resources) { + String[] items = StringUtils.split(resource, "="); + if (items.length == 2) { + plainAccessResource.addResourceAndPerm(isTopic ? items[0].trim() : PlainAccessResource.getRetryTopic(items[0].trim()), parsePermFromString(items[1].trim())); + } else { + throw new AclException(String.format("Parse resource permission failed for %s:%s", isTopic ? "topic" : "group", resource)); + } + } + } + + public static void checkResourcePerms(List resources) { + if (resources == null || resources.isEmpty()) { + return; + } + + for (String resource : resources) { + String[] items = StringUtils.split(resource, "="); + if (items.length != 2) { + throw new AclException(String.format("Parse Resource format error for %s.\n" + + "The expected resource format is 'Res=Perm'. For example: topicA=SUB", resource)); + } + + if (!AclConstants.DENY.equals(items[1].trim()) && Permission.DENY == Permission.parsePermFromString(items[1].trim())) { + throw new AclException(String.format("Parse resource permission error for %s.\n" + + "The expected permissions are 'SUB' or 'PUB' or 'SUB|PUB' or 'PUB|SUB'.", resource)); + } + } + } + + public static boolean needAdminPerm(Integer code) { + return ADMIN_CODE.contains(code); + } +} diff --git a/acl/src/main/java/org/apache/rocketmq/acl/common/SessionCredentials.java b/acl/src/main/java/org/apache/rocketmq/acl/common/SessionCredentials.java new file mode 100644 index 00000000000..dfc06d4f3a4 --- /dev/null +++ b/acl/src/main/java/org/apache/rocketmq/acl/common/SessionCredentials.java @@ -0,0 +1,160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.acl.common; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Properties; +import org.apache.rocketmq.common.MixAll; + +public class SessionCredentials { + public static final Charset CHARSET = StandardCharsets.UTF_8; + public static final String ACCESS_KEY = "AccessKey"; + public static final String SECRET_KEY = "SecretKey"; + public static final String SIGNATURE = "Signature"; + public static final String SECURITY_TOKEN = "SecurityToken"; + + public static final String KEY_FILE = System.getProperty("rocketmq.client.keyFile", + System.getProperty("user.home") + File.separator + "key"); + + private String accessKey; + private String secretKey; + private String securityToken; + private String signature; + + public SessionCredentials() { + String keyContent = null; + try { + keyContent = MixAll.file2String(KEY_FILE); + } catch (IOException ignore) { + } + if (keyContent != null) { + Properties prop = MixAll.string2Properties(keyContent); + if (prop != null) { + this.updateContent(prop); + } + } + } + + public SessionCredentials(String accessKey, String secretKey) { + this.accessKey = accessKey; + this.secretKey = secretKey; + } + + public SessionCredentials(String accessKey, String secretKey, String securityToken) { + this(accessKey, secretKey); + this.securityToken = securityToken; + } + + public void updateContent(Properties prop) { + { + String value = prop.getProperty(ACCESS_KEY); + if (value != null) { + this.accessKey = value.trim(); + } + } + { + String value = prop.getProperty(SECRET_KEY); + if (value != null) { + this.secretKey = value.trim(); + } + } + { + String value = prop.getProperty(SECURITY_TOKEN); + if (value != null) { + this.securityToken = value.trim(); + } + } + } + + public String getAccessKey() { + return accessKey; + } + + public void setAccessKey(String accessKey) { + this.accessKey = accessKey; + } + + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + + public String getSignature() { + return signature; + } + + public void setSignature(String signature) { + this.signature = signature; + } + + public String getSecurityToken() { + return securityToken; + } + + public void setSecurityToken(final String securityToken) { + this.securityToken = securityToken; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((accessKey == null) ? 0 : accessKey.hashCode()); + result = prime * result + ((secretKey == null) ? 0 : secretKey.hashCode()); + result = prime * result + ((signature == null) ? 0 : signature.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + + SessionCredentials other = (SessionCredentials) obj; + if (accessKey == null) { + if (other.accessKey != null) + return false; + } else if (!accessKey.equals(other.accessKey)) + return false; + + if (secretKey == null) { + if (other.secretKey != null) + return false; + } else if (!secretKey.equals(other.secretKey)) + return false; + + if (signature == null) { + return other.signature == null; + } else return signature.equals(other.signature); + } + + @Override + public String toString() { + return "SessionCredentials [accessKey=" + accessKey + ", secretKey=" + secretKey + ", signature=" + + signature + ", SecurityToken=" + securityToken + "]"; + } +} \ No newline at end of file diff --git a/acl/src/main/java/org/apache/rocketmq/acl/common/SigningAlgorithm.java b/acl/src/main/java/org/apache/rocketmq/acl/common/SigningAlgorithm.java new file mode 100644 index 00000000000..bfed7b2f233 --- /dev/null +++ b/acl/src/main/java/org/apache/rocketmq/acl/common/SigningAlgorithm.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.acl.common; + +public enum SigningAlgorithm { + HmacSHA1, + HmacSHA256, + HmacMD5; + +} diff --git a/acl/src/main/java/org/apache/rocketmq/acl/plain/PlainAccessData.java b/acl/src/main/java/org/apache/rocketmq/acl/plain/PlainAccessData.java new file mode 100644 index 00000000000..83c8cc40c49 --- /dev/null +++ b/acl/src/main/java/org/apache/rocketmq/acl/plain/PlainAccessData.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.acl.plain; + +import org.apache.rocketmq.common.PlainAccessConfig; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class PlainAccessData implements Serializable { + private static final long serialVersionUID = -7971775135605117152L; + + private List globalWhiteRemoteAddresses = new ArrayList<>(); + private List accounts = new ArrayList<>(); + private List dataVersion = new ArrayList<>(); + + public List getGlobalWhiteRemoteAddresses() { + return globalWhiteRemoteAddresses; + } + + public void setGlobalWhiteRemoteAddresses(List globalWhiteRemoteAddresses) { + this.globalWhiteRemoteAddresses = globalWhiteRemoteAddresses; + } + + public List getAccounts() { + return accounts; + } + + public void setAccounts(List accounts) { + this.accounts = accounts; + } + + public List getDataVersion() { + return dataVersion; + } + + public void setDataVersion(List dataVersion) { + this.dataVersion = dataVersion; + } + + public static class DataVersion implements Serializable { + private static final long serialVersionUID = 6437361970079056954L; + private long timestamp; + private long counter; + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public long getCounter() { + return counter; + } + + public void setCounter(long counter) { + this.counter = counter; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DataVersion that = (DataVersion) o; + return timestamp == that.timestamp && counter == that.counter; + } + + @Override + public int hashCode() { + return Objects.hash(timestamp, counter); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PlainAccessData that = (PlainAccessData) o; + return Objects.equals(globalWhiteRemoteAddresses, that.globalWhiteRemoteAddresses) && Objects.equals(accounts, that.accounts) && Objects.equals(dataVersion, that.dataVersion); + } + + @Override + public int hashCode() { + return Objects.hash(globalWhiteRemoteAddresses, accounts, dataVersion); + } +} diff --git a/acl/src/main/java/org/apache/rocketmq/acl/plain/PlainAccessResource.java b/acl/src/main/java/org/apache/rocketmq/acl/plain/PlainAccessResource.java new file mode 100644 index 00000000000..1e185afff6a --- /dev/null +++ b/acl/src/main/java/org/apache/rocketmq/acl/plain/PlainAccessResource.java @@ -0,0 +1,469 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.acl.plain; + +import apache.rocketmq.v2.AckMessageRequest; +import apache.rocketmq.v2.ChangeInvisibleDurationRequest; +import apache.rocketmq.v2.ClientType; +import apache.rocketmq.v2.EndTransactionRequest; +import apache.rocketmq.v2.ForwardMessageToDeadLetterQueueRequest; +import apache.rocketmq.v2.HeartbeatRequest; +import apache.rocketmq.v2.Message; +import apache.rocketmq.v2.NotifyClientTerminationRequest; +import apache.rocketmq.v2.QueryAssignmentRequest; +import apache.rocketmq.v2.QueryRouteRequest; +import apache.rocketmq.v2.ReceiveMessageRequest; +import apache.rocketmq.v2.Resource; +import apache.rocketmq.v2.SendMessageRequest; +import apache.rocketmq.v2.Subscription; +import apache.rocketmq.v2.SubscriptionEntry; +import apache.rocketmq.v2.TelemetryCommand; +import com.google.protobuf.GeneratedMessageV3; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.rocketmq.acl.AccessResource; +import org.apache.rocketmq.acl.common.AclException; +import org.apache.rocketmq.acl.common.AclUtils; +import org.apache.rocketmq.acl.common.AuthenticationHeader; +import org.apache.rocketmq.acl.common.AuthorizationHeader; +import org.apache.rocketmq.acl.common.Permission; +import org.apache.rocketmq.acl.common.SessionCredentials; +import org.apache.rocketmq.common.KeyBuilder; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.PlainAccessConfig; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerListByGroupRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UnregisterClientRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateConsumerOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumerData; +import org.apache.rocketmq.remoting.protocol.heartbeat.HeartbeatData; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; + +public class PlainAccessResource implements AccessResource { + + // Identify the user + private String accessKey; + + private String secretKey; + + private String whiteRemoteAddress; + + private boolean admin; + + private byte defaultTopicPerm = 1; + + private byte defaultGroupPerm = 1; + + private Map resourcePermMap; + + private RemoteAddressStrategy remoteAddressStrategy; + + private int requestCode; + + // The content to calculate the content + private byte[] content; + + private String signature; + + private String secretToken; + + private String recognition; + + public PlainAccessResource() { + } + + public static PlainAccessResource parse(RemotingCommand request, String remoteAddr) { + PlainAccessResource accessResource = new PlainAccessResource(); + if (remoteAddr != null && remoteAddr.contains(":")) { + accessResource.setWhiteRemoteAddress(remoteAddr.substring(0, remoteAddr.lastIndexOf(':'))); + } else { + accessResource.setWhiteRemoteAddress(remoteAddr); + } + + accessResource.setRequestCode(request.getCode()); + + if (request.getExtFields() == null) { + // If request's extFields is null,then return accessResource directly(users can use whiteAddress pattern) + // The following logic codes depend on the request's extFields not to be null. + return accessResource; + } + accessResource.setAccessKey(request.getExtFields().get(SessionCredentials.ACCESS_KEY)); + accessResource.setSignature(request.getExtFields().get(SessionCredentials.SIGNATURE)); + accessResource.setSecretToken(request.getExtFields().get(SessionCredentials.SECURITY_TOKEN)); + + try { + switch (request.getCode()) { + case RequestCode.SEND_MESSAGE: + final String topic = request.getExtFields().get("topic"); + if (PlainAccessResource.isRetryTopic(topic)) { + accessResource.addResourceAndPerm(getRetryTopic(request.getExtFields().get("group")), Permission.SUB); + } else { + accessResource.addResourceAndPerm(topic, Permission.PUB); + } + break; + case RequestCode.SEND_MESSAGE_V2: + case RequestCode.SEND_BATCH_MESSAGE: + final String topicV2 = request.getExtFields().get("b"); + if (PlainAccessResource.isRetryTopic(topicV2)) { + accessResource.addResourceAndPerm(getRetryTopic(request.getExtFields().get("a")), Permission.SUB); + } else { + accessResource.addResourceAndPerm(topicV2, Permission.PUB); + } + break; + case RequestCode.CONSUMER_SEND_MSG_BACK: + accessResource.addResourceAndPerm(getRetryTopic(request.getExtFields().get("group")), Permission.SUB); + break; + case RequestCode.PULL_MESSAGE: + accessResource.addResourceAndPerm(request.getExtFields().get("topic"), Permission.SUB); + accessResource.addResourceAndPerm(getRetryTopic(request.getExtFields().get("consumerGroup")), Permission.SUB); + break; + case RequestCode.QUERY_MESSAGE: + accessResource.addResourceAndPerm(request.getExtFields().get("topic"), Permission.SUB); + break; + case RequestCode.HEART_BEAT: + HeartbeatData heartbeatData = HeartbeatData.decode(request.getBody(), HeartbeatData.class); + for (ConsumerData data : heartbeatData.getConsumerDataSet()) { + accessResource.addResourceAndPerm(getRetryTopic(data.getGroupName()), Permission.SUB); + for (SubscriptionData subscriptionData : data.getSubscriptionDataSet()) { + accessResource.addResourceAndPerm(subscriptionData.getTopic(), Permission.SUB); + } + } + break; + case RequestCode.UNREGISTER_CLIENT: + final UnregisterClientRequestHeader unregisterClientRequestHeader = + (UnregisterClientRequestHeader) request + .decodeCommandCustomHeader(UnregisterClientRequestHeader.class); + accessResource.addResourceAndPerm(getRetryTopic(unregisterClientRequestHeader.getConsumerGroup()), Permission.SUB); + break; + case RequestCode.GET_CONSUMER_LIST_BY_GROUP: + final GetConsumerListByGroupRequestHeader getConsumerListByGroupRequestHeader = + (GetConsumerListByGroupRequestHeader) request + .decodeCommandCustomHeader(GetConsumerListByGroupRequestHeader.class); + accessResource.addResourceAndPerm(getRetryTopic(getConsumerListByGroupRequestHeader.getConsumerGroup()), Permission.SUB); + break; + case RequestCode.UPDATE_CONSUMER_OFFSET: + final UpdateConsumerOffsetRequestHeader updateConsumerOffsetRequestHeader = + (UpdateConsumerOffsetRequestHeader) request + .decodeCommandCustomHeader(UpdateConsumerOffsetRequestHeader.class); + accessResource.addResourceAndPerm(getRetryTopic(updateConsumerOffsetRequestHeader.getConsumerGroup()), Permission.SUB); + accessResource.addResourceAndPerm(updateConsumerOffsetRequestHeader.getTopic(), Permission.SUB); + break; + default: + break; + + } + } catch (Throwable t) { + throw new AclException(t.getMessage(), t); + } + + // Content + SortedMap map = new TreeMap<>(); + for (Map.Entry entry : request.getExtFields().entrySet()) { + if (request.getVersion() <= MQVersion.Version.V4_9_3.ordinal() && + MixAll.UNIQUE_MSG_QUERY_FLAG.equals(entry.getKey())) { + continue; + } + if (!SessionCredentials.SIGNATURE.equals(entry.getKey())) { + map.put(entry.getKey(), entry.getValue()); + } + } + accessResource.setContent(AclUtils.combineRequestContent(request, map)); + return accessResource; + } + + public static PlainAccessResource parse(GeneratedMessageV3 messageV3, AuthenticationHeader header) { + PlainAccessResource accessResource = new PlainAccessResource(); + String remoteAddress = header.getRemoteAddress(); + if (remoteAddress != null && remoteAddress.contains(":")) { + accessResource.setWhiteRemoteAddress(RemotingHelper.parseHostFromAddress(remoteAddress)); + } else { + accessResource.setWhiteRemoteAddress(remoteAddress); + } + try { + AuthorizationHeader authorizationHeader = new AuthorizationHeader(header.getAuthorization()); + accessResource.setAccessKey(authorizationHeader.getAccessKey()); + accessResource.setSignature(authorizationHeader.getSignature()); + } catch (DecoderException e) { + throw new AclException(e.getMessage(), e); + } + accessResource.setSecretToken(header.getSessionToken()); + accessResource.setRequestCode(header.getRequestCode()); + accessResource.setContent(header.getDatetime().getBytes(StandardCharsets.UTF_8)); + + try { + String rpcFullName = messageV3.getDescriptorForType().getFullName(); + if (HeartbeatRequest.getDescriptor().getFullName().equals(rpcFullName)) { + HeartbeatRequest request = (HeartbeatRequest) messageV3; + if (ClientType.PUSH_CONSUMER.equals(request.getClientType()) + || ClientType.SIMPLE_CONSUMER.equals(request.getClientType())) { + if (!request.hasGroup()) { + throw new AclException("Consumer heartbeat doesn't have group"); + } else { + accessResource.addGroupResourceAndPerm(request.getGroup(), Permission.SUB); + } + } + } else if (SendMessageRequest.getDescriptor().getFullName().equals(rpcFullName)) { + SendMessageRequest request = (SendMessageRequest) messageV3; + if (request.getMessagesCount() <= 0) { + throw new AclException("SendMessageRequest, messageCount is zero", ResponseCode.MESSAGE_ILLEGAL); + } + Resource topic = request.getMessages(0).getTopic(); + for (Message message : request.getMessagesList()) { + if (!message.getTopic().equals(topic)) { + throw new AclException("SendMessageRequest, messages' topic is not consistent", ResponseCode.MESSAGE_ILLEGAL); + } + } + accessResource.addResourceAndPerm(topic, Permission.PUB); + } else if (ReceiveMessageRequest.getDescriptor().getFullName().equals(rpcFullName)) { + ReceiveMessageRequest request = (ReceiveMessageRequest) messageV3; + accessResource.addGroupResourceAndPerm(request.getGroup(), Permission.SUB); + accessResource.addResourceAndPerm(request.getMessageQueue().getTopic(), Permission.SUB); + } else if (AckMessageRequest.getDescriptor().getFullName().equals(rpcFullName)) { + AckMessageRequest request = (AckMessageRequest) messageV3; + accessResource.addGroupResourceAndPerm(request.getGroup(), Permission.SUB); + accessResource.addResourceAndPerm(request.getTopic(), Permission.SUB); + } else if (ForwardMessageToDeadLetterQueueRequest.getDescriptor().getFullName().equals(rpcFullName)) { + ForwardMessageToDeadLetterQueueRequest request = (ForwardMessageToDeadLetterQueueRequest) messageV3; + accessResource.addGroupResourceAndPerm(request.getGroup(), Permission.SUB); + accessResource.addResourceAndPerm(request.getTopic(), Permission.SUB); + } else if (EndTransactionRequest.getDescriptor().getFullName().equals(rpcFullName)) { + EndTransactionRequest request = (EndTransactionRequest) messageV3; + accessResource.addResourceAndPerm(request.getTopic(), Permission.PUB); + } else if (TelemetryCommand.getDescriptor().getFullName().equals(rpcFullName)) { + TelemetryCommand command = (TelemetryCommand) messageV3; + if (command.getCommandCase() == TelemetryCommand.CommandCase.SETTINGS) { + if (command.getSettings().hasPublishing()) { + List topicList = command.getSettings().getPublishing().getTopicsList(); + for (Resource topic : topicList) { + accessResource.addResourceAndPerm(topic, Permission.PUB); + } + } + if (command.getSettings().hasSubscription()) { + Subscription subscription = command.getSettings().getSubscription(); + accessResource.addGroupResourceAndPerm(subscription.getGroup(), Permission.SUB); + for (SubscriptionEntry entry : subscription.getSubscriptionsList()) { + accessResource.addResourceAndPerm(entry.getTopic(), Permission.SUB); + } + } + if (!command.getSettings().hasPublishing() && !command.getSettings().hasSubscription()) { + throw new AclException("settings command doesn't have publishing or subscription"); + } + } + } else if (NotifyClientTerminationRequest.getDescriptor().getFullName().equals(rpcFullName)) { + NotifyClientTerminationRequest request = (NotifyClientTerminationRequest) messageV3; + accessResource.addGroupResourceAndPerm(request.getGroup(), Permission.SUB); + } else if (QueryRouteRequest.getDescriptor().getFullName().equals(rpcFullName)) { + QueryRouteRequest request = (QueryRouteRequest) messageV3; + accessResource.addResourceAndPerm(request.getTopic(), Permission.ANY); + } else if (QueryAssignmentRequest.getDescriptor().getFullName().equals(rpcFullName)) { + QueryAssignmentRequest request = (QueryAssignmentRequest) messageV3; + accessResource.addGroupResourceAndPerm(request.getGroup(), Permission.SUB); + accessResource.addResourceAndPerm(request.getTopic(), Permission.SUB); + } else if (ChangeInvisibleDurationRequest.getDescriptor().getFullName().equals(rpcFullName)) { + ChangeInvisibleDurationRequest request = (ChangeInvisibleDurationRequest) messageV3; + accessResource.addGroupResourceAndPerm(request.getGroup(), Permission.SUB); + accessResource.addResourceAndPerm(request.getTopic(), Permission.SUB); + } + } catch (Throwable t) { + throw new AclException(t.getMessage(), t); + } + return accessResource; + } + + private void addResourceAndPerm(Resource resource, byte permission) { + String resourceName = NamespaceUtil.wrapNamespace(resource.getResourceNamespace(), resource.getName()); + addResourceAndPerm(resourceName, permission); + } + + private void addGroupResourceAndPerm(Resource resource, byte permission) { + String resourceName = NamespaceUtil.wrapNamespace(resource.getResourceNamespace(), resource.getName()); + addResourceAndPerm(getRetryTopic(resourceName), permission); + } + + public static PlainAccessResource build(PlainAccessConfig plainAccessConfig, RemoteAddressStrategy remoteAddressStrategy) { + PlainAccessResource plainAccessResource = new PlainAccessResource(); + plainAccessResource.setAccessKey(plainAccessConfig.getAccessKey()); + plainAccessResource.setSecretKey(plainAccessConfig.getSecretKey()); + plainAccessResource.setWhiteRemoteAddress(plainAccessConfig.getWhiteRemoteAddress()); + + plainAccessResource.setAdmin(plainAccessConfig.isAdmin()); + + plainAccessResource.setDefaultGroupPerm(Permission.parsePermFromString(plainAccessConfig.getDefaultGroupPerm())); + plainAccessResource.setDefaultTopicPerm(Permission.parsePermFromString(plainAccessConfig.getDefaultTopicPerm())); + + Permission.parseResourcePerms(plainAccessResource, false, plainAccessConfig.getGroupPerms()); + Permission.parseResourcePerms(plainAccessResource, true, plainAccessConfig.getTopicPerms()); + + plainAccessResource.setRemoteAddressStrategy(remoteAddressStrategy); + return plainAccessResource; + } + + public static boolean isRetryTopic(String topic) { + return null != topic && topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX); + } + + public static String printStr(String resource, boolean isGroup) { + if (resource == null) { + return null; + } + if (isGroup) { + return String.format("%s:%s", "group", getGroupFromRetryTopic(resource)); + } else { + return String.format("%s:%s", "topic", resource); + } + } + + public static String getGroupFromRetryTopic(String retryTopic) { + if (retryTopic == null) { + return null; + } + return KeyBuilder.parseGroup(retryTopic); + } + + public static String getRetryTopic(String group) { + if (group == null) { + return null; + } + return MixAll.getRetryTopic(group); + } + + public void addResourceAndPerm(String resource, byte perm) { + if (resource == null) { + return; + } + if (resourcePermMap == null) { + resourcePermMap = new HashMap<>(); + } + resourcePermMap.put(resource, perm); + } + + public String getAccessKey() { + return accessKey; + } + + public void setAccessKey(String accessKey) { + this.accessKey = accessKey; + } + + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + + public String getWhiteRemoteAddress() { + return whiteRemoteAddress; + } + + public void setWhiteRemoteAddress(String whiteRemoteAddress) { + this.whiteRemoteAddress = whiteRemoteAddress; + } + + public boolean isAdmin() { + return admin; + } + + public void setAdmin(boolean admin) { + this.admin = admin; + } + + public byte getDefaultTopicPerm() { + return defaultTopicPerm; + } + + public void setDefaultTopicPerm(byte defaultTopicPerm) { + this.defaultTopicPerm = defaultTopicPerm; + } + + public byte getDefaultGroupPerm() { + return defaultGroupPerm; + } + + public void setDefaultGroupPerm(byte defaultGroupPerm) { + this.defaultGroupPerm = defaultGroupPerm; + } + + public Map getResourcePermMap() { + return resourcePermMap; + } + + public String getRecognition() { + return recognition; + } + + public void setRecognition(String recognition) { + this.recognition = recognition; + } + + public int getRequestCode() { + return requestCode; + } + + public void setRequestCode(int requestCode) { + this.requestCode = requestCode; + } + + public String getSecretToken() { + return secretToken; + } + + public void setSecretToken(String secretToken) { + this.secretToken = secretToken; + } + + public RemoteAddressStrategy getRemoteAddressStrategy() { + return remoteAddressStrategy; + } + + public void setRemoteAddressStrategy(RemoteAddressStrategy remoteAddressStrategy) { + this.remoteAddressStrategy = remoteAddressStrategy; + } + + public String getSignature() { + return signature; + } + + public void setSignature(String signature) { + this.signature = signature; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this); + } + + public byte[] getContent() { + return content; + } + + public void setContent(byte[] content) { + this.content = content; + } +} diff --git a/acl/src/main/java/org/apache/rocketmq/acl/plain/PlainAccessValidator.java b/acl/src/main/java/org/apache/rocketmq/acl/plain/PlainAccessValidator.java new file mode 100644 index 00000000000..a7015eaca73 --- /dev/null +++ b/acl/src/main/java/org/apache/rocketmq/acl/plain/PlainAccessValidator.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.acl.plain; + +import com.google.protobuf.GeneratedMessageV3; +import java.util.List; +import java.util.Map; +import org.apache.rocketmq.acl.AccessResource; +import org.apache.rocketmq.acl.AccessValidator; +import org.apache.rocketmq.acl.common.AuthenticationHeader; +import org.apache.rocketmq.common.AclConfig; +import org.apache.rocketmq.common.PlainAccessConfig; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public class PlainAccessValidator implements AccessValidator { + + private PlainPermissionManager aclPlugEngine; + + public PlainAccessValidator() { + aclPlugEngine = new PlainPermissionManager(); + } + + @Override + public AccessResource parse(RemotingCommand request, String remoteAddr) { + return PlainAccessResource.parse(request, remoteAddr); + } + + @Override + public AccessResource parse(GeneratedMessageV3 messageV3, AuthenticationHeader header) { + return PlainAccessResource.parse(messageV3, header); + } + + @Override + public void validate(AccessResource accessResource) { + aclPlugEngine.validate((PlainAccessResource) accessResource); + } + + @Override + public boolean updateAccessConfig(PlainAccessConfig plainAccessConfig) { + return aclPlugEngine.updateAccessConfig(plainAccessConfig); + } + + @Override + public boolean deleteAccessConfig(String accessKey) { + return aclPlugEngine.deleteAccessConfig(accessKey); + } + + @Override + public String getAclConfigVersion() { + return aclPlugEngine.getAclConfigDataVersion(); + } + + @Override + public boolean updateGlobalWhiteAddrsConfig(List globalWhiteAddrsList) { + return aclPlugEngine.updateGlobalWhiteAddrsConfig(globalWhiteAddrsList); + } + + @Override + public boolean updateGlobalWhiteAddrsConfig(List globalWhiteAddrsList, String aclFileFullPath) { + return aclPlugEngine.updateGlobalWhiteAddrsConfig(globalWhiteAddrsList, aclFileFullPath); + } + + @Override + public AclConfig getAllAclConfig() { + return aclPlugEngine.getAllAclConfig(); + } + + @Override + public Map getAllAclConfigVersion() { + return aclPlugEngine.getDataVersionMap(); + } +} diff --git a/acl/src/main/java/org/apache/rocketmq/acl/plain/PlainPermissionChecker.java b/acl/src/main/java/org/apache/rocketmq/acl/plain/PlainPermissionChecker.java new file mode 100644 index 00000000000..8e6c317b237 --- /dev/null +++ b/acl/src/main/java/org/apache/rocketmq/acl/plain/PlainPermissionChecker.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.acl.plain; + +import java.util.Map; +import org.apache.rocketmq.acl.AccessResource; +import org.apache.rocketmq.acl.PermissionChecker; +import org.apache.rocketmq.acl.common.AclException; +import org.apache.rocketmq.acl.common.Permission; + +public class PlainPermissionChecker implements PermissionChecker { + public void check(AccessResource checkedAccess, AccessResource ownedAccess) { + PlainAccessResource checkedPlainAccess = (PlainAccessResource) checkedAccess; + PlainAccessResource ownedPlainAccess = (PlainAccessResource) ownedAccess; + + if (ownedPlainAccess.isAdmin()) { + // admin user don't need verification + return; + } + if (Permission.needAdminPerm(checkedPlainAccess.getRequestCode())) { + throw new AclException(String.format("Need admin permission for request code=%d, but accessKey=%s is not", checkedPlainAccess.getRequestCode(), ownedPlainAccess.getAccessKey())); + } + + Map needCheckedPermMap = checkedPlainAccess.getResourcePermMap(); + Map ownedPermMap = ownedPlainAccess.getResourcePermMap(); + + if (needCheckedPermMap == null) { + // If the needCheckedPermMap is null,then return + return; + } + + for (Map.Entry needCheckedEntry : needCheckedPermMap.entrySet()) { + String resource = needCheckedEntry.getKey(); + Byte neededPerm = needCheckedEntry.getValue(); + boolean isGroup = PlainAccessResource.isRetryTopic(resource); + + if (ownedPermMap == null || !ownedPermMap.containsKey(resource)) { + // Check the default perm + byte ownedPerm = isGroup ? ownedPlainAccess.getDefaultGroupPerm() : + ownedPlainAccess.getDefaultTopicPerm(); + if (!Permission.checkPermission(neededPerm, ownedPerm)) { + throw new AclException(String.format("No default permission for %s", PlainAccessResource.printStr(resource, isGroup))); + } + continue; + } + if (!Permission.checkPermission(neededPerm, ownedPermMap.get(resource))) { + throw new AclException(String.format("No permission for %s", PlainAccessResource.printStr(resource, isGroup))); + } + } + } +} diff --git a/acl/src/main/java/org/apache/rocketmq/acl/plain/PlainPermissionManager.java b/acl/src/main/java/org/apache/rocketmq/acl/plain/PlainPermissionManager.java new file mode 100644 index 00000000000..345aed06c5a --- /dev/null +++ b/acl/src/main/java/org/apache/rocketmq/acl/plain/PlainPermissionManager.java @@ -0,0 +1,648 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.acl.plain; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.acl.PermissionChecker; +import org.apache.rocketmq.acl.common.AclConstants; +import org.apache.rocketmq.acl.common.AclException; +import org.apache.rocketmq.acl.common.AclUtils; +import org.apache.rocketmq.acl.common.Permission; +import org.apache.rocketmq.common.AclConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.PlainAccessConfig; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.srvutil.AclFileWatchService; + +public class PlainPermissionManager { + + private static final Logger log = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); + + private String fileHome = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, + System.getenv(MixAll.ROCKETMQ_HOME_ENV)); + + private String defaultAclDir; + + private String defaultAclFile; + + private Map> aclPlainAccessResourceMap = new HashMap<>(); + + private Map accessKeyTable = new HashMap<>(); + + private List globalWhiteRemoteAddressStrategy = new ArrayList<>(); + + private RemoteAddressStrategyFactory remoteAddressStrategyFactory = new RemoteAddressStrategyFactory(); + + private Map> globalWhiteRemoteAddressStrategyMap = new HashMap<>(); + + private boolean isWatchStart; + + private Map dataVersionMap = new HashMap<>(); + + @Deprecated + private final DataVersion dataVersion = new DataVersion(); + + private List fileList = new ArrayList<>(); + + private final PermissionChecker permissionChecker = new PlainPermissionChecker(); + + public PlainPermissionManager() { + this.defaultAclDir = MixAll.dealFilePath(fileHome + File.separator + "conf" + File.separator + "acl"); + this.defaultAclFile = MixAll.dealFilePath(fileHome + File.separator + System.getProperty("rocketmq.acl.plain.file", "conf" + File.separator + "plain_acl.yml")); + load(); + watch(); + } + + public List getAllAclFiles(String path) { + if (!new File(path).exists()) { + log.info("The default acl dir {} is not exist", path); + return new ArrayList<>(); + } + List allAclFileFullPath = new ArrayList<>(); + File file = new File(path); + File[] files = file.listFiles(); + for (int i = 0; i < files.length; i++) { + String fileName = files[i].getAbsolutePath(); + File f = new File(fileName); + if (fileName.equals(fileHome + MixAll.ACL_CONF_TOOLS_FILE)) { + continue; + } else if (fileName.endsWith(".yml") || fileName.endsWith(".yaml")) { + allAclFileFullPath.add(fileName); + } else if (f.isDirectory()) { + allAclFileFullPath.addAll(getAllAclFiles(fileName)); + } + } + return allAclFileFullPath; + } + + public void load() { + if (fileHome == null || fileHome.isEmpty()) { + return; + } + + Map> aclPlainAccessResourceMap = new HashMap<>(); + Map accessKeyTable = new HashMap<>(); + List globalWhiteRemoteAddressStrategy = new ArrayList<>(); + Map> globalWhiteRemoteAddressStrategyMap = new HashMap<>(); + Map dataVersionMap = new HashMap<>(); + + assureAclConfigFilesExist(); + + fileList = getAllAclFiles(defaultAclDir); + if (new File(defaultAclFile).exists() && !fileList.contains(defaultAclFile)) { + fileList.add(defaultAclFile); + } + + for (int i = 0; i < fileList.size(); i++) { + final String currentFile = MixAll.dealFilePath(fileList.get(i)); + PlainAccessData plainAclConfData = AclUtils.getYamlDataObject(currentFile, + PlainAccessData.class); + if (plainAclConfData == null) { + log.warn("No data in file {}", currentFile); + continue; + } + log.info("Broker plain acl conf data is : {}", plainAclConfData.toString()); + + List globalWhiteRemoteAddressStrategyList = new ArrayList<>(); + List globalWhiteRemoteAddressesList = plainAclConfData.getGlobalWhiteRemoteAddresses(); + if (globalWhiteRemoteAddressesList != null && !globalWhiteRemoteAddressesList.isEmpty()) { + for (int j = 0; j < globalWhiteRemoteAddressesList.size(); j++) { + globalWhiteRemoteAddressStrategyList.add(remoteAddressStrategyFactory. + getRemoteAddressStrategy(globalWhiteRemoteAddressesList.get(j))); + } + } + if (globalWhiteRemoteAddressStrategyList.size() > 0) { + globalWhiteRemoteAddressStrategyMap.put(currentFile, globalWhiteRemoteAddressStrategyList); + globalWhiteRemoteAddressStrategy.addAll(globalWhiteRemoteAddressStrategyList); + } + + List accounts = plainAclConfData.getAccounts(); + Map plainAccessResourceMap = new HashMap<>(); + if (accounts != null && !accounts.isEmpty()) { + for (PlainAccessConfig plainAccessConfig : accounts) { + PlainAccessResource plainAccessResource = buildPlainAccessResource(plainAccessConfig); + //AccessKey can not be defined in multiple ACL files + if (accessKeyTable.get(plainAccessResource.getAccessKey()) == null) { + plainAccessResourceMap.put(plainAccessResource.getAccessKey(), plainAccessResource); + accessKeyTable.put(plainAccessResource.getAccessKey(), currentFile); + } else { + log.warn("The accessKey {} is repeated in multiple ACL files", plainAccessResource.getAccessKey()); + } + } + } + if (plainAccessResourceMap.size() > 0) { + aclPlainAccessResourceMap.put(currentFile, plainAccessResourceMap); + } + + List dataVersions = plainAclConfData.getDataVersion(); + DataVersion dataVersion = new DataVersion(); + if (dataVersions != null && !dataVersions.isEmpty()) { + DataVersion firstElement = new DataVersion(); + firstElement.setCounter(new AtomicLong(dataVersions.get(0).getCounter())); + firstElement.setTimestamp(dataVersions.get(0).getTimestamp()); + dataVersion.assignNewOne(firstElement); + } + dataVersionMap.put(currentFile, dataVersion); + } + + if (dataVersionMap.containsKey(defaultAclFile)) { + this.dataVersion.assignNewOne(dataVersionMap.get(defaultAclFile)); + } + this.dataVersionMap = dataVersionMap; + this.globalWhiteRemoteAddressStrategyMap = globalWhiteRemoteAddressStrategyMap; + this.globalWhiteRemoteAddressStrategy = globalWhiteRemoteAddressStrategy; + this.aclPlainAccessResourceMap = aclPlainAccessResourceMap; + this.accessKeyTable = accessKeyTable; + } + + /** + * Currently GlobalWhiteAddress is defined in {@link #defaultAclFile}, so make sure it exists. + */ + private void assureAclConfigFilesExist() { + final Path defaultAclFilePath = Paths.get(this.defaultAclFile); + if (!Files.exists(defaultAclFilePath)) { + try { + Files.createFile(defaultAclFilePath); + } catch (FileAlreadyExistsException e) { + // Maybe created by other threads + } catch (IOException e) { + log.error("Error in creating " + this.defaultAclFile, e); + throw new AclException(e.getMessage()); + } + } + } + + public void load(String aclFilePath) { + aclFilePath = MixAll.dealFilePath(aclFilePath); + Map plainAccessResourceMap = new HashMap<>(); + List globalWhiteRemoteAddressStrategy = new ArrayList<>(); + + PlainAccessData plainAclConfData = AclUtils.getYamlDataObject(aclFilePath, + PlainAccessData.class); + if (plainAclConfData == null) { + log.warn("No data in {}, skip it", aclFilePath); + return; + } + log.info("Broker plain acl conf data is : {}", plainAclConfData.toString()); + List globalWhiteRemoteAddressesList = plainAclConfData.getGlobalWhiteRemoteAddresses(); + if (globalWhiteRemoteAddressesList != null && !globalWhiteRemoteAddressesList.isEmpty()) { + for (int i = 0; i < globalWhiteRemoteAddressesList.size(); i++) { + globalWhiteRemoteAddressStrategy.add(remoteAddressStrategyFactory. + getRemoteAddressStrategy(globalWhiteRemoteAddressesList.get(i))); + } + } + + this.globalWhiteRemoteAddressStrategy.addAll(globalWhiteRemoteAddressStrategy); + if (this.globalWhiteRemoteAddressStrategyMap.get(aclFilePath) != null) { + List remoteAddressStrategyList = this.globalWhiteRemoteAddressStrategyMap.get(aclFilePath); + for (int i = 0; i < remoteAddressStrategyList.size(); i++) { + this.globalWhiteRemoteAddressStrategy.remove(remoteAddressStrategyList.get(i)); + } + this.globalWhiteRemoteAddressStrategyMap.put(aclFilePath, globalWhiteRemoteAddressStrategy); + } + + List accounts = plainAclConfData.getAccounts(); + if (accounts != null && !accounts.isEmpty()) { + for (PlainAccessConfig plainAccessConfig : accounts) { + PlainAccessResource plainAccessResource = buildPlainAccessResource(plainAccessConfig); + //AccessKey can not be defined in multiple ACL files + String oldPath = this.accessKeyTable.get(plainAccessResource.getAccessKey()); + if (oldPath == null || aclFilePath.equals(oldPath)) { + plainAccessResourceMap.put(plainAccessResource.getAccessKey(), plainAccessResource); + this.accessKeyTable.put(plainAccessResource.getAccessKey(), aclFilePath); + } else { + log.warn("The accessKey {} is repeated in multiple ACL files", plainAccessResource.getAccessKey()); + } + } + } + + // For loading dataversion part just + List dataVersions = plainAclConfData.getDataVersion(); + DataVersion dataVersion = new DataVersion(); + if (dataVersions != null && !dataVersions.isEmpty()) { + DataVersion firstElement = new DataVersion(); + firstElement.setCounter(new AtomicLong(dataVersions.get(0).getCounter())); + firstElement.setTimestamp(dataVersions.get(0).getTimestamp()); + dataVersion.assignNewOne(firstElement); + } + + this.aclPlainAccessResourceMap.put(aclFilePath, plainAccessResourceMap); + this.dataVersionMap.put(aclFilePath, dataVersion); + if (aclFilePath.equals(defaultAclFile)) { + this.dataVersion.assignNewOne(dataVersion); + } + } + + @Deprecated + public String getAclConfigDataVersion() { + return this.dataVersion.toJson(); + } + + public Map getDataVersionMap() { + return this.dataVersionMap; + } + + public PlainAccessData updateAclConfigFileVersion(String aclFileName, PlainAccessData updateAclConfigMap) { + + List dataVersions = updateAclConfigMap.getDataVersion(); + DataVersion dataVersion = new DataVersion(); + if (dataVersions != null) { + if (dataVersions.size() > 0) { + dataVersion.setTimestamp(dataVersions.get(0).getTimestamp()); + dataVersion.setCounter(new AtomicLong(dataVersions.get(0).getCounter())); + } + } + dataVersion.nextVersion(); + List versionElement = new ArrayList<>(); + PlainAccessData.DataVersion dataVersionNew = new PlainAccessData.DataVersion(); + dataVersionNew.setTimestamp(dataVersion.getTimestamp()); + dataVersionNew.setCounter(dataVersion.getCounter().get()); + versionElement.add(dataVersionNew); + updateAclConfigMap.setDataVersion(versionElement); + + dataVersionMap.put(aclFileName, dataVersion); + + return updateAclConfigMap; + } + + public boolean updateAccessConfig(PlainAccessConfig plainAccessConfig) { + + if (plainAccessConfig == null) { + log.error("Parameter value plainAccessConfig is null,Please check your parameter"); + throw new AclException("Parameter value plainAccessConfig is null, Please check your parameter"); + } + checkPlainAccessConfig(plainAccessConfig); + + Permission.checkResourcePerms(plainAccessConfig.getTopicPerms()); + Permission.checkResourcePerms(plainAccessConfig.getGroupPerms()); + + if (accessKeyTable.containsKey(plainAccessConfig.getAccessKey())) { + PlainAccessConfig updateAccountMap = null; + String aclFileName = accessKeyTable.get(plainAccessConfig.getAccessKey()); + PlainAccessData aclAccessConfigMap = AclUtils.getYamlDataObject(aclFileName, PlainAccessData.class); + List accounts = aclAccessConfigMap.getAccounts(); + if (null != accounts) { + for (PlainAccessConfig account : accounts) { + if (account.getAccessKey().equals(plainAccessConfig.getAccessKey())) { + // Update acl access config elements + accounts.remove(account); + updateAccountMap = createAclAccessConfigMap(account, plainAccessConfig); + accounts.add(updateAccountMap); + aclAccessConfigMap.setAccounts(accounts); + break; + } + } + } else { + // Maybe deleted in file, add it back + accounts = new LinkedList<>(); + updateAccountMap = createAclAccessConfigMap(null, plainAccessConfig); + accounts.add(updateAccountMap); + aclAccessConfigMap.setAccounts(accounts); + } + Map accountMap = aclPlainAccessResourceMap.get(aclFileName); + if (accountMap == null) { + accountMap = new HashMap<>(1); + accountMap.put(plainAccessConfig.getAccessKey(), buildPlainAccessResource(plainAccessConfig)); + } else if (accountMap.size() == 0) { + accountMap.put(plainAccessConfig.getAccessKey(), buildPlainAccessResource(plainAccessConfig)); + } else { + for (Map.Entry entry : accountMap.entrySet()) { + if (entry.getValue().getAccessKey().equals(plainAccessConfig.getAccessKey())) { + PlainAccessResource plainAccessResource = buildPlainAccessResource(plainAccessConfig); + accountMap.put(entry.getKey(), plainAccessResource); + break; + } + } + } + aclPlainAccessResourceMap.put(aclFileName, accountMap); + return AclUtils.writeDataObject(aclFileName, updateAclConfigFileVersion(aclFileName, aclAccessConfigMap)); + } else { + String fileName = MixAll.dealFilePath(defaultAclFile); + //Create acl access config elements on the default acl file + if (aclPlainAccessResourceMap.get(defaultAclFile) == null || aclPlainAccessResourceMap.get(defaultAclFile).size() == 0) { + try { + File defaultAclFile = new File(fileName); + if (!defaultAclFile.exists()) { + defaultAclFile.createNewFile(); + } + } catch (IOException e) { + log.warn("create default acl file has exception when update accessConfig. ", e); + } + } + PlainAccessData aclAccessConfigMap = AclUtils.getYamlDataObject(defaultAclFile, PlainAccessData.class); + if (aclAccessConfigMap == null) { + aclAccessConfigMap = new PlainAccessData(); + } + List accounts = aclAccessConfigMap.getAccounts(); + // When no accounts defined + if (null == accounts) { + accounts = new ArrayList<>(); + } + accounts.add(createAclAccessConfigMap(null, plainAccessConfig)); + aclAccessConfigMap.setAccounts(accounts); + accessKeyTable.put(plainAccessConfig.getAccessKey(), fileName); + if (aclPlainAccessResourceMap.get(fileName) == null) { + Map plainAccessResourceMap = new HashMap<>(1); + plainAccessResourceMap.put(plainAccessConfig.getAccessKey(), buildPlainAccessResource(plainAccessConfig)); + aclPlainAccessResourceMap.put(fileName, plainAccessResourceMap); + } else { + Map plainAccessResourceMap = aclPlainAccessResourceMap.get(fileName); + plainAccessResourceMap.put(plainAccessConfig.getAccessKey(), buildPlainAccessResource(plainAccessConfig)); + aclPlainAccessResourceMap.put(fileName, plainAccessResourceMap); + } + return AclUtils.writeDataObject(defaultAclFile, updateAclConfigFileVersion(defaultAclFile, aclAccessConfigMap)); + } + } + + public PlainAccessConfig createAclAccessConfigMap(PlainAccessConfig existedAccountMap, + PlainAccessConfig plainAccessConfig) { + + PlainAccessConfig newAccountsMap = null; + if (existedAccountMap == null) { + newAccountsMap = new PlainAccessConfig(); + } else { + newAccountsMap = existedAccountMap; + } + + if (StringUtils.isEmpty(plainAccessConfig.getAccessKey()) || + plainAccessConfig.getAccessKey().length() <= AclConstants.ACCESS_KEY_MIN_LENGTH) { + throw new AclException(String.format( + "The accessKey=%s cannot be null and length should longer than 6", + plainAccessConfig.getAccessKey())); + } + newAccountsMap.setAccessKey(plainAccessConfig.getAccessKey()); + + if (!StringUtils.isEmpty(plainAccessConfig.getSecretKey())) { + if (plainAccessConfig.getSecretKey().length() <= AclConstants.SECRET_KEY_MIN_LENGTH) { + throw new AclException(String.format( + "The secretKey=%s value length should longer than 6", + plainAccessConfig.getSecretKey())); + } + newAccountsMap.setSecretKey(plainAccessConfig.getSecretKey()); + } + if (plainAccessConfig.getWhiteRemoteAddress() != null) { + newAccountsMap.setWhiteRemoteAddress(plainAccessConfig.getWhiteRemoteAddress()); + } + if (!StringUtils.isEmpty(String.valueOf(plainAccessConfig.isAdmin()))) { + newAccountsMap.setAdmin(plainAccessConfig.isAdmin()); + } + if (!StringUtils.isEmpty(plainAccessConfig.getDefaultTopicPerm())) { + newAccountsMap.setDefaultTopicPerm(plainAccessConfig.getDefaultTopicPerm()); + } + if (!StringUtils.isEmpty(plainAccessConfig.getDefaultGroupPerm())) { + newAccountsMap.setDefaultGroupPerm(plainAccessConfig.getDefaultGroupPerm()); + } + if (plainAccessConfig.getTopicPerms() != null) { + newAccountsMap.setTopicPerms(plainAccessConfig.getTopicPerms()); + } + if (plainAccessConfig.getGroupPerms() != null) { + newAccountsMap.setGroupPerms(plainAccessConfig.getGroupPerms()); + } + + return newAccountsMap; + } + + public boolean deleteAccessConfig(String accessKey) { + if (StringUtils.isEmpty(accessKey)) { + log.error("Parameter value accessKey is null or empty String,Please check your parameter"); + return false; + } + + if (accessKeyTable.containsKey(accessKey)) { + String aclFileName = accessKeyTable.get(accessKey); + PlainAccessData aclAccessConfigData = AclUtils.getYamlDataObject(aclFileName, + PlainAccessData.class); + if (aclAccessConfigData == null) { + log.warn("No data found in {} when deleting access config of {}", aclFileName, accessKey); + return true; + } + List accounts = aclAccessConfigData.getAccounts(); + Iterator itemIterator = accounts.iterator(); + while (itemIterator.hasNext()) { + if (itemIterator.next().getAccessKey().equals(accessKey)) { + // Delete the related acl config element + itemIterator.remove(); + accessKeyTable.remove(accessKey); + aclAccessConfigData.setAccounts(accounts); + return AclUtils.writeDataObject(aclFileName, updateAclConfigFileVersion(aclFileName, aclAccessConfigData)); + } + } + } + return false; + } + + public boolean updateGlobalWhiteAddrsConfig(List globalWhiteAddrsList) { + return this.updateGlobalWhiteAddrsConfig(globalWhiteAddrsList, this.defaultAclFile); + } + + public boolean updateGlobalWhiteAddrsConfig(List globalWhiteAddrsList, String fileName) { + if (fileName == null || fileName.equals("")) { + fileName = this.defaultAclFile; + } + + if (globalWhiteAddrsList == null) { + log.error("Parameter value globalWhiteAddrsList is null,Please check your parameter"); + return false; + } + + File file = new File(fileName); + if (!file.exists() || file.isDirectory()) { + log.error("Parameter value " + fileName + " is not exist or is a directory, please check your parameter"); + return false; + } + + if (!file.getAbsolutePath().startsWith(fileHome)) { + log.error("Parameter value " + fileName + " is not in the directory rocketmq.home.dir " + fileHome); + return false; + } + + if (!fileName.endsWith(".yml") && fileName.endsWith(".yaml")) { + log.error("Parameter value " + fileName + " is not a ACL configuration file"); + return false; + } + + PlainAccessData aclAccessConfigMap = AclUtils.getYamlDataObject(fileName, PlainAccessData.class); + if (aclAccessConfigMap == null) { + aclAccessConfigMap = new PlainAccessData(); + log.info("No data in {}, create a new aclAccessConfigMap", fileName); + } + // Update globalWhiteRemoteAddr element in memory map firstly + aclAccessConfigMap.setGlobalWhiteRemoteAddresses(new ArrayList<>(globalWhiteAddrsList)); + return AclUtils.writeDataObject(fileName, updateAclConfigFileVersion(fileName, aclAccessConfigMap)); + + } + + public AclConfig getAllAclConfig() { + AclConfig aclConfig = new AclConfig(); + List configs = new ArrayList<>(); + List whiteAddrs = new ArrayList<>(); + Set accessKeySets = new HashSet<>(); + + for (int i = 0; i < fileList.size(); i++) { + String path = fileList.get(i); + PlainAccessData plainAclConfData = AclUtils.getYamlDataObject(path, + PlainAccessData.class); + if (plainAclConfData == null) { + continue; + } + List globalWhiteAddrs = plainAclConfData.getGlobalWhiteRemoteAddresses(); + if (globalWhiteAddrs != null && !globalWhiteAddrs.isEmpty()) { + whiteAddrs.addAll(globalWhiteAddrs); + } + + List plainAccessConfigs = plainAclConfData.getAccounts(); + if (plainAccessConfigs != null && !plainAccessConfigs.isEmpty()) { + for (int j = 0; j < plainAccessConfigs.size(); j++) { + if (!accessKeySets.contains(plainAccessConfigs.get(j).getAccessKey())) { + accessKeySets.add(plainAccessConfigs.get(j).getAccessKey()); + PlainAccessConfig plainAccessConfig = new PlainAccessConfig(); + plainAccessConfig.setGroupPerms(plainAccessConfigs.get(j).getGroupPerms()); + plainAccessConfig.setDefaultTopicPerm(plainAccessConfigs.get(j).getDefaultTopicPerm()); + plainAccessConfig.setDefaultGroupPerm(plainAccessConfigs.get(j).getDefaultGroupPerm()); + plainAccessConfig.setAccessKey(plainAccessConfigs.get(j).getAccessKey()); + plainAccessConfig.setSecretKey(plainAccessConfigs.get(j).getSecretKey()); + plainAccessConfig.setAdmin(plainAccessConfigs.get(j).isAdmin()); + plainAccessConfig.setTopicPerms(plainAccessConfigs.get(j).getTopicPerms()); + plainAccessConfig.setWhiteRemoteAddress(plainAccessConfigs.get(j).getWhiteRemoteAddress()); + configs.add(plainAccessConfig); + } + } + } + } + aclConfig.setPlainAccessConfigs(configs); + aclConfig.setGlobalWhiteAddrs(whiteAddrs); + return aclConfig; + } + + private void watch() { + try { + AclFileWatchService aclFileWatchService = new AclFileWatchService(defaultAclDir, defaultAclFile, new AclFileWatchService.Listener() { + @Override + public void onFileChanged(String aclFileName) { + load(aclFileName); + } + + @Override + public void onFileNumChanged(String path) { + load(); + } + }); + aclFileWatchService.start(); + log.info("Succeed to start AclFileWatchService"); + this.isWatchStart = true; + } catch (Exception e) { + log.error("Failed to start AclWatcherService", e); + } + + } + + void checkPerm(PlainAccessResource needCheckedAccess, PlainAccessResource ownedAccess) { + permissionChecker.check(needCheckedAccess, ownedAccess); + } + + void clearPermissionInfo() { + this.aclPlainAccessResourceMap.clear(); + this.accessKeyTable.clear(); + this.globalWhiteRemoteAddressStrategy.clear(); + } + + public void checkPlainAccessConfig(PlainAccessConfig plainAccessConfig) throws AclException { + if (plainAccessConfig.getAccessKey() == null + || plainAccessConfig.getSecretKey() == null + || plainAccessConfig.getAccessKey().length() <= AclConstants.ACCESS_KEY_MIN_LENGTH + || plainAccessConfig.getSecretKey().length() <= AclConstants.SECRET_KEY_MIN_LENGTH) { + throw new AclException(String.format( + "The accessKey=%s and secretKey=%s cannot be null and length should longer than 6", + plainAccessConfig.getAccessKey(), plainAccessConfig.getSecretKey())); + } + } + + public PlainAccessResource buildPlainAccessResource(PlainAccessConfig plainAccessConfig) throws AclException { + checkPlainAccessConfig(plainAccessConfig); + return PlainAccessResource.build(plainAccessConfig, remoteAddressStrategyFactory. + getRemoteAddressStrategy(plainAccessConfig.getWhiteRemoteAddress())); + } + + public void validate(PlainAccessResource plainAccessResource) { + + // Check the global white remote addr + for (RemoteAddressStrategy remoteAddressStrategy : globalWhiteRemoteAddressStrategy) { + if (remoteAddressStrategy.match(plainAccessResource)) { + return; + } + } + + if (plainAccessResource.getAccessKey() == null) { + throw new AclException("No accessKey is configured"); + } + + if (!accessKeyTable.containsKey(plainAccessResource.getAccessKey())) { + throw new AclException(String.format("No acl config for %s", plainAccessResource.getAccessKey())); + } + + // Check the white addr for accessKey + String aclFileName = accessKeyTable.get(plainAccessResource.getAccessKey()); + PlainAccessResource ownedAccess = aclPlainAccessResourceMap.getOrDefault(aclFileName, new HashMap<>()).get(plainAccessResource.getAccessKey()); + if (ownedAccess == null) { + throw new AclException(String.format("No PlainAccessResource for accessKey=%s", plainAccessResource.getAccessKey())); + } + if (ownedAccess.getRemoteAddressStrategy().match(plainAccessResource)) { + return; + } + + // Check the signature + String signature = AclUtils.calSignature(plainAccessResource.getContent(), ownedAccess.getSecretKey()); + if (!signature.equals(plainAccessResource.getSignature())) { + throw new AclException(String.format("Check signature failed for accessKey=%s", plainAccessResource.getAccessKey())); + } + + //Skip the topic RMQ_SYS_TRACE_TOPIC permission check,if the topic RMQ_SYS_TRACE_TOPIC is used for message trace + Map resourcePermMap = plainAccessResource.getResourcePermMap(); + if (resourcePermMap != null) { + Byte permission = resourcePermMap.get(TopicValidator.RMQ_SYS_TRACE_TOPIC); + if (permission != null && permission == Permission.PUB) { + return; + } + } + + // Check perm of each resource + checkPerm(plainAccessResource, ownedAccess); + } + + public boolean isWatchStart() { + return isWatchStart; + } +} diff --git a/acl/src/main/java/org/apache/rocketmq/acl/plain/RemoteAddressStrategy.java b/acl/src/main/java/org/apache/rocketmq/acl/plain/RemoteAddressStrategy.java new file mode 100644 index 00000000000..8eab40c954b --- /dev/null +++ b/acl/src/main/java/org/apache/rocketmq/acl/plain/RemoteAddressStrategy.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.acl.plain; + +public interface RemoteAddressStrategy { + + boolean match(PlainAccessResource plainAccessResource); +} diff --git a/acl/src/main/java/org/apache/rocketmq/acl/plain/RemoteAddressStrategyFactory.java b/acl/src/main/java/org/apache/rocketmq/acl/plain/RemoteAddressStrategyFactory.java new file mode 100644 index 00000000000..fb4151e5366 --- /dev/null +++ b/acl/src/main/java/org/apache/rocketmq/acl/plain/RemoteAddressStrategyFactory.java @@ -0,0 +1,255 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.acl.plain; + +import java.util.HashSet; +import java.util.Set; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.validator.routines.InetAddressValidator; +import org.apache.rocketmq.acl.common.AclException; +import org.apache.rocketmq.acl.common.AclUtils; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class RemoteAddressStrategyFactory { + + private static final Logger log = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); + + public static final NullRemoteAddressStrategy NULL_NET_ADDRESS_STRATEGY = new NullRemoteAddressStrategy(); + + public static final BlankRemoteAddressStrategy BLANK_NET_ADDRESS_STRATEGY = new BlankRemoteAddressStrategy(); + + public RemoteAddressStrategy getRemoteAddressStrategy(PlainAccessResource plainAccessResource) { + return getRemoteAddressStrategy(plainAccessResource.getWhiteRemoteAddress()); + } + + public RemoteAddressStrategy getRemoteAddressStrategy(String remoteAddr) { + if (StringUtils.isBlank(remoteAddr)) { + return BLANK_NET_ADDRESS_STRATEGY; + } + if ("*".equals(remoteAddr) || "*.*.*.*".equals(remoteAddr) || "*:*:*:*:*:*:*:*".equals(remoteAddr)) { + return NULL_NET_ADDRESS_STRATEGY; + } + if (remoteAddr.endsWith("}")) { + if (AclUtils.isColon(remoteAddr)) { + String[] strArray = StringUtils.split(remoteAddr, ":"); + String last = strArray[strArray.length - 1]; + if (!last.startsWith("{")) { + throw new AclException(String.format("MultipleRemoteAddressStrategy netAddress examine scope Exception netAddress: %s", remoteAddr)); + } + return new MultipleRemoteAddressStrategy(AclUtils.getAddresses(remoteAddr, last)); + } else { + String[] strArray = StringUtils.split(remoteAddr, "."); + // However a right IP String provided by user,it always can be divided into 4 parts by '.'. + if (strArray.length < 4) { + throw new AclException(String.format("MultipleRemoteAddressStrategy has got a/some wrong format IP(s): %s ", remoteAddr)); + } + String lastStr = strArray[strArray.length - 1]; + if (!lastStr.startsWith("{")) { + throw new AclException(String.format("MultipleRemoteAddressStrategy netAddress examine scope Exception netAddress: %s", remoteAddr)); + } + return new MultipleRemoteAddressStrategy(AclUtils.getAddresses(remoteAddr, lastStr)); + } + } else if (AclUtils.isComma(remoteAddr)) { + return new MultipleRemoteAddressStrategy(StringUtils.split(remoteAddr, ",")); + } else if (AclUtils.isAsterisk(remoteAddr) || AclUtils.isMinus(remoteAddr)) { + return new RangeRemoteAddressStrategy(remoteAddr); + } + return new OneRemoteAddressStrategy(remoteAddr); + + } + + public static class NullRemoteAddressStrategy implements RemoteAddressStrategy { + @Override + public boolean match(PlainAccessResource plainAccessResource) { + return true; + } + + } + + public static class BlankRemoteAddressStrategy implements RemoteAddressStrategy { + @Override + public boolean match(PlainAccessResource plainAccessResource) { + return false; + } + + } + + public static class MultipleRemoteAddressStrategy implements RemoteAddressStrategy { + + private final Set multipleSet = new HashSet<>(); + + public MultipleRemoteAddressStrategy(String[] strArray) { + InetAddressValidator validator = InetAddressValidator.getInstance(); + for (String netAddress : strArray) { + if (validator.isValidInet4Address(netAddress)) { + multipleSet.add(netAddress); + } else if (validator.isValidInet6Address(netAddress)) { + multipleSet.add(AclUtils.expandIP(netAddress, 8)); + } else { + throw new AclException(String.format("NetAddress examine Exception netAddress is %s", netAddress)); + } + } + } + + @Override + public boolean match(PlainAccessResource plainAccessResource) { + InetAddressValidator validator = InetAddressValidator.getInstance(); + String whiteRemoteAddress = plainAccessResource.getWhiteRemoteAddress(); + if (validator.isValidInet6Address(whiteRemoteAddress)) { + whiteRemoteAddress = AclUtils.expandIP(whiteRemoteAddress, 8); + } + return multipleSet.contains(whiteRemoteAddress); + } + + } + + public static class OneRemoteAddressStrategy implements RemoteAddressStrategy { + + private String netAddress; + + public OneRemoteAddressStrategy(String netAddress) { + this.netAddress = netAddress; + InetAddressValidator validator = InetAddressValidator.getInstance(); + if (!(validator.isValidInet4Address(netAddress) || validator.isValidInet6Address( + netAddress))) { + throw new AclException(String.format("NetAddress examine Exception netAddress is %s", + netAddress)); + } + } + + @Override + public boolean match(PlainAccessResource plainAccessResource) { + String writeRemoteAddress = AclUtils.expandIP(plainAccessResource.getWhiteRemoteAddress(), 8).toUpperCase(); + return AclUtils.expandIP(netAddress, 8).toUpperCase().equals(writeRemoteAddress); + } + + } + + public static class RangeRemoteAddressStrategy implements RemoteAddressStrategy { + + private String head; + + private int start; + + private int end; + + private int index; + + public RangeRemoteAddressStrategy(String remoteAddr) { +// IPv6 Address + if (AclUtils.isColon(remoteAddr)) { + AclUtils.IPv6AddressCheck(remoteAddr); + String[] strArray = StringUtils.split(remoteAddr, ":"); + for (int i = 1; i < strArray.length; i++) { + if (ipv6Analysis(strArray, i)) { + AclUtils.verify(remoteAddr, index - 1); + String preAddress = AclUtils.v6ipProcess(remoteAddr); + this.index = StringUtils.split(preAddress, ":").length; + this.head = preAddress; + break; + } + } + } else { + String[] strArray = StringUtils.split(remoteAddr, "."); + if (analysis(strArray, 1) || analysis(strArray, 2) || analysis(strArray, 3)) { + AclUtils.verify(remoteAddr, index - 1); + StringBuilder sb = new StringBuilder(); + for (int j = 0; j < index; j++) { + sb.append(strArray[j].trim()).append("."); + } + this.head = sb.toString(); + } + } + } + + private boolean analysis(String[] strArray, int index) { + String value = strArray[index].trim(); + this.index = index; + if ("*".equals(value)) { + setValue(0, 255); + } else if (AclUtils.isMinus(value)) { + if (value.indexOf("-") == 0) { + throw new AclException(String.format("RangeRemoteAddressStrategy netAddress examine scope Exception value %s ", value)); + + } + String[] valueArray = StringUtils.split(value, "-"); + this.start = Integer.parseInt(valueArray[0]); + this.end = Integer.parseInt(valueArray[1]); + if (!(AclUtils.isScope(end) && AclUtils.isScope(start) && start <= end)) { + throw new AclException(String.format("RangeRemoteAddressStrategy netAddress examine scope Exception start is %s , end is %s", start, end)); + } + } + return this.end > 0; + } + + private boolean ipv6Analysis(String[] strArray, int index) { + String value = strArray[index].trim(); + this.index = index; + if ("*".equals(value)) { + int min = Integer.parseInt("0", 16); + int max = Integer.parseInt("ffff", 16); + setValue(min, max); + } else if (AclUtils.isMinus(value)) { + if (value.indexOf("-") == 0) { + throw new AclException(String.format("RangeRemoteAddressStrategy netAddress examine scope Exception value %s ", value)); + } + String[] valueArray = StringUtils.split(value, "-"); + this.start = Integer.parseInt(valueArray[0], 16); + this.end = Integer.parseInt(valueArray[1], 16); + if (!(AclUtils.isIPv6Scope(end) && AclUtils.isIPv6Scope(start) && start <= end)) { + throw new AclException(String.format("RangeRemoteAddressStrategy netAddress examine scope Exception start is %s , end is %s", start, end)); + } + } + return this.end > 0; + } + + private void setValue(int start, int end) { + this.start = start; + this.end = end; + } + + @Override + public boolean match(PlainAccessResource plainAccessResource) { + String netAddress = plainAccessResource.getWhiteRemoteAddress(); + InetAddressValidator validator = InetAddressValidator.getInstance(); + if (validator.isValidInet4Address(netAddress)) { + if (netAddress.startsWith(this.head)) { + String value; + if (index == 3) { + value = netAddress.substring(this.head.length()); + } else if (index == 2) { + value = netAddress.substring(this.head.length(), netAddress.lastIndexOf('.')); + } else { + value = netAddress.substring(this.head.length(), netAddress.lastIndexOf('.', netAddress.lastIndexOf('.') - 1)); + } + Integer address = Integer.valueOf(value); + return address >= this.start && address <= this.end; + } + } else if (validator.isValidInet6Address(netAddress)) { + netAddress = AclUtils.expandIP(netAddress, 8).toUpperCase(); + if (netAddress.startsWith(this.head)) { + String value = netAddress.substring(5 * index, 5 * index + 4); + Integer address = Integer.parseInt(value, 16); + return address >= this.start && address <= this.end; + } + } + return false; + } + } +} diff --git a/acl/src/test/java/org/apache/rocketmq/acl/RemotingClientAccessTest.java b/acl/src/test/java/org/apache/rocketmq/acl/RemotingClientAccessTest.java new file mode 100644 index 00000000000..88c5e09a94c --- /dev/null +++ b/acl/src/test/java/org/apache/rocketmq/acl/RemotingClientAccessTest.java @@ -0,0 +1,189 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.acl; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import org.apache.rocketmq.acl.common.AclClientRPCHook; +import org.apache.rocketmq.acl.common.AclException; +import org.apache.rocketmq.acl.common.SessionCredentials; +import org.apache.rocketmq.acl.plain.AclTestHelper; +import org.apache.rocketmq.acl.plain.PlainAccessResource; +import org.apache.rocketmq.acl.plain.PlainAccessValidator; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class RemotingClientAccessTest { + + private PlainAccessValidator plainAccessValidator; + private AclClientRPCHook aclClient; + private SessionCredentials sessionCredentials; + + private File confHome; + + private String clientAddress = "10.7.1.3"; + + @Before + public void init() throws IOException { + String folder = "access_acl_conf"; + confHome = AclTestHelper.copyResources(folder, true); + System.setProperty("rocketmq.home.dir", confHome.getAbsolutePath()); + System.setProperty("rocketmq.acl.plain.file", "/access_acl_conf/acl/plain_acl.yml".replace("/", File.separator)); + + plainAccessValidator = new PlainAccessValidator(); + sessionCredentials = new SessionCredentials(); + sessionCredentials.setAccessKey("rocketmq3"); + sessionCredentials.setSecretKey("12345678"); + aclClient = new AclClientRPCHook(sessionCredentials); + } + + @After + public void cleanUp() { + AclTestHelper.recursiveDelete(confHome); + } + + @Test(expected = AclException.class) + public void testProduceDenyTopic() { + SendMessageRequestHeader messageRequestHeader = new SendMessageRequestHeader(); + messageRequestHeader.setTopic("topicD"); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, messageRequestHeader); + aclClient.doBeforeRequest(clientAddress, remotingCommand); + + ByteBuffer buf = remotingCommand.encodeHeader(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse(RemotingCommand.decode(buf), clientAddress); + plainAccessValidator.validate(accessResource); + } catch (RemotingCommandException e) { + e.printStackTrace(); + Assert.fail("Should not throw IOException"); + } + } + + @Test + public void testProduceAuthorizedTopic() { + SendMessageRequestHeader messageRequestHeader = new SendMessageRequestHeader(); + messageRequestHeader.setTopic("topicA"); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, messageRequestHeader); + aclClient.doBeforeRequest(clientAddress, remotingCommand); + + ByteBuffer buf = remotingCommand.encodeHeader(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse(RemotingCommand.decode(buf), clientAddress); + plainAccessValidator.validate(accessResource); + } catch (RemotingCommandException e) { + e.printStackTrace(); + Assert.fail("Should not throw IOException"); + } + } + + + @Test(expected = AclException.class) + public void testConsumeDenyTopic() { + PullMessageRequestHeader pullMessageRequestHeader = new PullMessageRequestHeader(); + pullMessageRequestHeader.setTopic("topicD"); + pullMessageRequestHeader.setConsumerGroup("groupB"); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, pullMessageRequestHeader); + aclClient.doBeforeRequest("", remotingCommand); + ByteBuffer buf = remotingCommand.encodeHeader(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse(RemotingCommand.decode(buf), "123.4.5.6"); + plainAccessValidator.validate(accessResource); + } catch (RemotingCommandException e) { + e.printStackTrace(); + Assert.fail("Should not throw IOException"); + } + + } + + @Test + public void testConsumeAuthorizedTopic() { + PullMessageRequestHeader pullMessageRequestHeader = new PullMessageRequestHeader(); + pullMessageRequestHeader.setTopic("topicB"); + pullMessageRequestHeader.setConsumerGroup("groupB"); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, pullMessageRequestHeader); + aclClient.doBeforeRequest("", remotingCommand); + ByteBuffer buf = remotingCommand.encodeHeader(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse(RemotingCommand.decode(buf), "123.4.5.6"); + plainAccessValidator.validate(accessResource); + } catch (RemotingCommandException e) { + e.printStackTrace(); + Assert.fail("Should not throw IOException"); + } + } + + @Test(expected = AclException.class) + public void testConsumeInDeniedGroup() { + PullMessageRequestHeader pullMessageRequestHeader = new PullMessageRequestHeader(); + pullMessageRequestHeader.setTopic("topicB"); + pullMessageRequestHeader.setConsumerGroup("groupD"); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, pullMessageRequestHeader); + aclClient.doBeforeRequest("", remotingCommand); + ByteBuffer buf = remotingCommand.encodeHeader(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse(RemotingCommand.decode(buf), "123.4.5.6"); + plainAccessValidator.validate(accessResource); + } catch (RemotingCommandException e) { + e.printStackTrace(); + Assert.fail("Should not throw IOException"); + } + } + + @Test + public void testConsumeInAuthorizedGroup() { + PullMessageRequestHeader pullMessageRequestHeader = new PullMessageRequestHeader(); + pullMessageRequestHeader.setTopic("topicB"); + pullMessageRequestHeader.setConsumerGroup("groupB"); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, pullMessageRequestHeader); + aclClient.doBeforeRequest("", remotingCommand); + ByteBuffer buf = remotingCommand.encodeHeader(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse(RemotingCommand.decode(buf), "123.4.5.6"); + plainAccessValidator.validate(accessResource); + } catch (RemotingCommandException e) { + e.printStackTrace(); + Assert.fail("Should not throw IOException"); + } + } + +} diff --git a/acl/src/test/java/org/apache/rocketmq/acl/common/AclClientRPCHookTest.java b/acl/src/test/java/org/apache/rocketmq/acl/common/AclClientRPCHookTest.java new file mode 100644 index 00000000000..9789ed191c4 --- /dev/null +++ b/acl/src/test/java/org/apache/rocketmq/acl/common/AclClientRPCHookTest.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.acl.common; + +import java.lang.reflect.Field; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.RequestType; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.junit.Test; + +import static org.apache.rocketmq.acl.common.SessionCredentials.ACCESS_KEY; +import static org.apache.rocketmq.acl.common.SessionCredentials.SECURITY_TOKEN; +import static org.assertj.core.api.Assertions.assertThat; + +public class AclClientRPCHookTest { + protected ConcurrentHashMap, Field[]> fieldCache = + new ConcurrentHashMap<>(); + private AclClientRPCHook aclClientRPCHook = new AclClientRPCHook(null); + + @Test + public void testParseRequestContent() { + PullMessageRequestHeader requestHeader = new PullMessageRequestHeader(); + requestHeader.setConsumerGroup("group"); + requestHeader.setTopic("topic"); + requestHeader.setQueueId(1); + requestHeader.setQueueOffset(2L); + requestHeader.setMaxMsgNums(32); + requestHeader.setSysFlag(0); + requestHeader.setCommitOffset(0L); + requestHeader.setSuspendTimeoutMillis(15000L); + requestHeader.setSubVersion(0L); + RemotingCommand testPullRemotingCommand = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, requestHeader); + SortedMap oldContent = oldVersionParseRequestContent(testPullRemotingCommand, "ak", null); + byte[] oldBytes = AclUtils.combineRequestContent(testPullRemotingCommand, oldContent); + testPullRemotingCommand.addExtField(ACCESS_KEY, "ak"); + SortedMap content = aclClientRPCHook.parseRequestContent(testPullRemotingCommand); + byte[] newBytes = AclUtils.combineRequestContent(testPullRemotingCommand, content); + assertThat(newBytes).isEqualTo(oldBytes); + } + + @Test + public void testParseRequestContentWithStreamRequestType() { + PullMessageRequestHeader requestHeader = new PullMessageRequestHeader(); + requestHeader.setConsumerGroup("group"); + requestHeader.setTopic("topic"); + requestHeader.setQueueId(1); + requestHeader.setQueueOffset(2L); + requestHeader.setMaxMsgNums(32); + requestHeader.setSysFlag(0); + requestHeader.setCommitOffset(0L); + requestHeader.setSuspendTimeoutMillis(15000L); + requestHeader.setSubVersion(0L); + RemotingCommand testPullRemotingCommand = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, requestHeader); + testPullRemotingCommand.addExtField(MixAll.REQ_T, String.valueOf(RequestType.STREAM.getCode())); + testPullRemotingCommand.addExtField(ACCESS_KEY, "ak"); + SortedMap content = aclClientRPCHook.parseRequestContent(testPullRemotingCommand); + assertThat(content.get(MixAll.REQ_T)).isEqualTo(String.valueOf(RequestType.STREAM.getCode())); + } + + private SortedMap oldVersionParseRequestContent(RemotingCommand request, String ak, String securityToken) { + CommandCustomHeader header = request.readCustomHeader(); + // Sort property + SortedMap map = new TreeMap<>(); + map.put(ACCESS_KEY, ak); + if (securityToken != null) { + map.put(SECURITY_TOKEN, securityToken); + } + try { + // Add header properties + if (null != header) { + Field[] fields = fieldCache.get(header.getClass()); + if (null == fields) { + fields = header.getClass().getDeclaredFields(); + for (Field field : fields) { + field.setAccessible(true); + } + Field[] tmp = fieldCache.putIfAbsent(header.getClass(), fields); + if (null != tmp) { + fields = tmp; + } + } + + for (Field field : fields) { + Object value = field.get(header); + if (null != value && !field.isSynthetic()) { + map.put(field.getName(), value.toString()); + } + } + } + return map; + } catch (Exception e) { + throw new RuntimeException("incompatible exception.", e); + } + } +} diff --git a/acl/src/test/java/org/apache/rocketmq/acl/common/AclSignerTest.java b/acl/src/test/java/org/apache/rocketmq/acl/common/AclSignerTest.java new file mode 100644 index 00000000000..2680d6bd820 --- /dev/null +++ b/acl/src/test/java/org/apache/rocketmq/acl/common/AclSignerTest.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.acl.common; + +import org.junit.Assert; +import org.junit.Test; + +public class AclSignerTest { + + @Test(expected = Exception.class) + public void calSignatureExceptionTest() { + AclSigner.calSignature(new byte[]{},""); + } + + @Test + public void calSignatureTest() { + String expectedSignature = "IUc8rrO/0gDch8CjObLQsW2rsiA="; + Assert.assertEquals(expectedSignature, AclSigner.calSignature("RocketMQ", "12345678")); + Assert.assertEquals(expectedSignature, AclSigner.calSignature("RocketMQ".getBytes(), "12345678")); + } + +} diff --git a/acl/src/test/java/org/apache/rocketmq/acl/common/AclUtilsTest.java b/acl/src/test/java/org/apache/rocketmq/acl/common/AclUtilsTest.java new file mode 100644 index 00000000000..03bceade770 --- /dev/null +++ b/acl/src/test/java/org/apache/rocketmq/acl/common/AclUtilsTest.java @@ -0,0 +1,299 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.acl.common; + +import com.alibaba.fastjson.JSONObject; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.acl.plain.PlainAccessData; +import org.apache.rocketmq.common.PlainAccessConfig; +import org.apache.rocketmq.remoting.RPCHook; +import org.junit.Assert; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class AclUtilsTest { + + @Test + public void testGetAddresses() { + String address = "1.1.1.{1,2,3,4}"; + String[] addressArray = AclUtils.getAddresses(address, "{1,2,3,4}"); + List newAddressList = new ArrayList<>(Arrays.asList(addressArray)); + + List addressList = new ArrayList<>(); + addressList.add("1.1.1.1"); + addressList.add("1.1.1.2"); + addressList.add("1.1.1.3"); + addressList.add("1.1.1.4"); + Assert.assertEquals(newAddressList, addressList); + + // IPv6 test + String ipv6Address = "1:ac41:9987::bb22:666:{1,2,3,4}"; + String[] ipv6AddressArray = AclUtils.getAddresses(ipv6Address, "{1,2,3,4}"); + List newIPv6AddressList = new ArrayList<>(); + Collections.addAll(newIPv6AddressList, ipv6AddressArray); + + List ipv6AddressList = new ArrayList<>(); + ipv6AddressList.add("1:ac41:9987::bb22:666:1"); + ipv6AddressList.add("1:ac41:9987::bb22:666:2"); + ipv6AddressList.add("1:ac41:9987::bb22:666:3"); + ipv6AddressList.add("1:ac41:9987::bb22:666:4"); + Assert.assertEquals(newIPv6AddressList, ipv6AddressList); + } + + @Test + public void testIsScope_StringArray() { + String address = "12"; + + for (int i = 0; i < 6; i++) { + boolean isScope = AclUtils.isScope(address, 4); + if (i == 3) { + Assert.assertTrue(isScope); + } else { + Assert.assertFalse(isScope); + } + address = address + ".12"; + } + } + + @Test + public void testIsScope_Array() { + String[] address = StringUtils.split("12.12.12.12", "."); + boolean isScope = AclUtils.isScope(address, 4); + Assert.assertTrue(isScope); + isScope = AclUtils.isScope(address, 3); + Assert.assertTrue(isScope); + + address = StringUtils.split("12.12.1222.1222", "."); + isScope = AclUtils.isScope(address, 4); + Assert.assertFalse(isScope); + isScope = AclUtils.isScope(address, 3); + Assert.assertFalse(isScope); + + // IPv6 test + address = StringUtils.split("1050:0000:0000:0000:0005:0600:300c:326b", ":"); + isScope = AclUtils.isIPv6Scope(address, 8); + Assert.assertTrue(isScope); + isScope = AclUtils.isIPv6Scope(address, 4); + Assert.assertTrue(isScope); + + address = StringUtils.split("1050:9876:0000:0000:0005:akkg:300c:326b", ":"); + isScope = AclUtils.isIPv6Scope(address, 8); + Assert.assertFalse(isScope); + isScope = AclUtils.isIPv6Scope(address, 4); + Assert.assertTrue(isScope); + + address = StringUtils.split(AclUtils.expandIP("1050::0005:akkg:300c:326b", 8), ":"); + isScope = AclUtils.isIPv6Scope(address, 8); + Assert.assertFalse(isScope); + isScope = AclUtils.isIPv6Scope(address, 4); + Assert.assertTrue(isScope); + } + + @Test + public void testIsScope_String() { + for (int i = 0; i < 256; i++) { + boolean isScope = AclUtils.isScope(i + ""); + Assert.assertTrue(isScope); + } + boolean isScope = AclUtils.isScope("-1"); + Assert.assertFalse(isScope); + isScope = AclUtils.isScope("256"); + Assert.assertFalse(isScope); + } + + @Test + public void testIsScope_Integral() { + for (int i = 0; i < 256; i++) { + boolean isScope = AclUtils.isScope(i); + Assert.assertTrue(isScope); + } + boolean isScope = AclUtils.isScope(-1); + Assert.assertFalse(isScope); + isScope = AclUtils.isScope(256); + Assert.assertFalse(isScope); + + // IPv6 test + int min = Integer.parseInt("0", 16); + int max = Integer.parseInt("ffff", 16); + for (int i = min; i < max + 1; i++) { + isScope = AclUtils.isIPv6Scope(i); + Assert.assertTrue(isScope); + } + isScope = AclUtils.isIPv6Scope(-1); + Assert.assertFalse(isScope); + isScope = AclUtils.isIPv6Scope(max + 1); + Assert.assertFalse(isScope); + } + + @Test + public void testIsAsterisk() { + boolean isAsterisk = AclUtils.isAsterisk("*"); + Assert.assertTrue(isAsterisk); + + isAsterisk = AclUtils.isAsterisk(","); + Assert.assertFalse(isAsterisk); + } + + @Test + public void testIsComma() { + boolean isColon = AclUtils.isComma(","); + Assert.assertTrue(isColon); + + isColon = AclUtils.isComma("-"); + Assert.assertFalse(isColon); + } + + @Test + public void testIsMinus() { + boolean isMinus = AclUtils.isMinus("-"); + Assert.assertTrue(isMinus); + + isMinus = AclUtils.isMinus("*"); + Assert.assertFalse(isMinus); + } + + @Test + public void testV6ipProcess() { + String remoteAddr = "5::7:6:1-200:*"; + Assert.assertEquals(AclUtils.v6ipProcess(remoteAddr), "0005:0000:0000:0000:0007:0006"); + + remoteAddr = "5::7:6:1-200"; + Assert.assertEquals(AclUtils.v6ipProcess(remoteAddr), "0005:0000:0000:0000:0000:0007:0006"); + remoteAddr = "5::7:6:*"; + Assert.assertEquals(AclUtils.v6ipProcess(remoteAddr), "0005:0000:0000:0000:0000:0007:0006"); + + remoteAddr = "5:7:6:*"; + Assert.assertEquals(AclUtils.v6ipProcess(remoteAddr), "0005:0007:0006"); + } + + @Test + public void testExpandIP() { + Assert.assertEquals(AclUtils.expandIP("::", 8), "0000:0000:0000:0000:0000:0000:0000:0000"); + Assert.assertEquals(AclUtils.expandIP("::1", 8), "0000:0000:0000:0000:0000:0000:0000:0001"); + Assert.assertEquals(AclUtils.expandIP("3::", 8), "0003:0000:0000:0000:0000:0000:0000:0000"); + Assert.assertEquals(AclUtils.expandIP("2::2", 8), "0002:0000:0000:0000:0000:0000:0000:0002"); + Assert.assertEquals(AclUtils.expandIP("4::aac4:92", 8), "0004:0000:0000:0000:0000:0000:AAC4:0092"); + Assert.assertEquals(AclUtils.expandIP("ab23:56:901a::cc6:765:bb:9011", 8), "AB23:0056:901A:0000:0CC6:0765:00BB:9011"); + Assert.assertEquals(AclUtils.expandIP("ab23:56:901a:1:cc6:765:bb:9011", 8), "AB23:0056:901A:0001:0CC6:0765:00BB:9011"); + Assert.assertEquals(AclUtils.expandIP("5::7:6", 6), "0005:0000:0000:0000:0007:0006"); + } + + @SuppressWarnings("unchecked") + @Test + public void testGetYamlDataObject() throws IOException { + try (InputStream is = AclUtilsTest.class.getClassLoader().getResourceAsStream("conf/plain_acl_correct.yml")) { + Map map = AclUtils.getYamlDataObject(is, Map.class); + Assert.assertNotNull(map); + Assert.assertFalse(map.isEmpty()); + } + } + + private static String randomTmpFile() { + String tmpFileName = System.getProperty("java.io.tmpdir"); + // https://rationalpi.wordpress.com/2007/01/26/javaiotmpdir-inconsitency/ + if (!tmpFileName.endsWith(File.separator)) { + tmpFileName += File.separator; + } + tmpFileName += UUID.randomUUID() + ".yml"; + return tmpFileName; + } + + @Test + public void writeDataObject2YamlFileTest() throws IOException { + String targetFileName = randomTmpFile(); + File transport = new File(targetFileName); + Assert.assertTrue(transport.createNewFile()); + transport.deleteOnExit(); + + PlainAccessData aclYamlMap = new PlainAccessData(); + + // For globalWhiteRemoteAddrs element in acl yaml config file + List globalWhiteRemoteAddrs = new ArrayList<>(); + globalWhiteRemoteAddrs.add("10.10.103.*"); + globalWhiteRemoteAddrs.add("192.168.0.*"); + aclYamlMap.setGlobalWhiteRemoteAddresses(globalWhiteRemoteAddrs); + + // For accounts element in acl yaml config file + List accounts = new ArrayList<>(); + PlainAccessConfig accountsMap = new PlainAccessConfig() { + { + setAccessKey("RocketMQ"); + setSecretKey("12345678"); + setWhiteRemoteAddress("whiteRemoteAddress"); + setAdmin(true); + } + }; + accounts.add(accountsMap); + aclYamlMap.setAccounts(accounts); + Assert.assertTrue(AclUtils.writeDataObject(targetFileName, aclYamlMap)); + } + + @Test + public void updateExistedYamlFileTest() throws IOException { + String targetFileName = randomTmpFile(); + File transport = new File(targetFileName); + Assert.assertTrue(transport.createNewFile()); + transport.deleteOnExit(); + + PlainAccessData aclYamlMap = new PlainAccessData(); + + // For globalWhiteRemoteAddrs element in acl yaml config file + List globalWhiteRemoteAddrs = new ArrayList<>(); + globalWhiteRemoteAddrs.add("10.10.103.*"); + globalWhiteRemoteAddrs.add("192.168.0.*"); + aclYamlMap.setGlobalWhiteRemoteAddresses(globalWhiteRemoteAddrs); + + // Write file to yaml file + AclUtils.writeDataObject(targetFileName, aclYamlMap); + + PlainAccessData updatedMap = AclUtils.getYamlDataObject(targetFileName, PlainAccessData.class); + List globalWhiteRemoteAddrList = updatedMap.getGlobalWhiteRemoteAddresses(); + globalWhiteRemoteAddrList.clear(); + globalWhiteRemoteAddrList.add("192.168.1.2"); + + // Update file and flush to yaml file + AclUtils.writeDataObject(targetFileName, updatedMap); + + PlainAccessData readableMap = AclUtils.getYamlDataObject(targetFileName, PlainAccessData.class); + List updatedGlobalWhiteRemoteAddrs = readableMap.getGlobalWhiteRemoteAddresses(); + Assert.assertEquals("192.168.1.2", updatedGlobalWhiteRemoteAddrs.get(0)); + } + + @Test + public void getYamlDataIgnoreFileNotFoundExceptionTest() { + + JSONObject yamlDataObject = AclUtils.getYamlDataObject("plain_acl.yml", JSONObject.class); + Assert.assertNull(yamlDataObject); + } + + @Test + public void getAclRPCHookTest() throws IOException { + try (InputStream is = AclUtilsTest.class.getClassLoader().getResourceAsStream("conf/plain_acl_incomplete.yml")) { + RPCHook incompleteContRPCHook = AclUtils.getAclRPCHook(is); + Assert.assertNull(incompleteContRPCHook); + } + } +} diff --git a/acl/src/test/java/org/apache/rocketmq/acl/common/PermissionTest.java b/acl/src/test/java/org/apache/rocketmq/acl/common/PermissionTest.java new file mode 100644 index 00000000000..8fd8052c8a4 --- /dev/null +++ b/acl/src/test/java/org/apache/rocketmq/acl/common/PermissionTest.java @@ -0,0 +1,192 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.acl.common; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.rocketmq.acl.plain.PlainAccessResource; +import org.junit.Assert; +import org.junit.Test; + +public class PermissionTest { + + @Test + public void fromStringGetPermissionTest() { + byte perm = Permission.parsePermFromString("PUB"); + Assert.assertEquals(perm, Permission.PUB); + + perm = Permission.parsePermFromString("SUB"); + Assert.assertEquals(perm, Permission.SUB); + + perm = Permission.parsePermFromString("PUB|SUB"); + Assert.assertEquals(perm, Permission.PUB | Permission.SUB); + + perm = Permission.parsePermFromString("SUB|PUB"); + Assert.assertEquals(perm, Permission.PUB | Permission.SUB); + + perm = Permission.parsePermFromString("DENY"); + Assert.assertEquals(perm, Permission.DENY); + + perm = Permission.parsePermFromString("1"); + Assert.assertEquals(perm, Permission.DENY); + + perm = Permission.parsePermFromString(null); + Assert.assertEquals(perm, Permission.DENY); + + } + + @Test + public void checkPermissionTest() { + boolean boo = Permission.checkPermission(Permission.DENY, Permission.DENY); + Assert.assertFalse(boo); + + boo = Permission.checkPermission(Permission.PUB, Permission.PUB); + Assert.assertTrue(boo); + + boo = Permission.checkPermission(Permission.SUB, Permission.SUB); + Assert.assertTrue(boo); + + boo = Permission.checkPermission(Permission.PUB, (byte) (Permission.PUB | Permission.SUB)); + Assert.assertTrue(boo); + + boo = Permission.checkPermission(Permission.SUB, (byte) (Permission.PUB | Permission.SUB)); + Assert.assertTrue(boo); + + boo = Permission.checkPermission(Permission.ANY, (byte) (Permission.PUB | Permission.SUB)); + Assert.assertTrue(boo); + + boo = Permission.checkPermission(Permission.ANY, Permission.SUB); + Assert.assertTrue(boo); + + boo = Permission.checkPermission(Permission.ANY, Permission.PUB); + Assert.assertTrue(boo); + + boo = Permission.checkPermission(Permission.DENY, Permission.ANY); + Assert.assertFalse(boo); + + boo = Permission.checkPermission(Permission.DENY, Permission.PUB); + Assert.assertFalse(boo); + + boo = Permission.checkPermission(Permission.DENY, Permission.SUB); + Assert.assertFalse(boo); + + } + + @Test(expected = AclException.class) + public void setTopicPermTest() { + PlainAccessResource plainAccessResource = new PlainAccessResource(); + Map resourcePermMap = plainAccessResource.getResourcePermMap(); + + Permission.parseResourcePerms(plainAccessResource, false, null); + Assert.assertNull(resourcePermMap); + + List groups = new ArrayList<>(); + Permission.parseResourcePerms(plainAccessResource, false, groups); + Assert.assertNull(resourcePermMap); + + groups.add("groupA=DENY"); + groups.add("groupB=PUB|SUB"); + groups.add("groupC=PUB"); + Permission.parseResourcePerms(plainAccessResource, false, groups); + resourcePermMap = plainAccessResource.getResourcePermMap(); + + byte perm = resourcePermMap.get(PlainAccessResource.getRetryTopic("groupA")); + Assert.assertEquals(perm, Permission.DENY); + + perm = resourcePermMap.get(PlainAccessResource.getRetryTopic("groupB")); + Assert.assertEquals(perm,Permission.PUB | Permission.SUB); + + perm = resourcePermMap.get(PlainAccessResource.getRetryTopic("groupC")); + Assert.assertEquals(perm, Permission.PUB); + + List topics = new ArrayList<>(); + topics.add("topicA=DENY"); + topics.add("topicB=PUB|SUB"); + topics.add("topicC=PUB"); + + Permission.parseResourcePerms(plainAccessResource, true, topics); + + perm = resourcePermMap.get("topicA"); + Assert.assertEquals(perm, Permission.DENY); + + perm = resourcePermMap.get("topicB"); + Assert.assertEquals(perm, Permission.PUB | Permission.SUB); + + perm = resourcePermMap.get("topicC"); + Assert.assertEquals(perm, Permission.PUB); + + List erron = new ArrayList<>(); + erron.add(""); + Permission.parseResourcePerms(plainAccessResource, false, erron); + } + + @Test + public void checkAdminCodeTest() { + Set code = new HashSet<>(); + code.add(17); + code.add(25); + code.add(215); + code.add(200); + code.add(207); + + for (int i = 0; i < 400; i++) { + boolean boo = Permission.needAdminPerm(i); + if (boo) { + Assert.assertTrue(code.contains(i)); + } + } + } + + @Test + public void AclExceptionTest() { + AclException aclException = new AclException("CAL_SIGNATURE_FAILED",10015); + AclException aclExceptionWithMessage = new AclException("CAL_SIGNATURE_FAILED",10015,"CAL_SIGNATURE_FAILED Exception"); + Assert.assertEquals(aclException.getCode(),10015); + Assert.assertEquals(aclExceptionWithMessage.getStatus(),"CAL_SIGNATURE_FAILED"); + aclException.setCode(10016); + Assert.assertEquals(aclException.getCode(),10016); + aclException.setStatus("netAddress examine scope Exception netAddress"); + Assert.assertEquals(aclException.getStatus(),"netAddress examine scope Exception netAddress"); + } + + @Test + public void checkResourcePermsNormalTest() { + Permission.checkResourcePerms(null); + Permission.checkResourcePerms(new ArrayList<>()); + Permission.checkResourcePerms(Arrays.asList("topicA=PUB")); + Permission.checkResourcePerms(Arrays.asList("topicA=PUB", "topicB=SUB", "topicC=PUB|SUB")); + } + + @Test(expected = AclException.class) + public void checkResourcePermsExceptionTest1() { + Permission.checkResourcePerms(Arrays.asList("topicA")); + } + + @Test(expected = AclException.class) + public void checkResourcePermsExceptionTest2() { + Permission.checkResourcePerms(Arrays.asList("topicA=")); + } + + @Test(expected = AclException.class) + public void checkResourcePermsExceptionTest3() { + Permission.checkResourcePerms(Arrays.asList("topicA=DENY1")); + } +} diff --git a/acl/src/test/java/org/apache/rocketmq/acl/common/SessionCredentialsTest.java b/acl/src/test/java/org/apache/rocketmq/acl/common/SessionCredentialsTest.java new file mode 100644 index 00000000000..79512f14790 --- /dev/null +++ b/acl/src/test/java/org/apache/rocketmq/acl/common/SessionCredentialsTest.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.acl.common; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Properties; + +public class SessionCredentialsTest { + + @Test + public void equalsTest() { + SessionCredentials sessionCredentials = new SessionCredentials("RocketMQ","12345678"); + sessionCredentials.setSecurityToken("abcd"); + SessionCredentials other = new SessionCredentials("RocketMQ","12345678","abcd"); + Assert.assertTrue(sessionCredentials.equals(other)); + } + + @Test + public void updateContentTest() { + SessionCredentials sessionCredentials = new SessionCredentials(); + Properties properties = new Properties(); + properties.setProperty(SessionCredentials.ACCESS_KEY,"RocketMQ"); + properties.setProperty(SessionCredentials.SECRET_KEY,"12345678"); + properties.setProperty(SessionCredentials.SECURITY_TOKEN,"abcd"); + sessionCredentials.updateContent(properties); + } + + @Test + public void SessionCredentialHashCodeTest() { + SessionCredentials sessionCredentials = new SessionCredentials(); + Properties properties = new Properties(); + properties.setProperty(SessionCredentials.ACCESS_KEY,"RocketMQ"); + properties.setProperty(SessionCredentials.SECRET_KEY,"12345678"); + properties.setProperty(SessionCredentials.SECURITY_TOKEN,"abcd"); + sessionCredentials.updateContent(properties); + Assert.assertEquals(sessionCredentials.hashCode(),353652211); + } + + @Test + public void SessionCredentialEqualsTest() { + SessionCredentials sessionCredential1 = new SessionCredentials(); + Properties properties1 = new Properties(); + properties1.setProperty(SessionCredentials.ACCESS_KEY,"RocketMQ"); + properties1.setProperty(SessionCredentials.SECRET_KEY,"12345678"); + properties1.setProperty(SessionCredentials.SECURITY_TOKEN,"abcd"); + sessionCredential1.updateContent(properties1); + + SessionCredentials sessionCredential2 = new SessionCredentials(); + Properties properties2 = new Properties(); + properties2.setProperty(SessionCredentials.ACCESS_KEY,"RocketMQ"); + properties2.setProperty(SessionCredentials.SECRET_KEY,"12345678"); + properties2.setProperty(SessionCredentials.SECURITY_TOKEN,"abcd"); + sessionCredential2.updateContent(properties2); + + Assert.assertTrue(sessionCredential2.equals(sessionCredential1)); + sessionCredential2.setSecretKey("1234567899"); + sessionCredential2.setSignature("1234567899"); + Assert.assertFalse(sessionCredential2.equals(sessionCredential1)); + } + + @Test + public void SessionCredentialToStringTest() { + SessionCredentials sessionCredential1 = new SessionCredentials(); + Properties properties1 = new Properties(); + properties1.setProperty(SessionCredentials.ACCESS_KEY,"RocketMQ"); + properties1.setProperty(SessionCredentials.SECRET_KEY,"12345678"); + properties1.setProperty(SessionCredentials.SECURITY_TOKEN,"abcd"); + sessionCredential1.updateContent(properties1); + + Assert.assertEquals(sessionCredential1.toString(), + "SessionCredentials [accessKey=RocketMQ, secretKey=12345678, signature=null, SecurityToken=abcd]"); + } + + +} diff --git a/acl/src/test/java/org/apache/rocketmq/acl/plain/AclTestHelper.java b/acl/src/test/java/org/apache/rocketmq/acl/plain/AclTestHelper.java new file mode 100644 index 00000000000..a1bfc53ed51 --- /dev/null +++ b/acl/src/test/java/org/apache/rocketmq/acl/plain/AclTestHelper.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.acl.plain; + +import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.UUID; +import java.util.Iterator; +import org.junit.Assert; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; + +public final class AclTestHelper { + private AclTestHelper() { + } + + private static void copyTo(String path, InputStream src, File dstDir, String flag, boolean into) + throws IOException { + Preconditions.checkNotNull(flag); + Iterator iterator = Splitter.on(File.separatorChar).split(path).iterator(); + boolean found = false; + File dir = dstDir; + while (iterator.hasNext()) { + String current = iterator.next(); + if (!found && flag.equals(current)) { + found = true; + if (into) { + dir = new File(dir, flag); + if (!dir.exists()) { + Assert.assertTrue(dir.mkdirs()); + } + } + continue; + } + + if (found) { + if (!iterator.hasNext()) { + dir = new File(dir, current); + } else { + dir = new File(dir, current); + if (!dir.exists()) { + Assert.assertTrue(dir.mkdir()); + } + } + } + } + + Assert.assertTrue(dir.createNewFile()); + byte[] buffer = new byte[4096]; + BufferedInputStream bis = new BufferedInputStream(src); + int len = 0; + try (BufferedOutputStream bos = new BufferedOutputStream(Files.newOutputStream(dir.toPath()))) { + while ((len = bis.read(buffer)) > 0) { + bos.write(buffer, 0, len); + } + } + } + + public static void recursiveDelete(File file) { + if (file.isFile()) { + file.delete(); + } else { + File[] files = file.listFiles(); + if (null != files) { + for (File f : files) { + recursiveDelete(f); + } + } + file.delete(); + } + } + + public static File copyResources(String folder) throws IOException { + return copyResources(folder, false); + } + + public static File copyResources(String folder, boolean into) throws IOException { + File home = new File(System.getProperty("java.io.tmpdir"), UUID.randomUUID().toString().replace('-', '_')); + if (!home.exists()) { + Assert.assertTrue(home.mkdirs()); + } + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(AclTestHelper.class.getClassLoader()); + Resource[] resources = resolver.getResources(String.format("classpath:%s/**/*", folder)); + for (Resource resource : resources) { + if (!resource.isReadable()) { + continue; + } + String description = resource.getDescription(); + int start = description.indexOf('['); + int end = description.lastIndexOf(']'); + String path = description.substring(start + 1, end); + try (InputStream inputStream = resource.getInputStream()) { + copyTo(path, inputStream, home, folder, into); + } + } + return home; + } +} diff --git a/acl/src/test/java/org/apache/rocketmq/acl/plain/PlainAccessControlFlowTest.java b/acl/src/test/java/org/apache/rocketmq/acl/plain/PlainAccessControlFlowTest.java new file mode 100644 index 00000000000..5193457146d --- /dev/null +++ b/acl/src/test/java/org/apache/rocketmq/acl/plain/PlainAccessControlFlowTest.java @@ -0,0 +1,311 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.acl.plain; + +import org.apache.rocketmq.acl.common.AclClientRPCHook; +import org.apache.rocketmq.acl.common.AclConstants; +import org.apache.rocketmq.acl.common.AclException; +import org.apache.rocketmq.acl.common.AclUtils; +import org.apache.rocketmq.acl.common.SessionCredentials; +import org.apache.rocketmq.common.AclConfig; +import org.apache.rocketmq.common.PlainAccessConfig; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeaderV2; +import org.junit.Assert; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +/** + *

In this class, we'll test the following scenarios, each containing several consecutive operations on ACL, + *

like updating and deleting ACL, changing config files and checking validations. + *

Case 1: Only conf/plain_acl.yml exists; + *

Case 2: Only conf/acl/plain_acl.yml exists; + *

Case 3: Both conf/plain_acl.yml and conf/acl/plain_acl.yml exists. + */ +public class PlainAccessControlFlowTest { + public static final String DEFAULT_TOPIC = "topic-acl"; + + public static final String DEFAULT_GROUP = "GID_acl"; + + public static final String DEFAULT_PRODUCER_AK = "ak11111"; + public static final String DEFAULT_PRODUCER_SK = "1234567"; + + public static final String DEFAULT_CONSUMER_SK = "7654321"; + public static final String DEFAULT_CONSUMER_AK = "ak22222"; + + public static final String DEFAULT_GLOBAL_WHITE_ADDR = "172.16.123.123"; + public static final List DEFAULT_GLOBAL_WHITE_ADDRS_LIST = Collections.singletonList(DEFAULT_GLOBAL_WHITE_ADDR); + + @Test + public void testEmptyAclFolderCase() throws NoSuchFieldException, IllegalAccessException, + IOException { + String folder = "empty_acl_folder_conf"; + File home = AclTestHelper.copyResources(folder); + System.setProperty("rocketmq.home.dir", home.getAbsolutePath()); + PlainAccessValidator plainAccessValidator = new PlainAccessValidator(); + checkDefaultAclFileExists(); + testValidationAfterConsecutiveUpdates(plainAccessValidator); + testValidationAfterConfigFileChanged(plainAccessValidator); + AclTestHelper.recursiveDelete(home); + } + + @Test + public void testOnlyAclFolderCase() throws NoSuchFieldException, IllegalAccessException, IOException { + String folder = "only_acl_folder_conf"; + File home = AclTestHelper.copyResources(folder); + System.setProperty("rocketmq.home.dir", home.getAbsolutePath()); + PlainAccessValidator plainAccessValidator = new PlainAccessValidator(); + checkDefaultAclFileExists(); + testValidationAfterConsecutiveUpdates(plainAccessValidator); + testValidationAfterConfigFileChanged(plainAccessValidator); + AclTestHelper.recursiveDelete(home); + } + + @Test + public void testBothAclFileAndFolderCase() throws NoSuchFieldException, IllegalAccessException, + IOException { + String folder = "both_acl_file_folder_conf"; + File root = AclTestHelper.copyResources(folder); + System.setProperty("rocketmq.home.dir", root.getAbsolutePath()); + PlainAccessValidator plainAccessValidator = new PlainAccessValidator(); + checkDefaultAclFileExists(); + testValidationAfterConsecutiveUpdates(plainAccessValidator); + testValidationAfterConfigFileChanged(plainAccessValidator); + AclTestHelper.recursiveDelete(root); + } + + private void testValidationAfterConfigFileChanged( + PlainAccessValidator plainAccessValidator) throws NoSuchFieldException, IllegalAccessException { + PlainAccessConfig producerAccessConfig = generateProducerAccessConfig(); + PlainAccessConfig consumerAccessConfig = generateConsumerAccessConfig(); + List plainAccessConfigList = new LinkedList<>(); + plainAccessConfigList.add(producerAccessConfig); + plainAccessConfigList.add(consumerAccessConfig); + PlainAccessData ymlMap = new PlainAccessData(); + ymlMap.setAccounts(plainAccessConfigList); + + // write prepared PlainAccessConfigs to file + final String aclConfigFile = System.getProperty("rocketmq.home.dir") + File.separator + "conf/plain_acl.yml"; + AclUtils.writeDataObject(aclConfigFile, ymlMap); + + loadConfigFile(plainAccessValidator, aclConfigFile); + + // check if added successfully + final AclConfig allAclConfig = plainAccessValidator.getAllAclConfig(); + final List plainAccessConfigs = allAclConfig.getPlainAccessConfigs(); + checkPlainAccessConfig(producerAccessConfig, plainAccessConfigs); + checkPlainAccessConfig(consumerAccessConfig, plainAccessConfigs); + + //delete consumer account + plainAccessConfigList.remove(consumerAccessConfig); + AclUtils.writeDataObject(aclConfigFile, ymlMap); + + loadConfigFile(plainAccessValidator, aclConfigFile); + + // sending messages will be successful using prepared credentials + SessionCredentials producerCredential = new SessionCredentials(DEFAULT_PRODUCER_AK, DEFAULT_PRODUCER_SK); + AclClientRPCHook producerHook = new AclClientRPCHook(producerCredential); + validateSendMessage(RequestCode.SEND_MESSAGE, DEFAULT_TOPIC, producerHook, "", plainAccessValidator); + validateSendMessage(RequestCode.SEND_MESSAGE_V2, DEFAULT_TOPIC, producerHook, "", plainAccessValidator); + + // consuming messages will be failed for account has been deleted + SessionCredentials consumerCredential = new SessionCredentials(DEFAULT_CONSUMER_AK, DEFAULT_CONSUMER_SK); + AclClientRPCHook consumerHook = new AclClientRPCHook(consumerCredential); + boolean isConsumeFailed = false; + try { + validatePullMessage(DEFAULT_TOPIC, DEFAULT_GROUP, consumerHook, "", plainAccessValidator); + } catch (AclException e) { + isConsumeFailed = true; + } + Assert.assertTrue("Message should not be consumed after account deleted", isConsumeFailed); + + } + + private void testValidationAfterConsecutiveUpdates( + PlainAccessValidator plainAccessValidator) throws NoSuchFieldException, IllegalAccessException { + PlainAccessConfig producerAccessConfig = generateProducerAccessConfig(); + plainAccessValidator.updateAccessConfig(producerAccessConfig); + + PlainAccessConfig consumerAccessConfig = generateConsumerAccessConfig(); + plainAccessValidator.updateAccessConfig(consumerAccessConfig); + + plainAccessValidator.updateGlobalWhiteAddrsConfig(DEFAULT_GLOBAL_WHITE_ADDRS_LIST, null); + + // check if the above config updated successfully + final AclConfig allAclConfig = plainAccessValidator.getAllAclConfig(); + final List plainAccessConfigs = allAclConfig.getPlainAccessConfigs(); + checkPlainAccessConfig(producerAccessConfig, plainAccessConfigs); + checkPlainAccessConfig(consumerAccessConfig, plainAccessConfigs); + + Assert.assertEquals(DEFAULT_GLOBAL_WHITE_ADDRS_LIST, allAclConfig.getGlobalWhiteAddrs()); + + // check sending and consuming messages + SessionCredentials producerCredential = new SessionCredentials(DEFAULT_PRODUCER_AK, DEFAULT_PRODUCER_SK); + AclClientRPCHook producerHook = new AclClientRPCHook(producerCredential); + validateSendMessage(RequestCode.SEND_MESSAGE, DEFAULT_TOPIC, producerHook, "", plainAccessValidator); + validateSendMessage(RequestCode.SEND_MESSAGE_V2, DEFAULT_TOPIC, producerHook, "", plainAccessValidator); + + SessionCredentials consumerCredential = new SessionCredentials(DEFAULT_CONSUMER_AK, DEFAULT_CONSUMER_SK); + AclClientRPCHook consumerHook = new AclClientRPCHook(consumerCredential); + validatePullMessage(DEFAULT_TOPIC, DEFAULT_GROUP, consumerHook, "", plainAccessValidator); + + // load from file + loadConfigFile(plainAccessValidator, + System.getProperty("rocketmq.home.dir") + File.separator + "conf/plain_acl.yml"); + SessionCredentials unmatchedCredential = new SessionCredentials("non_exists_sk", "non_exists_sk"); + AclClientRPCHook dummyHook = new AclClientRPCHook(unmatchedCredential); + validateSendMessage(RequestCode.SEND_MESSAGE, DEFAULT_TOPIC, dummyHook, DEFAULT_GLOBAL_WHITE_ADDR, plainAccessValidator); + validateSendMessage(RequestCode.SEND_MESSAGE_V2, DEFAULT_TOPIC, dummyHook, DEFAULT_GLOBAL_WHITE_ADDR, plainAccessValidator); + validatePullMessage(DEFAULT_TOPIC, DEFAULT_GROUP, dummyHook, DEFAULT_GLOBAL_WHITE_ADDR, plainAccessValidator); + + //recheck after reloading + validateSendMessage(RequestCode.SEND_MESSAGE, DEFAULT_TOPIC, producerHook, "", plainAccessValidator); + validateSendMessage(RequestCode.SEND_MESSAGE_V2, DEFAULT_TOPIC, producerHook, "", plainAccessValidator); + validatePullMessage(DEFAULT_TOPIC, DEFAULT_GROUP, consumerHook, "", plainAccessValidator); + + } + + private void loadConfigFile(PlainAccessValidator plainAccessValidator, + String configFileName) throws NoSuchFieldException, IllegalAccessException { + Class clazz = PlainAccessValidator.class; + Field f = clazz.getDeclaredField("aclPlugEngine"); + f.setAccessible(true); + PlainPermissionManager aclPlugEngine = (PlainPermissionManager) f.get(plainAccessValidator); + aclPlugEngine.load(configFileName); + } + + private PlainAccessConfig generateConsumerAccessConfig() { + PlainAccessConfig plainAccessConfig2 = new PlainAccessConfig(); + plainAccessConfig2.setAccessKey(DEFAULT_CONSUMER_AK); + plainAccessConfig2.setSecretKey(DEFAULT_CONSUMER_SK); + plainAccessConfig2.setAdmin(false); + plainAccessConfig2.setDefaultTopicPerm(AclConstants.DENY); + plainAccessConfig2.setDefaultGroupPerm(AclConstants.DENY); + plainAccessConfig2.setTopicPerms(Collections.singletonList(DEFAULT_TOPIC + "=" + AclConstants.SUB)); + plainAccessConfig2.setGroupPerms(Collections.singletonList(DEFAULT_GROUP + "=" + AclConstants.SUB)); + return plainAccessConfig2; + } + + private PlainAccessConfig generateProducerAccessConfig() { + PlainAccessConfig plainAccessConfig = new PlainAccessConfig(); + plainAccessConfig.setAccessKey(DEFAULT_PRODUCER_AK); + plainAccessConfig.setSecretKey(DEFAULT_PRODUCER_SK); + plainAccessConfig.setAdmin(false); + plainAccessConfig.setDefaultTopicPerm(AclConstants.DENY); + plainAccessConfig.setDefaultGroupPerm(AclConstants.DENY); + plainAccessConfig.setTopicPerms(Collections.singletonList(DEFAULT_TOPIC + "=" + AclConstants.PUB)); + return plainAccessConfig; + } + + public void validatePullMessage(String topic, + String group, + AclClientRPCHook aclClientRPCHook, + String remoteAddr, + PlainAccessValidator plainAccessValidator) { + PullMessageRequestHeader pullMessageRequestHeader = new PullMessageRequestHeader(); + pullMessageRequestHeader.setTopic(topic); + pullMessageRequestHeader.setConsumerGroup(group); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, + pullMessageRequestHeader); + aclClientRPCHook.doBeforeRequest(remoteAddr, remotingCommand); + ByteBuffer buf = remotingCommand.encodeHeader(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse( + RemotingCommand.decode(buf), remoteAddr); + plainAccessValidator.validate(accessResource); + } catch (RemotingCommandException e) { + e.printStackTrace(); + Assert.fail("Should not throw RemotingCommandException"); + } + } + + public void validateSendMessage(int requestCode, + String topic, + AclClientRPCHook aclClientRPCHook, + String remoteAddr, + PlainAccessValidator plainAccessValidator) { + SendMessageRequestHeader messageRequestHeader = new SendMessageRequestHeader(); + messageRequestHeader.setTopic(topic); + RemotingCommand remotingCommand; + if (RequestCode.SEND_MESSAGE == requestCode) { + remotingCommand = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, messageRequestHeader); + } else { + remotingCommand = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE_V2, + SendMessageRequestHeaderV2.createSendMessageRequestHeaderV2(messageRequestHeader)); + } + + aclClientRPCHook.doBeforeRequest(remoteAddr, remotingCommand); + + ByteBuffer buf = remotingCommand.encodeHeader(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse( + RemotingCommand.decode(buf), remoteAddr); + plainAccessValidator.validate(accessResource); + } catch (RemotingCommandException e) { + e.printStackTrace(); + Assert.fail("Should not throw RemotingCommandException"); + } + } + + private void checkPlainAccessConfig(final PlainAccessConfig plainAccessConfig, + final List plainAccessConfigs) { + for (PlainAccessConfig config : plainAccessConfigs) { + if (config.getAccessKey().equals(plainAccessConfig.getAccessKey())) { + Assert.assertEquals(plainAccessConfig.getSecretKey(), config.getSecretKey()); + Assert.assertEquals(plainAccessConfig.isAdmin(), config.isAdmin()); + Assert.assertEquals(plainAccessConfig.getDefaultGroupPerm(), config.getDefaultGroupPerm()); + Assert.assertEquals(plainAccessConfig.getDefaultGroupPerm(), config.getDefaultGroupPerm()); + Assert.assertEquals(plainAccessConfig.getWhiteRemoteAddress(), config.getWhiteRemoteAddress()); + if (null != plainAccessConfig.getTopicPerms()) { + Assert.assertNotNull(config.getTopicPerms()); + Assert.assertTrue(config.getTopicPerms().containsAll(plainAccessConfig.getTopicPerms())); + } + if (null != plainAccessConfig.getGroupPerms()) { + Assert.assertNotNull(config.getGroupPerms()); + Assert.assertTrue(config.getGroupPerms().containsAll(plainAccessConfig.getGroupPerms())); + } + } + } + } + + private void checkDefaultAclFileExists() { + boolean isExists = Files.exists(Paths.get(System.getProperty("rocketmq.home.dir") + + File.separator + "conf" + File.separator + "plain_acl.yml")); + Assert.assertTrue("default acl config file should exist", isExists); + } + +} diff --git a/acl/src/test/java/org/apache/rocketmq/acl/plain/PlainAccessValidatorTest.java b/acl/src/test/java/org/apache/rocketmq/acl/plain/PlainAccessValidatorTest.java new file mode 100644 index 00000000000..ef0cffbdcc8 --- /dev/null +++ b/acl/src/test/java/org/apache/rocketmq/acl/plain/PlainAccessValidatorTest.java @@ -0,0 +1,1112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.acl.plain; + +import com.google.common.base.Joiner; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.rocketmq.acl.common.AclClientRPCHook; +import org.apache.rocketmq.acl.common.AclConstants; +import org.apache.rocketmq.acl.common.AclException; +import org.apache.rocketmq.acl.common.AclUtils; +import org.apache.rocketmq.acl.common.SessionCredentials; +import org.apache.rocketmq.common.AclConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.PlainAccessConfig; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.header.ConsumerSendMsgBackRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerListByGroupRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeaderV2; +import org.apache.rocketmq.remoting.protocol.header.UnregisterClientRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateConsumerOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumerData; +import org.apache.rocketmq.remoting.protocol.heartbeat.HeartbeatData; +import org.apache.rocketmq.remoting.protocol.heartbeat.ProducerData; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class PlainAccessValidatorTest { + + private PlainAccessValidator plainAccessValidator; + private AclClientRPCHook aclClient; + private SessionCredentials sessionCredentials; + + private File confHome; + + @Before + public void init() throws IOException { + String folder = "conf"; + confHome = AclTestHelper.copyResources(folder, true); + System.setProperty("rocketmq.home.dir", confHome.getAbsolutePath()); + plainAccessValidator = new PlainAccessValidator(); + sessionCredentials = new SessionCredentials(); + sessionCredentials.setAccessKey("RocketMQ"); + sessionCredentials.setSecretKey("12345678"); + sessionCredentials.setSecurityToken("87654321"); + aclClient = new AclClientRPCHook(sessionCredentials); + } + + @After + public void cleanUp() { + AclTestHelper.recursiveDelete(confHome); + } + + @Test + public void contentTest() { + SendMessageRequestHeader messageRequestHeader = new SendMessageRequestHeader(); + messageRequestHeader.setTopic("topicA"); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, messageRequestHeader); + aclClient.doBeforeRequest("", remotingCommand); + + ByteBuffer buf = remotingCommand.encodeHeader(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse(RemotingCommand.decode(buf), "127.0.0.1"); + String signature = AclUtils.calSignature(accessResource.getContent(), sessionCredentials.getSecretKey()); + + Assert.assertEquals(accessResource.getSignature(), signature); + } catch (RemotingCommandException e) { + e.printStackTrace(); + + Assert.fail("Should not throw IOException"); + } + + } + + @Test + public void validateTest() { + SendMessageRequestHeader messageRequestHeader = new SendMessageRequestHeader(); + messageRequestHeader.setTopic("topicB"); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, messageRequestHeader); + aclClient.doBeforeRequest("", remotingCommand); + + ByteBuffer buf = remotingCommand.encodeHeader(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse(RemotingCommand.decode(buf), "123.4.5.6"); + plainAccessValidator.validate(accessResource); + } catch (RemotingCommandException e) { + e.printStackTrace(); + + Assert.fail("Should not throw IOException"); + } + + } + + @Test + public void validateSendMessageTest() { + SendMessageRequestHeader messageRequestHeader = new SendMessageRequestHeader(); + messageRequestHeader.setTopic("topicB"); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, messageRequestHeader); + aclClient.doBeforeRequest("", remotingCommand); + + ByteBuffer buf = remotingCommand.encodeHeader(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse(RemotingCommand.decode(buf), "123.4.5.6"); + plainAccessValidator.validate(accessResource); + } catch (RemotingCommandException e) { + e.printStackTrace(); + + Assert.fail("Should not throw IOException"); + } + } + + @Test + public void validateSendMessageToRetryTopicTest() { + SendMessageRequestHeader messageRequestHeader = new SendMessageRequestHeader(); + messageRequestHeader.setTopic(MixAll.getRetryTopic("groupB")); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, messageRequestHeader); + aclClient.doBeforeRequest("", remotingCommand); + + ByteBuffer buf = remotingCommand.encodeHeader(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse(RemotingCommand.decode(buf), "123.4.5.6"); + plainAccessValidator.validate(accessResource); + } catch (RemotingCommandException e) { + e.printStackTrace(); + + Assert.fail("Should not throw IOException"); + } + } + + @Test + public void validateSendMessageV2Test() { + SendMessageRequestHeader messageRequestHeader = new SendMessageRequestHeader(); + messageRequestHeader.setTopic("topicB"); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE_V2, SendMessageRequestHeaderV2.createSendMessageRequestHeaderV2(messageRequestHeader)); + aclClient.doBeforeRequest("", remotingCommand); + + ByteBuffer buf = remotingCommand.encodeHeader(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse(RemotingCommand.decode(buf), "123.4.5.6"); + plainAccessValidator.validate(accessResource); + } catch (RemotingCommandException e) { + e.printStackTrace(); + + Assert.fail("Should not throw IOException"); + } + } + + @Test + public void validateSendMessageV2ToRetryTopicTest() { + SendMessageRequestHeader messageRequestHeader = new SendMessageRequestHeader(); + messageRequestHeader.setTopic(MixAll.getRetryTopic("groupC")); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE_V2, SendMessageRequestHeaderV2.createSendMessageRequestHeaderV2(messageRequestHeader)); + aclClient.doBeforeRequest("", remotingCommand); + + ByteBuffer buf = remotingCommand.encodeHeader(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse(RemotingCommand.decode(buf), "123.4.5.6:9876"); + plainAccessValidator.validate(accessResource); + } catch (RemotingCommandException e) { + e.printStackTrace(); + + Assert.fail("Should not throw IOException"); + } + } + + @Test + public void validateForAdminCommandWithOutAclRPCHook() { + RemotingCommand consumerOffsetAdminRequest = RemotingCommand.createRequestCommand(RequestCode.GET_ALL_CONSUMER_OFFSET, null); + plainAccessValidator.parse(consumerOffsetAdminRequest, "192.168.0.1:9876"); + + RemotingCommand subscriptionGroupAdminRequest = RemotingCommand.createRequestCommand(RequestCode.GET_ALL_SUBSCRIPTIONGROUP_CONFIG, null); + plainAccessValidator.parse(subscriptionGroupAdminRequest, "192.168.0.1:9876"); + + RemotingCommand delayOffsetAdminRequest = RemotingCommand.createRequestCommand(RequestCode.GET_ALL_DELAY_OFFSET, null); + plainAccessValidator.parse(delayOffsetAdminRequest, "192.168.0.1:9876"); + + RemotingCommand allTopicConfigAdminRequest = RemotingCommand.createRequestCommand(RequestCode.GET_ALL_TOPIC_CONFIG, null); + plainAccessValidator.parse(allTopicConfigAdminRequest, "192.168.0.1:9876"); + + } + + @Test + public void validatePullMessageTest() { + PullMessageRequestHeader pullMessageRequestHeader = new PullMessageRequestHeader(); + pullMessageRequestHeader.setTopic("topicC"); + pullMessageRequestHeader.setConsumerGroup("groupC"); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, pullMessageRequestHeader); + aclClient.doBeforeRequest("", remotingCommand); + ByteBuffer buf = remotingCommand.encodeHeader(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse(RemotingCommand.decode(buf), "123.4.5.6"); + plainAccessValidator.validate(accessResource); + } catch (RemotingCommandException e) { + e.printStackTrace(); + + Assert.fail("Should not throw IOException"); + } + } + + @Test + public void validateConsumeMessageBackTest() { + ConsumerSendMsgBackRequestHeader consumerSendMsgBackRequestHeader = new ConsumerSendMsgBackRequestHeader(); + consumerSendMsgBackRequestHeader.setOriginTopic("topicC"); + consumerSendMsgBackRequestHeader.setGroup("groupC"); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.CONSUMER_SEND_MSG_BACK, consumerSendMsgBackRequestHeader); + aclClient.doBeforeRequest("", remotingCommand); + ByteBuffer buf = remotingCommand.encodeHeader(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse(RemotingCommand.decode(buf), "123.4.5.6"); + plainAccessValidator.validate(accessResource); + } catch (RemotingCommandException e) { + e.printStackTrace(); + + Assert.fail("Should not throw IOException"); + } + } + + @Test + public void validateQueryMessageTest() { + QueryMessageRequestHeader queryMessageRequestHeader = new QueryMessageRequestHeader(); + queryMessageRequestHeader.setTopic("topicC"); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.QUERY_MESSAGE, queryMessageRequestHeader); + aclClient.doBeforeRequest("", remotingCommand); + ByteBuffer buf = remotingCommand.encodeHeader(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse(RemotingCommand.decode(buf), "123.4.5.6"); + plainAccessValidator.validate(accessResource); + } catch (RemotingCommandException e) { + e.printStackTrace(); + + Assert.fail("Should not throw IOException"); + } + } + + @Test + public void validateQueryMessageByKeyTest() { + QueryMessageRequestHeader queryMessageRequestHeader = new QueryMessageRequestHeader(); + queryMessageRequestHeader.setTopic("topicC"); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.QUERY_MESSAGE, queryMessageRequestHeader); + aclClient.doBeforeRequest("", remotingCommand); + remotingCommand.addExtField(MixAll.UNIQUE_MSG_QUERY_FLAG, "false"); + ByteBuffer buf = remotingCommand.encodeHeader(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse(RemotingCommand.decode(buf), "192.168.1.1:9876"); + plainAccessValidator.validate(accessResource); + } catch (RemotingCommandException e) { + e.printStackTrace(); + + Assert.fail("Should not throw IOException"); + } + } + + @Test + public void validateHeartBeatTest() { + HeartbeatData heartbeatData = new HeartbeatData(); + Set producerDataSet = new HashSet<>(); + Set consumerDataSet = new HashSet<>(); + Set subscriptionDataSet = new HashSet<>(); + ProducerData producerData = new ProducerData(); + producerData.setGroupName("groupB"); + ConsumerData consumerData = new ConsumerData(); + consumerData.setGroupName("groupC"); + SubscriptionData subscriptionData = new SubscriptionData(); + subscriptionData.setTopic("topicC"); + producerDataSet.add(producerData); + consumerDataSet.add(consumerData); + subscriptionDataSet.add(subscriptionData); + consumerData.setSubscriptionDataSet(subscriptionDataSet); + heartbeatData.setProducerDataSet(producerDataSet); + heartbeatData.setConsumerDataSet(consumerDataSet); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.HEART_BEAT, null); + remotingCommand.setBody(heartbeatData.encode()); + aclClient.doBeforeRequest("", remotingCommand); + ByteBuffer buf = remotingCommand.encode(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse(RemotingCommand.decode(buf), "123.4.5.6"); + plainAccessValidator.validate(accessResource); + } catch (RemotingCommandException e) { + e.printStackTrace(); + + Assert.fail("Should not throw IOException"); + } + } + + @Test + public void validateUnRegisterClientTest() { + UnregisterClientRequestHeader unregisterClientRequestHeader = new UnregisterClientRequestHeader(); + unregisterClientRequestHeader.setConsumerGroup("groupB"); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.UNREGISTER_CLIENT, unregisterClientRequestHeader); + aclClient.doBeforeRequest("", remotingCommand); + ByteBuffer buf = remotingCommand.encodeHeader(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse(RemotingCommand.decode(buf), "123.4.5.6"); + plainAccessValidator.validate(accessResource); + } catch (RemotingCommandException e) { + e.printStackTrace(); + + Assert.fail("Should not throw IOException"); + } + } + + @Test + public void validateGetConsumerListByGroupTest() { + GetConsumerListByGroupRequestHeader getConsumerListByGroupRequestHeader = new GetConsumerListByGroupRequestHeader(); + getConsumerListByGroupRequestHeader.setConsumerGroup("groupB"); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.GET_CONSUMER_LIST_BY_GROUP, getConsumerListByGroupRequestHeader); + aclClient.doBeforeRequest("", remotingCommand); + ByteBuffer buf = remotingCommand.encodeHeader(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse(RemotingCommand.decode(buf), "123.4.5.6"); + plainAccessValidator.validate(accessResource); + } catch (RemotingCommandException e) { + e.printStackTrace(); + + Assert.fail("Should not throw IOException"); + } + } + + @Test + public void validateUpdateConsumerOffSetTest() { + UpdateConsumerOffsetRequestHeader updateConsumerOffsetRequestHeader = new UpdateConsumerOffsetRequestHeader(); + updateConsumerOffsetRequestHeader.setConsumerGroup("groupB"); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.UPDATE_CONSUMER_OFFSET, updateConsumerOffsetRequestHeader); + aclClient.doBeforeRequest("", remotingCommand); + ByteBuffer buf = remotingCommand.encodeHeader(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse(RemotingCommand.decode(buf), "123.4.5.6"); + plainAccessValidator.validate(accessResource); + } catch (RemotingCommandException e) { + e.printStackTrace(); + + Assert.fail("Should not throw IOException"); + } + } + + @Test(expected = AclException.class) + public void validateNullAccessKeyTest() { + SessionCredentials sessionCredentials = new SessionCredentials(); + sessionCredentials.setAccessKey("RocketMQ1"); + sessionCredentials.setSecretKey("1234"); + AclClientRPCHook aclClientRPCHook = new AclClientRPCHook(sessionCredentials); + SendMessageRequestHeader messageRequestHeader = new SendMessageRequestHeader(); + messageRequestHeader.setTopic("topicB"); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, messageRequestHeader); + aclClientRPCHook.doBeforeRequest("", remotingCommand); + + ByteBuffer buf = remotingCommand.encodeHeader(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse(RemotingCommand.decode(buf), "192.168.1.1"); + plainAccessValidator.validate(accessResource); + } catch (RemotingCommandException e) { + e.printStackTrace(); + + Assert.fail("Should not throw IOException"); + } + } + + @Test(expected = AclException.class) + public void validateErrorSecretKeyTest() { + SessionCredentials sessionCredentials = new SessionCredentials(); + sessionCredentials.setAccessKey("RocketMQ"); + sessionCredentials.setSecretKey("1234"); + AclClientRPCHook aclClientRPCHook = new AclClientRPCHook(sessionCredentials); + SendMessageRequestHeader messageRequestHeader = new SendMessageRequestHeader(); + messageRequestHeader.setTopic("topicB"); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, messageRequestHeader); + aclClientRPCHook.doBeforeRequest("", remotingCommand); + + ByteBuffer buf = remotingCommand.encodeHeader(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse(RemotingCommand.decode(buf), "192.168.1.1"); + plainAccessValidator.validate(accessResource); + } catch (RemotingCommandException e) { + e.printStackTrace(); + + Assert.fail("Should not throw IOException"); + } + } + + @Test + public void validateGetAllTopicConfigTest() { + String whiteRemoteAddress = "192.168.0.1"; + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.GET_ALL_TOPIC_CONFIG, null); + + ByteBuffer buf = remotingCommand.encodeHeader(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse(RemotingCommand.decode(buf), whiteRemoteAddress); + plainAccessValidator.validate(accessResource); + } catch (RemotingCommandException e) { + e.printStackTrace(); + + Assert.fail("Should not throw IOException"); + } + } + + @Test + public void addAccessAclYamlConfigTest() throws InterruptedException { + String backupFileName = System.getProperty("rocketmq.home.dir") + + File.separator + "conf/plain_acl_bak.yml".replace("/", File.separator); + String targetFileName = System.getProperty("rocketmq.home.dir") + + File.separator + "conf/plain_acl.yml".replace("/", File.separator); + PlainAccessData backUpAclConfigMap = AclUtils.getYamlDataObject(backupFileName, PlainAccessData.class); + AclUtils.writeDataObject(targetFileName, backUpAclConfigMap); + + PlainAccessConfig plainAccessConfig = new PlainAccessConfig(); + plainAccessConfig.setAccessKey("rocketmq3"); + plainAccessConfig.setSecretKey("1234567890"); + plainAccessConfig.setWhiteRemoteAddress("192.168.0.*"); + plainAccessConfig.setDefaultGroupPerm("PUB"); + plainAccessConfig.setDefaultTopicPerm("SUB"); + List topicPerms = new ArrayList<>(); + topicPerms.add("topicC=PUB|SUB"); + topicPerms.add("topicB=PUB"); + plainAccessConfig.setTopicPerms(topicPerms); + List groupPerms = new ArrayList<>(); + groupPerms.add("groupB=PUB|SUB"); + groupPerms.add("groupC=DENY"); + plainAccessConfig.setGroupPerms(groupPerms); + + PlainAccessValidator plainAccessValidator = new PlainAccessValidator(); + plainAccessValidator.updateAccessConfig(plainAccessConfig); + Thread.sleep(10000); + + Map verifyMap = new HashMap<>(); + AclConfig aclConfig = plainAccessValidator.getAllAclConfig(); + List plainAccessConfigs = aclConfig.getPlainAccessConfigs(); + for (PlainAccessConfig plainAccessConfig1 : plainAccessConfigs) { + if (plainAccessConfig1.getAccessKey().equals(plainAccessConfig.getAccessKey())) { + verifyMap.put(AclConstants.CONFIG_SECRET_KEY, plainAccessConfig1.getSecretKey()); + verifyMap.put(AclConstants.CONFIG_DEFAULT_TOPIC_PERM, plainAccessConfig1.getDefaultTopicPerm()); + verifyMap.put(AclConstants.CONFIG_DEFAULT_GROUP_PERM, plainAccessConfig1.getDefaultGroupPerm()); + verifyMap.put(AclConstants.CONFIG_ADMIN_ROLE, plainAccessConfig1.isAdmin()); + verifyMap.put(AclConstants.CONFIG_WHITE_ADDR, plainAccessConfig1.getWhiteRemoteAddress()); + verifyMap.put(AclConstants.CONFIG_TOPIC_PERMS, plainAccessConfig1.getTopicPerms()); + verifyMap.put(AclConstants.CONFIG_GROUP_PERMS, plainAccessConfig1.getGroupPerms()); + } + } + + Assert.assertEquals(verifyMap.get(AclConstants.CONFIG_SECRET_KEY), "1234567890"); + Assert.assertEquals(verifyMap.get(AclConstants.CONFIG_DEFAULT_TOPIC_PERM), "SUB"); + Assert.assertEquals(verifyMap.get(AclConstants.CONFIG_DEFAULT_GROUP_PERM), "PUB"); + Assert.assertEquals(verifyMap.get(AclConstants.CONFIG_ADMIN_ROLE), false); + Assert.assertEquals(verifyMap.get(AclConstants.CONFIG_WHITE_ADDR), "192.168.0.*"); + Assert.assertEquals(((List) verifyMap.get(AclConstants.CONFIG_TOPIC_PERMS)).size(), 2); + Assert.assertEquals(((List) verifyMap.get(AclConstants.CONFIG_GROUP_PERMS)).size(), 2); + + String aclFileName = System.getProperty("rocketmq.home.dir") + File.separator + "conf/plain_acl.yml"; + PlainAccessData readableMap = AclUtils.getYamlDataObject(aclFileName, PlainAccessData.class); + List dataVersions = readableMap.getDataVersion(); + Assert.assertEquals(1L, dataVersions.get(0).getCounter()); + + AclUtils.writeDataObject(targetFileName, backUpAclConfigMap); + } + + @Test + public void getAccessAclYamlConfigTest() { + String accessKey = "rocketmq2"; + PlainAccessValidator plainAccessValidator = new PlainAccessValidator(); + AclConfig aclConfig = plainAccessValidator.getAllAclConfig(); + List plainAccessConfigs = aclConfig.getPlainAccessConfigs(); + Map verifyMap = new HashMap<>(); + for (PlainAccessConfig plainAccessConfig : plainAccessConfigs) { + if (plainAccessConfig.getAccessKey().equals(accessKey)) { + verifyMap.put(AclConstants.CONFIG_SECRET_KEY, plainAccessConfig.getSecretKey()); + verifyMap.put(AclConstants.CONFIG_ADMIN_ROLE, plainAccessConfig.isAdmin()); + verifyMap.put(AclConstants.CONFIG_WHITE_ADDR, plainAccessConfig.getWhiteRemoteAddress()); + } + } + + Assert.assertEquals(verifyMap.get(AclConstants.CONFIG_SECRET_KEY), "12345678"); + Assert.assertEquals(verifyMap.get(AclConstants.CONFIG_ADMIN_ROLE), true); + Assert.assertEquals(verifyMap.get(AclConstants.CONFIG_WHITE_ADDR), "192.168.1.*"); + + String aclFileName = System.getProperty("rocketmq.home.dir") + + File.separator + "conf/plain_acl.yml".replace("/", File.separator); + Map dataVersionMap = plainAccessValidator.getAllAclConfigVersion(); + DataVersion dataVersion = dataVersionMap.get(aclFileName); + Assert.assertEquals(0, dataVersion.getCounter().get()); + } + + @Test + public void updateAccessAclYamlConfigTest() throws InterruptedException { + String backupFileName = System.getProperty("rocketmq.home.dir") + + File.separator + "conf/plain_acl_bak.yml".replace("/", File.separator); + String targetFileName = System.getProperty("rocketmq.home.dir") + + File.separator + "conf/plain_acl.yml".replace("/", File.separator); + PlainAccessData backUpAclConfigMap = AclUtils.getYamlDataObject(backupFileName, PlainAccessData.class); + AclUtils.writeDataObject(targetFileName, backUpAclConfigMap); + + PlainAccessConfig plainAccessConfig = new PlainAccessConfig(); + plainAccessConfig.setAccessKey("rocketmq3"); + plainAccessConfig.setSecretKey("1234567890"); + plainAccessConfig.setWhiteRemoteAddress("192.168.0.*"); + plainAccessConfig.setDefaultGroupPerm("PUB"); + plainAccessConfig.setDefaultTopicPerm("SUB"); + List topicPerms = new ArrayList<>(); + topicPerms.add("topicC=PUB|SUB"); + topicPerms.add("topicB=PUB"); + plainAccessConfig.setTopicPerms(topicPerms); + List groupPerms = new ArrayList<>(); + groupPerms.add("groupB=PUB|SUB"); + groupPerms.add("groupC=DENY"); + plainAccessConfig.setGroupPerms(groupPerms); + + PlainAccessValidator plainAccessValidator = new PlainAccessValidator(); + plainAccessValidator.updateAccessConfig(plainAccessConfig); + + Thread.sleep(10000); + + PlainAccessConfig plainAccessConfig1 = new PlainAccessConfig(); + plainAccessConfig1.setAccessKey("rocketmq3"); + plainAccessConfig1.setSecretKey("1234567891"); + plainAccessConfig1.setWhiteRemoteAddress("192.168.0.*"); + plainAccessConfig1.setDefaultGroupPerm("PUB"); + plainAccessConfig1.setDefaultTopicPerm("SUB"); + List topicPerms1 = new ArrayList<>(); + topicPerms1.add("topicC=PUB|SUB"); + topicPerms1.add("topicB=PUB"); + plainAccessConfig1.setTopicPerms(topicPerms1); + List groupPerms1 = new ArrayList<>(); + groupPerms1.add("groupB=PUB|SUB"); + groupPerms1.add("groupC=DENY"); + plainAccessConfig1.setGroupPerms(groupPerms1); + + plainAccessValidator.updateAccessConfig(plainAccessConfig1); + + Thread.sleep(10000); + + Map verifyMap = new HashMap<>(); + AclConfig aclConfig = plainAccessValidator.getAllAclConfig(); + List plainAccessConfigs = aclConfig.getPlainAccessConfigs(); + for (PlainAccessConfig plainAccessConfig2 : plainAccessConfigs) { + if (plainAccessConfig2.getAccessKey().equals(plainAccessConfig1.getAccessKey())) { + verifyMap.put(AclConstants.CONFIG_SECRET_KEY, plainAccessConfig2.getSecretKey()); + verifyMap.put(AclConstants.CONFIG_DEFAULT_TOPIC_PERM, plainAccessConfig2.getDefaultTopicPerm()); + verifyMap.put(AclConstants.CONFIG_DEFAULT_GROUP_PERM, plainAccessConfig2.getDefaultGroupPerm()); + verifyMap.put(AclConstants.CONFIG_ADMIN_ROLE, plainAccessConfig2.isAdmin()); + verifyMap.put(AclConstants.CONFIG_WHITE_ADDR, plainAccessConfig2.getWhiteRemoteAddress()); + verifyMap.put(AclConstants.CONFIG_TOPIC_PERMS, plainAccessConfig2.getTopicPerms()); + verifyMap.put(AclConstants.CONFIG_GROUP_PERMS, plainAccessConfig2.getGroupPerms()); + } + } + + Assert.assertEquals(verifyMap.get(AclConstants.CONFIG_SECRET_KEY), "1234567891"); + Assert.assertEquals(verifyMap.get(AclConstants.CONFIG_DEFAULT_TOPIC_PERM), "SUB"); + Assert.assertEquals(verifyMap.get(AclConstants.CONFIG_DEFAULT_GROUP_PERM), "PUB"); + Assert.assertEquals(verifyMap.get(AclConstants.CONFIG_ADMIN_ROLE), false); + Assert.assertEquals(verifyMap.get(AclConstants.CONFIG_WHITE_ADDR), "192.168.0.*"); + Assert.assertEquals(((List) verifyMap.get(AclConstants.CONFIG_TOPIC_PERMS)).size(), 2); + Assert.assertEquals(((List) verifyMap.get(AclConstants.CONFIG_GROUP_PERMS)).size(), 2); + + String aclFileName = System.getProperty("rocketmq.home.dir") + + File.separator + "conf/plain_acl.yml".replace("/", File.separator); + PlainAccessData readableMap = AclUtils.getYamlDataObject(aclFileName, PlainAccessData.class); + List dataVersions = readableMap.getDataVersion(); + Assert.assertEquals(2L, dataVersions.get(0).getCounter()); + + AclUtils.writeDataObject(targetFileName, backUpAclConfigMap); + } + + @Test + public void deleteAccessAclYamlConfigTest() throws InterruptedException { + String backupFileName = System.getProperty("rocketmq.home.dir") + + File.separator + "conf/plain_acl_bak.yml".replace("/", File.separator); + String targetFileName = System.getProperty("rocketmq.home.dir") + + File.separator + "conf/plain_acl.yml".replace("/", File.separator); + PlainAccessData backUpAclConfigMap = AclUtils.getYamlDataObject(backupFileName, PlainAccessData.class); + AclUtils.writeDataObject(targetFileName, backUpAclConfigMap); + + PlainAccessConfig plainAccessConfig = new PlainAccessConfig(); + plainAccessConfig.setAccessKey("rocketmq3"); + plainAccessConfig.setSecretKey("1234567890"); + plainAccessConfig.setWhiteRemoteAddress("192.168.0.*"); + plainAccessConfig.setDefaultGroupPerm("PUB"); + plainAccessConfig.setDefaultTopicPerm("SUB"); + List topicPerms = new ArrayList<>(); + topicPerms.add("topicC=PUB|SUB"); + topicPerms.add("topicB=PUB"); + plainAccessConfig.setTopicPerms(topicPerms); + List groupPerms = new ArrayList<>(); + groupPerms.add("groupB=PUB|SUB"); + groupPerms.add("groupC=DENY"); + plainAccessConfig.setGroupPerms(groupPerms); + + PlainAccessValidator plainAccessValidator = new PlainAccessValidator(); + plainAccessValidator.updateAccessConfig(plainAccessConfig); + + String accessKey = "rocketmq3"; + plainAccessValidator.deleteAccessConfig(accessKey); + Thread.sleep(10000); + + Map verifyMap = new HashMap<>(); + AclConfig aclConfig = plainAccessValidator.getAllAclConfig(); + List plainAccessConfigs = aclConfig.getPlainAccessConfigs(); + for (PlainAccessConfig plainAccessConfig1 : plainAccessConfigs) { + if (plainAccessConfig1.getAccessKey().equals(accessKey)) { + verifyMap.put(AclConstants.CONFIG_SECRET_KEY, plainAccessConfig1.getSecretKey()); + verifyMap.put(AclConstants.CONFIG_DEFAULT_TOPIC_PERM, plainAccessConfig1.getDefaultTopicPerm()); + verifyMap.put(AclConstants.CONFIG_DEFAULT_GROUP_PERM, plainAccessConfig1.getDefaultGroupPerm()); + verifyMap.put(AclConstants.CONFIG_ADMIN_ROLE, plainAccessConfig1.isAdmin()); + verifyMap.put(AclConstants.CONFIG_WHITE_ADDR, plainAccessConfig1.getWhiteRemoteAddress()); + verifyMap.put(AclConstants.CONFIG_TOPIC_PERMS, plainAccessConfig1.getTopicPerms()); + verifyMap.put(AclConstants.CONFIG_GROUP_PERMS, plainAccessConfig1.getGroupPerms()); + } + } + + Assert.assertEquals(verifyMap.size(), 0); + + AclUtils.writeDataObject(targetFileName, backUpAclConfigMap); + } + + @Test + public void updateGlobalWhiteRemoteAddressesTest() throws InterruptedException { + String backupFileName = System.getProperty("rocketmq.home.dir") + + File.separator + "conf/plain_acl_bak.yml".replace("/", File.separator); + String targetFileName = System.getProperty("rocketmq.home.dir") + + File.separator + "conf/plain_acl.yml".replace("/", File.separator); + PlainAccessData backUpAclConfigMap = AclUtils.getYamlDataObject(backupFileName, PlainAccessData.class); + AclUtils.writeDataObject(targetFileName, backUpAclConfigMap); + + List globalWhiteAddrsList = new ArrayList<>(); + globalWhiteAddrsList.add("192.168.1.*"); + + PlainAccessValidator plainAccessValidator = new PlainAccessValidator(); + Assert.assertEquals(plainAccessValidator.updateGlobalWhiteAddrsConfig(globalWhiteAddrsList, null), true); + + String aclFileName = System.getProperty("rocketmq.home.dir") + + File.separator + "conf/plain_acl.yml".replace("/", File.separator); + PlainAccessData readableMap = AclUtils.getYamlDataObject(aclFileName, PlainAccessData.class); + List dataVersions = readableMap.getDataVersion(); + Assert.assertEquals(1L, dataVersions.get(0).getCounter()); + AclUtils.writeDataObject(targetFileName, backUpAclConfigMap); + } + + @Test + public void addYamlConfigTest() throws IOException, InterruptedException { + String fileName = System.getProperty("rocketmq.home.dir") + + File.separator + "conf/acl/plain_acl_test.yml".replace("/", File.separator); + File transport = new File(fileName); + transport.delete(); + transport.createNewFile(); + FileWriter writer = new FileWriter(transport); + writer.write("accounts:\r\n"); + writer.write("- accessKey: watchrocketmqx\r\n"); + writer.write(" secretKey: 12345678\r\n"); + writer.write(" whiteRemoteAddress: 127.0.0.1\r\n"); + writer.write(" admin: true\r\n"); + writer.flush(); + writer.close(); + + Thread.sleep(1000); + + PlainAccessValidator plainAccessValidator = new PlainAccessValidator(); + AclConfig aclConfig = plainAccessValidator.getAllAclConfig(); + List plainAccessConfigs = aclConfig.getPlainAccessConfigs(); + Map verifyMap = new HashMap<>(); + for (PlainAccessConfig plainAccessConfig : plainAccessConfigs) { + if (plainAccessConfig.getAccessKey().equals("watchrocketmqx")) { + verifyMap.put(AclConstants.CONFIG_SECRET_KEY, plainAccessConfig.getSecretKey()); + verifyMap.put(AclConstants.CONFIG_WHITE_ADDR, plainAccessConfig.getWhiteRemoteAddress()); + verifyMap.put(AclConstants.CONFIG_ADMIN_ROLE, plainAccessConfig.isAdmin()); + } + } + + Assert.assertEquals(verifyMap.get(AclConstants.CONFIG_SECRET_KEY), "12345678"); + Assert.assertEquals(verifyMap.get(AclConstants.CONFIG_WHITE_ADDR), "127.0.0.1"); + Assert.assertEquals(verifyMap.get(AclConstants.CONFIG_ADMIN_ROLE), true); + + Map dataVersionMap = plainAccessValidator.getAllAclConfigVersion(); + DataVersion dataVersion = dataVersionMap.get(fileName); + Assert.assertEquals(0, dataVersion.getCounter().get()); + + transport.delete(); + } + + @Test + public void updateAccessAnotherAclYamlConfigTest() throws IOException, InterruptedException { + String fileName = System.getProperty("rocketmq.home.dir") + + File.separator + "conf/acl/plain_acl_test.yml".replace("/", File.separator); + File transport = new File(fileName); + transport.delete(); + transport.createNewFile(); + FileWriter writer = new FileWriter(transport); + writer.write("accounts:\r\n"); + writer.write("- accessKey: watchrocketmqy\r\n"); + writer.write(" secretKey: 12345678\r\n"); + writer.write(" whiteRemoteAddress: 127.0.0.1\r\n"); + writer.write(" admin: true\r\n"); + writer.write("- accessKey: watchrocketmqx\r\n"); + writer.write(" secretKey: 123456781\r\n"); + writer.write(" whiteRemoteAddress: 127.0.0.1\r\n"); + writer.write(" admin: true\r\n"); + writer.flush(); + writer.close(); + + Thread.sleep(1000); + + PlainAccessValidator plainAccessValidator = new PlainAccessValidator(); + + PlainAccessConfig plainAccessConfig = new PlainAccessConfig(); + plainAccessConfig.setAccessKey("watchrocketmqy"); + plainAccessConfig.setSecretKey("1234567890"); + plainAccessConfig.setWhiteRemoteAddress("127.0.0.1"); + plainAccessConfig.setAdmin(false); + + plainAccessValidator.updateAccessConfig(plainAccessConfig); + + Thread.sleep(1000); + + AclConfig aclConfig = plainAccessValidator.getAllAclConfig(); + List plainAccessConfigs = aclConfig.getPlainAccessConfigs(); + Map verifyMap = new HashMap<>(); + for (PlainAccessConfig plainAccessConfig1 : plainAccessConfigs) { + if (plainAccessConfig1.getAccessKey().equals("watchrocketmqy")) { + verifyMap.put(AclConstants.CONFIG_SECRET_KEY, plainAccessConfig1.getSecretKey()); + verifyMap.put(AclConstants.CONFIG_WHITE_ADDR, plainAccessConfig1.getWhiteRemoteAddress()); + verifyMap.put(AclConstants.CONFIG_ADMIN_ROLE, plainAccessConfig1.isAdmin()); + } + } + + Assert.assertEquals(verifyMap.get(AclConstants.CONFIG_SECRET_KEY), "1234567890"); + Assert.assertEquals(verifyMap.get(AclConstants.CONFIG_WHITE_ADDR), "127.0.0.1"); + Assert.assertEquals(verifyMap.get(AclConstants.CONFIG_ADMIN_ROLE), false); + + Map dataVersionMap = plainAccessValidator.getAllAclConfigVersion(); + DataVersion dataVersion = dataVersionMap.get(fileName); + Assert.assertEquals(1, dataVersion.getCounter().get()); + + transport.delete(); + + } + + @Test(expected = AclException.class) + public void createAndUpdateAccessAclNullSkExceptionTest() { + String backupFileName = System.getProperty("rocketmq.home.dir") + + File.separator + "conf/plain_acl_bak.yml".replace("/", File.separator); + String targetFileName = System.getProperty("rocketmq.home.dir") + + File.separator + "conf/plain_acl.yml".replace("/", File.separator); + PlainAccessData backUpAclConfigMap = AclUtils.getYamlDataObject(backupFileName, PlainAccessData.class); + AclUtils.writeDataObject(targetFileName, backUpAclConfigMap); + + PlainAccessConfig plainAccessConfig = new PlainAccessConfig(); + plainAccessConfig.setAccessKey("RocketMQ33"); + // secret key is null + + PlainAccessValidator plainAccessValidator = new PlainAccessValidator(); + plainAccessValidator.updateAccessConfig(plainAccessConfig); + + AclUtils.writeDataObject(targetFileName, backUpAclConfigMap); + } + + @Test + public void addAccessDefaultAclYamlConfigTest() throws InterruptedException { + PlainAccessValidator plainAccessValidator = new PlainAccessValidator(); + String backupFileName = System.getProperty("rocketmq.home.dir") + + File.separator + "conf/plain_acl_bak.yml".replace("/", File.separator); + String targetFileName = System.getProperty("rocketmq.home.dir") + + File.separator + "conf/plain_acl.yml".replace("/", File.separator); + PlainAccessData backUpAclConfigMap = AclUtils.getYamlDataObject(backupFileName, PlainAccessData.class); + AclUtils.writeDataObject(targetFileName, backUpAclConfigMap); + + PlainAccessConfig plainAccessConfig = new PlainAccessConfig(); + plainAccessConfig.setAccessKey("watchrocketmqh"); + plainAccessConfig.setSecretKey("1234567890"); + plainAccessConfig.setWhiteRemoteAddress("127.0.0.1"); + plainAccessConfig.setAdmin(false); + + plainAccessValidator.updateAccessConfig(plainAccessConfig); + + Thread.sleep(10000); + + AclConfig aclConfig = plainAccessValidator.getAllAclConfig(); + List plainAccessConfigs = aclConfig.getPlainAccessConfigs(); + Map verifyMap = new HashMap<>(); + for (PlainAccessConfig plainAccessConfig1 : plainAccessConfigs) { + if (plainAccessConfig1.getAccessKey().equals("watchrocketmqh")) { + verifyMap.put(AclConstants.CONFIG_SECRET_KEY, plainAccessConfig1.getSecretKey()); + verifyMap.put(AclConstants.CONFIG_WHITE_ADDR, plainAccessConfig1.getWhiteRemoteAddress()); + verifyMap.put(AclConstants.CONFIG_ADMIN_ROLE, plainAccessConfig1.isAdmin()); + } + } + + Assert.assertEquals(verifyMap.get(AclConstants.CONFIG_SECRET_KEY), "1234567890"); + Assert.assertEquals(verifyMap.get(AclConstants.CONFIG_WHITE_ADDR), "127.0.0.1"); + Assert.assertEquals(verifyMap.get(AclConstants.CONFIG_ADMIN_ROLE), false); + + PlainAccessData readableMap = AclUtils.getYamlDataObject(targetFileName, PlainAccessData.class); + List dataVersions = readableMap.getDataVersion(); + Assert.assertEquals(1L, dataVersions.get(0).getCounter()); + + AclUtils.writeDataObject(targetFileName, backUpAclConfigMap); + } + + @Test + public void deleteAccessAnotherAclYamlConfigTest() throws IOException, InterruptedException { + String fileName = System.getProperty("rocketmq.home.dir") + + File.separator + "conf/acl/plain_acl_test.yml".replace("/", File.separator); + File transport = new File(fileName); + transport.delete(); + transport.createNewFile(); + FileWriter writer = new FileWriter(transport); + writer.write("accounts:\r\n"); + writer.write("- accessKey: watchrocketmqx\r\n"); + writer.write(" secretKey: 12345678\r\n"); + writer.write(" whiteRemoteAddress: 127.0.0.1\r\n"); + writer.write(" admin: true\r\n"); + writer.write("- accessKey: watchrocketmqy\r\n"); + writer.write(" secretKey: 1234567890\r\n"); + writer.write(" whiteRemoteAddress: 127.0.0.1\r\n"); + writer.write(" admin: false\r\n"); + writer.flush(); + writer.close(); + + Thread.sleep(1000); + + PlainAccessValidator plainAccessValidator = new PlainAccessValidator(); + plainAccessValidator.deleteAccessConfig("watchrocketmqx"); + Thread.sleep(10000); + + Map verifyMap = new HashMap<>(); + AclConfig aclConfig = plainAccessValidator.getAllAclConfig(); + List plainAccessConfigs = aclConfig.getPlainAccessConfigs(); + for (PlainAccessConfig plainAccessConfig : plainAccessConfigs) { + if (plainAccessConfig.getAccessKey().equals("watchrocketmqx")) { + verifyMap.put(AclConstants.CONFIG_SECRET_KEY, plainAccessConfig.getSecretKey()); + verifyMap.put(AclConstants.CONFIG_DEFAULT_TOPIC_PERM, plainAccessConfig.getDefaultTopicPerm()); + verifyMap.put(AclConstants.CONFIG_DEFAULT_GROUP_PERM, plainAccessConfig.getDefaultGroupPerm()); + verifyMap.put(AclConstants.CONFIG_ADMIN_ROLE, plainAccessConfig.isAdmin()); + verifyMap.put(AclConstants.CONFIG_WHITE_ADDR, plainAccessConfig.getWhiteRemoteAddress()); + verifyMap.put(AclConstants.CONFIG_TOPIC_PERMS, plainAccessConfig.getTopicPerms()); + verifyMap.put(AclConstants.CONFIG_GROUP_PERMS, plainAccessConfig.getGroupPerms()); + } + } + + Assert.assertEquals(verifyMap.size(), 0); + + transport.delete(); + } + + @Test + public void getAllAclConfigTest() { + PlainAccessValidator plainAccessValidator = new PlainAccessValidator(); + AclConfig aclConfig = plainAccessValidator.getAllAclConfig(); + Assert.assertEquals(aclConfig.getGlobalWhiteAddrs().size(), 4); + Assert.assertEquals(aclConfig.getPlainAccessConfigs().size(), 2); + } + + @Test + public void updateAccessConfigEmptyPermListTest() { + String backupFileName = System.getProperty("rocketmq.home.dir") + + File.separator + "conf/plain_acl_bak.yml".replace("/", File.separator); + String targetFileName = System.getProperty("rocketmq.home.dir") + + File.separator + "conf/plain_acl.yml".replace("/", File.separator); + PlainAccessData backUpAclConfigMap = AclUtils.getYamlDataObject(backupFileName, PlainAccessData.class); + AclUtils.writeDataObject(targetFileName, backUpAclConfigMap); + + PlainAccessValidator plainAccessValidator = new PlainAccessValidator(); + PlainAccessConfig plainAccessConfig = new PlainAccessConfig(); + String accessKey = "updateAccessConfigEmptyPerm"; + plainAccessConfig.setAccessKey(accessKey); + plainAccessConfig.setSecretKey("123456789111"); + plainAccessConfig.setTopicPerms(Collections.singletonList("topicB=PUB")); + plainAccessValidator.updateAccessConfig(plainAccessConfig); + + plainAccessConfig.setTopicPerms(new ArrayList<>()); + plainAccessValidator.updateAccessConfig(plainAccessConfig); + + List plainAccessConfigs = plainAccessValidator.getAllAclConfig().getPlainAccessConfigs(); + for (int i = 0; i < plainAccessConfigs.size(); i++) { + PlainAccessConfig plainAccessConfig1 = plainAccessConfigs.get(i); + if (plainAccessConfig1.getAccessKey() == accessKey) { + Assert.assertEquals(0, plainAccessConfig1.getTopicPerms().size()); + } + } + + plainAccessValidator.deleteAccessConfig(accessKey); + AclUtils.writeDataObject(targetFileName, backUpAclConfigMap); + } + + @Test + public void updateAccessConfigEmptyWhiteRemoteAddressTest() { + String backupFileName = System.getProperty("rocketmq.home.dir") + + File.separator + "conf/plain_acl_bak.yml".replace("/", File.separator); + String targetFileName = System.getProperty("rocketmq.home.dir") + + File.separator + "conf/plain_acl.yml".replace("/", File.separator); + PlainAccessData backUpAclConfigMap = AclUtils.getYamlDataObject(backupFileName, PlainAccessData.class); + AclUtils.writeDataObject(targetFileName, backUpAclConfigMap); + + PlainAccessValidator plainAccessValidator = new PlainAccessValidator(); + PlainAccessConfig plainAccessConfig = new PlainAccessConfig(); + String accessKey = "updateAccessConfigEmptyWhiteRemoteAddress"; + plainAccessConfig.setAccessKey(accessKey); + plainAccessConfig.setSecretKey("123456789111"); + plainAccessConfig.setWhiteRemoteAddress("127.0.0.1"); + plainAccessValidator.updateAccessConfig(plainAccessConfig); + + plainAccessConfig.setWhiteRemoteAddress(""); + plainAccessValidator.updateAccessConfig(plainAccessConfig); + + List plainAccessConfigs = plainAccessValidator.getAllAclConfig().getPlainAccessConfigs(); + for (int i = 0; i < plainAccessConfigs.size(); i++) { + PlainAccessConfig plainAccessConfig1 = plainAccessConfigs.get(i); + if (plainAccessConfig1.getAccessKey() == accessKey) { + Assert.assertEquals("", plainAccessConfig1.getWhiteRemoteAddress()); + } + } + + plainAccessValidator.deleteAccessConfig(accessKey); + AclUtils.writeDataObject(targetFileName, backUpAclConfigMap); + } + + @Test + public void deleteAccessAclToEmptyTest() { + final String bakAclFileProp = System.getProperty("rocketmq.acl.plain.file"); + System.setProperty("rocketmq.acl.plain.file", "conf/empty.yml".replace("/", File.separator)); + PlainAccessConfig plainAccessConfig = new PlainAccessConfig(); + plainAccessConfig.setAccessKey("deleteAccessAclToEmpty"); + plainAccessConfig.setSecretKey("12345678"); + + PlainAccessValidator plainAccessValidator = new PlainAccessValidator(); + plainAccessValidator.updateAccessConfig(plainAccessConfig); + boolean success = plainAccessValidator.deleteAccessConfig("deleteAccessAclToEmpty"); + if (null != bakAclFileProp) { + System.setProperty("rocketmq.acl.plain.file", bakAclFileProp); + } else { + System.clearProperty("rocketmq.acl.plain.file"); + } + Assert.assertTrue(success); + } + + @Test + public void testValidateAfterUpdateAccessConfig() throws NoSuchFieldException, IllegalAccessException { + String targetFileName = System.getProperty("rocketmq.home.dir") + + File.separator + "conf/update.yml".replace("/", File.separator); + System.setProperty("rocketmq.acl.plain.file", "conf/update.yml".replace("/", File.separator)); + PlainAccessValidator plainAccessValidator = new PlainAccessValidator(); + PlainAccessConfig plainAccessConfig = new PlainAccessConfig(); + String accessKey = "updateAccessConfig"; + String secretKey = "123456789111"; + plainAccessConfig.setAccessKey(accessKey); + plainAccessConfig.setSecretKey(secretKey); + plainAccessConfig.setAdmin(true); + // update + plainAccessValidator.updateAccessConfig(plainAccessConfig); + // call load + Class clazz = PlainAccessValidator.class; + Field f = clazz.getDeclaredField("aclPlugEngine"); + f.setAccessible(true); + PlainPermissionManager aclPlugEngine = (PlainPermissionManager) f.get(plainAccessValidator); + aclPlugEngine.load(targetFileName); + + // call validate + PullMessageRequestHeader pullMessageRequestHeader = new PullMessageRequestHeader(); + pullMessageRequestHeader.setTopic("topicC"); + pullMessageRequestHeader.setConsumerGroup("consumerGroupA"); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, pullMessageRequestHeader); + + AclClientRPCHook aclClient = new AclClientRPCHook(new SessionCredentials(accessKey, secretKey)); + aclClient.doBeforeRequest("", remotingCommand); + ByteBuffer buf = remotingCommand.encodeHeader(); + buf.getInt(); + buf = ByteBuffer.allocate(buf.limit() - buf.position()).put(buf); + buf.position(0); + try { + PlainAccessResource accessResource = (PlainAccessResource) plainAccessValidator.parse(RemotingCommand.decode(buf), "1.1.1.1:9876"); + plainAccessValidator.validate(accessResource); + } catch (RemotingCommandException e) { + e.printStackTrace(); + Assert.fail("Should not throw IOException"); + } finally { + System.setProperty("rocketmq.acl.plain.file", "conf/plain_acl.yml".replace("/", File.separator)); + } + } + + /** + * Fixme: this test case is not thread safe. The design itself is buggy. + * @throws IOException + */ + @Test + public void testUpdateSpecifiedAclFileGlobalWhiteAddrsConfig() throws IOException { + String folder = "update_global_white_addr"; + File home = AclTestHelper.copyResources(folder); + System.setProperty("rocketmq.home.dir", home.getAbsolutePath()); + System.setProperty("rocketmq.acl.plain.file", "/conf/plain_acl.yml".replace("/", File.separator)); + + String targetFileName = Joiner.on(File.separator).join(new String[]{home.getAbsolutePath(), "conf", "plain_acl.yml"}); + PlainAccessData backUpAclConfigMap = AclUtils.getYamlDataObject(targetFileName, PlainAccessData.class); + + String targetFileName1 = Joiner.on(File.separator).join(new String[]{home.getAbsolutePath(), "conf", "acl", "plain_acl.yml"}); + PlainAccessData backUpAclConfigMap1 = AclUtils.getYamlDataObject(targetFileName1, PlainAccessData.class); + + String targetFileName2 = Joiner.on(File.separator).join(new String[]{home.getAbsolutePath(), "conf", "acl", "empty.yml"}); + PlainAccessData backUpAclConfigMap2 = AclUtils.getYamlDataObject(targetFileName2, PlainAccessData.class); + + PlainAccessValidator plainAccessValidator = new PlainAccessValidator(); + List globalWhiteAddrsList1 = new ArrayList<>(); + globalWhiteAddrsList1.add("10.10.154.1"); + List globalWhiteAddrsList2 = new ArrayList<>(); + globalWhiteAddrsList2.add("10.10.154.2"); + List globalWhiteAddrsList3 = new ArrayList<>(); + globalWhiteAddrsList3.add("10.10.154.3"); + + //Test parameter p is null + plainAccessValidator.updateGlobalWhiteAddrsConfig(globalWhiteAddrsList1, null); + String defaultAclFile = targetFileName; + PlainAccessData defaultAclFileMap = AclUtils.getYamlDataObject(defaultAclFile, PlainAccessData.class); + List defaultAclFileGlobalWhiteAddrList = defaultAclFileMap.getGlobalWhiteRemoteAddresses(); + Assert.assertTrue(defaultAclFileGlobalWhiteAddrList.contains("10.10.154.1")); + //Test parameter p is not null + plainAccessValidator.updateGlobalWhiteAddrsConfig(globalWhiteAddrsList2, targetFileName1); + PlainAccessData aclFileMap1 = AclUtils.getYamlDataObject(targetFileName1, PlainAccessData.class); + List aclFileGlobalWhiteAddrList1 = aclFileMap1.getGlobalWhiteRemoteAddresses(); + Assert.assertTrue(aclFileGlobalWhiteAddrList1.contains("10.10.154.2")); + //Test parameter p is not null, but the file does not have globalWhiteRemoteAddresses + plainAccessValidator.updateGlobalWhiteAddrsConfig(globalWhiteAddrsList3, targetFileName2); + PlainAccessData aclFileMap2 = AclUtils.getYamlDataObject(targetFileName2, PlainAccessData.class); + List aclFileGlobalWhiteAddrList2 = aclFileMap2.getGlobalWhiteRemoteAddresses(); + Assert.assertTrue(aclFileGlobalWhiteAddrList2.contains("10.10.154.3")); + + AclUtils.writeDataObject(targetFileName, backUpAclConfigMap); + AclUtils.writeDataObject(targetFileName1, backUpAclConfigMap1); + AclUtils.writeDataObject(targetFileName2, backUpAclConfigMap2); + + AclTestHelper.recursiveDelete(home); + } + + +} diff --git a/acl/src/test/java/org/apache/rocketmq/acl/plain/PlainPermissionManagerTest.java b/acl/src/test/java/org/apache/rocketmq/acl/plain/PlainPermissionManagerTest.java new file mode 100644 index 00000000000..941d8c77923 --- /dev/null +++ b/acl/src/test/java/org/apache/rocketmq/acl/plain/PlainPermissionManagerTest.java @@ -0,0 +1,379 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.acl.plain; + +import com.google.common.base.Joiner; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.acl.common.AclConstants; +import org.apache.rocketmq.acl.common.AclException; +import org.apache.rocketmq.acl.common.AclUtils; +import org.apache.rocketmq.acl.common.Permission; +import org.apache.rocketmq.common.AclConfig; +import org.apache.rocketmq.common.PlainAccessConfig; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.assertj.core.api.Assertions; +import org.assertj.core.util.Lists; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class PlainPermissionManagerTest { + + PlainPermissionManager plainPermissionManager; + PlainAccessResource pubPlainAccessResource; + PlainAccessResource subPlainAccessResource; + PlainAccessResource anyPlainAccessResource; + PlainAccessResource denyPlainAccessResource; + PlainAccessResource plainAccessResource = new PlainAccessResource(); + PlainAccessConfig plainAccessConfig = new PlainAccessConfig(); + Set adminCode = new HashSet<>(); + + private static final String DEFAULT_TOPIC = "topic-acl"; + + private File confHome; + + @Before + public void init() throws NoSuchFieldException, SecurityException, IOException { + // UPDATE_AND_CREATE_TOPIC + adminCode.add(17); + // UPDATE_BROKER_CONFIG + adminCode.add(25); + // DELETE_TOPIC_IN_BROKER + adminCode.add(215); + // UPDATE_AND_CREATE_SUBSCRIPTIONGROUP + adminCode.add(200); + // DELETE_SUBSCRIPTIONGROUP + adminCode.add(207); + + pubPlainAccessResource = clonePlainAccessResource(Permission.PUB); + subPlainAccessResource = clonePlainAccessResource(Permission.SUB); + anyPlainAccessResource = clonePlainAccessResource(Permission.ANY); + denyPlainAccessResource = clonePlainAccessResource(Permission.DENY); + + String folder = "conf"; + confHome = AclTestHelper.copyResources(folder, true); + System.setProperty("rocketmq.home.dir", confHome.getAbsolutePath()); + plainPermissionManager = new PlainPermissionManager(); + } + + public PlainAccessResource clonePlainAccessResource(byte perm) { + PlainAccessResource painAccessResource = new PlainAccessResource(); + painAccessResource.setAccessKey("RocketMQ"); + painAccessResource.setSecretKey("12345678"); + painAccessResource.setWhiteRemoteAddress("127.0." + perm + ".*"); + painAccessResource.setDefaultGroupPerm(perm); + painAccessResource.setDefaultTopicPerm(perm); + painAccessResource.addResourceAndPerm(PlainAccessResource.getRetryTopic("groupA"), Permission.PUB); + painAccessResource.addResourceAndPerm(PlainAccessResource.getRetryTopic("groupB"), Permission.SUB); + painAccessResource.addResourceAndPerm(PlainAccessResource.getRetryTopic("groupC"), Permission.ANY); + painAccessResource.addResourceAndPerm(PlainAccessResource.getRetryTopic("groupD"), Permission.DENY); + + painAccessResource.addResourceAndPerm("topicA", Permission.PUB); + painAccessResource.addResourceAndPerm("topicB", Permission.SUB); + painAccessResource.addResourceAndPerm("topicC", Permission.ANY); + painAccessResource.addResourceAndPerm("topicD", Permission.DENY); + return painAccessResource; + } + + @Test + public void buildPlainAccessResourceTest() { + PlainAccessResource plainAccessResource = null; + PlainAccessConfig plainAccess = new PlainAccessConfig(); + + plainAccess.setAccessKey("RocketMQ"); + plainAccess.setSecretKey("12345678"); + plainAccessResource = plainPermissionManager.buildPlainAccessResource(plainAccess); + Assert.assertEquals(plainAccessResource.getAccessKey(), "RocketMQ"); + Assert.assertEquals(plainAccessResource.getSecretKey(), "12345678"); + + plainAccess.setWhiteRemoteAddress("127.0.0.1"); + plainAccessResource = plainPermissionManager.buildPlainAccessResource(plainAccess); + Assert.assertEquals(plainAccessResource.getWhiteRemoteAddress(), "127.0.0.1"); + + plainAccess.setAdmin(true); + plainAccessResource = plainPermissionManager.buildPlainAccessResource(plainAccess); + Assert.assertEquals(plainAccessResource.isAdmin(), true); + + List groups = new ArrayList<>(); + groups.add("groupA=DENY"); + groups.add("groupB=PUB|SUB"); + groups.add("groupC=PUB"); + plainAccess.setGroupPerms(groups); + plainAccessResource = plainPermissionManager.buildPlainAccessResource(plainAccess); + Map resourcePermMap = plainAccessResource.getResourcePermMap(); + Assert.assertEquals(resourcePermMap.size(), 3); + + Assert.assertEquals(resourcePermMap.get(PlainAccessResource.getRetryTopic("groupA")).byteValue(), Permission.DENY); + Assert.assertEquals(resourcePermMap.get(PlainAccessResource.getRetryTopic("groupB")).byteValue(), Permission.PUB | Permission.SUB); + Assert.assertEquals(resourcePermMap.get(PlainAccessResource.getRetryTopic("groupC")).byteValue(), Permission.PUB); + + List topics = new ArrayList<>(); + topics.add("topicA=DENY"); + topics.add("topicB=PUB|SUB"); + topics.add("topicC=PUB"); + plainAccess.setTopicPerms(topics); + plainAccessResource = plainPermissionManager.buildPlainAccessResource(plainAccess); + resourcePermMap = plainAccessResource.getResourcePermMap(); + Assert.assertEquals(resourcePermMap.size(), 6); + + Assert.assertEquals(resourcePermMap.get("topicA").byteValue(), Permission.DENY); + Assert.assertEquals(resourcePermMap.get("topicB").byteValue(), Permission.PUB | Permission.SUB); + Assert.assertEquals(resourcePermMap.get("topicC").byteValue(), Permission.PUB); + } + + @Test(expected = AclException.class) + public void checkPermAdmin() { + PlainAccessResource plainAccessResource = new PlainAccessResource(); + plainAccessResource.setRequestCode(17); + plainPermissionManager.checkPerm(plainAccessResource, pubPlainAccessResource); + } + + @Test + public void checkPerm() { + + PlainAccessResource plainAccessResource = new PlainAccessResource(); + plainAccessResource.addResourceAndPerm("topicA", Permission.PUB); + plainPermissionManager.checkPerm(plainAccessResource, pubPlainAccessResource); + plainAccessResource.addResourceAndPerm("topicB", Permission.SUB); + plainPermissionManager.checkPerm(plainAccessResource, anyPlainAccessResource); + + plainAccessResource = new PlainAccessResource(); + plainAccessResource.addResourceAndPerm("topicB", Permission.SUB); + plainPermissionManager.checkPerm(plainAccessResource, subPlainAccessResource); + plainAccessResource.addResourceAndPerm("topicA", Permission.PUB); + plainPermissionManager.checkPerm(plainAccessResource, anyPlainAccessResource); + + } + + @Test(expected = AclException.class) + public void checkErrorPermDefaultValueNotMatch() { + + plainAccessResource = new PlainAccessResource(); + plainAccessResource.addResourceAndPerm("topicF", Permission.PUB); + plainPermissionManager.checkPerm(plainAccessResource, subPlainAccessResource); + } + + @Test(expected = AclException.class) + public void accountNullTest() { + plainAccessConfig.setAccessKey(null); + plainPermissionManager.buildPlainAccessResource(plainAccessConfig); + } + + @Test(expected = AclException.class) + public void accountThanTest() { + plainAccessConfig.setAccessKey("123"); + plainPermissionManager.buildPlainAccessResource(plainAccessConfig); + } + + @Test(expected = AclException.class) + public void passWordtNullTest() { + plainAccessConfig.setAccessKey(null); + plainPermissionManager.buildPlainAccessResource(plainAccessConfig); + } + + @Test(expected = AclException.class) + public void passWordThanTest() { + plainAccessConfig.setSecretKey("123"); + plainPermissionManager.buildPlainAccessResource(plainAccessConfig); + } + + @SuppressWarnings("unchecked") + @Test + public void cleanAuthenticationInfoTest() throws IllegalAccessException { + // PlainPermissionManager.addPlainAccessResource(plainAccessResource); + Map> plainAccessResourceMap = (Map>) FieldUtils.readDeclaredField(plainPermissionManager, "aclPlainAccessResourceMap", true); + Assert.assertFalse(plainAccessResourceMap.isEmpty()); + + plainPermissionManager.clearPermissionInfo(); + plainAccessResourceMap = (Map>) FieldUtils.readDeclaredField(plainPermissionManager, "aclPlainAccessResourceMap", true); + Assert.assertTrue(plainAccessResourceMap.isEmpty()); + } + + @Test + public void isWatchStartTest() { + + PlainPermissionManager plainPermissionManager = new PlainPermissionManager(); + Assert.assertTrue(plainPermissionManager.isWatchStart()); + } + + @Test + public void testWatch() throws IOException, IllegalAccessException, InterruptedException { + String fileName = Joiner.on(File.separator).join(new String[]{System.getProperty("rocketmq.home.dir"), "conf", "acl", "plain_acl_test.yml"}); + File transport = new File(fileName); + transport.delete(); + transport.createNewFile(); + FileWriter writer = new FileWriter(transport); + writer.write("accounts:\r\n"); + writer.write("- accessKey: watchrocketmqx\r\n"); + writer.write(" secretKey: 12345678\r\n"); + writer.write(" whiteRemoteAddress: 127.0.0.1\r\n"); + writer.write(" admin: true\r\n"); + writer.flush(); + writer.close(); + + Thread.sleep(1000); + + PlainPermissionManager plainPermissionManager = new PlainPermissionManager(); + Assert.assertTrue(plainPermissionManager.isWatchStart()); + + Map accessKeyTable = (Map) FieldUtils.readDeclaredField(plainPermissionManager, "accessKeyTable", true); + String aclFileName = accessKeyTable.get("watchrocketmqx"); + { + Map> plainAccessResourceMap = (Map>) FieldUtils.readDeclaredField(plainPermissionManager, "aclPlainAccessResourceMap", true); + PlainAccessResource accessResource = plainAccessResourceMap.get(aclFileName).get("watchrocketmqx"); + Assert.assertNotNull(accessResource); + Assert.assertEquals(accessResource.getSecretKey(), "12345678"); + Assert.assertTrue(accessResource.isAdmin()); + + } + + PlainAccessData updatedMap = AclUtils.getYamlDataObject(fileName, PlainAccessData.class); + List accounts = updatedMap.getAccounts(); + accounts.get(0).setAccessKey("watchrocketmq1y"); + accounts.get(0).setSecretKey("88888888"); + accounts.get(0).setAdmin(false); + // Update file and flush to yaml file + AclUtils.writeDataObject(fileName, updatedMap); + + Thread.sleep(10000); + { + Map> plainAccessResourceMap = (Map>) FieldUtils.readDeclaredField(plainPermissionManager, "aclPlainAccessResourceMap", true); + PlainAccessResource accessResource = plainAccessResourceMap.get(aclFileName).get("watchrocketmq1y"); + Assert.assertNotNull(accessResource); + Assert.assertEquals(accessResource.getSecretKey(), "88888888"); + Assert.assertFalse(accessResource.isAdmin()); + + } + transport.delete(); + } + + @Test + public void updateAccessConfigTest() { + Assert.assertThrows(AclException.class, () -> plainPermissionManager.updateAccessConfig(null)); + + plainAccessConfig.setAccessKey("admin_test"); + // Invalid parameter + plainAccessConfig.setSecretKey("123456"); + plainAccessConfig.setAdmin(true); + Assert.assertThrows(AclException.class, () -> plainPermissionManager.updateAccessConfig(plainAccessConfig)); + + plainAccessConfig.setSecretKey("12345678"); + // Invalid parameter + plainAccessConfig.setGroupPerms(Lists.newArrayList("groupA!SUB")); + Assert.assertThrows(AclException.class, () -> plainPermissionManager.updateAccessConfig(plainAccessConfig)); + + // first update + plainAccessConfig.setGroupPerms(Lists.newArrayList("groupA=SUB")); + plainPermissionManager.updateAccessConfig(plainAccessConfig); + + // second update + plainAccessConfig.setTopicPerms(Lists.newArrayList("topicA=SUB")); + plainPermissionManager.updateAccessConfig(plainAccessConfig); + } + + @Test + public void getAllAclFilesTest() { + final List notExistList = plainPermissionManager.getAllAclFiles("aa/bb"); + Assertions.assertThat(notExistList).isEmpty(); + final List files = plainPermissionManager.getAllAclFiles(confHome.getAbsolutePath()); + Assertions.assertThat(files).isNotEmpty(); + } + + @Test + public void loadTest() { + plainPermissionManager.load(); + final Map map = plainPermissionManager.getDataVersionMap(); + Assertions.assertThat(map).isNotEmpty(); + } + + @Test + public void updateAclConfigFileVersionTest() { + String aclFileName = "test_plain_acl"; + PlainAccessData updateAclConfigMap = new PlainAccessData(); + List versionElement = new ArrayList<>(); + PlainAccessData.DataVersion accountsMap = new PlainAccessData.DataVersion(); + accountsMap.setCounter(1); + accountsMap.setTimestamp(System.currentTimeMillis()); + versionElement.add(accountsMap); + + updateAclConfigMap.setDataVersion(versionElement); + final PlainAccessData map = plainPermissionManager.updateAclConfigFileVersion(aclFileName, updateAclConfigMap); + final List version = map.getDataVersion(); + Assert.assertEquals(2L, version.get(0).getCounter()); + } + + @Test + public void createAclAccessConfigMapTest() { + PlainAccessConfig existedAccountMap = new PlainAccessConfig(); + plainAccessConfig.setAccessKey("admin123"); + plainAccessConfig.setSecretKey("12345678"); + plainAccessConfig.setWhiteRemoteAddress("192.168.1.1"); + plainAccessConfig.setAdmin(false); + plainAccessConfig.setDefaultGroupPerm(AclConstants.SUB_PUB); + plainAccessConfig.setTopicPerms(Arrays.asList(DEFAULT_TOPIC + "=" + AclConstants.PUB)); + plainAccessConfig.setGroupPerms(Lists.newArrayList("groupA=SUB")); + + final PlainAccessConfig map = plainPermissionManager.createAclAccessConfigMap(existedAccountMap, plainAccessConfig); + Assert.assertEquals(AclConstants.SUB_PUB, map.getDefaultGroupPerm()); + Assert.assertEquals("groupA=SUB", map.getGroupPerms().get(0)); + Assert.assertEquals("12345678", map.getSecretKey()); + Assert.assertEquals("admin123", map.getAccessKey()); + Assert.assertEquals("192.168.1.1", map.getWhiteRemoteAddress()); + Assert.assertEquals("topic-acl=PUB", map.getTopicPerms().get(0)); + Assert.assertEquals(false, map.isAdmin()); + } + + @Test + public void deleteAccessConfigTest() throws InterruptedException { + // delete not exist accessConfig + final boolean flag1 = plainPermissionManager.deleteAccessConfig("test_delete"); + assert !flag1; + + plainAccessConfig.setAccessKey("test_delete"); + plainAccessConfig.setSecretKey("12345678"); + plainAccessConfig.setWhiteRemoteAddress("192.168.1.1"); + plainAccessConfig.setAdmin(false); + plainAccessConfig.setDefaultGroupPerm(AclConstants.SUB_PUB); + plainAccessConfig.setTopicPerms(Arrays.asList(DEFAULT_TOPIC + "=" + AclConstants.PUB)); + plainAccessConfig.setGroupPerms(Lists.newArrayList("groupA=SUB")); + plainPermissionManager.updateAccessConfig(plainAccessConfig); + + //delete existed accessConfig + final boolean flag2 = plainPermissionManager.deleteAccessConfig("test_delete"); + assert flag2; + + } + + @Test + public void updateGlobalWhiteAddrsConfigTest() { + final boolean flag = plainPermissionManager.updateGlobalWhiteAddrsConfig(Lists.newArrayList("192.168.1.2")); + assert flag; + final AclConfig config = plainPermissionManager.getAllAclConfig(); + Assert.assertEquals(true, config.getGlobalWhiteAddrs().contains("192.168.1.2")); + } + +} diff --git a/acl/src/test/java/org/apache/rocketmq/acl/plain/RemoteAddressStrategyTest.java b/acl/src/test/java/org/apache/rocketmq/acl/plain/RemoteAddressStrategyTest.java new file mode 100644 index 00000000000..df7dd0c5460 --- /dev/null +++ b/acl/src/test/java/org/apache/rocketmq/acl/plain/RemoteAddressStrategyTest.java @@ -0,0 +1,379 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.acl.plain; + +import org.apache.rocketmq.acl.common.AclException; +import org.junit.Assert; +import org.junit.Test; + +public class RemoteAddressStrategyTest { + + RemoteAddressStrategyFactory remoteAddressStrategyFactory = new RemoteAddressStrategyFactory(); + + @Test + public void netAddressStrategyFactoryExceptionTest() { + PlainAccessResource plainAccessResource = new PlainAccessResource(); + remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + Assert.assertEquals(remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource).getClass(), + RemoteAddressStrategyFactory.BlankRemoteAddressStrategy.class); + } + + @Test + public void netAddressStrategyFactoryTest() { + PlainAccessResource plainAccessResource = new PlainAccessResource(); + + plainAccessResource.setWhiteRemoteAddress("*"); + RemoteAddressStrategy remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + Assert.assertEquals(remoteAddressStrategy, RemoteAddressStrategyFactory.NULL_NET_ADDRESS_STRATEGY); + + plainAccessResource.setWhiteRemoteAddress("*.*.*.*"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + Assert.assertEquals(remoteAddressStrategy, RemoteAddressStrategyFactory.NULL_NET_ADDRESS_STRATEGY); + + plainAccessResource.setWhiteRemoteAddress("127.0.0.1"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + Assert.assertEquals(remoteAddressStrategy.getClass(), RemoteAddressStrategyFactory.OneRemoteAddressStrategy.class); + + plainAccessResource.setWhiteRemoteAddress("127.0.0.1,127.0.0.2,127.0.0.3"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + Assert.assertEquals(remoteAddressStrategy.getClass(), RemoteAddressStrategyFactory.MultipleRemoteAddressStrategy.class); + + plainAccessResource.setWhiteRemoteAddress("127.0.0.{1,2,3}"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + Assert.assertEquals(remoteAddressStrategy.getClass(), RemoteAddressStrategyFactory.MultipleRemoteAddressStrategy.class); + + plainAccessResource.setWhiteRemoteAddress("127.0.0.1-200"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + Assert.assertEquals(remoteAddressStrategy.getClass(), RemoteAddressStrategyFactory.RangeRemoteAddressStrategy.class); + + plainAccessResource.setWhiteRemoteAddress("127.0.0.*"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + Assert.assertEquals(remoteAddressStrategy.getClass(), RemoteAddressStrategyFactory.RangeRemoteAddressStrategy.class); + + plainAccessResource.setWhiteRemoteAddress("127.0.1-20.*"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + Assert.assertEquals(remoteAddressStrategy.getClass(), RemoteAddressStrategyFactory.RangeRemoteAddressStrategy.class); + + plainAccessResource.setWhiteRemoteAddress(""); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + Assert.assertEquals(remoteAddressStrategy.getClass(), RemoteAddressStrategyFactory.BlankRemoteAddressStrategy.class); + +// IPv6 test + plainAccessResource.setWhiteRemoteAddress("*:*:*:*:*:*:*:*"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + Assert.assertEquals(remoteAddressStrategy, RemoteAddressStrategyFactory.NULL_NET_ADDRESS_STRATEGY); + + plainAccessResource.setWhiteRemoteAddress("1050:0000:0000:0000:0005:0600:300c:326b"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + Assert.assertEquals(remoteAddressStrategy.getClass(), RemoteAddressStrategyFactory.OneRemoteAddressStrategy.class); + + plainAccessResource.setWhiteRemoteAddress("1050::0005:0600:300c:3261,1050::0005:0600:300c:3262,1050::0005:0600:300c:3263"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + Assert.assertEquals(remoteAddressStrategy.getClass(), RemoteAddressStrategyFactory.MultipleRemoteAddressStrategy.class); + + plainAccessResource.setWhiteRemoteAddress("1050::0005:0600:300c:3261:{1,2,3}"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + Assert.assertEquals(remoteAddressStrategy.getClass(), RemoteAddressStrategyFactory.MultipleRemoteAddressStrategy.class); + + plainAccessResource.setWhiteRemoteAddress("1050::0005:0600:300c:3261:1-200"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + Assert.assertEquals(remoteAddressStrategy.getClass(), RemoteAddressStrategyFactory.RangeRemoteAddressStrategy.class); + + plainAccessResource.setWhiteRemoteAddress("1050:0005:0600:300c:3261:*"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + Assert.assertEquals(remoteAddressStrategy.getClass(), RemoteAddressStrategyFactory.RangeRemoteAddressStrategy.class); + + plainAccessResource.setWhiteRemoteAddress("1050::0005:0600:300c:3261:1-20:*"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + Assert.assertEquals(remoteAddressStrategy.getClass(), RemoteAddressStrategyFactory.RangeRemoteAddressStrategy.class); + } + + @Test(expected = AclException.class) + public void verifyTest() { + PlainAccessResource plainAccessResource = new PlainAccessResource(); + plainAccessResource.setWhiteRemoteAddress("127.0.0.1"); + remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + plainAccessResource.setWhiteRemoteAddress("256.0.0.1"); + remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + plainAccessResource.setWhiteRemoteAddress("::1ggg"); + remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + } + + @Test + public void nullNetAddressStrategyTest() { + boolean isMatch = RemoteAddressStrategyFactory.NULL_NET_ADDRESS_STRATEGY.match(new PlainAccessResource()); + Assert.assertTrue(isMatch); + } + + @Test + public void blankNetAddressStrategyTest() { + boolean isMatch = RemoteAddressStrategyFactory.BLANK_NET_ADDRESS_STRATEGY.match(new PlainAccessResource()); + Assert.assertFalse(isMatch); + } + + @Test + public void oneNetAddressStrategyTest() { + PlainAccessResource plainAccessResource = new PlainAccessResource(); + plainAccessResource.setWhiteRemoteAddress("127.0.0.1"); + RemoteAddressStrategy remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + plainAccessResource.setWhiteRemoteAddress(""); + boolean match = remoteAddressStrategy.match(plainAccessResource); + Assert.assertFalse(match); + + plainAccessResource.setWhiteRemoteAddress("127.0.0.2"); + match = remoteAddressStrategy.match(plainAccessResource); + Assert.assertFalse(match); + + plainAccessResource.setWhiteRemoteAddress("127.0.0.1"); + match = remoteAddressStrategy.match(plainAccessResource); + Assert.assertTrue(match); + +// Ipv6 test + plainAccessResource = new PlainAccessResource(); + plainAccessResource.setWhiteRemoteAddress("::1"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + plainAccessResource.setWhiteRemoteAddress(""); + match = remoteAddressStrategy.match(plainAccessResource); + Assert.assertFalse(match); + + plainAccessResource.setWhiteRemoteAddress("::2"); + match = remoteAddressStrategy.match(plainAccessResource); + Assert.assertFalse(match); + + plainAccessResource.setWhiteRemoteAddress("::1"); + match = remoteAddressStrategy.match(plainAccessResource); + Assert.assertTrue(match); + + plainAccessResource.setWhiteRemoteAddress("0000:0000:0000:0000:0000:0000:0000:0001"); + match = remoteAddressStrategy.match(plainAccessResource); + Assert.assertTrue(match); + } + + @Test + public void multipleNetAddressStrategyTest() { + PlainAccessResource plainAccessResource = new PlainAccessResource(); + plainAccessResource.setWhiteRemoteAddress("127.0.0.1,127.0.0.2,127.0.0.3"); + RemoteAddressStrategy remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + multipleNetAddressStrategyTest(remoteAddressStrategy); + + plainAccessResource.setWhiteRemoteAddress("127.0.0.{1,2,3}"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + multipleNetAddressStrategyTest(remoteAddressStrategy); + + plainAccessResource.setWhiteRemoteAddress("192.100-150.*.*"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + plainAccessResource.setWhiteRemoteAddress("192.130.0.2"); + boolean match = remoteAddressStrategy.match(plainAccessResource); + Assert.assertTrue(match); + + plainAccessResource = new PlainAccessResource(); + plainAccessResource.setWhiteRemoteAddress("1050::0005:0600:300c:1,1050::0005:0600:300c:2,1050::0005:0600:300c:3"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + multipleIPv6NetAddressStrategyTest(remoteAddressStrategy); + + plainAccessResource.setWhiteRemoteAddress("1050::0005:0600:300c:{1,2,3}"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + multipleIPv6NetAddressStrategyTest(remoteAddressStrategy); + + } + + @Test(expected = AclException.class) + public void multipleNetAddressStrategyExceptionTest() { + PlainAccessResource plainAccessResource = new PlainAccessResource(); + plainAccessResource.setWhiteRemoteAddress("127.0.0.1,2,3}"); + remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + plainAccessResource.setWhiteRemoteAddress("::1,2,3}"); + remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + plainAccessResource.setWhiteRemoteAddress("192.168.1.{1}"); + remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + plainAccessResource.setWhiteRemoteAddress("192.168.1.{1,2}"); + remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + plainAccessResource.setWhiteRemoteAddress("192.168.{1}"); + remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + plainAccessResource.setWhiteRemoteAddress("{192.168.1}"); + remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + plainAccessResource.setWhiteRemoteAddress("{192.168.1.1}"); + remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + } + + private void multipleNetAddressStrategyTest(RemoteAddressStrategy remoteAddressStrategy) { + PlainAccessResource plainAccessResource = new PlainAccessResource(); + plainAccessResource.setWhiteRemoteAddress("127.0.0.1"); + boolean match = remoteAddressStrategy.match(plainAccessResource); + Assert.assertTrue(match); + + plainAccessResource.setWhiteRemoteAddress("127.0.0.2"); + match = remoteAddressStrategy.match(plainAccessResource); + Assert.assertTrue(match); + + plainAccessResource.setWhiteRemoteAddress("127.0.0.3"); + match = remoteAddressStrategy.match(plainAccessResource); + Assert.assertTrue(match); + + plainAccessResource.setWhiteRemoteAddress("127.0.0.4"); + match = remoteAddressStrategy.match(plainAccessResource); + Assert.assertFalse(match); + + plainAccessResource.setWhiteRemoteAddress("127.0.0.0"); + match = remoteAddressStrategy.match(plainAccessResource); + Assert.assertFalse(match); + + } + + private void multipleIPv6NetAddressStrategyTest(RemoteAddressStrategy remoteAddressStrategy) { + PlainAccessResource plainAccessResource = new PlainAccessResource(); + plainAccessResource.setWhiteRemoteAddress("1050:0000:0000:0000:0005:0600:300c:1"); + boolean match = remoteAddressStrategy.match(plainAccessResource); + Assert.assertTrue(match); + + plainAccessResource.setWhiteRemoteAddress("1050:0000:0000:0000:0005:0600:300c:2"); + match = remoteAddressStrategy.match(plainAccessResource); + Assert.assertTrue(match); + + plainAccessResource.setWhiteRemoteAddress("1050:0000:0000:0000:0005:0600:300c:3"); + match = remoteAddressStrategy.match(plainAccessResource); + Assert.assertTrue(match); + + plainAccessResource.setWhiteRemoteAddress("1050:0000:0000:0000:0005:0600:300c:4"); + match = remoteAddressStrategy.match(plainAccessResource); + Assert.assertFalse(match); + + plainAccessResource.setWhiteRemoteAddress("1050:0000:0000:0000:0005:0600:300c:0"); + match = remoteAddressStrategy.match(plainAccessResource); + Assert.assertFalse(match); + + } + + @Test + public void rangeNetAddressStrategyTest() { + String head = "127.0.0."; + PlainAccessResource plainAccessResource = new PlainAccessResource(); + plainAccessResource.setWhiteRemoteAddress("127.0.0.1-200"); + RemoteAddressStrategy remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + rangeNetAddressStrategyTest(remoteAddressStrategy, head, 1, 200, true); + + plainAccessResource.setWhiteRemoteAddress("127.0.0.*"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + rangeNetAddressStrategyTest(remoteAddressStrategy, head, 0, 255, true); + + plainAccessResource.setWhiteRemoteAddress("127.0.1-200.*"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + rangeNetAddressStrategyThirdlyTest(remoteAddressStrategy, head, 1, 200); + + plainAccessResource.setWhiteRemoteAddress("127.*.*.*"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + rangeNetAddressStrategyTest(remoteAddressStrategy, head, 0, 255, true); + + plainAccessResource.setWhiteRemoteAddress("127.1-150.*.*"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + rangeNetAddressStrategyThirdlyTest(remoteAddressStrategy, head, 1, 200); + +// IPv6 test + head = "1050::0005:0600:300c:"; + plainAccessResource = new PlainAccessResource(); + plainAccessResource.setWhiteRemoteAddress("1050::0005:0600:300c:1-200"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + rangeIPv6NetAddressStrategyTest(remoteAddressStrategy, head, "1", "200", true); + + plainAccessResource.setWhiteRemoteAddress("1050::0005:0600:300c:*"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + rangeIPv6NetAddressStrategyTest(remoteAddressStrategy, head, "0", "ffff", true); + + plainAccessResource.setWhiteRemoteAddress("1050::0005:0600:3001:*"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + rangeIPv6NetAddressStrategyTest(remoteAddressStrategy, head, "0", "ffff", false); + + head = "1050::0005:0600:300c:1:"; + plainAccessResource.setWhiteRemoteAddress("1050::0005:0600:300c:1-200:*"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + rangeIPv6NetAddressStrategyTest(remoteAddressStrategy, head, "0", "ffff", true); + + head = "1050::0005:0600:300c:201:"; + plainAccessResource.setWhiteRemoteAddress("1050::0005:0600:300c:1-200:*"); + remoteAddressStrategy = remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + rangeIPv6NetAddressStrategyTest(remoteAddressStrategy, head, "0", "ffff", false); + + } + + private void rangeNetAddressStrategyTest(RemoteAddressStrategy remoteAddressStrategy, String head, int start, + int end, + boolean isFalse) { + PlainAccessResource plainAccessResource = new PlainAccessResource(); + for (int i = -10; i < 300; i++) { + plainAccessResource.setWhiteRemoteAddress(head + i); + boolean match = remoteAddressStrategy.match(plainAccessResource); + if (isFalse && i >= start && i <= end) { + Assert.assertTrue(match); + continue; + } + Assert.assertFalse(match); + + } + } + + private void rangeNetAddressStrategyThirdlyTest(RemoteAddressStrategy remoteAddressStrategy, String head, int start, + int end) { + String newHead; + for (int i = -10; i < 300; i++) { + newHead = head + i; + if (i >= start && i <= end) { + rangeNetAddressStrategyTest(remoteAddressStrategy, newHead, 0, 255, false); + } + } + } + + private void rangeIPv6NetAddressStrategyTest(RemoteAddressStrategy remoteAddressStrategy, String head, String start, + String end, + boolean isFalse) { + PlainAccessResource plainAccessResource = new PlainAccessResource(); + for (int i = -10; i < 65536 + 100; i++) { + String hex = Integer.toHexString(i); + plainAccessResource.setWhiteRemoteAddress(head + hex); + boolean match = remoteAddressStrategy.match(plainAccessResource); + int startNum = Integer.parseInt(start, 16); + int endNum = Integer.parseInt(end, 16); + if (isFalse && i >= startNum && i <= endNum) { + Assert.assertTrue(match); + continue; + } + Assert.assertFalse(match); + + } + } + + @Test(expected = AclException.class) + public void rangeNetAddressStrategyExceptionStartGreaterEndTest() { + rangeNetAddressStrategyExceptionTest("127.0.0.2-1"); + } + + @Test(expected = AclException.class) + public void rangeNetAddressStrategyExceptionScopeTest() { + rangeNetAddressStrategyExceptionTest("127.0.0.-1-200"); + } + + @Test(expected = AclException.class) + public void rangeNetAddressStrategyExceptionScopeTwoTest() { + rangeNetAddressStrategyExceptionTest("127.0.0.0-256"); + } + + private void rangeNetAddressStrategyExceptionTest(String netAddress) { + PlainAccessResource plainAccessResource = new PlainAccessResource(); + plainAccessResource.setWhiteRemoteAddress(netAddress); + remoteAddressStrategyFactory.getRemoteAddressStrategy(plainAccessResource); + } + +} diff --git a/acl/src/test/resources/access_acl_conf/acl/plain_acl.yml b/acl/src/test/resources/access_acl_conf/acl/plain_acl.yml new file mode 100644 index 00000000000..28a8c488805 --- /dev/null +++ b/acl/src/test/resources/access_acl_conf/acl/plain_acl.yml @@ -0,0 +1,31 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +accounts: + - accessKey: rocketmq3 + secretKey: 12345678 + admin: false + defaultTopicPerm: DENY + defaultGroupPerm: DENY + topicPerms: + - topicA=PUB + - topicB=SUB + - topicC=PUB|SUB + - topicD=DENY + groupPerms: + - groupB=SUB + - groupC=PUB|SUB + - groupD=DENY + diff --git a/acl/src/test/resources/both_acl_file_folder_conf/conf/acl/plain_acl.yml b/acl/src/test/resources/both_acl_file_folder_conf/conf/acl/plain_acl.yml new file mode 100644 index 00000000000..cf4ea7f4a5b --- /dev/null +++ b/acl/src/test/resources/both_acl_file_folder_conf/conf/acl/plain_acl.yml @@ -0,0 +1,39 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## no global white addresses in this file, define them in ../plain_acl.yml +accounts: + - accessKey: RocketMQ + secretKey: 12345678 + whiteRemoteAddress: 192.168.0.* + admin: false + defaultTopicPerm: DENY + defaultGroupPerm: SUB + topicPerms: + - topicA=DENY + - topicB=PUB|SUB + - topicC=SUB + groupPerms: + # the group should convert to retry topic + - groupA=DENY + - groupB=SUB + - groupC=SUB + + - accessKey: rocketmq2 + secretKey: 12345678 + whiteRemoteAddress: 192.168.1.* + # if it is admin, it could access all resources + admin: true + diff --git a/acl/src/test/resources/both_acl_file_folder_conf/conf/plain_acl.yml b/acl/src/test/resources/both_acl_file_folder_conf/conf/plain_acl.yml new file mode 100644 index 00000000000..41afea097ae --- /dev/null +++ b/acl/src/test/resources/both_acl_file_folder_conf/conf/plain_acl.yml @@ -0,0 +1,21 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## suggested format + +globalWhiteRemoteAddresses: + - 10.10.103.* + - 192.168.0.* + diff --git a/acl/src/test/resources/conf/acl/plain_acl.yml b/acl/src/test/resources/conf/acl/plain_acl.yml new file mode 100644 index 00000000000..34e46696d8e --- /dev/null +++ b/acl/src/test/resources/conf/acl/plain_acl.yml @@ -0,0 +1,43 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## suggested format + +globalWhiteRemoteAddresses: + - 10.10.103.* + - 192.168.0.* + +accounts: + - accessKey: RocketMQ + secretKey: 12345678 + whiteRemoteAddress: 192.168.0.* + admin: false + defaultTopicPerm: DENY + defaultGroupPerm: SUB + topicPerms: + - topicA=DENY + - topicB=PUB|SUB + - topicC=SUB + groupPerms: + # the group should convert to retry topic + - groupA=DENY + - groupB=SUB + - groupC=SUB + + - accessKey: rocketmq2 + secretKey: 12345678 + whiteRemoteAddress: 192.168.1.* + # if it is admin, it could access all resources + admin: true diff --git a/acl/src/test/resources/conf/plain_acl.yml b/acl/src/test/resources/conf/plain_acl.yml new file mode 100644 index 00000000000..59bd6d4ff29 --- /dev/null +++ b/acl/src/test/resources/conf/plain_acl.yml @@ -0,0 +1,39 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## suggested format + +globalWhiteRemoteAddresses: +- 10.10.103.* +- 192.168.0.* +accounts: +- accessKey: RocketMQ + secretKey: 12345678 + whiteRemoteAddress: 192.168.0.* + admin: false + defaultTopicPerm: DENY + defaultGroupPerm: SUB + topicPerms: + - topicA=DENY + - topicB=PUB|SUB + - topicC=SUB + groupPerms: + - groupA=DENY + - groupB=SUB + - groupC=SUB +- accessKey: rocketmq2 + secretKey: 12345678 + whiteRemoteAddress: 192.168.1.* + admin: true diff --git a/acl/src/test/resources/conf/plain_acl_bak.yml b/acl/src/test/resources/conf/plain_acl_bak.yml new file mode 100644 index 00000000000..59bd6d4ff29 --- /dev/null +++ b/acl/src/test/resources/conf/plain_acl_bak.yml @@ -0,0 +1,39 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## suggested format + +globalWhiteRemoteAddresses: +- 10.10.103.* +- 192.168.0.* +accounts: +- accessKey: RocketMQ + secretKey: 12345678 + whiteRemoteAddress: 192.168.0.* + admin: false + defaultTopicPerm: DENY + defaultGroupPerm: SUB + topicPerms: + - topicA=DENY + - topicB=PUB|SUB + - topicC=SUB + groupPerms: + - groupA=DENY + - groupB=SUB + - groupC=SUB +- accessKey: rocketmq2 + secretKey: 12345678 + whiteRemoteAddress: 192.168.1.* + admin: true diff --git a/acl/src/test/resources/conf/plain_acl_correct.yml b/acl/src/test/resources/conf/plain_acl_correct.yml new file mode 100644 index 00000000000..59bd6d4ff29 --- /dev/null +++ b/acl/src/test/resources/conf/plain_acl_correct.yml @@ -0,0 +1,39 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## suggested format + +globalWhiteRemoteAddresses: +- 10.10.103.* +- 192.168.0.* +accounts: +- accessKey: RocketMQ + secretKey: 12345678 + whiteRemoteAddress: 192.168.0.* + admin: false + defaultTopicPerm: DENY + defaultGroupPerm: SUB + topicPerms: + - topicA=DENY + - topicB=PUB|SUB + - topicC=SUB + groupPerms: + - groupA=DENY + - groupB=SUB + - groupC=SUB +- accessKey: rocketmq2 + secretKey: 12345678 + whiteRemoteAddress: 192.168.1.* + admin: true diff --git a/acl/src/test/resources/conf/plain_acl_delete.yml b/acl/src/test/resources/conf/plain_acl_delete.yml new file mode 100644 index 00000000000..59bd6d4ff29 --- /dev/null +++ b/acl/src/test/resources/conf/plain_acl_delete.yml @@ -0,0 +1,39 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## suggested format + +globalWhiteRemoteAddresses: +- 10.10.103.* +- 192.168.0.* +accounts: +- accessKey: RocketMQ + secretKey: 12345678 + whiteRemoteAddress: 192.168.0.* + admin: false + defaultTopicPerm: DENY + defaultGroupPerm: SUB + topicPerms: + - topicA=DENY + - topicB=PUB|SUB + - topicC=SUB + groupPerms: + - groupA=DENY + - groupB=SUB + - groupC=SUB +- accessKey: rocketmq2 + secretKey: 12345678 + whiteRemoteAddress: 192.168.1.* + admin: true diff --git a/acl/src/test/resources/conf/plain_acl_global_white_addrs.yml b/acl/src/test/resources/conf/plain_acl_global_white_addrs.yml new file mode 100644 index 00000000000..59bd6d4ff29 --- /dev/null +++ b/acl/src/test/resources/conf/plain_acl_global_white_addrs.yml @@ -0,0 +1,39 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## suggested format + +globalWhiteRemoteAddresses: +- 10.10.103.* +- 192.168.0.* +accounts: +- accessKey: RocketMQ + secretKey: 12345678 + whiteRemoteAddress: 192.168.0.* + admin: false + defaultTopicPerm: DENY + defaultGroupPerm: SUB + topicPerms: + - topicA=DENY + - topicB=PUB|SUB + - topicC=SUB + groupPerms: + - groupA=DENY + - groupB=SUB + - groupC=SUB +- accessKey: rocketmq2 + secretKey: 12345678 + whiteRemoteAddress: 192.168.1.* + admin: true diff --git a/acl/src/test/resources/conf/plain_acl_incomplete.yml b/acl/src/test/resources/conf/plain_acl_incomplete.yml new file mode 100644 index 00000000000..0a6bdde7072 --- /dev/null +++ b/acl/src/test/resources/conf/plain_acl_incomplete.yml @@ -0,0 +1,22 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## suggested format + +- accessKey: rocketmq2 + secretKey: + whiteRemoteAddress: 192.168.1.* + # if it is admin, it could access all resources + admin: true \ No newline at end of file diff --git a/acl/src/test/resources/conf/plain_acl_update_create.yml b/acl/src/test/resources/conf/plain_acl_update_create.yml new file mode 100644 index 00000000000..59bd6d4ff29 --- /dev/null +++ b/acl/src/test/resources/conf/plain_acl_update_create.yml @@ -0,0 +1,39 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## suggested format + +globalWhiteRemoteAddresses: +- 10.10.103.* +- 192.168.0.* +accounts: +- accessKey: RocketMQ + secretKey: 12345678 + whiteRemoteAddress: 192.168.0.* + admin: false + defaultTopicPerm: DENY + defaultGroupPerm: SUB + topicPerms: + - topicA=DENY + - topicB=PUB|SUB + - topicC=SUB + groupPerms: + - groupA=DENY + - groupB=SUB + - groupC=SUB +- accessKey: rocketmq2 + secretKey: 12345678 + whiteRemoteAddress: 192.168.1.* + admin: true diff --git a/acl/src/test/resources/conf/plain_acl_with_no_accouts.yml b/acl/src/test/resources/conf/plain_acl_with_no_accouts.yml new file mode 100644 index 00000000000..939f7c98ca6 --- /dev/null +++ b/acl/src/test/resources/conf/plain_acl_with_no_accouts.yml @@ -0,0 +1,20 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## suggested format + +globalWhiteRemoteAddresses: +- 10.10.103.* +- 192.168.0.* \ No newline at end of file diff --git a/acl/src/test/resources/conf/watch/plain_acl_watch.yml b/acl/src/test/resources/conf/watch/plain_acl_watch.yml new file mode 100644 index 00000000000..9d2c3954941 --- /dev/null +++ b/acl/src/test/resources/conf/watch/plain_acl_watch.yml @@ -0,0 +1,25 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## suggested format +accounts: +- accessKey: watchrocketmq + secretKey: 12345678 + whiteRemoteAddress: 127.0.0.1 + admin: true +- accessKey: watchrocketmq1 + secretKey: 88888888 + whiteRemoteAddress: 127.0.0.1 + admin: false diff --git a/acl/src/test/resources/empty_acl_folder_conf/conf/plain_acl.yml b/acl/src/test/resources/empty_acl_folder_conf/conf/plain_acl.yml new file mode 100644 index 00000000000..6ade46723b7 --- /dev/null +++ b/acl/src/test/resources/empty_acl_folder_conf/conf/plain_acl.yml @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +globalWhiteRemoteAddresses: + - 10.10.103.* + - 192.168.0.* diff --git a/acl/src/test/resources/only_acl_folder_conf/conf/acl/plain_acl.yml b/acl/src/test/resources/only_acl_folder_conf/conf/acl/plain_acl.yml new file mode 100644 index 00000000000..cf4ea7f4a5b --- /dev/null +++ b/acl/src/test/resources/only_acl_folder_conf/conf/acl/plain_acl.yml @@ -0,0 +1,39 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## no global white addresses in this file, define them in ../plain_acl.yml +accounts: + - accessKey: RocketMQ + secretKey: 12345678 + whiteRemoteAddress: 192.168.0.* + admin: false + defaultTopicPerm: DENY + defaultGroupPerm: SUB + topicPerms: + - topicA=DENY + - topicB=PUB|SUB + - topicC=SUB + groupPerms: + # the group should convert to retry topic + - groupA=DENY + - groupB=SUB + - groupC=SUB + + - accessKey: rocketmq2 + secretKey: 12345678 + whiteRemoteAddress: 192.168.1.* + # if it is admin, it could access all resources + admin: true + diff --git a/acl/src/test/resources/rmq.logback-test.xml b/acl/src/test/resources/rmq.logback-test.xml new file mode 100644 index 00000000000..8695d52d57c --- /dev/null +++ b/acl/src/test/resources/rmq.logback-test.xml @@ -0,0 +1,36 @@ + + + + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + \ No newline at end of file diff --git a/acl/src/test/resources/update_global_white_addr/conf/acl/empty.yml b/acl/src/test/resources/update_global_white_addr/conf/acl/empty.yml new file mode 100644 index 00000000000..52ff50c2bef --- /dev/null +++ b/acl/src/test/resources/update_global_white_addr/conf/acl/empty.yml @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## suggested format + +accounts: [] diff --git a/acl/src/test/resources/update_global_white_addr/conf/acl/plain_acl.yml b/acl/src/test/resources/update_global_white_addr/conf/acl/plain_acl.yml new file mode 100644 index 00000000000..59bd6d4ff29 --- /dev/null +++ b/acl/src/test/resources/update_global_white_addr/conf/acl/plain_acl.yml @@ -0,0 +1,39 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## suggested format + +globalWhiteRemoteAddresses: +- 10.10.103.* +- 192.168.0.* +accounts: +- accessKey: RocketMQ + secretKey: 12345678 + whiteRemoteAddress: 192.168.0.* + admin: false + defaultTopicPerm: DENY + defaultGroupPerm: SUB + topicPerms: + - topicA=DENY + - topicB=PUB|SUB + - topicC=SUB + groupPerms: + - groupA=DENY + - groupB=SUB + - groupC=SUB +- accessKey: rocketmq2 + secretKey: 12345678 + whiteRemoteAddress: 192.168.1.* + admin: true diff --git a/acl/src/test/resources/update_global_white_addr/conf/plain_acl.yml b/acl/src/test/resources/update_global_white_addr/conf/plain_acl.yml new file mode 100644 index 00000000000..64c6e34169c --- /dev/null +++ b/acl/src/test/resources/update_global_white_addr/conf/plain_acl.yml @@ -0,0 +1,36 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## suggested format + +accounts: +- accessKey: RocketMQ + secretKey: 12345678 + whiteRemoteAddress: 192.168.0.* + admin: false + defaultTopicPerm: DENY + defaultGroupPerm: SUB + topicPerms: + - topicA=DENY + - topicB=PUB|SUB + - topicC=SUB + groupPerms: + - groupA=DENY + - groupB=SUB + - groupC=SUB +- accessKey: rocketmq2 + secretKey: 12345678 + whiteRemoteAddress: 192.168.1.* + admin: true diff --git a/bazel/BUILD.bazel b/bazel/BUILD.bazel new file mode 100644 index 00000000000..a9fd83fea0b --- /dev/null +++ b/bazel/BUILD.bazel @@ -0,0 +1,16 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# \ No newline at end of file diff --git a/bazel/GenTestRules.bzl b/bazel/GenTestRules.bzl new file mode 100644 index 00000000000..fb9b6991de0 --- /dev/null +++ b/bazel/GenTestRules.bzl @@ -0,0 +1,111 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Generate java test rules from given test_files. + +Instead of having to create one test rule per test in the BUILD file, this rule +provides a handy way to create a bunch of test rules for the specified test +files. + +""" + +def GenTestRules( + name, + test_files, + deps, + exclude_tests = [], + default_test_size = "small", + small_tests = [], + medium_tests = [], + large_tests = [], + enormous_tests = [], + resources = [], + data = [], + flaky_tests = [], + tags = [], + prefix = "", + jvm_flags = [], + args = [], + visibility = None, + shard_count = 1): + for test in _get_test_names(test_files): + if test in exclude_tests: + continue + test_size = default_test_size + if test in small_tests: + test_size = "small" + if test in medium_tests: + test_size = "medium" + if test in large_tests: + test_size = "large" + if test in enormous_tests: + test_size = "enormous" + flaky = 0 + if (test in flaky_tests) or ("flaky" in tags): + flaky = 1 + java_class = _package_from_path( + native.package_name() + "/" + _strip_right(test, ".java"), + ) + package = java_class[:java_class.rfind(".")] + native.java_test( + name = prefix + test, + runtime_deps = deps, + resources = resources, + size = test_size, + jvm_flags = jvm_flags, + args = args, + flaky = flaky, + tags = tags, + test_class = java_class, + visibility = visibility, + shard_count = shard_count, + data = data, + ) + +def _get_test_names(test_files): + test_names = [] + for test_file in test_files: + if not test_file.endswith("Test.java") and not test_file.endswith("IT.java"): + continue + test_names += [test_file[:-5]] + return test_names + +def _package_from_path(package_path, src_impls = None): + src_impls = src_impls or ["javatests/", "java/"] + for src_impl in src_impls: + if not src_impl.endswith("/"): + src_impl += "/" + index = _index_of_end(package_path, src_impl) + if index >= 0: + package_path = package_path[index:] + break + return package_path.replace("/", ".") + +def _strip_right(str, suffix): + """Returns str without the suffix if it ends with suffix.""" + if str.endswith(suffix): + return str[0:len(str) - len(suffix)] + else: + return str + +def _index_of_end(str, part): + """If part is in str, return the index of the first character after part. + Return -1 if part is not in str.""" + index = str.find(part) + if index >= 0: + return index + len(part) + return -1 diff --git a/broker/BUILD.bazel b/broker/BUILD.bazel new file mode 100644 index 00000000000..ab413d3d060 --- /dev/null +++ b/broker/BUILD.bazel @@ -0,0 +1,97 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +load("//bazel:GenTestRules.bzl", "GenTestRules") + +java_library( + name = "broker", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//acl", + "//client", + "//common", + "//filter", + "//remoting", + "//srvutil", + "//store", + "@maven//:ch_qos_logback_logback_classic", + "@maven//:com_alibaba_fastjson", + "@maven//:com_github_luben_zstd_jni", + "@maven//:com_google_guava_guava", + "@maven//:com_googlecode_concurrentlinkedhashmap_concurrentlinkedhashmap_lru", + "@maven//:commons_cli_commons_cli", + "@maven//:commons_collections_commons_collections", + "@maven//:commons_io_commons_io", + "@maven//:commons_validator_commons_validator", + "@maven//:io_netty_netty_all", + "@maven//:io_openmessaging_storage_dledger", + "@maven//:io_opentelemetry_opentelemetry_api", + "@maven//:io_opentelemetry_opentelemetry_context", + "@maven//:io_opentelemetry_opentelemetry_exporter_otlp", + "@maven//:io_opentelemetry_opentelemetry_exporter_prometheus", + "@maven//:io_opentelemetry_opentelemetry_exporter_logging", + "@maven//:io_opentelemetry_opentelemetry_exporter_logging_otlp", + "@maven//:io_opentelemetry_opentelemetry_sdk", + "@maven//:io_opentelemetry_opentelemetry_sdk_common", + "@maven//:io_opentelemetry_opentelemetry_sdk_metrics", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:org_lz4_lz4_java", + "@maven//:io_github_aliyunmq_rocketmq_slf4j_api", + "@maven//:io_github_aliyunmq_rocketmq_logback_classic", + "@maven//:org_slf4j_jul_to_slf4j", + "@maven//:io_github_aliyunmq_rocketmq_shaded_slf4j_api_bridge", + "@maven//:org_apache_rocketmq_rocketmq_rocksdb", + "@maven//:net_java_dev_jna_jna", + ], +) + +java_library( + name = "tests", + srcs = glob(["src/test/java/**/*.java"]), + resources = [ + "src/test/resources/META-INF/service/org.apache.rocketmq.acl.AccessValidator", + "src/test/resources/META-INF/service/org.apache.rocketmq.broker.transaction.AbstractTransactionalMessageCheckListener", + "src/test/resources/META-INF/service/org.apache.rocketmq.broker.transaction.TransactionalMessageService", + "src/test/resources/rmq.logback-test.xml", + ], + visibility = ["//visibility:public"], + deps = [ + ":broker", + "//:test_deps", + "//acl", + "//client", + "//common", + "//filter", + "//remoting", + "//store", + "@maven//:com_alibaba_fastjson", + "@maven//:com_google_guava_guava", + "@maven//:io_netty_netty_all", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:io_github_aliyunmq_rocketmq_slf4j_api", + "@maven//:org_powermock_powermock_core", + "@maven//:io_opentelemetry_opentelemetry_api", + ], +) + +GenTestRules( + name = "GeneratedTestRules", + test_files = glob(["src/test/java/**/*Test.java"]), + deps = [ + ":tests", + ], +) diff --git a/broker/pom.xml b/broker/pom.xml index d655959a9a8..3eef8846b78 100644 --- a/broker/pom.xml +++ b/broker/pom.xml @@ -1,25 +1,19 @@ - + org.apache.rocketmq rocketmq-all - 4.2.0 + 5.2.0 4.0.0 @@ -27,10 +21,14 @@ rocketmq-broker rocketmq-broker ${project.version} + + ${basedir}/.. + + ${project.groupId} - rocketmq-common + rocketmq-remoting ${project.groupId} @@ -38,7 +36,15 @@ ${project.groupId} - rocketmq-remoting + rocketmq-tiered-store + + + io.github.aliyunmq + rocketmq-slf4j-api + + + io.github.aliyunmq + rocketmq-logback-classic ${project.groupId} @@ -53,12 +59,12 @@ rocketmq-filter - ch.qos.logback - logback-classic + ${project.groupId} + rocketmq-acl - ch.qos.logback - logback-core + commons-io + commons-io com.alibaba @@ -68,13 +74,29 @@ org.javassist javassist + + org.bouncycastle + bcpkix-jdk15on + + + com.googlecode.concurrentlinkedhashmap + concurrentlinkedhashmap-lru + + + io.github.aliyunmq + rocketmq-shaded-slf4j-api-bridge + + + org.slf4j + jul-to-slf4j + maven-surefire-plugin - 2.19.1 + ${maven-surefire-plugin.version} 1 false diff --git a/broker/src/main/java/org/apache/rocketmq/broker/BrokerController.java b/broker/src/main/java/org/apache/rocketmq/broker/BrokerController.java index 0a6f0b45e9d..af90e5f87eb 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/BrokerController.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/BrokerController.java @@ -18,18 +18,33 @@ import java.io.IOException; import java.net.InetSocketAddress; +import java.util.AbstractMap; import java.util.ArrayList; -import java.util.Iterator; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; +import java.util.stream.Collectors; + +import com.google.common.collect.Lists; + +import org.apache.rocketmq.acl.AccessValidator; +import org.apache.rocketmq.acl.plain.PlainAccessValidator; import org.apache.rocketmq.broker.client.ClientHousekeepingService; import org.apache.rocketmq.broker.client.ConsumerIdsChangeListener; import org.apache.rocketmq.broker.client.ConsumerManager; @@ -37,105 +52,251 @@ import org.apache.rocketmq.broker.client.ProducerManager; import org.apache.rocketmq.broker.client.net.Broker2Client; import org.apache.rocketmq.broker.client.rebalance.RebalanceLockManager; +import org.apache.rocketmq.broker.coldctr.ColdDataCgCtrService; +import org.apache.rocketmq.broker.coldctr.ColdDataPullRequestHoldService; +import org.apache.rocketmq.broker.controller.ReplicasManager; +import org.apache.rocketmq.broker.dledger.DLedgerRoleChangeHandler; +import org.apache.rocketmq.broker.failover.EscapeBridge; import org.apache.rocketmq.broker.filter.CommitLogDispatcherCalcBitMap; import org.apache.rocketmq.broker.filter.ConsumerFilterManager; -import org.apache.rocketmq.broker.filtersrv.FilterServerManager; import org.apache.rocketmq.broker.latency.BrokerFastFailure; -import org.apache.rocketmq.broker.latency.BrokerFixedThreadPoolExecutor; +import org.apache.rocketmq.broker.longpolling.LmqPullRequestHoldService; import org.apache.rocketmq.broker.longpolling.NotifyMessageArrivingListener; import org.apache.rocketmq.broker.longpolling.PullRequestHoldService; +import org.apache.rocketmq.broker.metrics.BrokerMetricsManager; import org.apache.rocketmq.broker.mqtrace.ConsumeMessageHook; import org.apache.rocketmq.broker.mqtrace.SendMessageHook; +import org.apache.rocketmq.broker.offset.BroadcastOffsetManager; import org.apache.rocketmq.broker.offset.ConsumerOffsetManager; +import org.apache.rocketmq.broker.offset.ConsumerOrderInfoManager; +import org.apache.rocketmq.broker.offset.LmqConsumerOffsetManager; +import org.apache.rocketmq.broker.offset.RocksDBConsumerOffsetManager; +import org.apache.rocketmq.broker.offset.RocksDBLmqConsumerOffsetManager; import org.apache.rocketmq.broker.out.BrokerOuterAPI; -import org.apache.rocketmq.broker.plugin.MessageStoreFactory; -import org.apache.rocketmq.broker.plugin.MessageStorePluginContext; +import org.apache.rocketmq.broker.plugin.BrokerAttachedPlugin; +import org.apache.rocketmq.broker.processor.AckMessageProcessor; import org.apache.rocketmq.broker.processor.AdminBrokerProcessor; +import org.apache.rocketmq.broker.processor.ChangeInvisibleTimeProcessor; import org.apache.rocketmq.broker.processor.ClientManageProcessor; import org.apache.rocketmq.broker.processor.ConsumerManageProcessor; import org.apache.rocketmq.broker.processor.EndTransactionProcessor; +import org.apache.rocketmq.broker.processor.NotificationProcessor; +import org.apache.rocketmq.broker.processor.PeekMessageProcessor; +import org.apache.rocketmq.broker.processor.PollingInfoProcessor; +import org.apache.rocketmq.broker.processor.PopInflightMessageCounter; +import org.apache.rocketmq.broker.processor.PopMessageProcessor; import org.apache.rocketmq.broker.processor.PullMessageProcessor; +import org.apache.rocketmq.broker.processor.QueryAssignmentProcessor; import org.apache.rocketmq.broker.processor.QueryMessageProcessor; +import org.apache.rocketmq.broker.processor.ReplyMessageProcessor; import org.apache.rocketmq.broker.processor.SendMessageProcessor; +import org.apache.rocketmq.broker.schedule.ScheduleMessageService; import org.apache.rocketmq.broker.slave.SlaveSynchronize; +import org.apache.rocketmq.broker.subscription.LmqSubscriptionGroupManager; +import org.apache.rocketmq.broker.subscription.RocksDBLmqSubscriptionGroupManager; +import org.apache.rocketmq.broker.subscription.RocksDBSubscriptionGroupManager; import org.apache.rocketmq.broker.subscription.SubscriptionGroupManager; +import org.apache.rocketmq.broker.topic.LmqTopicConfigManager; +import org.apache.rocketmq.broker.topic.RocksDBLmqTopicConfigManager; +import org.apache.rocketmq.broker.topic.RocksDBTopicConfigManager; import org.apache.rocketmq.broker.topic.TopicConfigManager; +import org.apache.rocketmq.broker.topic.TopicQueueMappingCleanService; +import org.apache.rocketmq.broker.topic.TopicQueueMappingManager; +import org.apache.rocketmq.broker.topic.TopicRouteInfoManager; +import org.apache.rocketmq.broker.transaction.AbstractTransactionalMessageCheckListener; +import org.apache.rocketmq.broker.transaction.TransactionMetricsFlushService; +import org.apache.rocketmq.broker.transaction.TransactionalMessageCheckService; +import org.apache.rocketmq.broker.transaction.TransactionalMessageService; +import org.apache.rocketmq.broker.transaction.queue.DefaultTransactionalMessageCheckListener; +import org.apache.rocketmq.broker.transaction.queue.TransactionalMessageBridge; +import org.apache.rocketmq.broker.transaction.queue.TransactionalMessageServiceImpl; +import org.apache.rocketmq.broker.util.HookUtils; +import org.apache.rocketmq.common.AbstractBrokerRunnable; import org.apache.rocketmq.common.BrokerConfig; -import org.apache.rocketmq.common.Configuration; +import org.apache.rocketmq.common.BrokerIdentity; +import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.ThreadFactoryImpl; import org.apache.rocketmq.common.TopicConfig; import org.apache.rocketmq.common.UtilAll; import org.apache.rocketmq.common.constant.LoggerName; import org.apache.rocketmq.common.constant.PermName; -import org.apache.rocketmq.common.namesrv.RegisterBrokerResult; -import org.apache.rocketmq.common.protocol.RequestCode; -import org.apache.rocketmq.common.protocol.body.TopicConfigSerializeWrapper; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; import org.apache.rocketmq.common.stats.MomentStatsItem; +import org.apache.rocketmq.common.utils.ServiceProvider; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.Configuration; import org.apache.rocketmq.remoting.RPCHook; import org.apache.rocketmq.remoting.RemotingServer; +import org.apache.rocketmq.remoting.common.TlsMode; import org.apache.rocketmq.remoting.netty.NettyClientConfig; import org.apache.rocketmq.remoting.netty.NettyRemotingServer; import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; import org.apache.rocketmq.remoting.netty.NettyServerConfig; import org.apache.rocketmq.remoting.netty.RequestTask; +import org.apache.rocketmq.remoting.netty.TlsSystemConfig; +import org.apache.rocketmq.remoting.protocol.BrokerSyncInfo; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.body.BrokerMemberGroup; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigAndMappingSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.namesrv.RegisterBrokerResult; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingDetail; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingInfo; +import org.apache.rocketmq.srvutil.FileWatchService; import org.apache.rocketmq.store.DefaultMessageStore; import org.apache.rocketmq.store.MessageArrivingListener; import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.RocksDBMessageStore; import org.apache.rocketmq.store.config.BrokerRole; import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.dledger.DLedgerCommitLog; +import org.apache.rocketmq.store.hook.PutMessageHook; +import org.apache.rocketmq.store.hook.SendMessageBackHook; +import org.apache.rocketmq.store.plugin.MessageStoreFactory; +import org.apache.rocketmq.store.plugin.MessageStorePluginContext; import org.apache.rocketmq.store.stats.BrokerStats; import org.apache.rocketmq.store.stats.BrokerStatsManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.stats.LmqBrokerStatsManager; +import org.apache.rocketmq.store.timer.TimerCheckpoint; +import org.apache.rocketmq.store.timer.TimerMessageStore; +import org.apache.rocketmq.store.timer.TimerMetrics; public class BrokerController { - private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + protected static final Logger LOG = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); private static final Logger LOG_PROTECTION = LoggerFactory.getLogger(LoggerName.PROTECTION_LOGGER_NAME); private static final Logger LOG_WATER_MARK = LoggerFactory.getLogger(LoggerName.WATER_MARK_LOGGER_NAME); - private final BrokerConfig brokerConfig; + protected static final int HA_ADDRESS_MIN_LENGTH = 6; + + protected final BrokerConfig brokerConfig; private final NettyServerConfig nettyServerConfig; private final NettyClientConfig nettyClientConfig; - private final MessageStoreConfig messageStoreConfig; - private final ConsumerOffsetManager consumerOffsetManager; - private final ConsumerManager consumerManager; - private final ConsumerFilterManager consumerFilterManager; - private final ProducerManager producerManager; - private final ClientHousekeepingService clientHousekeepingService; - private final PullMessageProcessor pullMessageProcessor; - private final PullRequestHoldService pullRequestHoldService; - private final MessageArrivingListener messageArrivingListener; - private final Broker2Client broker2Client; - private final SubscriptionGroupManager subscriptionGroupManager; - private final ConsumerIdsChangeListener consumerIdsChangeListener; + protected final MessageStoreConfig messageStoreConfig; + protected final ConsumerOffsetManager consumerOffsetManager; + protected final BroadcastOffsetManager broadcastOffsetManager; + protected final ConsumerManager consumerManager; + protected final ConsumerFilterManager consumerFilterManager; + protected final ConsumerOrderInfoManager consumerOrderInfoManager; + protected final PopInflightMessageCounter popInflightMessageCounter; + protected final ProducerManager producerManager; + protected final ScheduleMessageService scheduleMessageService; + protected final ClientHousekeepingService clientHousekeepingService; + protected final PullMessageProcessor pullMessageProcessor; + protected final PeekMessageProcessor peekMessageProcessor; + protected final PopMessageProcessor popMessageProcessor; + protected final AckMessageProcessor ackMessageProcessor; + protected final ChangeInvisibleTimeProcessor changeInvisibleTimeProcessor; + protected final NotificationProcessor notificationProcessor; + protected final PollingInfoProcessor pollingInfoProcessor; + protected final QueryAssignmentProcessor queryAssignmentProcessor; + protected final ClientManageProcessor clientManageProcessor; + protected final SendMessageProcessor sendMessageProcessor; + protected final ReplyMessageProcessor replyMessageProcessor; + protected final PullRequestHoldService pullRequestHoldService; + protected final MessageArrivingListener messageArrivingListener; + protected final Broker2Client broker2Client; + protected final ConsumerIdsChangeListener consumerIdsChangeListener; + protected final EndTransactionProcessor endTransactionProcessor; private final RebalanceLockManager rebalanceLockManager = new RebalanceLockManager(); - private final BrokerOuterAPI brokerOuterAPI; - private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl( - "BrokerControllerScheduledThread")); - private final SlaveSynchronize slaveSynchronize; - private final BlockingQueue sendThreadPoolQueue; - private final BlockingQueue pullThreadPoolQueue; - private final BlockingQueue queryThreadPoolQueue; - private final BlockingQueue clientManagerThreadPoolQueue; - private final BlockingQueue consumerManagerThreadPoolQueue; - private final FilterServerManager filterServerManager; - private final BrokerStatsManager brokerStatsManager; - private final List sendMessageHookList = new ArrayList(); - private final List consumeMessageHookList = new ArrayList(); - private MessageStore messageStore; - private RemotingServer remotingServer; - private RemotingServer fastRemotingServer; - private TopicConfigManager topicConfigManager; - private ExecutorService sendMessageExecutor; - private ExecutorService pullMessageExecutor; - private ExecutorService queryMessageExecutor; - private ExecutorService adminBrokerExecutor; - private ExecutorService clientManageExecutor; - private ExecutorService consumerManageExecutor; - private boolean updateMasterHAServerAddrPeriodically = false; + private final TopicRouteInfoManager topicRouteInfoManager; + protected BrokerOuterAPI brokerOuterAPI; + protected ScheduledExecutorService scheduledExecutorService; + protected ScheduledExecutorService syncBrokerMemberGroupExecutorService; + protected ScheduledExecutorService brokerHeartbeatExecutorService; + protected final SlaveSynchronize slaveSynchronize; + protected final BlockingQueue sendThreadPoolQueue; + protected final BlockingQueue putThreadPoolQueue; + protected final BlockingQueue ackThreadPoolQueue; + protected final BlockingQueue pullThreadPoolQueue; + protected final BlockingQueue litePullThreadPoolQueue; + protected final BlockingQueue replyThreadPoolQueue; + protected final BlockingQueue queryThreadPoolQueue; + protected final BlockingQueue clientManagerThreadPoolQueue; + protected final BlockingQueue heartbeatThreadPoolQueue; + protected final BlockingQueue consumerManagerThreadPoolQueue; + protected final BlockingQueue endTransactionThreadPoolQueue; + protected final BlockingQueue adminBrokerThreadPoolQueue; + protected final BlockingQueue loadBalanceThreadPoolQueue; + protected final BrokerStatsManager brokerStatsManager; + protected final List sendMessageHookList = new ArrayList<>(); + protected final List consumeMessageHookList = new ArrayList<>(); + protected MessageStore messageStore; + protected RemotingServer remotingServer; + protected CountDownLatch remotingServerStartLatch; + protected RemotingServer fastRemotingServer; + protected TopicConfigManager topicConfigManager; + protected SubscriptionGroupManager subscriptionGroupManager; + protected TopicQueueMappingManager topicQueueMappingManager; + protected ExecutorService sendMessageExecutor; + protected ExecutorService pullMessageExecutor; + protected ExecutorService litePullMessageExecutor; + protected ExecutorService putMessageFutureExecutor; + protected ExecutorService ackMessageExecutor; + protected ExecutorService replyMessageExecutor; + protected ExecutorService queryMessageExecutor; + protected ExecutorService adminBrokerExecutor; + protected ExecutorService clientManageExecutor; + protected ExecutorService heartbeatExecutor; + protected ExecutorService consumerManageExecutor; + protected ExecutorService loadBalanceExecutor; + protected ExecutorService endTransactionExecutor; + protected boolean updateMasterHAServerAddrPeriodically = false; private BrokerStats brokerStats; private InetSocketAddress storeHost; - private BrokerFastFailure brokerFastFailure; + private TimerMessageStore timerMessageStore; + private TimerCheckpoint timerCheckpoint; + protected BrokerFastFailure brokerFastFailure; private Configuration configuration; + protected TopicQueueMappingCleanService topicQueueMappingCleanService; + protected FileWatchService fileWatchService; + protected TransactionalMessageCheckService transactionalMessageCheckService; + protected TransactionalMessageService transactionalMessageService; + protected AbstractTransactionalMessageCheckListener transactionalMessageCheckListener; + protected Map accessValidatorMap = new HashMap<>(); + protected volatile boolean shutdown = false; + protected ShutdownHook shutdownHook; + private volatile boolean isScheduleServiceStart = false; + private volatile boolean isTransactionCheckServiceStart = false; + protected volatile BrokerMemberGroup brokerMemberGroup; + protected EscapeBridge escapeBridge; + protected List brokerAttachedPlugins = new ArrayList<>(); + protected volatile long shouldStartTime; + private BrokerPreOnlineService brokerPreOnlineService; + protected volatile boolean isIsolated = false; + protected volatile long minBrokerIdInGroup = 0; + protected volatile String minBrokerAddrInGroup = null; + private final Lock lock = new ReentrantLock(); + protected final List> scheduledFutures = new ArrayList<>(); + protected ReplicasManager replicasManager; + private long lastSyncTimeMs = System.currentTimeMillis(); + private BrokerMetricsManager brokerMetricsManager; + private ColdDataPullRequestHoldService coldDataPullRequestHoldService; + private ColdDataCgCtrService coldDataCgCtrService; + private TransactionMetricsFlushService transactionMetricsFlushService; + + public BrokerController( + final BrokerConfig brokerConfig, + final NettyServerConfig nettyServerConfig, + final NettyClientConfig nettyClientConfig, + final MessageStoreConfig messageStoreConfig, + final ShutdownHook shutdownHook + ) { + this(brokerConfig, nettyServerConfig, nettyClientConfig, messageStoreConfig); + this.shutdownHook = shutdownHook; + } + + public BrokerController( + final BrokerConfig brokerConfig, + final MessageStoreConfig messageStoreConfig + ) { + this(brokerConfig, null, null, messageStoreConfig); + } public BrokerController( final BrokerConfig brokerConfig, @@ -147,38 +308,112 @@ public BrokerController( this.nettyServerConfig = nettyServerConfig; this.nettyClientConfig = nettyClientConfig; this.messageStoreConfig = messageStoreConfig; - this.consumerOffsetManager = new ConsumerOffsetManager(this); - this.topicConfigManager = new TopicConfigManager(this); + this.setStoreHost(new InetSocketAddress(this.getBrokerConfig().getBrokerIP1(), getListenPort())); + this.brokerStatsManager = messageStoreConfig.isEnableLmq() ? new LmqBrokerStatsManager(this.brokerConfig.getBrokerClusterName(), this.brokerConfig.isEnableDetailStat()) : new BrokerStatsManager(this.brokerConfig.getBrokerClusterName(), this.brokerConfig.isEnableDetailStat()); + this.broadcastOffsetManager = new BroadcastOffsetManager(this); + if (this.messageStoreConfig.isEnableRocksDBStore()) { + this.topicConfigManager = messageStoreConfig.isEnableLmq() ? new RocksDBLmqTopicConfigManager(this) : new RocksDBTopicConfigManager(this); + this.subscriptionGroupManager = messageStoreConfig.isEnableLmq() ? new RocksDBLmqSubscriptionGroupManager(this) : new RocksDBSubscriptionGroupManager(this); + this.consumerOffsetManager = messageStoreConfig.isEnableLmq() ? new RocksDBLmqConsumerOffsetManager(this) : new RocksDBConsumerOffsetManager(this); + } else { + this.topicConfigManager = messageStoreConfig.isEnableLmq() ? new LmqTopicConfigManager(this) : new TopicConfigManager(this); + this.subscriptionGroupManager = messageStoreConfig.isEnableLmq() ? new LmqSubscriptionGroupManager(this) : new SubscriptionGroupManager(this); + this.consumerOffsetManager = messageStoreConfig.isEnableLmq() ? new LmqConsumerOffsetManager(this) : new ConsumerOffsetManager(this); + } + this.topicQueueMappingManager = new TopicQueueMappingManager(this); this.pullMessageProcessor = new PullMessageProcessor(this); - this.pullRequestHoldService = new PullRequestHoldService(this); - this.messageArrivingListener = new NotifyMessageArrivingListener(this.pullRequestHoldService); + this.peekMessageProcessor = new PeekMessageProcessor(this); + this.pullRequestHoldService = messageStoreConfig.isEnableLmq() ? new LmqPullRequestHoldService(this) : new PullRequestHoldService(this); + this.popMessageProcessor = new PopMessageProcessor(this); + this.notificationProcessor = new NotificationProcessor(this); + this.pollingInfoProcessor = new PollingInfoProcessor(this); + this.ackMessageProcessor = new AckMessageProcessor(this); + this.changeInvisibleTimeProcessor = new ChangeInvisibleTimeProcessor(this); + this.sendMessageProcessor = new SendMessageProcessor(this); + this.replyMessageProcessor = new ReplyMessageProcessor(this); + this.messageArrivingListener = new NotifyMessageArrivingListener(this.pullRequestHoldService, this.popMessageProcessor, this.notificationProcessor); this.consumerIdsChangeListener = new DefaultConsumerIdsChangeListener(this); - this.consumerManager = new ConsumerManager(this.consumerIdsChangeListener); + this.consumerManager = new ConsumerManager(this.consumerIdsChangeListener, this.brokerStatsManager, this.brokerConfig); + this.producerManager = new ProducerManager(this.brokerStatsManager); this.consumerFilterManager = new ConsumerFilterManager(this); - this.producerManager = new ProducerManager(); + this.consumerOrderInfoManager = new ConsumerOrderInfoManager(this); + this.popInflightMessageCounter = new PopInflightMessageCounter(this); this.clientHousekeepingService = new ClientHousekeepingService(this); this.broker2Client = new Broker2Client(this); - this.subscriptionGroupManager = new SubscriptionGroupManager(this); - this.brokerOuterAPI = new BrokerOuterAPI(nettyClientConfig); - this.filterServerManager = new FilterServerManager(this); - - this.slaveSynchronize = new SlaveSynchronize(this); + this.scheduleMessageService = new ScheduleMessageService(this); + this.coldDataPullRequestHoldService = new ColdDataPullRequestHoldService(this); + this.coldDataCgCtrService = new ColdDataCgCtrService(this); - this.sendThreadPoolQueue = new LinkedBlockingQueue(this.brokerConfig.getSendThreadPoolQueueCapacity()); - this.pullThreadPoolQueue = new LinkedBlockingQueue(this.brokerConfig.getPullThreadPoolQueueCapacity()); - this.queryThreadPoolQueue = new LinkedBlockingQueue(this.brokerConfig.getQueryThreadPoolQueueCapacity()); - this.clientManagerThreadPoolQueue = new LinkedBlockingQueue(this.brokerConfig.getClientManagerThreadPoolQueueCapacity()); - this.consumerManagerThreadPoolQueue = new LinkedBlockingQueue(this.brokerConfig.getConsumerManagerThreadPoolQueueCapacity()); + if (nettyClientConfig != null) { + this.brokerOuterAPI = new BrokerOuterAPI(nettyClientConfig); + } - this.brokerStatsManager = new BrokerStatsManager(this.brokerConfig.getBrokerClusterName()); - this.setStoreHost(new InetSocketAddress(this.getBrokerConfig().getBrokerIP1(), this.getNettyServerConfig().getListenPort())); + this.queryAssignmentProcessor = new QueryAssignmentProcessor(this); + this.clientManageProcessor = new ClientManageProcessor(this); + this.slaveSynchronize = new SlaveSynchronize(this); + this.endTransactionProcessor = new EndTransactionProcessor(this); + + this.sendThreadPoolQueue = new LinkedBlockingQueue<>(this.brokerConfig.getSendThreadPoolQueueCapacity()); + this.putThreadPoolQueue = new LinkedBlockingQueue<>(this.brokerConfig.getPutThreadPoolQueueCapacity()); + this.pullThreadPoolQueue = new LinkedBlockingQueue<>(this.brokerConfig.getPullThreadPoolQueueCapacity()); + this.litePullThreadPoolQueue = new LinkedBlockingQueue<>(this.brokerConfig.getLitePullThreadPoolQueueCapacity()); + + this.ackThreadPoolQueue = new LinkedBlockingQueue<>(this.brokerConfig.getAckThreadPoolQueueCapacity()); + this.replyThreadPoolQueue = new LinkedBlockingQueue<>(this.brokerConfig.getReplyThreadPoolQueueCapacity()); + this.queryThreadPoolQueue = new LinkedBlockingQueue<>(this.brokerConfig.getQueryThreadPoolQueueCapacity()); + this.clientManagerThreadPoolQueue = new LinkedBlockingQueue<>(this.brokerConfig.getClientManagerThreadPoolQueueCapacity()); + this.consumerManagerThreadPoolQueue = new LinkedBlockingQueue<>(this.brokerConfig.getConsumerManagerThreadPoolQueueCapacity()); + this.heartbeatThreadPoolQueue = new LinkedBlockingQueue<>(this.brokerConfig.getHeartbeatThreadPoolQueueCapacity()); + this.endTransactionThreadPoolQueue = new LinkedBlockingQueue<>(this.brokerConfig.getEndTransactionPoolQueueCapacity()); + this.adminBrokerThreadPoolQueue = new LinkedBlockingQueue<>(this.brokerConfig.getAdminBrokerThreadPoolQueueCapacity()); + this.loadBalanceThreadPoolQueue = new LinkedBlockingQueue<>(this.brokerConfig.getLoadBalanceThreadPoolQueueCapacity()); this.brokerFastFailure = new BrokerFastFailure(this); + + String brokerConfigPath; + if (brokerConfig.getBrokerConfigPath() != null && !brokerConfig.getBrokerConfigPath().isEmpty()) { + brokerConfigPath = brokerConfig.getBrokerConfigPath(); + } else { + brokerConfigPath = BrokerPathConfigHelper.getBrokerConfigPath(); + } this.configuration = new Configuration( - log, - BrokerPathConfigHelper.getBrokerConfigPath(), + LOG, + brokerConfigPath, this.brokerConfig, this.nettyServerConfig, this.nettyClientConfig, this.messageStoreConfig ); + + this.brokerStatsManager.setProduerStateGetter(new BrokerStatsManager.StateGetter() { + @Override + public boolean online(String instanceId, String group, String topic) { + if (getTopicConfigManager().getTopicConfigTable().containsKey(NamespaceUtil.wrapNamespace(instanceId, topic))) { + return getProducerManager().groupOnline(NamespaceUtil.wrapNamespace(instanceId, group)); + } else { + return getProducerManager().groupOnline(group); + } + } + }); + this.brokerStatsManager.setConsumerStateGetter(new BrokerStatsManager.StateGetter() { + @Override + public boolean online(String instanceId, String group, String topic) { + String topicFullName = NamespaceUtil.wrapNamespace(instanceId, topic); + if (getTopicConfigManager().getTopicConfigTable().containsKey(topicFullName)) { + return getConsumerManager().findSubscriptionData(NamespaceUtil.wrapNamespace(instanceId, group), topicFullName) != null; + } else { + return getConsumerManager().findSubscriptionData(group, topic) != null; + } + } + }); + + this.brokerMemberGroup = new BrokerMemberGroup(this.brokerConfig.getBrokerClusterName(), this.brokerConfig.getBrokerName()); + this.brokerMemberGroup.getBrokerAddrs().put(this.brokerConfig.getBrokerId(), this.getBrokerAddr()); + + this.escapeBridge = new EscapeBridge(this); + + this.topicRouteInfoManager = new TopicRouteInfoManager(this); + + if (this.brokerConfig.isEnableSlaveActingMaster() && !this.brokerConfig.isSkipPreOnline()) { + this.brokerPreOnlineService = new BrokerPreOnlineService(this); + } } public BrokerConfig getBrokerConfig() { @@ -189,6 +424,10 @@ public NettyServerConfig getNettyServerConfig() { return nettyServerConfig; } + public NettyClientConfig getNettyClientConfig() { + return nettyClientConfig; + } + public BlockingQueue getPullThreadPoolQueue() { return pullThreadPoolQueue; } @@ -197,166 +436,220 @@ public BlockingQueue getQueryThreadPoolQueue() { return queryThreadPoolQueue; } - public boolean initialize() throws CloneNotSupportedException { - boolean result = this.topicConfigManager.load(); + public BrokerMetricsManager getBrokerMetricsManager() { + return brokerMetricsManager; + } - result = result && this.consumerOffsetManager.load(); - result = result && this.subscriptionGroupManager.load(); - result = result && this.consumerFilterManager.load(); + protected void initializeRemotingServer() throws CloneNotSupportedException { + this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.clientHousekeepingService); + NettyServerConfig fastConfig = (NettyServerConfig) this.nettyServerConfig.clone(); - if (result) { - try { - this.messageStore = - new DefaultMessageStore(this.messageStoreConfig, this.brokerStatsManager, this.messageArrivingListener, - this.brokerConfig); - this.brokerStats = new BrokerStats((DefaultMessageStore) this.messageStore); - //load plugin - MessageStorePluginContext context = new MessageStorePluginContext(messageStoreConfig, brokerStatsManager, messageArrivingListener, brokerConfig); - this.messageStore = MessageStoreFactory.build(context, this.messageStore); - this.messageStore.getDispatcherList().addFirst(new CommitLogDispatcherCalcBitMap(this.brokerConfig, this.consumerFilterManager)); - } catch (IOException e) { - result = false; - log.error("Failed to initialize", e); - } + int listeningPort = nettyServerConfig.getListenPort() - 2; + if (listeningPort < 0) { + listeningPort = 0; } + fastConfig.setListenPort(listeningPort); - result = result && this.messageStore.load(); + this.fastRemotingServer = new NettyRemotingServer(fastConfig, this.clientHousekeepingService); + } - if (result) { - this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.clientHousekeepingService); - NettyServerConfig fastConfig = (NettyServerConfig) this.nettyServerConfig.clone(); - fastConfig.setListenPort(nettyServerConfig.getListenPort() - 2); - this.fastRemotingServer = new NettyRemotingServer(fastConfig, this.clientHousekeepingService); - this.sendMessageExecutor = new BrokerFixedThreadPoolExecutor( - this.brokerConfig.getSendMessageThreadPoolNums(), - this.brokerConfig.getSendMessageThreadPoolNums(), - 1000 * 60, - TimeUnit.MILLISECONDS, - this.sendThreadPoolQueue, - new ThreadFactoryImpl("SendMessageThread_")); - - this.pullMessageExecutor = new BrokerFixedThreadPoolExecutor( - this.brokerConfig.getPullMessageThreadPoolNums(), - this.brokerConfig.getPullMessageThreadPoolNums(), - 1000 * 60, - TimeUnit.MILLISECONDS, - this.pullThreadPoolQueue, - new ThreadFactoryImpl("PullMessageThread_")); - - this.queryMessageExecutor = new BrokerFixedThreadPoolExecutor( - this.brokerConfig.getQueryMessageThreadPoolNums(), - this.brokerConfig.getQueryMessageThreadPoolNums(), - 1000 * 60, - TimeUnit.MILLISECONDS, - this.queryThreadPoolQueue, - new ThreadFactoryImpl("QueryMessageThread_")); - - this.adminBrokerExecutor = - Executors.newFixedThreadPool(this.brokerConfig.getAdminBrokerThreadPoolNums(), new ThreadFactoryImpl( - "AdminBrokerThread_")); - - this.clientManageExecutor = new ThreadPoolExecutor( - this.brokerConfig.getClientManageThreadPoolNums(), - this.brokerConfig.getClientManageThreadPoolNums(), - 1000 * 60, - TimeUnit.MILLISECONDS, - this.clientManagerThreadPoolQueue, - new ThreadFactoryImpl("ClientManageThread_")); - - this.consumerManageExecutor = - Executors.newFixedThreadPool(this.brokerConfig.getConsumerManageThreadPoolNums(), new ThreadFactoryImpl( - "ConsumerManageThread_")); - - this.registerProcessor(); - - final long initialDelay = UtilAll.computNextMorningTimeMillis() - System.currentTimeMillis(); - final long period = 1000 * 60 * 60 * 24; - this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { - @Override - public void run() { - try { - BrokerController.this.getBrokerStats().record(); - } catch (Throwable e) { - log.error("schedule record error.", e); - } - } - }, initialDelay, period, TimeUnit.MILLISECONDS); + /** + * Initialize resources including remoting server and thread executors. + */ + protected void initializeResources() { + this.scheduledExecutorService = ThreadUtils.newScheduledThreadPool(1, + new ThreadFactoryImpl("BrokerControllerScheduledThread", true, getBrokerIdentity())); + + this.sendMessageExecutor = ThreadUtils.newThreadPoolExecutor( + this.brokerConfig.getSendMessageThreadPoolNums(), + this.brokerConfig.getSendMessageThreadPoolNums(), + 1000 * 60, + TimeUnit.MILLISECONDS, + this.sendThreadPoolQueue, + new ThreadFactoryImpl("SendMessageThread_", getBrokerIdentity())); + + this.pullMessageExecutor = ThreadUtils.newThreadPoolExecutor( + this.brokerConfig.getPullMessageThreadPoolNums(), + this.brokerConfig.getPullMessageThreadPoolNums(), + 1000 * 60, + TimeUnit.MILLISECONDS, + this.pullThreadPoolQueue, + new ThreadFactoryImpl("PullMessageThread_", getBrokerIdentity())); + + this.litePullMessageExecutor = ThreadUtils.newThreadPoolExecutor( + this.brokerConfig.getLitePullMessageThreadPoolNums(), + this.brokerConfig.getLitePullMessageThreadPoolNums(), + 1000 * 60, + TimeUnit.MILLISECONDS, + this.litePullThreadPoolQueue, + new ThreadFactoryImpl("LitePullMessageThread_", getBrokerIdentity())); + + this.putMessageFutureExecutor = ThreadUtils.newThreadPoolExecutor( + this.brokerConfig.getPutMessageFutureThreadPoolNums(), + this.brokerConfig.getPutMessageFutureThreadPoolNums(), + 1000 * 60, + TimeUnit.MILLISECONDS, + this.putThreadPoolQueue, + new ThreadFactoryImpl("SendMessageThread_", getBrokerIdentity())); + + this.ackMessageExecutor = ThreadUtils.newThreadPoolExecutor( + this.brokerConfig.getAckMessageThreadPoolNums(), + this.brokerConfig.getAckMessageThreadPoolNums(), + 1000 * 60, + TimeUnit.MILLISECONDS, + this.ackThreadPoolQueue, + new ThreadFactoryImpl("AckMessageThread_", getBrokerIdentity())); + + this.queryMessageExecutor = ThreadUtils.newThreadPoolExecutor( + this.brokerConfig.getQueryMessageThreadPoolNums(), + this.brokerConfig.getQueryMessageThreadPoolNums(), + 1000 * 60, + TimeUnit.MILLISECONDS, + this.queryThreadPoolQueue, + new ThreadFactoryImpl("QueryMessageThread_", getBrokerIdentity())); + + this.adminBrokerExecutor = ThreadUtils.newThreadPoolExecutor( + this.brokerConfig.getAdminBrokerThreadPoolNums(), + this.brokerConfig.getAdminBrokerThreadPoolNums(), + 1000 * 60, + TimeUnit.MILLISECONDS, + this.adminBrokerThreadPoolQueue, + new ThreadFactoryImpl("AdminBrokerThread_", getBrokerIdentity())); + + this.clientManageExecutor = ThreadUtils.newThreadPoolExecutor( + this.brokerConfig.getClientManageThreadPoolNums(), + this.brokerConfig.getClientManageThreadPoolNums(), + 1000 * 60, + TimeUnit.MILLISECONDS, + this.clientManagerThreadPoolQueue, + new ThreadFactoryImpl("ClientManageThread_", getBrokerIdentity())); + + this.heartbeatExecutor = ThreadUtils.newThreadPoolExecutor( + this.brokerConfig.getHeartbeatThreadPoolNums(), + this.brokerConfig.getHeartbeatThreadPoolNums(), + 1000 * 60, + TimeUnit.MILLISECONDS, + this.heartbeatThreadPoolQueue, + new ThreadFactoryImpl("HeartbeatThread_", true, getBrokerIdentity())); + + this.consumerManageExecutor = ThreadUtils.newThreadPoolExecutor( + this.brokerConfig.getConsumerManageThreadPoolNums(), + this.brokerConfig.getConsumerManageThreadPoolNums(), + 1000 * 60, + TimeUnit.MILLISECONDS, + this.consumerManagerThreadPoolQueue, + new ThreadFactoryImpl("ConsumerManageThread_", true, getBrokerIdentity())); + + this.replyMessageExecutor = ThreadUtils.newThreadPoolExecutor( + this.brokerConfig.getProcessReplyMessageThreadPoolNums(), + this.brokerConfig.getProcessReplyMessageThreadPoolNums(), + 1000 * 60, + TimeUnit.MILLISECONDS, + this.replyThreadPoolQueue, + new ThreadFactoryImpl("ProcessReplyMessageThread_", getBrokerIdentity())); + + this.endTransactionExecutor = ThreadUtils.newThreadPoolExecutor( + this.brokerConfig.getEndTransactionThreadPoolNums(), + this.brokerConfig.getEndTransactionThreadPoolNums(), + 1000 * 60, + TimeUnit.MILLISECONDS, + this.endTransactionThreadPoolQueue, + new ThreadFactoryImpl("EndTransactionThread_", getBrokerIdentity())); + + this.loadBalanceExecutor = ThreadUtils.newThreadPoolExecutor( + this.brokerConfig.getLoadBalanceProcessorThreadPoolNums(), + this.brokerConfig.getLoadBalanceProcessorThreadPoolNums(), + 1000 * 60, + TimeUnit.MILLISECONDS, + this.loadBalanceThreadPoolQueue, + new ThreadFactoryImpl("LoadBalanceProcessorThread_", getBrokerIdentity())); + + this.syncBrokerMemberGroupExecutorService = ThreadUtils.newScheduledThreadPool(1, + new ThreadFactoryImpl("BrokerControllerSyncBrokerScheduledThread", getBrokerIdentity())); + this.brokerHeartbeatExecutorService = ThreadUtils.newScheduledThreadPool(1, + new ThreadFactoryImpl("BrokerControllerHeartbeatScheduledThread", getBrokerIdentity())); + + this.topicQueueMappingCleanService = new TopicQueueMappingCleanService(this); + } - this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { - @Override - public void run() { - try { - BrokerController.this.consumerOffsetManager.persist(); - } catch (Throwable e) { - log.error("schedule persist consumerOffset error.", e); - } + protected void initializeBrokerScheduledTasks() { + final long initialDelay = UtilAll.computeNextMorningTimeMillis() - System.currentTimeMillis(); + final long period = TimeUnit.DAYS.toMillis(1); + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + BrokerController.this.getBrokerStats().record(); + } catch (Throwable e) { + LOG.error("BrokerController: failed to record broker stats", e); } - }, 1000 * 10, this.brokerConfig.getFlushConsumerOffsetInterval(), TimeUnit.MILLISECONDS); + } + }, initialDelay, period, TimeUnit.MILLISECONDS); - this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { - @Override - public void run() { - try { - BrokerController.this.consumerFilterManager.persist(); - } catch (Throwable e) { - log.error("schedule persist consumer filter error.", e); - } + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + BrokerController.this.consumerOffsetManager.persist(); + } catch (Throwable e) { + LOG.error( + "BrokerController: failed to persist config file of consumerOffset", e); } - }, 1000 * 10, 1000 * 10, TimeUnit.MILLISECONDS); + } + }, 1000 * 10, this.brokerConfig.getFlushConsumerOffsetInterval(), TimeUnit.MILLISECONDS); - this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { - @Override - public void run() { - try { - BrokerController.this.protectBroker(); - } catch (Throwable e) { - log.error("protectBroker error.", e); - } + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + BrokerController.this.consumerFilterManager.persist(); + BrokerController.this.consumerOrderInfoManager.persist(); + } catch (Throwable e) { + LOG.error( + "BrokerController: failed to persist config file of consumerFilter or consumerOrderInfo", + e); } - }, 3, 3, TimeUnit.MINUTES); + } + }, 1000 * 10, 1000 * 10, TimeUnit.MILLISECONDS); - this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { - @Override - public void run() { - try { - BrokerController.this.printWaterMark(); - } catch (Throwable e) { - log.error("printWaterMark error.", e); - } + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + BrokerController.this.protectBroker(); + } catch (Throwable e) { + LOG.error("BrokerController: failed to protectBroker", e); } - }, 10, 1, TimeUnit.SECONDS); - - this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + } + }, 3, 3, TimeUnit.MINUTES); - @Override - public void run() { - try { - log.info("dispatch behind commit log {} bytes", BrokerController.this.getMessageStore().dispatchBehindBytes()); - } catch (Throwable e) { - log.error("schedule dispatchBehindBytes error.", e); - } + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + BrokerController.this.printWaterMark(); + } catch (Throwable e) { + LOG.error("BrokerController: failed to print broker watermark", e); } - }, 1000 * 10, 1000 * 60, TimeUnit.MILLISECONDS); + } + }, 10, 1, TimeUnit.SECONDS); - if (this.brokerConfig.getNamesrvAddr() != null) { - this.brokerOuterAPI.updateNameServerAddressList(this.brokerConfig.getNamesrvAddr()); - log.info("Set user specified name server address: {}", this.brokerConfig.getNamesrvAddr()); - } else if (this.brokerConfig.isFetchNamesrvAddrByAddressServer()) { - this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { - @Override - public void run() { - try { - BrokerController.this.brokerOuterAPI.fetchNameServerAddr(); - } catch (Throwable e) { - log.error("ScheduledTask fetchNameServerAddr exception", e); - } - } - }, 1000 * 10, 1000 * 60 * 2, TimeUnit.MILLISECONDS); + @Override + public void run() { + try { + LOG.info("Dispatch task fall behind commit log {}bytes", + BrokerController.this.getMessageStore().dispatchBehindBytes()); + } catch (Throwable e) { + LOG.error("Failed to print dispatchBehindBytes", e); + } } + }, 1000 * 10, 1000 * 60, TimeUnit.MILLISECONDS); + if (!messageStoreConfig.isEnableDLegerCommitLog() && !messageStoreConfig.isDuplicationEnable() && !brokerConfig.isEnableControllerMode()) { if (BrokerRole.SLAVE == this.messageStoreConfig.getBrokerRole()) { - if (this.messageStoreConfig.getHaMasterAddress() != null && this.messageStoreConfig.getHaMasterAddress().length() >= 6) { + if (this.messageStoreConfig.getHaMasterAddress() != null && this.messageStoreConfig.getHaMasterAddress().length() >= HA_ADDRESS_MIN_LENGTH) { this.messageStore.updateHaMasterAddress(this.messageStoreConfig.getHaMasterAddress()); this.updateMasterHAServerAddrPeriodically = false; } else { @@ -368,12 +661,21 @@ public void run() { @Override public void run() { try { - BrokerController.this.slaveSynchronize.syncAll(); + if (System.currentTimeMillis() - lastSyncTimeMs > 60 * 1000) { + BrokerController.this.getSlaveSynchronize().syncAll(); + lastSyncTimeMs = System.currentTimeMillis(); + } + + //timer checkpoint, latency-sensitive, so sync it more frequently + if (messageStoreConfig.isTimerWheelEnable()) { + BrokerController.this.getSlaveSynchronize().syncTimerCheckPoint(); + } } catch (Throwable e) { - log.error("ScheduledTask syncAll slave exception", e); + LOG.error("Failed to sync all config for slave.", e); } } - }, 1000 * 10, 1000 * 60, TimeUnit.MILLISECONDS); + }, 1000 * 10, 3 * 1000, TimeUnit.MILLISECONDS); + } else { this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { @@ -382,155 +684,531 @@ public void run() { try { BrokerController.this.printMasterAndSlaveDiff(); } catch (Throwable e) { - log.error("schedule printMasterAndSlaveDiff error.", e); + LOG.error("Failed to print diff of master and slave.", e); } } }, 1000 * 10, 1000 * 60, TimeUnit.MILLISECONDS); } } - return result; + if (this.brokerConfig.isEnableControllerMode()) { + this.updateMasterHAServerAddrPeriodically = true; + } } - public void registerProcessor() { - /** - * SendMessageProcessor - */ - SendMessageProcessor sendProcessor = new SendMessageProcessor(this); - sendProcessor.registerSendMessageHook(sendMessageHookList); - sendProcessor.registerConsumeMessageHook(consumeMessageHookList); - - this.remotingServer.registerProcessor(RequestCode.SEND_MESSAGE, sendProcessor, this.sendMessageExecutor); - this.remotingServer.registerProcessor(RequestCode.SEND_MESSAGE_V2, sendProcessor, this.sendMessageExecutor); - this.remotingServer.registerProcessor(RequestCode.SEND_BATCH_MESSAGE, sendProcessor, this.sendMessageExecutor); - this.remotingServer.registerProcessor(RequestCode.CONSUMER_SEND_MSG_BACK, sendProcessor, this.sendMessageExecutor); - this.fastRemotingServer.registerProcessor(RequestCode.SEND_MESSAGE, sendProcessor, this.sendMessageExecutor); - this.fastRemotingServer.registerProcessor(RequestCode.SEND_MESSAGE_V2, sendProcessor, this.sendMessageExecutor); - this.fastRemotingServer.registerProcessor(RequestCode.SEND_BATCH_MESSAGE, sendProcessor, this.sendMessageExecutor); - this.fastRemotingServer.registerProcessor(RequestCode.CONSUMER_SEND_MSG_BACK, sendProcessor, this.sendMessageExecutor); - /** - * PullMessageProcessor - */ - this.remotingServer.registerProcessor(RequestCode.PULL_MESSAGE, this.pullMessageProcessor, this.pullMessageExecutor); - this.pullMessageProcessor.registerConsumeMessageHook(consumeMessageHookList); - - /** - * QueryMessageProcessor - */ - NettyRequestProcessor queryProcessor = new QueryMessageProcessor(this); - this.remotingServer.registerProcessor(RequestCode.QUERY_MESSAGE, queryProcessor, this.queryMessageExecutor); - this.remotingServer.registerProcessor(RequestCode.VIEW_MESSAGE_BY_ID, queryProcessor, this.queryMessageExecutor); - - this.fastRemotingServer.registerProcessor(RequestCode.QUERY_MESSAGE, queryProcessor, this.queryMessageExecutor); - this.fastRemotingServer.registerProcessor(RequestCode.VIEW_MESSAGE_BY_ID, queryProcessor, this.queryMessageExecutor); - - /** - * ClientManageProcessor - */ - ClientManageProcessor clientProcessor = new ClientManageProcessor(this); - this.remotingServer.registerProcessor(RequestCode.HEART_BEAT, clientProcessor, this.clientManageExecutor); - this.remotingServer.registerProcessor(RequestCode.UNREGISTER_CLIENT, clientProcessor, this.clientManageExecutor); - this.remotingServer.registerProcessor(RequestCode.CHECK_CLIENT_CONFIG, clientProcessor, this.clientManageExecutor); - - this.fastRemotingServer.registerProcessor(RequestCode.HEART_BEAT, clientProcessor, this.clientManageExecutor); - this.fastRemotingServer.registerProcessor(RequestCode.UNREGISTER_CLIENT, clientProcessor, this.clientManageExecutor); - this.fastRemotingServer.registerProcessor(RequestCode.CHECK_CLIENT_CONFIG, clientProcessor, this.clientManageExecutor); + protected void initializeScheduledTasks() { - /** - * ConsumerManageProcessor - */ - ConsumerManageProcessor consumerManageProcessor = new ConsumerManageProcessor(this); - this.remotingServer.registerProcessor(RequestCode.GET_CONSUMER_LIST_BY_GROUP, consumerManageProcessor, this.consumerManageExecutor); - this.remotingServer.registerProcessor(RequestCode.UPDATE_CONSUMER_OFFSET, consumerManageProcessor, this.consumerManageExecutor); - this.remotingServer.registerProcessor(RequestCode.QUERY_CONSUMER_OFFSET, consumerManageProcessor, this.consumerManageExecutor); - - this.fastRemotingServer.registerProcessor(RequestCode.GET_CONSUMER_LIST_BY_GROUP, consumerManageProcessor, this.consumerManageExecutor); - this.fastRemotingServer.registerProcessor(RequestCode.UPDATE_CONSUMER_OFFSET, consumerManageProcessor, this.consumerManageExecutor); - this.fastRemotingServer.registerProcessor(RequestCode.QUERY_CONSUMER_OFFSET, consumerManageProcessor, this.consumerManageExecutor); + initializeBrokerScheduledTasks(); - /** - * EndTransactionProcessor - */ - this.remotingServer.registerProcessor(RequestCode.END_TRANSACTION, new EndTransactionProcessor(this), this.sendMessageExecutor); - this.fastRemotingServer.registerProcessor(RequestCode.END_TRANSACTION, new EndTransactionProcessor(this), this.sendMessageExecutor); + if (this.brokerConfig.getNamesrvAddr() != null) { + this.updateNamesrvAddr(); + LOG.info("Set user specified name server address: {}", this.brokerConfig.getNamesrvAddr()); + // also auto update namesrv if specify + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + BrokerController.this.updateNamesrvAddr(); + } catch (Throwable e) { + LOG.error("Failed to update nameServer address list", e); + } + } + }, 1000 * 10, 1000 * 60 * 2, TimeUnit.MILLISECONDS); + } else if (this.brokerConfig.isFetchNamesrvAddrByAddressServer()) { + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { - /** - * Default - */ - AdminBrokerProcessor adminProcessor = new AdminBrokerProcessor(this); - this.remotingServer.registerDefaultProcessor(adminProcessor, this.adminBrokerExecutor); - this.fastRemotingServer.registerDefaultProcessor(adminProcessor, this.adminBrokerExecutor); + @Override + public void run() { + try { + BrokerController.this.brokerOuterAPI.fetchNameServerAddr(); + } catch (Throwable e) { + LOG.error("Failed to fetch nameServer address", e); + } + } + }, 1000 * 10, this.brokerConfig.getFetchNamesrvAddrInterval(), TimeUnit.MILLISECONDS); + } } - public BrokerStats getBrokerStats() { - return brokerStats; + private void updateNamesrvAddr() { + if (this.brokerConfig.isFetchNameSrvAddrByDnsLookup()) { + this.brokerOuterAPI.updateNameServerAddressListByDnsLookup(this.brokerConfig.getNamesrvAddr()); + } else { + this.brokerOuterAPI.updateNameServerAddressList(this.brokerConfig.getNamesrvAddr()); + } } - public void setBrokerStats(BrokerStats brokerStats) { - this.brokerStats = brokerStats; + public boolean initializeMetadata() { + boolean result = this.topicConfigManager.load(); + result = result && this.topicQueueMappingManager.load(); + result = result && this.consumerOffsetManager.load(); + result = result && this.subscriptionGroupManager.load(); + result = result && this.consumerFilterManager.load(); + result = result && this.consumerOrderInfoManager.load(); + return result; } - public void protectBroker() { - if (this.brokerConfig.isDisableConsumeIfConsumerReadSlowly()) { - final Iterator> it = this.brokerStatsManager.getMomentStatsItemSetFallSize().getStatsItemTable().entrySet().iterator(); - while (it.hasNext()) { - final Map.Entry next = it.next(); - final long fallBehindBytes = next.getValue().getValue().get(); - if (fallBehindBytes > this.brokerConfig.getConsumerFallbehindThreshold()) { - final String[] split = next.getValue().getStatsKey().split("@"); - final String group = split[2]; - LOG_PROTECTION.info("[PROTECT_BROKER] the consumer[{}] consume slowly, {} bytes, disable it", group, fallBehindBytes); - this.subscriptionGroupManager.disableConsume(group); - } + public boolean initializeMessageStore() { + boolean result = true; + try { + DefaultMessageStore defaultMessageStore; + if (this.messageStoreConfig.isEnableRocksDBStore()) { + defaultMessageStore = new RocksDBMessageStore(this.messageStoreConfig, this.brokerStatsManager, this.messageArrivingListener, this.brokerConfig, topicConfigManager.getTopicConfigTable()); + } else { + defaultMessageStore = new DefaultMessageStore(this.messageStoreConfig, this.brokerStatsManager, this.messageArrivingListener, this.brokerConfig, topicConfigManager.getTopicConfigTable()); } + + if (messageStoreConfig.isEnableDLegerCommitLog()) { + DLedgerRoleChangeHandler roleChangeHandler = + new DLedgerRoleChangeHandler(this, defaultMessageStore); + ((DLedgerCommitLog) defaultMessageStore.getCommitLog()) + .getdLedgerServer().getDLedgerLeaderElector().addRoleChangeHandler(roleChangeHandler); + } + + this.brokerStats = new BrokerStats(defaultMessageStore); + + // Load store plugin + MessageStorePluginContext context = new MessageStorePluginContext( + messageStoreConfig, brokerStatsManager, messageArrivingListener, brokerConfig, configuration); + this.messageStore = MessageStoreFactory.build(context, defaultMessageStore); + this.messageStore.getDispatcherList().addFirst(new CommitLogDispatcherCalcBitMap(this.brokerConfig, this.consumerFilterManager)); + if (messageStoreConfig.isTimerWheelEnable()) { + this.timerCheckpoint = new TimerCheckpoint(BrokerPathConfigHelper.getTimerCheckPath(messageStoreConfig.getStorePathRootDir())); + TimerMetrics timerMetrics = new TimerMetrics(BrokerPathConfigHelper.getTimerMetricsPath(messageStoreConfig.getStorePathRootDir())); + this.timerMessageStore = new TimerMessageStore(messageStore, messageStoreConfig, timerCheckpoint, timerMetrics, brokerStatsManager); + this.timerMessageStore.registerEscapeBridgeHook(msg -> escapeBridge.putMessage(msg)); + this.messageStore.setTimerMessageStore(this.timerMessageStore); + } + } catch (IOException e) { + result = false; + LOG.error("BrokerController#initialize: unexpected error occurs", e); } + return result; } - public long headSlowTimeMills(BlockingQueue q) { - long slowTimeMills = 0; - final Runnable peek = q.peek(); - if (peek != null) { - RequestTask rt = BrokerFastFailure.castRunnable(peek); - slowTimeMills = rt == null ? 0 : this.messageStore.now() - rt.getCreateTimestamp(); - } + public boolean initialize() throws CloneNotSupportedException { - if (slowTimeMills < 0) - slowTimeMills = 0; + boolean result = this.initializeMetadata(); + if (!result) { + return false; + } - return slowTimeMills; - } + result = this.initializeMessageStore(); + if (!result) { + return false; + } - public long headSlowTimeMills4SendThreadPoolQueue() { - return this.headSlowTimeMills(this.sendThreadPoolQueue); + return this.recoverAndInitService(); } - public long headSlowTimeMills4PullThreadPoolQueue() { - return this.headSlowTimeMills(this.pullThreadPoolQueue); - } + public boolean recoverAndInitService() throws CloneNotSupportedException { - public long headSlowTimeMills4QueryThreadPoolQueue() { - return this.headSlowTimeMills(this.queryThreadPoolQueue); - } + boolean result = true; - public void printWaterMark() { - LOG_WATER_MARK.info("[WATERMARK] Send Queue Size: {} SlowTimeMills: {}", this.sendThreadPoolQueue.size(), headSlowTimeMills4SendThreadPoolQueue()); - LOG_WATER_MARK.info("[WATERMARK] Pull Queue Size: {} SlowTimeMills: {}", this.pullThreadPoolQueue.size(), headSlowTimeMills4PullThreadPoolQueue()); - LOG_WATER_MARK.info("[WATERMARK] Query Queue Size: {} SlowTimeMills: {}", this.queryThreadPoolQueue.size(), headSlowTimeMills4QueryThreadPoolQueue()); - } + if (this.brokerConfig.isEnableControllerMode()) { + this.replicasManager = new ReplicasManager(this); + this.replicasManager.setFenced(true); + } - public MessageStore getMessageStore() { - return messageStore; - } + if (messageStore != null) { + registerMessageStoreHook(); + result = this.messageStore.load(); + } - public void setMessageStore(MessageStore messageStore) { - this.messageStore = messageStore; - } + if (messageStoreConfig.isTimerWheelEnable()) { + result = result && this.timerMessageStore.load(); + } - private void printMasterAndSlaveDiff() { - long diff = this.messageStore.slaveFallBehindMuch(); + //scheduleMessageService load after messageStore load success + result = result && this.scheduleMessageService.load(); - // XXX: warn and notify me - log.info("Slave fall behind master: {} bytes", diff); + for (BrokerAttachedPlugin brokerAttachedPlugin : brokerAttachedPlugins) { + if (brokerAttachedPlugin != null) { + result = result && brokerAttachedPlugin.load(); + } + } + + this.brokerMetricsManager = new BrokerMetricsManager(this); + + if (result) { + + initializeRemotingServer(); + + initializeResources(); + + registerProcessor(); + + initializeScheduledTasks(); + + initialTransaction(); + + initialAcl(); + + initialRpcHooks(); + + if (TlsSystemConfig.tlsMode != TlsMode.DISABLED) { + // Register a listener to reload SslContext + try { + fileWatchService = new FileWatchService( + new String[] { + TlsSystemConfig.tlsServerCertPath, + TlsSystemConfig.tlsServerKeyPath, + TlsSystemConfig.tlsServerTrustCertPath + }, + new FileWatchService.Listener() { + boolean certChanged, keyChanged = false; + + @Override + public void onChanged(String path) { + if (path.equals(TlsSystemConfig.tlsServerTrustCertPath)) { + LOG.info("The trust certificate changed, reload the ssl context"); + reloadServerSslContext(); + } + if (path.equals(TlsSystemConfig.tlsServerCertPath)) { + certChanged = true; + } + if (path.equals(TlsSystemConfig.tlsServerKeyPath)) { + keyChanged = true; + } + if (certChanged && keyChanged) { + LOG.info("The certificate and private key changed, reload the ssl context"); + certChanged = keyChanged = false; + reloadServerSslContext(); + } + } + + private void reloadServerSslContext() { + ((NettyRemotingServer) remotingServer).loadSslContext(); + ((NettyRemotingServer) fastRemotingServer).loadSslContext(); + } + }); + } catch (Exception e) { + result = false; + LOG.warn("FileWatchService created error, can't load the certificate dynamically"); + } + } + } + + return result; + } + + public void registerMessageStoreHook() { + List putMessageHookList = messageStore.getPutMessageHookList(); + + putMessageHookList.add(new PutMessageHook() { + @Override + public String hookName() { + return "checkBeforePutMessage"; + } + + @Override + public PutMessageResult executeBeforePutMessage(MessageExt msg) { + return HookUtils.checkBeforePutMessage(BrokerController.this, msg); + } + }); + + putMessageHookList.add(new PutMessageHook() { + @Override + public String hookName() { + return "innerBatchChecker"; + } + + @Override + public PutMessageResult executeBeforePutMessage(MessageExt msg) { + if (msg instanceof MessageExtBrokerInner) { + return HookUtils.checkInnerBatch(BrokerController.this, msg); + } + return null; + } + }); + + putMessageHookList.add(new PutMessageHook() { + @Override + public String hookName() { + return "handleScheduleMessage"; + } + + @Override + public PutMessageResult executeBeforePutMessage(MessageExt msg) { + if (msg instanceof MessageExtBrokerInner) { + return HookUtils.handleScheduleMessage(BrokerController.this, (MessageExtBrokerInner) msg); + } + return null; + } + }); + + SendMessageBackHook sendMessageBackHook = new SendMessageBackHook() { + @Override + public boolean executeSendMessageBack(List msgList, String brokerName, String brokerAddr) { + return HookUtils.sendMessageBack(BrokerController.this, msgList, brokerName, brokerAddr); + } + }; + + if (messageStore != null) { + messageStore.setSendMessageBackHook(sendMessageBackHook); + } + } + + private void initialTransaction() { + this.transactionalMessageService = ServiceProvider.loadClass(TransactionalMessageService.class); + if (null == this.transactionalMessageService) { + this.transactionalMessageService = new TransactionalMessageServiceImpl( + new TransactionalMessageBridge(this, this.getMessageStore())); + LOG.warn("Load default transaction message hook service: {}", + TransactionalMessageServiceImpl.class.getSimpleName()); + } + this.transactionalMessageCheckListener = ServiceProvider.loadClass( + AbstractTransactionalMessageCheckListener.class); + if (null == this.transactionalMessageCheckListener) { + this.transactionalMessageCheckListener = new DefaultTransactionalMessageCheckListener(); + LOG.warn("Load default discard message hook service: {}", + DefaultTransactionalMessageCheckListener.class.getSimpleName()); + } + this.transactionalMessageCheckListener.setBrokerController(this); + this.transactionalMessageCheckService = new TransactionalMessageCheckService(this); + this.transactionMetricsFlushService = new TransactionMetricsFlushService(this); + this.transactionMetricsFlushService.start(); + + } + + private void initialAcl() { + if (!this.brokerConfig.isAclEnable()) { + LOG.info("The broker dose not enable acl"); + return; + } + + List accessValidators = ServiceProvider.load(AccessValidator.class); + if (accessValidators.isEmpty()) { + LOG.info("ServiceProvider loaded no AccessValidator, using default org.apache.rocketmq.acl.plain.PlainAccessValidator"); + accessValidators.add(new PlainAccessValidator()); + } + + for (AccessValidator accessValidator : accessValidators) { + final AccessValidator validator = accessValidator; + accessValidatorMap.put(validator.getClass(), validator); + this.registerServerRPCHook(new RPCHook() { + + @Override + public void doBeforeRequest(String remoteAddr, RemotingCommand request) { + //Do not catch the exception + validator.validate(validator.parse(request, remoteAddr)); + } + + @Override + public void doAfterResponse(String remoteAddr, RemotingCommand request, RemotingCommand response) { + } + + }); + } + } + + private void initialRpcHooks() { + + List rpcHooks = ServiceProvider.load(RPCHook.class); + if (rpcHooks == null || rpcHooks.isEmpty()) { + return; + } + for (RPCHook rpcHook : rpcHooks) { + this.registerServerRPCHook(rpcHook); + } + } + + public void registerProcessor() { + /* + * SendMessageProcessor + */ + sendMessageProcessor.registerSendMessageHook(sendMessageHookList); + sendMessageProcessor.registerConsumeMessageHook(consumeMessageHookList); + + this.remotingServer.registerProcessor(RequestCode.SEND_MESSAGE, sendMessageProcessor, this.sendMessageExecutor); + this.remotingServer.registerProcessor(RequestCode.SEND_MESSAGE_V2, sendMessageProcessor, this.sendMessageExecutor); + this.remotingServer.registerProcessor(RequestCode.SEND_BATCH_MESSAGE, sendMessageProcessor, this.sendMessageExecutor); + this.remotingServer.registerProcessor(RequestCode.CONSUMER_SEND_MSG_BACK, sendMessageProcessor, this.sendMessageExecutor); + this.fastRemotingServer.registerProcessor(RequestCode.SEND_MESSAGE, sendMessageProcessor, this.sendMessageExecutor); + this.fastRemotingServer.registerProcessor(RequestCode.SEND_MESSAGE_V2, sendMessageProcessor, this.sendMessageExecutor); + this.fastRemotingServer.registerProcessor(RequestCode.SEND_BATCH_MESSAGE, sendMessageProcessor, this.sendMessageExecutor); + this.fastRemotingServer.registerProcessor(RequestCode.CONSUMER_SEND_MSG_BACK, sendMessageProcessor, this.sendMessageExecutor); + /** + * PullMessageProcessor + */ + this.remotingServer.registerProcessor(RequestCode.PULL_MESSAGE, this.pullMessageProcessor, this.pullMessageExecutor); + this.remotingServer.registerProcessor(RequestCode.LITE_PULL_MESSAGE, this.pullMessageProcessor, this.litePullMessageExecutor); + this.pullMessageProcessor.registerConsumeMessageHook(consumeMessageHookList); + /** + * PeekMessageProcessor + */ + this.remotingServer.registerProcessor(RequestCode.PEEK_MESSAGE, this.peekMessageProcessor, this.pullMessageExecutor); + /** + * PopMessageProcessor + */ + this.remotingServer.registerProcessor(RequestCode.POP_MESSAGE, this.popMessageProcessor, this.pullMessageExecutor); + + /** + * AckMessageProcessor + */ + this.remotingServer.registerProcessor(RequestCode.ACK_MESSAGE, this.ackMessageProcessor, this.ackMessageExecutor); + this.fastRemotingServer.registerProcessor(RequestCode.ACK_MESSAGE, this.ackMessageProcessor, this.ackMessageExecutor); + + this.remotingServer.registerProcessor(RequestCode.BATCH_ACK_MESSAGE, this.ackMessageProcessor, this.ackMessageExecutor); + this.fastRemotingServer.registerProcessor(RequestCode.BATCH_ACK_MESSAGE, this.ackMessageProcessor, this.ackMessageExecutor); + /** + * ChangeInvisibleTimeProcessor + */ + this.remotingServer.registerProcessor(RequestCode.CHANGE_MESSAGE_INVISIBLETIME, this.changeInvisibleTimeProcessor, this.ackMessageExecutor); + this.fastRemotingServer.registerProcessor(RequestCode.CHANGE_MESSAGE_INVISIBLETIME, this.changeInvisibleTimeProcessor, this.ackMessageExecutor); + /** + * notificationProcessor + */ + this.remotingServer.registerProcessor(RequestCode.NOTIFICATION, this.notificationProcessor, this.pullMessageExecutor); + + /** + * pollingInfoProcessor + */ + this.remotingServer.registerProcessor(RequestCode.POLLING_INFO, this.pollingInfoProcessor, this.pullMessageExecutor); + + /** + * ReplyMessageProcessor + */ + + replyMessageProcessor.registerSendMessageHook(sendMessageHookList); + + this.remotingServer.registerProcessor(RequestCode.SEND_REPLY_MESSAGE, replyMessageProcessor, replyMessageExecutor); + this.remotingServer.registerProcessor(RequestCode.SEND_REPLY_MESSAGE_V2, replyMessageProcessor, replyMessageExecutor); + this.fastRemotingServer.registerProcessor(RequestCode.SEND_REPLY_MESSAGE, replyMessageProcessor, replyMessageExecutor); + this.fastRemotingServer.registerProcessor(RequestCode.SEND_REPLY_MESSAGE_V2, replyMessageProcessor, replyMessageExecutor); + + /** + * QueryMessageProcessor + */ + NettyRequestProcessor queryProcessor = new QueryMessageProcessor(this); + this.remotingServer.registerProcessor(RequestCode.QUERY_MESSAGE, queryProcessor, this.queryMessageExecutor); + this.remotingServer.registerProcessor(RequestCode.VIEW_MESSAGE_BY_ID, queryProcessor, this.queryMessageExecutor); + + this.fastRemotingServer.registerProcessor(RequestCode.QUERY_MESSAGE, queryProcessor, this.queryMessageExecutor); + this.fastRemotingServer.registerProcessor(RequestCode.VIEW_MESSAGE_BY_ID, queryProcessor, this.queryMessageExecutor); + + /** + * ClientManageProcessor + */ + this.remotingServer.registerProcessor(RequestCode.HEART_BEAT, clientManageProcessor, this.heartbeatExecutor); + this.remotingServer.registerProcessor(RequestCode.UNREGISTER_CLIENT, clientManageProcessor, this.clientManageExecutor); + this.remotingServer.registerProcessor(RequestCode.CHECK_CLIENT_CONFIG, clientManageProcessor, this.clientManageExecutor); + + this.fastRemotingServer.registerProcessor(RequestCode.HEART_BEAT, clientManageProcessor, this.heartbeatExecutor); + this.fastRemotingServer.registerProcessor(RequestCode.UNREGISTER_CLIENT, clientManageProcessor, this.clientManageExecutor); + this.fastRemotingServer.registerProcessor(RequestCode.CHECK_CLIENT_CONFIG, clientManageProcessor, this.clientManageExecutor); + + /** + * ConsumerManageProcessor + */ + ConsumerManageProcessor consumerManageProcessor = new ConsumerManageProcessor(this); + this.remotingServer.registerProcessor(RequestCode.GET_CONSUMER_LIST_BY_GROUP, consumerManageProcessor, this.consumerManageExecutor); + this.remotingServer.registerProcessor(RequestCode.UPDATE_CONSUMER_OFFSET, consumerManageProcessor, this.consumerManageExecutor); + this.remotingServer.registerProcessor(RequestCode.QUERY_CONSUMER_OFFSET, consumerManageProcessor, this.consumerManageExecutor); + + this.fastRemotingServer.registerProcessor(RequestCode.GET_CONSUMER_LIST_BY_GROUP, consumerManageProcessor, this.consumerManageExecutor); + this.fastRemotingServer.registerProcessor(RequestCode.UPDATE_CONSUMER_OFFSET, consumerManageProcessor, this.consumerManageExecutor); + this.fastRemotingServer.registerProcessor(RequestCode.QUERY_CONSUMER_OFFSET, consumerManageProcessor, this.consumerManageExecutor); + + /** + * QueryAssignmentProcessor + */ + this.remotingServer.registerProcessor(RequestCode.QUERY_ASSIGNMENT, queryAssignmentProcessor, loadBalanceExecutor); + this.fastRemotingServer.registerProcessor(RequestCode.QUERY_ASSIGNMENT, queryAssignmentProcessor, loadBalanceExecutor); + this.remotingServer.registerProcessor(RequestCode.SET_MESSAGE_REQUEST_MODE, queryAssignmentProcessor, loadBalanceExecutor); + this.fastRemotingServer.registerProcessor(RequestCode.SET_MESSAGE_REQUEST_MODE, queryAssignmentProcessor, loadBalanceExecutor); + + /** + * EndTransactionProcessor + */ + this.remotingServer.registerProcessor(RequestCode.END_TRANSACTION, endTransactionProcessor, this.endTransactionExecutor); + this.fastRemotingServer.registerProcessor(RequestCode.END_TRANSACTION, endTransactionProcessor, this.endTransactionExecutor); + + /* + * Default + */ + AdminBrokerProcessor adminProcessor = new AdminBrokerProcessor(this); + this.remotingServer.registerDefaultProcessor(adminProcessor, this.adminBrokerExecutor); + this.fastRemotingServer.registerDefaultProcessor(adminProcessor, this.adminBrokerExecutor); + } + + public BrokerStats getBrokerStats() { + return brokerStats; + } + + public void setBrokerStats(BrokerStats brokerStats) { + this.brokerStats = brokerStats; + } + + public void protectBroker() { + if (this.brokerConfig.isDisableConsumeIfConsumerReadSlowly()) { + for (Map.Entry next : this.brokerStatsManager.getMomentStatsItemSetFallSize().getStatsItemTable().entrySet()) { + final long fallBehindBytes = next.getValue().getValue().get(); + if (fallBehindBytes > this.brokerConfig.getConsumerFallbehindThreshold()) { + final String[] split = next.getValue().getStatsKey().split("@"); + final String group = split[2]; + LOG_PROTECTION.info("[PROTECT_BROKER] the consumer[{}] consume slowly, {} bytes, disable it", group, fallBehindBytes); + this.subscriptionGroupManager.disableConsume(group); + } + } + } + } + + public long headSlowTimeMills(BlockingQueue q) { + long slowTimeMills = 0; + final Runnable peek = q.peek(); + if (peek != null) { + RequestTask rt = BrokerFastFailure.castRunnable(peek); + slowTimeMills = rt == null ? 0 : this.messageStore.now() - rt.getCreateTimestamp(); + } + + if (slowTimeMills < 0) { + slowTimeMills = 0; + } + + return slowTimeMills; + } + + public long headSlowTimeMills4SendThreadPoolQueue() { + return this.headSlowTimeMills(this.sendThreadPoolQueue); + } + + public long headSlowTimeMills4PullThreadPoolQueue() { + return this.headSlowTimeMills(this.pullThreadPoolQueue); + } + + public long headSlowTimeMills4LitePullThreadPoolQueue() { + return this.headSlowTimeMills(this.litePullThreadPoolQueue); + } + + public long headSlowTimeMills4QueryThreadPoolQueue() { + return this.headSlowTimeMills(this.queryThreadPoolQueue); + } + + public void printWaterMark() { + LOG_WATER_MARK.info("[WATERMARK] Send Queue Size: {} SlowTimeMills: {}", this.sendThreadPoolQueue.size(), headSlowTimeMills4SendThreadPoolQueue()); + LOG_WATER_MARK.info("[WATERMARK] Pull Queue Size: {} SlowTimeMills: {}", this.pullThreadPoolQueue.size(), headSlowTimeMills4PullThreadPoolQueue()); + LOG_WATER_MARK.info("[WATERMARK] Query Queue Size: {} SlowTimeMills: {}", this.queryThreadPoolQueue.size(), headSlowTimeMills4QueryThreadPoolQueue()); + LOG_WATER_MARK.info("[WATERMARK] Lite Pull Queue Size: {} SlowTimeMills: {}", this.litePullThreadPoolQueue.size(), headSlowTimeMills4LitePullThreadPoolQueue()); + LOG_WATER_MARK.info("[WATERMARK] Transaction Queue Size: {} SlowTimeMills: {}", this.endTransactionThreadPoolQueue.size(), headSlowTimeMills(this.endTransactionThreadPoolQueue)); + LOG_WATER_MARK.info("[WATERMARK] ClientManager Queue Size: {} SlowTimeMills: {}", this.clientManagerThreadPoolQueue.size(), this.headSlowTimeMills(this.clientManagerThreadPoolQueue)); + LOG_WATER_MARK.info("[WATERMARK] Heartbeat Queue Size: {} SlowTimeMills: {}", this.heartbeatThreadPoolQueue.size(), this.headSlowTimeMills(this.heartbeatThreadPoolQueue)); + LOG_WATER_MARK.info("[WATERMARK] Ack Queue Size: {} SlowTimeMills: {}", this.ackThreadPoolQueue.size(), headSlowTimeMills(this.ackThreadPoolQueue)); + LOG_WATER_MARK.info("[WATERMARK] Admin Queue Size: {} SlowTimeMills: {}", this.adminBrokerThreadPoolQueue.size(), headSlowTimeMills(this.adminBrokerThreadPoolQueue)); + } + + public MessageStore getMessageStore() { + return messageStore; + } + + public void setMessageStore(MessageStore messageStore) { + this.messageStore = messageStore; + } + + protected void printMasterAndSlaveDiff() { + if (messageStore.getHaService() != null && messageStore.getHaService().getConnectionCount().get() > 0) { + long diff = this.messageStore.slaveFallBehindMuch(); + LOG.info("CommitLog: slave fall behind master {}bytes", diff); + } } public Broker2Client getBroker2Client() { @@ -545,10 +1223,22 @@ public ConsumerFilterManager getConsumerFilterManager() { return consumerFilterManager; } + public ConsumerOrderInfoManager getConsumerOrderInfoManager() { + return consumerOrderInfoManager; + } + + public PopInflightMessageCounter getPopInflightMessageCounter() { + return popInflightMessageCounter; + } + public ConsumerOffsetManager getConsumerOffsetManager() { return consumerOffsetManager; } + public BroadcastOffsetManager getBroadcastOffsetManager() { + return broadcastOffsetManager; + } + public MessageStoreConfig getMessageStoreConfig() { return messageStoreConfig; } @@ -561,185 +1251,897 @@ public void setFastRemotingServer(RemotingServer fastRemotingServer) { this.fastRemotingServer = fastRemotingServer; } + public RemotingServer getFastRemotingServer() { + return fastRemotingServer; + } + public PullMessageProcessor getPullMessageProcessor() { return pullMessageProcessor; } - public PullRequestHoldService getPullRequestHoldService() { - return pullRequestHoldService; + public PullRequestHoldService getPullRequestHoldService() { + return pullRequestHoldService; + } + + public void setSubscriptionGroupManager(SubscriptionGroupManager subscriptionGroupManager) { + this.subscriptionGroupManager = subscriptionGroupManager; + } + + public SubscriptionGroupManager getSubscriptionGroupManager() { + return subscriptionGroupManager; + } + + public PopMessageProcessor getPopMessageProcessor() { + return popMessageProcessor; + } + + public NotificationProcessor getNotificationProcessor() { + return notificationProcessor; + } + + public TimerMessageStore getTimerMessageStore() { + return timerMessageStore; + } + + public void setTimerMessageStore(TimerMessageStore timerMessageStore) { + this.timerMessageStore = timerMessageStore; + } + + public AckMessageProcessor getAckMessageProcessor() { + return ackMessageProcessor; + } + + public ChangeInvisibleTimeProcessor getChangeInvisibleTimeProcessor() { + return changeInvisibleTimeProcessor; + } + + protected void shutdownBasicService() { + + shutdown = true; + + this.unregisterBrokerAll(); + + if (this.shutdownHook != null) { + this.shutdownHook.beforeShutdown(this); + } + + if (this.remotingServer != null) { + this.remotingServer.shutdown(); + } + + if (this.fastRemotingServer != null) { + this.fastRemotingServer.shutdown(); + } + + if (this.brokerMetricsManager != null) { + this.brokerMetricsManager.shutdown(); + } + + if (this.brokerStatsManager != null) { + this.brokerStatsManager.shutdown(); + } + + if (this.clientHousekeepingService != null) { + this.clientHousekeepingService.shutdown(); + } + + if (this.pullRequestHoldService != null) { + this.pullRequestHoldService.shutdown(); + } + + { + this.popMessageProcessor.getPopLongPollingService().shutdown(); + this.popMessageProcessor.getQueueLockManager().shutdown(); + } + + { + this.popMessageProcessor.getPopBufferMergeService().shutdown(); + this.ackMessageProcessor.shutdownPopReviveService(); + } + + if (this.transactionalMessageService != null) { + this.transactionalMessageService.close(); + } + + if (this.notificationProcessor != null) { + this.notificationProcessor.getPopLongPollingService().shutdown(); + } + + if (this.consumerIdsChangeListener != null) { + this.consumerIdsChangeListener.shutdown(); + } + + if (this.topicQueueMappingCleanService != null) { + this.topicQueueMappingCleanService.shutdown(); + } + //it is better to make sure the timerMessageStore shutdown firstly + if (this.timerMessageStore != null) { + this.timerMessageStore.shutdown(); + } + if (this.fileWatchService != null) { + this.fileWatchService.shutdown(); + } + + if (this.broadcastOffsetManager != null) { + this.broadcastOffsetManager.shutdown(); + } + + if (this.messageStore != null) { + this.messageStore.shutdown(); + } + + if (this.replicasManager != null) { + this.replicasManager.shutdown(); + } + + shutdownScheduledExecutorService(this.scheduledExecutorService); + + if (this.sendMessageExecutor != null) { + this.sendMessageExecutor.shutdown(); + } + + if (this.litePullMessageExecutor != null) { + this.litePullMessageExecutor.shutdown(); + } + + if (this.pullMessageExecutor != null) { + this.pullMessageExecutor.shutdown(); + } + + if (this.replyMessageExecutor != null) { + this.replyMessageExecutor.shutdown(); + } + + if (this.putMessageFutureExecutor != null) { + this.putMessageFutureExecutor.shutdown(); + } + + if (this.ackMessageExecutor != null) { + this.ackMessageExecutor.shutdown(); + } + + if (this.adminBrokerExecutor != null) { + this.adminBrokerExecutor.shutdown(); + } + + if (this.brokerFastFailure != null) { + this.brokerFastFailure.shutdown(); + } + + if (this.consumerFilterManager != null) { + this.consumerFilterManager.persist(); + } + + if (this.consumerOrderInfoManager != null) { + this.consumerOrderInfoManager.persist(); + } + + if (this.scheduleMessageService != null) { + this.scheduleMessageService.persist(); + this.scheduleMessageService.shutdown(); + } + + if (this.clientManageExecutor != null) { + this.clientManageExecutor.shutdown(); + } + + if (this.queryMessageExecutor != null) { + this.queryMessageExecutor.shutdown(); + } + + if (this.heartbeatExecutor != null) { + this.heartbeatExecutor.shutdown(); + } + + if (this.consumerManageExecutor != null) { + this.consumerManageExecutor.shutdown(); + } + + if (this.transactionalMessageCheckService != null) { + this.transactionalMessageCheckService.shutdown(false); + } + + if (this.endTransactionExecutor != null) { + this.endTransactionExecutor.shutdown(); + } + + if (this.transactionMetricsFlushService != null) { + this.transactionMetricsFlushService.shutdown(); + } + + if (this.escapeBridge != null) { + escapeBridge.shutdown(); + } + + if (this.topicRouteInfoManager != null) { + this.topicRouteInfoManager.shutdown(); + } + + if (this.brokerPreOnlineService != null && !this.brokerPreOnlineService.isStopped()) { + this.brokerPreOnlineService.shutdown(); + } + + if (this.coldDataPullRequestHoldService != null) { + this.coldDataPullRequestHoldService.shutdown(); + } + + if (this.coldDataCgCtrService != null) { + this.coldDataCgCtrService.shutdown(); + } + + shutdownScheduledExecutorService(this.syncBrokerMemberGroupExecutorService); + shutdownScheduledExecutorService(this.brokerHeartbeatExecutorService); + + if (this.topicConfigManager != null) { + this.topicConfigManager.persist(); + this.topicConfigManager.stop(); + } + + if (this.subscriptionGroupManager != null) { + this.subscriptionGroupManager.persist(); + this.subscriptionGroupManager.stop(); + } + + if (this.consumerOffsetManager != null) { + this.consumerOffsetManager.persist(); + this.consumerOffsetManager.stop(); + } + + for (BrokerAttachedPlugin brokerAttachedPlugin : brokerAttachedPlugins) { + if (brokerAttachedPlugin != null) { + brokerAttachedPlugin.shutdown(); + } + } + } + + public void shutdown() { + + shutdownBasicService(); + + for (ScheduledFuture scheduledFuture : scheduledFutures) { + scheduledFuture.cancel(true); + } + + if (this.brokerOuterAPI != null) { + this.brokerOuterAPI.shutdown(); + } + } + + protected void shutdownScheduledExecutorService(ScheduledExecutorService scheduledExecutorService) { + if (scheduledExecutorService == null) { + return; + } + scheduledExecutorService.shutdown(); + try { + scheduledExecutorService.awaitTermination(5000, TimeUnit.MILLISECONDS); + } catch (InterruptedException ignore) { + BrokerController.LOG.warn("shutdown ScheduledExecutorService was Interrupted! ", ignore); + Thread.currentThread().interrupt(); + } + } + + protected void unregisterBrokerAll() { + this.brokerOuterAPI.unregisterBrokerAll( + this.brokerConfig.getBrokerClusterName(), + this.getBrokerAddr(), + this.brokerConfig.getBrokerName(), + this.brokerConfig.getBrokerId()); + } + + public String getBrokerAddr() { + return this.brokerConfig.getBrokerIP1() + ":" + this.nettyServerConfig.getListenPort(); + } + + protected void startBasicService() throws Exception { + + if (this.messageStore != null) { + this.messageStore.start(); + } + + if (this.timerMessageStore != null) { + this.timerMessageStore.start(); + } + + if (this.replicasManager != null) { + this.replicasManager.start(); + } + + if (remotingServerStartLatch != null) { + remotingServerStartLatch.await(); + } + + if (this.remotingServer != null) { + this.remotingServer.start(); + + // In test scenarios where it is up to OS to pick up an available port, set the listening port back to config + if (null != nettyServerConfig && 0 == nettyServerConfig.getListenPort()) { + nettyServerConfig.setListenPort(remotingServer.localListenPort()); + } + } + + if (this.fastRemotingServer != null) { + this.fastRemotingServer.start(); + } + + this.storeHost = new InetSocketAddress(this.getBrokerConfig().getBrokerIP1(), this.getNettyServerConfig().getListenPort()); + + for (BrokerAttachedPlugin brokerAttachedPlugin : brokerAttachedPlugins) { + if (brokerAttachedPlugin != null) { + brokerAttachedPlugin.start(); + } + } + + if (this.popMessageProcessor != null) { + this.popMessageProcessor.getPopLongPollingService().start(); + this.popMessageProcessor.getPopBufferMergeService().start(); + this.popMessageProcessor.getQueueLockManager().start(); + } + + if (this.ackMessageProcessor != null) { + this.ackMessageProcessor.startPopReviveService(); + } + + if (this.notificationProcessor != null) { + this.notificationProcessor.getPopLongPollingService().start(); + } + + if (this.topicQueueMappingCleanService != null) { + this.topicQueueMappingCleanService.start(); + } + + if (this.fileWatchService != null) { + this.fileWatchService.start(); + } + + if (this.pullRequestHoldService != null) { + this.pullRequestHoldService.start(); + } + + if (this.clientHousekeepingService != null) { + this.clientHousekeepingService.start(); + } + + if (this.brokerStatsManager != null) { + this.brokerStatsManager.start(); + } + + if (this.brokerFastFailure != null) { + this.brokerFastFailure.start(); + } + + if (this.broadcastOffsetManager != null) { + this.broadcastOffsetManager.start(); + } + + if (this.escapeBridge != null) { + this.escapeBridge.start(); + } + + if (this.topicRouteInfoManager != null) { + this.topicRouteInfoManager.start(); + } + + if (this.brokerPreOnlineService != null) { + this.brokerPreOnlineService.start(); + } + + if (this.coldDataPullRequestHoldService != null) { + this.coldDataPullRequestHoldService.start(); + } + + if (this.coldDataCgCtrService != null) { + this.coldDataCgCtrService.start(); + } + } + + public void start() throws Exception { + + this.shouldStartTime = System.currentTimeMillis() + messageStoreConfig.getDisappearTimeAfterStart(); + + if (messageStoreConfig.getTotalReplicas() > 1 && this.brokerConfig.isEnableSlaveActingMaster()) { + isIsolated = true; + } + + if (this.brokerOuterAPI != null) { + this.brokerOuterAPI.start(); + } + + startBasicService(); + + if (!isIsolated && !this.messageStoreConfig.isEnableDLegerCommitLog() && !this.messageStoreConfig.isDuplicationEnable()) { + changeSpecialServiceStatus(this.brokerConfig.getBrokerId() == MixAll.MASTER_ID); + this.registerBrokerAll(true, false, true); + } + + scheduledFutures.add(this.scheduledExecutorService.scheduleAtFixedRate(new AbstractBrokerRunnable(this.getBrokerIdentity()) { + @Override + public void run0() { + try { + if (System.currentTimeMillis() < shouldStartTime) { + BrokerController.LOG.info("Register to namesrv after {}", shouldStartTime); + return; + } + if (isIsolated) { + BrokerController.LOG.info("Skip register for broker is isolated"); + return; + } + BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister()); + } catch (Throwable e) { + BrokerController.LOG.error("registerBrokerAll Exception", e); + } + } + }, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)), TimeUnit.MILLISECONDS)); + + if (this.brokerConfig.isEnableSlaveActingMaster()) { + scheduleSendHeartbeat(); + + scheduledFutures.add(this.syncBrokerMemberGroupExecutorService.scheduleAtFixedRate(new AbstractBrokerRunnable(this.getBrokerIdentity()) { + @Override + public void run0() { + try { + BrokerController.this.syncBrokerMemberGroup(); + } catch (Throwable e) { + BrokerController.LOG.error("sync BrokerMemberGroup error. ", e); + } + } + }, 1000, this.brokerConfig.getSyncBrokerMemberGroupPeriod(), TimeUnit.MILLISECONDS)); + } + + if (this.brokerConfig.isEnableControllerMode()) { + scheduleSendHeartbeat(); + } + + if (brokerConfig.isSkipPreOnline()) { + startServiceWithoutCondition(); + } + + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + BrokerController.this.brokerOuterAPI.refreshMetadata(); + } catch (Exception e) { + LOG.error("ScheduledTask refresh metadata exception", e); + } + } + }, 10, 5, TimeUnit.SECONDS); + } + + protected void scheduleSendHeartbeat() { + scheduledFutures.add(this.brokerHeartbeatExecutorService.scheduleAtFixedRate(new AbstractBrokerRunnable(this.getBrokerIdentity()) { + @Override + public void run0() { + if (isIsolated) { + return; + } + try { + BrokerController.this.sendHeartbeat(); + } catch (Exception e) { + BrokerController.LOG.error("sendHeartbeat Exception", e); + } + + } + }, 1000, brokerConfig.getBrokerHeartbeatInterval(), TimeUnit.MILLISECONDS)); + } + + public synchronized void registerSingleTopicAll(final TopicConfig topicConfig) { + TopicConfig tmpTopic = topicConfig; + if (!PermName.isWriteable(this.getBrokerConfig().getBrokerPermission()) + || !PermName.isReadable(this.getBrokerConfig().getBrokerPermission())) { + // Copy the topic config and modify the perm + tmpTopic = new TopicConfig(topicConfig); + tmpTopic.setPerm(topicConfig.getPerm() & this.brokerConfig.getBrokerPermission()); + } + this.brokerOuterAPI.registerSingleTopicAll(this.brokerConfig.getBrokerName(), tmpTopic, 3000); + } + + public synchronized void registerIncrementBrokerData(TopicConfig topicConfig, DataVersion dataVersion) { + this.registerIncrementBrokerData(Collections.singletonList(topicConfig), dataVersion); } - public SubscriptionGroupManager getSubscriptionGroupManager() { - return subscriptionGroupManager; - } + public synchronized void registerIncrementBrokerData(List topicConfigList, DataVersion dataVersion) { + if (topicConfigList == null || topicConfigList.isEmpty()) { + return; + } - public void shutdown() { - if (this.brokerStatsManager != null) { - this.brokerStatsManager.shutdown(); + TopicConfigAndMappingSerializeWrapper topicConfigSerializeWrapper = new TopicConfigAndMappingSerializeWrapper(); + topicConfigSerializeWrapper.setDataVersion(dataVersion); + + ConcurrentMap topicConfigTable = topicConfigList.stream() + .map(topicConfig -> { + TopicConfig registerTopicConfig; + if (!PermName.isWriteable(this.getBrokerConfig().getBrokerPermission()) + || !PermName.isReadable(this.getBrokerConfig().getBrokerPermission())) { + registerTopicConfig = + new TopicConfig(topicConfig.getTopicName(), + topicConfig.getReadQueueNums(), + topicConfig.getWriteQueueNums(), + topicConfig.getPerm() + & this.brokerConfig.getBrokerPermission(), topicConfig.getTopicSysFlag()); + } else { + registerTopicConfig = new TopicConfig(topicConfig); + } + return registerTopicConfig; + }) + .collect(Collectors.toConcurrentMap(TopicConfig::getTopicName, Function.identity())); + topicConfigSerializeWrapper.setTopicConfigTable(topicConfigTable); + + Map topicQueueMappingInfoMap = topicConfigList.stream() + .map(TopicConfig::getTopicName) + .map(topicName -> Optional.ofNullable(this.topicQueueMappingManager.getTopicQueueMapping(topicName)) + .map(info -> new AbstractMap.SimpleImmutableEntry<>(topicName, TopicQueueMappingDetail.cloneAsMappingInfo(info))) + .orElse(null)) + .filter(Objects::nonNull) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + if (!topicQueueMappingInfoMap.isEmpty()) { + topicConfigSerializeWrapper.setTopicQueueMappingInfoMap(topicQueueMappingInfoMap); } - if (this.clientHousekeepingService != null) { - this.clientHousekeepingService.shutdown(); + doRegisterBrokerAll(true, false, topicConfigSerializeWrapper); + } + + public synchronized void registerBrokerAll(final boolean checkOrderConfig, boolean oneway, boolean forceRegister) { + ConcurrentMap topicConfigMap = this.getTopicConfigManager().getTopicConfigTable(); + ConcurrentHashMap topicConfigTable = new ConcurrentHashMap<>(); + + for (TopicConfig topicConfig : topicConfigMap.values()) { + if (!PermName.isWriteable(this.getBrokerConfig().getBrokerPermission()) + || !PermName.isReadable(this.getBrokerConfig().getBrokerPermission())) { + topicConfigTable.put(topicConfig.getTopicName(), + new TopicConfig(topicConfig.getTopicName(), topicConfig.getReadQueueNums(), topicConfig.getWriteQueueNums(), + topicConfig.getPerm() & getBrokerConfig().getBrokerPermission())); + } else { + topicConfigTable.put(topicConfig.getTopicName(), topicConfig); + } + + if (this.brokerConfig.isEnableSplitRegistration() + && topicConfigTable.size() >= this.brokerConfig.getSplitRegistrationSize()) { + TopicConfigAndMappingSerializeWrapper topicConfigWrapper = this.getTopicConfigManager().buildSerializeWrapper(topicConfigTable); + doRegisterBrokerAll(checkOrderConfig, oneway, topicConfigWrapper); + topicConfigTable.clear(); + } } - if (this.pullRequestHoldService != null) { - this.pullRequestHoldService.shutdown(); + Map topicQueueMappingInfoMap = this.getTopicQueueMappingManager().getTopicQueueMappingTable().entrySet().stream() + .map(entry -> new AbstractMap.SimpleImmutableEntry<>(entry.getKey(), TopicQueueMappingDetail.cloneAsMappingInfo(entry.getValue()))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + TopicConfigAndMappingSerializeWrapper topicConfigWrapper = this.getTopicConfigManager(). + buildSerializeWrapper(topicConfigTable, topicQueueMappingInfoMap); + if (this.brokerConfig.isEnableSplitRegistration() || forceRegister || needRegister(this.brokerConfig.getBrokerClusterName(), + this.getBrokerAddr(), + this.brokerConfig.getBrokerName(), + this.brokerConfig.getBrokerId(), + this.brokerConfig.getRegisterBrokerTimeoutMills(), + this.brokerConfig.isInBrokerContainer())) { + doRegisterBrokerAll(checkOrderConfig, oneway, topicConfigWrapper); } + } - if (this.remotingServer != null) { - this.remotingServer.shutdown(); + protected void doRegisterBrokerAll(boolean checkOrderConfig, boolean oneway, + TopicConfigSerializeWrapper topicConfigWrapper) { + + if (shutdown) { + BrokerController.LOG.info("BrokerController#doRegisterBrokerAll: broker has shutdown, no need to register any more."); + return; } + List registerBrokerResultList = this.brokerOuterAPI.registerBrokerAll( + this.brokerConfig.getBrokerClusterName(), + this.getBrokerAddr(), + this.brokerConfig.getBrokerName(), + this.brokerConfig.getBrokerId(), + this.getHAServerAddr(), + topicConfigWrapper, + Lists.newArrayList(), + oneway, + this.brokerConfig.getRegisterBrokerTimeoutMills(), + this.brokerConfig.isEnableSlaveActingMaster(), + this.brokerConfig.isCompressedRegister(), + this.brokerConfig.isEnableSlaveActingMaster() ? this.brokerConfig.getBrokerNotActiveTimeoutMillis() : null, + this.getBrokerIdentity()); - if (this.fastRemotingServer != null) { - this.fastRemotingServer.shutdown(); + handleRegisterBrokerResult(registerBrokerResultList, checkOrderConfig); + } + + protected void sendHeartbeat() { + if (this.brokerConfig.isEnableControllerMode()) { + this.replicasManager.sendHeartbeatToController(); } - if (this.messageStore != null) { - this.messageStore.shutdown(); + if (this.brokerConfig.isEnableSlaveActingMaster()) { + if (this.brokerConfig.isCompatibleWithOldNameSrv()) { + this.brokerOuterAPI.sendHeartbeatViaDataVersion( + this.brokerConfig.getBrokerClusterName(), + this.getBrokerAddr(), + this.brokerConfig.getBrokerName(), + this.brokerConfig.getBrokerId(), + this.brokerConfig.getSendHeartbeatTimeoutMillis(), + this.getTopicConfigManager().getDataVersion(), + this.brokerConfig.isInBrokerContainer()); + } else { + this.brokerOuterAPI.sendHeartbeat( + this.brokerConfig.getBrokerClusterName(), + this.getBrokerAddr(), + this.brokerConfig.getBrokerName(), + this.brokerConfig.getBrokerId(), + this.brokerConfig.getSendHeartbeatTimeoutMillis(), + this.brokerConfig.isInBrokerContainer()); + } } + } - this.scheduledExecutorService.shutdown(); + protected void syncBrokerMemberGroup() { try { - this.scheduledExecutorService.awaitTermination(5000, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { + brokerMemberGroup = this.getBrokerOuterAPI() + .syncBrokerMemberGroup(this.brokerConfig.getBrokerClusterName(), this.brokerConfig.getBrokerName(), this.brokerConfig.isCompatibleWithOldNameSrv()); + } catch (Exception e) { + BrokerController.LOG.error("syncBrokerMemberGroup from namesrv failed, ", e); + return; } - - this.unregisterBrokerAll(); - - if (this.sendMessageExecutor != null) { - this.sendMessageExecutor.shutdown(); + if (brokerMemberGroup == null || brokerMemberGroup.getBrokerAddrs().size() == 0) { + BrokerController.LOG.warn("Couldn't find any broker member from namesrv in {}/{}", this.brokerConfig.getBrokerClusterName(), this.brokerConfig.getBrokerName()); + return; } + this.messageStore.setAliveReplicaNumInGroup(calcAliveBrokerNumInGroup(brokerMemberGroup.getBrokerAddrs())); - if (this.pullMessageExecutor != null) { - this.pullMessageExecutor.shutdown(); + if (!this.isIsolated) { + long minBrokerId = brokerMemberGroup.minimumBrokerId(); + this.updateMinBroker(minBrokerId, brokerMemberGroup.getBrokerAddrs().get(minBrokerId)); } + } - if (this.adminBrokerExecutor != null) { - this.adminBrokerExecutor.shutdown(); + private int calcAliveBrokerNumInGroup(Map brokerAddrTable) { + if (brokerAddrTable.containsKey(this.brokerConfig.getBrokerId())) { + return brokerAddrTable.size(); + } else { + return brokerAddrTable.size() + 1; } + } - if (this.brokerOuterAPI != null) { - this.brokerOuterAPI.shutdown(); + protected void handleRegisterBrokerResult(List registerBrokerResultList, + boolean checkOrderConfig) { + for (RegisterBrokerResult registerBrokerResult : registerBrokerResultList) { + if (registerBrokerResult != null) { + if (this.updateMasterHAServerAddrPeriodically && registerBrokerResult.getHaServerAddr() != null) { + this.messageStore.updateHaMasterAddress(registerBrokerResult.getHaServerAddr()); + this.messageStore.updateMasterAddress(registerBrokerResult.getMasterAddr()); + } + + this.slaveSynchronize.setMasterAddr(registerBrokerResult.getMasterAddr()); + if (checkOrderConfig) { + this.getTopicConfigManager().updateOrderTopicConfig(registerBrokerResult.getKvTable()); + } + break; + } } + } - this.consumerOffsetManager.persist(); + private boolean needRegister(final String clusterName, + final String brokerAddr, + final String brokerName, + final long brokerId, + final int timeoutMills, + final boolean isInBrokerContainer) { - if (this.filterServerManager != null) { - this.filterServerManager.shutdown(); + TopicConfigSerializeWrapper topicConfigWrapper = this.getTopicConfigManager().buildTopicConfigSerializeWrapper(); + List changeList = brokerOuterAPI.needRegister(clusterName, brokerAddr, brokerName, brokerId, topicConfigWrapper, timeoutMills, isInBrokerContainer); + boolean needRegister = false; + for (Boolean changed : changeList) { + if (changed) { + needRegister = true; + break; + } } + return needRegister; + } - if (this.brokerFastFailure != null) { - this.brokerFastFailure.shutdown(); - } + public void startService(long minBrokerId, String minBrokerAddr) { + BrokerController.LOG.info("{} start service, min broker id is {}, min broker addr: {}", + this.brokerConfig.getCanonicalName(), minBrokerId, minBrokerAddr); + this.minBrokerIdInGroup = minBrokerId; + this.minBrokerAddrInGroup = minBrokerAddr; - if (this.consumerFilterManager != null) { - this.consumerFilterManager.persist(); - } + this.changeSpecialServiceStatus(this.brokerConfig.getBrokerId() == minBrokerId); + this.registerBrokerAll(true, false, brokerConfig.isForceRegister()); + + isIsolated = false; } - private void unregisterBrokerAll() { - this.brokerOuterAPI.unregisterBrokerAll( - this.brokerConfig.getBrokerClusterName(), - this.getBrokerAddr(), - this.brokerConfig.getBrokerName(), - this.brokerConfig.getBrokerId()); + public void startServiceWithoutCondition() { + BrokerController.LOG.info("{} start service", this.brokerConfig.getCanonicalName()); + + this.changeSpecialServiceStatus(this.brokerConfig.getBrokerId() == MixAll.MASTER_ID); + this.registerBrokerAll(true, false, brokerConfig.isForceRegister()); + + isIsolated = false; } - public String getBrokerAddr() { - return this.brokerConfig.getBrokerIP1() + ":" + this.nettyServerConfig.getListenPort(); + public void stopService() { + BrokerController.LOG.info("{} stop service", this.getBrokerConfig().getCanonicalName()); + isIsolated = true; + this.changeSpecialServiceStatus(false); } - public void start() throws Exception { - if (this.messageStore != null) { - this.messageStore.start(); + public boolean isSpecialServiceRunning() { + if (isScheduleServiceStart() && isTransactionCheckServiceStart()) { + return true; } - if (this.remotingServer != null) { - this.remotingServer.start(); - } + return this.ackMessageProcessor != null && this.ackMessageProcessor.isPopReviveServiceRunning(); + } - if (this.fastRemotingServer != null) { - this.fastRemotingServer.start(); + private void onMasterOffline() { + // close channels with master broker + String masterAddr = this.slaveSynchronize.getMasterAddr(); + if (masterAddr != null) { + this.brokerOuterAPI.getRemotingClient().closeChannels( + Arrays.asList(masterAddr, MixAll.brokerVIPChannel(true, masterAddr))); } + // master not available, stop sync + this.slaveSynchronize.setMasterAddr(null); + this.messageStore.updateHaMasterAddress(null); + } - if (this.brokerOuterAPI != null) { - this.brokerOuterAPI.start(); - } + private void onMasterOnline(String masterAddr, String masterHaAddr) { + boolean needSyncMasterFlushOffset = this.messageStore.getMasterFlushedOffset() == 0 + && this.messageStoreConfig.isSyncMasterFlushOffsetWhenStartup(); + if (masterHaAddr == null || needSyncMasterFlushOffset) { + try { + BrokerSyncInfo brokerSyncInfo = this.brokerOuterAPI.retrieveBrokerHaInfo(masterAddr); - if (this.pullRequestHoldService != null) { - this.pullRequestHoldService.start(); + if (needSyncMasterFlushOffset) { + LOG.info("Set master flush offset in slave to {}", brokerSyncInfo.getMasterFlushOffset()); + this.messageStore.setMasterFlushedOffset(brokerSyncInfo.getMasterFlushOffset()); + } + + if (masterHaAddr == null) { + this.messageStore.updateHaMasterAddress(brokerSyncInfo.getMasterHaAddress()); + this.messageStore.updateMasterAddress(brokerSyncInfo.getMasterAddress()); + } + } catch (Exception e) { + LOG.error("retrieve master ha info exception, {}", e); + } } - if (this.clientHousekeepingService != null) { - this.clientHousekeepingService.start(); + // set master HA address. + if (masterHaAddr != null) { + this.messageStore.updateHaMasterAddress(masterHaAddr); } - if (this.filterServerManager != null) { - this.filterServerManager.start(); + // wakeup HAClient + this.messageStore.wakeupHAClient(); + } + + private void onMinBrokerChange(long minBrokerId, String minBrokerAddr, String offlineBrokerAddr, + String masterHaAddr) { + LOG.info("Min broker changed, old: {}-{}, new {}-{}", + this.minBrokerIdInGroup, this.minBrokerAddrInGroup, minBrokerId, minBrokerAddr); + + this.minBrokerIdInGroup = minBrokerId; + this.minBrokerAddrInGroup = minBrokerAddr; + + this.changeSpecialServiceStatus(this.brokerConfig.getBrokerId() == this.minBrokerIdInGroup); + + if (offlineBrokerAddr != null && offlineBrokerAddr.equals(this.slaveSynchronize.getMasterAddr())) { + // master offline + onMasterOffline(); } - this.registerBrokerAll(true, false); + if (minBrokerId == MixAll.MASTER_ID && minBrokerAddr != null) { + // master online + onMasterOnline(minBrokerAddr, masterHaAddr); + } - this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + // notify PullRequest on hold to pull from master. + if (this.minBrokerIdInGroup == MixAll.MASTER_ID) { + this.pullRequestHoldService.notifyMasterOnline(); + } + } - @Override - public void run() { + public void updateMinBroker(long minBrokerId, String minBrokerAddr) { + if (brokerConfig.isEnableSlaveActingMaster() && brokerConfig.getBrokerId() != MixAll.MASTER_ID) { + if (lock.tryLock()) { try { - BrokerController.this.registerBrokerAll(true, false); - } catch (Throwable e) { - log.error("registerBrokerAll Exception", e); + if (minBrokerId != this.minBrokerIdInGroup) { + String offlineBrokerAddr = null; + if (minBrokerId > this.minBrokerIdInGroup) { + offlineBrokerAddr = this.minBrokerAddrInGroup; + } + onMinBrokerChange(minBrokerId, minBrokerAddr, offlineBrokerAddr, null); + } + } finally { + lock.unlock(); } } - }, 1000 * 10, 1000 * 30, TimeUnit.MILLISECONDS); - - if (this.brokerStatsManager != null) { - this.brokerStatsManager.start(); } + } - if (this.brokerFastFailure != null) { - this.brokerFastFailure.start(); + public void updateMinBroker(long minBrokerId, String minBrokerAddr, String offlineBrokerAddr, + String masterHaAddr) { + if (brokerConfig.isEnableSlaveActingMaster() && brokerConfig.getBrokerId() != MixAll.MASTER_ID) { + try { + if (lock.tryLock(3000, TimeUnit.MILLISECONDS)) { + try { + if (minBrokerId != this.minBrokerIdInGroup) { + onMinBrokerChange(minBrokerId, minBrokerAddr, offlineBrokerAddr, masterHaAddr); + } + } finally { + lock.unlock(); + } + + } + } catch (InterruptedException e) { + LOG.error("Update min broker error, {}", e); + } } } - public synchronized void registerBrokerAll(final boolean checkOrderConfig, boolean oneway) { - TopicConfigSerializeWrapper topicConfigWrapper = this.getTopicConfigManager().buildTopicConfigSerializeWrapper(); + public void changeSpecialServiceStatus(boolean shouldStart) { - if (!PermName.isWriteable(this.getBrokerConfig().getBrokerPermission()) - || !PermName.isReadable(this.getBrokerConfig().getBrokerPermission())) { - ConcurrentHashMap topicConfigTable = new ConcurrentHashMap(); - for (TopicConfig topicConfig : topicConfigWrapper.getTopicConfigTable().values()) { - TopicConfig tmp = - new TopicConfig(topicConfig.getTopicName(), topicConfig.getReadQueueNums(), topicConfig.getWriteQueueNums(), - this.brokerConfig.getBrokerPermission()); - topicConfigTable.put(topicConfig.getTopicName(), tmp); + for (BrokerAttachedPlugin brokerAttachedPlugin : brokerAttachedPlugins) { + if (brokerAttachedPlugin != null) { + brokerAttachedPlugin.statusChanged(shouldStart); } - topicConfigWrapper.setTopicConfigTable(topicConfigTable); } - RegisterBrokerResult registerBrokerResult = this.brokerOuterAPI.registerBrokerAll( - this.brokerConfig.getBrokerClusterName(), - this.getBrokerAddr(), - this.brokerConfig.getBrokerName(), - this.brokerConfig.getBrokerId(), - this.getHAServerAddr(), - topicConfigWrapper, - this.filterServerManager.buildNewFilterServerList(), - oneway, - this.brokerConfig.getRegisterBrokerTimeoutMills()); + changeScheduleServiceStatus(shouldStart); - if (registerBrokerResult != null) { - if (this.updateMasterHAServerAddrPeriodically && registerBrokerResult.getHaServerAddr() != null) { - this.messageStore.updateHaMasterAddress(registerBrokerResult.getHaServerAddr()); + changeTransactionCheckServiceStatus(shouldStart); + + if (this.ackMessageProcessor != null) { + LOG.info("Set PopReviveService Status to {}", shouldStart); + this.ackMessageProcessor.setPopReviveServiceStatus(shouldStart); + } + } + + private synchronized void changeTransactionCheckServiceStatus(boolean shouldStart) { + if (isTransactionCheckServiceStart != shouldStart) { + LOG.info("TransactionCheckService status changed to {}", shouldStart); + if (shouldStart) { + this.transactionalMessageCheckService.start(); + } else { + this.transactionalMessageCheckService.shutdown(true); } + isTransactionCheckServiceStart = shouldStart; + } + } - this.slaveSynchronize.setMasterAddr(registerBrokerResult.getMasterAddr()); + public synchronized void changeScheduleServiceStatus(boolean shouldStart) { + if (isScheduleServiceStart != shouldStart) { + LOG.info("ScheduleServiceStatus changed to {}", shouldStart); + if (shouldStart) { + this.scheduleMessageService.start(); + } else { + this.scheduleMessageService.stop(); + } + isScheduleServiceStart = shouldStart; - if (checkOrderConfig) { - this.getTopicConfigManager().updateOrderTopicConfig(registerBrokerResult.getKvTable()); + if (timerMessageStore != null) { + timerMessageStore.syncLastReadTimeMs(); + timerMessageStore.setShouldRunningDequeue(shouldStart); } } } + public MessageStore getMessageStoreByBrokerName(String brokerName) { + if (this.brokerConfig.getBrokerName().equals(brokerName)) { + return this.getMessageStore(); + } + return null; + } + + public BrokerIdentity getBrokerIdentity() { + if (messageStoreConfig.isEnableDLegerCommitLog()) { + return new BrokerIdentity( + brokerConfig.getBrokerClusterName(), brokerConfig.getBrokerName(), + Integer.parseInt(messageStoreConfig.getdLegerSelfId().substring(1)), brokerConfig.isInBrokerContainer()); + } else { + return new BrokerIdentity( + brokerConfig.getBrokerClusterName(), brokerConfig.getBrokerName(), + brokerConfig.getBrokerId(), brokerConfig.isInBrokerContainer()); + } + } + public TopicConfigManager getTopicConfigManager() { return topicConfigManager; } @@ -748,6 +2150,10 @@ public void setTopicConfigManager(TopicConfigManager topicConfigManager) { this.topicConfigManager = topicConfigManager; } + public TopicQueueMappingManager getTopicQueueMappingManager() { + return topicQueueMappingManager; + } + public String getHAServerAddr() { return this.brokerConfig.getBrokerIP2() + ":" + this.messageStoreConfig.getHaListenPort(); } @@ -760,10 +2166,18 @@ public SlaveSynchronize getSlaveSynchronize() { return slaveSynchronize; } + public ScheduledExecutorService getScheduledExecutorService() { + return scheduledExecutorService; + } + public ExecutorService getPullMessageExecutor() { return pullMessageExecutor; } + public ExecutorService getPutMessageFutureExecutor() { + return putMessageFutureExecutor; + } + public void setPullMessageExecutor(ExecutorService pullMessageExecutor) { this.pullMessageExecutor = pullMessageExecutor; } @@ -772,8 +2186,8 @@ public BlockingQueue getSendThreadPoolQueue() { return sendThreadPoolQueue; } - public FilterServerManager getFilterServerManager() { - return filterServerManager; + public BlockingQueue getAckThreadPoolQueue() { + return ackThreadPoolQueue; } public BrokerStatsManager getBrokerStatsManager() { @@ -786,7 +2200,7 @@ public List getSendMessageHookList() { public void registerSendMessageHook(final SendMessageHook hook) { this.sendMessageHookList.add(hook); - log.info("register SendMessageHook Hook, {}", hook.hookName()); + LOG.info("register SendMessageHook Hook, {}", hook.hookName()); } public List getConsumeMessageHookList() { @@ -795,11 +2209,12 @@ public List getConsumeMessageHookList() { public void registerConsumeMessageHook(final ConsumeMessageHook hook) { this.consumeMessageHookList.add(hook); - log.info("register ConsumeMessageHook Hook, {}", hook.hookName()); + LOG.info("register ConsumeMessageHook Hook, {}", hook.hookName()); } public void registerServerRPCHook(RPCHook rpcHook) { getRemotingServer().registerRPCHook(rpcHook); + this.fastRemotingServer.registerRPCHook(rpcHook); } public RemotingServer getRemotingServer() { @@ -810,6 +2225,14 @@ public void setRemotingServer(RemotingServer remotingServer) { this.remotingServer = remotingServer; } + public CountDownLatch getRemotingServerStartLatch() { + return remotingServerStartLatch; + } + + public void setRemotingServerStartLatch(CountDownLatch remotingServerStartLatch) { + this.remotingServerStartLatch = remotingServerStartLatch; + } + public void registerClientRPCHook(RPCHook rpcHook) { this.getBrokerOuterAPI().registerRPCHook(rpcHook); } @@ -829,4 +2252,180 @@ public void setStoreHost(InetSocketAddress storeHost) { public Configuration getConfiguration() { return this.configuration; } + + public BlockingQueue getHeartbeatThreadPoolQueue() { + return heartbeatThreadPoolQueue; + } + + public TransactionalMessageCheckService getTransactionalMessageCheckService() { + return transactionalMessageCheckService; + } + + public void setTransactionalMessageCheckService( + TransactionalMessageCheckService transactionalMessageCheckService) { + this.transactionalMessageCheckService = transactionalMessageCheckService; + } + + public TransactionalMessageService getTransactionalMessageService() { + return transactionalMessageService; + } + + public void setTransactionalMessageService(TransactionalMessageService transactionalMessageService) { + this.transactionalMessageService = transactionalMessageService; + } + + public AbstractTransactionalMessageCheckListener getTransactionalMessageCheckListener() { + return transactionalMessageCheckListener; + } + + public void setTransactionalMessageCheckListener( + AbstractTransactionalMessageCheckListener transactionalMessageCheckListener) { + this.transactionalMessageCheckListener = transactionalMessageCheckListener; + } + + public BlockingQueue getEndTransactionThreadPoolQueue() { + return endTransactionThreadPoolQueue; + + } + + public Map getAccessValidatorMap() { + return accessValidatorMap; + } + + public ExecutorService getSendMessageExecutor() { + return sendMessageExecutor; + } + + public SendMessageProcessor getSendMessageProcessor() { + return sendMessageProcessor; + } + + public QueryAssignmentProcessor getQueryAssignmentProcessor() { + return queryAssignmentProcessor; + } + + public TopicQueueMappingCleanService getTopicQueueMappingCleanService() { + return topicQueueMappingCleanService; + } + + public ExecutorService getAdminBrokerExecutor() { + return adminBrokerExecutor; + } + + public BlockingQueue getLitePullThreadPoolQueue() { + return litePullThreadPoolQueue; + } + + public ShutdownHook getShutdownHook() { + return shutdownHook; + } + + public void setShutdownHook(ShutdownHook shutdownHook) { + this.shutdownHook = shutdownHook; + } + + public long getMinBrokerIdInGroup() { + return this.brokerConfig.getBrokerId(); + } + + public BrokerController peekMasterBroker() { + return brokerConfig.getBrokerId() == MixAll.MASTER_ID ? this : null; + } + + public BrokerMemberGroup getBrokerMemberGroup() { + return this.brokerMemberGroup; + } + + public int getListenPort() { + return this.nettyServerConfig.getListenPort(); + } + + public List getBrokerAttachedPlugins() { + return brokerAttachedPlugins; + } + + public EscapeBridge getEscapeBridge() { + return escapeBridge; + } + + public long getShouldStartTime() { + return shouldStartTime; + } + + public BrokerPreOnlineService getBrokerPreOnlineService() { + return brokerPreOnlineService; + } + + public EndTransactionProcessor getEndTransactionProcessor() { + return endTransactionProcessor; + } + + public boolean isScheduleServiceStart() { + return isScheduleServiceStart; + } + + public boolean isTransactionCheckServiceStart() { + return isTransactionCheckServiceStart; + } + + public ScheduleMessageService getScheduleMessageService() { + return scheduleMessageService; + } + + public ReplicasManager getReplicasManager() { + return replicasManager; + } + + public void setIsolated(boolean isolated) { + isIsolated = isolated; + } + + public boolean isIsolated() { + return this.isIsolated; + } + + public TimerCheckpoint getTimerCheckpoint() { + return timerCheckpoint; + } + + public TopicRouteInfoManager getTopicRouteInfoManager() { + return this.topicRouteInfoManager; + } + + public BlockingQueue getClientManagerThreadPoolQueue() { + return clientManagerThreadPoolQueue; + } + + public BlockingQueue getConsumerManagerThreadPoolQueue() { + return consumerManagerThreadPoolQueue; + } + + public BlockingQueue getAsyncPutThreadPoolQueue() { + return putThreadPoolQueue; + } + + public BlockingQueue getReplyThreadPoolQueue() { + return replyThreadPoolQueue; + } + + public BlockingQueue getAdminBrokerThreadPoolQueue() { + return adminBrokerThreadPoolQueue; + } + + public ColdDataPullRequestHoldService getColdDataPullRequestHoldService() { + return coldDataPullRequestHoldService; + } + + public void setColdDataPullRequestHoldService( + ColdDataPullRequestHoldService coldDataPullRequestHoldService) { + this.coldDataPullRequestHoldService = coldDataPullRequestHoldService; + } + + public ColdDataCgCtrService getColdDataCgCtrService() { + return coldDataCgCtrService; + } + + public void setColdDataCgCtrService(ColdDataCgCtrService coldDataCgCtrService) { + this.coldDataCgCtrService = coldDataCgCtrService; + } } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/BrokerPathConfigHelper.java b/broker/src/main/java/org/apache/rocketmq/broker/BrokerPathConfigHelper.java index 42c8da9f3fb..0b2f52f32eb 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/BrokerPathConfigHelper.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/BrokerPathConfigHelper.java @@ -35,15 +35,40 @@ public static String getTopicConfigPath(final String rootDir) { return rootDir + File.separator + "config" + File.separator + "topics.json"; } + public static String getTopicQueueMappingPath(final String rootDir) { + return rootDir + File.separator + "config" + File.separator + "topicQueueMapping.json"; + } + public static String getConsumerOffsetPath(final String rootDir) { return rootDir + File.separator + "config" + File.separator + "consumerOffset.json"; } + public static String getLmqConsumerOffsetPath(final String rootDir) { + return rootDir + File.separator + "config" + File.separator + "lmqConsumerOffset.json"; + } + + public static String getConsumerOrderInfoPath(final String rootDir) { + return rootDir + File.separator + "config" + File.separator + "consumerOrderInfo.json"; + } + public static String getSubscriptionGroupPath(final String rootDir) { return rootDir + File.separator + "config" + File.separator + "subscriptionGroup.json"; } + public static String getTimerCheckPath(final String rootDir) { + return rootDir + File.separator + "config" + File.separator + "timercheck"; + } + public static String getTimerMetricsPath(final String rootDir) { + return rootDir + File.separator + "config" + File.separator + "timermetrics"; + } + public static String getTransactionMetricsPath(final String rootDir) { + return rootDir + File.separator + "config" + File.separator + "transactionMetrics"; + } public static String getConsumerFilterPath(final String rootDir) { return rootDir + File.separator + "config" + File.separator + "consumerFilter.json"; } + + public static String getMessageRequestModePath(final String rootDir) { + return rootDir + File.separator + "config" + File.separator + "messageRequestMode.json"; + } } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/BrokerPreOnlineService.java b/broker/src/main/java/org/apache/rocketmq/broker/BrokerPreOnlineService.java new file mode 100644 index 00000000000..de2ccb29399 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/BrokerPreOnlineService.java @@ -0,0 +1,287 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.broker.plugin.BrokerAttachedPlugin; +import org.apache.rocketmq.broker.schedule.DelayOffsetSerializeWrapper; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.protocol.BrokerSyncInfo; +import org.apache.rocketmq.remoting.protocol.body.BrokerMemberGroup; +import org.apache.rocketmq.remoting.protocol.body.ConsumerOffsetSerializeWrapper; +import org.apache.rocketmq.store.config.StorePathConfigHelper; +import org.apache.rocketmq.store.ha.HAConnectionState; +import org.apache.rocketmq.store.ha.HAConnectionStateNotificationRequest; +import org.apache.rocketmq.store.timer.TimerCheckpoint; + +public class BrokerPreOnlineService extends ServiceThread { + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + private final BrokerController brokerController; + + private int waitBrokerIndex = 0; + + public BrokerPreOnlineService(BrokerController brokerController) { + this.brokerController = brokerController; + } + + @Override + public String getServiceName() { + if (this.brokerController != null && this.brokerController.getBrokerConfig().isInBrokerContainer()) { + return brokerController.getBrokerIdentity().getIdentifier() + BrokerPreOnlineService.class.getSimpleName(); + } + return BrokerPreOnlineService.class.getSimpleName(); + } + + @Override + public void run() { + LOGGER.info(this.getServiceName() + " service started"); + + while (!this.isStopped()) { + if (!this.brokerController.isIsolated()) { + LOGGER.info("broker {} is online", this.brokerController.getBrokerConfig().getCanonicalName()); + break; + } + try { + boolean isSuccess = this.prepareForBrokerOnline(); + if (!isSuccess) { + this.waitForRunning(1000); + } else { + break; + } + } catch (Exception e) { + LOGGER.error("Broker preOnline error, ", e); + } + } + + LOGGER.info(this.getServiceName() + " service end"); + } + + CompletableFuture waitForHaHandshakeComplete(String brokerAddr) { + LOGGER.info("wait for handshake completion with {}", brokerAddr); + HAConnectionStateNotificationRequest request = + new HAConnectionStateNotificationRequest(HAConnectionState.TRANSFER, RemotingHelper.parseHostFromAddress(brokerAddr), true); + if (this.brokerController.getMessageStore().getHaService() != null) { + this.brokerController.getMessageStore().getHaService().putGroupConnectionStateRequest(request); + } else { + LOGGER.error("HAService is null, maybe broker config is wrong. For example, duplicationEnable is true"); + request.getRequestFuture().complete(false); + } + return request.getRequestFuture(); + } + + private boolean futureWaitAction(boolean result, BrokerMemberGroup brokerMemberGroup) { + if (!result) { + LOGGER.error("wait for handshake completion failed, HA connection lost"); + return false; + } + if (this.brokerController.getBrokerConfig().getBrokerId() != MixAll.MASTER_ID) { + LOGGER.info("slave preOnline complete, start service"); + long minBrokerId = getMinBrokerId(brokerMemberGroup.getBrokerAddrs()); + this.brokerController.startService(minBrokerId, brokerMemberGroup.getBrokerAddrs().get(minBrokerId)); + } + return true; + } + + private boolean prepareForMasterOnline(BrokerMemberGroup brokerMemberGroup) { + List brokerIdList = new ArrayList<>(brokerMemberGroup.getBrokerAddrs().keySet()); + Collections.sort(brokerIdList); + while (true) { + if (waitBrokerIndex >= brokerIdList.size()) { + LOGGER.info("master preOnline complete, start service"); + this.brokerController.startService(MixAll.MASTER_ID, this.brokerController.getBrokerAddr()); + return true; + } + + String brokerAddrToWait = brokerMemberGroup.getBrokerAddrs().get(brokerIdList.get(waitBrokerIndex)); + + try { + this.brokerController.getBrokerOuterAPI(). + sendBrokerHaInfo(brokerAddrToWait, this.brokerController.getHAServerAddr(), + this.brokerController.getMessageStore().getBrokerInitMaxOffset(), this.brokerController.getBrokerAddr()); + } catch (Exception e) { + LOGGER.error("send ha address to {} exception, {}", brokerAddrToWait, e); + return false; + } + + CompletableFuture haHandshakeFuture = waitForHaHandshakeComplete(brokerAddrToWait) + .thenApply(result -> futureWaitAction(result, brokerMemberGroup)); + + try { + if (!haHandshakeFuture.get()) { + return false; + } + } catch (Exception e) { + LOGGER.error("Wait handshake completion exception, {}", e); + return false; + } + + if (syncMetadataReverse(brokerAddrToWait)) { + waitBrokerIndex++; + } else { + return false; + } + } + } + + private boolean syncMetadataReverse(String brokerAddr) { + try { + LOGGER.info("Get metadata reverse from {}", brokerAddr); + + String delayOffset = this.brokerController.getBrokerOuterAPI().getAllDelayOffset(brokerAddr); + DelayOffsetSerializeWrapper delayOffsetSerializeWrapper = + DelayOffsetSerializeWrapper.fromJson(delayOffset, DelayOffsetSerializeWrapper.class); + + ConsumerOffsetSerializeWrapper consumerOffsetSerializeWrapper = this.brokerController.getBrokerOuterAPI().getAllConsumerOffset(brokerAddr); + + TimerCheckpoint timerCheckpoint = this.brokerController.getBrokerOuterAPI().getTimerCheckPoint(brokerAddr); + + if (null != consumerOffsetSerializeWrapper && brokerController.getConsumerOffsetManager().getDataVersion().compare(consumerOffsetSerializeWrapper.getDataVersion()) <= 0) { + LOGGER.info("{}'s consumerOffset data version is larger than master broker, {}'s consumerOffset will be used.", brokerAddr, brokerAddr); + this.brokerController.getConsumerOffsetManager().getOffsetTable() + .putAll(consumerOffsetSerializeWrapper.getOffsetTable()); + this.brokerController.getConsumerOffsetManager().getDataVersion().assignNewOne(consumerOffsetSerializeWrapper.getDataVersion()); + this.brokerController.getConsumerOffsetManager().persist(); + } + + if (null != delayOffset && brokerController.getScheduleMessageService().getDataVersion().compare(delayOffsetSerializeWrapper.getDataVersion()) <= 0) { + LOGGER.info("{}'s scheduleMessageService data version is larger than master broker, {}'s delayOffset will be used.", brokerAddr, brokerAddr); + String fileName = + StorePathConfigHelper.getDelayOffsetStorePath(this.brokerController + .getMessageStoreConfig().getStorePathRootDir()); + try { + MixAll.string2File(delayOffset, fileName); + this.brokerController.getScheduleMessageService().load(); + } catch (IOException e) { + LOGGER.error("Persist file Exception, {}", fileName, e); + } + } + + if (null != this.brokerController.getTimerCheckpoint() && this.brokerController.getTimerCheckpoint().getDataVersion().compare(timerCheckpoint.getDataVersion()) <= 0) { + LOGGER.info("{}'s timerCheckpoint data version is larger than master broker, {}'s timerCheckpoint will be used.", brokerAddr, brokerAddr); + this.brokerController.getTimerCheckpoint().setLastReadTimeMs(timerCheckpoint.getLastReadTimeMs()); + this.brokerController.getTimerCheckpoint().setMasterTimerQueueOffset(timerCheckpoint.getMasterTimerQueueOffset()); + this.brokerController.getTimerCheckpoint().getDataVersion().assignNewOne(timerCheckpoint.getDataVersion()); + this.brokerController.getTimerCheckpoint().flush(); + } + + for (BrokerAttachedPlugin brokerAttachedPlugin : brokerController.getBrokerAttachedPlugins()) { + if (brokerAttachedPlugin != null) { + brokerAttachedPlugin.syncMetadataReverse(brokerAddr); + } + } + + } catch (Exception e) { + LOGGER.error("GetMetadataReverse Failed", e); + return false; + } + + return true; + } + + private boolean prepareForSlaveOnline(BrokerMemberGroup brokerMemberGroup) { + BrokerSyncInfo brokerSyncInfo; + try { + brokerSyncInfo = this.brokerController.getBrokerOuterAPI() + .retrieveBrokerHaInfo(brokerMemberGroup.getBrokerAddrs().get(MixAll.MASTER_ID)); + } catch (Exception e) { + LOGGER.error("retrieve master ha info exception, {}", e); + return false; + } + + if (this.brokerController.getMessageStore().getMasterFlushedOffset() == 0 + && this.brokerController.getMessageStoreConfig().isSyncMasterFlushOffsetWhenStartup()) { + LOGGER.info("Set master flush offset in slave to {}", brokerSyncInfo.getMasterFlushOffset()); + this.brokerController.getMessageStore().setMasterFlushedOffset(brokerSyncInfo.getMasterFlushOffset()); + } + + if (brokerSyncInfo.getMasterHaAddress() != null) { + this.brokerController.getMessageStore().updateHaMasterAddress(brokerSyncInfo.getMasterHaAddress()); + this.brokerController.getMessageStore().updateMasterAddress(brokerSyncInfo.getMasterAddress()); + } else { + LOGGER.info("fetch master ha address return null, start service directly"); + long minBrokerId = getMinBrokerId(brokerMemberGroup.getBrokerAddrs()); + this.brokerController.startService(minBrokerId, brokerMemberGroup.getBrokerAddrs().get(minBrokerId)); + return true; + } + + CompletableFuture haHandshakeFuture = waitForHaHandshakeComplete(brokerSyncInfo.getMasterHaAddress()) + .thenApply(result -> futureWaitAction(result, brokerMemberGroup)); + + try { + if (!haHandshakeFuture.get()) { + return false; + } + } catch (Exception e) { + LOGGER.error("Wait handshake completion exception, {}", e); + return false; + } + + return true; + } + + private boolean prepareForBrokerOnline() { + BrokerMemberGroup brokerMemberGroup; + try { + brokerMemberGroup = this.brokerController.getBrokerOuterAPI().syncBrokerMemberGroup( + this.brokerController.getBrokerConfig().getBrokerClusterName(), + this.brokerController.getBrokerConfig().getBrokerName(), + this.brokerController.getBrokerConfig().isCompatibleWithOldNameSrv()); + } catch (Exception e) { + LOGGER.error("syncBrokerMemberGroup from namesrv error, start service failed, will try later, ", e); + return false; + } + + if (brokerMemberGroup != null && !brokerMemberGroup.getBrokerAddrs().isEmpty()) { + long minBrokerId = getMinBrokerId(brokerMemberGroup.getBrokerAddrs()); + + if (this.brokerController.getBrokerConfig().getBrokerId() == MixAll.MASTER_ID) { + return prepareForMasterOnline(brokerMemberGroup); + } else if (minBrokerId == MixAll.MASTER_ID) { + return prepareForSlaveOnline(brokerMemberGroup); + } else { + LOGGER.info("no master online, start service directly"); + this.brokerController.startService(minBrokerId, brokerMemberGroup.getBrokerAddrs().get(minBrokerId)); + } + } else { + LOGGER.info("no other broker online, will start service directly"); + this.brokerController.startService(this.brokerController.getBrokerConfig().getBrokerId(), this.brokerController.getBrokerAddr()); + } + + return true; + } + + private long getMinBrokerId(Map brokerAddrMap) { + Map brokerAddrMapCopy = new HashMap<>(brokerAddrMap); + brokerAddrMapCopy.remove(this.brokerController.getBrokerConfig().getBrokerId()); + if (!brokerAddrMapCopy.isEmpty()) { + return Collections.min(brokerAddrMapCopy.keySet()); + } + return this.brokerController.getBrokerConfig().getBrokerId(); + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/BrokerStartup.java b/broker/src/main/java/org/apache/rocketmq/broker/BrokerStartup.java index e768c7f95eb..3151683161d 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/BrokerStartup.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/BrokerStartup.java @@ -16,41 +16,35 @@ */ package org.apache.rocketmq.broker; -import ch.qos.logback.classic.LoggerContext; -import ch.qos.logback.classic.joran.JoranConfigurator; import java.io.BufferedInputStream; -import java.io.FileInputStream; import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.Properties; import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; -import org.apache.commons.cli.PosixParser; +import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.common.BrokerConfig; import org.apache.rocketmq.common.MQVersion; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.remoting.common.RemotingUtil; -import org.apache.rocketmq.remoting.common.TlsMode; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.remoting.netty.NettyClientConfig; import org.apache.rocketmq.remoting.netty.NettyServerConfig; -import org.apache.rocketmq.remoting.netty.NettySystemConfig; -import org.apache.rocketmq.remoting.netty.TlsSystemConfig; import org.apache.rocketmq.remoting.protocol.RemotingCommand; import org.apache.rocketmq.srvutil.ServerUtil; import org.apache.rocketmq.store.config.BrokerRole; import org.apache.rocketmq.store.config.MessageStoreConfig; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_ENABLE; public class BrokerStartup { - public static Properties properties = null; - public static CommandLine commandLine = null; - public static String configFile = null; + public static Logger log; + public static final SystemConfigFileHelper CONFIG_FILE_HELPER = new SystemConfigFileHelper(); public static void main(String[] args) { start(createBrokerController(args)); @@ -58,17 +52,18 @@ public static void main(String[] args) { public static BrokerController start(BrokerController controller) { try { - controller.start(); - String tip = "The broker[" + controller.getBrokerConfig().getBrokerName() + ", " - + controller.getBrokerAddr() + "] boot success. serializeType=" + RemotingCommand.getSerializeTypeConfigInThisServer(); + String tip = String.format("The broker[%s, %s] boot success. serializeType=%s", + controller.getBrokerConfig().getBrokerName(), controller.getBrokerAddr(), + RemotingCommand.getSerializeTypeConfigInThisServer()); if (null != controller.getBrokerConfig().getNamesrvAddr()) { tip += " and name server is " + controller.getBrokerConfig().getNamesrvAddr(); } log.info(tip); + System.out.printf("%s%n", tip); return controller; } catch (Throwable e) { e.printStackTrace(); @@ -78,166 +73,181 @@ public static BrokerController start(BrokerController controller) { return null; } - public static BrokerController createBrokerController(String[] args) { - System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION)); - - if (null == System.getProperty(NettySystemConfig.COM_ROCKETMQ_REMOTING_SOCKET_SNDBUF_SIZE)) { - NettySystemConfig.socketSndbufSize = 131072; + public static void shutdown(final BrokerController controller) { + if (null != controller) { + controller.shutdown(); } + } + + public static BrokerController buildBrokerController(String[] args) throws Exception { + System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION)); - if (null == System.getProperty(NettySystemConfig.COM_ROCKETMQ_REMOTING_SOCKET_RCVBUF_SIZE)) { - NettySystemConfig.socketRcvbufSize = 131072; + final BrokerConfig brokerConfig = new BrokerConfig(); + final NettyServerConfig nettyServerConfig = new NettyServerConfig(); + final NettyClientConfig nettyClientConfig = new NettyClientConfig(); + final MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + nettyServerConfig.setListenPort(10911); + messageStoreConfig.setHaListenPort(0); + + Options options = ServerUtil.buildCommandlineOptions(new Options()); + CommandLine commandLine = ServerUtil.parseCmdLine( + "mqbroker", args, buildCommandlineOptions(options), new DefaultParser()); + if (null == commandLine) { + System.exit(-1); } - try { - //PackageConflictDetect.detectFastjson(); - Options options = ServerUtil.buildCommandlineOptions(new Options()); - commandLine = ServerUtil.parseCmdLine("mqbroker", args, buildCommandlineOptions(options), - new PosixParser()); - if (null == commandLine) { - System.exit(-1); + Properties properties = null; + if (commandLine.hasOption('c')) { + String file = commandLine.getOptionValue('c'); + if (file != null) { + CONFIG_FILE_HELPER.setFile(file); + BrokerPathConfigHelper.setBrokerConfigPath(file); + properties = CONFIG_FILE_HELPER.loadConfig(); } + } - final BrokerConfig brokerConfig = new BrokerConfig(); - final NettyServerConfig nettyServerConfig = new NettyServerConfig(); - final NettyClientConfig nettyClientConfig = new NettyClientConfig(); - - nettyClientConfig.setUseTLS(Boolean.parseBoolean(System.getProperty(TLS_ENABLE, - String.valueOf(TlsSystemConfig.tlsMode == TlsMode.ENFORCING)))); - nettyServerConfig.setListenPort(10911); - final MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + if (properties != null) { + properties2SystemEnv(properties); + MixAll.properties2Object(properties, brokerConfig); + MixAll.properties2Object(properties, nettyServerConfig); + MixAll.properties2Object(properties, nettyClientConfig); + MixAll.properties2Object(properties, messageStoreConfig); + } - if (BrokerRole.SLAVE == messageStoreConfig.getBrokerRole()) { - int ratio = messageStoreConfig.getAccessMessageInMemoryMaxRatio() - 10; - messageStoreConfig.setAccessMessageInMemoryMaxRatio(ratio); - } + MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), brokerConfig); + if (null == brokerConfig.getRocketmqHome()) { + System.out.printf("Please set the %s variable in your environment " + + "to match the location of the RocketMQ installation", MixAll.ROCKETMQ_HOME_ENV); + System.exit(-2); + } - if (commandLine.hasOption('c')) { - String file = commandLine.getOptionValue('c'); - if (file != null) { - configFile = file; - InputStream in = new BufferedInputStream(new FileInputStream(file)); - properties = new Properties(); - properties.load(in); - - properties2SystemEnv(properties); - MixAll.properties2Object(properties, brokerConfig); - MixAll.properties2Object(properties, nettyServerConfig); - MixAll.properties2Object(properties, nettyClientConfig); - MixAll.properties2Object(properties, messageStoreConfig); - - BrokerPathConfigHelper.setBrokerConfigPath(file); - in.close(); + // Validate namesrvAddr + String namesrvAddr = brokerConfig.getNamesrvAddr(); + if (StringUtils.isNotBlank(namesrvAddr)) { + try { + String[] addrArray = namesrvAddr.split(";"); + for (String addr : addrArray) { + NetworkUtil.string2SocketAddress(addr); } + } catch (Exception e) { + System.out.printf("The Name Server Address[%s] illegal, please set it as follows, " + + "\"127.0.0.1:9876;192.168.0.1:9876\"%n", namesrvAddr); + System.exit(-3); } + } - MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), brokerConfig); - - if (null == brokerConfig.getRocketmqHome()) { - System.out.printf("Please set the " + MixAll.ROCKETMQ_HOME_ENV - + " variable in your environment to match the location of the RocketMQ installation"); - System.exit(-2); - } - - String namesrvAddr = brokerConfig.getNamesrvAddr(); - if (null != namesrvAddr) { - try { - String[] addrArray = namesrvAddr.split(";"); - for (String addr : addrArray) { - RemotingUtil.string2SocketAddress(addr); - } - } catch (Exception e) { - System.out.printf( - "The Name Server Address[%s] illegal, please set it as follows, \"127.0.0.1:9876;192.168.0.1:9876\"%n", - namesrvAddr); - System.exit(-3); - } - } + if (BrokerRole.SLAVE == messageStoreConfig.getBrokerRole()) { + int ratio = messageStoreConfig.getAccessMessageInMemoryMaxRatio() - 10; + messageStoreConfig.setAccessMessageInMemoryMaxRatio(ratio); + } + // Set broker role according to ha config + if (!brokerConfig.isEnableControllerMode()) { switch (messageStoreConfig.getBrokerRole()) { case ASYNC_MASTER: case SYNC_MASTER: brokerConfig.setBrokerId(MixAll.MASTER_ID); break; case SLAVE: - if (brokerConfig.getBrokerId() <= 0) { - System.out.printf("Slave's brokerId must be > 0"); + if (brokerConfig.getBrokerId() <= MixAll.MASTER_ID) { + System.out.printf("Slave's brokerId must be > 0%n"); System.exit(-3); } - break; default: break; } + } + + if (messageStoreConfig.isEnableDLegerCommitLog()) { + brokerConfig.setBrokerId(-1); + } + + if (brokerConfig.isEnableControllerMode() && messageStoreConfig.isEnableDLegerCommitLog()) { + System.out.printf("The config enableControllerMode and enableDLegerCommitLog cannot both be true.%n"); + System.exit(-4); + } + if (messageStoreConfig.getHaListenPort() <= 0) { messageStoreConfig.setHaListenPort(nettyServerConfig.getListenPort() + 1); - LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); - JoranConfigurator configurator = new JoranConfigurator(); - configurator.setContext(lc); - lc.reset(); - configurator.doConfigure(brokerConfig.getRocketmqHome() + "/conf/logback_broker.xml"); - - if (commandLine.hasOption('p')) { - Logger console = LoggerFactory.getLogger(LoggerName.BROKER_CONSOLE_NAME); - MixAll.printObjectProperties(console, brokerConfig); - MixAll.printObjectProperties(console, nettyServerConfig); - MixAll.printObjectProperties(console, nettyClientConfig); - MixAll.printObjectProperties(console, messageStoreConfig); - System.exit(0); - } else if (commandLine.hasOption('m')) { - Logger console = LoggerFactory.getLogger(LoggerName.BROKER_CONSOLE_NAME); - MixAll.printObjectProperties(console, brokerConfig, true); - MixAll.printObjectProperties(console, nettyServerConfig, true); - MixAll.printObjectProperties(console, nettyClientConfig, true); - MixAll.printObjectProperties(console, messageStoreConfig, true); - System.exit(0); - } + } - log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); - MixAll.printObjectProperties(log, brokerConfig); - MixAll.printObjectProperties(log, nettyServerConfig); - MixAll.printObjectProperties(log, nettyClientConfig); - MixAll.printObjectProperties(log, messageStoreConfig); + brokerConfig.setInBrokerContainer(false); - final BrokerController controller = new BrokerController( - brokerConfig, - nettyServerConfig, - nettyClientConfig, - messageStoreConfig); - // remember all configs to prevent discard - controller.getConfiguration().registerConfig(properties); + System.setProperty("brokerLogDir", ""); + if (brokerConfig.isIsolateLogEnable()) { + System.setProperty("brokerLogDir", brokerConfig.getBrokerName() + "_" + brokerConfig.getBrokerId()); + } + if (brokerConfig.isIsolateLogEnable() && messageStoreConfig.isEnableDLegerCommitLog()) { + System.setProperty("brokerLogDir", brokerConfig.getBrokerName() + "_" + messageStoreConfig.getdLegerSelfId()); + } + + if (commandLine.hasOption('p')) { + Logger console = LoggerFactory.getLogger(LoggerName.BROKER_CONSOLE_NAME); + MixAll.printObjectProperties(console, brokerConfig); + MixAll.printObjectProperties(console, nettyServerConfig); + MixAll.printObjectProperties(console, nettyClientConfig); + MixAll.printObjectProperties(console, messageStoreConfig); + System.exit(0); + } else if (commandLine.hasOption('m')) { + Logger console = LoggerFactory.getLogger(LoggerName.BROKER_CONSOLE_NAME); + MixAll.printObjectProperties(console, brokerConfig, true); + MixAll.printObjectProperties(console, nettyServerConfig, true); + MixAll.printObjectProperties(console, nettyClientConfig, true); + MixAll.printObjectProperties(console, messageStoreConfig, true); + System.exit(0); + } + + log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + MixAll.printObjectProperties(log, brokerConfig); + MixAll.printObjectProperties(log, nettyServerConfig); + MixAll.printObjectProperties(log, nettyClientConfig); + MixAll.printObjectProperties(log, messageStoreConfig); + + final BrokerController controller = new BrokerController( + brokerConfig, nettyServerConfig, nettyClientConfig, messageStoreConfig); + // Remember all configs to prevent discard + controller.getConfiguration().registerConfig(properties); + + return controller; + } + + public static Runnable buildShutdownHook(BrokerController brokerController) { + return new Runnable() { + private volatile boolean hasShutdown = false; + private final AtomicInteger shutdownTimes = new AtomicInteger(0); + + @Override + public void run() { + synchronized (this) { + log.info("Shutdown hook was invoked, {}", this.shutdownTimes.incrementAndGet()); + if (!this.hasShutdown) { + this.hasShutdown = true; + long beginTime = System.currentTimeMillis(); + brokerController.shutdown(); + long consumingTimeTotal = System.currentTimeMillis() - beginTime; + log.info("Shutdown hook over, consuming total time(ms): {}", consumingTimeTotal); + } + } + } + }; + } + + public static BrokerController createBrokerController(String[] args) { + try { + BrokerController controller = buildBrokerController(args); boolean initResult = controller.initialize(); if (!initResult) { controller.shutdown(); System.exit(-3); } - - Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { - private volatile boolean hasShutdown = false; - private AtomicInteger shutdownTimes = new AtomicInteger(0); - - @Override - public void run() { - synchronized (this) { - log.info("Shutdown hook was invoked, {}", this.shutdownTimes.incrementAndGet()); - if (!this.hasShutdown) { - this.hasShutdown = true; - long beginTime = System.currentTimeMillis(); - controller.shutdown(); - long consumingTimeTotal = System.currentTimeMillis() - beginTime; - log.info("Shutdown hook over, consuming total time(ms): {}", consumingTimeTotal); - } - } - } - }, "ShutdownHook")); - + Runtime.getRuntime().addShutdownHook(new Thread(buildShutdownHook(controller))); return controller; } catch (Throwable e) { e.printStackTrace(); System.exit(-1); } - return null; } @@ -266,4 +276,33 @@ private static Options buildCommandlineOptions(final Options options) { return options; } + + public static class SystemConfigFileHelper { + private static final Logger LOGGER = LoggerFactory.getLogger(SystemConfigFileHelper.class); + + private String file; + + public SystemConfigFileHelper() { + } + + public Properties loadConfig() throws Exception { + InputStream in = new BufferedInputStream(Files.newInputStream(Paths.get(file))); + Properties properties = new Properties(); + properties.load(in); + in.close(); + return properties; + } + + public void update(Properties properties) throws Exception { + LOGGER.error("[SystemConfigFileHelper] update no thing."); + } + + public void setFile(String file) { + this.file = file; + } + + public String getFile() { + return file; + } + } } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/ShutdownHook.java b/broker/src/main/java/org/apache/rocketmq/broker/ShutdownHook.java new file mode 100644 index 00000000000..63567f8c3d9 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/ShutdownHook.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker; + +public interface ShutdownHook { + /** + * Code to execute before broker shutdown. + * + * @param controller broker to shutdown + */ + void beforeShutdown(BrokerController controller); +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/client/ClientHousekeepingService.java b/broker/src/main/java/org/apache/rocketmq/broker/client/ClientHousekeepingService.java index c61531c201d..7878d0eec59 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/client/ClientHousekeepingService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/client/ClientHousekeepingService.java @@ -17,25 +17,26 @@ package org.apache.rocketmq.broker.client; import io.netty.channel.Channel; -import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.apache.rocketmq.broker.BrokerController; import org.apache.rocketmq.common.ThreadFactoryImpl; import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.remoting.ChannelEventListener; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class ClientHousekeepingService implements ChannelEventListener { private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); private final BrokerController brokerController; - private ScheduledExecutorService scheduledExecutorService = Executors - .newSingleThreadScheduledExecutor(new ThreadFactoryImpl("ClientHousekeepingScheduledThread")); + private ScheduledExecutorService scheduledExecutorService; public ClientHousekeepingService(final BrokerController brokerController) { this.brokerController = brokerController; + scheduledExecutorService = ThreadUtils.newScheduledThreadPool(1, + new ThreadFactoryImpl("ClientHousekeepingScheduledThread", brokerController.getBrokerIdentity())); } public void start() { @@ -55,7 +56,6 @@ public void run() { private void scanExceptionChannel() { this.brokerController.getProducerManager().scanNotActiveChannel(); this.brokerController.getConsumerManager().scanNotActiveChannel(); - this.brokerController.getFilterServerManager().scanNotActiveChannel(); } public void shutdown() { @@ -64,27 +64,32 @@ public void shutdown() { @Override public void onChannelConnect(String remoteAddr, Channel channel) { - + this.brokerController.getBrokerStatsManager().incChannelConnectNum(); } @Override public void onChannelClose(String remoteAddr, Channel channel) { this.brokerController.getProducerManager().doChannelCloseEvent(remoteAddr, channel); this.brokerController.getConsumerManager().doChannelCloseEvent(remoteAddr, channel); - this.brokerController.getFilterServerManager().doChannelCloseEvent(remoteAddr, channel); + this.brokerController.getBrokerStatsManager().incChannelCloseNum(); } @Override public void onChannelException(String remoteAddr, Channel channel) { this.brokerController.getProducerManager().doChannelCloseEvent(remoteAddr, channel); this.brokerController.getConsumerManager().doChannelCloseEvent(remoteAddr, channel); - this.brokerController.getFilterServerManager().doChannelCloseEvent(remoteAddr, channel); + this.brokerController.getBrokerStatsManager().incChannelExceptionNum(); } @Override public void onChannelIdle(String remoteAddr, Channel channel) { this.brokerController.getProducerManager().doChannelCloseEvent(remoteAddr, channel); this.brokerController.getConsumerManager().doChannelCloseEvent(remoteAddr, channel); - this.brokerController.getFilterServerManager().doChannelCloseEvent(remoteAddr, channel); + this.brokerController.getBrokerStatsManager().incChannelIdleNum(); + } + + @Override + public void onChannelActive(String remoteAddr, Channel channel) { + } } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/client/ConsumerGroupEvent.java b/broker/src/main/java/org/apache/rocketmq/broker/client/ConsumerGroupEvent.java index 717fb7085e5..6c0a58db061 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/client/ConsumerGroupEvent.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/client/ConsumerGroupEvent.java @@ -29,5 +29,13 @@ public enum ConsumerGroupEvent { /** * The group of consumer is registered. */ - REGISTER + REGISTER, + /** + * The client of this consumer is new registered. + */ + CLIENT_REGISTER, + /** + * The client of this consumer is unregistered. + */ + CLIENT_UNREGISTER } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/client/ConsumerGroupInfo.java b/broker/src/main/java/org/apache/rocketmq/broker/client/ConsumerGroupInfo.java index 91b6c8181ec..1ea58c1253d 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/client/ConsumerGroupInfo.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/client/ConsumerGroupInfo.java @@ -18,6 +18,7 @@ import io.netty.channel.Channel; import java.util.ArrayList; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map.Entry; @@ -26,19 +27,19 @@ import java.util.concurrent.ConcurrentMap; import org.apache.rocketmq.common.constant.LoggerName; import org.apache.rocketmq.common.consumer.ConsumeFromWhere; -import org.apache.rocketmq.common.protocol.heartbeat.ConsumeType; -import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; public class ConsumerGroupInfo { private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); private final String groupName; private final ConcurrentMap subscriptionTable = - new ConcurrentHashMap(); + new ConcurrentHashMap<>(); private final ConcurrentMap channelInfoTable = - new ConcurrentHashMap(16); + new ConcurrentHashMap<>(16); private volatile ConsumeType consumeType; private volatile MessageModel messageModel; private volatile ConsumeFromWhere consumeFromWhere; @@ -52,6 +53,10 @@ public ConsumerGroupInfo(String groupName, ConsumeType consumeType, MessageModel this.consumeFromWhere = consumeFromWhere; } + public ConsumerGroupInfo(String groupName) { + this.groupName = groupName; + } + public ClientChannelInfo findChannel(final String clientId) { Iterator> it = this.channelInfoTable.entrySet().iterator(); while (it.hasNext()) { @@ -68,6 +73,10 @@ public ConcurrentMap getSubscriptionTable() { return subscriptionTable; } + public ClientChannelInfo findChannel(final Channel channel) { + return this.channelInfoTable.get(channel); + } + public ConcurrentMap getChannelInfoTable() { return channelInfoTable; } @@ -94,25 +103,35 @@ public List getAllClientId() { return result; } - public void unregisterChannel(final ClientChannelInfo clientChannelInfo) { + public boolean unregisterChannel(final ClientChannelInfo clientChannelInfo) { ClientChannelInfo old = this.channelInfoTable.remove(clientChannelInfo.getChannel()); if (old != null) { log.info("unregister a consumer[{}] from consumerGroupInfo {}", this.groupName, old.toString()); + return true; } + return false; } - public boolean doChannelCloseEvent(final String remoteAddr, final Channel channel) { + public ClientChannelInfo doChannelCloseEvent(final String remoteAddr, final Channel channel) { final ClientChannelInfo info = this.channelInfoTable.remove(channel); if (info != null) { log.warn( "NETTY EVENT: remove not active channel[{}] from ConsumerGroupInfo groupChannelTable, consumer group: {}", info.toString(), groupName); - return true; } - return false; + return info; } + /** + * Update {@link #channelInfoTable} in {@link ConsumerGroupInfo} + * + * @param infoNew Channel info of new client. + * @param consumeType consume type of new client. + * @param messageModel message consuming model (CLUSTERING/BROADCASTING) of new client. + * @param consumeFromWhere indicate the position when the client consume message firstly. + * @return the result that if new connector is connected or not. + */ public boolean updateChannel(final ClientChannelInfo infoNew, ConsumeType consumeType, MessageModel messageModel, ConsumeFromWhere consumeFromWhere) { boolean updated = false; @@ -132,9 +151,9 @@ public boolean updateChannel(final ClientChannelInfo infoNew, ConsumeType consum infoOld = infoNew; } else { if (!infoOld.getClientId().equals(infoNew.getClientId())) { - log.error("[BUG] consumer channel exist in broker, but clientId not equal. GROUP: {} OLD: {} NEW: {} ", - this.groupName, - infoOld.toString(), + log.error( + "ConsumerGroupInfo: consumer channel exists in broker, but clientId is not the same one, " + + "group={}, old clientChannelInfo={}, new clientChannelInfo={}", groupName, infoOld.toString(), infoNew.toString()); this.channelInfoTable.put(infoNew.getChannel(), infoNew); } @@ -146,9 +165,15 @@ public boolean updateChannel(final ClientChannelInfo infoNew, ConsumeType consum return updated; } + /** + * Update subscription. + * + * @param subList set of {@link SubscriptionData} + * @return the boolean indicates the subscription has changed or not. + */ public boolean updateSubscription(final Set subList) { boolean updated = false; - + Set topicSet = new HashSet<>(); for (SubscriptionData sub : subList) { SubscriptionData old = this.subscriptionTable.get(sub.getTopic()); if (old == null) { @@ -170,22 +195,16 @@ public boolean updateSubscription(final Set subList) { this.subscriptionTable.put(sub.getTopic(), sub); } + // Add all new topics to the HashSet + topicSet.add(sub.getTopic()); } Iterator> it = this.subscriptionTable.entrySet().iterator(); while (it.hasNext()) { Entry next = it.next(); String oldTopic = next.getKey(); - - boolean exist = false; - for (SubscriptionData sub : subList) { - if (sub.getTopic().equals(oldTopic)) { - exist = true; - break; - } - } - - if (!exist) { + // Check HashSet with O(1) time complexity + if (!topicSet.contains(oldTopic)) { log.warn("subscription changed, group: {} remove topic {} {}", this.groupName, oldTopic, diff --git a/broker/src/main/java/org/apache/rocketmq/broker/client/ConsumerIdsChangeListener.java b/broker/src/main/java/org/apache/rocketmq/broker/client/ConsumerIdsChangeListener.java index 831e2932733..144092ca680 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/client/ConsumerIdsChangeListener.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/client/ConsumerIdsChangeListener.java @@ -19,4 +19,6 @@ public interface ConsumerIdsChangeListener { void handle(ConsumerGroupEvent event, String group, Object... args); + + void shutdown(); } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/client/ConsumerManager.java b/broker/src/main/java/org/apache/rocketmq/broker/client/ConsumerManager.java index 32632fc5149..42e71e7e997 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/client/ConsumerManager.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/client/ConsumerManager.java @@ -17,31 +17,51 @@ package org.apache.rocketmq.broker.client; import io.netty.channel.Channel; +import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; +import org.apache.rocketmq.common.BrokerConfig; import org.apache.rocketmq.common.constant.LoggerName; import org.apache.rocketmq.common.consumer.ConsumeFromWhere; -import org.apache.rocketmq.common.protocol.heartbeat.ConsumeType; -import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.remoting.common.RemotingHelper; -import org.apache.rocketmq.remoting.common.RemotingUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.store.stats.BrokerStatsManager; public class ConsumerManager { - private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); - private static final long CHANNEL_EXPIRED_TIMEOUT = 1000 * 120; - private final ConcurrentMap consumerTable = - new ConcurrentHashMap(1024); - private final ConsumerIdsChangeListener consumerIdsChangeListener; + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + private final ConcurrentMap consumerTable = + new ConcurrentHashMap<>(1024); + private final ConcurrentMap consumerCompensationTable = + new ConcurrentHashMap<>(1024); + private final List consumerIdsChangeListenerList = new CopyOnWriteArrayList<>(); + protected final BrokerStatsManager brokerStatsManager; + private final long channelExpiredTimeout; + private final long subscriptionExpiredTimeout; - public ConsumerManager(final ConsumerIdsChangeListener consumerIdsChangeListener) { - this.consumerIdsChangeListener = consumerIdsChangeListener; + public ConsumerManager(final ConsumerIdsChangeListener consumerIdsChangeListener, long expiredTimeout) { + this.consumerIdsChangeListenerList.add(consumerIdsChangeListener); + this.brokerStatsManager = null; + this.channelExpiredTimeout = expiredTimeout; + this.subscriptionExpiredTimeout = expiredTimeout; + } + + public ConsumerManager(final ConsumerIdsChangeListener consumerIdsChangeListener, + final BrokerStatsManager brokerStatsManager, BrokerConfig brokerConfig) { + this.consumerIdsChangeListenerList.add(consumerIdsChangeListener); + this.brokerStatsManager = brokerStatsManager; + this.channelExpiredTimeout = brokerConfig.getChannelExpiredTimeout(); + this.subscriptionExpiredTimeout = brokerConfig.getSubscriptionExpiredTimeout(); } public ClientChannelInfo findChannel(final String group, final String clientId) { @@ -52,17 +72,51 @@ public ClientChannelInfo findChannel(final String group, final String clientId) return null; } + public ClientChannelInfo findChannel(final String group, final Channel channel) { + ConsumerGroupInfo consumerGroupInfo = this.consumerTable.get(group); + if (consumerGroupInfo != null) { + return consumerGroupInfo.findChannel(channel); + } + return null; + } + public SubscriptionData findSubscriptionData(final String group, final String topic) { - ConsumerGroupInfo consumerGroupInfo = this.getConsumerGroupInfo(group); + return findSubscriptionData(group, topic, true); + } + + public SubscriptionData findSubscriptionData(final String group, final String topic, + boolean fromCompensationTable) { + ConsumerGroupInfo consumerGroupInfo = getConsumerGroupInfo(group, false); if (consumerGroupInfo != null) { - return consumerGroupInfo.findSubscriptionData(topic); + SubscriptionData subscriptionData = consumerGroupInfo.findSubscriptionData(topic); + if (subscriptionData != null) { + return subscriptionData; + } } + if (fromCompensationTable) { + ConsumerGroupInfo consumerGroupCompensationInfo = consumerCompensationTable.get(group); + if (consumerGroupCompensationInfo != null) { + return consumerGroupCompensationInfo.findSubscriptionData(topic); + } + } return null; } + public ConcurrentMap getConsumerTable() { + return this.consumerTable; + } + public ConsumerGroupInfo getConsumerGroupInfo(final String group) { - return this.consumerTable.get(group); + return getConsumerGroupInfo(group, false); + } + + public ConsumerGroupInfo getConsumerGroupInfo(String group, boolean fromCompensationTable) { + ConsumerGroupInfo consumerGroupInfo = consumerTable.get(group); + if (consumerGroupInfo == null && fromCompensationTable) { + consumerGroupInfo = consumerCompensationTable.get(group); + } + return consumerGroupInfo; } public int findSubscriptionDataCount(final String group) { @@ -74,33 +128,58 @@ public int findSubscriptionDataCount(final String group) { return 0; } - public void doChannelCloseEvent(final String remoteAddr, final Channel channel) { + public boolean doChannelCloseEvent(final String remoteAddr, final Channel channel) { + boolean removed = false; Iterator> it = this.consumerTable.entrySet().iterator(); while (it.hasNext()) { Entry next = it.next(); ConsumerGroupInfo info = next.getValue(); - boolean removed = info.doChannelCloseEvent(remoteAddr, channel); - if (removed) { + ClientChannelInfo clientChannelInfo = info.doChannelCloseEvent(remoteAddr, channel); + if (clientChannelInfo != null) { + callConsumerIdsChangeListener(ConsumerGroupEvent.CLIENT_UNREGISTER, next.getKey(), clientChannelInfo, info.getSubscribeTopics()); if (info.getChannelInfoTable().isEmpty()) { ConsumerGroupInfo remove = this.consumerTable.remove(next.getKey()); if (remove != null) { - log.info("unregister consumer ok, no any connection, and remove consumer group, {}", + LOGGER.info("unregister consumer ok, no any connection, and remove consumer group, {}", next.getKey()); - this.consumerIdsChangeListener.handle(ConsumerGroupEvent.UNREGISTER, next.getKey()); + callConsumerIdsChangeListener(ConsumerGroupEvent.UNREGISTER, next.getKey()); } } - this.consumerIdsChangeListener.handle(ConsumerGroupEvent.CHANGE, next.getKey(), info.getAllChannel()); + callConsumerIdsChangeListener(ConsumerGroupEvent.CHANGE, next.getKey(), info.getAllChannel()); } } + return removed; + } + + // compensate consumer info for consumer without heartbeat + public void compensateBasicConsumerInfo(String group, ConsumeType consumeType, MessageModel messageModel) { + ConsumerGroupInfo consumerGroupInfo = consumerCompensationTable.computeIfAbsent(group, ConsumerGroupInfo::new); + consumerGroupInfo.setConsumeType(consumeType); + consumerGroupInfo.setMessageModel(messageModel); + } + + // compensate subscription for pull consumer and consumer via proxy + public void compensateSubscribeData(String group, String topic, SubscriptionData subscriptionData) { + ConsumerGroupInfo consumerGroupInfo = consumerCompensationTable.computeIfAbsent(group, ConsumerGroupInfo::new); + consumerGroupInfo.getSubscriptionTable().put(topic, subscriptionData); } public boolean registerConsumer(final String group, final ClientChannelInfo clientChannelInfo, ConsumeType consumeType, MessageModel messageModel, ConsumeFromWhere consumeFromWhere, final Set subList, boolean isNotifyConsumerIdsChangedEnable) { + return registerConsumer(group, clientChannelInfo, consumeType, messageModel, consumeFromWhere, subList, + isNotifyConsumerIdsChangedEnable, true); + } + public boolean registerConsumer(final String group, final ClientChannelInfo clientChannelInfo, + ConsumeType consumeType, MessageModel messageModel, ConsumeFromWhere consumeFromWhere, + final Set subList, boolean isNotifyConsumerIdsChangedEnable, boolean updateSubscription) { + long start = System.currentTimeMillis(); ConsumerGroupInfo consumerGroupInfo = this.consumerTable.get(group); if (null == consumerGroupInfo) { + callConsumerIdsChangeListener(ConsumerGroupEvent.CLIENT_REGISTER, group, clientChannelInfo, + subList.stream().map(SubscriptionData::getTopic).collect(Collectors.toSet())); ConsumerGroupInfo tmp = new ConsumerGroupInfo(group, consumeType, messageModel, consumeFromWhere); ConsumerGroupInfo prev = this.consumerTable.putIfAbsent(group, tmp); consumerGroupInfo = prev != null ? prev : tmp; @@ -109,35 +188,86 @@ public boolean registerConsumer(final String group, final ClientChannelInfo clie boolean r1 = consumerGroupInfo.updateChannel(clientChannelInfo, consumeType, messageModel, consumeFromWhere); - boolean r2 = consumerGroupInfo.updateSubscription(subList); + boolean r2 = false; + if (updateSubscription) { + r2 = consumerGroupInfo.updateSubscription(subList); + } if (r1 || r2) { if (isNotifyConsumerIdsChangedEnable) { - this.consumerIdsChangeListener.handle(ConsumerGroupEvent.CHANGE, group, consumerGroupInfo.getAllChannel()); + callConsumerIdsChangeListener(ConsumerGroupEvent.CHANGE, group, consumerGroupInfo.getAllChannel()); } } + if (null != this.brokerStatsManager) { + this.brokerStatsManager.incConsumerRegisterTime((int) (System.currentTimeMillis() - start)); + } - this.consumerIdsChangeListener.handle(ConsumerGroupEvent.REGISTER, group, subList); + callConsumerIdsChangeListener(ConsumerGroupEvent.REGISTER, group, subList, clientChannelInfo); return r1 || r2; } + public boolean registerConsumerWithoutSub(final String group, final ClientChannelInfo clientChannelInfo, + ConsumeType consumeType, MessageModel messageModel, ConsumeFromWhere consumeFromWhere, boolean isNotifyConsumerIdsChangedEnable) { + long start = System.currentTimeMillis(); + ConsumerGroupInfo consumerGroupInfo = this.consumerTable.get(group); + if (null == consumerGroupInfo) { + ConsumerGroupInfo tmp = new ConsumerGroupInfo(group, consumeType, messageModel, consumeFromWhere); + ConsumerGroupInfo prev = this.consumerTable.putIfAbsent(group, tmp); + consumerGroupInfo = prev != null ? prev : tmp; + } + boolean updateChannelRst = consumerGroupInfo.updateChannel(clientChannelInfo, consumeType, messageModel, consumeFromWhere); + if (updateChannelRst && isNotifyConsumerIdsChangedEnable) { + callConsumerIdsChangeListener(ConsumerGroupEvent.CHANGE, group, consumerGroupInfo.getAllChannel()); + } + if (null != this.brokerStatsManager) { + this.brokerStatsManager.incConsumerRegisterTime((int) (System.currentTimeMillis() - start)); + } + return updateChannelRst; + } + public void unregisterConsumer(final String group, final ClientChannelInfo clientChannelInfo, boolean isNotifyConsumerIdsChangedEnable) { ConsumerGroupInfo consumerGroupInfo = this.consumerTable.get(group); if (null != consumerGroupInfo) { - consumerGroupInfo.unregisterChannel(clientChannelInfo); + boolean removed = consumerGroupInfo.unregisterChannel(clientChannelInfo); + if (removed) { + callConsumerIdsChangeListener(ConsumerGroupEvent.CLIENT_UNREGISTER, group, clientChannelInfo, consumerGroupInfo.getSubscribeTopics()); + } if (consumerGroupInfo.getChannelInfoTable().isEmpty()) { ConsumerGroupInfo remove = this.consumerTable.remove(group); if (remove != null) { - log.info("unregister consumer ok, no any connection, and remove consumer group, {}", group); + LOGGER.info("unregister consumer ok, no any connection, and remove consumer group, {}", group); - this.consumerIdsChangeListener.handle(ConsumerGroupEvent.UNREGISTER, group); + callConsumerIdsChangeListener(ConsumerGroupEvent.UNREGISTER, group); } } if (isNotifyConsumerIdsChangedEnable) { - this.consumerIdsChangeListener.handle(ConsumerGroupEvent.CHANGE, group, consumerGroupInfo.getAllChannel()); + callConsumerIdsChangeListener(ConsumerGroupEvent.CHANGE, group, consumerGroupInfo.getAllChannel()); + } + } + } + + public void removeExpireConsumerGroupInfo() { + List removeList = new ArrayList<>(); + consumerCompensationTable.forEach((group, consumerGroupInfo) -> { + List removeTopicList = new ArrayList<>(); + ConcurrentMap subscriptionTable = consumerGroupInfo.getSubscriptionTable(); + subscriptionTable.forEach((topic, subscriptionData) -> { + long diff = System.currentTimeMillis() - subscriptionData.getSubVersion(); + if (diff > subscriptionExpiredTimeout) { + removeTopicList.add(topic); + } + }); + for (String topic : removeTopicList) { + subscriptionTable.remove(topic); + if (subscriptionTable.isEmpty()) { + removeList.add(group); + } } + }); + for (String group : removeList) { + consumerCompensationTable.remove(group); } } @@ -155,22 +285,24 @@ public void scanNotActiveChannel() { Entry nextChannel = itChannel.next(); ClientChannelInfo clientChannelInfo = nextChannel.getValue(); long diff = System.currentTimeMillis() - clientChannelInfo.getLastUpdateTimestamp(); - if (diff > CHANNEL_EXPIRED_TIMEOUT) { - log.warn( + if (diff > channelExpiredTimeout) { + LOGGER.warn( "SCAN: remove expired channel from ConsumerManager consumerTable. channel={}, consumerGroup={}", RemotingHelper.parseChannelRemoteAddr(clientChannelInfo.getChannel()), group); - RemotingUtil.closeChannel(clientChannelInfo.getChannel()); + callConsumerIdsChangeListener(ConsumerGroupEvent.CLIENT_UNREGISTER, group, clientChannelInfo, consumerGroupInfo.getSubscribeTopics()); + RemotingHelper.closeChannel(clientChannelInfo.getChannel()); itChannel.remove(); } } if (channelInfoTable.isEmpty()) { - log.warn( + LOGGER.warn( "SCAN: remove expired channel from ConsumerManager consumerTable, all clear, consumerGroup={}", group); it.remove(); } } + removeExpireConsumerGroupInfo(); } public HashSet queryTopicConsumeByWho(final String topic) { @@ -186,4 +318,18 @@ public HashSet queryTopicConsumeByWho(final String topic) { } return groups; } + + public void appendConsumerIdsChangeListener(ConsumerIdsChangeListener listener) { + consumerIdsChangeListenerList.add(listener); + } + + protected void callConsumerIdsChangeListener(ConsumerGroupEvent event, String group, Object... args) { + for (ConsumerIdsChangeListener listener : consumerIdsChangeListenerList) { + try { + listener.handle(event, group, args); + } catch (Throwable t) { + LOGGER.error("err when call consumerIdsChangeListener", t); + } + } + } } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/client/DefaultConsumerIdsChangeListener.java b/broker/src/main/java/org/apache/rocketmq/broker/client/DefaultConsumerIdsChangeListener.java index d716a339fa0..d17a2a5470c 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/client/DefaultConsumerIdsChangeListener.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/client/DefaultConsumerIdsChangeListener.java @@ -17,18 +17,44 @@ package org.apache.rocketmq.broker.client; import io.netty.channel.Channel; - import java.util.Collection; import java.util.List; - +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import org.apache.rocketmq.broker.BrokerController; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.common.AbstractBrokerRunnable; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; public class DefaultConsumerIdsChangeListener implements ConsumerIdsChangeListener { + private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); private final BrokerController brokerController; + private final int cacheSize = 8096; + + private final ScheduledExecutorService scheduledExecutorService = ThreadUtils.newScheduledThreadPool(1, + ThreadUtils.newGenericThreadFactory("DefaultConsumerIdsChangeListener", true)); + + private ConcurrentHashMap> consumerChannelMap = new ConcurrentHashMap<>(cacheSize); public DefaultConsumerIdsChangeListener(BrokerController brokerController) { this.brokerController = brokerController; + + scheduledExecutorService.scheduleAtFixedRate(new AbstractBrokerRunnable(brokerController.getBrokerConfig()) { + @Override + public void run0() { + try { + notifyConsumerChange(); + } catch (Exception e) { + log.error( + "DefaultConsumerIdsChangeListen#notifyConsumerChange: unexpected error occurs", e); + } + } + }, 30, 15, TimeUnit.SECONDS); } @Override @@ -43,8 +69,12 @@ public void handle(ConsumerGroupEvent event, String group, Object... args) { } List channels = (List) args[0]; if (channels != null && brokerController.getBrokerConfig().isNotifyConsumerIdsChangedEnable()) { - for (Channel chl : channels) { - this.brokerController.getBroker2Client().notifyConsumerIdsChanged(chl, group); + if (this.brokerController.getBrokerConfig().isRealTimeNotifyConsumerChange()) { + for (Channel chl : channels) { + this.brokerController.getBroker2Client().notifyConsumerIdsChanged(chl, group); + } + } else { + consumerChannelMap.put(group, channels); } } break; @@ -58,8 +88,41 @@ public void handle(ConsumerGroupEvent event, String group, Object... args) { Collection subscriptionDataList = (Collection) args[0]; this.brokerController.getConsumerFilterManager().register(group, subscriptionDataList); break; + case CLIENT_REGISTER: + case CLIENT_UNREGISTER: + break; default: throw new RuntimeException("Unknown event " + event); } } + + private void notifyConsumerChange() { + + if (consumerChannelMap.isEmpty()) { + return; + } + + ConcurrentHashMap> processMap = new ConcurrentHashMap<>(consumerChannelMap); + consumerChannelMap = new ConcurrentHashMap<>(cacheSize); + + for (Map.Entry> entry : processMap.entrySet()) { + String consumerId = entry.getKey(); + List channelList = entry.getValue(); + try { + if (channelList != null && brokerController.getBrokerConfig().isNotifyConsumerIdsChangedEnable()) { + for (Channel chl : channelList) { + this.brokerController.getBroker2Client().notifyConsumerIdsChanged(chl, consumerId); + } + } + } catch (Exception e) { + log.error("Failed to notify consumer when some consumers changed, consumerId to notify: {}", + consumerId, e); + } + } + } + + @Override + public void shutdown() { + this.scheduledExecutorService.shutdown(); + } } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/client/ProducerChangeListener.java b/broker/src/main/java/org/apache/rocketmq/broker/client/ProducerChangeListener.java new file mode 100644 index 00000000000..f8183d33fa0 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/client/ProducerChangeListener.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.client; + +/** + * producer manager will call this listener when something happen + *

+ * event type: {@link ProducerGroupEvent} + */ +public interface ProducerChangeListener { + + void handle(ProducerGroupEvent event, String group, ClientChannelInfo clientChannelInfo); +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/client/ProducerGroupEvent.java b/broker/src/main/java/org/apache/rocketmq/broker/client/ProducerGroupEvent.java new file mode 100644 index 00000000000..cbf27ce61e6 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/client/ProducerGroupEvent.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.client; + +public enum ProducerGroupEvent { + /** + * The group of producer is unregistered. + */ + GROUP_UNREGISTER, + /** + * The client of this producer is unregistered. + */ + CLIENT_UNREGISTER +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/client/ProducerManager.java b/broker/src/main/java/org/apache/rocketmq/broker/client/ProducerManager.java index 010c1aef6f3..f9fe1193e22 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/client/ProducerManager.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/client/ProducerManager.java @@ -17,172 +17,250 @@ package org.apache.rocketmq.broker.client; import io.netty.channel.Channel; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import org.apache.rocketmq.broker.util.PositiveAtomicCounter; import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.remoting.common.RemotingHelper; -import org.apache.rocketmq.remoting.common.RemotingUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.body.ProducerInfo; +import org.apache.rocketmq.remoting.protocol.body.ProducerTableInfo; +import org.apache.rocketmq.store.stats.BrokerStatsManager; public class ProducerManager { private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); - private static final long LOCK_TIMEOUT_MILLIS = 3000; private static final long CHANNEL_EXPIRED_TIMEOUT = 1000 * 120; - private final Lock groupChannelLock = new ReentrantLock(); - private final HashMap> groupChannelTable = - new HashMap>(); + private static final int GET_AVAILABLE_CHANNEL_RETRY_COUNT = 3; + private final ConcurrentHashMap> groupChannelTable = + new ConcurrentHashMap<>(); + private final ConcurrentHashMap clientChannelTable = new ConcurrentHashMap<>(); + protected final BrokerStatsManager brokerStatsManager; + private PositiveAtomicCounter positiveAtomicCounter = new PositiveAtomicCounter(); + private final List producerChangeListenerList = new CopyOnWriteArrayList<>(); public ProducerManager() { + this.brokerStatsManager = null; } - public HashMap> getGroupChannelTable() { - HashMap> newGroupChannelTable = - new HashMap>(); - try { - if (this.groupChannelLock.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { - try { - newGroupChannelTable.putAll(groupChannelTable); - } finally { - groupChannelLock.unlock(); + public ProducerManager(final BrokerStatsManager brokerStatsManager) { + this.brokerStatsManager = brokerStatsManager; + } + + public int groupSize() { + return this.groupChannelTable.size(); + } + + public boolean groupOnline(String group) { + Map channels = this.groupChannelTable.get(group); + return channels != null && !channels.isEmpty(); + } + + public ConcurrentHashMap> getGroupChannelTable() { + return groupChannelTable; + } + + public ProducerTableInfo getProducerTable() { + Map> map = new HashMap<>(); + for (String group : this.groupChannelTable.keySet()) { + for (Entry entry: this.groupChannelTable.get(group).entrySet()) { + ClientChannelInfo clientChannelInfo = entry.getValue(); + if (map.containsKey(group)) { + map.get(group).add(new ProducerInfo( + clientChannelInfo.getClientId(), + clientChannelInfo.getChannel().remoteAddress().toString(), + clientChannelInfo.getLanguage(), + clientChannelInfo.getVersion(), + clientChannelInfo.getLastUpdateTimestamp() + )); + } else { + map.put(group, new ArrayList<>(Collections.singleton(new ProducerInfo( + clientChannelInfo.getClientId(), + clientChannelInfo.getChannel().remoteAddress().toString(), + clientChannelInfo.getLanguage(), + clientChannelInfo.getVersion(), + clientChannelInfo.getLastUpdateTimestamp() + )))); } } - } catch (InterruptedException e) { - log.error("", e); } - return newGroupChannelTable; + return new ProducerTableInfo(map); } public void scanNotActiveChannel() { - try { - if (this.groupChannelLock.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { - try { - for (final Map.Entry> entry : this.groupChannelTable - .entrySet()) { - final String group = entry.getKey(); - final HashMap chlMap = entry.getValue(); - - Iterator> it = chlMap.entrySet().iterator(); - while (it.hasNext()) { - Entry item = it.next(); - // final Integer id = item.getKey(); - final ClientChannelInfo info = item.getValue(); - - long diff = System.currentTimeMillis() - info.getLastUpdateTimestamp(); - if (diff > CHANNEL_EXPIRED_TIMEOUT) { - it.remove(); - log.warn( - "SCAN: remove expired channel[{}] from ProducerManager groupChannelTable, producer group name: {}", - RemotingHelper.parseChannelRemoteAddr(info.getChannel()), group); - RemotingUtil.closeChannel(info.getChannel()); - } - } + Iterator>> iterator = this.groupChannelTable.entrySet().iterator(); + + while (iterator.hasNext()) { + Map.Entry> entry = iterator.next(); + + final String group = entry.getKey(); + final ConcurrentHashMap chlMap = entry.getValue(); + + Iterator> it = chlMap.entrySet().iterator(); + while (it.hasNext()) { + Entry item = it.next(); + // final Integer id = item.getKey(); + final ClientChannelInfo info = item.getValue(); + + long diff = System.currentTimeMillis() - info.getLastUpdateTimestamp(); + if (diff > CHANNEL_EXPIRED_TIMEOUT) { + it.remove(); + Channel channelInClientTable = clientChannelTable.get(info.getClientId()); + if (channelInClientTable != null && channelInClientTable.equals(info.getChannel())) { + clientChannelTable.remove(info.getClientId()); } - } finally { - this.groupChannelLock.unlock(); + log.warn( + "ProducerManager#scanNotActiveChannel: remove expired channel[{}] from ProducerManager groupChannelTable, producer group name: {}", + RemotingHelper.parseChannelRemoteAddr(info.getChannel()), group); + callProducerChangeListener(ProducerGroupEvent.CLIENT_UNREGISTER, group, info); + RemotingHelper.closeChannel(info.getChannel()); } - } else { - log.warn("ProducerManager scanNotActiveChannel lock timeout"); } - } catch (InterruptedException e) { - log.error("", e); + + if (chlMap.isEmpty()) { + log.warn("SCAN: remove expired channel from ProducerManager groupChannelTable, all clear, group={}", group); + iterator.remove(); + callProducerChangeListener(ProducerGroupEvent.GROUP_UNREGISTER, group, null); + } } } - public void doChannelCloseEvent(final String remoteAddr, final Channel channel) { + public synchronized boolean doChannelCloseEvent(final String remoteAddr, final Channel channel) { + boolean removed = false; if (channel != null) { - try { - if (this.groupChannelLock.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { - try { - for (final Map.Entry> entry : this.groupChannelTable - .entrySet()) { - final String group = entry.getKey(); - final HashMap clientChannelInfoTable = - entry.getValue(); - final ClientChannelInfo clientChannelInfo = - clientChannelInfoTable.remove(channel); - if (clientChannelInfo != null) { - log.info( - "NETTY EVENT: remove channel[{}][{}] from ProducerManager groupChannelTable, producer group: {}", - clientChannelInfo.toString(), remoteAddr, group); - } - + for (final Map.Entry> entry : this.groupChannelTable + .entrySet()) { + final String group = entry.getKey(); + final ConcurrentHashMap clientChannelInfoTable = + entry.getValue(); + final ClientChannelInfo clientChannelInfo = + clientChannelInfoTable.remove(channel); + if (clientChannelInfo != null) { + clientChannelTable.remove(clientChannelInfo.getClientId()); + removed = true; + log.info( + "NETTY EVENT: remove channel[{}][{}] from ProducerManager groupChannelTable, producer group: {}", + clientChannelInfo.toString(), remoteAddr, group); + callProducerChangeListener(ProducerGroupEvent.CLIENT_UNREGISTER, group, clientChannelInfo); + if (clientChannelInfoTable.isEmpty()) { + ConcurrentHashMap oldGroupTable = this.groupChannelTable.remove(group); + if (oldGroupTable != null) { + log.info("unregister a producer group[{}] from groupChannelTable", group); + callProducerChangeListener(ProducerGroupEvent.GROUP_UNREGISTER, group, null); } - } finally { - this.groupChannelLock.unlock(); } - } else { - log.warn("ProducerManager doChannelCloseEvent lock timeout"); } - } catch (InterruptedException e) { - log.error("", e); + } } + return removed; } - public void registerProducer(final String group, final ClientChannelInfo clientChannelInfo) { - try { - ClientChannelInfo clientChannelInfoFound = null; + public synchronized void registerProducer(final String group, final ClientChannelInfo clientChannelInfo) { + ClientChannelInfo clientChannelInfoFound = null; - if (this.groupChannelLock.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { - try { - HashMap channelTable = this.groupChannelTable.get(group); - if (null == channelTable) { - channelTable = new HashMap<>(); - this.groupChannelTable.put(group, channelTable); - } + ConcurrentHashMap channelTable = this.groupChannelTable.get(group); + if (null == channelTable) { + channelTable = new ConcurrentHashMap<>(); + this.groupChannelTable.put(group, channelTable); + } - clientChannelInfoFound = channelTable.get(clientChannelInfo.getChannel()); - if (null == clientChannelInfoFound) { - channelTable.put(clientChannelInfo.getChannel(), clientChannelInfo); - log.info("new producer connected, group: {} channel: {}", group, - clientChannelInfo.toString()); - } - } finally { - this.groupChannelLock.unlock(); - } + clientChannelInfoFound = channelTable.get(clientChannelInfo.getChannel()); + if (null == clientChannelInfoFound) { + channelTable.put(clientChannelInfo.getChannel(), clientChannelInfo); + clientChannelTable.put(clientChannelInfo.getClientId(), clientChannelInfo.getChannel()); + log.info("new producer connected, group: {} channel: {}", group, + clientChannelInfo.toString()); + } - if (clientChannelInfoFound != null) { - clientChannelInfoFound.setLastUpdateTimestamp(System.currentTimeMillis()); - } - } else { - log.warn("ProducerManager registerProducer lock timeout"); + + if (clientChannelInfoFound != null) { + clientChannelInfoFound.setLastUpdateTimestamp(System.currentTimeMillis()); + } + } + + public synchronized void unregisterProducer(final String group, final ClientChannelInfo clientChannelInfo) { + ConcurrentHashMap channelTable = this.groupChannelTable.get(group); + if (null != channelTable && !channelTable.isEmpty()) { + ClientChannelInfo old = channelTable.remove(clientChannelInfo.getChannel()); + clientChannelTable.remove(clientChannelInfo.getClientId()); + if (old != null) { + log.info("unregister a producer[{}] from groupChannelTable {}", group, + clientChannelInfo.toString()); + callProducerChangeListener(ProducerGroupEvent.CLIENT_UNREGISTER, group, clientChannelInfo); + } + + if (channelTable.isEmpty()) { + this.groupChannelTable.remove(group); + callProducerChangeListener(ProducerGroupEvent.GROUP_UNREGISTER, group, null); + log.info("unregister a producer group[{}] from groupChannelTable", group); } - } catch (InterruptedException e) { - log.error("", e); } } - public void unregisterProducer(final String group, final ClientChannelInfo clientChannelInfo) { - try { - if (this.groupChannelLock.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { - try { - HashMap channelTable = this.groupChannelTable.get(group); - if (null != channelTable && !channelTable.isEmpty()) { - ClientChannelInfo old = channelTable.remove(clientChannelInfo.getChannel()); - if (old != null) { - log.info("unregister a producer[{}] from groupChannelTable {}", group, - clientChannelInfo.toString()); - } + public Channel getAvailableChannel(String groupId) { + if (groupId == null) { + return null; + } + List channelList; + ConcurrentHashMap channelClientChannelInfoHashMap = groupChannelTable.get(groupId); + if (channelClientChannelInfoHashMap != null) { + channelList = new ArrayList<>(channelClientChannelInfoHashMap.keySet()); + } else { + log.warn("Check transaction failed, channel table is empty. groupId={}", groupId); + return null; + } - if (channelTable.isEmpty()) { - this.groupChannelTable.remove(group); - log.info("unregister a producer group[{}] from groupChannelTable", group); - } - } - } finally { - this.groupChannelLock.unlock(); - } - } else { - log.warn("ProducerManager unregisterProducer lock timeout"); + int size = channelList.size(); + if (0 == size) { + log.warn("Channel list is empty. groupId={}", groupId); + return null; + } + + Channel lastActiveChannel = null; + + int index = positiveAtomicCounter.incrementAndGet() % size; + Channel channel = channelList.get(index); + int count = 0; + boolean isOk = channel.isActive() && channel.isWritable(); + while (count++ < GET_AVAILABLE_CHANNEL_RETRY_COUNT) { + if (isOk) { + return channel; } - } catch (InterruptedException e) { - log.error("", e); + if (channel.isActive()) { + lastActiveChannel = channel; + } + index = (++index) % size; + channel = channelList.get(index); + isOk = channel.isActive() && channel.isWritable(); } + + return lastActiveChannel; + } + + public Channel findChannel(String clientId) { + return clientChannelTable.get(clientId); + } + + private void callProducerChangeListener(ProducerGroupEvent event, String group, + ClientChannelInfo clientChannelInfo) { + for (ProducerChangeListener listener : producerChangeListenerList) { + try { + listener.handle(event, group, clientChannelInfo); + } catch (Throwable t) { + log.error("err when call producerChangeListener", t); + } + } + } + + public void appendProducerChangeListener(ProducerChangeListener producerChangeListener) { + producerChangeListenerList.add(producerChangeListener); } } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/client/net/Broker2Client.java b/broker/src/main/java/org/apache/rocketmq/broker/client/net/Broker2Client.java index 65b444e6e4b..8d95d843dba 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/client/net/Broker2Client.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/client/net/Broker2Client.java @@ -17,9 +17,6 @@ package org.apache.rocketmq.broker.client.net; import io.netty.channel.Channel; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelFutureListener; -import io.netty.channel.FileRegion; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -29,29 +26,29 @@ import org.apache.rocketmq.broker.BrokerController; import org.apache.rocketmq.broker.client.ClientChannelInfo; import org.apache.rocketmq.broker.client.ConsumerGroupInfo; -import org.apache.rocketmq.broker.pagecache.OneMessageTransfer; import org.apache.rocketmq.common.MQVersion; import org.apache.rocketmq.common.TopicConfig; import org.apache.rocketmq.common.UtilAll; import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageQueue; import org.apache.rocketmq.common.message.MessageQueueForC; -import org.apache.rocketmq.common.protocol.RequestCode; -import org.apache.rocketmq.common.protocol.ResponseCode; -import org.apache.rocketmq.common.protocol.body.GetConsumerStatusBody; -import org.apache.rocketmq.common.protocol.body.ResetOffsetBody; -import org.apache.rocketmq.common.protocol.body.ResetOffsetBodyForC; -import org.apache.rocketmq.common.protocol.header.CheckTransactionStateRequestHeader; -import org.apache.rocketmq.common.protocol.header.GetConsumerStatusRequestHeader; -import org.apache.rocketmq.common.protocol.header.NotifyConsumerIdsChangedRequestHeader; -import org.apache.rocketmq.common.protocol.header.ResetOffsetRequestHeader; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.remoting.common.RemotingHelper; import org.apache.rocketmq.remoting.exception.RemotingSendRequestException; import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; import org.apache.rocketmq.remoting.protocol.RemotingCommand; -import org.apache.rocketmq.store.SelectMappedBufferResult; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.GetConsumerStatusBody; +import org.apache.rocketmq.remoting.protocol.body.ResetOffsetBody; +import org.apache.rocketmq.remoting.protocol.body.ResetOffsetBodyForC; +import org.apache.rocketmq.remoting.protocol.header.CheckTransactionStateRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerStatusRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.NotifyConsumerIdsChangedRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ResetOffsetRequestHeader; public class Broker2Client { private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); @@ -62,29 +59,18 @@ public Broker2Client(BrokerController brokerController) { } public void checkProducerTransactionState( + final String group, final Channel channel, final CheckTransactionStateRequestHeader requestHeader, - final SelectMappedBufferResult selectMappedBufferResult) { + final MessageExt messageExt) throws Exception { RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CHECK_TRANSACTION_STATE, requestHeader); - request.markOnewayRPC(); - + request.setBody(MessageDecoder.encode(messageExt, false)); try { - FileRegion fileRegion = - new OneMessageTransfer(request.encodeHeader(selectMappedBufferResult.getSize()), - selectMappedBufferResult); - channel.writeAndFlush(fileRegion).addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) throws Exception { - selectMappedBufferResult.release(); - if (!future.isSuccess()) { - log.error("invokeProducer failed,", future.cause()); - } - } - }); - } catch (Throwable e) { - log.error("invokeProducer exception", e); - selectMappedBufferResult.release(); + this.brokerController.getRemotingServer().invokeOneway(channel, request, 10); + } catch (Exception e) { + log.error("Check transaction failed because invoke producer exception. group={}, msgId={}, error={}", + group, messageExt.getMsgId(), e.toString()); } } @@ -110,10 +96,11 @@ public void notifyConsumerIdsChanged( try { this.brokerController.getRemotingServer().invokeOneway(channel, request, 10); } catch (Exception e) { - log.error("notifyConsumerIdsChanged exception, " + consumerGroup, e.getMessage()); + log.error("notifyConsumerIdsChanged exception. group={}, error={}", consumerGroup, e.toString()); } } + public RemotingCommand resetOffset(String topic, String group, long timeStamp, boolean isForce) { return resetOffset(topic, group, timeStamp, isForce, false); } @@ -130,7 +117,7 @@ public RemotingCommand resetOffset(String topic, String group, long timeStamp, b return response; } - Map offsetTable = new HashMap(); + Map offsetTable = new HashMap<>(); for (int i = 0; i < topicConfig.getWriteQueueNums(); i++) { MessageQueue mq = new MessageQueue(); @@ -199,14 +186,14 @@ public RemotingCommand resetOffset(String topic, String group, long timeStamp, b log.info("[reset-offset] reset offset success. topic={}, group={}, clientId={}", topic, group, entry.getValue().getClientId()); } catch (Exception e) { - log.error("[reset-offset] reset offset exception. topic={}, group={}", - new Object[] {topic, group}, e); + log.error("[reset-offset] reset offset exception. topic={}, group={} ,error={}", + topic, group, e.toString()); } } else { response.setCode(ResponseCode.SYSTEM_ERROR); response.setRemark("the client does not support this feature. version=" + MQVersion.getVersionDesc(version)); - log.warn("[reset-offset] the client does not support this feature. version={}", + log.warn("[reset-offset] the client does not support this feature. channel={}, version={}", RemotingHelper.parseChannelRemoteAddr(entry.getKey()), MQVersion.getVersionDesc(version)); return response; } @@ -250,8 +237,7 @@ public RemotingCommand getConsumeStatus(String topic, String group, String origi RemotingCommand.createRequestCommand(RequestCode.GET_CONSUMER_STATUS_FROM_CLIENT, requestHeader); - Map> consumerStatusTable = - new HashMap>(); + Map> consumerStatusTable = new HashMap<>(); ConcurrentMap channelInfoTable = this.brokerController.getConsumerManager().getConsumerGroupInfo(group).getChannelInfoTable(); if (null == channelInfoTable || channelInfoTable.isEmpty()) { @@ -267,7 +253,7 @@ public RemotingCommand getConsumeStatus(String topic, String group, String origi result.setCode(ResponseCode.SYSTEM_ERROR); result.setRemark("the client does not support this feature. version=" + MQVersion.getVersionDesc(version)); - log.warn("[get-consumer-status] the client does not support this feature. version={}", + log.warn("[get-consumer-status] the client does not support this feature. channel={}, version={}", RemotingHelper.parseChannelRemoteAddr(entry.getKey()), MQVersion.getVersionDesc(version)); return result; } else if (UtilAll.isBlank(originClientId) || originClientId.equals(clientId)) { @@ -293,8 +279,8 @@ public RemotingCommand getConsumeStatus(String topic, String group, String origi } } catch (Exception e) { log.error( - "[get-consumer-status] get consumer status exception. topic={}, group={}, offset={}", - new Object[] {topic, group}, e); + "[get-consumer-status] get consumer status exception. topic={}, group={}, error={}", + topic, group, e.toString()); } if (!UtilAll.isBlank(originClientId) && originClientId.equals(clientId)) { diff --git a/broker/src/main/java/org/apache/rocketmq/broker/client/rebalance/RebalanceLockManager.java b/broker/src/main/java/org/apache/rocketmq/broker/client/rebalance/RebalanceLockManager.java index 519745e0919..bf7d0964bef 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/client/rebalance/RebalanceLockManager.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/client/rebalance/RebalanceLockManager.java @@ -16,16 +16,17 @@ */ package org.apache.rocketmq.broker.client.rebalance; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + import java.util.HashSet; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.common.message.MessageQueue; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class RebalanceLockManager { private static final Logger log = LoggerFactory.getLogger(LoggerName.REBALANCE_LOCK_LOGGER_NAME); @@ -33,7 +34,20 @@ public class RebalanceLockManager { "rocketmq.broker.rebalance.lockMaxLiveTime", "60000")); private final Lock lock = new ReentrantLock(); private final ConcurrentMap> mqLockTable = - new ConcurrentHashMap>(1024); + new ConcurrentHashMap<>(1024); + + public boolean isLockAllExpired(final String group) { + final ConcurrentHashMap lockEntryMap = mqLockTable.get(group); + if (null == lockEntryMap) { + return true; + } + for (LockEntry entry : lockEntryMap.values()) { + if (!entry.isExpired()) { + return false; + } + } + return true; + } public boolean tryLock(final String group, final MessageQueue mq, final String clientId) { @@ -52,10 +66,9 @@ public boolean tryLock(final String group, final MessageQueue mq, final String c lockEntry = new LockEntry(); lockEntry.setClientId(clientId); groupValue.put(mq, lockEntry); - log.info("tryLock, message queue not locked, I got it. Group: {} NewClientId: {} {}", - group, - clientId, - mq); + log.info( + "RebalanceLockManager#tryLock: lock a message queue which has not been locked yet, " + + "group={}, clientId={}, mq={}", group, clientId, mq); } if (lockEntry.isLocked(clientId)) { @@ -69,26 +82,21 @@ public boolean tryLock(final String group, final MessageQueue mq, final String c lockEntry.setClientId(clientId); lockEntry.setLastUpdateTimestamp(System.currentTimeMillis()); log.warn( - "tryLock, message queue lock expired, I got it. Group: {} OldClientId: {} NewClientId: {} {}", - group, - oldClientId, - clientId, - mq); + "RebalanceLockManager#tryLock: try to lock a expired message queue, group={}, mq={}, old " + + "client id={}, new client id={}", group, mq, oldClientId, clientId); return true; } log.warn( - "tryLock, message queue locked by other client. Group: {} OtherClientId: {} NewClientId: {} {}", - group, - oldClientId, - clientId, - mq); + "RebalanceLockManager#tryLock: message queue has been locked by other client, group={}, " + + "mq={}, locked client id={}, current client id={}", group, mq, oldClientId, clientId); return false; } finally { this.lock.unlock(); } } catch (InterruptedException e) { - log.error("putMessage exception", e); + log.error("RebalanceLockManager#tryLock: unexpected error, group={}, mq={}, clientId={}", group, mq, + clientId, e); } } else { @@ -116,8 +124,8 @@ private boolean isLocked(final String group, final MessageQueue mq, final String public Set tryLockBatch(final String group, final Set mqs, final String clientId) { - Set lockedMqs = new HashSet(mqs.size()); - Set notLockedMqs = new HashSet(mqs.size()); + Set lockedMqs = new HashSet<>(mqs.size()); + Set notLockedMqs = new HashSet<>(mqs.size()); for (MessageQueue mq : mqs) { if (this.isLocked(group, mq, clientId)) { @@ -144,10 +152,8 @@ public Set tryLockBatch(final String group, final Set tryLockBatch(final String group, final Set mqs, final S if (null != lockEntry) { if (lockEntry.getClientId().equals(clientId)) { groupValue.remove(mq); - log.info("unlockBatch, Group: {} {} {}", - group, - mq, - clientId); + log.info("RebalanceLockManager#unlockBatch: unlock mq, group={}, clientId={}, mqs={}", + group, clientId, mq); } else { - log.warn("unlockBatch, but mq locked by other client: {}, Group: {} {} {}", - lockEntry.getClientId(), - group, - mq, - clientId); + log.warn( + "RebalanceLockManager#unlockBatch: mq locked by other client, group={}, locked " + + "clientId={}, current clientId={}, mqs={}", group, lockEntry.getClientId(), + clientId, mq); } } else { - log.warn("unlockBatch, but mq not locked, Group: {} {} {}", - group, - mq, - clientId); + log.warn("RebalanceLockManager#unlockBatch: mq not locked, group={}, clientId={}, mq={}", + group, clientId, mq); } } } else { - log.warn("unlockBatch, group not exist, Group: {} {}", - group, - clientId); + log.warn("RebalanceLockManager#unlockBatch: group not exist, group={}, clientId={}, mqs={}", group, + clientId, mqs); } } finally { this.lock.unlock(); } } catch (InterruptedException e) { - log.error("putMessage exception", e); + log.error("RebalanceLockManager#unlockBatch: unexpected error, group={}, mqs={}, clientId={}", group, mqs, + clientId); } } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/coldctr/ColdCtrStrategy.java b/broker/src/main/java/org/apache/rocketmq/broker/coldctr/ColdCtrStrategy.java new file mode 100644 index 00000000000..11fa0e707f4 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/coldctr/ColdCtrStrategy.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.coldctr; + +public interface ColdCtrStrategy { + /** + * Calculate the determining factor about whether to accelerate or decelerate + * @return + */ + Double decisionFactor(); + /** + * Promote the speed for consumerGroup to read cold data + * @param consumerGroup + * @param currentThreshold + */ + void promote(String consumerGroup, Long currentThreshold); + /** + * Decelerate the speed for consumerGroup to read cold data + * @param consumerGroup + * @param currentThreshold + */ + void decelerate(String consumerGroup, Long currentThreshold); + /** + * Collect the total number of cold read data in the system + * @param globalAcc + */ + void collect(Long globalAcc); +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/coldctr/ColdDataCgCtrService.java b/broker/src/main/java/org/apache/rocketmq/broker/coldctr/ColdDataCgCtrService.java new file mode 100644 index 00000000000..dd9278fb755 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/coldctr/ColdDataCgCtrService.java @@ -0,0 +1,250 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.coldctr; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +import com.alibaba.fastjson.JSONObject; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.SystemClock; +import org.apache.rocketmq.common.coldctr.AccAndTimeStamp; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.config.MessageStoreConfig; + +/** + * store the cg cold read ctr table and acc the size of the cold + * reading msg, timing to clear the table and set acc to zero + */ +public class ColdDataCgCtrService extends ServiceThread { + private static final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_COLDCTR_LOGGER_NAME); + private final SystemClock systemClock = new SystemClock(); + private final long cgColdAccResideTimeoutMills = 60 * 1000; + private static final AtomicLong GLOBAL_ACC = new AtomicLong(0L); + private static final String ADAPTIVE = "||adaptive"; + /** + * as soon as the consumerGroup read the cold data then it will be put into @code cgColdThresholdMapRuntime, + * and it also will be removed when does not read cold data in @code cgColdAccResideTimeoutMills later; + */ + private final ConcurrentHashMap cgColdThresholdMapRuntime = new ConcurrentHashMap<>(); + /** + * if the system admin wants to set the special cold read threshold for some consumerGroup, the configuration will + * be putted into @code cgColdThresholdMapConfig + */ + private final ConcurrentHashMap cgColdThresholdMapConfig = new ConcurrentHashMap<>(); + private final BrokerConfig brokerConfig; + private final MessageStoreConfig messageStoreConfig; + private final ColdCtrStrategy coldCtrStrategy; + + public ColdDataCgCtrService(BrokerController brokerController) { + this.brokerConfig = brokerController.getBrokerConfig(); + this.messageStoreConfig = brokerController.getMessageStoreConfig(); + this.coldCtrStrategy = brokerConfig.isUsePIDColdCtrStrategy() ? new PIDAdaptiveColdCtrStrategy(this, (long)(brokerConfig.getGlobalColdReadThreshold() * 0.8)) : new SimpleColdCtrStrategy(this); + } + + @Override + public String getServiceName() { + return ColdDataCgCtrService.class.getSimpleName(); + } + + @Override + public void run() { + log.info("{} service started", this.getServiceName()); + while (!this.isStopped()) { + try { + if (messageStoreConfig.isColdDataFlowControlEnable()) { + this.waitForRunning(5 * 1000); + } else { + this.waitForRunning(180 * 1000); + } + long beginLockTimestamp = this.systemClock.now(); + clearDataAcc(); + if (!brokerConfig.isColdCtrStrategyEnable()) { + clearAdaptiveConfig(); + } + long costTime = this.systemClock.now() - beginLockTimestamp; + log.info("[{}] clearTheDataAcc-cost {} ms.", costTime > 3 * 1000 ? "NOTIFYME" : "OK", costTime); + } catch (Throwable e) { + log.warn(this.getServiceName() + " service has exception", e); + } + } + log.info("{} service end", this.getServiceName()); + } + + public String getColdDataFlowCtrInfo() { + JSONObject result = new JSONObject(); + result.put("runtimeTable", this.cgColdThresholdMapRuntime); + result.put("configTable", this.cgColdThresholdMapConfig); + result.put("cgColdReadThreshold", this.brokerConfig.getCgColdReadThreshold()); + result.put("globalColdReadThreshold", this.brokerConfig.getGlobalColdReadThreshold()); + result.put("globalAcc", GLOBAL_ACC.get()); + return result.toJSONString(); + } + + /** + * clear the long time no cold read cg in the table; + * update the acc to zero for the cg in the table; + * use the strategy to promote or decelerate the cg; + */ + private void clearDataAcc() { + log.info("clearDataAcc cgColdThresholdMapRuntime key size: {}", cgColdThresholdMapRuntime.size()); + if (brokerConfig.isColdCtrStrategyEnable()) { + coldCtrStrategy.collect(GLOBAL_ACC.get()); + } + Iterator> iterator = cgColdThresholdMapRuntime.entrySet().iterator(); + while (iterator.hasNext()) { + Entry next = iterator.next(); + if (System.currentTimeMillis() >= cgColdAccResideTimeoutMills + next.getValue().getLastColdReadTimeMills()) { + if (brokerConfig.isColdCtrStrategyEnable()) { + cgColdThresholdMapConfig.remove(buildAdaptiveKey(next.getKey())); + } + iterator.remove(); + } else if (next.getValue().getColdAcc().get() >= getThresholdByConsumerGroup(next.getKey())) { + log.info("Coldctr consumerGroup: {}, acc: {}, threshold: {}", next.getKey(), next.getValue().getColdAcc().get(), getThresholdByConsumerGroup(next.getKey())); + if (brokerConfig.isColdCtrStrategyEnable() && !isGlobalColdCtr() && !isAdminConfig(next.getKey())) { + coldCtrStrategy.promote(buildAdaptiveKey(next.getKey()), getThresholdByConsumerGroup(next.getKey())); + } + } + next.getValue().getColdAcc().set(0L); + } + if (isGlobalColdCtr()) { + log.info("Coldctr global acc: {}, threshold: {}", GLOBAL_ACC.get(), this.brokerConfig.getGlobalColdReadThreshold()); + } + if (brokerConfig.isColdCtrStrategyEnable()) { + sortAndDecelerate(); + } + GLOBAL_ACC.set(0L); + } + + private void sortAndDecelerate() { + List> configMapList = new ArrayList>(cgColdThresholdMapConfig.entrySet()); + configMapList.sort(new Comparator>() { + @Override + public int compare(Entry o1, Entry o2) { + return (int)(o2.getValue() - o1.getValue()); + } + }); + Iterator> iterator = configMapList.iterator(); + int maxDecelerate = 3; + while (iterator.hasNext() && maxDecelerate > 0) { + Entry next = iterator.next(); + if (!isAdminConfig(next.getKey())) { + coldCtrStrategy.decelerate(next.getKey(), getThresholdByConsumerGroup(next.getKey())); + maxDecelerate --; + } + } + } + + public void coldAcc(String consumerGroup, long coldDataToAcc) { + if (coldDataToAcc <= 0) { + return; + } + GLOBAL_ACC.addAndGet(coldDataToAcc); + AccAndTimeStamp atomicAcc = cgColdThresholdMapRuntime.get(consumerGroup); + if (null == atomicAcc) { + atomicAcc = new AccAndTimeStamp(new AtomicLong(coldDataToAcc)); + atomicAcc = cgColdThresholdMapRuntime.putIfAbsent(consumerGroup, atomicAcc); + } + if (null != atomicAcc) { + atomicAcc.getColdAcc().addAndGet(coldDataToAcc); + atomicAcc.setLastColdReadTimeMills(System.currentTimeMillis()); + } + } + + public void addOrUpdateGroupConfig(String consumerGroup, Long threshold) { + cgColdThresholdMapConfig.put(consumerGroup, threshold); + } + + public void removeGroupConfig(String consumerGroup) { + cgColdThresholdMapConfig.remove(consumerGroup); + } + + public boolean isCgNeedColdDataFlowCtr(String consumerGroup) { + if (!this.messageStoreConfig.isColdDataFlowControlEnable()) { + return false; + } + if (MixAll.isSysConsumerGroupForNoColdReadLimit(consumerGroup)) { + return false; + } + AccAndTimeStamp accAndTimeStamp = cgColdThresholdMapRuntime.get(consumerGroup); + if (null == accAndTimeStamp) { + return false; + } + + Long threshold = getThresholdByConsumerGroup(consumerGroup); + if (accAndTimeStamp.getColdAcc().get() >= threshold) { + return true; + } + return GLOBAL_ACC.get() >= this.brokerConfig.getGlobalColdReadThreshold(); + } + + public boolean isGlobalColdCtr() { + return GLOBAL_ACC.get() > this.brokerConfig.getGlobalColdReadThreshold(); + } + + public BrokerConfig getBrokerConfig() { + return brokerConfig; + } + + private Long getThresholdByConsumerGroup(String consumerGroup) { + if (isAdminConfig(consumerGroup)) { + if (consumerGroup.endsWith(ADAPTIVE)) { + return cgColdThresholdMapConfig.get(consumerGroup.split(ADAPTIVE)[0]); + } + return cgColdThresholdMapConfig.get(consumerGroup); + } + Long threshold = null; + if (brokerConfig.isColdCtrStrategyEnable()) { + if (consumerGroup.endsWith(ADAPTIVE)) { + threshold = cgColdThresholdMapConfig.get(consumerGroup); + } else { + threshold = cgColdThresholdMapConfig.get(buildAdaptiveKey(consumerGroup)); + } + } + if (null == threshold) { + threshold = this.brokerConfig.getCgColdReadThreshold(); + } + return threshold; + } + + private String buildAdaptiveKey(String consumerGroup) { + return consumerGroup + ADAPTIVE; + } + + private boolean isAdminConfig(String consumerGroup) { + if (consumerGroup.endsWith(ADAPTIVE)) { + consumerGroup = consumerGroup.split(ADAPTIVE)[0]; + } + return cgColdThresholdMapConfig.containsKey(consumerGroup); + } + + private void clearAdaptiveConfig() { + cgColdThresholdMapConfig.entrySet().removeIf(next -> next.getKey().endsWith(ADAPTIVE)); + } + +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/coldctr/ColdDataPullRequestHoldService.java b/broker/src/main/java/org/apache/rocketmq/broker/coldctr/ColdDataPullRequestHoldService.java new file mode 100644 index 00000000000..c38d886fd33 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/coldctr/ColdDataPullRequestHoldService.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.coldctr; + +import java.util.Iterator; +import java.util.concurrent.LinkedBlockingQueue; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.longpolling.PullRequest; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.SystemClock; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +/** + * just requests are type of pull have the qualification to be put into this hold queue. + * if the pull request is reading cold data and that request will be cold at the first time, + * then the pull request will be cold in this @code pullRequestLinkedBlockingQueue, + * in @code coldTimeoutMillis later the pull request will be warm and marked holded + */ +public class ColdDataPullRequestHoldService extends ServiceThread { + + private static final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_COLDCTR_LOGGER_NAME); + public static final String NO_SUSPEND_KEY = "_noSuspend_"; + + private final long coldHoldTimeoutMillis = 3000; + private final SystemClock systemClock = new SystemClock(); + private final BrokerController brokerController; + private final LinkedBlockingQueue pullRequestColdHoldQueue = new LinkedBlockingQueue<>(10000); + + public void suspendColdDataReadRequest(PullRequest pullRequest) { + if (this.brokerController.getMessageStoreConfig().isColdDataFlowControlEnable()) { + pullRequestColdHoldQueue.offer(pullRequest); + } + } + + public ColdDataPullRequestHoldService(BrokerController brokerController) { + this.brokerController = brokerController; + } + + @Override + public String getServiceName() { + return ColdDataPullRequestHoldService.class.getSimpleName(); + } + + @Override + public void run() { + log.info("{} service started", this.getServiceName()); + while (!this.isStopped()) { + try { + if (!this.brokerController.getMessageStoreConfig().isColdDataFlowControlEnable()) { + this.waitForRunning(20 * 1000); + } else { + this.waitForRunning(5 * 1000); + } + long beginClockTimestamp = this.systemClock.now(); + this.checkColdDataPullRequest(); + long costTime = this.systemClock.now() - beginClockTimestamp; + log.info("[{}] checkColdDataPullRequest-cost {} ms.", costTime > 5 * 1000 ? "NOTIFYME" : "OK", costTime); + } catch (Throwable e) { + log.warn(this.getServiceName() + " service has exception", e); + } + } + log.info("{} service end", this.getServiceName()); + } + + private void checkColdDataPullRequest() { + int succTotal = 0, errorTotal = 0, queueSize = pullRequestColdHoldQueue.size() ; + Iterator iterator = pullRequestColdHoldQueue.iterator(); + while (iterator.hasNext()) { + PullRequest pullRequest = iterator.next(); + if (System.currentTimeMillis() >= pullRequest.getSuspendTimestamp() + coldHoldTimeoutMillis) { + try { + pullRequest.getRequestCommand().addExtField(NO_SUSPEND_KEY, "1"); + this.brokerController.getPullMessageProcessor().executeRequestWhenWakeup( + pullRequest.getClientChannel(), pullRequest.getRequestCommand()); + succTotal++; + } catch (Exception e) { + log.error("PullRequestColdHoldService checkColdDataPullRequest error", e); + errorTotal++; + } + //remove the timeout request from the iterator + iterator.remove(); + } + } + log.info("checkColdPullRequest-info-finish, queueSize: {} successTotal: {} errorTotal: {}", + queueSize, succTotal, errorTotal); + } + +} \ No newline at end of file diff --git a/broker/src/main/java/org/apache/rocketmq/broker/coldctr/PIDAdaptiveColdCtrStrategy.java b/broker/src/main/java/org/apache/rocketmq/broker/coldctr/PIDAdaptiveColdCtrStrategy.java new file mode 100644 index 00000000000..87d9789f71f --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/coldctr/PIDAdaptiveColdCtrStrategy.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.coldctr; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class PIDAdaptiveColdCtrStrategy implements ColdCtrStrategy { + /** + * Stores the maximum number of recent et val + */ + private static final int MAX_STORE_NUMS = 10; + /** + * The weights of the three modules of the PID formula + */ + private static final Double KP = 0.5, KI = 0.3, KD = 0.2; + private final List historyEtValList = new ArrayList<>(); + private final ColdDataCgCtrService coldDataCgCtrService; + private final Long expectGlobalVal; + private long et = 0L; + + public PIDAdaptiveColdCtrStrategy(ColdDataCgCtrService coldDataCgCtrService, Long expectGlobalVal) { + this.coldDataCgCtrService = coldDataCgCtrService; + this.expectGlobalVal = expectGlobalVal; + } + + @Override + public Double decisionFactor() { + if (historyEtValList.size() < MAX_STORE_NUMS) { + return 0.0; + } + Long et1 = historyEtValList.get(historyEtValList.size() - 1); + Long et2 = historyEtValList.get(historyEtValList.size() - 2); + Long differential = et1 - et2; + Double integration = 0.0; + for (Long item: historyEtValList) { + integration += item; + } + return KP * et + KI * integration + KD * differential; + } + + @Override + public void promote(String consumerGroup, Long currentThreshold) { + if (decisionFactor() > 0) { + coldDataCgCtrService.addOrUpdateGroupConfig(consumerGroup, (long)(currentThreshold * 1.5)); + } + } + + @Override + public void decelerate(String consumerGroup, Long currentThreshold) { + if (decisionFactor() < 0) { + long changedThresholdVal = (long)(currentThreshold * 0.8); + if (changedThresholdVal < coldDataCgCtrService.getBrokerConfig().getCgColdReadThreshold()) { + changedThresholdVal = coldDataCgCtrService.getBrokerConfig().getCgColdReadThreshold(); + } + coldDataCgCtrService.addOrUpdateGroupConfig(consumerGroup, changedThresholdVal); + } + } + + @Override + public void collect(Long globalAcc) { + et = expectGlobalVal - globalAcc; + historyEtValList.add(et); + Iterator iterator = historyEtValList.iterator(); + while (historyEtValList.size() > MAX_STORE_NUMS && iterator.hasNext()) { + iterator.next(); + iterator.remove(); + } + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/coldctr/SimpleColdCtrStrategy.java b/broker/src/main/java/org/apache/rocketmq/broker/coldctr/SimpleColdCtrStrategy.java new file mode 100644 index 00000000000..f26a242f9aa --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/coldctr/SimpleColdCtrStrategy.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.coldctr; + +public class SimpleColdCtrStrategy implements ColdCtrStrategy { + private final ColdDataCgCtrService coldDataCgCtrService; + + public SimpleColdCtrStrategy(ColdDataCgCtrService coldDataCgCtrService) { + this.coldDataCgCtrService = coldDataCgCtrService; + } + + @Override + public Double decisionFactor() { + return null; + } + + @Override + public void promote(String consumerGroup, Long currentThreshold) { + coldDataCgCtrService.addOrUpdateGroupConfig(consumerGroup, (long)(currentThreshold * 1.5)); + } + + @Override + public void decelerate(String consumerGroup, Long currentThreshold) { + if (!coldDataCgCtrService.isGlobalColdCtr()) { + return; + } + long changedThresholdVal = (long)(currentThreshold * 0.8); + if (changedThresholdVal < coldDataCgCtrService.getBrokerConfig().getCgColdReadThreshold()) { + changedThresholdVal = coldDataCgCtrService.getBrokerConfig().getCgColdReadThreshold(); + } + coldDataCgCtrService.addOrUpdateGroupConfig(consumerGroup, changedThresholdVal); + } + + @Override + public void collect(Long globalAcc) { + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/controller/ReplicasManager.java b/broker/src/main/java/org/apache/rocketmq/broker/controller/ReplicasManager.java new file mode 100644 index 00000000000..a1d711cb275 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/controller/ReplicasManager.java @@ -0,0 +1,879 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.controller; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.out.BrokerOuterAPI; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.EpochEntry; +import org.apache.rocketmq.remoting.protocol.body.SyncStateSet; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetMetaDataResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetReplicaInfoResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.ApplyBrokerIdResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.GetNextBrokerIdResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.RegisterBrokerToControllerResponseHeader; +import org.apache.rocketmq.store.config.BrokerRole; +import org.apache.rocketmq.store.ha.autoswitch.AutoSwitchHAService; +import org.apache.rocketmq.store.ha.autoswitch.BrokerMetadata; +import org.apache.rocketmq.store.ha.autoswitch.TempBrokerMetadata; + +import static org.apache.rocketmq.remoting.protocol.ResponseCode.CONTROLLER_BROKER_METADATA_NOT_EXIST; + +/** + * The manager of broker replicas, including: 0.regularly syncing controller metadata, change controller leader address, + * both master and slave will start this timed task. 1.regularly syncing metadata from controllers, and changing broker + * roles and master if needed, both master and slave will start this timed task. 2.regularly expanding and Shrinking + * syncStateSet, only master will start this timed task. + */ +public class ReplicasManager { + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + + private static final int RETRY_INTERVAL_SECOND = 5; + + private final ScheduledExecutorService scheduledService; + private final ExecutorService executorService; + private final ExecutorService scanExecutor; + private final BrokerController brokerController; + private final AutoSwitchHAService haService; + private final BrokerConfig brokerConfig; + private final String brokerAddress; + private final BrokerOuterAPI brokerOuterAPI; + private List controllerAddresses; + private final ConcurrentMap availableControllerAddresses; + + private volatile String controllerLeaderAddress = ""; + private volatile State state = State.INITIAL; + + private volatile RegisterState registerState = RegisterState.INITIAL; + + private ScheduledFuture checkSyncStateSetTaskFuture; + private ScheduledFuture slaveSyncFuture; + + private Long brokerControllerId; + + private Long masterBrokerId; + + private BrokerMetadata brokerMetadata; + + private TempBrokerMetadata tempBrokerMetadata; + + private Set syncStateSet; + private int syncStateSetEpoch = 0; + private String masterAddress = ""; + private int masterEpoch = 0; + private long lastSyncTimeMs = System.currentTimeMillis(); + private Random random = new Random(); + + public ReplicasManager(final BrokerController brokerController) { + this.brokerController = brokerController; + this.brokerOuterAPI = brokerController.getBrokerOuterAPI(); + this.scheduledService = ThreadUtils.newScheduledThreadPool(3, new ThreadFactoryImpl("ReplicasManager_ScheduledService_", brokerController.getBrokerIdentity())); + this.executorService = ThreadUtils.newThreadPoolExecutor(3, new ThreadFactoryImpl("ReplicasManager_ExecutorService_", brokerController.getBrokerIdentity())); + this.scanExecutor = ThreadUtils.newThreadPoolExecutor(4, 10, 60, TimeUnit.SECONDS, + new ArrayBlockingQueue<>(32), new ThreadFactoryImpl("ReplicasManager_scan_thread_", brokerController.getBrokerIdentity())); + this.haService = (AutoSwitchHAService) brokerController.getMessageStore().getHaService(); + this.brokerConfig = brokerController.getBrokerConfig(); + this.availableControllerAddresses = new ConcurrentHashMap<>(); + this.syncStateSet = new HashSet<>(); + this.brokerAddress = brokerController.getBrokerAddr(); + this.brokerMetadata = new BrokerMetadata(this.brokerController.getMessageStoreConfig().getStorePathBrokerIdentity()); + this.tempBrokerMetadata = new TempBrokerMetadata(this.brokerController.getMessageStoreConfig().getStorePathBrokerIdentity() + "-temp"); + } + + enum State { + INITIAL, + FIRST_TIME_SYNC_CONTROLLER_METADATA_DONE, + REGISTER_TO_CONTROLLER_DONE, + RUNNING, + SHUTDOWN, + } + + enum RegisterState { + INITIAL, + CREATE_TEMP_METADATA_FILE_DONE, + CREATE_METADATA_FILE_DONE, + REGISTERED + } + + public void start() { + this.state = State.INITIAL; + updateControllerAddr(); + scanAvailableControllerAddresses(); + this.scheduledService.scheduleAtFixedRate(this::updateControllerAddr, 2 * 60 * 1000, 2 * 60 * 1000, TimeUnit.MILLISECONDS); + this.scheduledService.scheduleAtFixedRate(this::scanAvailableControllerAddresses, 3 * 1000, 3 * 1000, TimeUnit.MILLISECONDS); + if (!startBasicService()) { + LOGGER.error("Failed to start replicasManager"); + this.executorService.submit(() -> { + int retryTimes = 0; + do { + try { + TimeUnit.SECONDS.sleep(RETRY_INTERVAL_SECOND); + } catch (InterruptedException ignored) { + + } + retryTimes++; + LOGGER.warn("Failed to start replicasManager, retry times:{}, current state:{}, try it again", retryTimes, this.state); + } + while (!startBasicService()); + + LOGGER.info("Start replicasManager success, retry times:{}", retryTimes); + }); + } + } + + private boolean startBasicService() { + if (this.state == State.SHUTDOWN) + return false; + if (this.state == State.INITIAL) { + if (schedulingSyncControllerMetadata()) { + this.state = State.FIRST_TIME_SYNC_CONTROLLER_METADATA_DONE; + LOGGER.info("First time sync controller metadata success, change state to: {}", this.state); + } else { + return false; + } + } + + if (this.state == State.FIRST_TIME_SYNC_CONTROLLER_METADATA_DONE) { + for (int retryTimes = 0; retryTimes < 5; retryTimes++) { + if (register()) { + this.state = State.REGISTER_TO_CONTROLLER_DONE; + LOGGER.info("First time register broker success, change state to: {}", this.state); + break; + } + + // Try to avoid registration concurrency conflicts in random sleep + try { + Thread.sleep(random.nextInt(1000)); + } catch (Exception ignore) { + + } + } + // register 5 times but still unsuccessful + if (this.state != State.REGISTER_TO_CONTROLLER_DONE) { + LOGGER.error("Register to broker failed 5 times"); + return false; + } + } + + if (this.state == State.REGISTER_TO_CONTROLLER_DONE) { + // The scheduled task for heartbeat sending is not starting now, so we should manually send heartbeat request + this.sendHeartbeatToController(); + if (this.masterBrokerId != null || brokerElect()) { + LOGGER.info("Master in this broker set is elected, masterBrokerId: {}, masterBrokerAddr: {}", this.masterBrokerId, this.masterAddress); + this.state = State.RUNNING; + setFenced(false); + LOGGER.info("All register process has been done, change state to: {}", this.state); + } else { + return false; + } + } + + schedulingSyncBrokerMetadata(); + + // Register syncStateSet changed listener. + this.haService.registerSyncStateSetChangedListener(this::doReportSyncStateSetChanged); + return true; + } + + public void shutdown() { + this.state = State.SHUTDOWN; + this.registerState = RegisterState.INITIAL; + this.executorService.shutdownNow(); + this.scheduledService.shutdownNow(); + this.scanExecutor.shutdownNow(); + } + + public synchronized void changeBrokerRole(final Long newMasterBrokerId, final String newMasterAddress, + final Integer newMasterEpoch, + final Integer syncStateSetEpoch, final Set syncStateSet) throws Exception { + if (newMasterBrokerId != null && newMasterEpoch > this.masterEpoch) { + if (newMasterBrokerId.equals(this.brokerControllerId)) { + changeToMaster(newMasterEpoch, syncStateSetEpoch, syncStateSet); + } else { + changeToSlave(newMasterAddress, newMasterEpoch, newMasterBrokerId); + } + } + } + + public void changeToMaster(final int newMasterEpoch, final int syncStateSetEpoch, final Set syncStateSet) throws Exception { + synchronized (this) { + if (newMasterEpoch > this.masterEpoch) { + LOGGER.info("Begin to change to master, brokerName:{}, replicas:{}, new Epoch:{}", this.brokerConfig.getBrokerName(), this.brokerAddress, newMasterEpoch); + this.masterEpoch = newMasterEpoch; + if (this.masterBrokerId != null && this.masterBrokerId.equals(this.brokerControllerId) && this.brokerController.getBrokerConfig().getBrokerId() == MixAll.MASTER_ID) { + // Change SyncStateSet + final HashSet newSyncStateSet = new HashSet<>(syncStateSet); + changeSyncStateSet(newSyncStateSet, syncStateSetEpoch); + // if master doesn't change + this.haService.changeToMasterWhenLastRoleIsMaster(newMasterEpoch); + this.brokerController.getTopicConfigManager().getDataVersion().nextVersion(newMasterEpoch); + this.executorService.submit(this::checkSyncStateSetAndDoReport); + registerBrokerWhenRoleChange(); + return; + } + + // Change SyncStateSet + final HashSet newSyncStateSet = new HashSet<>(syncStateSet); + changeSyncStateSet(newSyncStateSet, syncStateSetEpoch); + + // Handle the slave synchronise + handleSlaveSynchronize(BrokerRole.SYNC_MASTER); + + // Notify ha service, change to master + this.haService.changeToMaster(newMasterEpoch); + + this.brokerController.getBrokerConfig().setBrokerId(MixAll.MASTER_ID); + this.brokerController.getMessageStoreConfig().setBrokerRole(BrokerRole.SYNC_MASTER); + this.brokerController.changeSpecialServiceStatus(true); + + // Change record + this.masterAddress = this.brokerAddress; + this.masterBrokerId = this.brokerControllerId; + + schedulingCheckSyncStateSet(); + + this.brokerController.getTopicConfigManager().getDataVersion().nextVersion(newMasterEpoch); + this.executorService.submit(this::checkSyncStateSetAndDoReport); + registerBrokerWhenRoleChange(); + } + } + } + + public void changeToSlave(final String newMasterAddress, final int newMasterEpoch, Long newMasterBrokerId) { + synchronized (this) { + if (newMasterEpoch > this.masterEpoch) { + LOGGER.info("Begin to change to slave, brokerName={}, brokerId={}, newMasterBrokerId={}, newMasterAddress={}, newMasterEpoch={}", + this.brokerConfig.getBrokerName(), this.brokerControllerId, newMasterBrokerId, newMasterAddress, newMasterEpoch); + + this.masterEpoch = newMasterEpoch; + if (newMasterBrokerId.equals(this.masterBrokerId)) { + // if master doesn't change + this.haService.changeToSlaveWhenMasterNotChange(newMasterAddress, newMasterEpoch); + this.brokerController.getTopicConfigManager().getDataVersion().nextVersion(newMasterEpoch); + registerBrokerWhenRoleChange(); + return; + } + + // Stop checking syncStateSet because only master is able to check + stopCheckSyncStateSet(); + + // Change config(compatibility problem) + this.brokerController.getMessageStoreConfig().setBrokerRole(BrokerRole.SLAVE); + this.brokerController.changeSpecialServiceStatus(false); + // The brokerId in brokerConfig just means its role(master[0] or slave[>=1]) + this.brokerConfig.setBrokerId(brokerControllerId); + + // Change record + this.masterAddress = newMasterAddress; + this.masterBrokerId = newMasterBrokerId; + + // Handle the slave synchronise + handleSlaveSynchronize(BrokerRole.SLAVE); + + // Notify ha service, change to slave + this.haService.changeToSlave(newMasterAddress, newMasterEpoch, brokerControllerId); + + this.brokerController.getTopicConfigManager().getDataVersion().nextVersion(newMasterEpoch); + registerBrokerWhenRoleChange(); + } + } + } + + public void registerBrokerWhenRoleChange() { + + this.executorService.submit(() -> { + // Register broker to name-srv + try { + this.brokerController.registerBrokerAll(true, false, this.brokerController.getBrokerConfig().isForceRegister()); + } catch (final Throwable e) { + LOGGER.error("Error happen when register broker to name-srv, Failed to change broker to {}", this.brokerController.getMessageStoreConfig().getBrokerRole(), e); + return; + } + LOGGER.info("Change broker [id:{}][address:{}] to {}, newMasterBrokerId:{}, newMasterAddress:{}, newMasterEpoch:{}, syncStateSetEpoch:{}", + this.brokerControllerId, this.brokerAddress, this.brokerController.getMessageStoreConfig().getBrokerRole(), this.masterBrokerId, this.masterAddress, this.masterEpoch, this.syncStateSetEpoch); + }); + + } + + private void changeSyncStateSet(final Set newSyncStateSet, final int newSyncStateSetEpoch) { + synchronized (this) { + if (newSyncStateSetEpoch > this.syncStateSetEpoch) { + LOGGER.info("SyncStateSet changed from {} to {}", this.syncStateSet, newSyncStateSet); + this.syncStateSetEpoch = newSyncStateSetEpoch; + this.syncStateSet = new HashSet<>(newSyncStateSet); + this.haService.setSyncStateSet(newSyncStateSet); + } + } + } + + private void handleSlaveSynchronize(final BrokerRole role) { + if (role == BrokerRole.SLAVE) { + if (this.slaveSyncFuture != null) { + this.slaveSyncFuture.cancel(false); + } + this.brokerController.getSlaveSynchronize().setMasterAddr(this.masterAddress); + slaveSyncFuture = this.brokerController.getScheduledExecutorService().scheduleAtFixedRate(() -> { + try { + if (System.currentTimeMillis() - lastSyncTimeMs > 10 * 1000) { + brokerController.getSlaveSynchronize().syncAll(); + lastSyncTimeMs = System.currentTimeMillis(); + } + //timer checkpoint, latency-sensitive, so sync it more frequently + brokerController.getSlaveSynchronize().syncTimerCheckPoint(); + } catch (final Throwable e) { + LOGGER.error("ScheduledTask SlaveSynchronize syncAll error.", e); + } + }, 1000 * 3, 1000 * 3, TimeUnit.MILLISECONDS); + + } else { + if (this.slaveSyncFuture != null) { + this.slaveSyncFuture.cancel(false); + } + this.brokerController.getSlaveSynchronize().setMasterAddr(null); + } + } + + private boolean brokerElect() { + // Broker try to elect itself as a master in broker set. + try { + Pair> tryElectResponsePair = this.brokerOuterAPI.brokerElect(this.controllerLeaderAddress, this.brokerConfig.getBrokerClusterName(), + this.brokerConfig.getBrokerName(), this.brokerControllerId); + ElectMasterResponseHeader tryElectResponse = tryElectResponsePair.getObject1(); + Set syncStateSet = tryElectResponsePair.getObject2(); + final String masterAddress = tryElectResponse.getMasterAddress(); + final Long masterBrokerId = tryElectResponse.getMasterBrokerId(); + if (StringUtils.isEmpty(masterAddress) || masterBrokerId == null) { + LOGGER.warn("Now no master in broker set"); + return false; + } + + if (masterBrokerId.equals(this.brokerControllerId)) { + changeToMaster(tryElectResponse.getMasterEpoch(), tryElectResponse.getSyncStateSetEpoch(), syncStateSet); + } else { + changeToSlave(masterAddress, tryElectResponse.getMasterEpoch(), tryElectResponse.getMasterBrokerId()); + } + return true; + } catch (Exception e) { + LOGGER.error("Failed to try elect", e); + return false; + } + } + + public void sendHeartbeatToController() { + final List controllerAddresses = this.getAvailableControllerAddresses(); + for (String controllerAddress : controllerAddresses) { + if (StringUtils.isNotEmpty(controllerAddress)) { + this.brokerOuterAPI.sendHeartbeatToController( + controllerAddress, + this.brokerConfig.getBrokerClusterName(), + this.brokerAddress, + this.brokerConfig.getBrokerName(), + this.brokerControllerId, + this.brokerConfig.getSendHeartbeatTimeoutMillis(), + this.brokerConfig.isInBrokerContainer(), this.getLastEpoch(), + this.brokerController.getMessageStore().getMaxPhyOffset(), + this.brokerController.getMessageStore().getConfirmOffset(), + this.brokerConfig.getControllerHeartBeatTimeoutMills(), + this.brokerConfig.getBrokerElectionPriority() + ); + } + } + } + + /** + * Register broker to controller, and persist the metadata to file + * + * @return whether registering process succeeded + */ + private boolean register() { + try { + // 1. confirm now registering state + confirmNowRegisteringState(); + LOGGER.info("Confirm now register state: {}", this.registerState); + // 2. check metadata/tempMetadata if valid + if (!checkMetadataValid()) { + LOGGER.error("Check and find that metadata/tempMetadata invalid, you can modify the broker config to make them valid"); + return false; + } + // 2. get next assigning brokerId, and create temp metadata file + if (this.registerState == RegisterState.INITIAL) { + Long nextBrokerId = getNextBrokerId(); + if (nextBrokerId == null || !createTempMetadataFile(nextBrokerId)) { + LOGGER.error("Failed to create temp metadata file, nextBrokerId: {}", nextBrokerId); + return false; + } + this.registerState = RegisterState.CREATE_TEMP_METADATA_FILE_DONE; + LOGGER.info("Register state change to {}, temp metadata: {}", this.registerState, this.tempBrokerMetadata); + } + // 3. apply brokerId to controller, and create metadata file + if (this.registerState == RegisterState.CREATE_TEMP_METADATA_FILE_DONE) { + if (!applyBrokerId()) { + // apply broker id failed, means that this brokerId has been used + // delete temp metadata file + this.tempBrokerMetadata.clear(); + // back to the first step + this.registerState = RegisterState.INITIAL; + LOGGER.info("Register state change to: {}", this.registerState); + return false; + } + if (!createMetadataFileAndDeleteTemp()) { + LOGGER.error("Failed to create metadata file and delete temp metadata file, temp metadata: {}", this.tempBrokerMetadata); + return false; + } + this.registerState = RegisterState.CREATE_METADATA_FILE_DONE; + LOGGER.info("Register state change to: {}, metadata: {}", this.registerState, this.brokerMetadata); + } + // 4. register + if (this.registerState == RegisterState.CREATE_METADATA_FILE_DONE) { + if (!registerBrokerToController()) { + LOGGER.error("Failed to register broker to controller"); + return false; + } + this.registerState = RegisterState.REGISTERED; + LOGGER.info("Register state change to: {}, masterBrokerId: {}, masterBrokerAddr: {}", this.registerState, this.masterBrokerId, this.masterAddress); + } + return true; + } catch (final Exception e) { + LOGGER.error("Failed to register broker to controller", e); + return false; + } + } + + /** + * Send GetNextBrokerRequest to controller for getting next assigning brokerId in this broker-set + * + * @return next brokerId in this broker-set + */ + private Long getNextBrokerId() { + try { + GetNextBrokerIdResponseHeader nextBrokerIdResp = this.brokerOuterAPI.getNextBrokerId(this.brokerConfig.getBrokerClusterName(), this.brokerConfig.getBrokerName(), this.controllerLeaderAddress); + return nextBrokerIdResp.getNextBrokerId(); + } catch (Exception e) { + LOGGER.error("fail to get next broker id from controller", e); + return null; + } + } + + /** + * Create temp metadata file in local file system, records the brokerId and registerCheckCode + * + * @param brokerId the brokerId that is expected to be assigned + * @return whether the temp meta file is created successfully + */ + + private boolean createTempMetadataFile(Long brokerId) { + // generate register check code, format like that: $ipAddress;$timestamp + String registerCheckCode = this.brokerAddress + ";" + System.currentTimeMillis(); + try { + this.tempBrokerMetadata.updateAndPersist(brokerConfig.getBrokerClusterName(), brokerConfig.getBrokerName(), brokerId, registerCheckCode); + return true; + } catch (Exception e) { + LOGGER.error("update and persist temp broker metadata file failed", e); + this.tempBrokerMetadata.clear(); + return false; + } + } + + /** + * Send applyBrokerId request to controller + * + * @return whether controller has assigned this brokerId for this broker + */ + private boolean applyBrokerId() { + try { + ApplyBrokerIdResponseHeader response = this.brokerOuterAPI.applyBrokerId(brokerConfig.getBrokerClusterName(), brokerConfig.getBrokerName(), + tempBrokerMetadata.getBrokerId(), tempBrokerMetadata.getRegisterCheckCode(), this.controllerLeaderAddress); + return true; + + } catch (Exception e) { + LOGGER.error("fail to apply broker id: {}", e, tempBrokerMetadata.getBrokerId()); + return false; + } + } + + /** + * Create metadata file and delete temp metadata file + * + * @return whether process success + */ + private boolean createMetadataFileAndDeleteTemp() { + // create metadata file and delete temp metadata file + try { + this.brokerMetadata.updateAndPersist(brokerConfig.getBrokerClusterName(), brokerConfig.getBrokerName(), tempBrokerMetadata.getBrokerId()); + this.tempBrokerMetadata.clear(); + this.brokerControllerId = this.brokerMetadata.getBrokerId(); + this.haService.setLocalBrokerId(this.brokerControllerId); + return true; + } catch (Exception e) { + LOGGER.error("fail to create metadata file", e); + this.brokerMetadata.clear(); + return false; + } + } + + /** + * Send registerBrokerToController request to inform controller that now broker has been registered successfully and + * controller should update broker ipAddress if changed + * + * @return whether request success + */ + private boolean registerBrokerToController() { + try { + Pair> responsePair = this.brokerOuterAPI.registerBrokerToController(brokerConfig.getBrokerClusterName(), brokerConfig.getBrokerName(), brokerControllerId, brokerAddress, controllerLeaderAddress); + if (responsePair == null) + return false; + RegisterBrokerToControllerResponseHeader response = responsePair.getObject1(); + Set syncStateSet = responsePair.getObject2(); + final Long masterBrokerId = response.getMasterBrokerId(); + final String masterAddress = response.getMasterAddress(); + if (masterBrokerId == null) { + return true; + } + if (this.brokerControllerId.equals(masterBrokerId)) { + changeToMaster(response.getMasterEpoch(), response.getSyncStateSetEpoch(), syncStateSet); + } else { + changeToSlave(masterAddress, response.getMasterEpoch(), masterBrokerId); + } + return true; + } catch (Exception e) { + LOGGER.error("fail to send registerBrokerToController request to controller", e); + return false; + } + } + + /** + * Confirm the registering state now + */ + private void confirmNowRegisteringState() { + // 1. check if metadata exist + try { + this.brokerMetadata.readFromFile(); + } catch (Exception e) { + LOGGER.error("Read metadata file failed", e); + } + if (this.brokerMetadata.isLoaded()) { + this.registerState = RegisterState.CREATE_METADATA_FILE_DONE; + this.brokerControllerId = brokerMetadata.getBrokerId(); + this.haService.setLocalBrokerId(this.brokerControllerId); + return; + } + // 2. check if temp metadata exist + try { + this.tempBrokerMetadata.readFromFile(); + } catch (Exception e) { + LOGGER.error("Read temp metadata file failed", e); + } + if (this.tempBrokerMetadata.isLoaded()) { + this.registerState = RegisterState.CREATE_TEMP_METADATA_FILE_DONE; + } + } + + private boolean checkMetadataValid() { + if (this.registerState == RegisterState.CREATE_TEMP_METADATA_FILE_DONE) { + if (this.tempBrokerMetadata.getClusterName() == null || !this.tempBrokerMetadata.getClusterName().equals(this.brokerConfig.getBrokerClusterName())) { + LOGGER.error("The clusterName: {} in broker temp metadata is different from the clusterName: {} in broker config", + this.tempBrokerMetadata.getClusterName(), this.brokerConfig.getBrokerClusterName()); + return false; + } + if (this.tempBrokerMetadata.getBrokerName() == null || !this.tempBrokerMetadata.getBrokerName().equals(this.brokerConfig.getBrokerName())) { + LOGGER.error("The brokerName: {} in broker temp metadata is different from the brokerName: {} in broker config", + this.tempBrokerMetadata.getBrokerName(), this.brokerConfig.getBrokerName()); + return false; + } + } + if (this.registerState == RegisterState.CREATE_METADATA_FILE_DONE) { + if (this.brokerMetadata.getClusterName() == null || !this.brokerMetadata.getClusterName().equals(this.brokerConfig.getBrokerClusterName())) { + LOGGER.error("The clusterName: {} in broker metadata is different from the clusterName: {} in broker config", + this.brokerMetadata.getClusterName(), this.brokerConfig.getBrokerClusterName()); + return false; + } + if (this.brokerMetadata.getBrokerName() == null || !this.brokerMetadata.getBrokerName().equals(this.brokerConfig.getBrokerName())) { + LOGGER.error("The brokerName: {} in broker metadata is different from the brokerName: {} in broker config", + this.brokerMetadata.getBrokerName(), this.brokerConfig.getBrokerName()); + return false; + } + } + return true; + } + + /** + * Scheduling sync broker metadata form controller. + */ + private void schedulingSyncBrokerMetadata() { + this.scheduledService.scheduleAtFixedRate(() -> { + try { + final Pair result = this.brokerOuterAPI.getReplicaInfo(this.controllerLeaderAddress, this.brokerConfig.getBrokerName()); + final GetReplicaInfoResponseHeader info = result.getObject1(); + final SyncStateSet syncStateSet = result.getObject2(); + final String newMasterAddress = info.getMasterAddress(); + final int newMasterEpoch = info.getMasterEpoch(); + final Long masterBrokerId = info.getMasterBrokerId(); + synchronized (this) { + // Check if master changed + if (newMasterEpoch > this.masterEpoch) { + if (StringUtils.isNoneEmpty(newMasterAddress) && masterBrokerId != null) { + if (masterBrokerId.equals(this.brokerControllerId)) { + // If this broker is now the master + changeToMaster(newMasterEpoch, syncStateSet.getSyncStateSetEpoch(), syncStateSet.getSyncStateSet()); + } else { + // If this broker is now the slave, and master has been changed + changeToSlave(newMasterAddress, newMasterEpoch, masterBrokerId); + } + } else { + // In this case, the master in controller is null, try elect in controller, this will trigger the electMasterEvent in controller. + brokerElect(); + } + } else if (newMasterEpoch == this.masterEpoch) { + // Check if SyncStateSet changed + if (isMasterState()) { + changeSyncStateSet(syncStateSet.getSyncStateSet(), syncStateSet.getSyncStateSetEpoch()); + } + } + } + } catch (final MQBrokerException exception) { + LOGGER.warn("Error happen when get broker {}'s metadata", this.brokerConfig.getBrokerName(), exception); + if (exception.getResponseCode() == CONTROLLER_BROKER_METADATA_NOT_EXIST) { + try { + registerBrokerToController(); + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException ignore) { + + } + } + } catch (final Exception e) { + LOGGER.warn("Error happen when get broker {}'s metadata", this.brokerConfig.getBrokerName(), e); + } + }, 3 * 1000, this.brokerConfig.getSyncBrokerMetadataPeriod(), TimeUnit.MILLISECONDS); + } + + /** + * Scheduling sync controller medata. + */ + private boolean schedulingSyncControllerMetadata() { + // Get controller metadata first. + int tryTimes = 0; + while (tryTimes < 3) { + boolean flag = updateControllerMetadata(); + if (flag) { + this.scheduledService.scheduleAtFixedRate(this::updateControllerMetadata, 1000 * 3, this.brokerConfig.getSyncControllerMetadataPeriod(), TimeUnit.MILLISECONDS); + return true; + } + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException ignore) { + + } + tryTimes++; + } + LOGGER.error("Failed to init controller metadata, maybe the controllers in {} is not available", this.controllerAddresses); + return false; + } + + /** + * Update controller leader address by rpc. + */ + private boolean updateControllerMetadata() { + for (String address : this.availableControllerAddresses.keySet()) { + try { + final GetMetaDataResponseHeader responseHeader = this.brokerOuterAPI.getControllerMetaData(address); + if (responseHeader != null && StringUtils.isNoneEmpty(responseHeader.getControllerLeaderAddress())) { + this.controllerLeaderAddress = responseHeader.getControllerLeaderAddress(); + LOGGER.info("Update controller leader address to {}", this.controllerLeaderAddress); + return true; + } + } catch (final Exception e) { + LOGGER.error("Failed to update controller metadata", e); + } + } + return false; + } + + /** + * Scheduling check syncStateSet. + */ + private void schedulingCheckSyncStateSet() { + if (this.checkSyncStateSetTaskFuture != null) { + this.checkSyncStateSetTaskFuture.cancel(false); + } + this.checkSyncStateSetTaskFuture = this.scheduledService.scheduleAtFixedRate(this::checkSyncStateSetAndDoReport, 3 * 1000, + this.brokerConfig.getCheckSyncStateSetPeriod(), TimeUnit.MILLISECONDS); + } + + private void checkSyncStateSetAndDoReport() { + try { + final Set newSyncStateSet = this.haService.maybeShrinkSyncStateSet(); + newSyncStateSet.add(this.brokerControllerId); + synchronized (this) { + if (this.syncStateSet != null) { + // Check if syncStateSet changed + if (this.syncStateSet.size() == newSyncStateSet.size() && this.syncStateSet.containsAll(newSyncStateSet)) { + return; + } + } + } + doReportSyncStateSetChanged(newSyncStateSet); + } catch (Exception e) { + LOGGER.error("Check syncStateSet error", e); + } + } + + private void doReportSyncStateSetChanged(Set newSyncStateSet) { + try { + final SyncStateSet result = this.brokerOuterAPI.alterSyncStateSet(this.controllerLeaderAddress, this.brokerConfig.getBrokerName(), this.brokerControllerId, this.masterEpoch, newSyncStateSet, this.syncStateSetEpoch); + if (result != null) { + changeSyncStateSet(result.getSyncStateSet(), result.getSyncStateSetEpoch()); + } + } catch (final Exception e) { + LOGGER.error("Error happen when change SyncStateSet, broker:{}, masterAddress:{}, masterEpoch:{}, oldSyncStateSet:{}, newSyncStateSet:{}, syncStateSetEpoch:{}", + this.brokerConfig.getBrokerName(), this.masterAddress, this.masterEpoch, this.syncStateSet, newSyncStateSet, this.syncStateSetEpoch, e); + } + } + + private void stopCheckSyncStateSet() { + if (this.checkSyncStateSetTaskFuture != null) { + this.checkSyncStateSetTaskFuture.cancel(false); + } + } + + private void scanAvailableControllerAddresses() { + if (controllerAddresses == null) { + LOGGER.warn("scanAvailableControllerAddresses addresses of controller is null!"); + return; + } + + for (String address : availableControllerAddresses.keySet()) { + if (!controllerAddresses.contains(address)) { + LOGGER.warn("scanAvailableControllerAddresses remove invalid address {}", address); + availableControllerAddresses.remove(address); + } + } + + for (String address : controllerAddresses) { + scanExecutor.submit(() -> { + if (brokerOuterAPI.checkAddressReachable(address)) { + availableControllerAddresses.putIfAbsent(address, true); + } else { + Boolean value = availableControllerAddresses.remove(address); + if (value != null) { + LOGGER.warn("scanAvailableControllerAddresses remove unconnected address {}", address); + } + } + }); + } + } + + private void updateControllerAddr() { + if (brokerConfig.isFetchControllerAddrByDnsLookup()) { + this.controllerAddresses = brokerOuterAPI.dnsLookupAddressByDomain(this.brokerConfig.getControllerAddr()); + } else { + final String controllerPaths = this.brokerConfig.getControllerAddr(); + final String[] controllers = controllerPaths.split(";"); + assert controllers.length > 0; + this.controllerAddresses = Arrays.asList(controllers); + } + } + + public int getLastEpoch() { + return this.haService.getLastEpoch(); + } + + public BrokerRole getBrokerRole() { + return this.brokerController.getMessageStoreConfig().getBrokerRole(); + } + + public boolean isMasterState() { + return getBrokerRole() == BrokerRole.SYNC_MASTER; + } + + public SyncStateSet getSyncStateSet() { + return new SyncStateSet(this.syncStateSet, this.syncStateSetEpoch); + } + + public String getBrokerAddress() { + return brokerAddress; + } + + public String getMasterAddress() { + return masterAddress; + } + + public int getMasterEpoch() { + return masterEpoch; + } + + public List getControllerAddresses() { + return controllerAddresses; + } + + public List getEpochEntries() { + return this.haService.getEpochEntries(); + } + + public List getAvailableControllerAddresses() { + return new ArrayList<>(availableControllerAddresses.keySet()); + } + + public Long getBrokerControllerId() { + return brokerControllerId; + } + + public RegisterState getRegisterState() { + return registerState; + } + + public State getState() { + return state; + } + + public BrokerMetadata getBrokerMetadata() { + return brokerMetadata; + } + + public TempBrokerMetadata getTempBrokerMetadata() { + return tempBrokerMetadata; + } + + public void setFenced(boolean fenced) { + this.brokerController.setIsolated(fenced); + this.brokerController.getMessageStore().getRunningFlags().makeFenced(fenced); + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/dledger/DLedgerRoleChangeHandler.java b/broker/src/main/java/org/apache/rocketmq/broker/dledger/DLedgerRoleChangeHandler.java new file mode 100644 index 00000000000..e6cb97640bd --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/dledger/DLedgerRoleChangeHandler.java @@ -0,0 +1,188 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.dledger; + +import io.openmessaging.storage.dledger.DLedgerLeaderElector; +import io.openmessaging.storage.dledger.DLedgerServer; +import io.openmessaging.storage.dledger.MemberState; +import io.openmessaging.storage.dledger.utils.DLedgerUtils; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.config.BrokerRole; +import org.apache.rocketmq.store.dledger.DLedgerCommitLog; + +public class DLedgerRoleChangeHandler implements DLedgerLeaderElector.RoleChangeHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + private ExecutorService executorService; + private BrokerController brokerController; + private DefaultMessageStore messageStore; + private DLedgerCommitLog dLedgerCommitLog; + private DLedgerServer dLegerServer; + private Future slaveSyncFuture; + private long lastSyncTimeMs = System.currentTimeMillis(); + + public DLedgerRoleChangeHandler(BrokerController brokerController, DefaultMessageStore messageStore) { + this.brokerController = brokerController; + this.messageStore = messageStore; + this.dLedgerCommitLog = (DLedgerCommitLog) messageStore.getCommitLog(); + this.dLegerServer = dLedgerCommitLog.getdLedgerServer(); + this.executorService = ThreadUtils.newSingleThreadExecutor( + new ThreadFactoryImpl("DLegerRoleChangeHandler_", brokerController.getBrokerIdentity())); + } + + @Override + public void handle(long term, MemberState.Role role) { + Runnable runnable = new Runnable() { + @Override + public void run() { + long start = System.currentTimeMillis(); + try { + boolean succ = true; + LOGGER.info("Begin handling broker role change term={} role={} currStoreRole={}", term, role, messageStore.getMessageStoreConfig().getBrokerRole()); + switch (role) { + case CANDIDATE: + if (messageStore.getMessageStoreConfig().getBrokerRole() != BrokerRole.SLAVE) { + changeToSlave(dLedgerCommitLog.getId()); + } + break; + case FOLLOWER: + changeToSlave(dLedgerCommitLog.getId()); + break; + case LEADER: + while (true) { + if (!dLegerServer.getMemberState().isLeader()) { + succ = false; + break; + } + if (dLegerServer.getDLedgerStore().getLedgerEndIndex() == -1) { + break; + } + if (dLegerServer.getDLedgerStore().getLedgerEndIndex() == dLegerServer.getDLedgerStore().getCommittedIndex() + && messageStore.dispatchBehindBytes() == 0) { + break; + } + Thread.sleep(100); + } + if (succ) { + messageStore.recoverTopicQueueTable(); + changeToMaster(BrokerRole.SYNC_MASTER); + } + break; + default: + break; + } + LOGGER.info("Finish handling broker role change succ={} term={} role={} currStoreRole={} cost={}", succ, term, role, messageStore.getMessageStoreConfig().getBrokerRole(), DLedgerUtils.elapsed(start)); + } catch (Throwable t) { + LOGGER.info("[MONITOR]Failed handling broker role change term={} role={} currStoreRole={} cost={}", term, role, messageStore.getMessageStoreConfig().getBrokerRole(), DLedgerUtils.elapsed(start), t); + } + } + }; + executorService.submit(runnable); + } + + private void handleSlaveSynchronize(BrokerRole role) { + if (role == BrokerRole.SLAVE) { + if (null != slaveSyncFuture) { + slaveSyncFuture.cancel(false); + } + this.brokerController.getSlaveSynchronize().setMasterAddr(null); + slaveSyncFuture = this.brokerController.getScheduledExecutorService().scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + if (System.currentTimeMillis() - lastSyncTimeMs > 10 * 1000) { + brokerController.getSlaveSynchronize().syncAll(); + lastSyncTimeMs = System.currentTimeMillis(); + } + //timer checkpoint, latency-sensitive, so sync it more frequently + brokerController.getSlaveSynchronize().syncTimerCheckPoint(); + } catch (Throwable e) { + LOGGER.error("ScheduledTask SlaveSynchronize syncAll error.", e); + } + } + }, 1000 * 3, 1000 * 3, TimeUnit.MILLISECONDS); + } else { + //handle the slave synchronise + if (null != slaveSyncFuture) { + slaveSyncFuture.cancel(false); + } + this.brokerController.getSlaveSynchronize().setMasterAddr(null); + } + } + + public void changeToSlave(int brokerId) { + LOGGER.info("Begin to change to slave brokerName={} brokerId={}", this.brokerController.getBrokerConfig().getBrokerName(), brokerId); + + //change the role + this.brokerController.getBrokerConfig().setBrokerId(brokerId == 0 ? 1 : brokerId); //TO DO check + this.brokerController.getMessageStoreConfig().setBrokerRole(BrokerRole.SLAVE); + + this.brokerController.changeSpecialServiceStatus(false); + + //handle the slave synchronise + handleSlaveSynchronize(BrokerRole.SLAVE); + + try { + this.brokerController.registerBrokerAll(true, true, this.brokerController.getBrokerConfig().isForceRegister()); + } catch (Throwable ignored) { + + } + LOGGER.info("Finish to change to slave brokerName={} brokerId={}", this.brokerController.getBrokerConfig().getBrokerName(), brokerId); + } + + public void changeToMaster(BrokerRole role) { + if (role == BrokerRole.SLAVE) { + return; + } + LOGGER.info("Begin to change to master brokerName={}", this.brokerController.getBrokerConfig().getBrokerName()); + + //handle the slave synchronise + handleSlaveSynchronize(role); + + this.brokerController.changeSpecialServiceStatus(true); + + //if the operations above are totally successful, we change to master + this.brokerController.getBrokerConfig().setBrokerId(0); //TO DO check + this.brokerController.getMessageStoreConfig().setBrokerRole(role); + + try { + this.brokerController.registerBrokerAll(true, true, this.brokerController.getBrokerConfig().isForceRegister()); + } catch (Throwable ignored) { + + } + LOGGER.info("Finish to change to master brokerName={}", this.brokerController.getBrokerConfig().getBrokerName()); + } + + @Override + public void startup() { + + } + + @Override + public void shutdown() { + executorService.shutdown(); + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/failover/EscapeBridge.java b/broker/src/main/java/org/apache/rocketmq/broker/failover/EscapeBridge.java new file mode 100644 index 00000000000..6a081748014 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/failover/EscapeBridge.java @@ -0,0 +1,356 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.failover; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.transaction.queue.TransactionalMessageUtil; +import org.apache.rocketmq.client.consumer.PullStatus; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.impl.producer.TopicPublishInfo; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.GetMessageStatus; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; + +public class EscapeBridge { + protected static final Logger LOG = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + private static final long SEND_TIMEOUT = 3000L; + private static final long DEFAULT_PULL_TIMEOUT_MILLIS = 1000 * 10L; + private final String innerProducerGroupName; + private final String innerConsumerGroupName; + + private final BrokerController brokerController; + + private ExecutorService defaultAsyncSenderExecutor; + + public EscapeBridge(BrokerController brokerController) { + this.brokerController = brokerController; + this.innerProducerGroupName = "InnerProducerGroup_" + brokerController.getBrokerConfig().getBrokerName() + "_" + brokerController.getBrokerConfig().getBrokerId(); + this.innerConsumerGroupName = "InnerConsumerGroup_" + brokerController.getBrokerConfig().getBrokerName() + "_" + brokerController.getBrokerConfig().getBrokerId(); + } + + public void start() throws Exception { + if (brokerController.getBrokerConfig().isEnableSlaveActingMaster() && brokerController.getBrokerConfig().isEnableRemoteEscape()) { + final BlockingQueue asyncSenderThreadPoolQueue = new LinkedBlockingQueue<>(50000); + this.defaultAsyncSenderExecutor = ThreadUtils.newThreadPoolExecutor( + Runtime.getRuntime().availableProcessors(), + Runtime.getRuntime().availableProcessors(), + 1000 * 60, + TimeUnit.MILLISECONDS, + asyncSenderThreadPoolQueue, + new ThreadFactoryImpl("AsyncEscapeBridgeExecutor_", this.brokerController.getBrokerIdentity()) + ); + LOG.info("init executor for escaping messages asynchronously success."); + } + } + + public void shutdown() { + if (null != this.defaultAsyncSenderExecutor) { + this.defaultAsyncSenderExecutor.shutdown(); + } + } + + public PutMessageResult putMessage(MessageExtBrokerInner messageExt) { + BrokerController masterBroker = this.brokerController.peekMasterBroker(); + if (masterBroker != null) { + return masterBroker.getMessageStore().putMessage(messageExt); + } else if (this.brokerController.getBrokerConfig().isEnableSlaveActingMaster() + && this.brokerController.getBrokerConfig().isEnableRemoteEscape()) { + + try { + messageExt.setWaitStoreMsgOK(false); + final SendResult sendResult = putMessageToRemoteBroker(messageExt); + return transformSendResult2PutResult(sendResult); + } catch (Exception e) { + LOG.error("sendMessageInFailover to remote failed", e); + return new PutMessageResult(PutMessageStatus.PUT_TO_REMOTE_BROKER_FAIL, null, true); + } + } else { + LOG.warn("Put message failed, enableSlaveActingMaster={}, enableRemoteEscape={}.", + this.brokerController.getBrokerConfig().isEnableSlaveActingMaster(), this.brokerController.getBrokerConfig().isEnableRemoteEscape()); + return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null); + } + } + + private SendResult putMessageToRemoteBroker(MessageExtBrokerInner messageExt) { + final boolean isTransHalfMessage = TransactionalMessageUtil.buildHalfTopic().equals(messageExt.getTopic()); + MessageExtBrokerInner messageToPut = messageExt; + if (isTransHalfMessage) { + messageToPut = TransactionalMessageUtil.buildTransactionalMessageFromHalfMessage(messageExt); + } + final TopicPublishInfo topicPublishInfo = this.brokerController.getTopicRouteInfoManager().tryToFindTopicPublishInfo(messageToPut.getTopic()); + if (null == topicPublishInfo || !topicPublishInfo.ok()) { + LOG.warn("putMessageToRemoteBroker: no route info of topic {} when escaping message, msgId={}", + messageToPut.getTopic(), messageToPut.getMsgId()); + return null; + } + + final MessageQueue mqSelected = topicPublishInfo.selectOneMessageQueue(); + + messageToPut.setQueueId(mqSelected.getQueueId()); + + final String brokerNameToSend = mqSelected.getBrokerName(); + final String brokerAddrToSend = this.brokerController.getTopicRouteInfoManager().findBrokerAddressInPublish(brokerNameToSend); + + final long beginTimestamp = System.currentTimeMillis(); + try { + final SendResult sendResult = this.brokerController.getBrokerOuterAPI().sendMessageToSpecificBroker( + brokerAddrToSend, brokerNameToSend, + messageToPut, this.getProducerGroup(messageToPut), SEND_TIMEOUT); + if (null != sendResult && SendStatus.SEND_OK.equals(sendResult.getSendStatus())) { + return sendResult; + } else { + LOG.error("Escaping failed! cost {}ms, Topic: {}, MsgId: {}, Broker: {}", + System.currentTimeMillis() - beginTimestamp, messageExt.getTopic(), + messageExt.getMsgId(), brokerNameToSend); + } + } catch (RemotingException | MQBrokerException e) { + LOG.error(String.format("putMessageToRemoteBroker exception, MsgId: %s, RT: %sms, Broker: %s", + messageToPut.getMsgId(), System.currentTimeMillis() - beginTimestamp, mqSelected), e); + } catch (InterruptedException e) { + LOG.error(String.format("putMessageToRemoteBroker interrupted, MsgId: %s, RT: %sms, Broker: %s", + messageToPut.getMsgId(), System.currentTimeMillis() - beginTimestamp, mqSelected), e); + Thread.currentThread().interrupt(); + } + + return null; + } + + public CompletableFuture asyncPutMessage(MessageExtBrokerInner messageExt) { + BrokerController masterBroker = this.brokerController.peekMasterBroker(); + if (masterBroker != null) { + return masterBroker.getMessageStore().asyncPutMessage(messageExt); + } else if (this.brokerController.getBrokerConfig().isEnableSlaveActingMaster() + && this.brokerController.getBrokerConfig().isEnableRemoteEscape()) { + try { + messageExt.setWaitStoreMsgOK(false); + + final TopicPublishInfo topicPublishInfo = this.brokerController.getTopicRouteInfoManager().tryToFindTopicPublishInfo(messageExt.getTopic()); + final String producerGroup = getProducerGroup(messageExt); + + final MessageQueue mqSelected = topicPublishInfo.selectOneMessageQueue(); + messageExt.setQueueId(mqSelected.getQueueId()); + + final String brokerNameToSend = mqSelected.getBrokerName(); + final String brokerAddrToSend = this.brokerController.getTopicRouteInfoManager().findBrokerAddressInPublish(brokerNameToSend); + final CompletableFuture future = this.brokerController.getBrokerOuterAPI().sendMessageToSpecificBrokerAsync(brokerAddrToSend, + brokerNameToSend, messageExt, + producerGroup, SEND_TIMEOUT); + + return future.exceptionally(throwable -> null) + .thenApplyAsync(sendResult -> transformSendResult2PutResult(sendResult), this.defaultAsyncSenderExecutor) + .exceptionally(throwable -> transformSendResult2PutResult(null)); + + } catch (Exception e) { + LOG.error("sendMessageInFailover to remote failed", e); + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.PUT_TO_REMOTE_BROKER_FAIL, null, true)); + } + } else { + LOG.warn("Put message failed, enableSlaveActingMaster={}, enableRemoteEscape={}.", + this.brokerController.getBrokerConfig().isEnableSlaveActingMaster(), this.brokerController.getBrokerConfig().isEnableRemoteEscape()); + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null)); + } + } + + + private String getProducerGroup(MessageExtBrokerInner messageExt) { + if (null == messageExt) { + return this.innerProducerGroupName; + } + String producerGroup = messageExt.getProperty(MessageConst.PROPERTY_PRODUCER_GROUP); + if (StringUtils.isEmpty(producerGroup)) { + producerGroup = this.innerProducerGroupName; + } + return producerGroup; + } + + + public PutMessageResult putMessageToSpecificQueue(MessageExtBrokerInner messageExt) { + BrokerController masterBroker = this.brokerController.peekMasterBroker(); + if (masterBroker != null) { + return masterBroker.getMessageStore().putMessage(messageExt); + } else if (this.brokerController.getBrokerConfig().isEnableSlaveActingMaster() + && this.brokerController.getBrokerConfig().isEnableRemoteEscape()) { + try { + messageExt.setWaitStoreMsgOK(false); + + final TopicPublishInfo topicPublishInfo = this.brokerController.getTopicRouteInfoManager().tryToFindTopicPublishInfo(messageExt.getTopic()); + List mqs = topicPublishInfo.getMessageQueueList(); + + if (null == mqs || mqs.isEmpty()) { + return new PutMessageResult(PutMessageStatus.PUT_TO_REMOTE_BROKER_FAIL, null, true); + } + + String id = messageExt.getTopic() + messageExt.getStoreHost(); + final int index = Math.floorMod(id.hashCode(), mqs.size()); + + MessageQueue mq = mqs.get(index); + messageExt.setQueueId(mq.getQueueId()); + + String brokerNameToSend = mq.getBrokerName(); + String brokerAddrToSend = this.brokerController.getTopicRouteInfoManager().findBrokerAddressInPublish(brokerNameToSend); + final SendResult sendResult = this.brokerController.getBrokerOuterAPI().sendMessageToSpecificBroker( + brokerAddrToSend, brokerNameToSend, + messageExt, this.getProducerGroup(messageExt), SEND_TIMEOUT); + + return transformSendResult2PutResult(sendResult); + } catch (Exception e) { + LOG.error("sendMessageInFailover to remote failed", e); + return new PutMessageResult(PutMessageStatus.PUT_TO_REMOTE_BROKER_FAIL, null, true); + } + } else { + LOG.warn("Put message to specific queue failed, enableSlaveActingMaster={}, enableRemoteEscape={}.", + this.brokerController.getBrokerConfig().isEnableSlaveActingMaster(), this.brokerController.getBrokerConfig().isEnableRemoteEscape()); + return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null); + } + } + + private PutMessageResult transformSendResult2PutResult(SendResult sendResult) { + if (sendResult == null) { + return new PutMessageResult(PutMessageStatus.PUT_TO_REMOTE_BROKER_FAIL, null, true); + } + switch (sendResult.getSendStatus()) { + case SEND_OK: + return new PutMessageResult(PutMessageStatus.PUT_OK, null, true); + case SLAVE_NOT_AVAILABLE: + return new PutMessageResult(PutMessageStatus.SLAVE_NOT_AVAILABLE, null, true); + case FLUSH_DISK_TIMEOUT: + return new PutMessageResult(PutMessageStatus.FLUSH_DISK_TIMEOUT, null, true); + case FLUSH_SLAVE_TIMEOUT: + return new PutMessageResult(PutMessageStatus.FLUSH_SLAVE_TIMEOUT, null, true); + default: + return new PutMessageResult(PutMessageStatus.PUT_TO_REMOTE_BROKER_FAIL, null, true); + } + } + + public Pair getMessage(String topic, long offset, int queueId, String brokerName, boolean deCompressBody) { + return getMessageAsync(topic, offset, queueId, brokerName, deCompressBody).join(); + } + + public CompletableFuture> getMessageAsync(String topic, long offset, int queueId, String brokerName, boolean deCompressBody) { + MessageStore messageStore = brokerController.getMessageStoreByBrokerName(brokerName); + if (messageStore != null) { + return messageStore.getMessageAsync(innerConsumerGroupName, topic, queueId, offset, 1, null) + .thenApply(result -> { + if (result == null) { + LOG.warn("getMessageResult is null , innerConsumerGroupName {}, topic {}, offset {}, queueId {}", innerConsumerGroupName, topic, offset, queueId); + return new Pair<>(GetMessageStatus.MESSAGE_WAS_REMOVING, null); + } + List list = decodeMsgList(result, deCompressBody); + if (list == null || list.isEmpty()) { + LOG.warn("Can not get msg , topic {}, offset {}, queueId {}, result is {}", topic, offset, queueId, result); + return new Pair<>(result.getStatus(), null); + } + return new Pair<>(result.getStatus(), list.get(0)); + }); + } else { + return getMessageFromRemoteAsync(topic, offset, queueId, brokerName) + .thenApply(msg -> { + if (msg == null) { + return new Pair<>(GetMessageStatus.MESSAGE_WAS_REMOVING, null); + } + return new Pair<>(GetMessageStatus.FOUND, msg); + }); + } + } + + protected List decodeMsgList(GetMessageResult getMessageResult, boolean deCompressBody) { + List foundList = new ArrayList<>(); + try { + List messageBufferList = getMessageResult.getMessageBufferList(); + if (messageBufferList != null) { + for (int i = 0; i < messageBufferList.size(); i++) { + ByteBuffer bb = messageBufferList.get(i); + if (bb == null) { + LOG.error("bb is null {}", getMessageResult); + continue; + } + MessageExt msgExt = MessageDecoder.decode(bb, true, deCompressBody); + if (msgExt == null) { + LOG.error("decode msgExt is null {}", getMessageResult); + continue; + } + // use CQ offset, not offset in Message + msgExt.setQueueOffset(getMessageResult.getMessageQueueOffset().get(i)); + foundList.add(msgExt); + } + } + } finally { + getMessageResult.release(); + } + + return foundList; + } + + protected MessageExt getMessageFromRemote(String topic, long offset, int queueId, String brokerName) { + return getMessageFromRemoteAsync(topic, offset, queueId, brokerName).join(); + } + + protected CompletableFuture getMessageFromRemoteAsync(String topic, long offset, int queueId, String brokerName) { + try { + String brokerAddr = this.brokerController.getTopicRouteInfoManager().findBrokerAddressInSubscribe(brokerName, MixAll.MASTER_ID, false); + if (null == brokerAddr) { + this.brokerController.getTopicRouteInfoManager().updateTopicRouteInfoFromNameServer(topic, true, false); + brokerAddr = this.brokerController.getTopicRouteInfoManager().findBrokerAddressInSubscribe(brokerName, MixAll.MASTER_ID, false); + + if (null == brokerAddr) { + LOG.warn("can't find broker address for topic {}", topic); + return CompletableFuture.completedFuture(null); + } + } + + return this.brokerController.getBrokerOuterAPI().pullMessageFromSpecificBrokerAsync(brokerName, + brokerAddr, this.innerConsumerGroupName, topic, queueId, offset, 1, DEFAULT_PULL_TIMEOUT_MILLIS) + .thenApply(pullResult -> { + if (pullResult.getPullStatus().equals(PullStatus.FOUND) && !pullResult.getMsgFoundList().isEmpty()) { + return pullResult.getMsgFoundList().get(0); + } + return null; + }); + } catch (Exception e) { + LOG.error("Get message from remote failed.", e); + } + + return CompletableFuture.completedFuture(null); + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/filter/CommitLogDispatcherCalcBitMap.java b/broker/src/main/java/org/apache/rocketmq/broker/filter/CommitLogDispatcherCalcBitMap.java index 85415d6297c..00f0c13dd3a 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/filter/CommitLogDispatcherCalcBitMap.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/filter/CommitLogDispatcherCalcBitMap.java @@ -18,12 +18,13 @@ package org.apache.rocketmq.broker.filter; import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.UtilAll; import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.filter.util.BitsArray; import org.apache.rocketmq.store.CommitLogDispatcher; import org.apache.rocketmq.store.DispatchRequest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.Collection; import java.util.Iterator; @@ -98,10 +99,10 @@ public void dispatch(DispatchRequest request) { request.setBitMap(filterBitMap.bytes()); - long eclipseTime = System.currentTimeMillis() - startTime; + long elapsedTime = UtilAll.computeElapsedTimeMilliseconds(startTime); // 1ms - if (eclipseTime >= 1) { - log.warn("Spend {} ms to calc bit map, consumerNum={}, topic={}", eclipseTime, filterDatas.size(), request.getTopic()); + if (elapsedTime >= 1) { + log.warn("Spend {} ms to calc bit map, consumerNum={}, topic={}", elapsedTime, filterDatas.size(), request.getTopic()); } } catch (Throwable e) { log.error("Calc bit map error! topic={}, offset={}, queueId={}, {}", request.getTopic(), request.getCommitLogOffset(), request.getQueueId(), e); diff --git a/broker/src/main/java/org/apache/rocketmq/broker/filter/ConsumerFilterManager.java b/broker/src/main/java/org/apache/rocketmq/broker/filter/ConsumerFilterManager.java index 482893f7cee..3a48f96b987 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/filter/ConsumerFilterManager.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/filter/ConsumerFilterManager.java @@ -17,25 +17,25 @@ package org.apache.rocketmq.broker.filter; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.apache.rocketmq.broker.BrokerController; import org.apache.rocketmq.broker.BrokerPathConfigHelper; import org.apache.rocketmq.common.ConfigManager; import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; -import org.apache.rocketmq.filter.FilterFactory; import org.apache.rocketmq.common.filter.ExpressionType; +import org.apache.rocketmq.filter.FilterFactory; import org.apache.rocketmq.filter.util.BloomFilter; import org.apache.rocketmq.filter.util.BloomFilterData; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; /** * Consumer filter data manager.Just manage the consumers use expression filter. @@ -47,7 +47,7 @@ public class ConsumerFilterManager extends ConfigManager { private static final long MS_24_HOUR = 24 * 3600 * 1000; private ConcurrentMap - filterDataByTopic = new ConcurrentHashMap(256); + filterDataByTopic = new ConcurrentHashMap<>(256); private transient BrokerController brokerController; private transient BloomFilter bloomFilter; @@ -158,8 +158,8 @@ public boolean register(final String topic, final String consumerGroup, final St } public void unRegister(final String consumerGroup) { - for (String topic : filterDataByTopic.keySet()) { - this.filterDataByTopic.get(topic).unRegister(consumerGroup); + for (Entry entry : filterDataByTopic.entrySet()) { + entry.getValue().unRegister(consumerGroup); } } @@ -175,7 +175,7 @@ public ConsumerFilterData get(final String topic, final String consumerGroup) { } public Collection getByGroup(final String consumerGroup) { - Collection ret = new HashSet(); + Collection ret = new HashSet<>(); Iterator topicIterator = this.filterDataByTopic.values().iterator(); while (topicIterator.hasNext()) { @@ -230,15 +230,15 @@ public void decode(final String jsonString) { ConsumerFilterManager load = RemotingSerializable.fromJson(jsonString, ConsumerFilterManager.class); if (load != null && load.filterDataByTopic != null) { boolean bloomChanged = false; - for (String topic : load.filterDataByTopic.keySet()) { - FilterDataMapByTopic dataMapByTopic = load.filterDataByTopic.get(topic); + for (Entry entry : load.filterDataByTopic.entrySet()) { + FilterDataMapByTopic dataMapByTopic = entry.getValue(); if (dataMapByTopic == null) { continue; } - for (String group : dataMapByTopic.getGroupFilterData().keySet()) { + for (Entry groupEntry : dataMapByTopic.getGroupFilterData().entrySet()) { - ConsumerFilterData filterData = dataMapByTopic.getGroupFilterData().get(group); + ConsumerFilterData filterData = groupEntry.getValue(); if (filterData == null) { continue; @@ -246,7 +246,7 @@ public void decode(final String jsonString) { try { filterData.setCompiledExpression( - FilterFactory.INSTANCE.get(filterData.getExpressionType()).compile(filterData.getExpression()) + FilterFactory.INSTANCE.get(filterData.getExpressionType()).compile(filterData.getExpression()) ); } catch (Exception e) { log.error("load filter data error, " + filterData, e); @@ -266,7 +266,7 @@ public void decode(final String jsonString) { // we think all consumers are dead when load long deadTime = System.currentTimeMillis() - 30 * 1000; filterData.setDeadTime( - deadTime <= filterData.getBornTime() ? filterData.getBornTime() : deadTime + deadTime <= filterData.getBornTime() ? filterData.getBornTime() : deadTime ); } } @@ -323,7 +323,7 @@ public void setFilterDataByTopic(final ConcurrentHashMap - groupFilterData = new ConcurrentHashMap(); + groupFilterData = new ConcurrentHashMap<>(); private String topic; diff --git a/broker/src/main/java/org/apache/rocketmq/broker/filter/ExpressionForRetryMessageFilter.java b/broker/src/main/java/org/apache/rocketmq/broker/filter/ExpressionForRetryMessageFilter.java index 7f7da05dd02..cc3e37bf4b3 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/filter/ExpressionForRetryMessageFilter.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/filter/ExpressionForRetryMessageFilter.java @@ -17,14 +17,14 @@ package org.apache.rocketmq.broker.filter; +import java.nio.ByteBuffer; +import java.util.Map; +import org.apache.rocketmq.common.KeyBuilder; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.filter.ExpressionType; import org.apache.rocketmq.common.message.MessageConst; import org.apache.rocketmq.common.message.MessageDecoder; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; - -import java.nio.ByteBuffer; -import java.util.Map; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; /** * Support filter to retry topic. @@ -46,12 +46,12 @@ public boolean isMatchedByCommitLog(ByteBuffer msgBuffer, Map pr return true; } - boolean isRetryTopic = subscriptionData.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX); - - if (!isRetryTopic && ExpressionType.isTagType(subscriptionData.getExpressionType())) { + if (ExpressionType.isTagType(subscriptionData.getExpressionType())) { return true; } + boolean isRetryTopic = subscriptionData.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX); + ConsumerFilterData realFilterData = this.consumerFilterData; Map tempProperties = properties; boolean decoded = false; @@ -63,7 +63,7 @@ public boolean isMatchedByCommitLog(ByteBuffer msgBuffer, Map pr tempProperties = MessageDecoder.decodeProperties(msgBuffer); } String realTopic = tempProperties.get(MessageConst.PROPERTY_RETRY_TOPIC); - String group = subscriptionData.getTopic().substring(MixAll.RETRY_GROUP_TOPIC_PREFIX.length()); + String group = KeyBuilder.parseGroup(subscriptionData.getTopic()); realFilterData = this.consumerFilterManager.get(realTopic, group); } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/filter/ExpressionMessageFilter.java b/broker/src/main/java/org/apache/rocketmq/broker/filter/ExpressionMessageFilter.java index 64c28ece3e6..5d0340c3bce 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/filter/ExpressionMessageFilter.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/filter/ExpressionMessageFilter.java @@ -17,19 +17,18 @@ package org.apache.rocketmq.broker.filter; +import java.nio.ByteBuffer; +import java.util.Map; import org.apache.rocketmq.common.constant.LoggerName; import org.apache.rocketmq.common.filter.ExpressionType; import org.apache.rocketmq.common.message.MessageDecoder; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; import org.apache.rocketmq.filter.util.BitsArray; import org.apache.rocketmq.filter.util.BloomFilter; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; import org.apache.rocketmq.store.ConsumeQueueExt; import org.apache.rocketmq.store.MessageFilter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.nio.ByteBuffer; -import java.util.Map; public class ExpressionMessageFilter implements MessageFilter { diff --git a/broker/src/main/java/org/apache/rocketmq/broker/filter/MessageEvaluationContext.java b/broker/src/main/java/org/apache/rocketmq/broker/filter/MessageEvaluationContext.java index 879d17908f6..980b738dda3 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/filter/MessageEvaluationContext.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/filter/MessageEvaluationContext.java @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Map.Entry; /** * Evaluation context from message. @@ -47,10 +48,10 @@ public Map keyValues() { return null; } - Map copy = new HashMap(properties.size(), 1); + Map copy = new HashMap<>(properties.size(), 1); - for (String key : properties.keySet()) { - copy.put(key, properties.get(key)); + for (Entry entry : properties.entrySet()) { + copy.put(entry.getKey(), entry.getValue()); } return copy; diff --git a/broker/src/main/java/org/apache/rocketmq/broker/filtersrv/FilterServerManager.java b/broker/src/main/java/org/apache/rocketmq/broker/filtersrv/FilterServerManager.java deleted file mode 100644 index f8f9943e826..00000000000 --- a/broker/src/main/java/org/apache/rocketmq/broker/filtersrv/FilterServerManager.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.broker.filtersrv; - -import io.netty.channel.Channel; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Map.Entry; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import org.apache.rocketmq.broker.BrokerController; -import org.apache.rocketmq.broker.BrokerStartup; -import org.apache.rocketmq.common.ThreadFactoryImpl; -import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.remoting.common.RemotingUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class FilterServerManager { - - public static final long FILTER_SERVER_MAX_IDLE_TIME_MILLS = 30000; - private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); - private final ConcurrentMap filterServerTable = - new ConcurrentHashMap(16); - private final BrokerController brokerController; - - private ScheduledExecutorService scheduledExecutorService = Executors - .newSingleThreadScheduledExecutor(new ThreadFactoryImpl("FilterServerManagerScheduledThread")); - - public FilterServerManager(final BrokerController brokerController) { - this.brokerController = brokerController; - } - - public void start() { - - this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { - @Override - public void run() { - try { - FilterServerManager.this.createFilterServer(); - } catch (Exception e) { - log.error("", e); - } - } - }, 1000 * 5, 1000 * 30, TimeUnit.MILLISECONDS); - } - - public void createFilterServer() { - int more = - this.brokerController.getBrokerConfig().getFilterServerNums() - this.filterServerTable.size(); - String cmd = this.buildStartCommand(); - for (int i = 0; i < more; i++) { - FilterServerUtil.callShell(cmd, log); - } - } - - private String buildStartCommand() { - String config = ""; - if (BrokerStartup.configFile != null) { - config = String.format("-c %s", BrokerStartup.configFile); - } - - if (this.brokerController.getBrokerConfig().getNamesrvAddr() != null) { - config += String.format(" -n %s", this.brokerController.getBrokerConfig().getNamesrvAddr()); - } - - if (RemotingUtil.isWindowsPlatform()) { - return String.format("start /b %s\\bin\\mqfiltersrv.exe %s", - this.brokerController.getBrokerConfig().getRocketmqHome(), - config); - } else { - return String.format("sh %s/bin/startfsrv.sh %s", - this.brokerController.getBrokerConfig().getRocketmqHome(), - config); - } - } - - public void shutdown() { - this.scheduledExecutorService.shutdown(); - } - - public void registerFilterServer(final Channel channel, final String filterServerAddr) { - FilterServerInfo filterServerInfo = this.filterServerTable.get(channel); - if (filterServerInfo != null) { - filterServerInfo.setLastUpdateTimestamp(System.currentTimeMillis()); - } else { - filterServerInfo = new FilterServerInfo(); - filterServerInfo.setFilterServerAddr(filterServerAddr); - filterServerInfo.setLastUpdateTimestamp(System.currentTimeMillis()); - this.filterServerTable.put(channel, filterServerInfo); - log.info("Receive a New Filter Server<{}>", filterServerAddr); - } - } - - public void scanNotActiveChannel() { - - Iterator> it = this.filterServerTable.entrySet().iterator(); - while (it.hasNext()) { - Entry next = it.next(); - long timestamp = next.getValue().getLastUpdateTimestamp(); - Channel channel = next.getKey(); - if ((System.currentTimeMillis() - timestamp) > FILTER_SERVER_MAX_IDLE_TIME_MILLS) { - log.info("The Filter Server<{}> expired, remove it", next.getKey()); - it.remove(); - RemotingUtil.closeChannel(channel); - } - } - } - - public void doChannelCloseEvent(final String remoteAddr, final Channel channel) { - FilterServerInfo old = this.filterServerTable.remove(channel); - if (old != null) { - log.warn("The Filter Server<{}> connection<{}> closed, remove it", old.getFilterServerAddr(), - remoteAddr); - } - } - - public List buildNewFilterServerList() { - List addr = new ArrayList<>(); - Iterator> it = this.filterServerTable.entrySet().iterator(); - while (it.hasNext()) { - Entry next = it.next(); - addr.add(next.getValue().getFilterServerAddr()); - } - return addr; - } - - static class FilterServerInfo { - private String filterServerAddr; - private long lastUpdateTimestamp; - - public String getFilterServerAddr() { - return filterServerAddr; - } - - public void setFilterServerAddr(String filterServerAddr) { - this.filterServerAddr = filterServerAddr; - } - - public long getLastUpdateTimestamp() { - return lastUpdateTimestamp; - } - - public void setLastUpdateTimestamp(long lastUpdateTimestamp) { - this.lastUpdateTimestamp = lastUpdateTimestamp; - } - } -} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/filtersrv/FilterServerUtil.java b/broker/src/main/java/org/apache/rocketmq/broker/filtersrv/FilterServerUtil.java deleted file mode 100644 index 5b142c11eca..00000000000 --- a/broker/src/main/java/org/apache/rocketmq/broker/filtersrv/FilterServerUtil.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.broker.filtersrv; - -import org.slf4j.Logger; - -public class FilterServerUtil { - public static void callShell(final String shellString, final Logger log) { - Process process = null; - try { - String[] cmdArray = splitShellString(shellString); - process = Runtime.getRuntime().exec(cmdArray); - process.waitFor(); - log.info("CallShell: <{}> OK", shellString); - } catch (Throwable e) { - log.error("CallShell: readLine IOException, {}", shellString, e); - } finally { - if (null != process) - process.destroy(); - } - } - - private static String[] splitShellString(final String shellString) { - return shellString.split(" "); - } -} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/latency/BrokerFastFailure.java b/broker/src/main/java/org/apache/rocketmq/broker/latency/BrokerFastFailure.java index 6aefe81a113..3b6e9dc676e 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/latency/BrokerFastFailure.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/latency/BrokerFastFailure.java @@ -17,46 +17,55 @@ package org.apache.rocketmq.broker.latency; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.AbstractBrokerRunnable; import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.UtilAll; import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.future.FutureTaskExt; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.remoting.netty.RequestTask; import org.apache.rocketmq.remoting.protocol.RemotingSysResponseCode; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** - * BrokerFastFailure will cover {@link BrokerController#sendThreadPoolQueue} and - * {@link BrokerController#pullThreadPoolQueue} + * BrokerFastFailure will cover {@link BrokerController#getSendThreadPoolQueue()} and {@link + * BrokerController#getPullThreadPoolQueue()} */ public class BrokerFastFailure { - private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); - private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl( - "BrokerFastFailureScheduledThread")); + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + private final ScheduledExecutorService scheduledExecutorService; private final BrokerController brokerController; + private volatile long jstackTime = System.currentTimeMillis(); + public BrokerFastFailure(final BrokerController brokerController) { this.brokerController = brokerController; + this.scheduledExecutorService = ThreadUtils.newScheduledThreadPool(1, + new ThreadFactoryImpl("BrokerFastFailureScheduledThread", true, + brokerController == null ? null : brokerController.getBrokerConfig())); } public static RequestTask castRunnable(final Runnable runnable) { try { - FutureTaskExt object = (FutureTaskExt) runnable; - return (RequestTask) object.getRunnable(); + if (runnable instanceof FutureTaskExt) { + FutureTaskExt object = (FutureTaskExt) runnable; + return (RequestTask) object.getRunnable(); + } } catch (Throwable e) { - log.error(String.format("castRunnable exception, %s", runnable.getClass().getName()), e); + LOGGER.error(String.format("castRunnable exception, %s", runnable.getClass().getName()), e); } return null; } public void start() { - this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + this.scheduledExecutorService.scheduleAtFixedRate(new AbstractBrokerRunnable(this.brokerController.getBrokerConfig()) { @Override - public void run() { + public void run0() { if (brokerController.getBrokerConfig().isBrokerFastFailureEnable()) { cleanExpiredRequest(); } @@ -65,6 +74,7 @@ public void run() { } private void cleanExpiredRequest() { + while (this.brokerController.getMessageStore().isOSPageCacheBusy()) { try { if (!this.brokerController.getSendThreadPoolQueue().isEmpty()) { @@ -74,7 +84,13 @@ private void cleanExpiredRequest() { } final RequestTask rt = castRunnable(runnable); - rt.returnResponse(RemotingSysResponseCode.SYSTEM_BUSY, String.format("[PCBUSY_CLEAN_QUEUE]broker busy, start flow control for a while, period in queue: %sms, size of queue: %d", System.currentTimeMillis() - rt.getCreateTimestamp(), this.brokerController.getSendThreadPoolQueue().size())); + if (rt != null) { + rt.returnResponse(RemotingSysResponseCode.SYSTEM_BUSY, String.format( + "[PCBUSY_CLEAN_QUEUE]broker busy, start flow control for a while, period in queue: %sms, " + + "size of queue: %d", + System.currentTimeMillis() - rt.getCreateTimestamp(), + this.brokerController.getSendThreadPoolQueue().size())); + } } else { break; } @@ -87,6 +103,18 @@ private void cleanExpiredRequest() { cleanExpiredRequestInQueue(this.brokerController.getPullThreadPoolQueue(), this.brokerController.getBrokerConfig().getWaitTimeMillsInPullQueue()); + + cleanExpiredRequestInQueue(this.brokerController.getLitePullThreadPoolQueue(), + this.brokerController.getBrokerConfig().getWaitTimeMillsInLitePullQueue()); + + cleanExpiredRequestInQueue(this.brokerController.getHeartbeatThreadPoolQueue(), + this.brokerController.getBrokerConfig().getWaitTimeMillsInHeartbeatQueue()); + + cleanExpiredRequestInQueue(this.brokerController.getEndTransactionThreadPoolQueue(), this + .brokerController.getBrokerConfig().getWaitTimeMillsInTransactionQueue()); + + cleanExpiredRequestInQueue(this.brokerController.getAckThreadPoolQueue(), + brokerController.getBrokerConfig().getWaitTimeMillsInAckQueue()); } void cleanExpiredRequestInQueue(final BlockingQueue blockingQueue, final long maxWaitTimeMillsInQueue) { @@ -107,6 +135,10 @@ void cleanExpiredRequestInQueue(final BlockingQueue blockingQueue, fin if (blockingQueue.remove(runnable)) { rt.setStopRun(true); rt.returnResponse(RemotingSysResponseCode.SYSTEM_BUSY, String.format("[TIMEOUT_CLEAN_QUEUE]broker busy, start flow control for a while, period in queue: %sms, size of queue: %d", behind, blockingQueue.size())); + if (System.currentTimeMillis() - jstackTime > 15000) { + jstackTime = System.currentTimeMillis(); + LOGGER.warn("broker jstack \n " + UtilAll.jstack()); + } } } else { break; diff --git a/broker/src/main/java/org/apache/rocketmq/broker/latency/BrokerFixedThreadPoolExecutor.java b/broker/src/main/java/org/apache/rocketmq/broker/latency/BrokerFixedThreadPoolExecutor.java deleted file mode 100644 index 8060fd00c79..00000000000 --- a/broker/src/main/java/org/apache/rocketmq/broker/latency/BrokerFixedThreadPoolExecutor.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.broker.latency; - -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.RunnableFuture; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -public class BrokerFixedThreadPoolExecutor extends ThreadPoolExecutor { - public BrokerFixedThreadPoolExecutor(final int corePoolSize, final int maximumPoolSize, final long keepAliveTime, - final TimeUnit unit, - final BlockingQueue workQueue) { - super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); - } - - public BrokerFixedThreadPoolExecutor(final int corePoolSize, final int maximumPoolSize, final long keepAliveTime, - final TimeUnit unit, - final BlockingQueue workQueue, final ThreadFactory threadFactory) { - super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory); - } - - public BrokerFixedThreadPoolExecutor(final int corePoolSize, final int maximumPoolSize, final long keepAliveTime, - final TimeUnit unit, - final BlockingQueue workQueue, final RejectedExecutionHandler handler) { - super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler); - } - - public BrokerFixedThreadPoolExecutor(final int corePoolSize, final int maximumPoolSize, final long keepAliveTime, - final TimeUnit unit, - final BlockingQueue workQueue, final ThreadFactory threadFactory, - final RejectedExecutionHandler handler) { - super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler); - } - - @Override - protected RunnableFuture newTaskFor(final Runnable runnable, final T value) { - return new FutureTaskExt(runnable, value); - } -} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/loadbalance/MessageRequestModeManager.java b/broker/src/main/java/org/apache/rocketmq/broker/loadbalance/MessageRequestModeManager.java new file mode 100644 index 00000000000..0c69e2de94f --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/loadbalance/MessageRequestModeManager.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.loadbalance; + +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.BrokerPathConfigHelper; +import org.apache.rocketmq.common.ConfigManager; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.body.SetMessageRequestModeRequestBody; + +public class MessageRequestModeManager extends ConfigManager { + + private transient BrokerController brokerController; + + private ConcurrentHashMap> + messageRequestModeMap = new ConcurrentHashMap<>(); + + public MessageRequestModeManager() { + // empty construct for decode + } + + public MessageRequestModeManager(BrokerController brokerController) { + this.brokerController = brokerController; + } + + public void setMessageRequestMode(String topic, String consumerGroup, SetMessageRequestModeRequestBody requestBody) { + ConcurrentHashMap consumerGroup2ModeMap = messageRequestModeMap.get(topic); + if (consumerGroup2ModeMap == null) { + consumerGroup2ModeMap = new ConcurrentHashMap<>(); + ConcurrentHashMap pre = + messageRequestModeMap.putIfAbsent(topic, consumerGroup2ModeMap); + if (pre != null) { + consumerGroup2ModeMap = pre; + } + } + consumerGroup2ModeMap.put(consumerGroup, requestBody); + } + + public SetMessageRequestModeRequestBody getMessageRequestMode(String topic, String consumerGroup) { + ConcurrentHashMap consumerGroup2ModeMap = messageRequestModeMap.get(topic); + if (consumerGroup2ModeMap != null) { + return consumerGroup2ModeMap.get(consumerGroup); + } + + return null; + } + + public ConcurrentHashMap> getMessageRequestModeMap() { + return this.messageRequestModeMap; + } + + public void setMessageRequestModeMap(ConcurrentHashMap> messageRequestModeMap) { + this.messageRequestModeMap = messageRequestModeMap; + } + + @Override + public String encode() { + return this.encode(false); + } + + @Override + public String configFilePath() { + return BrokerPathConfigHelper.getMessageRequestModePath(this.brokerController.getMessageStoreConfig().getStorePathRootDir()); + } + + @Override + public void decode(String jsonString) { + if (jsonString != null) { + MessageRequestModeManager obj = RemotingSerializable.fromJson(jsonString, MessageRequestModeManager.class); + if (obj != null) { + this.messageRequestModeMap = obj.messageRequestModeMap; + } + } + } + + @Override + public String encode(boolean prettyFormat) { + return RemotingSerializable.toJson(this, prettyFormat); + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/longpolling/LmqPullRequestHoldService.java b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/LmqPullRequestHoldService.java new file mode 100644 index 00000000000..88e74fd6e5a --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/LmqPullRequestHoldService.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.longpolling; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + + +public class LmqPullRequestHoldService extends PullRequestHoldService { + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + + public LmqPullRequestHoldService(BrokerController brokerController) { + super(brokerController); + } + + @Override + public String getServiceName() { + if (brokerController != null && brokerController.getBrokerConfig().isInBrokerContainer()) { + return this.brokerController.getBrokerIdentity().getIdentifier() + LmqPullRequestHoldService.class.getSimpleName(); + } + return LmqPullRequestHoldService.class.getSimpleName(); + } + + @Override + public void checkHoldRequest() { + for (String key : pullRequestTable.keySet()) { + int idx = key.lastIndexOf(TOPIC_QUEUEID_SEPARATOR); + if (idx <= 0 || idx >= key.length() - 1) { + pullRequestTable.remove(key); + continue; + } + String topic = key.substring(0, idx); + int queueId = Integer.parseInt(key.substring(idx + 1)); + final long offset = brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId); + try { + this.notifyMessageArriving(topic, queueId, offset); + } catch (Throwable e) { + LOGGER.error("check hold request failed. topic={}, queueId={}", topic, queueId, e); + } + if (MixAll.isLmq(topic)) { + ManyPullRequest mpr = pullRequestTable.get(key); + if (mpr == null || mpr.getPullRequestList() == null || mpr.getPullRequestList().isEmpty()) { + pullRequestTable.remove(key); + } + } + } + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/longpolling/ManyPullRequest.java b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/ManyPullRequest.java index d956c223c0e..08703deaf0c 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/longpolling/ManyPullRequest.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/ManyPullRequest.java @@ -39,4 +39,12 @@ public synchronized List cloneListAndClear() { return null; } + + public ArrayList getPullRequestList() { + return pullRequestList; + } + + public synchronized boolean isEmpty() { + return this.pullRequestList.isEmpty(); + } } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/longpolling/NotificationRequest.java b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/NotificationRequest.java new file mode 100644 index 00000000000..2ff9a73a4bb --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/NotificationRequest.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.longpolling; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +import io.netty.channel.Channel; + +public class NotificationRequest { + private RemotingCommand remotingCommand; + private Channel channel; + private long expired; + private AtomicBoolean complete = new AtomicBoolean(false); + + public NotificationRequest(RemotingCommand remotingCommand, Channel channel, long expired) { + this.channel = channel; + this.remotingCommand = remotingCommand; + this.expired = expired; + } + + public Channel getChannel() { + return channel; + } + + public RemotingCommand getRemotingCommand() { + return remotingCommand; + } + + public boolean isTimeout() { + return System.currentTimeMillis() > (expired - 500); + } + + public boolean complete() { + return complete.compareAndSet(false, true); + } + + @Override + public String toString() { + return remotingCommand.toString(); + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/longpolling/NotifyMessageArrivingListener.java b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/NotifyMessageArrivingListener.java index ff0901126f2..e55ed2778ac 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/longpolling/NotifyMessageArrivingListener.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/NotifyMessageArrivingListener.java @@ -17,21 +17,28 @@ package org.apache.rocketmq.broker.longpolling; -import org.apache.rocketmq.store.MessageArrivingListener; - import java.util.Map; +import org.apache.rocketmq.broker.processor.NotificationProcessor; +import org.apache.rocketmq.broker.processor.PopMessageProcessor; +import org.apache.rocketmq.store.MessageArrivingListener; public class NotifyMessageArrivingListener implements MessageArrivingListener { private final PullRequestHoldService pullRequestHoldService; + private final PopMessageProcessor popMessageProcessor; + private final NotificationProcessor notificationProcessor; - public NotifyMessageArrivingListener(final PullRequestHoldService pullRequestHoldService) { + public NotifyMessageArrivingListener(final PullRequestHoldService pullRequestHoldService, final PopMessageProcessor popMessageProcessor, final NotificationProcessor notificationProcessor) { this.pullRequestHoldService = pullRequestHoldService; + this.popMessageProcessor = popMessageProcessor; + this.notificationProcessor = notificationProcessor; } @Override public void arriving(String topic, int queueId, long logicOffset, long tagsCode, - long msgStoreTime, byte[] filterBitMap, Map properties) { + long msgStoreTime, byte[] filterBitMap, Map properties) { this.pullRequestHoldService.notifyMessageArriving(topic, queueId, logicOffset, tagsCode, msgStoreTime, filterBitMap, properties); + this.popMessageProcessor.notifyMessageArriving(topic, queueId); + this.notificationProcessor.notifyMessageArriving(topic, queueId); } } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PollingHeader.java b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PollingHeader.java new file mode 100644 index 00000000000..9f6774a0f3c --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PollingHeader.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.longpolling; + +import org.apache.rocketmq.remoting.protocol.header.NotificationRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PopMessageRequestHeader; + +public class PollingHeader { + private final String consumerGroup; + private final String topic; + private final int queueId; + private final long bornTime; + private final long pollTime; + + public PollingHeader(PopMessageRequestHeader requestHeader) { + this.consumerGroup = requestHeader.getConsumerGroup(); + this.topic = requestHeader.getTopic(); + this.queueId = requestHeader.getQueueId(); + this.bornTime = requestHeader.getBornTime(); + this.pollTime = requestHeader.getPollTime(); + } + + public PollingHeader(NotificationRequestHeader requestHeader) { + this.consumerGroup = requestHeader.getConsumerGroup(); + this.topic = requestHeader.getTopic(); + this.queueId = requestHeader.getQueueId(); + this.bornTime = requestHeader.getBornTime(); + this.pollTime = requestHeader.getPollTime(); + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public String getTopic() { + return topic; + } + + public int getQueueId() { + return queueId; + } + + public long getBornTime() { + return bornTime; + } + + public long getPollTime() { + return pollTime; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PollingResult.java b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PollingResult.java new file mode 100644 index 00000000000..6b7c4fa4a89 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PollingResult.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.longpolling; + +public enum PollingResult { + POLLING_SUC, + POLLING_FULL, + POLLING_TIMEOUT, + NOT_POLLING; +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PopLongPollingService.java b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PopLongPollingService.java new file mode 100644 index 00000000000..f1bc9adc463 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PopLongPollingService.java @@ -0,0 +1,343 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.longpolling; + +import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap; +import io.netty.channel.ChannelHandlerContext; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.KeyBuilder; +import org.apache.rocketmq.common.PopAckConstants; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.netty.NettyRemotingAbstract; +import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; +import org.apache.rocketmq.remoting.netty.RequestTask; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +import static org.apache.rocketmq.broker.longpolling.PollingResult.NOT_POLLING; +import static org.apache.rocketmq.broker.longpolling.PollingResult.POLLING_FULL; +import static org.apache.rocketmq.broker.longpolling.PollingResult.POLLING_SUC; +import static org.apache.rocketmq.broker.longpolling.PollingResult.POLLING_TIMEOUT; + +public class PopLongPollingService extends ServiceThread { + private static final Logger POP_LOGGER = + LoggerFactory.getLogger(LoggerName.ROCKETMQ_POP_LOGGER_NAME); + private final BrokerController brokerController; + private final NettyRequestProcessor processor; + private final ConcurrentHashMap> topicCidMap; + private final ConcurrentLinkedHashMap> pollingMap; + private long lastCleanTime = 0; + + private final AtomicLong totalPollingNum = new AtomicLong(0); + + public PopLongPollingService(BrokerController brokerController, NettyRequestProcessor processor) { + this.brokerController = brokerController; + this.processor = processor; + // 100000 topic default, 100000 lru topic + cid + qid + this.topicCidMap = new ConcurrentHashMap<>(brokerController.getBrokerConfig().getPopPollingMapSize()); + this.pollingMap = new ConcurrentLinkedHashMap.Builder>() + .maximumWeightedCapacity(this.brokerController.getBrokerConfig().getPopPollingMapSize()).build(); + } + + @Override + public String getServiceName() { + if (brokerController.getBrokerConfig().isInBrokerContainer()) { + return brokerController.getBrokerIdentity().getIdentifier() + PopLongPollingService.class.getSimpleName(); + } + return PopLongPollingService.class.getSimpleName(); + } + + @Override + public void run() { + int i = 0; + while (!this.stopped) { + try { + this.waitForRunning(20); + i++; + if (pollingMap.isEmpty()) { + continue; + } + long tmpTotalPollingNum = 0; + for (Map.Entry> entry : pollingMap.entrySet()) { + String key = entry.getKey(); + ConcurrentSkipListSet popQ = entry.getValue(); + if (popQ == null) { + continue; + } + PopRequest first; + do { + first = popQ.pollFirst(); + if (first == null) { + break; + } + if (!first.isTimeout()) { + if (popQ.add(first)) { + break; + } else { + POP_LOGGER.info("polling, add fail again: {}", first); + } + } + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("timeout , wakeUp polling : {}", first); + } + totalPollingNum.decrementAndGet(); + wakeUp(first); + } + while (true); + if (i >= 100) { + long tmpPollingNum = popQ.size(); + tmpTotalPollingNum = tmpTotalPollingNum + tmpPollingNum; + if (tmpPollingNum > 100) { + POP_LOGGER.info("polling queue {} , size={} ", key, tmpPollingNum); + } + } + } + + if (i >= 100) { + POP_LOGGER.info("pollingMapSize={},tmpTotalSize={},atomicTotalSize={},diffSize={}", + pollingMap.size(), tmpTotalPollingNum, totalPollingNum.get(), + Math.abs(totalPollingNum.get() - tmpTotalPollingNum)); + totalPollingNum.set(tmpTotalPollingNum); + i = 0; + } + + // clean unused + if (lastCleanTime == 0 || System.currentTimeMillis() - lastCleanTime > 5 * 60 * 1000) { + cleanUnusedResource(); + } + } catch (Throwable e) { + POP_LOGGER.error("checkPolling error", e); + } + } + // clean all; + try { + for (Map.Entry> entry : pollingMap.entrySet()) { + ConcurrentSkipListSet popQ = entry.getValue(); + PopRequest first; + while ((first = popQ.pollFirst()) != null) { + wakeUp(first); + } + } + } catch (Throwable e) { + } + } + + public void notifyMessageArrivingWithRetryTopic(final String topic, final int queueId) { + String notifyTopic; + if (KeyBuilder.isPopRetryTopicV2(topic)) { + notifyTopic = KeyBuilder.parseNormalTopic(topic); + } else { + notifyTopic = topic; + } + notifyMessageArriving(notifyTopic, queueId); + } + + public void notifyMessageArriving(final String topic, final int queueId) { + ConcurrentHashMap cids = topicCidMap.get(topic); + if (cids == null) { + return; + } + for (Map.Entry cid : cids.entrySet()) { + if (queueId >= 0) { + notifyMessageArriving(topic, cid.getKey(), -1); + } + notifyMessageArriving(topic, cid.getKey(), queueId); + } + } + + public boolean notifyMessageArriving(final String topic, final String cid, final int queueId) { + ConcurrentSkipListSet remotingCommands = pollingMap.get(KeyBuilder.buildPollingKey(topic, cid, queueId)); + if (remotingCommands == null || remotingCommands.isEmpty()) { + return false; + } + PopRequest popRequest = remotingCommands.pollFirst(); + //clean inactive channel + while (popRequest != null && !popRequest.getChannel().isActive()) { + totalPollingNum.decrementAndGet(); + popRequest = remotingCommands.pollFirst(); + } + + if (popRequest == null) { + return false; + } + totalPollingNum.decrementAndGet(); + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("lock release , new msg arrive , wakeUp : {}", popRequest); + } + return wakeUp(popRequest); + } + + public boolean wakeUp(final PopRequest request) { + if (request == null || !request.complete()) { + return false; + } + if (!request.getCtx().channel().isActive()) { + return false; + } + Runnable run = () -> { + try { + final RemotingCommand response = processor.processRequest(request.getCtx(), request.getRemotingCommand()); + if (response != null) { + response.setOpaque(request.getRemotingCommand().getOpaque()); + response.markResponseType(); + NettyRemotingAbstract.writeResponse(request.getChannel(), request.getRemotingCommand(), response, future -> { + if (!future.isSuccess()) { + POP_LOGGER.error("ProcessRequestWrapper response to {} failed", request.getChannel().remoteAddress(), future.cause()); + POP_LOGGER.error(request.toString()); + POP_LOGGER.error(response.toString()); + } + }); + } + } catch (Exception e1) { + POP_LOGGER.error("ExecuteRequestWhenWakeup run", e1); + } + }; + this.brokerController.getPullMessageExecutor().submit(new RequestTask(run, request.getChannel(), request.getRemotingCommand())); + return true; + } + + /** + * @param ctx + * @param remotingCommand + * @param requestHeader + * @return + */ + public PollingResult polling(final ChannelHandlerContext ctx, RemotingCommand remotingCommand, + final PollingHeader requestHeader) { + if (requestHeader.getPollTime() <= 0 || this.isStopped()) { + return NOT_POLLING; + } + ConcurrentHashMap cids = topicCidMap.get(requestHeader.getTopic()); + if (cids == null) { + cids = new ConcurrentHashMap<>(); + ConcurrentHashMap old = topicCidMap.putIfAbsent(requestHeader.getTopic(), cids); + if (old != null) { + cids = old; + } + } + cids.putIfAbsent(requestHeader.getConsumerGroup(), Byte.MIN_VALUE); + long expired = requestHeader.getBornTime() + requestHeader.getPollTime(); + final PopRequest request = new PopRequest(remotingCommand, ctx, expired); + boolean isFull = totalPollingNum.get() >= this.brokerController.getBrokerConfig().getMaxPopPollingSize(); + if (isFull) { + POP_LOGGER.info("polling {}, result POLLING_FULL, total:{}", remotingCommand, totalPollingNum.get()); + return POLLING_FULL; + } + boolean isTimeout = request.isTimeout(); + if (isTimeout) { + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("polling {}, result POLLING_TIMEOUT", remotingCommand); + } + return POLLING_TIMEOUT; + } + String key = KeyBuilder.buildPollingKey(requestHeader.getTopic(), requestHeader.getConsumerGroup(), + requestHeader.getQueueId()); + ConcurrentSkipListSet queue = pollingMap.get(key); + if (queue == null) { + queue = new ConcurrentSkipListSet<>(PopRequest.COMPARATOR); + ConcurrentSkipListSet old = pollingMap.putIfAbsent(key, queue); + if (old != null) { + queue = old; + } + } else { + // check size + int size = queue.size(); + if (size > brokerController.getBrokerConfig().getPopPollingSize()) { + POP_LOGGER.info("polling {}, result POLLING_FULL, singleSize:{}", remotingCommand, size); + return POLLING_FULL; + } + } + if (queue.add(request)) { + remotingCommand.setSuspended(true); + totalPollingNum.incrementAndGet(); + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("polling {}, result POLLING_SUC", remotingCommand); + } + return POLLING_SUC; + } else { + POP_LOGGER.info("polling {}, result POLLING_FULL, add fail, {}", request, queue); + return POLLING_FULL; + } + } + + public ConcurrentLinkedHashMap> getPollingMap() { + return pollingMap; + } + + private void cleanUnusedResource() { + try { + { + Iterator>> topicCidMapIter = topicCidMap.entrySet().iterator(); + while (topicCidMapIter.hasNext()) { + Map.Entry> entry = topicCidMapIter.next(); + String topic = entry.getKey(); + if (brokerController.getTopicConfigManager().selectTopicConfig(topic) == null) { + POP_LOGGER.info("remove not exit topic {} in topicCidMap!", topic); + topicCidMapIter.remove(); + continue; + } + Iterator> cidMapIter = entry.getValue().entrySet().iterator(); + while (cidMapIter.hasNext()) { + Map.Entry cidEntry = cidMapIter.next(); + String cid = cidEntry.getKey(); + if (!brokerController.getSubscriptionGroupManager().getSubscriptionGroupTable().containsKey(cid)) { + POP_LOGGER.info("remove not exit sub {} of topic {} in topicCidMap!", cid, topic); + cidMapIter.remove(); + } + } + } + } + + { + Iterator>> pollingMapIter = pollingMap.entrySet().iterator(); + while (pollingMapIter.hasNext()) { + Map.Entry> entry = pollingMapIter.next(); + if (entry.getKey() == null) { + continue; + } + String[] keyArray = entry.getKey().split(PopAckConstants.SPLIT); + if (keyArray.length != 3) { + continue; + } + String topic = keyArray[0]; + String cid = keyArray[1]; + if (brokerController.getTopicConfigManager().selectTopicConfig(topic) == null) { + POP_LOGGER.info("remove not exit topic {} in pollingMap!", topic); + pollingMapIter.remove(); + continue; + } + if (!brokerController.getSubscriptionGroupManager().getSubscriptionGroupTable().containsKey(cid)) { + POP_LOGGER.info("remove not exit sub {} of topic {} in pollingMap!", cid, topic); + pollingMapIter.remove(); + } + } + } + } catch (Throwable e) { + POP_LOGGER.error("cleanUnusedResource", e); + } + + lastCleanTime = System.currentTimeMillis(); + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PopRequest.java b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PopRequest.java new file mode 100644 index 00000000000..a45bcce9f60 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PopRequest.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.longpolling; + +import io.netty.channel.ChannelHandlerContext; +import java.util.Comparator; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +import io.netty.channel.Channel; + +public class PopRequest { + private static final AtomicLong COUNTER = new AtomicLong(Long.MIN_VALUE); + + private final RemotingCommand remotingCommand; + private final ChannelHandlerContext ctx; + private final long expired; + private final AtomicBoolean complete = new AtomicBoolean(false); + private final long op = COUNTER.getAndIncrement(); + + public PopRequest(RemotingCommand remotingCommand, ChannelHandlerContext ctx, long expired) { + this.ctx = ctx; + this.remotingCommand = remotingCommand; + this.expired = expired; + } + + public Channel getChannel() { + return ctx.channel(); + } + + public ChannelHandlerContext getCtx() { + return ctx; + } + + public RemotingCommand getRemotingCommand() { + return remotingCommand; + } + + public boolean isTimeout() { + return System.currentTimeMillis() > (expired - 50); + } + + public boolean complete() { + return complete.compareAndSet(false, true); + } + + public long getExpired() { + return expired; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("PopRequest{"); + sb.append("cmd=").append(remotingCommand); + sb.append(", ctx=").append(ctx); + sb.append(", expired=").append(expired); + sb.append(", complete=").append(complete); + sb.append(", op=").append(op); + sb.append('}'); + return sb.toString(); + } + + public static final Comparator COMPARATOR = (o1, o2) -> { + int ret = (int) (o1.getExpired() - o2.getExpired()); + + if (ret != 0) { + return ret; + } + ret = (int) (o1.op - o2.op); + if (ret != 0) { + return ret; + } + return -1; + }; +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PullRequest.java b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PullRequest.java index 045ab9b16a2..5e47105579d 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PullRequest.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PullRequest.java @@ -17,8 +17,8 @@ package org.apache.rocketmq.broker.longpolling; import io.netty.channel.Channel; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; import org.apache.rocketmq.store.MessageFilter; public class PullRequest { diff --git a/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PullRequestHoldService.java b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PullRequestHoldService.java index d0668cb45b0..e8da9d0c47c 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PullRequestHoldService.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/longpolling/PullRequestHoldService.java @@ -25,17 +25,17 @@ import org.apache.rocketmq.common.ServiceThread; import org.apache.rocketmq.common.SystemClock; import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.store.ConsumeQueueExt; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class PullRequestHoldService extends ServiceThread { private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); - private static final String TOPIC_QUEUEID_SEPARATOR = "@"; - private final BrokerController brokerController; + protected static final String TOPIC_QUEUEID_SEPARATOR = "@"; + protected final BrokerController brokerController; private final SystemClock systemClock = new SystemClock(); - private ConcurrentMap pullRequestTable = - new ConcurrentHashMap(1024); + protected ConcurrentMap pullRequestTable = + new ConcurrentHashMap<>(1024); public PullRequestHoldService(final BrokerController brokerController) { this.brokerController = brokerController; @@ -52,11 +52,12 @@ public void suspendPullRequest(final String topic, final int queueId, final Pull } } + pullRequest.getRequestCommand().setSuspended(true); mpr.addPullRequest(pullRequest); } private String buildKey(final String topic, final int queueId) { - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(topic.length() + 5); sb.append(topic); sb.append(TOPIC_QUEUEID_SEPARATOR); sb.append(queueId); @@ -78,7 +79,7 @@ public void run() { this.checkHoldRequest(); long costTime = this.systemClock.now() - beginLockTimestamp; if (costTime > 5 * 1000) { - log.info("[NOTIFYME] check hold request cost {} ms.", costTime); + log.warn("PullRequestHoldService: check hold pull request cost {}ms", costTime); } } catch (Throwable e) { log.warn(this.getServiceName() + " service has exception. ", e); @@ -90,10 +91,13 @@ public void run() { @Override public String getServiceName() { + if (brokerController != null && brokerController.getBrokerConfig().isInBrokerContainer()) { + return this.brokerController.getBrokerIdentity().getIdentifier() + PullRequestHoldService.class.getSimpleName(); + } return PullRequestHoldService.class.getSimpleName(); } - private void checkHoldRequest() { + protected void checkHoldRequest() { for (String key : this.pullRequestTable.keySet()) { String[] kArray = key.split(TOPIC_QUEUEID_SEPARATOR); if (2 == kArray.length) { @@ -103,7 +107,9 @@ private void checkHoldRequest() { try { this.notifyMessageArriving(topic, queueId, offset); } catch (Throwable e) { - log.error("check hold request failed. topic={}, queueId={}", topic, queueId, e); + log.error( + "PullRequestHoldService: failed to check hold request failed, topic={}, queueId={}", topic, + queueId, e); } } } @@ -120,7 +126,7 @@ public void notifyMessageArriving(final String topic, final int queueId, final l if (mpr != null) { List requestList = mpr.cloneListAndClear(); if (requestList != null) { - List replayList = new ArrayList(); + List replayList = new ArrayList<>(); for (PullRequest request : requestList) { long newestOffset = maxOffset; @@ -141,7 +147,9 @@ public void notifyMessageArriving(final String topic, final int queueId, final l this.brokerController.getPullMessageProcessor().executeRequestWhenWakeup(request.getClientChannel(), request.getRequestCommand()); } catch (Throwable e) { - log.error("execute request when wakeup failed.", e); + log.error( + "PullRequestHoldService#notifyMessageArriving: failed to execute request when " + + "message matched, topic={}, queueId={}", topic, queueId, e); } continue; } @@ -152,7 +160,9 @@ public void notifyMessageArriving(final String topic, final int queueId, final l this.brokerController.getPullMessageProcessor().executeRequestWhenWakeup(request.getClientChannel(), request.getRequestCommand()); } catch (Throwable e) { - log.error("execute request when wakeup failed.", e); + log.error( + "PullRequestHoldService#notifyMessageArriving: failed to execute request when time's " + + "up, topic={}, queueId={}", topic, queueId, e); } continue; } @@ -166,4 +176,22 @@ public void notifyMessageArriving(final String topic, final int queueId, final l } } } + + public void notifyMasterOnline() { + for (ManyPullRequest mpr : this.pullRequestTable.values()) { + if (mpr == null || mpr.isEmpty()) { + continue; + } + for (PullRequest request : mpr.cloneListAndClear()) { + try { + log.info("notify master online, wakeup {} {}", request.getClientChannel(), request.getRequestCommand()); + this.brokerController.getPullMessageProcessor().executeRequestWhenWakeup(request.getClientChannel(), + request.getRequestCommand()); + } catch (Throwable e) { + log.error("execute request when master online failed.", e); + } + } + } + + } } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/metrics/BrokerMetricsConstant.java b/broker/src/main/java/org/apache/rocketmq/broker/metrics/BrokerMetricsConstant.java new file mode 100644 index 00000000000..5733aa40bac --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/metrics/BrokerMetricsConstant.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.metrics; + +public class BrokerMetricsConstant { + public static final String OPEN_TELEMETRY_METER_NAME = "broker-meter"; + + public static final String GAUGE_PROCESSOR_WATERMARK = "rocketmq_processor_watermark"; + public static final String GAUGE_BROKER_PERMISSION = "rocketmq_broker_permission"; + + public static final String COUNTER_MESSAGES_IN_TOTAL = "rocketmq_messages_in_total"; + public static final String COUNTER_MESSAGES_OUT_TOTAL = "rocketmq_messages_out_total"; + public static final String COUNTER_THROUGHPUT_IN_TOTAL = "rocketmq_throughput_in_total"; + public static final String COUNTER_THROUGHPUT_OUT_TOTAL = "rocketmq_throughput_out_total"; + public static final String HISTOGRAM_MESSAGE_SIZE = "rocketmq_message_size"; + + public static final String GAUGE_PRODUCER_CONNECTIONS = "rocketmq_producer_connections"; + public static final String GAUGE_CONSUMER_CONNECTIONS = "rocketmq_consumer_connections"; + + public static final String GAUGE_CONSUMER_LAG_MESSAGES = "rocketmq_consumer_lag_messages"; + public static final String GAUGE_CONSUMER_LAG_LATENCY = "rocketmq_consumer_lag_latency"; + public static final String GAUGE_CONSUMER_INFLIGHT_MESSAGES = "rocketmq_consumer_inflight_messages"; + public static final String GAUGE_CONSUMER_QUEUEING_LATENCY = "rocketmq_consumer_queueing_latency"; + public static final String GAUGE_CONSUMER_READY_MESSAGES = "rocketmq_consumer_ready_messages"; + public static final String COUNTER_CONSUMER_SEND_TO_DLQ_MESSAGES_TOTAL = "rocketmq_send_to_dlq_messages_total"; + + public static final String COUNTER_COMMIT_MESSAGES_TOTAL = "rocketmq_commit_messages_total"; + public static final String COUNTER_ROLLBACK_MESSAGES_TOTAL = "rocketmq_rollback_messages_total"; + public static final String HISTOGRAM_FINISH_MSG_LATENCY = "rocketmq_finish_message_latency"; + public static final String GAUGE_HALF_MESSAGES = "rocketmq_half_messages"; + + public static final String LABEL_CLUSTER_NAME = "cluster"; + public static final String LABEL_NODE_TYPE = "node_type"; + public static final String NODE_TYPE_BROKER = "broker"; + public static final String LABEL_NODE_ID = "node_id"; + public static final String LABEL_AGGREGATION = "aggregation"; + public static final String AGGREGATION_DELTA = "delta"; + public static final String LABEL_PROCESSOR = "processor"; + + public static final String LABEL_TOPIC = "topic"; + public static final String LABEL_IS_RETRY = "is_retry"; + public static final String LABEL_IS_SYSTEM = "is_system"; + public static final String LABEL_CONSUMER_GROUP = "consumer_group"; + public static final String LABEL_MESSAGE_TYPE = "message_type"; + public static final String LABEL_LANGUAGE = "language"; + public static final String LABEL_VERSION = "version"; + public static final String LABEL_CONSUME_MODE = "consume_mode"; +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/metrics/BrokerMetricsManager.java b/broker/src/main/java/org/apache/rocketmq/broker/metrics/BrokerMetricsManager.java new file mode 100644 index 00000000000..fc7e97bda95 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/metrics/BrokerMetricsManager.java @@ -0,0 +1,644 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.metrics; + +import com.google.common.base.Splitter; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.ObservableLongGauge; +import io.opentelemetry.exporter.logging.otlp.OtlpJsonLoggingMetricExporter; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporterBuilder; +import io.opentelemetry.exporter.prometheus.PrometheusHttpServer; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.metrics.Aggregation; +import io.opentelemetry.sdk.metrics.InstrumentSelector; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; +import io.opentelemetry.sdk.metrics.View; +import io.opentelemetry.sdk.metrics.ViewBuilder; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; +import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil; +import io.opentelemetry.sdk.resources.Resource; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.client.ConsumerManager; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.attribute.TopicMessageType; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.metrics.MetricsExporterType; +import org.apache.rocketmq.common.metrics.NopLongCounter; +import org.apache.rocketmq.common.metrics.NopLongHistogram; +import org.apache.rocketmq.common.metrics.NopObservableLongGauge; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.metrics.RemotingMetricsManager; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant; +import org.slf4j.bridge.SLF4JBridgeHandler; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.AGGREGATION_DELTA; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.COUNTER_COMMIT_MESSAGES_TOTAL; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.COUNTER_CONSUMER_SEND_TO_DLQ_MESSAGES_TOTAL; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.COUNTER_MESSAGES_IN_TOTAL; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.COUNTER_MESSAGES_OUT_TOTAL; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.COUNTER_ROLLBACK_MESSAGES_TOTAL; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.COUNTER_THROUGHPUT_IN_TOTAL; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.COUNTER_THROUGHPUT_OUT_TOTAL; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.GAUGE_BROKER_PERMISSION; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.GAUGE_CONSUMER_CONNECTIONS; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.GAUGE_CONSUMER_INFLIGHT_MESSAGES; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.GAUGE_CONSUMER_LAG_LATENCY; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.GAUGE_CONSUMER_LAG_MESSAGES; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.GAUGE_CONSUMER_QUEUEING_LATENCY; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.GAUGE_CONSUMER_READY_MESSAGES; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.GAUGE_HALF_MESSAGES; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.GAUGE_PROCESSOR_WATERMARK; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.GAUGE_PRODUCER_CONNECTIONS; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.HISTOGRAM_FINISH_MSG_LATENCY; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.HISTOGRAM_MESSAGE_SIZE; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_AGGREGATION; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_CLUSTER_NAME; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_CONSUMER_GROUP; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_CONSUME_MODE; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_IS_RETRY; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_IS_SYSTEM; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_LANGUAGE; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_NODE_ID; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_NODE_TYPE; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_PROCESSOR; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_TOPIC; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_VERSION; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.NODE_TYPE_BROKER; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.OPEN_TELEMETRY_METER_NAME; +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.LABEL_PROTOCOL_TYPE; +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.PROTOCOL_TYPE_REMOTING; + +public class BrokerMetricsManager { + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + + private final BrokerConfig brokerConfig; + private final MessageStore messageStore; + private final BrokerController brokerController; + private final ConsumerLagCalculator consumerLagCalculator; + private final static Map LABEL_MAP = new HashMap<>(); + private OtlpGrpcMetricExporter metricExporter; + private PeriodicMetricReader periodicMetricReader; + private PrometheusHttpServer prometheusHttpServer; + private MetricExporter loggingMetricExporter; + private Meter brokerMeter; + + public static Supplier attributesBuilderSupplier = Attributes::builder; + + // broker stats metrics + public static ObservableLongGauge processorWatermark = new NopObservableLongGauge(); + public static ObservableLongGauge brokerPermission = new NopObservableLongGauge(); + + // request metrics + public static LongCounter messagesInTotal = new NopLongCounter(); + public static LongCounter messagesOutTotal = new NopLongCounter(); + public static LongCounter throughputInTotal = new NopLongCounter(); + public static LongCounter throughputOutTotal = new NopLongCounter(); + public static LongHistogram messageSize = new NopLongHistogram(); + + // client connection metrics + public static ObservableLongGauge producerConnection = new NopObservableLongGauge(); + public static ObservableLongGauge consumerConnection = new NopObservableLongGauge(); + + // Lag metrics + public static ObservableLongGauge consumerLagMessages = new NopObservableLongGauge(); + public static ObservableLongGauge consumerLagLatency = new NopObservableLongGauge(); + public static ObservableLongGauge consumerInflightMessages = new NopObservableLongGauge(); + public static ObservableLongGauge consumerQueueingLatency = new NopObservableLongGauge(); + public static ObservableLongGauge consumerReadyMessages = new NopObservableLongGauge(); + public static LongCounter sendToDlqMessages = new NopLongCounter(); + public static ObservableLongGauge halfMessages = new NopObservableLongGauge(); + public static LongCounter commitMessagesTotal = new NopLongCounter(); + public static LongCounter rollBackMessagesTotal = new NopLongCounter(); + public static LongHistogram transactionFinishLatency = new NopLongHistogram(); + + public static final List SYSTEM_GROUP_PREFIX_LIST = new ArrayList() { + { + add(MixAll.CID_RMQ_SYS_PREFIX.toLowerCase()); + } + }; + + public BrokerMetricsManager(BrokerController brokerController) { + this.brokerController = brokerController; + brokerConfig = brokerController.getBrokerConfig(); + this.messageStore = brokerController.getMessageStore(); + this.consumerLagCalculator = new ConsumerLagCalculator(brokerController); + init(); + } + + public static AttributesBuilder newAttributesBuilder() { + AttributesBuilder attributesBuilder; + if (attributesBuilderSupplier == null) { + attributesBuilderSupplier = Attributes::builder; + } + attributesBuilder = attributesBuilderSupplier.get(); + LABEL_MAP.forEach(attributesBuilder::put); + return attributesBuilder; + } + + private Attributes buildLagAttributes(ConsumerLagCalculator.BaseCalculateResult result) { + AttributesBuilder attributesBuilder = newAttributesBuilder(); + attributesBuilder.put(LABEL_CONSUMER_GROUP, result.group); + attributesBuilder.put(LABEL_TOPIC, result.topic); + attributesBuilder.put(LABEL_IS_RETRY, result.isRetry); + attributesBuilder.put(LABEL_IS_SYSTEM, isSystem(result.topic, result.group)); + return attributesBuilder.build(); + } + + public static boolean isRetryOrDlqTopic(String topic) { + if (StringUtils.isBlank(topic)) { + return false; + } + return topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX) || topic.startsWith(MixAll.DLQ_GROUP_TOPIC_PREFIX); + } + + public static boolean isSystemGroup(String group) { + if (StringUtils.isBlank(group)) { + return false; + } + String groupInLowerCase = group.toLowerCase(); + for (String prefix : SYSTEM_GROUP_PREFIX_LIST) { + if (groupInLowerCase.startsWith(prefix)) { + return true; + } + } + return false; + } + + public static boolean isSystem(String topic, String group) { + return TopicValidator.isSystemTopic(topic) || isSystemGroup(group); + } + + public static TopicMessageType getMessageType(SendMessageRequestHeader requestHeader) { + Map properties = MessageDecoder.string2messageProperties(requestHeader.getProperties()); + String traFlag = properties.get(MessageConst.PROPERTY_TRANSACTION_PREPARED); + TopicMessageType topicMessageType = TopicMessageType.NORMAL; + if (Boolean.parseBoolean(traFlag)) { + topicMessageType = TopicMessageType.TRANSACTION; + } else if (properties.containsKey(MessageConst.PROPERTY_SHARDING_KEY)) { + topicMessageType = TopicMessageType.FIFO; + } else if (properties.get("__STARTDELIVERTIME") != null + || properties.get(MessageConst.PROPERTY_DELAY_TIME_LEVEL) != null + || properties.get(MessageConst.PROPERTY_TIMER_DELIVER_MS) != null + || properties.get(MessageConst.PROPERTY_TIMER_DELAY_SEC) != null + || properties.get(MessageConst.PROPERTY_TIMER_DELAY_MS) != null) { + topicMessageType = TopicMessageType.DELAY; + } + return topicMessageType; + } + + public Meter getBrokerMeter() { + return brokerMeter; + } + + private boolean checkConfig() { + if (brokerConfig == null) { + return false; + } + MetricsExporterType exporterType = brokerConfig.getMetricsExporterType(); + if (!exporterType.isEnable()) { + return false; + } + + switch (exporterType) { + case OTLP_GRPC: + return StringUtils.isNotBlank(brokerConfig.getMetricsGrpcExporterTarget()); + case PROM: + return true; + case LOG: + return true; + } + return false; + } + + private void init() { + MetricsExporterType metricsExporterType = brokerConfig.getMetricsExporterType(); + if (metricsExporterType == MetricsExporterType.DISABLE) { + return; + } + + if (!checkConfig()) { + LOGGER.error("check metrics config failed, will not export metrics"); + return; + } + + String labels = brokerConfig.getMetricsLabel(); + if (StringUtils.isNotBlank(labels)) { + List kvPairs = Splitter.on(',').omitEmptyStrings().splitToList(labels); + for (String item : kvPairs) { + String[] split = item.split(":"); + if (split.length != 2) { + LOGGER.warn("metricsLabel is not valid: {}", labels); + continue; + } + LABEL_MAP.put(split[0], split[1]); + } + } + if (brokerConfig.isMetricsInDelta()) { + LABEL_MAP.put(LABEL_AGGREGATION, AGGREGATION_DELTA); + } + LABEL_MAP.put(LABEL_NODE_TYPE, NODE_TYPE_BROKER); + LABEL_MAP.put(LABEL_CLUSTER_NAME, brokerConfig.getBrokerClusterName()); + LABEL_MAP.put(LABEL_NODE_ID, brokerConfig.getBrokerName()); + + SdkMeterProviderBuilder providerBuilder = SdkMeterProvider.builder() + .setResource(Resource.empty()); + + if (metricsExporterType == MetricsExporterType.OTLP_GRPC) { + String endpoint = brokerConfig.getMetricsGrpcExporterTarget(); + if (!endpoint.startsWith("http")) { + endpoint = "https://" + endpoint; + } + OtlpGrpcMetricExporterBuilder metricExporterBuilder = OtlpGrpcMetricExporter.builder() + .setEndpoint(endpoint) + .setTimeout(brokerConfig.getMetricGrpcExporterTimeOutInMills(), TimeUnit.MILLISECONDS) + .setAggregationTemporalitySelector(type -> { + if (brokerConfig.isMetricsInDelta() && + (type == InstrumentType.COUNTER || type == InstrumentType.OBSERVABLE_COUNTER || type == InstrumentType.HISTOGRAM)) { + return AggregationTemporality.DELTA; + } + return AggregationTemporality.CUMULATIVE; + }); + + String headers = brokerConfig.getMetricsGrpcExporterHeader(); + if (StringUtils.isNotBlank(headers)) { + Map headerMap = new HashMap<>(); + List kvPairs = Splitter.on(',').omitEmptyStrings().splitToList(headers); + for (String item : kvPairs) { + String[] split = item.split(":"); + if (split.length != 2) { + LOGGER.warn("metricsGrpcExporterHeader is not valid: {}", headers); + continue; + } + headerMap.put(split[0], split[1]); + } + headerMap.forEach(metricExporterBuilder::addHeader); + } + + metricExporter = metricExporterBuilder.build(); + + periodicMetricReader = PeriodicMetricReader.builder(metricExporter) + .setInterval(brokerConfig.getMetricGrpcExporterIntervalInMills(), TimeUnit.MILLISECONDS) + .build(); + + providerBuilder.registerMetricReader(periodicMetricReader); + } + + if (metricsExporterType == MetricsExporterType.PROM) { + String promExporterHost = brokerConfig.getMetricsPromExporterHost(); + if (StringUtils.isBlank(promExporterHost)) { + promExporterHost = brokerConfig.getBrokerIP1(); + } + prometheusHttpServer = PrometheusHttpServer.builder() + .setHost(promExporterHost) + .setPort(brokerConfig.getMetricsPromExporterPort()) + .build(); + providerBuilder.registerMetricReader(prometheusHttpServer); + } + + if (metricsExporterType == MetricsExporterType.LOG) { + SLF4JBridgeHandler.removeHandlersForRootLogger(); + SLF4JBridgeHandler.install(); + loggingMetricExporter = OtlpJsonLoggingMetricExporter.create(brokerConfig.isMetricsInDelta() ? AggregationTemporality.DELTA : AggregationTemporality.CUMULATIVE); + java.util.logging.Logger.getLogger(OtlpJsonLoggingMetricExporter.class.getName()).setLevel(java.util.logging.Level.FINEST); + periodicMetricReader = PeriodicMetricReader.builder(loggingMetricExporter) + .setInterval(brokerConfig.getMetricLoggingExporterIntervalInMills(), TimeUnit.MILLISECONDS) + .build(); + providerBuilder.registerMetricReader(periodicMetricReader); + } + + registerMetricsView(providerBuilder); + + brokerMeter = OpenTelemetrySdk.builder() + .setMeterProvider(providerBuilder.build()) + .build() + .getMeter(OPEN_TELEMETRY_METER_NAME); + + initStatsMetrics(); + initRequestMetrics(); + initConnectionMetrics(); + initLagAndDlqMetrics(); + initTransactionMetrics(); + initOtherMetrics(); + } + + private void registerMetricsView(SdkMeterProviderBuilder providerBuilder) { + // message size buckets, 1k, 4k, 512k, 1M, 2M, 4M + List messageSizeBuckets = Arrays.asList( + 1d * 1024, //1KB + 4d * 1024, //4KB + 512d * 1024, //512KB + 1d * 1024 * 1024, //1MB + 2d * 1024 * 1024, //2MB + 4d * 1024 * 1024 //4MB + ); + + List commitLatencyBuckets = Arrays.asList( + 1d * 1 * 1 * 5, //5s + 1d * 1 * 1 * 60, //1min + 1d * 1 * 10 * 60, //10min + 1d * 1 * 60 * 60, //1h + 1d * 12 * 60 * 60, //12h + 1d * 24 * 60 * 60 //24h + ); + InstrumentSelector messageSizeSelector = InstrumentSelector.builder() + .setType(InstrumentType.HISTOGRAM) + .setName(HISTOGRAM_MESSAGE_SIZE) + .build(); + ViewBuilder messageSizeViewBuilder = View.builder() + .setAggregation(Aggregation.explicitBucketHistogram(messageSizeBuckets)); + // To config the cardinalityLimit for openTelemetry metrics exporting. + SdkMeterProviderUtil.setCardinalityLimit(messageSizeViewBuilder, brokerConfig.getMetricsOtelCardinalityLimit()); + providerBuilder.registerView(messageSizeSelector, messageSizeViewBuilder.build()); + + InstrumentSelector commitLatencySelector = InstrumentSelector.builder() + .setType(InstrumentType.HISTOGRAM) + .setName(HISTOGRAM_FINISH_MSG_LATENCY) + .build(); + ViewBuilder commitLatencyViewBuilder = View.builder() + .setAggregation(Aggregation.explicitBucketHistogram(commitLatencyBuckets)); + // To config the cardinalityLimit for openTelemetry metrics exporting. + SdkMeterProviderUtil.setCardinalityLimit(commitLatencyViewBuilder, brokerConfig.getMetricsOtelCardinalityLimit()); + providerBuilder.registerView(commitLatencySelector, commitLatencyViewBuilder.build()); + + for (Pair selectorViewPair : RemotingMetricsManager.getMetricsView()) { + ViewBuilder viewBuilder = selectorViewPair.getObject2(); + SdkMeterProviderUtil.setCardinalityLimit(viewBuilder, brokerConfig.getMetricsOtelCardinalityLimit()); + providerBuilder.registerView(selectorViewPair.getObject1(), viewBuilder.build()); + } + + for (Pair selectorViewPair : messageStore.getMetricsView()) { + ViewBuilder viewBuilder = selectorViewPair.getObject2(); + SdkMeterProviderUtil.setCardinalityLimit(viewBuilder, brokerConfig.getMetricsOtelCardinalityLimit()); + providerBuilder.registerView(selectorViewPair.getObject1(), viewBuilder.build()); + } + + for (Pair selectorViewPair : PopMetricsManager.getMetricsView()) { + ViewBuilder viewBuilder = selectorViewPair.getObject2(); + SdkMeterProviderUtil.setCardinalityLimit(viewBuilder, brokerConfig.getMetricsOtelCardinalityLimit()); + providerBuilder.registerView(selectorViewPair.getObject1(), viewBuilder.build()); + } + + // default view builder for all counter. + InstrumentSelector defaultCounterSelector = InstrumentSelector.builder() + .setType(InstrumentType.COUNTER) + .build(); + ViewBuilder defaultCounterViewBuilder = View.builder().setDescription("default view for counter."); + SdkMeterProviderUtil.setCardinalityLimit(defaultCounterViewBuilder, brokerConfig.getMetricsOtelCardinalityLimit()); + providerBuilder.registerView(defaultCounterSelector, defaultCounterViewBuilder.build()); + + //default view builder for all observable gauge. + InstrumentSelector defaultGaugeSelector = InstrumentSelector.builder() + .setType(InstrumentType.OBSERVABLE_GAUGE) + .build(); + ViewBuilder defaultGaugeViewBuilder = View.builder().setDescription("default view for gauge."); + SdkMeterProviderUtil.setCardinalityLimit(defaultGaugeViewBuilder, brokerConfig.getMetricsOtelCardinalityLimit()); + providerBuilder.registerView(defaultGaugeSelector, defaultGaugeViewBuilder.build()); + } + + private void initStatsMetrics() { + processorWatermark = brokerMeter.gaugeBuilder(GAUGE_PROCESSOR_WATERMARK) + .setDescription("Request processor watermark") + .ofLongs() + .buildWithCallback(measurement -> { + measurement.record(brokerController.getSendThreadPoolQueue().size(), newAttributesBuilder().put(LABEL_PROCESSOR, "send").build()); + measurement.record(brokerController.getAsyncPutThreadPoolQueue().size(), newAttributesBuilder().put(LABEL_PROCESSOR, "async_put").build()); + measurement.record(brokerController.getPullThreadPoolQueue().size(), newAttributesBuilder().put(LABEL_PROCESSOR, "pull").build()); + measurement.record(brokerController.getAckThreadPoolQueue().size(), newAttributesBuilder().put(LABEL_PROCESSOR, "ack").build()); + measurement.record(brokerController.getQueryThreadPoolQueue().size(), newAttributesBuilder().put(LABEL_PROCESSOR, "query_message").build()); + measurement.record(brokerController.getClientManagerThreadPoolQueue().size(), newAttributesBuilder().put(LABEL_PROCESSOR, "client_manager").build()); + measurement.record(brokerController.getHeartbeatThreadPoolQueue().size(), newAttributesBuilder().put(LABEL_PROCESSOR, "heartbeat").build()); + measurement.record(brokerController.getLitePullThreadPoolQueue().size(), newAttributesBuilder().put(LABEL_PROCESSOR, "lite_pull").build()); + measurement.record(brokerController.getEndTransactionThreadPoolQueue().size(), newAttributesBuilder().put(LABEL_PROCESSOR, "transaction").build()); + measurement.record(brokerController.getConsumerManagerThreadPoolQueue().size(), newAttributesBuilder().put(LABEL_PROCESSOR, "consumer_manager").build()); + measurement.record(brokerController.getAdminBrokerThreadPoolQueue().size(), newAttributesBuilder().put(LABEL_PROCESSOR, "admin").build()); + measurement.record(brokerController.getReplyThreadPoolQueue().size(), newAttributesBuilder().put(LABEL_PROCESSOR, "reply").build()); + }); + + brokerPermission = brokerMeter.gaugeBuilder(GAUGE_BROKER_PERMISSION) + .setDescription("Broker permission") + .ofLongs() + .buildWithCallback(measurement -> measurement.record(brokerConfig.getBrokerPermission(), newAttributesBuilder().build())); + } + + private void initRequestMetrics() { + messagesInTotal = brokerMeter.counterBuilder(COUNTER_MESSAGES_IN_TOTAL) + .setDescription("Total number of incoming messages") + .build(); + + messagesOutTotal = brokerMeter.counterBuilder(COUNTER_MESSAGES_OUT_TOTAL) + .setDescription("Total number of outgoing messages") + .build(); + + throughputInTotal = brokerMeter.counterBuilder(COUNTER_THROUGHPUT_IN_TOTAL) + .setDescription("Total traffic of incoming messages") + .build(); + + throughputOutTotal = brokerMeter.counterBuilder(COUNTER_THROUGHPUT_OUT_TOTAL) + .setDescription("Total traffic of outgoing messages") + .build(); + + messageSize = brokerMeter.histogramBuilder(HISTOGRAM_MESSAGE_SIZE) + .setDescription("Incoming messages size") + .ofLongs() + .build(); + } + + private void initConnectionMetrics() { + producerConnection = brokerMeter.gaugeBuilder(GAUGE_PRODUCER_CONNECTIONS) + .setDescription("Producer connections") + .ofLongs() + .buildWithCallback(measurement -> { + Map metricsMap = new HashMap<>(); + brokerController.getProducerManager() + .getGroupChannelTable() + .values() + .stream() + .flatMap(map -> map.values().stream()) + .forEach(info -> { + ProducerAttr attr = new ProducerAttr(info.getLanguage(), info.getVersion()); + Integer count = metricsMap.computeIfAbsent(attr, k -> 0); + metricsMap.put(attr, count + 1); + }); + metricsMap.forEach((attr, count) -> { + Attributes attributes = newAttributesBuilder() + .put(LABEL_LANGUAGE, attr.language.name().toLowerCase()) + .put(LABEL_VERSION, MQVersion.getVersionDesc(attr.version).toLowerCase()) + .put(LABEL_PROTOCOL_TYPE, PROTOCOL_TYPE_REMOTING) + .build(); + measurement.record(count, attributes); + }); + }); + + consumerConnection = brokerMeter.gaugeBuilder(GAUGE_CONSUMER_CONNECTIONS) + .setDescription("Consumer connections") + .ofLongs() + .buildWithCallback(measurement -> { + Map metricsMap = new HashMap<>(); + ConsumerManager consumerManager = brokerController.getConsumerManager(); + consumerManager.getConsumerTable() + .forEach((group, groupInfo) -> { + if (groupInfo != null) { + groupInfo.getChannelInfoTable().values().forEach(info -> { + ConsumerAttr attr = new ConsumerAttr(group, info.getLanguage(), info.getVersion(), groupInfo.getConsumeType()); + Integer count = metricsMap.computeIfAbsent(attr, k -> 0); + metricsMap.put(attr, count + 1); + }); + } + }); + metricsMap.forEach((attr, count) -> { + Attributes attributes = newAttributesBuilder() + .put(LABEL_CONSUMER_GROUP, attr.group) + .put(LABEL_LANGUAGE, attr.language.name().toLowerCase()) + .put(LABEL_VERSION, MQVersion.getVersionDesc(attr.version).toLowerCase()) + .put(LABEL_CONSUME_MODE, attr.consumeMode.getTypeCN().toLowerCase()) + .put(LABEL_PROTOCOL_TYPE, PROTOCOL_TYPE_REMOTING) + .put(LABEL_IS_SYSTEM, isSystemGroup(attr.group)) + .build(); + measurement.record(count, attributes); + }); + }); + } + + private void initLagAndDlqMetrics() { + consumerLagMessages = brokerMeter.gaugeBuilder(GAUGE_CONSUMER_LAG_MESSAGES) + .setDescription("Consumer lag messages") + .ofLongs() + .buildWithCallback(measurement -> + consumerLagCalculator.calculateLag(result -> measurement.record(result.lag, buildLagAttributes(result)))); + + consumerLagLatency = brokerMeter.gaugeBuilder(GAUGE_CONSUMER_LAG_LATENCY) + .setDescription("Consumer lag time") + .setUnit("milliseconds") + .ofLongs() + .buildWithCallback(measurement -> consumerLagCalculator.calculateLag(result -> { + long latency = 0; + long curTimeStamp = System.currentTimeMillis(); + if (result.earliestUnconsumedTimestamp != 0) { + latency = curTimeStamp - result.earliestUnconsumedTimestamp; + } + measurement.record(latency, buildLagAttributes(result)); + })); + + consumerInflightMessages = brokerMeter.gaugeBuilder(GAUGE_CONSUMER_INFLIGHT_MESSAGES) + .setDescription("Consumer inflight messages") + .ofLongs() + .buildWithCallback(measurement -> + consumerLagCalculator.calculateInflight(result -> measurement.record(result.inFlight, buildLagAttributes(result)))); + + consumerQueueingLatency = brokerMeter.gaugeBuilder(GAUGE_CONSUMER_QUEUEING_LATENCY) + .setDescription("Consumer queueing time") + .setUnit("milliseconds") + .ofLongs() + .buildWithCallback(measurement -> consumerLagCalculator.calculateInflight(result -> { + long latency = 0; + long curTimeStamp = System.currentTimeMillis(); + if (result.earliestUnPulledTimestamp != 0) { + latency = curTimeStamp - result.earliestUnPulledTimestamp; + } + measurement.record(latency, buildLagAttributes(result)); + })); + + consumerReadyMessages = brokerMeter.gaugeBuilder(GAUGE_CONSUMER_READY_MESSAGES) + .setDescription("Consumer ready messages") + .ofLongs() + .buildWithCallback(measurement -> + consumerLagCalculator.calculateAvailable(result -> measurement.record(result.available, buildLagAttributes(result)))); + + sendToDlqMessages = brokerMeter.counterBuilder(COUNTER_CONSUMER_SEND_TO_DLQ_MESSAGES_TOTAL) + .setDescription("Consumer send to DLQ messages") + .build(); + } + + private void initTransactionMetrics() { + commitMessagesTotal = brokerMeter.counterBuilder(COUNTER_COMMIT_MESSAGES_TOTAL) + .setDescription("Total number of commit messages") + .build(); + + rollBackMessagesTotal = brokerMeter.counterBuilder(COUNTER_ROLLBACK_MESSAGES_TOTAL) + .setDescription("Total number of rollback messages") + .build(); + + transactionFinishLatency = brokerMeter.histogramBuilder(HISTOGRAM_FINISH_MSG_LATENCY) + .setDescription("Transaction finish latency") + .ofLongs() + .setUnit("ms") + .build(); + + halfMessages = brokerMeter.gaugeBuilder(GAUGE_HALF_MESSAGES) + .setDescription("Half messages of all topics") + .ofLongs() + .buildWithCallback(measurement -> { + brokerController.getTransactionalMessageService().getTransactionMetrics().getTransactionCounts() + .forEach((topic, metric) -> { + measurement.record( + metric.getCount().get(), + newAttributesBuilder().put(DefaultStoreMetricsConstant.LABEL_TOPIC, topic).build() + ); + }); + }); + } + private void initOtherMetrics() { + RemotingMetricsManager.initMetrics(brokerMeter, BrokerMetricsManager::newAttributesBuilder); + messageStore.initMetrics(brokerMeter, BrokerMetricsManager::newAttributesBuilder); + PopMetricsManager.initMetrics(brokerMeter, brokerController, BrokerMetricsManager::newAttributesBuilder); + } + + public void shutdown() { + if (brokerConfig.getMetricsExporterType() == MetricsExporterType.OTLP_GRPC) { + periodicMetricReader.forceFlush(); + periodicMetricReader.shutdown(); + metricExporter.shutdown(); + } + if (brokerConfig.getMetricsExporterType() == MetricsExporterType.PROM) { + prometheusHttpServer.forceFlush(); + prometheusHttpServer.shutdown(); + } + if (brokerConfig.getMetricsExporterType() == MetricsExporterType.LOG) { + periodicMetricReader.forceFlush(); + periodicMetricReader.shutdown(); + loggingMetricExporter.shutdown(); + } + } + +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/metrics/ConsumerAttr.java b/broker/src/main/java/org/apache/rocketmq/broker/metrics/ConsumerAttr.java new file mode 100644 index 00000000000..28f36ccd3fa --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/metrics/ConsumerAttr.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.metrics; + +import com.google.common.base.Objects; +import org.apache.rocketmq.remoting.protocol.LanguageCode; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; + +public class ConsumerAttr { + String group; + LanguageCode language; + int version; + ConsumeType consumeMode; + + public ConsumerAttr(String group, LanguageCode language, int version, ConsumeType consumeMode) { + this.group = group; + this.language = language; + this.version = version; + this.consumeMode = consumeMode; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ConsumerAttr attr = (ConsumerAttr) o; + return version == attr.version && Objects.equal(group, attr.group) && language == attr.language && consumeMode == attr.consumeMode; + } + + @Override + public int hashCode() { + return Objects.hashCode(group, language, version, consumeMode); + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/metrics/ConsumerLagCalculator.java b/broker/src/main/java/org/apache/rocketmq/broker/metrics/ConsumerLagCalculator.java new file mode 100644 index 00000000000..1930d0dfcb6 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/metrics/ConsumerLagCalculator.java @@ -0,0 +1,484 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.metrics; + +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.client.ConsumerGroupInfo; +import org.apache.rocketmq.broker.client.ConsumerManager; +import org.apache.rocketmq.broker.filter.ConsumerFilterData; +import org.apache.rocketmq.broker.filter.ConsumerFilterManager; +import org.apache.rocketmq.broker.filter.ExpressionMessageFilter; +import org.apache.rocketmq.broker.offset.ConsumerOffsetManager; +import org.apache.rocketmq.broker.processor.PopBufferMergeService; +import org.apache.rocketmq.broker.processor.PopInflightMessageCounter; +import org.apache.rocketmq.broker.subscription.SubscriptionGroupManager; +import org.apache.rocketmq.broker.topic.TopicConfigManager; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.KeyBuilder; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.constant.PermName; +import org.apache.rocketmq.common.filter.ExpressionType; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.filter.FilterAPI; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.remoting.protocol.subscription.SimpleSubscriptionData; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.store.DefaultMessageFilter; +import org.apache.rocketmq.store.MessageStore; + +public class ConsumerLagCalculator { + private final BrokerConfig brokerConfig; + private final TopicConfigManager topicConfigManager; + private final ConsumerManager consumerManager; + private final ConsumerOffsetManager offsetManager; + private final ConsumerFilterManager consumerFilterManager; + private final SubscriptionGroupManager subscriptionGroupManager; + private final MessageStore messageStore; + private final PopBufferMergeService popBufferMergeService; + private final PopInflightMessageCounter popInflightMessageCounter; + + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + + public ConsumerLagCalculator(BrokerController brokerController) { + this.brokerConfig = brokerController.getBrokerConfig(); + this.topicConfigManager = brokerController.getTopicConfigManager(); + this.consumerManager = brokerController.getConsumerManager(); + this.offsetManager = brokerController.getConsumerOffsetManager(); + this.consumerFilterManager = brokerController.getConsumerFilterManager(); + this.subscriptionGroupManager = brokerController.getSubscriptionGroupManager(); + this.messageStore = brokerController.getMessageStore(); + this.popBufferMergeService = brokerController.getPopMessageProcessor().getPopBufferMergeService(); + this.popInflightMessageCounter = brokerController.getPopInflightMessageCounter(); + } + + private static class ProcessGroupInfo { + public String group; + public String topic; + public boolean isPop; + public String retryTopic; + + public ProcessGroupInfo(String group, String topic, boolean isPop, + String retryTopic) { + this.group = group; + this.topic = topic; + this.isPop = isPop; + this.retryTopic = retryTopic; + } + } + + public static class BaseCalculateResult { + public String group; + public String topic; + public boolean isRetry; + + public BaseCalculateResult(String group, String topic, boolean isRetry) { + this.group = group; + this.topic = topic; + this.isRetry = isRetry; + } + } + + public static class CalculateLagResult extends BaseCalculateResult { + public long lag; + public long earliestUnconsumedTimestamp; + + public CalculateLagResult(String group, String topic, boolean isRetry) { + super(group, topic, isRetry); + } + } + + public static class CalculateInflightResult extends BaseCalculateResult { + public long inFlight; + public long earliestUnPulledTimestamp; + + public CalculateInflightResult(String group, String topic, boolean isRetry) { + super(group, topic, isRetry); + } + } + + public static class CalculateAvailableResult extends BaseCalculateResult { + public long available; + + public CalculateAvailableResult(String group, String topic, boolean isRetry) { + super(group, topic, isRetry); + } + } + + private void processAllGroup(Consumer consumer) { + for (Map.Entry subscriptionEntry : + subscriptionGroupManager.getSubscriptionGroupTable().entrySet()) { + + String group = subscriptionEntry.getKey(); + ConsumerGroupInfo consumerGroupInfo = consumerManager.getConsumerGroupInfo(group, true); + boolean isPop = false; + if (consumerGroupInfo != null) { + isPop = consumerGroupInfo.getConsumeType() == ConsumeType.CONSUME_POP; + } + Set topics; + if (brokerConfig.isUseStaticSubscription()) { + SubscriptionGroupConfig subscriptionGroupConfig = subscriptionEntry.getValue(); + if (subscriptionGroupConfig.getSubscriptionDataSet() == null || + subscriptionGroupConfig.getSubscriptionDataSet().isEmpty()) { + continue; + } + topics = subscriptionGroupConfig.getSubscriptionDataSet() + .stream() + .map(SimpleSubscriptionData::getTopic) + .collect(Collectors.toSet()); + } else { + if (consumerGroupInfo == null) { + continue; + } + topics = consumerGroupInfo.getSubscribeTopics(); + } + + if (null == topics || topics.isEmpty()) { + continue; + } + for (String topic : topics) { + // skip retry topic + if (topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + continue; + } + + TopicConfig topicConfig = topicConfigManager.selectTopicConfig(topic); + if (topicConfig == null) { + continue; + } + + // skip no perm topic + int topicPerm = topicConfig.getPerm() & brokerConfig.getBrokerPermission(); + if (!PermName.isReadable(topicPerm) && !PermName.isWriteable(topicPerm)) { + continue; + } + + if (isPop) { + String retryTopic = KeyBuilder.buildPopRetryTopic(topic, group, brokerConfig.isEnableRetryTopicV2()); + TopicConfig retryTopicConfig = topicConfigManager.selectTopicConfig(retryTopic); + if (retryTopicConfig != null) { + int retryTopicPerm = retryTopicConfig.getPerm() & brokerConfig.getBrokerPermission(); + if (PermName.isReadable(retryTopicPerm) || PermName.isWriteable(retryTopicPerm)) { + consumer.accept(new ProcessGroupInfo(group, topic, true, retryTopic)); + continue; + } + } + if (brokerConfig.isEnableRetryTopicV2() && brokerConfig.isRetrieveMessageFromPopRetryTopicV1()) { + String retryTopicV1 = KeyBuilder.buildPopRetryTopicV1(topic, group); + TopicConfig retryTopicConfigV1 = topicConfigManager.selectTopicConfig(retryTopicV1); + if (retryTopicConfigV1 != null) { + int retryTopicPerm = retryTopicConfigV1.getPerm() & brokerConfig.getBrokerPermission(); + if (PermName.isReadable(retryTopicPerm) || PermName.isWriteable(retryTopicPerm)) { + consumer.accept(new ProcessGroupInfo(group, topic, true, retryTopicV1)); + continue; + } + } + } + consumer.accept(new ProcessGroupInfo(group, topic, true, null)); + } else { + consumer.accept(new ProcessGroupInfo(group, topic, false, null)); + } + } + } + } + + public void calculateLag(Consumer lagRecorder) { + processAllGroup(info -> { + if (info.group == null || info.topic == null) { + return; + } + + CalculateLagResult result = new CalculateLagResult(info.group, info.topic, false); + + Pair lag = getConsumerLagStats(info.group, info.topic, info.isPop); + if (lag != null) { + result.lag = lag.getObject1(); + result.earliestUnconsumedTimestamp = lag.getObject2(); + } + lagRecorder.accept(result); + + if (info.isPop) { + Pair retryLag = getConsumerLagStats(info.group, info.retryTopic, true); + + result = new CalculateLagResult(info.group, info.topic, true); + if (retryLag != null) { + result.lag = retryLag.getObject1(); + result.earliestUnconsumedTimestamp = retryLag.getObject2(); + } + lagRecorder.accept(result); + } + }); + } + + public void calculateInflight(Consumer inflightRecorder) { + processAllGroup(info -> { + CalculateInflightResult result = new CalculateInflightResult(info.group, info.topic, false); + Pair inFlight = getInFlightMsgStats(info.group, info.topic, info.isPop); + if (inFlight != null) { + result.inFlight = inFlight.getObject1(); + result.earliestUnPulledTimestamp = inFlight.getObject2(); + } + inflightRecorder.accept(result); + + if (info.isPop) { + Pair retryInFlight = getInFlightMsgStats(info.group, info.retryTopic, true); + + result = new CalculateInflightResult(info.group, info.topic, true); + if (retryInFlight != null) { + result.inFlight = retryInFlight.getObject1(); + result.earliestUnPulledTimestamp = retryInFlight.getObject2(); + } + inflightRecorder.accept(result); + } + }); + } + + public void calculateAvailable(Consumer availableRecorder) { + processAllGroup(info -> { + CalculateAvailableResult result = new CalculateAvailableResult(info.group, info.topic, false); + + result.available = getAvailableMsgCount(info.group, info.topic, info.isPop); + availableRecorder.accept(result); + + if (info.isPop) { + long retryAvailable = getAvailableMsgCount(info.group, info.retryTopic, true); + + result = new CalculateAvailableResult(info.group, info.topic, true); + result.available = retryAvailable; + availableRecorder.accept(result); + } + }); + } + + public Pair getConsumerLagStats(String group, String topic, boolean isPop) { + long total = 0L; + long earliestUnconsumedTimestamp = Long.MAX_VALUE; + + if (group == null || topic == null) { + return new Pair<>(total, earliestUnconsumedTimestamp); + } + + TopicConfig topicConfig = topicConfigManager.selectTopicConfig(topic); + if (topicConfig != null) { + for (int queueId = 0; queueId < topicConfig.getWriteQueueNums(); queueId++) { + Pair pair = getConsumerLagStats(group, topic, queueId, isPop); + total += pair.getObject1(); + earliestUnconsumedTimestamp = Math.min(earliestUnconsumedTimestamp, pair.getObject2()); + } + } else { + LOGGER.warn("failed to get config of topic {}", topic); + } + + if (earliestUnconsumedTimestamp < 0 || earliestUnconsumedTimestamp == Long.MAX_VALUE) { + earliestUnconsumedTimestamp = 0L; + } + + return new Pair<>(total, earliestUnconsumedTimestamp); + } + + public Pair getConsumerLagStats(String group, String topic, int queueId, boolean isPop) { + long brokerOffset = messageStore.getMaxOffsetInQueue(topic, queueId); + if (brokerOffset < 0) { + brokerOffset = 0; + } + + if (isPop) { + long pullOffset = popBufferMergeService.getLatestOffset(topic, group, queueId); + if (pullOffset < 0) { + pullOffset = offsetManager.queryOffset(group, topic, queueId); + } + if (pullOffset < 0) { + pullOffset = brokerOffset; + } + long inFlightNum = popInflightMessageCounter.getGroupPopInFlightMessageNum(topic, group, queueId); + long lag = calculateMessageCount(group, topic, queueId, pullOffset, brokerOffset) + inFlightNum; + long consumerOffset = pullOffset - inFlightNum; + long consumerStoreTimeStamp = getStoreTimeStamp(topic, queueId, consumerOffset); + return new Pair<>(lag, consumerStoreTimeStamp); + } + + long consumerOffset = offsetManager.queryOffset(group, topic, queueId); + if (consumerOffset < 0) { + consumerOffset = brokerOffset; + } + + long lag = calculateMessageCount(group, topic, queueId, consumerOffset, brokerOffset); + long consumerStoreTimeStamp = getStoreTimeStamp(topic, queueId, consumerOffset); + return new Pair<>(lag, consumerStoreTimeStamp); + } + + public Pair getInFlightMsgStats(String group, String topic, boolean isPop) { + long total = 0L; + long earliestUnPulledTimestamp = Long.MAX_VALUE; + + if (group == null || topic == null) { + return new Pair<>(total, earliestUnPulledTimestamp); + } + + TopicConfig topicConfig = topicConfigManager.selectTopicConfig(topic); + if (topicConfig != null) { + for (int queueId = 0; queueId < topicConfig.getWriteQueueNums(); queueId++) { + Pair pair = getInFlightMsgStats(group, topic, queueId, isPop); + total += pair.getObject1(); + earliestUnPulledTimestamp = Math.min(earliestUnPulledTimestamp, pair.getObject2()); + } + } else { + LOGGER.warn("failed to get config of topic {}", topic); + } + + if (earliestUnPulledTimestamp < 0 || earliestUnPulledTimestamp == Long.MAX_VALUE) { + earliestUnPulledTimestamp = 0L; + } + + return new Pair<>(total, earliestUnPulledTimestamp); + } + + public Pair getInFlightMsgStats(String group, String topic, int queueId, boolean isPop) { + if (isPop) { + long inflight = popInflightMessageCounter.getGroupPopInFlightMessageNum(topic, group, queueId); + long pullOffset = popBufferMergeService.getLatestOffset(topic, group, queueId); + if (pullOffset < 0) { + pullOffset = offsetManager.queryOffset(group, topic, queueId); + } + if (pullOffset < 0) { + pullOffset = messageStore.getMaxOffsetInQueue(topic, queueId); + } + long pullStoreTimeStamp = getStoreTimeStamp(topic, queueId, pullOffset); + return new Pair<>(inflight, pullStoreTimeStamp); + } + + long pullOffset = offsetManager.queryPullOffset(group, topic, queueId); + if (pullOffset < 0) { + pullOffset = 0; + } + + long commitOffset = offsetManager.queryOffset(group, topic, queueId); + if (commitOffset < 0) { + commitOffset = pullOffset; + } + + long inflight = calculateMessageCount(group, topic, queueId, commitOffset, pullOffset); + long pullStoreTimeStamp = getStoreTimeStamp(topic, queueId, pullOffset); + return new Pair<>(inflight, pullStoreTimeStamp); + } + + public long getAvailableMsgCount(String group, String topic, boolean isPop) { + long total = 0L; + + if (group == null || topic == null) { + return total; + } + + TopicConfig topicConfig = topicConfigManager.selectTopicConfig(topic); + if (topicConfig != null) { + for (int queueId = 0; queueId < topicConfig.getWriteQueueNums(); queueId++) { + total += getAvailableMsgCount(group, topic, queueId, isPop); + } + } else { + LOGGER.warn("failed to get config of topic {}", topic); + } + + return total; + } + + public long getAvailableMsgCount(String group, String topic, int queueId, boolean isPop) { + long brokerOffset = messageStore.getMaxOffsetInQueue(topic, queueId); + if (brokerOffset < 0) { + brokerOffset = 0; + } + + long pullOffset; + if (isPop) { + pullOffset = popBufferMergeService.getLatestOffset(topic, group, queueId); + if (pullOffset < 0) { + pullOffset = offsetManager.queryOffset(group, topic, queueId); + } + if (pullOffset < 0) { + pullOffset = brokerOffset; + } + } else { + pullOffset = offsetManager.queryPullOffset(group, topic, queueId); + } + if (pullOffset < 0) { + pullOffset = brokerOffset; + } + + return calculateMessageCount(group, topic, queueId, pullOffset, brokerOffset); + } + + public long getStoreTimeStamp(String topic, int queueId, long offset) { + long storeTimeStamp = Long.MAX_VALUE; + if (offset >= 0) { + storeTimeStamp = messageStore.getMessageStoreTimeStamp(topic, queueId, offset); + storeTimeStamp = storeTimeStamp > 0 ? storeTimeStamp : Long.MAX_VALUE; + } + return storeTimeStamp; + } + + public long calculateMessageCount(String group, String topic, int queueId, long from, long to) { + long count = to - from; + + if (brokerConfig.isEstimateAccumulation() && to > from) { + SubscriptionData subscriptionData = null; + if (brokerConfig.isUseStaticSubscription()) { + SubscriptionGroupConfig subscriptionGroupConfig = subscriptionGroupManager.findSubscriptionGroupConfig(group); + if (subscriptionGroupConfig != null) { + for (SimpleSubscriptionData simpleSubscriptionData : subscriptionGroupConfig.getSubscriptionDataSet()) { + if (topic.equals(simpleSubscriptionData.getTopic())) { + try { + subscriptionData = FilterAPI.buildSubscriptionData(simpleSubscriptionData.getTopic(), + simpleSubscriptionData.getExpression(), simpleSubscriptionData.getExpressionType()); + } catch (Exception e) { + LOGGER.error("Try to build subscription for group:{}, topic:{} exception.", group, topic, e); + } + break; + } + } + } + } else { + ConsumerGroupInfo consumerGroupInfo = consumerManager.getConsumerGroupInfo(group, true); + if (consumerGroupInfo != null) { + subscriptionData = consumerGroupInfo.findSubscriptionData(topic); + } + } + + if (null != subscriptionData) { + if (ExpressionType.TAG.equalsIgnoreCase(subscriptionData.getExpressionType()) + && !SubscriptionData.SUB_ALL.equals(subscriptionData.getSubString())) { + count = messageStore.estimateMessageCount(topic, queueId, from, to, + new DefaultMessageFilter(subscriptionData)); + } else if (ExpressionType.SQL92.equalsIgnoreCase(subscriptionData.getExpressionType())) { + ConsumerFilterData consumerFilterData = consumerFilterManager.get(topic, group); + count = messageStore.estimateMessageCount(topic, queueId, from, to, + new ExpressionMessageFilter(subscriptionData, + consumerFilterData, + consumerFilterManager)); + } + } + + } + return count < 0 ? 0 : count; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/metrics/PopMetricsConstant.java b/broker/src/main/java/org/apache/rocketmq/broker/metrics/PopMetricsConstant.java new file mode 100644 index 00000000000..41917ed5066 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/metrics/PopMetricsConstant.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.metrics; + +public class PopMetricsConstant { + public static final String HISTOGRAM_POP_BUFFER_SCAN_TIME_CONSUME = "rocketmq_pop_buffer_scan_time_consume"; + public static final String COUNTER_POP_REVIVE_IN_MESSAGE_TOTAL = "rocketmq_pop_revive_in_message_total"; + public static final String COUNTER_POP_REVIVE_OUT_MESSAGE_TOTAL = "rocketmq_pop_revive_out_message_total"; + public static final String COUNTER_POP_REVIVE_RETRY_MESSAGES_TOTAL = "rocketmq_pop_revive_retry_messages_total"; + + public static final String GAUGE_POP_REVIVE_LAG = "rocketmq_pop_revive_lag"; + public static final String GAUGE_POP_REVIVE_LATENCY = "rocketmq_pop_revive_latency"; + public static final String GAUGE_POP_OFFSET_BUFFER_SIZE = "rocketmq_pop_offset_buffer_size"; + public static final String GAUGE_POP_CHECKPOINT_BUFFER_SIZE = "rocketmq_pop_checkpoint_buffer_size"; + + public static final String LABEL_REVIVE_MESSAGE_TYPE = "revive_message_type"; + public static final String LABEL_PUT_STATUS = "put_status"; + public static final String LABEL_QUEUE_ID = "queue_id"; +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/metrics/PopMetricsManager.java b/broker/src/main/java/org/apache/rocketmq/broker/metrics/PopMetricsManager.java new file mode 100644 index 00000000000..2de220da166 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/metrics/PopMetricsManager.java @@ -0,0 +1,213 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.metrics; + +import com.google.common.collect.Lists; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.ObservableLongMeasurement; +import io.opentelemetry.sdk.metrics.Aggregation; +import io.opentelemetry.sdk.metrics.InstrumentSelector; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.View; +import io.opentelemetry.sdk.metrics.ViewBuilder; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.function.Supplier; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.processor.PopBufferMergeService; +import org.apache.rocketmq.broker.processor.PopReviveService; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.metrics.NopLongCounter; +import org.apache.rocketmq.common.metrics.NopLongHistogram; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.pop.AckMsg; +import org.apache.rocketmq.store.pop.PopCheckPoint; + +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_CONSUMER_GROUP; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_TOPIC; +import static org.apache.rocketmq.broker.metrics.PopMetricsConstant.COUNTER_POP_REVIVE_IN_MESSAGE_TOTAL; +import static org.apache.rocketmq.broker.metrics.PopMetricsConstant.COUNTER_POP_REVIVE_OUT_MESSAGE_TOTAL; +import static org.apache.rocketmq.broker.metrics.PopMetricsConstant.COUNTER_POP_REVIVE_RETRY_MESSAGES_TOTAL; +import static org.apache.rocketmq.broker.metrics.PopMetricsConstant.GAUGE_POP_CHECKPOINT_BUFFER_SIZE; +import static org.apache.rocketmq.broker.metrics.PopMetricsConstant.GAUGE_POP_OFFSET_BUFFER_SIZE; +import static org.apache.rocketmq.broker.metrics.PopMetricsConstant.GAUGE_POP_REVIVE_LAG; +import static org.apache.rocketmq.broker.metrics.PopMetricsConstant.GAUGE_POP_REVIVE_LATENCY; +import static org.apache.rocketmq.broker.metrics.PopMetricsConstant.HISTOGRAM_POP_BUFFER_SCAN_TIME_CONSUME; +import static org.apache.rocketmq.broker.metrics.PopMetricsConstant.LABEL_PUT_STATUS; +import static org.apache.rocketmq.broker.metrics.PopMetricsConstant.LABEL_QUEUE_ID; +import static org.apache.rocketmq.broker.metrics.PopMetricsConstant.LABEL_REVIVE_MESSAGE_TYPE; + +public class PopMetricsManager { + public static Supplier attributesBuilderSupplier; + + private static LongHistogram popBufferScanTimeConsume = new NopLongHistogram(); + private static LongCounter popRevivePutTotal = new NopLongCounter(); + private static LongCounter popReviveGetTotal = new NopLongCounter(); + private static LongCounter popReviveRetryMessageTotal = new NopLongCounter(); + + public static List> getMetricsView() { + List rpcCostTimeBuckets = Arrays.asList( + (double) Duration.ofMillis(1).toMillis(), + (double) Duration.ofMillis(10).toMillis(), + (double) Duration.ofMillis(100).toMillis(), + (double) Duration.ofSeconds(1).toMillis(), + (double) Duration.ofSeconds(2).toMillis(), + (double) Duration.ofSeconds(3).toMillis() + ); + InstrumentSelector popBufferScanTimeConsumeSelector = InstrumentSelector.builder() + .setType(InstrumentType.HISTOGRAM) + .setName(HISTOGRAM_POP_BUFFER_SCAN_TIME_CONSUME) + .build(); + ViewBuilder popBufferScanTimeConsumeViewBuilder = View.builder() + .setAggregation(Aggregation.explicitBucketHistogram(rpcCostTimeBuckets)); + + return Lists.newArrayList(new Pair<>(popBufferScanTimeConsumeSelector, popBufferScanTimeConsumeViewBuilder)); + } + + public static void initMetrics(Meter meter, BrokerController brokerController, + Supplier attributesBuilderSupplier) { + PopMetricsManager.attributesBuilderSupplier = attributesBuilderSupplier; + + popBufferScanTimeConsume = meter.histogramBuilder(HISTOGRAM_POP_BUFFER_SCAN_TIME_CONSUME) + .setDescription("Time consuming of pop buffer scan") + .setUnit("milliseconds") + .ofLongs() + .build(); + popRevivePutTotal = meter.counterBuilder(COUNTER_POP_REVIVE_IN_MESSAGE_TOTAL) + .setDescription("Total number of put message to revive topic") + .build(); + popReviveGetTotal = meter.counterBuilder(COUNTER_POP_REVIVE_OUT_MESSAGE_TOTAL) + .setDescription("Total number of get message from revive topic") + .build(); + popReviveRetryMessageTotal = meter.counterBuilder(COUNTER_POP_REVIVE_RETRY_MESSAGES_TOTAL) + .setDescription("Total number of put message to pop retry topic") + .build(); + + meter.gaugeBuilder(GAUGE_POP_OFFSET_BUFFER_SIZE) + .setDescription("Time number of buffered offset") + .ofLongs() + .buildWithCallback(measurement -> calculatePopBufferOffsetSize(brokerController, measurement)); + meter.gaugeBuilder(GAUGE_POP_CHECKPOINT_BUFFER_SIZE) + .setDescription("The number of buffered checkpoint") + .ofLongs() + .buildWithCallback(measurement -> calculatePopBufferCkSize(brokerController, measurement)); + meter.gaugeBuilder(GAUGE_POP_REVIVE_LAG) + .setDescription("The processing lag of revive topic") + .setUnit("milliseconds") + .ofLongs() + .buildWithCallback(measurement -> calculatePopReviveLag(brokerController, measurement)); + meter.gaugeBuilder(GAUGE_POP_REVIVE_LATENCY) + .setDescription("The processing latency of revive topic") + .setUnit("milliseconds") + .ofLongs() + .buildWithCallback(measurement -> calculatePopReviveLatency(brokerController, measurement)); + } + + private static void calculatePopBufferOffsetSize(BrokerController brokerController, + ObservableLongMeasurement measurement) { + PopBufferMergeService popBufferMergeService = brokerController.getPopMessageProcessor().getPopBufferMergeService(); + measurement.record(popBufferMergeService.getOffsetTotalSize(), newAttributesBuilder().build()); + } + + private static void calculatePopBufferCkSize(BrokerController brokerController, + ObservableLongMeasurement measurement) { + PopBufferMergeService popBufferMergeService = brokerController.getPopMessageProcessor().getPopBufferMergeService(); + measurement.record(popBufferMergeService.getBufferedCKSize(), newAttributesBuilder().build()); + } + + private static void calculatePopReviveLatency(BrokerController brokerController, + ObservableLongMeasurement measurement) { + PopReviveService[] popReviveServices = brokerController.getAckMessageProcessor().getPopReviveServices(); + for (PopReviveService popReviveService : popReviveServices) { + measurement.record(popReviveService.getReviveBehindMillis(), newAttributesBuilder() + .put(LABEL_QUEUE_ID, popReviveService.getQueueId()) + .build()); + } + } + + private static void calculatePopReviveLag(BrokerController brokerController, + ObservableLongMeasurement measurement) { + PopReviveService[] popReviveServices = brokerController.getAckMessageProcessor().getPopReviveServices(); + for (PopReviveService popReviveService : popReviveServices) { + measurement.record(popReviveService.getReviveBehindMessages(), newAttributesBuilder() + .put(LABEL_QUEUE_ID, popReviveService.getQueueId()) + .build()); + } + } + + public static void incPopReviveAckPutCount(AckMsg ackMsg, PutMessageStatus status) { + incPopRevivePutCount(ackMsg.getConsumerGroup(), ackMsg.getTopic(), PopReviveMessageType.ACK, status, 1); + } + + public static void incPopReviveCkPutCount(PopCheckPoint checkPoint, PutMessageStatus status) { + incPopRevivePutCount(checkPoint.getCId(), checkPoint.getTopic(), PopReviveMessageType.CK, status, 1); + } + + public static void incPopRevivePutCount(String group, String topic, PopReviveMessageType messageType, + PutMessageStatus status, int num) { + Attributes attributes = newAttributesBuilder() + .put(LABEL_CONSUMER_GROUP, group) + .put(LABEL_TOPIC, topic) + .put(LABEL_REVIVE_MESSAGE_TYPE, messageType.name()) + .put(LABEL_PUT_STATUS, status.name()) + .build(); + popRevivePutTotal.add(num, attributes); + } + + public static void incPopReviveAckGetCount(AckMsg ackMsg, int queueId) { + incPopReviveGetCount(ackMsg.getConsumerGroup(), ackMsg.getTopic(), PopReviveMessageType.ACK, queueId, 1); + } + + public static void incPopReviveCkGetCount(PopCheckPoint checkPoint, int queueId) { + incPopReviveGetCount(checkPoint.getCId(), checkPoint.getTopic(), PopReviveMessageType.CK, queueId, 1); + } + + public static void incPopReviveGetCount(String group, String topic, PopReviveMessageType messageType, int queueId, + int num) { + AttributesBuilder builder = newAttributesBuilder(); + Attributes attributes = builder + .put(LABEL_CONSUMER_GROUP, group) + .put(LABEL_TOPIC, topic) + .put(LABEL_QUEUE_ID, queueId) + .put(LABEL_REVIVE_MESSAGE_TYPE, messageType.name()) + .build(); + popReviveGetTotal.add(num, attributes); + } + + public static void incPopReviveRetryMessageCount(PopCheckPoint checkPoint, PutMessageStatus status) { + AttributesBuilder builder = newAttributesBuilder(); + Attributes attributes = builder + .put(LABEL_CONSUMER_GROUP, checkPoint.getCId()) + .put(LABEL_TOPIC, checkPoint.getTopic()) + .put(LABEL_PUT_STATUS, status.name()) + .build(); + popReviveRetryMessageTotal.add(1, attributes); + } + + public static void recordPopBufferScanTimeConsume(long time) { + popBufferScanTimeConsume.record(time, newAttributesBuilder().build()); + } + + public static AttributesBuilder newAttributesBuilder() { + return attributesBuilderSupplier != null ? attributesBuilderSupplier.get() : Attributes.builder(); + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/metrics/PopReviveMessageType.java b/broker/src/main/java/org/apache/rocketmq/broker/metrics/PopReviveMessageType.java new file mode 100644 index 00000000000..3f6fe9c4750 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/metrics/PopReviveMessageType.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.metrics; + +public enum PopReviveMessageType { + CK, + ACK +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/metrics/ProducerAttr.java b/broker/src/main/java/org/apache/rocketmq/broker/metrics/ProducerAttr.java new file mode 100644 index 00000000000..d40aba2501a --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/metrics/ProducerAttr.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.metrics; + +import com.google.common.base.Objects; +import org.apache.rocketmq.remoting.protocol.LanguageCode; + +public class ProducerAttr { + LanguageCode language; + int version; + + public ProducerAttr(LanguageCode language, int version) { + this.language = language; + this.version = version; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ProducerAttr attr = (ProducerAttr) o; + return version == attr.version && language == attr.language; + } + + @Override + public int hashCode() { + return Objects.hashCode(language, version); + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/mqtrace/ConsumeMessageContext.java b/broker/src/main/java/org/apache/rocketmq/broker/mqtrace/ConsumeMessageContext.java index 5a0f5c05e90..ed7bfba06d6 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/mqtrace/ConsumeMessageContext.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/mqtrace/ConsumeMessageContext.java @@ -17,6 +17,8 @@ package org.apache.rocketmq.broker.mqtrace; import java.util.Map; + +import org.apache.rocketmq.common.TopicConfig; import org.apache.rocketmq.store.stats.BrokerStatsManager; public class ConsumeMessageContext { @@ -30,12 +32,22 @@ public class ConsumeMessageContext { private boolean success; private String status; private Object mqTraceContext; + private TopicConfig topicConfig; + + private String accountAuthType; + private String accountOwnerParent; + private String accountOwnerSelf; + private int rcvMsgNum; + private int rcvMsgSize; + private BrokerStatsManager.StatsType rcvStat; + private int commercialRcvMsgNum; private String commercialOwner; private BrokerStatsManager.StatsType commercialRcvStats; private int commercialRcvTimes; private int commercialRcvSize; + private String namespace; public String getConsumerGroup() { return consumerGroup; } @@ -108,6 +120,14 @@ public void setMqTraceContext(Object mqTraceContext) { this.mqTraceContext = mqTraceContext; } + public TopicConfig getTopicConfig() { + return topicConfig; + } + + public void setTopicConfig(TopicConfig topicConfig) { + this.topicConfig = topicConfig; + } + public int getBodyLength() { return bodyLength; } @@ -116,6 +136,62 @@ public void setBodyLength(int bodyLength) { this.bodyLength = bodyLength; } + public String getAccountAuthType() { + return accountAuthType; + } + + public void setAccountAuthType(String accountAuthType) { + this.accountAuthType = accountAuthType; + } + + public String getAccountOwnerParent() { + return accountOwnerParent; + } + + public void setAccountOwnerParent(String accountOwnerParent) { + this.accountOwnerParent = accountOwnerParent; + } + + public String getAccountOwnerSelf() { + return accountOwnerSelf; + } + + public void setAccountOwnerSelf(String accountOwnerSelf) { + this.accountOwnerSelf = accountOwnerSelf; + } + + public int getRcvMsgNum() { + return rcvMsgNum; + } + + public void setRcvMsgNum(int rcvMsgNum) { + this.rcvMsgNum = rcvMsgNum; + } + + public int getRcvMsgSize() { + return rcvMsgSize; + } + + public void setRcvMsgSize(int rcvMsgSize) { + this.rcvMsgSize = rcvMsgSize; + } + + public BrokerStatsManager.StatsType getRcvStat() { + return rcvStat; + } + + public void setRcvStat(BrokerStatsManager.StatsType rcvStat) { + this.rcvStat = rcvStat; + } + + public int getCommercialRcvMsgNum() { + return commercialRcvMsgNum; + } + + public void setCommercialRcvMsgNum(int commercialRcvMsgNum) { + this.commercialRcvMsgNum = commercialRcvMsgNum; + } + public String getCommercialOwner() { return commercialOwner; } @@ -147,4 +223,12 @@ public int getCommercialRcvSize() { public void setCommercialRcvSize(final int commercialRcvSize) { this.commercialRcvSize = commercialRcvSize; } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/mqtrace/SendMessageContext.java b/broker/src/main/java/org/apache/rocketmq/broker/mqtrace/SendMessageContext.java index 0833403011f..aaa84b720c1 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/mqtrace/SendMessageContext.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/mqtrace/SendMessageContext.java @@ -17,11 +17,16 @@ package org.apache.rocketmq.broker.mqtrace; import java.util.Properties; + import org.apache.rocketmq.common.message.MessageType; import org.apache.rocketmq.store.stats.BrokerStatsManager; public class SendMessageContext { + /** namespace */ + private String namespace; + /** producer group without namespace. */ private String producerGroup; + /** topic without namespace. */ private String topic; private String msgId; private String originMsgId; @@ -38,14 +43,38 @@ public class SendMessageContext { private String brokerRegionId; private String msgUniqueKey; private long bornTimeStamp; + private long requestTimeStamp; private MessageType msgType = MessageType.Trans_msg_Commit; + private boolean isSuccess = false; - //For Commercial + + /** + * Account Statistics + */ + private String accountAuthType; + private String accountOwnerParent; + private String accountOwnerSelf; + private int sendMsgNum; + private int sendMsgSize; + private BrokerStatsManager.StatsType sendStat; + private int commercialSendMsgNum; + + /** + * For Commercial + */ private String commercialOwner; private BrokerStatsManager.StatsType commercialSendStats; private int commercialSendSize; private int commercialSendTimes; + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + public boolean isSuccess() { return isSuccess; } @@ -78,6 +107,14 @@ public void setBornTimeStamp(final long bornTimeStamp) { this.bornTimeStamp = bornTimeStamp; } + public long getRequestTimeStamp() { + return requestTimeStamp; + } + + public void setRequestTimeStamp(long requestTimeStamp) { + this.requestTimeStamp = requestTimeStamp; + } + public String getBrokerRegionId() { return brokerRegionId; } @@ -206,10 +243,66 @@ public void setCommercialOwner(final String commercialOwner) { this.commercialOwner = commercialOwner; } + public String getAccountAuthType() { + return accountAuthType; + } + + public void setAccountAuthType(String accountAuthType) { + this.accountAuthType = accountAuthType; + } + + public String getAccountOwnerParent() { + return accountOwnerParent; + } + + public void setAccountOwnerParent(String accountOwnerParent) { + this.accountOwnerParent = accountOwnerParent; + } + + public String getAccountOwnerSelf() { + return accountOwnerSelf; + } + + public void setAccountOwnerSelf(String accountOwnerSelf) { + this.accountOwnerSelf = accountOwnerSelf; + } + + public int getSendMsgNum() { + return sendMsgNum; + } + + public void setSendMsgNum(int sendMsgNum) { + this.sendMsgNum = sendMsgNum; + } + + public int getSendMsgSize() { + return sendMsgSize; + } + + public void setSendMsgSize(int sendMsgSize) { + this.sendMsgSize = sendMsgSize; + } + + public BrokerStatsManager.StatsType getSendStat() { + return sendStat; + } + + public void setSendStat(BrokerStatsManager.StatsType sendStat) { + this.sendStat = sendStat; + } + public BrokerStatsManager.StatsType getCommercialSendStats() { return commercialSendStats; } + public int getCommercialSendMsgNum() { + return commercialSendMsgNum; + } + + public void setCommercialSendMsgNum(int commercialSendMsgNum) { + this.commercialSendMsgNum = commercialSendMsgNum; + } + public void setCommercialSendStats(final BrokerStatsManager.StatsType commercialSendStats) { this.commercialSendStats = commercialSendStats; } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/mqtrace/SendMessageHook.java b/broker/src/main/java/org/apache/rocketmq/broker/mqtrace/SendMessageHook.java index a74b6d66ce8..a89bace193e 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/mqtrace/SendMessageHook.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/mqtrace/SendMessageHook.java @@ -17,9 +17,9 @@ package org.apache.rocketmq.broker.mqtrace; public interface SendMessageHook { - public String hookName(); + String hookName(); - public void sendMessageBefore(final SendMessageContext context); + void sendMessageBefore(final SendMessageContext context); - public void sendMessageAfter(final SendMessageContext context); + void sendMessageAfter(final SendMessageContext context); } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/offset/BroadcastOffsetManager.java b/broker/src/main/java/org/apache/rocketmq/broker/offset/BroadcastOffsetManager.java new file mode 100644 index 00000000000..9896735dd1c --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/offset/BroadcastOffsetManager.java @@ -0,0 +1,242 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.offset; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.ServiceThread; + +/** + * manage the offset of broadcast. + * now, use this to support switch remoting client between proxy and broker + */ +public class BroadcastOffsetManager extends ServiceThread { + private static final String TOPIC_GROUP_SEPARATOR = "@"; + private final BrokerController brokerController; + private final BrokerConfig brokerConfig; + + /** + * k: topic@groupId + * v: the pull offset of all client of all queue + */ + protected final ConcurrentHashMap offsetStoreMap = + new ConcurrentHashMap<>(); + + public BroadcastOffsetManager(BrokerController brokerController) { + this.brokerController = brokerController; + this.brokerConfig = brokerController.getBrokerConfig(); + } + + public void updateOffset(String topic, String group, int queueId, long offset, String clientId, boolean fromProxy) { + BroadcastOffsetData broadcastOffsetData = offsetStoreMap.computeIfAbsent( + buildKey(topic, group), key -> new BroadcastOffsetData(topic, group)); + + broadcastOffsetData.clientOffsetStore.compute(clientId, (clientIdKey, broadcastTimedOffsetStore) -> { + if (broadcastTimedOffsetStore == null) { + broadcastTimedOffsetStore = new BroadcastTimedOffsetStore(fromProxy); + } + + broadcastTimedOffsetStore.timestamp = System.currentTimeMillis(); + broadcastTimedOffsetStore.fromProxy = fromProxy; + broadcastTimedOffsetStore.offsetStore.updateOffset(queueId, offset, true); + return broadcastTimedOffsetStore; + }); + } + + /** + * the time need init offset + * 1. client connect to proxy -> client connect to broker + * 2. client connect to broker -> client connect to proxy + * 3. client connect to proxy at the first time + * + * @return -1 means no init offset, use the queueOffset in pullRequestHeader + */ + public Long queryInitOffset(String topic, String groupId, int queueId, String clientId, long requestOffset, + boolean fromProxy) { + + BroadcastOffsetData broadcastOffsetData = offsetStoreMap.get(buildKey(topic, groupId)); + if (broadcastOffsetData == null) { + if (fromProxy && requestOffset < 0) { + return getOffset(null, topic, groupId, queueId); + } else { + return -1L; + } + } + + final AtomicLong offset = new AtomicLong(-1L); + broadcastOffsetData.clientOffsetStore.compute(clientId, (clientIdK, offsetStore) -> { + if (offsetStore == null) { + offsetStore = new BroadcastTimedOffsetStore(fromProxy); + } + + if (offsetStore.fromProxy && requestOffset < 0) { + // when from proxy and requestOffset is -1 + // means proxy need a init offset to pull message + offset.set(getOffset(offsetStore, topic, groupId, queueId)); + return offsetStore; + } + + if (offsetStore.fromProxy == fromProxy) { + return offsetStore; + } + + offset.set(getOffset(offsetStore, topic, groupId, queueId)); + return offsetStore; + }); + return offset.get(); + } + + private long getOffset(BroadcastTimedOffsetStore offsetStore, String topic, String groupId, int queueId) { + long storeOffset = -1; + if (offsetStore != null) { + storeOffset = offsetStore.offsetStore.readOffset(queueId); + } + if (storeOffset < 0) { + storeOffset = + brokerController.getConsumerOffsetManager().queryOffset(broadcastGroupId(groupId), topic, queueId); + } + if (storeOffset < 0) { + if (this.brokerController.getMessageStore().checkInMemByConsumeOffset(topic, queueId, 0, 1)) { + storeOffset = 0; + } else { + storeOffset = brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId, true); + } + } + return storeOffset; + } + + /** + * 1. scan expire offset + * 2. calculate the min offset of all client of one topic@group, + * and then commit consumer offset by group@broadcast + */ + protected void scanOffsetData() { + for (String k : offsetStoreMap.keySet()) { + BroadcastOffsetData broadcastOffsetData = offsetStoreMap.get(k); + if (broadcastOffsetData == null) { + continue; + } + + Map queueMinOffset = new HashMap<>(); + + for (String clientId : broadcastOffsetData.clientOffsetStore.keySet()) { + broadcastOffsetData.clientOffsetStore + .computeIfPresent(clientId, (clientIdKey, broadcastTimedOffsetStore) -> { + long interval = System.currentTimeMillis() - broadcastTimedOffsetStore.timestamp; + boolean clientIsOnline = brokerController.getConsumerManager().findChannel(broadcastOffsetData.group, clientId) != null; + if (clientIsOnline || interval < Duration.ofSeconds(brokerConfig.getBroadcastOffsetExpireSecond()).toMillis()) { + Set queueSet = broadcastTimedOffsetStore.offsetStore.queueList(); + for (Integer queue : queueSet) { + long offset = broadcastTimedOffsetStore.offsetStore.readOffset(queue); + offset = Math.min(queueMinOffset.getOrDefault(queue, offset), offset); + queueMinOffset.put(queue, offset); + } + } + if (clientIsOnline && interval >= Duration.ofSeconds(brokerConfig.getBroadcastOffsetExpireMaxSecond()).toMillis()) { + return null; + } + if (!clientIsOnline && interval >= Duration.ofSeconds(brokerConfig.getBroadcastOffsetExpireSecond()).toMillis()) { + return null; + } + return broadcastTimedOffsetStore; + }); + } + + offsetStoreMap.computeIfPresent(k, (key, broadcastOffsetDataVal) -> { + if (broadcastOffsetDataVal.clientOffsetStore.isEmpty()) { + return null; + } + return broadcastOffsetDataVal; + }); + + queueMinOffset.forEach((queueId, offset) -> + this.brokerController.getConsumerOffsetManager().commitOffset("BroadcastOffset", + broadcastGroupId(broadcastOffsetData.group), broadcastOffsetData.topic, queueId, offset)); + } + } + + private String buildKey(String topic, String group) { + return topic + TOPIC_GROUP_SEPARATOR + group; + } + + /** + * @param group group of users + * @return the groupId used to commit offset + */ + private static String broadcastGroupId(String group) { + return group + TOPIC_GROUP_SEPARATOR + "broadcast"; + } + + @Override + public String getServiceName() { + return "BroadcastOffsetManager"; + } + + @Override + public void run() { + while (!this.isStopped()) { + this.waitForRunning(Duration.ofSeconds(5).toMillis()); + } + } + + @Override + protected void onWaitEnd() { + this.scanOffsetData(); + } + + public static class BroadcastOffsetData { + private final String topic; + private final String group; + private final ConcurrentHashMap clientOffsetStore; + + public BroadcastOffsetData(String topic, String group) { + this.topic = topic; + this.group = group; + this.clientOffsetStore = new ConcurrentHashMap<>(); + } + } + + public static class BroadcastTimedOffsetStore { + + /** + * the timeStamp of last update occurred + */ + private volatile long timestamp; + + /** + * mark the offset of this client is updated by proxy or not + */ + private volatile boolean fromProxy; + + /** + * the pulled offset of each queue + */ + private final BroadcastOffsetStore offsetStore; + + public BroadcastTimedOffsetStore(boolean fromProxy) { + this.timestamp = System.currentTimeMillis(); + this.fromProxy = fromProxy; + this.offsetStore = new BroadcastOffsetStore(); + } + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/offset/BroadcastOffsetStore.java b/broker/src/main/java/org/apache/rocketmq/broker/offset/BroadcastOffsetStore.java new file mode 100644 index 00000000000..3770e576ac8 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/offset/BroadcastOffsetStore.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.offset; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.rocketmq.common.MixAll; + +public class BroadcastOffsetStore { + + private final ConcurrentMap offsetTable = new ConcurrentHashMap<>(); + + public void updateOffset(int queueId, long offset, boolean increaseOnly) { + AtomicLong offsetOld = this.offsetTable.get(queueId); + if (null == offsetOld) { + offsetOld = this.offsetTable.putIfAbsent(queueId, new AtomicLong(offset)); + } + + if (null != offsetOld) { + if (increaseOnly) { + MixAll.compareAndIncreaseOnly(offsetOld, offset); + } else { + offsetOld.set(offset); + } + } + } + + public long readOffset(int queueId) { + AtomicLong offset = this.offsetTable.get(queueId); + if (offset != null) { + return offset.get(); + } + return -1L; + } + + public Set queueList() { + return offsetTable.keySet(); + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/offset/ConsumerOffsetManager.java b/broker/src/main/java/org/apache/rocketmq/broker/offset/ConsumerOffsetManager.java index 0257f94b737..21f20dde325 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/offset/ConsumerOffsetManager.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/offset/ConsumerOffsetManager.java @@ -24,23 +24,38 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicLong; + +import com.google.common.base.Strings; + import org.apache.rocketmq.broker.BrokerController; import org.apache.rocketmq.broker.BrokerPathConfigHelper; import org.apache.rocketmq.common.ConfigManager; import org.apache.rocketmq.common.UtilAll; import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.DataVersion; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class ConsumerOffsetManager extends ConfigManager { - private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); - private static final String TOPIC_GROUP_SEPARATOR = "@"; + protected static final Logger LOG = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + public static final String TOPIC_GROUP_SEPARATOR = "@"; + + private DataVersion dataVersion = new DataVersion(); + + protected ConcurrentMap> offsetTable = + new ConcurrentHashMap<>(512); + + private final ConcurrentMap> resetOffsetTable = + new ConcurrentHashMap<>(512); + + private final ConcurrentMap> pullOffsetTable = + new ConcurrentHashMap<>(512); - private ConcurrentMap> offsetTable = - new ConcurrentHashMap>(512); + protected transient BrokerController brokerController; - private transient BrokerController brokerController; + private final transient AtomicLong versionChangeCounter = new AtomicLong(0); public ConsumerOffsetManager() { } @@ -49,6 +64,42 @@ public ConsumerOffsetManager(BrokerController brokerController) { this.brokerController = brokerController; } + protected void removeConsumerOffset(String topicAtGroup) { + + } + + public void cleanOffset(String group) { + Iterator>> it = this.offsetTable.entrySet().iterator(); + while (it.hasNext()) { + Entry> next = it.next(); + String topicAtGroup = next.getKey(); + if (topicAtGroup.contains(group)) { + String[] arrays = topicAtGroup.split(TOPIC_GROUP_SEPARATOR); + if (arrays.length == 2 && group.equals(arrays[1])) { + it.remove(); + removeConsumerOffset(topicAtGroup); + LOG.warn("Clean group's offset, {}, {}", topicAtGroup, next.getValue()); + } + } + } + } + + public void cleanOffsetByTopic(String topic) { + Iterator>> it = this.offsetTable.entrySet().iterator(); + while (it.hasNext()) { + Entry> next = it.next(); + String topicAtGroup = next.getKey(); + if (topicAtGroup.contains(topic)) { + String[] arrays = topicAtGroup.split(TOPIC_GROUP_SEPARATOR); + if (arrays.length == 2 && topic.equals(arrays[0])) { + it.remove(); + removeConsumerOffset(topicAtGroup); + LOG.warn("Clean topic's offset, {}, {}", topicAtGroup, next.getValue()); + } + } + } + } + public void scanUnsubscribedTopic() { Iterator>> it = this.offsetTable.entrySet().iterator(); while (it.hasNext()) { @@ -62,7 +113,8 @@ public void scanUnsubscribedTopic() { if (null == brokerController.getConsumerManager().findSubscriptionData(group, topic) && this.offsetBehindMuchThanData(topic, next.getValue())) { it.remove(); - log.warn("remove topic offset, {}", topicAtGroup); + removeConsumerOffset(topicAtGroup); + LOG.warn("remove topic offset, {}", topicAtGroup); } } } @@ -83,7 +135,7 @@ private boolean offsetBehindMuchThanData(final String topic, ConcurrentMap whichTopicByConsumer(final String group) { - Set topics = new HashSet(); + Set topics = new HashSet<>(); Iterator>> it = this.offsetTable.entrySet().iterator(); while (it.hasNext()) { @@ -101,7 +153,7 @@ public Set whichTopicByConsumer(final String group) { } public Set whichGroupByTopic(final String topic) { - Set groups = new HashSet(); + Set groups = new HashSet<>(); Iterator>> it = this.offsetTable.entrySet().iterator(); while (it.hasNext()) { @@ -118,6 +170,28 @@ public Set whichGroupByTopic(final String topic) { return groups; } + public Map> getGroupTopicMap() { + Map> retMap = new HashMap<>(128); + + for (String key : this.offsetTable.keySet()) { + String[] arr = key.split(TOPIC_GROUP_SEPARATOR); + if (arr.length == 2) { + String topic = arr[0]; + String group = arr[1]; + + Set topics = retMap.get(group); + if (topics == null) { + topics = new HashSet<>(8); + retMap.put(group, topics); + } + + topics.add(topic); + } + } + + return retMap; + } + public void commitOffset(final String clientHost, final String group, final String topic, final int queueId, final long offset) { // topic@group @@ -128,30 +202,85 @@ public void commitOffset(final String clientHost, final String group, final Stri private void commitOffset(final String clientHost, final String key, final int queueId, final long offset) { ConcurrentMap map = this.offsetTable.get(key); if (null == map) { - map = new ConcurrentHashMap(32); + map = new ConcurrentHashMap<>(32); map.put(queueId, offset); this.offsetTable.put(key, map); } else { Long storeOffset = map.put(queueId, offset); if (storeOffset != null && offset < storeOffset) { - log.warn("[NOTIFYME]update consumer offset less than store. clientHost={}, key={}, queueId={}, requestOffset={}, storeOffset={}", clientHost, key, queueId, offset, storeOffset); + LOG.warn("[NOTIFYME]update consumer offset less than store. clientHost={}, key={}, queueId={}, requestOffset={}, storeOffset={}", clientHost, key, queueId, offset, storeOffset); } } + if (versionChangeCounter.incrementAndGet() % brokerController.getBrokerConfig().getConsumerOffsetUpdateVersionStep() == 0) { + long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0; + dataVersion.nextVersion(stateMachineVersion); + } + } + + public void commitPullOffset(final String clientHost, final String group, final String topic, final int queueId, + final long offset) { + // topic@group + String key = topic + TOPIC_GROUP_SEPARATOR + group; + ConcurrentMap map = this.pullOffsetTable.computeIfAbsent( + key, k -> new ConcurrentHashMap<>(32)); + map.put(queueId, offset); } + /** + * If the target queue has temporary reset offset, return the reset-offset. + * Otherwise, return the current consume offset in the offset store. + * @param group Consumer group + * @param topic Topic + * @param queueId Queue ID + * @return current consume offset or reset offset if there were one. + */ public long queryOffset(final String group, final String topic, final int queueId) { // topic@group String key = topic + TOPIC_GROUP_SEPARATOR + group; + + if (this.brokerController.getBrokerConfig().isUseServerSideResetOffset()) { + Map reset = resetOffsetTable.get(key); + if (null != reset && reset.containsKey(queueId)) { + return reset.get(queueId); + } + } + ConcurrentMap map = this.offsetTable.get(key); if (null != map) { Long offset = map.get(queueId); - if (offset != null) + if (offset != null) { return offset; + } + } + + return -1L; + } + + /** + * Query pull offset in pullOffsetTable + * @param group Consumer group + * @param topic Topic + * @param queueId Queue ID + * @return latest pull offset of consumer group + */ + public long queryPullOffset(final String group, final String topic, final int queueId) { + // topic@group + String key = topic + TOPIC_GROUP_SEPARATOR + group; + Long offset = null; + + ConcurrentMap map = this.pullOffsetTable.get(key); + if (null != map) { + offset = map.get(queueId); } - return -1; + if (offset == null) { + offset = queryOffset(group, topic, queueId); + } + + return offset; } + @Override public String encode() { return this.encode(false); } @@ -166,11 +295,13 @@ public void decode(String jsonString) { if (jsonString != null) { ConsumerOffsetManager obj = RemotingSerializable.fromJson(jsonString, ConsumerOffsetManager.class); if (obj != null) { - this.offsetTable = obj.offsetTable; + this.setOffsetTable(obj.getOffsetTable()); + this.dataVersion = obj.dataVersion; } } } + @Override public String encode(final boolean prettyFormat) { return RemotingSerializable.toJson(this, prettyFormat); } @@ -179,20 +310,22 @@ public ConcurrentMap> getOffsetTable() { return offsetTable; } - public void setOffsetTable(ConcurrentHashMap> offsetTable) { + public void setOffsetTable(ConcurrentMap> offsetTable) { this.offsetTable = offsetTable; } public Map queryMinOffsetInAllGroup(final String topic, final String filterGroups) { - Map queueMinOffset = new HashMap(); + Map queueMinOffset = new HashMap<>(); Set topicGroups = this.offsetTable.keySet(); if (!UtilAll.isBlank(filterGroups)) { for (String group : filterGroups.split(",")) { Iterator it = topicGroups.iterator(); while (it.hasNext()) { - if (group.equals(it.next().split(TOPIC_GROUP_SEPARATOR)[1])) { + String topicAtGroup = it.next(); + if (group.equals(topicAtGroup.split(TOPIC_GROUP_SEPARATOR)[1])) { it.remove(); + removeConsumerOffset(topicAtGroup); } } } @@ -228,8 +361,81 @@ public Map queryOffset(final String group, final String topic) { public void cloneOffset(final String srcGroup, final String destGroup, final String topic) { ConcurrentMap offsets = this.offsetTable.get(topic + TOPIC_GROUP_SEPARATOR + srcGroup); if (offsets != null) { - this.offsetTable.put(topic + TOPIC_GROUP_SEPARATOR + destGroup, new ConcurrentHashMap(offsets)); + this.offsetTable.put(topic + TOPIC_GROUP_SEPARATOR + destGroup, new ConcurrentHashMap<>(offsets)); } } + public DataVersion getDataVersion() { + return dataVersion; + } + + public void setDataVersion(DataVersion dataVersion) { + this.dataVersion = dataVersion; + } + + public void removeOffset(final String group) { + Iterator>> it = this.offsetTable.entrySet().iterator(); + while (it.hasNext()) { + Entry> next = it.next(); + String topicAtGroup = next.getKey(); + if (topicAtGroup.contains(group)) { + String[] arrays = topicAtGroup.split(TOPIC_GROUP_SEPARATOR); + if (arrays.length == 2 && group.equals(arrays[1])) { + it.remove(); + removeConsumerOffset(topicAtGroup); + LOG.warn("clean group offset {}", topicAtGroup); + } + } + } + } + + public void assignResetOffset(String topic, String group, int queueId, long offset) { + if (Strings.isNullOrEmpty(topic) || Strings.isNullOrEmpty(group) || queueId < 0 || offset < 0) { + LOG.warn("Illegal arguments when assigning reset offset. Topic={}, group={}, queueId={}, offset={}", + topic, group, queueId, offset); + return; + } + + String key = topic + TOPIC_GROUP_SEPARATOR + group; + ConcurrentMap map = resetOffsetTable.get(key); + if (null == map) { + map = new ConcurrentHashMap(); + ConcurrentMap previous = resetOffsetTable.putIfAbsent(key, map); + if (null != previous) { + map = previous; + } + } + + map.put(queueId, offset); + LOG.debug("Reset offset OK. Topic={}, group={}, queueId={}, resetOffset={}", + topic, group, queueId, offset); + + // Two things are important here: + // 1, currentOffsetMap might be null if there is no previous records; + // 2, Our overriding here may get overridden by the client instantly in concurrent cases; But it still makes + // sense in cases like clients are offline. + ConcurrentMap currentOffsetMap = offsetTable.get(key); + if (null != currentOffsetMap) { + currentOffsetMap.put(queueId, offset); + } + } + + public boolean hasOffsetReset(String topic, String group, int queueId) { + String key = topic + TOPIC_GROUP_SEPARATOR + group; + ConcurrentMap map = resetOffsetTable.get(key); + if (null == map) { + return false; + } + return map.containsKey(queueId); + } + + public Long queryThenEraseResetOffset(String topic, String group, Integer queueId) { + String key = topic + TOPIC_GROUP_SEPARATOR + group; + ConcurrentMap map = resetOffsetTable.get(key); + if (null == map) { + return null; + } else { + return map.remove(queueId); + } + } } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/offset/ConsumerOrderInfoLockManager.java b/broker/src/main/java/org/apache/rocketmq/broker/offset/ConsumerOrderInfoLockManager.java new file mode 100644 index 00000000000..37b3eed2302 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/offset/ConsumerOrderInfoLockManager.java @@ -0,0 +1,185 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.offset; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import io.netty.util.HashedWheelTimer; +import io.netty.util.Timeout; +import io.netty.util.Timer; +import io.netty.util.TimerTask; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class ConsumerOrderInfoLockManager { + private static final Logger POP_LOGGER = LoggerFactory.getLogger(LoggerName.ROCKETMQ_POP_LOGGER_NAME); + private final BrokerController brokerController; + private final Map timeoutMap = new ConcurrentHashMap<>(); + private final Timer timer; + private static final int TIMER_TICK_MS = 100; + + public ConsumerOrderInfoLockManager(BrokerController brokerController) { + this.brokerController = brokerController; + this.timer = new HashedWheelTimer( + new ThreadFactoryImpl("ConsumerOrderInfoLockManager_"), + TIMER_TICK_MS, TimeUnit.MILLISECONDS); + } + + /** + * when ConsumerOrderInfoManager load from disk, recover data + */ + public void recover(Map> table) { + if (!this.brokerController.getBrokerConfig().isEnableNotifyAfterPopOrderLockRelease()) { + return; + } + for (Map.Entry> entry : table.entrySet()) { + String topicAtGroup = entry.getKey(); + ConcurrentHashMap qs = entry.getValue(); + String[] arrays = ConsumerOrderInfoManager.decodeKey(topicAtGroup); + if (arrays.length != 2) { + continue; + } + String topic = arrays[0]; + String group = arrays[1]; + for (Map.Entry qsEntry : qs.entrySet()) { + Long lockFreeTimestamp = qsEntry.getValue().getLockFreeTimestamp(); + if (lockFreeTimestamp == null || lockFreeTimestamp <= System.currentTimeMillis()) { + continue; + } + this.updateLockFreeTimestamp(topic, group, qsEntry.getKey(), lockFreeTimestamp); + } + } + } + + public void updateLockFreeTimestamp(String topic, String group, int queueId, ConsumerOrderInfoManager.OrderInfo orderInfo) { + this.updateLockFreeTimestamp(topic, group, queueId, orderInfo.getLockFreeTimestamp()); + } + + public void updateLockFreeTimestamp(String topic, String group, int queueId, Long lockFreeTimestamp) { + if (!this.brokerController.getBrokerConfig().isEnableNotifyAfterPopOrderLockRelease()) { + return; + } + if (lockFreeTimestamp == null) { + return; + } + try { + this.timeoutMap.compute(new Key(topic, group, queueId), (key, oldTimeout) -> { + try { + long delay = lockFreeTimestamp - System.currentTimeMillis(); + Timeout newTimeout = this.timer.newTimeout(new NotifyLockFreeTimerTask(key), delay, TimeUnit.MILLISECONDS); + if (oldTimeout != null) { + // cancel prev timerTask + oldTimeout.cancel(); + } + return newTimeout; + } catch (Exception e) { + POP_LOGGER.warn("add timeout task failed. key:{}, lockFreeTimestamp:{}", key, lockFreeTimestamp, e); + return oldTimeout; + } + }); + } catch (Exception e) { + POP_LOGGER.error("unexpect error when updateLockFreeTimestamp. topic:{}, group:{}, queueId:{}, lockFreeTimestamp:{}", + topic, group, queueId, lockFreeTimestamp, e); + } + } + + protected void notifyLockIsFree(Key key) { + try { + this.brokerController.getPopMessageProcessor().notifyLongPollingRequestIfNeed(key.topic, key.group, key.queueId); + } catch (Exception e) { + POP_LOGGER.error("unexpect error when notifyLockIsFree. key:{}", key, e); + } + } + + public void shutdown() { + this.timer.stop(); + } + + @VisibleForTesting + protected Map getTimeoutMap() { + return timeoutMap; + } + + private class NotifyLockFreeTimerTask implements TimerTask { + + private final Key key; + + private NotifyLockFreeTimerTask(Key key) { + this.key = key; + } + + @Override + public void run(Timeout timeout) throws Exception { + if (timeout.isCancelled() || !brokerController.getBrokerConfig().isEnableNotifyAfterPopOrderLockRelease()) { + return; + } + notifyLockIsFree(key); + timeoutMap.computeIfPresent(key, (key1, curTimeout) -> { + if (curTimeout == timeout) { + // remove from map + return null; + } + return curTimeout; + }); + } + } + + private static class Key { + private final String topic; + private final String group; + private final int queueId; + + public Key(String topic, String group, int queueId) { + this.topic = topic; + this.group = group; + this.queueId = queueId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Key key = (Key) o; + return queueId == key.queueId && Objects.equal(topic, key.topic) && Objects.equal(group, key.group); + } + + @Override + public int hashCode() { + return Objects.hashCode(topic, group, queueId); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("topic", topic) + .add("group", group) + .add("queueId", queueId) + .toString(); + } + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/offset/ConsumerOrderInfoManager.java b/broker/src/main/java/org/apache/rocketmq/broker/offset/ConsumerOrderInfoManager.java new file mode 100644 index 00000000000..4eccc6c0374 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/offset/ConsumerOrderInfoManager.java @@ -0,0 +1,644 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.offset; + +import com.alibaba.fastjson.annotation.JSONField; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.BrokerPathConfigHelper; +import org.apache.rocketmq.common.ConfigManager; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.header.ExtraInfoUtil; + +public class ConsumerOrderInfoManager extends ConfigManager { + + private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + private static final String TOPIC_GROUP_SEPARATOR = "@"; + private static final long CLEAN_SPAN_FROM_LAST = 24 * 3600 * 1000; + + private ConcurrentHashMap> table = + new ConcurrentHashMap<>(128); + + private transient ConsumerOrderInfoLockManager consumerOrderInfoLockManager; + private transient BrokerController brokerController; + + public ConsumerOrderInfoManager() { + } + + public ConsumerOrderInfoManager(BrokerController brokerController) { + this.brokerController = brokerController; + this.consumerOrderInfoLockManager = new ConsumerOrderInfoLockManager(brokerController); + } + + public ConcurrentHashMap> getTable() { + return table; + } + + public void setTable(ConcurrentHashMap> table) { + this.table = table; + } + + protected static String buildKey(String topic, String group) { + return topic + TOPIC_GROUP_SEPARATOR + group; + } + + protected static String[] decodeKey(String key) { + return key.split(TOPIC_GROUP_SEPARATOR); + } + + private void updateLockFreeTimestamp(String topic, String group, int queueId, OrderInfo orderInfo) { + if (consumerOrderInfoLockManager != null) { + consumerOrderInfoLockManager.updateLockFreeTimestamp(topic, group, queueId, orderInfo); + } + } + + /** + * update the message list received + * + * @param isRetry is retry topic or not + * @param topic topic + * @param group group + * @param queueId queue id of message + * @param popTime the time of pop message + * @param invisibleTime invisible time + * @param msgQueueOffsetList the queue offsets of messages + * @param orderInfoBuilder will append order info to this builder + */ + public void update(String attemptId, boolean isRetry, String topic, String group, int queueId, long popTime, long invisibleTime, + List msgQueueOffsetList, StringBuilder orderInfoBuilder) { + String key = buildKey(topic, group); + ConcurrentHashMap qs = table.get(key); + if (qs == null) { + qs = new ConcurrentHashMap<>(16); + ConcurrentHashMap old = table.putIfAbsent(key, qs); + if (old != null) { + qs = old; + } + } + + OrderInfo orderInfo = qs.get(queueId); + + if (orderInfo != null) { + OrderInfo newOrderInfo = new OrderInfo(attemptId, popTime, invisibleTime, msgQueueOffsetList, System.currentTimeMillis(), 0); + newOrderInfo.mergeOffsetConsumedCount(orderInfo.attemptId, orderInfo.offsetList, orderInfo.offsetConsumedCount); + + orderInfo = newOrderInfo; + } else { + orderInfo = new OrderInfo(attemptId, popTime, invisibleTime, msgQueueOffsetList, System.currentTimeMillis(), 0); + } + qs.put(queueId, orderInfo); + + Map offsetConsumedCount = orderInfo.offsetConsumedCount; + int minConsumedTimes = Integer.MAX_VALUE; + if (offsetConsumedCount != null) { + Set offsetSet = offsetConsumedCount.keySet(); + for (Long offset : offsetSet) { + Integer consumedTimes = offsetConsumedCount.getOrDefault(offset, 0); + ExtraInfoUtil.buildQueueOffsetOrderCountInfo(orderInfoBuilder, topic, queueId, offset, consumedTimes); + minConsumedTimes = Math.min(minConsumedTimes, consumedTimes); + } + + if (offsetConsumedCount.size() != orderInfo.offsetList.size()) { + // offsetConsumedCount only save messages which consumed count is greater than 0 + // if size not equal, means there are some new messages + minConsumedTimes = 0; + } + } else { + minConsumedTimes = 0; + } + + // for compatibility + // the old pop sdk use queueId to get consumedTimes from orderCountInfo + ExtraInfoUtil.buildQueueIdOrderCountInfo(orderInfoBuilder, topic, queueId, minConsumedTimes); + updateLockFreeTimestamp(topic, group, queueId, orderInfo); + } + + public boolean checkBlock(String attemptId, String topic, String group, int queueId, long invisibleTime) { + String key = buildKey(topic, group); + ConcurrentHashMap qs = table.get(key); + if (qs == null) { + qs = new ConcurrentHashMap<>(16); + ConcurrentHashMap old = table.putIfAbsent(key, qs); + if (old != null) { + qs = old; + } + } + + OrderInfo orderInfo = qs.get(queueId); + + if (orderInfo == null) { + return false; + } + return orderInfo.needBlock(attemptId, invisibleTime); + } + + public void clearBlock(String topic, String group, int queueId) { + table.computeIfPresent(buildKey(topic, group), (key, val) -> { + val.remove(queueId); + return val; + }); + } + + /** + * mark message is consumed finished. return the consumer offset + * + * @param topic topic + * @param group group + * @param queueId queue id of message + * @param queueOffset queue offset of message + * @return -1 : illegal, -2 : no need commit, >= 0 : commit + */ + public long commitAndNext(String topic, String group, int queueId, long queueOffset, long popTime) { + String key = buildKey(topic, group); + ConcurrentHashMap qs = table.get(key); + + if (qs == null) { + return queueOffset + 1; + } + OrderInfo orderInfo = qs.get(queueId); + if (orderInfo == null) { + log.warn("OrderInfo is null, {}, {}, {}", key, queueOffset, orderInfo); + return queueOffset + 1; + } + + List o = orderInfo.offsetList; + if (o == null || o.isEmpty()) { + log.warn("OrderInfo is empty, {}, {}, {}", key, queueOffset, orderInfo); + return -1; + } + + if (popTime != orderInfo.popTime) { + log.warn("popTime is not equal to orderInfo saved. key: {}, offset: {}, orderInfo: {}, popTime: {}", key, queueOffset, orderInfo, popTime); + return -2; + } + + Long first = o.get(0); + int i = 0, size = o.size(); + for (; i < size; i++) { + long temp; + if (i == 0) { + temp = first; + } else { + temp = first + o.get(i); + } + if (queueOffset == temp) { + break; + } + } + // not found + if (i >= size) { + log.warn("OrderInfo not found commit offset, {}, {}, {}", key, queueOffset, orderInfo); + return -1; + } + //set bit + orderInfo.setCommitOffsetBit(orderInfo.commitOffsetBit | (1L << i)); + long nextOffset = orderInfo.getNextOffset(); + + updateLockFreeTimestamp(topic, group, queueId, orderInfo); + return nextOffset; + } + + /** + * update next visible time of this message + * + * @param topic topic + * @param group group + * @param queueId queue id of message + * @param queueOffset queue offset of message + * @param nextVisibleTime nex visible time + */ + public void updateNextVisibleTime(String topic, String group, int queueId, long queueOffset, long popTime, long nextVisibleTime) { + String key = buildKey(topic, group); + ConcurrentHashMap qs = table.get(key); + + if (qs == null) { + log.warn("orderInfo of queueId is null. key: {}, queueOffset: {}, queueId: {}", key, queueOffset, queueId); + return; + } + OrderInfo orderInfo = qs.get(queueId); + if (orderInfo == null) { + log.warn("orderInfo is null, key: {}, queueOffset: {}, queueId: {}", key, queueOffset, queueId); + return; + } + if (popTime != orderInfo.popTime) { + log.warn("popTime is not equal to orderInfo saved. key: {}, queueOffset: {}, orderInfo: {}, popTime: {}", key, queueOffset, orderInfo, popTime); + return; + } + + orderInfo.updateOffsetNextVisibleTime(queueOffset, nextVisibleTime); + updateLockFreeTimestamp(topic, group, queueId, orderInfo); + } + + protected void autoClean() { + if (brokerController == null) { + return; + } + Iterator>> iterator = + this.table.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry> entry = + iterator.next(); + String topicAtGroup = entry.getKey(); + ConcurrentHashMap qs = entry.getValue(); + String[] arrays = decodeKey(topicAtGroup); + if (arrays.length != 2) { + continue; + } + String topic = arrays[0]; + String group = arrays[1]; + + TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(topic); + if (topicConfig == null) { + iterator.remove(); + log.info("Topic not exist, Clean order info, {}:{}", topicAtGroup, qs); + continue; + } + + if (this.brokerController.getSubscriptionGroupManager().getSubscriptionGroupTable().get(group) == null) { + iterator.remove(); + log.info("Group not exist, Clean order info, {}:{}", topicAtGroup, qs); + continue; + } + + if (qs.isEmpty()) { + iterator.remove(); + log.info("Order table is empty, Clean order info, {}:{}", topicAtGroup, qs); + continue; + } + + Iterator> qsIterator = qs.entrySet().iterator(); + while (qsIterator.hasNext()) { + Map.Entry qsEntry = qsIterator.next(); + + if (qsEntry.getKey() >= topicConfig.getReadQueueNums()) { + qsIterator.remove(); + log.info("Queue not exist, Clean order info, {}:{}, {}", topicAtGroup, entry.getValue(), topicConfig); + continue; + } + + if (System.currentTimeMillis() - qsEntry.getValue().getLastConsumeTimestamp() > CLEAN_SPAN_FROM_LAST) { + qsIterator.remove(); + log.info("Not consume long time, Clean order info, {}:{}, {}", topicAtGroup, entry.getValue(), topicConfig); + } + } + } + } + + @Override + public String encode() { + return this.encode(false); + } + + @Override + public String configFilePath() { + if (brokerController != null) { + return BrokerPathConfigHelper.getConsumerOrderInfoPath(this.brokerController.getMessageStoreConfig().getStorePathRootDir()); + } else { + return BrokerPathConfigHelper.getConsumerOrderInfoPath("~"); + } + } + + @Override + public void decode(String jsonString) { + if (jsonString != null) { + ConsumerOrderInfoManager obj = RemotingSerializable.fromJson(jsonString, ConsumerOrderInfoManager.class); + if (obj != null) { + this.table = obj.table; + if (this.consumerOrderInfoLockManager != null) { + this.consumerOrderInfoLockManager.recover(this.table); + } + } + } + } + + @Override + public String encode(boolean prettyFormat) { + this.autoClean(); + return RemotingSerializable.toJson(this, prettyFormat); + } + + public void shutdown() { + if (this.consumerOrderInfoLockManager != null) { + this.consumerOrderInfoLockManager.shutdown(); + } + } + + @VisibleForTesting + protected ConsumerOrderInfoLockManager getConsumerOrderInfoLockManager() { + return consumerOrderInfoLockManager; + } + + public static class OrderInfo { + private long popTime; + /** + * the invisibleTime when pop message + */ + @JSONField(name = "i") + private Long invisibleTime; + /** + * offset + * offsetList[0] is the queue offset of message + * offsetList[i] (i > 0) is the distance between current message and offsetList[0] + */ + @JSONField(name = "o") + private List offsetList; + /** + * next visible timestamp for message + * key: message queue offset + */ + @JSONField(name = "ot") + private Map offsetNextVisibleTime; + /** + * message consumed count for offset + * key: message queue offset + */ + @JSONField(name = "oc") + private Map offsetConsumedCount; + /** + * last consume timestamp + */ + @JSONField(name = "l") + private long lastConsumeTimestamp; + /** + * commit offset bit + */ + @JSONField(name = "cm") + private long commitOffsetBit; + @JSONField(name = "a") + private String attemptId; + + public OrderInfo() { + } + + public OrderInfo(String attemptId, long popTime, long invisibleTime, List queueOffsetList, long lastConsumeTimestamp, + long commitOffsetBit) { + this.popTime = popTime; + this.invisibleTime = invisibleTime; + this.offsetList = buildOffsetList(queueOffsetList); + this.lastConsumeTimestamp = lastConsumeTimestamp; + this.commitOffsetBit = commitOffsetBit; + this.attemptId = attemptId; + } + + public List getOffsetList() { + return offsetList; + } + + public void setOffsetList(List offsetList) { + this.offsetList = offsetList; + } + + public long getLastConsumeTimestamp() { + return lastConsumeTimestamp; + } + + public void setLastConsumeTimestamp(long lastConsumeTimestamp) { + this.lastConsumeTimestamp = lastConsumeTimestamp; + } + + public long getCommitOffsetBit() { + return commitOffsetBit; + } + + public void setCommitOffsetBit(long commitOffsetBit) { + this.commitOffsetBit = commitOffsetBit; + } + + public long getPopTime() { + return popTime; + } + + public void setPopTime(long popTime) { + this.popTime = popTime; + } + + public Long getInvisibleTime() { + return invisibleTime; + } + + public void setInvisibleTime(Long invisibleTime) { + this.invisibleTime = invisibleTime; + } + + public Map getOffsetNextVisibleTime() { + return offsetNextVisibleTime; + } + + public void setOffsetNextVisibleTime(Map offsetNextVisibleTime) { + this.offsetNextVisibleTime = offsetNextVisibleTime; + } + + public Map getOffsetConsumedCount() { + return offsetConsumedCount; + } + + public void setOffsetConsumedCount(Map offsetConsumedCount) { + this.offsetConsumedCount = offsetConsumedCount; + } + + public String getAttemptId() { + return attemptId; + } + + public void setAttemptId(String attemptId) { + this.attemptId = attemptId; + } + + public static List buildOffsetList(List queueOffsetList) { + List simple = new ArrayList<>(); + if (queueOffsetList.size() == 1) { + simple.addAll(queueOffsetList); + return simple; + } + Long first = queueOffsetList.get(0); + simple.add(first); + for (int i = 1; i < queueOffsetList.size(); i++) { + simple.add(queueOffsetList.get(i) - first); + } + return simple; + } + + @JSONField(serialize = false, deserialize = false) + public boolean needBlock(String attemptId, long currentInvisibleTime) { + if (offsetList == null || offsetList.isEmpty()) { + return false; + } + if (this.attemptId != null && this.attemptId.equals(attemptId)) { + return false; + } + int num = offsetList.size(); + int i = 0; + if (this.invisibleTime == null || this.invisibleTime <= 0) { + this.invisibleTime = currentInvisibleTime; + } + long currentTime = System.currentTimeMillis(); + for (; i < num; i++) { + if (isNotAck(i)) { + long nextVisibleTime = popTime + invisibleTime; + if (offsetNextVisibleTime != null) { + Long time = offsetNextVisibleTime.get(this.getQueueOffset(i)); + if (time != null) { + nextVisibleTime = time; + } + } + if (currentTime < nextVisibleTime) { + return true; + } + } + } + return false; + } + + @JSONField(serialize = false, deserialize = false) + public Long getLockFreeTimestamp() { + if (offsetList == null || offsetList.isEmpty()) { + return null; + } + int num = offsetList.size(); + int i = 0; + long currentTime = System.currentTimeMillis(); + for (; i < num; i++) { + if (isNotAck(i)) { + if (invisibleTime == null || invisibleTime <= 0) { + return null; + } + long nextVisibleTime = popTime + invisibleTime; + if (offsetNextVisibleTime != null) { + Long time = offsetNextVisibleTime.get(this.getQueueOffset(i)); + if (time != null) { + nextVisibleTime = time; + } + } + if (currentTime < nextVisibleTime) { + return nextVisibleTime; + } + } + } + return currentTime; + } + + @JSONField(serialize = false, deserialize = false) + public void updateOffsetNextVisibleTime(long queueOffset, long nextVisibleTime) { + if (this.offsetNextVisibleTime == null) { + this.offsetNextVisibleTime = new HashMap<>(); + } + this.offsetNextVisibleTime.put(queueOffset, nextVisibleTime); + } + + @JSONField(serialize = false, deserialize = false) + public long getNextOffset() { + if (offsetList == null || offsetList.isEmpty()) { + return -2; + } + int num = offsetList.size(); + int i = 0; + for (; i < num; i++) { + if (isNotAck(i)) { + break; + } + } + if (i == num) { + // all ack + return getQueueOffset(num - 1) + 1; + } + return getQueueOffset(i); + } + + /** + * convert the offset at the index of offsetList to queue offset + * + * @param offsetIndex the index of offsetList + * @return queue offset of message + */ + @JSONField(serialize = false, deserialize = false) + public long getQueueOffset(int offsetIndex) { + return getQueueOffset(this.offsetList, offsetIndex); + } + + protected static long getQueueOffset(List offsetList, int offsetIndex) { + if (offsetIndex == 0) { + return offsetList.get(0); + } + return offsetList.get(0) + offsetList.get(offsetIndex); + } + + @JSONField(serialize = false, deserialize = false) + public boolean isNotAck(int offsetIndex) { + return (commitOffsetBit & (1L << offsetIndex)) == 0; + } + + /** + * calculate message consumed count of each message, and put nonzero value into offsetConsumedCount + * + * @param prevOffsetConsumedCount the offset list of message + */ + @JSONField(serialize = false, deserialize = false) + public void mergeOffsetConsumedCount(String preAttemptId, List preOffsetList, Map prevOffsetConsumedCount) { + Map offsetConsumedCount = new HashMap<>(); + if (prevOffsetConsumedCount == null) { + prevOffsetConsumedCount = new HashMap<>(); + } + if (preAttemptId != null && preAttemptId.equals(this.attemptId)) { + this.offsetConsumedCount = prevOffsetConsumedCount; + return; + } + Set preQueueOffsetSet = new HashSet<>(); + for (int i = 0; i < preOffsetList.size(); i++) { + preQueueOffsetSet.add(getQueueOffset(preOffsetList, i)); + } + for (int i = 0; i < offsetList.size(); i++) { + long queueOffset = this.getQueueOffset(i); + if (preQueueOffsetSet.contains(queueOffset)) { + int count = 1; + Integer preCount = prevOffsetConsumedCount.get(queueOffset); + if (preCount != null) { + count = preCount + 1; + } + offsetConsumedCount.put(queueOffset, count); + } + } + this.offsetConsumedCount = offsetConsumedCount; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("popTime", popTime) + .add("invisibleTime", invisibleTime) + .add("offsetList", offsetList) + .add("offsetNextVisibleTime", offsetNextVisibleTime) + .add("offsetConsumedCount", offsetConsumedCount) + .add("lastConsumeTimestamp", lastConsumeTimestamp) + .add("commitOffsetBit", commitOffsetBit) + .add("attemptId", attemptId) + .toString(); + } + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/offset/LmqConsumerOffsetManager.java b/broker/src/main/java/org/apache/rocketmq/broker/offset/LmqConsumerOffsetManager.java new file mode 100644 index 00000000000..ce70b1a820f --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/offset/LmqConsumerOffsetManager.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.offset; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.BrokerPathConfigHelper; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class LmqConsumerOffsetManager extends ConsumerOffsetManager { + private ConcurrentHashMap lmqOffsetTable = new ConcurrentHashMap<>(512); + + public LmqConsumerOffsetManager() { + + } + + public LmqConsumerOffsetManager(BrokerController brokerController) { + super(brokerController); + } + + @Override + public long queryOffset(final String group, final String topic, final int queueId) { + if (!MixAll.isLmq(group)) { + return super.queryOffset(group, topic, queueId); + } + // topic@group + String key = topic + TOPIC_GROUP_SEPARATOR + group; + Long offset = lmqOffsetTable.get(key); + if (offset != null) { + return offset; + } + return -1; + } + + @Override + public Map queryOffset(final String group, final String topic) { + if (!MixAll.isLmq(group)) { + return super.queryOffset(group, topic); + } + Map map = new HashMap<>(); + // topic@group + String key = topic + TOPIC_GROUP_SEPARATOR + group; + Long offset = lmqOffsetTable.get(key); + if (offset != null) { + map.put(0, offset); + } + return map; + } + + @Override + public void commitOffset(final String clientHost, final String group, final String topic, final int queueId, + final long offset) { + if (!MixAll.isLmq(group)) { + super.commitOffset(clientHost, group, topic, queueId, offset); + return; + } + // topic@group + String key = topic + TOPIC_GROUP_SEPARATOR + group; + lmqOffsetTable.put(key, offset); + } + + @Override + public String encode() { + return this.encode(false); + } + + @Override + public String configFilePath() { + return BrokerPathConfigHelper.getLmqConsumerOffsetPath(brokerController.getMessageStoreConfig().getStorePathRootDir()); + } + + @Override + public void decode(String jsonString) { + if (jsonString != null) { + LmqConsumerOffsetManager obj = RemotingSerializable.fromJson(jsonString, LmqConsumerOffsetManager.class); + if (obj != null) { + super.setOffsetTable(obj.getOffsetTable()); + this.lmqOffsetTable = obj.lmqOffsetTable; + } + } + } + + @Override + public String encode(final boolean prettyFormat) { + return RemotingSerializable.toJson(this, prettyFormat); + } + + public ConcurrentHashMap getLmqOffsetTable() { + return lmqOffsetTable; + } + + public void setLmqOffsetTable(ConcurrentHashMap lmqOffsetTable) { + this.lmqOffsetTable = lmqOffsetTable; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/offset/RocksDBConsumerOffsetManager.java b/broker/src/main/java/org/apache/rocketmq/broker/offset/RocksDBConsumerOffsetManager.java new file mode 100644 index 00000000000..05b53b0bcf2 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/offset/RocksDBConsumerOffsetManager.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.offset; + +import java.io.File; +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentMap; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.config.RocksDBConfigManager; +import org.apache.rocketmq.common.utils.DataConverter; +import org.rocksdb.WriteBatch; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.serializer.SerializerFeature; + +public class RocksDBConsumerOffsetManager extends ConsumerOffsetManager { + + public RocksDBConsumerOffsetManager(BrokerController brokerController) { + super(brokerController); + this.rocksDBConfigManager = new RocksDBConfigManager(brokerController.getMessageStoreConfig().getMemTableFlushIntervalMs()); + } + + @Override + public boolean load() { + return this.rocksDBConfigManager.load(configFilePath(), this::decode0); + } + + @Override + public boolean stop() { + return this.rocksDBConfigManager.stop(); + } + + @Override + protected void removeConsumerOffset(String topicAtGroup) { + try { + byte[] keyBytes = topicAtGroup.getBytes(DataConverter.CHARSET_UTF8); + this.rocksDBConfigManager.delete(keyBytes); + } catch (Exception e) { + LOG.error("kv remove consumerOffset Failed, {}", topicAtGroup); + } + } + + @Override + protected void decode0(final byte[] key, final byte[] body) { + String topicAtGroup = new String(key, DataConverter.CHARSET_UTF8); + RocksDBOffsetSerializeWrapper wrapper = JSON.parseObject(body, RocksDBOffsetSerializeWrapper.class); + + this.offsetTable.put(topicAtGroup, wrapper.getOffsetTable()); + LOG.info("load exist local offset, {}, {}", topicAtGroup, wrapper.getOffsetTable()); + } + + @Override + public String configFilePath() { + return this.brokerController.getMessageStoreConfig().getStorePathRootDir() + File.separator + "config" + File.separator + "consumerOffsets" + File.separator; + } + + @Override + public synchronized void persist() { + WriteBatch writeBatch = new WriteBatch(); + try { + Iterator>> iterator = this.offsetTable.entrySet().iterator(); + while (iterator.hasNext()) { + Entry> entry = iterator.next(); + putWriteBatch(writeBatch, entry.getKey(), entry.getValue()); + + if (writeBatch.getDataSize() >= 4 * 1024) { + this.rocksDBConfigManager.batchPutWithWal(writeBatch); + } + } + this.rocksDBConfigManager.batchPutWithWal(writeBatch); + this.rocksDBConfigManager.flushWAL(); + } catch (Exception e) { + LOG.error("consumer offset persist Failed", e); + } finally { + writeBatch.close(); + } + } + + private void putWriteBatch(final WriteBatch writeBatch, final String topicGroupName, final ConcurrentMap offsetMap) throws Exception { + byte[] keyBytes = topicGroupName.getBytes(DataConverter.CHARSET_UTF8); + RocksDBOffsetSerializeWrapper wrapper = new RocksDBOffsetSerializeWrapper(); + wrapper.setOffsetTable(offsetMap); + byte[] valueBytes = JSON.toJSONBytes(wrapper, SerializerFeature.BrowserCompatible); + writeBatch.put(keyBytes, valueBytes); + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/offset/RocksDBLmqConsumerOffsetManager.java b/broker/src/main/java/org/apache/rocketmq/broker/offset/RocksDBLmqConsumerOffsetManager.java new file mode 100644 index 00000000000..d0faa661406 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/offset/RocksDBLmqConsumerOffsetManager.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.offset; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class RocksDBLmqConsumerOffsetManager extends RocksDBConsumerOffsetManager { + private ConcurrentHashMap lmqOffsetTable = new ConcurrentHashMap<>(512); + + public RocksDBLmqConsumerOffsetManager(BrokerController brokerController) { + super(brokerController); + } + + @Override + public long queryOffset(final String group, final String topic, final int queueId) { + if (!MixAll.isLmq(group)) { + return super.queryOffset(group, topic, queueId); + } + // topic@group + String key = topic + TOPIC_GROUP_SEPARATOR + group; + Long offset = lmqOffsetTable.get(key); + if (offset != null) { + return offset; + } + return -1; + } + + @Override + public Map queryOffset(final String group, final String topic) { + if (!MixAll.isLmq(group)) { + return super.queryOffset(group, topic); + } + Map map = new HashMap<>(); + // topic@group + String key = topic + TOPIC_GROUP_SEPARATOR + group; + Long offset = lmqOffsetTable.get(key); + if (offset != null) { + map.put(0, offset); + } + return map; + } + + @Override + public void commitOffset(final String clientHost, final String group, final String topic, final int queueId, + final long offset) { + if (!MixAll.isLmq(group)) { + super.commitOffset(clientHost, group, topic, queueId, offset); + return; + } + // topic@group + String key = topic + TOPIC_GROUP_SEPARATOR + group; + lmqOffsetTable.put(key, offset); + } + + @Override + public String encode() { + return this.encode(false); + } + + @Override + public void decode(String jsonString) { + if (jsonString != null) { + RocksDBLmqConsumerOffsetManager obj = RemotingSerializable.fromJson(jsonString, RocksDBLmqConsumerOffsetManager.class); + if (obj != null) { + super.setOffsetTable(obj.getOffsetTable()); + this.lmqOffsetTable = obj.lmqOffsetTable; + } + } + } + + @Override + public String encode(final boolean prettyFormat) { + return RemotingSerializable.toJson(this, prettyFormat); + } + + public ConcurrentHashMap getLmqOffsetTable() { + return lmqOffsetTable; + } + + public void setLmqOffsetTable(ConcurrentHashMap lmqOffsetTable) { + this.lmqOffsetTable = lmqOffsetTable; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/offset/RocksDBOffsetSerializeWrapper.java b/broker/src/main/java/org/apache/rocketmq/broker/offset/RocksDBOffsetSerializeWrapper.java new file mode 100644 index 00000000000..7a90fd62fb8 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/offset/RocksDBOffsetSerializeWrapper.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.offset; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class RocksDBOffsetSerializeWrapper extends RemotingSerializable { + private ConcurrentMap offsetTable = new ConcurrentHashMap(16); + + public ConcurrentMap getOffsetTable() { + return offsetTable; + } + + public void setOffsetTable(ConcurrentMap offsetTable) { + this.offsetTable = offsetTable; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/out/BrokerOuterAPI.java b/broker/src/main/java/org/apache/rocketmq/broker/out/BrokerOuterAPI.java index cba70a0ee8a..01745a2b79d 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/out/BrokerOuterAPI.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/out/BrokerOuterAPI.java @@ -17,49 +17,152 @@ package org.apache.rocketmq.broker.out; import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.consumer.PullStatus; import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.impl.consumer.PullResultExt; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.common.AbstractBrokerRunnable; +import org.apache.rocketmq.common.BrokerIdentity; +import org.apache.rocketmq.common.LockCallback; import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.UnlockCallback; +import org.apache.rocketmq.common.UtilAll; import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.common.namesrv.RegisterBrokerResult; +import org.apache.rocketmq.common.filter.ExpressionType; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageBatch; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.namesrv.DefaultTopAddressing; import org.apache.rocketmq.common.namesrv.TopAddressing; -import org.apache.rocketmq.common.protocol.RequestCode; -import org.apache.rocketmq.common.protocol.ResponseCode; -import org.apache.rocketmq.common.protocol.body.ConsumerOffsetSerializeWrapper; -import org.apache.rocketmq.common.protocol.body.KVTable; -import org.apache.rocketmq.common.protocol.body.RegisterBrokerBody; -import org.apache.rocketmq.common.protocol.body.SubscriptionGroupWrapper; -import org.apache.rocketmq.common.protocol.body.TopicConfigSerializeWrapper; -import org.apache.rocketmq.common.protocol.header.namesrv.RegisterBrokerRequestHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.RegisterBrokerResponseHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.UnRegisterBrokerRequestHeader; +import org.apache.rocketmq.common.sysflag.PullSysFlag; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.InvokeCallback; import org.apache.rocketmq.remoting.RPCHook; import org.apache.rocketmq.remoting.RemotingClient; import org.apache.rocketmq.remoting.exception.RemotingCommandException; import org.apache.rocketmq.remoting.exception.RemotingConnectException; +import org.apache.rocketmq.remoting.exception.RemotingException; import org.apache.rocketmq.remoting.exception.RemotingSendRequestException; import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; import org.apache.rocketmq.remoting.exception.RemotingTooMuchRequestException; import org.apache.rocketmq.remoting.netty.NettyClientConfig; import org.apache.rocketmq.remoting.netty.NettyRemotingClient; +import org.apache.rocketmq.remoting.netty.ResponseFuture; +import org.apache.rocketmq.remoting.protocol.BrokerSyncInfo; +import org.apache.rocketmq.remoting.protocol.DataVersion; import org.apache.rocketmq.remoting.protocol.RemotingCommand; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.BrokerMemberGroup; +import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.body.ConsumerOffsetSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.body.ElectMasterResponseBody; +import org.apache.rocketmq.remoting.protocol.body.GetBrokerMemberGroupResponseBody; +import org.apache.rocketmq.remoting.protocol.body.KVTable; +import org.apache.rocketmq.remoting.protocol.body.LockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.body.LockBatchResponseBody; +import org.apache.rocketmq.remoting.protocol.body.MessageRequestModeSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.body.RegisterBrokerBody; +import org.apache.rocketmq.remoting.protocol.body.SubscriptionGroupWrapper; +import org.apache.rocketmq.remoting.protocol.body.SyncStateSet; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigAndMappingSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.body.UnlockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.header.ExchangeHAInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ExchangeHAInfoResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.GetBrokerMemberGroupRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMaxOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMaxOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMinOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMinOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.LockBatchMqRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PullMessageResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeaderV2; +import org.apache.rocketmq.remoting.protocol.header.SendMessageResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.UnlockBatchMqRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.AlterSyncStateSetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetMetaDataResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetReplicaInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetReplicaInfoResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.ApplyBrokerIdRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.ApplyBrokerIdResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.GetNextBrokerIdRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.GetNextBrokerIdResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.RegisterBrokerToControllerRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.RegisterBrokerToControllerResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.BrokerHeartbeatRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.GetRouteInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.QueryDataVersionRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.QueryDataVersionResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.RegisterBrokerRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.RegisterBrokerResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.RegisterTopicRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.UnRegisterBrokerRequestHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.remoting.protocol.namesrv.RegisterBrokerResult; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.apache.rocketmq.remoting.rpc.ClientMetadata; +import org.apache.rocketmq.remoting.rpc.RpcClient; +import org.apache.rocketmq.remoting.rpc.RpcClientImpl; +import org.apache.rocketmq.remoting.rpchook.DynamicalExtFieldRPCHook; +import org.apache.rocketmq.store.timer.TimerCheckpoint; +import org.apache.rocketmq.store.timer.TimerMetrics; + +import static org.apache.rocketmq.remoting.protocol.RemotingSysResponseCode.SUCCESS; +import static org.apache.rocketmq.remoting.protocol.ResponseCode.CONTROLLER_MASTER_STILL_EXIST; public class BrokerOuterAPI { - private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); private final RemotingClient remotingClient; - private final TopAddressing topAddressing = new TopAddressing(MixAll.getWSAddr()); + private final TopAddressing topAddressing = new DefaultTopAddressing(MixAll.getWSAddr()); + private final ExecutorService brokerOuterExecutor = ThreadUtils.newThreadPoolExecutor(4, 10, 1, TimeUnit.MINUTES, + new ArrayBlockingQueue<>(32), new ThreadFactoryImpl("brokerOutApi_thread_", true)); + private final ClientMetadata clientMetadata; + private final RpcClient rpcClient; private String nameSrvAddr = null; public BrokerOuterAPI(final NettyClientConfig nettyClientConfig) { - this(nettyClientConfig, null); + this(nettyClientConfig, new DynamicalExtFieldRPCHook(), new ClientMetadata()); } - public BrokerOuterAPI(final NettyClientConfig nettyClientConfig, RPCHook rpcHook) { + private BrokerOuterAPI(final NettyClientConfig nettyClientConfig, RPCHook rpcHook, ClientMetadata clientMetadata) { this.remotingClient = new NettyRemotingClient(nettyClientConfig); + this.clientMetadata = clientMetadata; this.remotingClient.registerRPCHook(rpcHook); + this.rpcClient = new RpcClientImpl(this.clientMetadata, this.remotingClient); } public void start() { @@ -68,36 +171,257 @@ public void start() { public void shutdown() { this.remotingClient.shutdown(); + this.brokerOuterExecutor.shutdown(); + } + + public List getNameServerAddressList() { + return this.remotingClient.getNameServerAddressList(); } public String fetchNameServerAddr() { try { String addrs = this.topAddressing.fetchNSAddr(); - if (addrs != null) { + if (!UtilAll.isBlank(addrs)) { if (!addrs.equals(this.nameSrvAddr)) { - log.info("name server address changed, old: {} new: {}", this.nameSrvAddr, addrs); + LOGGER.info("name server address changed, old: {} new: {}", this.nameSrvAddr, addrs); this.updateNameServerAddressList(addrs); this.nameSrvAddr = addrs; return nameSrvAddr; } } } catch (Exception e) { - log.error("fetchNameServerAddr Exception", e); + LOGGER.error("fetchNameServerAddr Exception", e); } return nameSrvAddr; } + public List dnsLookupAddressByDomain(String domain) { + List addressList = new ArrayList<>(); + try { + java.security.Security.setProperty("networkaddress.cache.ttl", "10"); + int index = domain.indexOf(":"); + String portStr = domain.substring(index); + String domainStr = domain.substring(0, index); + InetAddress[] addresses = InetAddress.getAllByName(domainStr); + for (InetAddress address : addresses) { + addressList.add(address.getHostAddress() + portStr); + } + LOGGER.info("dns lookup address by domain success, domain={}, result={}", domain, addressList); + } catch (Exception e) { + LOGGER.error("dns lookup address by domain error, domain={}", domain, e); + } + return addressList; + } + + public boolean checkAddressReachable(String address) { + return this.remotingClient.isAddressReachable(address); + } + public void updateNameServerAddressList(final String addrs) { - List lst = new ArrayList(); String[] addrArray = addrs.split(";"); - for (String addr : addrArray) { - lst.add(addr); - } + List lst = new ArrayList(Arrays.asList(addrArray)); + this.remotingClient.updateNameServerAddressList(lst); + } + public void updateNameServerAddressListByDnsLookup(final String domain) { + List lst = this.dnsLookupAddressByDomain(domain); this.remotingClient.updateNameServerAddressList(lst); } - public RegisterBrokerResult registerBrokerAll( + public BrokerMemberGroup syncBrokerMemberGroup(String clusterName, String brokerName) + throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException { + return syncBrokerMemberGroup(clusterName, brokerName, false); + } + + public BrokerMemberGroup syncBrokerMemberGroup(String clusterName, String brokerName, + boolean isCompatibleWithOldNameSrv) + throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException { + if (isCompatibleWithOldNameSrv) { + return getBrokerMemberGroupCompatible(clusterName, brokerName); + } else { + return getBrokerMemberGroup(clusterName, brokerName); + } + } + + public BrokerMemberGroup getBrokerMemberGroup(String clusterName, String brokerName) + throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException { + BrokerMemberGroup brokerMemberGroup = new BrokerMemberGroup(clusterName, brokerName); + + GetBrokerMemberGroupRequestHeader requestHeader = new GetBrokerMemberGroupRequestHeader(); + requestHeader.setClusterName(clusterName); + requestHeader.setBrokerName(brokerName); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_BROKER_MEMBER_GROUP, requestHeader); + + RemotingCommand response = null; + response = this.remotingClient.invokeSync(null, request, 3000); + assert response != null; + + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + byte[] body = response.getBody(); + if (body != null) { + GetBrokerMemberGroupResponseBody brokerMemberGroupResponseBody = + GetBrokerMemberGroupResponseBody.decode(body, GetBrokerMemberGroupResponseBody.class); + + return brokerMemberGroupResponseBody.getBrokerMemberGroup(); + } + } + default: + break; + } + + return brokerMemberGroup; + } + + public BrokerMemberGroup getBrokerMemberGroupCompatible(String clusterName, String brokerName) + throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException { + BrokerMemberGroup brokerMemberGroup = new BrokerMemberGroup(clusterName, brokerName); + + GetRouteInfoRequestHeader requestHeader = new GetRouteInfoRequestHeader(); + requestHeader.setTopic(TopicValidator.SYNC_BROKER_MEMBER_GROUP_PREFIX + brokerName); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ROUTEINFO_BY_TOPIC, requestHeader); + + RemotingCommand response; + response = this.remotingClient.invokeSync(null, request, 3000); + assert response != null; + + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + byte[] body = response.getBody(); + if (body != null) { + TopicRouteData topicRouteData = TopicRouteData.decode(body, TopicRouteData.class); + for (BrokerData brokerData : topicRouteData.getBrokerDatas()) { + if (brokerData != null + && brokerData.getBrokerName().equals(brokerName) + && brokerData.getCluster().equals(clusterName)) { + brokerMemberGroup.getBrokerAddrs().putAll(brokerData.getBrokerAddrs()); + break; + } + } + return brokerMemberGroup; + } + } + default: + break; + } + + return brokerMemberGroup; + } + + public void sendHeartbeatViaDataVersion( + final String clusterName, + final String brokerAddr, + final String brokerName, + final Long brokerId, + final int timeoutMillis, + final DataVersion dataVersion, + final boolean isInBrokerContainer) { + List nameServerAddressList = this.remotingClient.getAvailableNameSrvList(); + if (nameServerAddressList != null && nameServerAddressList.size() > 0) { + final QueryDataVersionRequestHeader requestHeader = new QueryDataVersionRequestHeader(); + requestHeader.setBrokerAddr(brokerAddr); + requestHeader.setBrokerName(brokerName); + requestHeader.setBrokerId(brokerId); + requestHeader.setClusterName(clusterName); + + for (final String namesrvAddr : nameServerAddressList) { + brokerOuterExecutor.execute(new AbstractBrokerRunnable(new BrokerIdentity(clusterName, brokerName, brokerId, isInBrokerContainer)) { + + @Override + public void run0() { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_DATA_VERSION, requestHeader); + request.setBody(dataVersion.encode()); + + try { + BrokerOuterAPI.this.remotingClient.invokeOneway(namesrvAddr, request, timeoutMillis); + } catch (Exception e) { + LOGGER.error("sendHeartbeat Exception " + namesrvAddr, e); + } + } + }); + } + } + } + + public void sendHeartbeat(final String clusterName, + final String brokerAddr, + final String brokerName, + final Long brokerId, + final int timeoutMills, + final boolean isInBrokerContainer) { + List nameServerAddressList = this.remotingClient.getAvailableNameSrvList(); + + final BrokerHeartbeatRequestHeader requestHeader = new BrokerHeartbeatRequestHeader(); + requestHeader.setClusterName(clusterName); + requestHeader.setBrokerAddr(brokerAddr); + requestHeader.setBrokerName(brokerName); + + if (nameServerAddressList != null && nameServerAddressList.size() > 0) { + for (final String namesrvAddr : nameServerAddressList) { + brokerOuterExecutor.execute(new AbstractBrokerRunnable(new BrokerIdentity(clusterName, brokerName, brokerId, isInBrokerContainer)) { + @Override + public void run0() { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.BROKER_HEARTBEAT, requestHeader); + + try { + BrokerOuterAPI.this.remotingClient.invokeOneway(namesrvAddr, request, timeoutMills); + } catch (Exception e) { + LOGGER.error("sendHeartbeat Exception " + namesrvAddr, e); + } + } + }); + } + } + } + + public BrokerSyncInfo retrieveBrokerHaInfo(String masterBrokerAddr) + throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, + MQBrokerException, RemotingCommandException { + ExchangeHAInfoRequestHeader requestHeader = new ExchangeHAInfoRequestHeader(); + requestHeader.setMasterHaAddress(null); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.EXCHANGE_BROKER_HA_INFO, requestHeader); + + RemotingCommand response = this.remotingClient.invokeSync(masterBrokerAddr, request, 3000); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + ExchangeHAInfoResponseHeader responseHeader = (ExchangeHAInfoResponseHeader) response.decodeCommandCustomHeader(ExchangeHAInfoResponseHeader.class); + return new BrokerSyncInfo(responseHeader.getMasterHaAddress(), responseHeader.getMasterFlushOffset(), responseHeader.getMasterAddress()); + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public void sendBrokerHaInfo(String brokerAddr, String masterHaAddr, long brokerInitMaxOffset, String masterAddr) + throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQBrokerException { + ExchangeHAInfoRequestHeader requestHeader = new ExchangeHAInfoRequestHeader(); + requestHeader.setMasterHaAddress(masterHaAddr); + requestHeader.setMasterFlushOffset(brokerInitMaxOffset); + requestHeader.setMasterAddress(masterAddr); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.EXCHANGE_BROKER_HA_INFO, requestHeader); + + RemotingCommand response = this.remotingClient.invokeSync(brokerAddr, request, 3000); + + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return; + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public List registerBrokerAll( final String clusterName, final String brokerAddr, final String brokerName, @@ -106,54 +430,119 @@ public RegisterBrokerResult registerBrokerAll( final TopicConfigSerializeWrapper topicConfigWrapper, final List filterServerList, final boolean oneway, - final int timeoutMills) { - RegisterBrokerResult registerBrokerResult = null; + final int timeoutMills, + final boolean enableActingMaster, + final boolean compressed, + final BrokerIdentity brokerIdentity) { + return registerBrokerAll(clusterName, + brokerAddr, + brokerName, + brokerId, + haServerAddr, + topicConfigWrapper, + filterServerList, + oneway, timeoutMills, + enableActingMaster, + compressed, + null, + brokerIdentity); + } - List nameServerAddressList = this.remotingClient.getNameServerAddressList(); - if (nameServerAddressList != null) { - for (String namesrvAddr : nameServerAddressList) { - try { - RegisterBrokerResult result = this.registerBroker(namesrvAddr, clusterName, brokerAddr, brokerName, brokerId, - haServerAddr, topicConfigWrapper, filterServerList, oneway, timeoutMills); - if (result != null) { - registerBrokerResult = result; + /** + * Considering compression brings much CPU overhead to name server, stream API will not support compression and + * compression feature is deprecated. + * + * @param clusterName + * @param brokerAddr + * @param brokerName + * @param brokerId + * @param haServerAddr + * @param topicConfigWrapper + * @param filterServerList + * @param oneway + * @param timeoutMills + * @param compressed default false + * @return + */ + public List registerBrokerAll( + final String clusterName, + final String brokerAddr, + final String brokerName, + final long brokerId, + final String haServerAddr, + final TopicConfigSerializeWrapper topicConfigWrapper, + final List filterServerList, + final boolean oneway, + final int timeoutMills, + final boolean enableActingMaster, + final boolean compressed, + final Long heartbeatTimeoutMillis, + final BrokerIdentity brokerIdentity) { + + final List registerBrokerResultList = new CopyOnWriteArrayList<>(); + List nameServerAddressList = this.remotingClient.getAvailableNameSrvList(); + if (nameServerAddressList != null && nameServerAddressList.size() > 0) { + + final RegisterBrokerRequestHeader requestHeader = new RegisterBrokerRequestHeader(); + requestHeader.setBrokerAddr(brokerAddr); + requestHeader.setBrokerId(brokerId); + requestHeader.setBrokerName(brokerName); + requestHeader.setClusterName(clusterName); + requestHeader.setHaServerAddr(haServerAddr); + requestHeader.setEnableActingMaster(enableActingMaster); + requestHeader.setCompressed(false); + if (heartbeatTimeoutMillis != null) { + requestHeader.setHeartbeatTimeoutMillis(heartbeatTimeoutMillis); + } + + RegisterBrokerBody requestBody = new RegisterBrokerBody(); + requestBody.setTopicConfigSerializeWrapper(TopicConfigAndMappingSerializeWrapper.from(topicConfigWrapper)); + requestBody.setFilterServerList(filterServerList); + final byte[] body = requestBody.encode(compressed); + final int bodyCrc32 = UtilAll.crc32(body); + requestHeader.setBodyCrc32(bodyCrc32); + final CountDownLatch countDownLatch = new CountDownLatch(nameServerAddressList.size()); + for (final String namesrvAddr : nameServerAddressList) { + brokerOuterExecutor.execute(new AbstractBrokerRunnable(brokerIdentity) { + @Override + public void run0() { + try { + RegisterBrokerResult result = registerBroker(namesrvAddr, oneway, timeoutMills, requestHeader, body); + if (result != null) { + registerBrokerResultList.add(result); + } + + LOGGER.info("Registering current broker to name server completed. TargetHost={}", namesrvAddr); + } catch (Exception e) { + LOGGER.error("Failed to register current broker to name server. TargetHost={}", namesrvAddr, e); + } finally { + countDownLatch.countDown(); + } } + }); + } - log.info("register broker to name server {} OK", namesrvAddr); - } catch (Exception e) { - log.warn("registerBroker Exception, {}", namesrvAddr, e); + try { + if (!countDownLatch.await(timeoutMills, TimeUnit.MILLISECONDS)) { + LOGGER.warn("Registration to one or more name servers does NOT complete within deadline. Timeout threshold: {}ms", timeoutMills); } + } catch (InterruptedException ignore) { } } - return registerBrokerResult; + return registerBrokerResultList; } private RegisterBrokerResult registerBroker( final String namesrvAddr, - final String clusterName, - final String brokerAddr, - final String brokerName, - final long brokerId, - final String haServerAddr, - final TopicConfigSerializeWrapper topicConfigWrapper, - final List filterServerList, final boolean oneway, - final int timeoutMills + final int timeoutMills, + final RegisterBrokerRequestHeader requestHeader, + final byte[] body ) throws RemotingCommandException, MQBrokerException, RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException { - RegisterBrokerRequestHeader requestHeader = new RegisterBrokerRequestHeader(); - requestHeader.setBrokerAddr(brokerAddr); - requestHeader.setBrokerId(brokerId); - requestHeader.setBrokerName(brokerName); - requestHeader.setClusterName(clusterName); - requestHeader.setHaServerAddr(haServerAddr); RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.REGISTER_BROKER, requestHeader); - - RegisterBrokerBody requestBody = new RegisterBrokerBody(); - requestBody.setTopicConfigSerializeWrapper(topicConfigWrapper); - requestBody.setFilterServerList(filterServerList); - request.setBody(requestBody.encode()); + request.setBody(body); if (oneway) { try { @@ -182,7 +571,7 @@ private RegisterBrokerResult registerBroker( break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), requestHeader == null ? null : requestHeader.getBrokerAddr()); } public void unregisterBrokerAll( @@ -196,9 +585,9 @@ public void unregisterBrokerAll( for (String namesrvAddr : nameServerAddressList) { try { this.unregisterBroker(namesrvAddr, clusterName, brokerAddr, brokerName, brokerId); - log.info("unregisterBroker OK, NamesrvAddr: {}", namesrvAddr); + LOGGER.info("unregisterBroker OK, NamesrvAddr: {}", namesrvAddr); } catch (Exception e) { - log.warn("unregisterBroker Exception, {}", namesrvAddr, e); + LOGGER.warn("unregisterBroker Exception, NamesrvAddr: {}", namesrvAddr, e); } } } @@ -228,10 +617,134 @@ public void unregisterBroker( break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), brokerAddr); + } + + /** + * Register the topic route info of single topic to all name server nodes. + * This method is used to replace incremental broker registration feature. + */ + public void registerSingleTopicAll( + final String brokerName, + final TopicConfig topicConfig, + final int timeoutMills) { + String topic = topicConfig.getTopicName(); + RegisterTopicRequestHeader requestHeader = new RegisterTopicRequestHeader(); + requestHeader.setTopic(topic); + + TopicRouteData topicRouteData = new TopicRouteData(); + List queueDatas = new ArrayList<>(); + topicRouteData.setQueueDatas(queueDatas); + + final QueueData queueData = new QueueData(); + queueData.setBrokerName(brokerName); + queueData.setPerm(topicConfig.getPerm()); + queueData.setReadQueueNums(topicConfig.getReadQueueNums()); + queueData.setWriteQueueNums(topicConfig.getWriteQueueNums()); + queueData.setTopicSysFlag(topicConfig.getTopicSysFlag()); + queueDatas.add(queueData); + final byte[] topicRouteBody = topicRouteData.encode(); + + List nameServerAddressList = this.remotingClient.getNameServerAddressList(); + final CountDownLatch countDownLatch = new CountDownLatch(nameServerAddressList.size()); + for (final String namesrvAddr : nameServerAddressList) { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.REGISTER_TOPIC_IN_NAMESRV, requestHeader); + request.setBody(topicRouteBody); + + try { + brokerOuterExecutor.execute(() -> { + try { + RemotingCommand response = BrokerOuterAPI.this.remotingClient.invokeSync(namesrvAddr, request, timeoutMills); + assert response != null; + LOGGER.info("Register single topic {} to broker {} with response code {}", topic, brokerName, response.getCode()); + } catch (Exception e) { + LOGGER.warn("Register single topic {} to broker {} exception", topic, brokerName, e); + } finally { + countDownLatch.countDown(); + } + }); + } catch (Exception e) { + LOGGER.warn("Execute single topic registration task failed, topic {}, broker name {}", topic, brokerName); + countDownLatch.countDown(); + } + + } + + try { + if (!countDownLatch.await(timeoutMills, TimeUnit.MILLISECONDS)) { + LOGGER.warn("Registration single topic to one or more name servers timeout. Timeout threshold: {}ms", timeoutMills); + } + } catch (InterruptedException ignore) { + } } - public TopicConfigSerializeWrapper getAllTopicConfig( + public List needRegister( + final String clusterName, + final String brokerAddr, + final String brokerName, + final long brokerId, + final TopicConfigSerializeWrapper topicConfigWrapper, + final int timeoutMills, + final boolean isInBrokerContainer) { + final List changedList = new CopyOnWriteArrayList<>(); + List nameServerAddressList = this.remotingClient.getNameServerAddressList(); + if (nameServerAddressList != null && nameServerAddressList.size() > 0) { + final CountDownLatch countDownLatch = new CountDownLatch(nameServerAddressList.size()); + for (final String namesrvAddr : nameServerAddressList) { + brokerOuterExecutor.execute(new AbstractBrokerRunnable(new BrokerIdentity(clusterName, brokerName, brokerId, isInBrokerContainer)) { + @Override + public void run0() { + try { + QueryDataVersionRequestHeader requestHeader = new QueryDataVersionRequestHeader(); + requestHeader.setBrokerAddr(brokerAddr); + requestHeader.setBrokerId(brokerId); + requestHeader.setBrokerName(brokerName); + requestHeader.setClusterName(clusterName); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_DATA_VERSION, requestHeader); + request.setBody(topicConfigWrapper.getDataVersion().encode()); + RemotingCommand response = remotingClient.invokeSync(namesrvAddr, request, timeoutMills); + DataVersion nameServerDataVersion = null; + Boolean changed = false; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + QueryDataVersionResponseHeader queryDataVersionResponseHeader = + (QueryDataVersionResponseHeader) response.decodeCommandCustomHeader(QueryDataVersionResponseHeader.class); + changed = queryDataVersionResponseHeader.getChanged(); + byte[] body = response.getBody(); + if (body != null) { + nameServerDataVersion = DataVersion.decode(body, DataVersion.class); + if (!topicConfigWrapper.getDataVersion().equals(nameServerDataVersion)) { + changed = true; + } + } + if (changed == null || changed) { + changedList.add(Boolean.TRUE); + } + } + default: + break; + } + LOGGER.warn("Query data version from name server {} OK, changed {}, broker {}, name server {}", namesrvAddr, changed, topicConfigWrapper.getDataVersion(), nameServerDataVersion == null ? "" : nameServerDataVersion); + } catch (Exception e) { + changedList.add(Boolean.TRUE); + LOGGER.error("Query data version from name server {} exception", namesrvAddr, e); + } finally { + countDownLatch.countDown(); + } + } + }); + + } + try { + countDownLatch.await(timeoutMills, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + LOGGER.error("query dataversion from nameserver countDownLatch await Exception", e); + } + } + return changedList; + } + + public TopicConfigAndMappingSerializeWrapper getAllTopicConfig( final String addr) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException { RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ALL_TOPIC_CONFIG, null); @@ -240,13 +753,49 @@ public TopicConfigSerializeWrapper getAllTopicConfig( assert response != null; switch (response.getCode()) { case ResponseCode.SUCCESS: { - return TopicConfigSerializeWrapper.decode(response.getBody(), TopicConfigSerializeWrapper.class); + return TopicConfigSerializeWrapper.decode(response.getBody(), TopicConfigAndMappingSerializeWrapper.class); } default: break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); + } + + public TimerCheckpoint getTimerCheckPoint( + final String addr) throws RemotingConnectException, RemotingSendRequestException, + RemotingTimeoutException, InterruptedException, MQBrokerException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_TIMER_CHECK_POINT, null); + + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(true, addr), request, 3000); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return TimerCheckpoint.decode(ByteBuffer.wrap(response.getBody())); + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); + } + + public TimerMetrics.TimerMetricsSerializeWrapper getTimerMetrics( + final String addr) throws RemotingConnectException, RemotingSendRequestException, + RemotingTimeoutException, InterruptedException, MQBrokerException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_TIMER_METRICS, null); + + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(true, addr), request, 3000); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return TimerMetrics.TimerMetricsSerializeWrapper.decode(response.getBody(), TimerMetrics.TimerMetricsSerializeWrapper.class); + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); } public ConsumerOffsetSerializeWrapper getAllConsumerOffset( @@ -263,7 +812,7 @@ public ConsumerOffsetSerializeWrapper getAllConsumerOffset( break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); } public String getAllDelayOffset( @@ -280,7 +829,7 @@ public String getAllDelayOffset( break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); } public SubscriptionGroupWrapper getAllSubscriptionGroupConfig( @@ -297,10 +846,632 @@ public SubscriptionGroupWrapper getAllSubscriptionGroupConfig( break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); } public void registerRPCHook(RPCHook rpcHook) { remotingClient.registerRPCHook(rpcHook); } + + public void clearRPCHook() { + remotingClient.clearRPCHook(); + } + + public long getMaxOffset(final String addr, final String topic, final int queueId, final boolean committed, + final boolean isOnlyThisBroker) + throws RemotingException, MQBrokerException, InterruptedException { + GetMaxOffsetRequestHeader requestHeader = new GetMaxOffsetRequestHeader(); + requestHeader.setTopic(topic); + requestHeader.setQueueId(queueId); + requestHeader.setCommitted(committed); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_MAX_OFFSET, requestHeader); + + RemotingCommand response = this.remotingClient.invokeSync(addr, request, 3000); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + GetMaxOffsetResponseHeader responseHeader = (GetMaxOffsetResponseHeader) response.decodeCommandCustomHeader(GetMaxOffsetResponseHeader.class); + + return responseHeader.getOffset(); + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public long getMinOffset(final String addr, final String topic, final int queueId, final boolean isOnlyThisBroker) + throws RemotingException, MQBrokerException, InterruptedException { + GetMinOffsetRequestHeader requestHeader = new GetMinOffsetRequestHeader(); + requestHeader.setTopic(topic); + requestHeader.setQueueId(queueId); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_MIN_OFFSET, requestHeader); + + RemotingCommand response = this.remotingClient.invokeSync(addr, request, 3000); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + GetMinOffsetResponseHeader responseHeader = (GetMinOffsetResponseHeader) response.decodeCommandCustomHeader(GetMinOffsetResponseHeader.class); + + return responseHeader.getOffset(); + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public void lockBatchMQAsync( + final String addr, + final LockBatchRequestBody requestBody, + final long timeoutMillis, + final LockCallback callback) throws RemotingException, InterruptedException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.LOCK_BATCH_MQ, new LockBatchMqRequestHeader()); + + request.setBody(requestBody.encode()); + this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { + + } + + @Override + public void operationSucceed(RemotingCommand response) { + if (callback == null) { + return; + } + if (response.getCode() == ResponseCode.SUCCESS) { + LockBatchResponseBody responseBody = LockBatchResponseBody.decode(response.getBody(), + LockBatchResponseBody.class); + Set messageQueues = responseBody.getLockOKMQSet(); + callback.onSuccess(messageQueues); + } else { + callback.onException(new MQBrokerException(response.getCode(), response.getRemark())); + } + } + + @Override + public void operationFail(Throwable throwable) { + if (callback == null) { + return; + } + callback.onException(throwable); + } + }); + } + + public void unlockBatchMQAsync( + final String addr, + final UnlockBatchRequestBody requestBody, + final long timeoutMillis, + final UnlockCallback callback) throws RemotingException, InterruptedException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UNLOCK_BATCH_MQ, new UnlockBatchMqRequestHeader()); + + request.setBody(requestBody.encode()); + + this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { + + } + + @Override + public void operationSucceed(RemotingCommand response) { + if (callback == null) { + return; + } + if (response.getCode() == ResponseCode.SUCCESS) { + callback.onSuccess(); + } else { + callback.onException(new MQBrokerException(response.getCode(), response.getRemark())); + } + } + + @Override + public void operationFail(Throwable throwable) { + if (callback == null) { + return; + } + callback.onException(throwable); + } + }); + } + + public RemotingClient getRemotingClient() { + return this.remotingClient; + } + + public SendResult sendMessageToSpecificBroker(String brokerAddr, final String brokerName, + final MessageExt msg, String group, + long timeoutMillis) throws RemotingException, MQBrokerException, InterruptedException { + + RemotingCommand request = buildSendMessageRequest(msg, group); + RemotingCommand response = this.remotingClient.invokeSync(brokerAddr, request, timeoutMillis); + return this.processSendResponse(brokerName, msg, response); + } + + public CompletableFuture sendMessageToSpecificBrokerAsync(String brokerAddr, final String brokerName, + final MessageExt msg, String group, + long timeoutMillis) { + RemotingCommand request = buildSendMessageRequest(msg, group); + + CompletableFuture cf = new CompletableFuture<>(); + final String msgId = msg.getMsgId(); + try { + this.remotingClient.invokeAsync(brokerAddr, request, timeoutMillis, new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { + + } + + @Override + public void operationSucceed(RemotingCommand response) { + try { + SendResult sendResult = processSendResponse(brokerName, msg, response); + cf.complete(sendResult); + } catch (MQBrokerException | RemotingCommandException e) { + LOGGER.error("processSendResponse in sendMessageToSpecificBrokerAsync failed, msgId=" + msgId, e); + cf.completeExceptionally(e); + } + } + + @Override + public void operationFail(Throwable throwable) { + cf.completeExceptionally(throwable); + } + }); + } catch (Throwable t) { + LOGGER.error("invokeAsync failed in sendMessageToSpecificBrokerAsync, msgId=" + msgId, t); + cf.completeExceptionally(t); + } + return cf; + } + + private static RemotingCommand buildSendMessageRequest(MessageExt msg, String group) { + SendMessageRequestHeaderV2 requestHeaderV2 = buildSendMessageRequestHeaderV2(msg, group); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE_V2, requestHeaderV2); + + request.setBody(msg.getBody()); + return request; + } + + private static SendMessageRequestHeaderV2 buildSendMessageRequestHeaderV2(MessageExt msg, String group) { + SendMessageRequestHeader requestHeader = new SendMessageRequestHeader(); + requestHeader.setProducerGroup(group); + requestHeader.setTopic(msg.getTopic()); + requestHeader.setDefaultTopic(TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC); + requestHeader.setDefaultTopicQueueNums(8); + requestHeader.setQueueId(msg.getQueueId()); + requestHeader.setSysFlag(msg.getSysFlag()); + requestHeader.setBornTimestamp(msg.getBornTimestamp()); + requestHeader.setFlag(msg.getFlag()); + requestHeader.setProperties(MessageDecoder.messageProperties2String(msg.getProperties())); + requestHeader.setReconsumeTimes(msg.getReconsumeTimes()); + requestHeader.setBatch(false); + + SendMessageRequestHeaderV2 requestHeaderV2 = SendMessageRequestHeaderV2.createSendMessageRequestHeaderV2(requestHeader); + return requestHeaderV2; + } + + private SendResult processSendResponse( + final String brokerName, + final Message msg, + final RemotingCommand response + ) throws MQBrokerException, RemotingCommandException { + SendStatus sendStatus = null; + switch (response.getCode()) { + case ResponseCode.FLUSH_DISK_TIMEOUT: + sendStatus = SendStatus.FLUSH_DISK_TIMEOUT; + break; + case ResponseCode.FLUSH_SLAVE_TIMEOUT: + sendStatus = SendStatus.FLUSH_SLAVE_TIMEOUT; + break; + case ResponseCode.SLAVE_NOT_AVAILABLE: + sendStatus = SendStatus.SLAVE_NOT_AVAILABLE; + break; + case ResponseCode.SUCCESS: { + sendStatus = SendStatus.SEND_OK; + break; + } + default: + break; + } + if (sendStatus != null) { + SendMessageResponseHeader responseHeader = + (SendMessageResponseHeader) response.decodeCommandCustomHeader(SendMessageResponseHeader.class); + + //If namespace not null , reset Topic without namespace. + String topic = msg.getTopic(); + + MessageQueue messageQueue = new MessageQueue(topic, brokerName, responseHeader.getQueueId()); + + String uniqMsgId = MessageClientIDSetter.getUniqID(msg); + if (msg instanceof MessageBatch) { + StringBuilder sb = new StringBuilder(); + for (Message message : (MessageBatch) msg) { + sb.append(sb.length() == 0 ? "" : ",").append(MessageClientIDSetter.getUniqID(message)); + } + uniqMsgId = sb.toString(); + } + SendResult sendResult = new SendResult(sendStatus, + uniqMsgId, + responseHeader.getMsgId(), messageQueue, responseHeader.getQueueOffset()); + sendResult.setTransactionId(responseHeader.getTransactionId()); + String regionId = response.getExtFields().get(MessageConst.PROPERTY_MSG_REGION); + String traceOn = response.getExtFields().get(MessageConst.PROPERTY_TRACE_SWITCH); + if (regionId == null || regionId.isEmpty()) { + regionId = MixAll.DEFAULT_TRACE_REGION_ID; + } + if (traceOn != null && traceOn.equals("false")) { + sendResult.setTraceOn(false); + } else { + sendResult.setTraceOn(true); + } + sendResult.setRegionId(regionId); + return sendResult; + } + + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public ExecutorService getBrokerOuterExecutor() { + return brokerOuterExecutor; + } + + public TopicRouteData getTopicRouteInfoFromNameServer(final String topic, final long timeoutMillis) + throws RemotingException, MQBrokerException, InterruptedException { + return getTopicRouteInfoFromNameServer(topic, timeoutMillis, true); + } + + public TopicRouteData getTopicRouteInfoFromNameServer(final String topic, final long timeoutMillis, + boolean allowTopicNotExist) throws MQBrokerException, InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException { + GetRouteInfoRequestHeader requestHeader = new GetRouteInfoRequestHeader(); + requestHeader.setTopic(topic); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ROUTEINFO_BY_TOPIC, requestHeader); + + RemotingCommand response = this.remotingClient.invokeSync(null, request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.TOPIC_NOT_EXIST: { + if (allowTopicNotExist) { + LOGGER.warn("get Topic [{}] RouteInfoFromNameServer is not exist value", topic); + } + + break; + } + case ResponseCode.SUCCESS: { + byte[] body = response.getBody(); + if (body != null) { + return TopicRouteData.decode(body, TopicRouteData.class); + } + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public ClusterInfo getBrokerClusterInfo() throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQBrokerException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_BROKER_CLUSTER_INFO, null); + RemotingCommand response = this.remotingClient.invokeSync(null, request, 3_000); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return ClusterInfo.decode(response.getBody(), ClusterInfo.class); + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public void forwardRequest(String brokerAddr, RemotingCommand request, long timeoutMillis, + InvokeCallback invokeCallback) throws InterruptedException, RemotingSendRequestException, RemotingTimeoutException, RemotingTooMuchRequestException, RemotingConnectException { + this.remotingClient.invokeAsync(brokerAddr, request, timeoutMillis, invokeCallback); + } + + public void refreshMetadata() throws Exception { + ClusterInfo brokerClusterInfo = getBrokerClusterInfo(); + clientMetadata.refreshClusterInfo(brokerClusterInfo); + } + + public ClientMetadata getClientMetadata() { + return clientMetadata; + } + + public RpcClient getRpcClient() { + return rpcClient; + } + + public MessageRequestModeSerializeWrapper getAllMessageRequestMode( + final String addr) throws RemotingSendRequestException, RemotingConnectException, + MQBrokerException, RemotingTimeoutException, InterruptedException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ALL_MESSAGE_REQUEST_MODE, null); + RemotingCommand response = this.remotingClient.invokeSync(addr, request, 3000); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return MessageRequestModeSerializeWrapper.decode(response.getBody(), MessageRequestModeSerializeWrapper.class); + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); + } + + public GetMetaDataResponseHeader getControllerMetaData(final String controllerAddress) throws Exception { + final RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CONTROLLER_GET_METADATA_INFO, null); + final RemotingCommand response = this.remotingClient.invokeSync(controllerAddress, request, 3000); + assert response != null; + if (response.getCode() == SUCCESS) { + return (GetMetaDataResponseHeader) response.decodeCommandCustomHeader(GetMetaDataResponseHeader.class); + } + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + /** + * Alter syncStateSet + */ + public SyncStateSet alterSyncStateSet( + final String controllerAddress, + final String brokerName, + final Long masterBrokerId, final int masterEpoch, + final Set newSyncStateSet, final int syncStateSetEpoch) throws Exception { + + final AlterSyncStateSetRequestHeader requestHeader = new AlterSyncStateSetRequestHeader(brokerName, masterBrokerId, masterEpoch); + final RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CONTROLLER_ALTER_SYNC_STATE_SET, requestHeader); + request.setBody(new SyncStateSet(newSyncStateSet, syncStateSetEpoch).encode()); + final RemotingCommand response = this.remotingClient.invokeSync(controllerAddress, request, 3000); + assert response != null; + switch (response.getCode()) { + case SUCCESS: { + assert response.getBody() != null; + return RemotingSerializable.decode(response.getBody(), SyncStateSet.class); + } + } + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + /** + * Broker try to elect itself as a master in broker set + */ + public Pair> brokerElect(String controllerAddress, String clusterName, + String brokerName, + Long brokerId) throws Exception { + + final ElectMasterRequestHeader requestHeader = ElectMasterRequestHeader.ofBrokerTrigger(clusterName, brokerName, brokerId); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CONTROLLER_ELECT_MASTER, requestHeader); + RemotingCommand response = this.remotingClient.invokeSync(controllerAddress, request, 3000); + assert response != null; + switch (response.getCode()) { + // Only record success response. + case CONTROLLER_MASTER_STILL_EXIST: + case SUCCESS: + final ElectMasterResponseHeader responseHeader = (ElectMasterResponseHeader) response.decodeCommandCustomHeader(ElectMasterResponseHeader.class); + final ElectMasterResponseBody responseBody = RemotingSerializable.decode(response.getBody(), ElectMasterResponseBody.class); + return new Pair<>(responseHeader, responseBody.getSyncStateSet()); + } + + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public GetNextBrokerIdResponseHeader getNextBrokerId(final String clusterName, final String brokerName, + final String controllerAddress) throws Exception { + final GetNextBrokerIdRequestHeader requestHeader = new GetNextBrokerIdRequestHeader(clusterName, brokerName); + final RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CONTROLLER_GET_NEXT_BROKER_ID, requestHeader); + final RemotingCommand response = this.remotingClient.invokeSync(controllerAddress, request, 3000); + assert response != null; + if (response.getCode() == SUCCESS) { + return (GetNextBrokerIdResponseHeader) response.decodeCommandCustomHeader(GetNextBrokerIdResponseHeader.class); + } + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public ApplyBrokerIdResponseHeader applyBrokerId(final String clusterName, final String brokerName, + final Long brokerId, final String registerCheckCode, final String controllerAddress) throws Exception { + final ApplyBrokerIdRequestHeader requestHeader = new ApplyBrokerIdRequestHeader(clusterName, brokerName, brokerId, registerCheckCode); + final RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CONTROLLER_APPLY_BROKER_ID, requestHeader); + final RemotingCommand response = this.remotingClient.invokeSync(controllerAddress, request, 3000); + assert response != null; + if (response.getCode() == SUCCESS) { + return (ApplyBrokerIdResponseHeader) response.decodeCommandCustomHeader(ApplyBrokerIdResponseHeader.class); + } + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public Pair> registerBrokerToController( + final String clusterName, final String brokerName, final Long brokerId, final String brokerAddress, + final String controllerAddress) throws Exception { + final RegisterBrokerToControllerRequestHeader requestHeader = new RegisterBrokerToControllerRequestHeader(clusterName, brokerName, brokerId, brokerAddress); + final RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CONTROLLER_REGISTER_BROKER, requestHeader); + final RemotingCommand response = this.remotingClient.invokeSync(controllerAddress, request, 3000); + assert response != null; + if (response.getCode() == SUCCESS) { + RegisterBrokerToControllerResponseHeader responseHeader = (RegisterBrokerToControllerResponseHeader) response.decodeCommandCustomHeader(RegisterBrokerToControllerResponseHeader.class); + Set syncStateSet = RemotingSerializable.decode(response.getBody(), SyncStateSet.class).getSyncStateSet(); + return new Pair<>(responseHeader, syncStateSet); + } + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + /** + * Get broker replica info + */ + public Pair getReplicaInfo(final String controllerAddress, + final String brokerName) throws Exception { + final GetReplicaInfoRequestHeader requestHeader = new GetReplicaInfoRequestHeader(brokerName); + final RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CONTROLLER_GET_REPLICA_INFO, requestHeader); + final RemotingCommand response = this.remotingClient.invokeSync(controllerAddress, request, 3000); + assert response != null; + switch (response.getCode()) { + case SUCCESS: { + final GetReplicaInfoResponseHeader header = (GetReplicaInfoResponseHeader) response.decodeCommandCustomHeader(GetReplicaInfoResponseHeader.class); + assert response.getBody() != null; + final SyncStateSet stateSet = RemotingSerializable.decode(response.getBody(), SyncStateSet.class); + return new Pair<>(header, stateSet); + } + } + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + /** + * Send heartbeat to controller + */ + public void sendHeartbeatToController(final String controllerAddress, + final String clusterName, + final String brokerAddr, + final String brokerName, + final Long brokerId, + final int sendHeartBeatTimeoutMills, + final boolean isInBrokerContainer, + final int epoch, + final long maxOffset, + final long confirmOffset, + final long controllerHeartBeatTimeoutMills, + final int electionPriority) { + if (StringUtils.isEmpty(controllerAddress)) { + return; + } + + final BrokerHeartbeatRequestHeader requestHeader = new BrokerHeartbeatRequestHeader(); + requestHeader.setClusterName(clusterName); + requestHeader.setBrokerAddr(brokerAddr); + requestHeader.setBrokerName(brokerName); + requestHeader.setEpoch(epoch); + requestHeader.setMaxOffset(maxOffset); + requestHeader.setConfirmOffset(confirmOffset); + requestHeader.setHeartbeatTimeoutMills(controllerHeartBeatTimeoutMills); + requestHeader.setElectionPriority(electionPriority); + requestHeader.setBrokerId(brokerId); + brokerOuterExecutor.execute(new AbstractBrokerRunnable(new BrokerIdentity(clusterName, brokerName, brokerId, isInBrokerContainer)) { + @Override + public void run0() { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.BROKER_HEARTBEAT, requestHeader); + + try { + BrokerOuterAPI.this.remotingClient.invokeOneway(controllerAddress, request, sendHeartBeatTimeoutMills); + } catch (Exception e) { + LOGGER.error("Error happen when send heartbeat to controller {}", controllerAddress, e); + } + } + }); + } + + public CompletableFuture pullMessageFromSpecificBrokerAsync(String brokerName, String brokerAddr, + String consumerGroup, String topic, int queueId, long offset, + int maxNums, long timeoutMillis) throws RemotingException, InterruptedException { + PullMessageRequestHeader requestHeader = new PullMessageRequestHeader(); + requestHeader.setConsumerGroup(consumerGroup); + requestHeader.setTopic(topic); + requestHeader.setQueueId(queueId); + requestHeader.setQueueOffset(offset); + requestHeader.setMaxMsgNums(maxNums); + requestHeader.setSysFlag(PullSysFlag.buildSysFlag(false, false, true, false)); + requestHeader.setCommitOffset(0L); + requestHeader.setSuspendTimeoutMillis(0L); + requestHeader.setSubscription(SubscriptionData.SUB_ALL); + requestHeader.setSubVersion(System.currentTimeMillis()); + requestHeader.setMaxMsgBytes(Integer.MAX_VALUE); + requestHeader.setExpressionType(ExpressionType.TAG); + requestHeader.setBrokerName(brokerName); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, requestHeader); + CompletableFuture pullResultFuture = new CompletableFuture<>(); + this.remotingClient.invokeAsync(brokerAddr, request, timeoutMillis, new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { + + } + + @Override + public void operationSucceed(RemotingCommand response) { + try { + PullResultExt pullResultExt = processPullResponse(response, brokerAddr); + processPullResult(pullResultExt, brokerName, queueId); + pullResultFuture.complete(pullResultExt); + } catch (Exception e) { + pullResultFuture.complete(new PullResult(PullStatus.NO_MATCHED_MSG, -1, -1, -1, new ArrayList<>())); + } + } + + @Override + public void operationFail(Throwable throwable) { + pullResultFuture.complete(new PullResult(PullStatus.NO_MATCHED_MSG, -1, -1, -1, new ArrayList<>())); + } + }); + return pullResultFuture; + } + + private PullResultExt processPullResponse( + final RemotingCommand response, + final String addr) throws MQBrokerException, RemotingCommandException { + PullStatus pullStatus = PullStatus.NO_NEW_MSG; + switch (response.getCode()) { + case ResponseCode.SUCCESS: + pullStatus = PullStatus.FOUND; + break; + case ResponseCode.PULL_NOT_FOUND: + pullStatus = PullStatus.NO_NEW_MSG; + break; + case ResponseCode.PULL_RETRY_IMMEDIATELY: + pullStatus = PullStatus.NO_MATCHED_MSG; + break; + case ResponseCode.PULL_OFFSET_MOVED: + pullStatus = PullStatus.OFFSET_ILLEGAL; + break; + + default: + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); + } + + PullMessageResponseHeader responseHeader = + (PullMessageResponseHeader) response.decodeCommandCustomHeader(PullMessageResponseHeader.class); + + return new PullResultExt(pullStatus, responseHeader.getNextBeginOffset(), responseHeader.getMinOffset(), + responseHeader.getMaxOffset(), null, responseHeader.getSuggestWhichBrokerId(), response.getBody(), responseHeader.getOffsetDelta()); + + } + + private PullResult processPullResult(final PullResultExt pullResult, String brokerName, int queueId) { + + if (PullStatus.FOUND == pullResult.getPullStatus()) { + ByteBuffer byteBuffer = ByteBuffer.wrap(pullResult.getMessageBinary()); + List msgList = MessageDecoder.decodesBatch( + byteBuffer, + true, + true, + true + ); + + // Currently batch messages are not supported + for (MessageExt msg : msgList) { + String traFlag = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED); + if (Boolean.parseBoolean(traFlag)) { + msg.setTransactionId(msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX)); + } + MessageAccessor.putProperty(msg, MessageConst.PROPERTY_MIN_OFFSET, + Long.toString(pullResult.getMinOffset())); + MessageAccessor.putProperty(msg, MessageConst.PROPERTY_MAX_OFFSET, + Long.toString(pullResult.getMaxOffset())); + msg.setBrokerName(brokerName); + msg.setQueueId(queueId); + if (pullResult.getOffsetDelta() != null) { + msg.setQueueOffset(pullResult.getOffsetDelta() + msg.getQueueOffset()); + } + } + + pullResult.setMsgFoundList(msgList); + } + + pullResult.setMessageBinary(null); + + return pullResult; + } + } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pagecache/ManyMessageTransfer.java b/broker/src/main/java/org/apache/rocketmq/broker/pagecache/ManyMessageTransfer.java index 968bcfb564f..373a0a48b67 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pagecache/ManyMessageTransfer.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pagecache/ManyMessageTransfer.java @@ -53,6 +53,11 @@ public long transfered() { return transferred; } + @Override + public long transferred() { + return transferred; + } + @Override public long count() { return byteBufferHeader.limit() + this.getMessageResult.getBufferTotalSize(); @@ -76,6 +81,28 @@ public long transferTo(WritableByteChannel target, long position) throws IOExcep return 0; } + @Override + public FileRegion retain() { + super.retain(); + return this; + } + + @Override + public FileRegion retain(int increment) { + super.retain(increment); + return this; + } + + @Override + public FileRegion touch() { + return this; + } + + @Override + public FileRegion touch(Object hint) { + return this; + } + public void close() { this.deallocate(); } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pagecache/OneMessageTransfer.java b/broker/src/main/java/org/apache/rocketmq/broker/pagecache/OneMessageTransfer.java index b795d2d253b..952cf4fbadd 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pagecache/OneMessageTransfer.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pagecache/OneMessageTransfer.java @@ -47,6 +47,11 @@ public long transfered() { return transferred; } + @Override + public long transferred() { + return transferred; + } + @Override public long count() { return this.byteBufferHeader.limit() + this.selectMappedBufferResult.getSize(); @@ -65,6 +70,29 @@ public long transferTo(WritableByteChannel target, long position) throws IOExcep return 0; } + + @Override + public FileRegion retain() { + super.retain(); + return this; + } + + @Override + public FileRegion retain(int increment) { + super.retain(increment); + return this; + } + + @Override + public FileRegion touch() { + return this; + } + + @Override + public FileRegion touch(Object hint) { + return this; + } + public void close() { this.deallocate(); } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/pagecache/QueryMessageTransfer.java b/broker/src/main/java/org/apache/rocketmq/broker/pagecache/QueryMessageTransfer.java index e8f30996677..db47b9e50e3 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/pagecache/QueryMessageTransfer.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/pagecache/QueryMessageTransfer.java @@ -53,6 +53,11 @@ public long transfered() { return transferred; } + @Override + public long transferred() { + return transferred; + } + @Override public long count() { return byteBufferHeader.limit() + this.queryMessageResult.getBufferTotalSize(); @@ -76,6 +81,28 @@ public long transferTo(WritableByteChannel target, long position) throws IOExcep return 0; } + @Override + public FileRegion retain() { + super.retain(); + return this; + } + + @Override + public FileRegion retain(int increment) { + super.retain(increment); + return this; + } + + @Override + public FileRegion touch() { + return this; + } + + @Override + public FileRegion touch(Object hint) { + return this; + } + public void close() { this.deallocate(); } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/plugin/AbstractPluginMessageStore.java b/broker/src/main/java/org/apache/rocketmq/broker/plugin/AbstractPluginMessageStore.java deleted file mode 100644 index f6f8a80afb6..00000000000 --- a/broker/src/main/java/org/apache/rocketmq/broker/plugin/AbstractPluginMessageStore.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.broker.plugin; - -import java.util.HashMap; -import java.util.LinkedList; -import java.util.Set; -import org.apache.rocketmq.common.message.MessageExt; -import org.apache.rocketmq.store.CommitLogDispatcher; -import org.apache.rocketmq.store.ConsumeQueue; -import org.apache.rocketmq.store.GetMessageResult; -import org.apache.rocketmq.store.MessageExtBrokerInner; -import org.apache.rocketmq.store.MessageFilter; -import org.apache.rocketmq.store.MessageStore; -import org.apache.rocketmq.store.PutMessageResult; -import org.apache.rocketmq.store.QueryMessageResult; -import org.apache.rocketmq.store.SelectMappedBufferResult; - -public abstract class AbstractPluginMessageStore implements MessageStore { - protected MessageStore next = null; - protected MessageStorePluginContext context; - - public AbstractPluginMessageStore(MessageStorePluginContext context, MessageStore next) { - this.next = next; - this.context = context; - } - - @Override - public long getEarliestMessageTime() { - return next.getEarliestMessageTime(); - } - - @Override - public long lockTimeMills() { - return next.lockTimeMills(); - } - - @Override - public boolean isOSPageCacheBusy() { - return next.isOSPageCacheBusy(); - } - - @Override - public boolean isTransientStorePoolDeficient() { - return next.isTransientStorePoolDeficient(); - } - - @Override - public boolean load() { - return next.load(); - } - - @Override - public void start() throws Exception { - next.start(); - } - - @Override - public void shutdown() { - next.shutdown(); - } - - @Override - public void destroy() { - next.destroy(); - } - - @Override - public PutMessageResult putMessage(MessageExtBrokerInner msg) { - return next.putMessage(msg); - } - - @Override - public GetMessageResult getMessage(String group, String topic, int queueId, long offset, - int maxMsgNums, final MessageFilter messageFilter) { - return next.getMessage(group, topic, queueId, offset, maxMsgNums, messageFilter); - } - - @Override - public long getMaxOffsetInQueue(String topic, int queueId) { - return next.getMaxOffsetInQueue(topic, queueId); - } - - @Override - public long getMinOffsetInQueue(String topic, int queueId) { - return next.getMinOffsetInQueue(topic, queueId); - } - - @Override - public long getCommitLogOffsetInQueue(String topic, int queueId, long consumeQueueOffset) { - return next.getCommitLogOffsetInQueue(topic, queueId, consumeQueueOffset); - } - - @Override - public long getOffsetInQueueByTime(String topic, int queueId, long timestamp) { - return next.getOffsetInQueueByTime(topic, queueId, timestamp); - } - - @Override - public MessageExt lookMessageByOffset(long commitLogOffset) { - return next.lookMessageByOffset(commitLogOffset); - } - - @Override - public SelectMappedBufferResult selectOneMessageByOffset(long commitLogOffset) { - return next.selectOneMessageByOffset(commitLogOffset); - } - - @Override - public SelectMappedBufferResult selectOneMessageByOffset(long commitLogOffset, int msgSize) { - return next.selectOneMessageByOffset(commitLogOffset, msgSize); - } - - @Override - public String getRunningDataInfo() { - return next.getRunningDataInfo(); - } - - @Override - public HashMap getRuntimeInfo() { - return next.getRuntimeInfo(); - } - - @Override - public long getMaxPhyOffset() { - return next.getMaxPhyOffset(); - } - - @Override - public long getMinPhyOffset() { - return next.getMinPhyOffset(); - } - - @Override - public long getEarliestMessageTime(String topic, int queueId) { - return next.getEarliestMessageTime(topic, queueId); - } - - @Override - public long getMessageStoreTimeStamp(String topic, int queueId, long consumeQueueOffset) { - return next.getMessageStoreTimeStamp(topic, queueId, consumeQueueOffset); - } - - @Override - public long getMessageTotalInQueue(String topic, int queueId) { - return next.getMessageTotalInQueue(topic, queueId); - } - - @Override - public SelectMappedBufferResult getCommitLogData(long offset) { - return next.getCommitLogData(offset); - } - - @Override - public boolean appendToCommitLog(long startOffset, byte[] data) { - return next.appendToCommitLog(startOffset, data); - } - - @Override - public void executeDeleteFilesManually() { - next.executeDeleteFilesManually(); - } - - @Override - public QueryMessageResult queryMessage(String topic, String key, int maxNum, long begin, - long end) { - return next.queryMessage(topic, key, maxNum, begin, end); - } - - @Override - public void updateHaMasterAddress(String newAddr) { - next.updateHaMasterAddress(newAddr); - } - - @Override - public long slaveFallBehindMuch() { - return next.slaveFallBehindMuch(); - } - - @Override - public long now() { - return next.now(); - } - - @Override - public int cleanUnusedTopic(Set topics) { - return next.cleanUnusedTopic(topics); - } - - @Override - public void cleanExpiredConsumerQueue() { - next.cleanExpiredConsumerQueue(); - } - - @Override - public boolean checkInDiskByConsumeOffset(String topic, int queueId, long consumeOffset) { - return next.checkInDiskByConsumeOffset(topic, queueId, consumeOffset); - } - - @Override - public long dispatchBehindBytes() { - return next.dispatchBehindBytes(); - } - - @Override - public long flush() { - return next.flush(); - } - - @Override - public boolean resetWriteOffset(long phyOffset) { - return next.resetWriteOffset(phyOffset); - } - - @Override - public long getConfirmOffset() { - return next.getConfirmOffset(); - } - - @Override - public void setConfirmOffset(long phyOffset) { - next.setConfirmOffset(phyOffset); - } - - @Override - public LinkedList getDispatcherList() { - return next.getDispatcherList(); - } - - @Override - public ConsumeQueue getConsumeQueue(String topic, int queueId) { - return next.getConsumeQueue(topic, queueId); - } -} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/plugin/BrokerAttachedPlugin.java b/broker/src/main/java/org/apache/rocketmq/broker/plugin/BrokerAttachedPlugin.java new file mode 100644 index 00000000000..0cd2a274765 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/plugin/BrokerAttachedPlugin.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.plugin; + +import java.util.Map; + +public interface BrokerAttachedPlugin { + + /** + * Get plugin name + * + * @return plugin name + */ + String pluginName(); + + /** + * Load broker attached plugin. + * + * @return load success or failed + */ + boolean load(); + + /** + * Start broker attached plugin. + */ + void start(); + + /** + * Shutdown broker attached plugin. + */ + void shutdown(); + + /** + * Sync metadata from master. + */ + void syncMetadata(); + + /** + * Sync metadata reverse from slave + * + * @param brokerAddr + */ + void syncMetadataReverse(String brokerAddr) throws Exception; + + /** + * Some plugin need build runningInfo when prepare runtime info. + * + * @param runtimeInfo + */ + void buildRuntimeInfo(Map runtimeInfo); + + /** + * Some plugin need do something when status changed. For example, brokerRole change to master or slave. + * + * @param shouldStart + */ + void statusChanged(boolean shouldStart); + +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/plugin/PullMessageResultHandler.java b/broker/src/main/java/org/apache/rocketmq/broker/plugin/PullMessageResultHandler.java new file mode 100644 index 00000000000..bddb57f1501 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/plugin/PullMessageResultHandler.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.plugin; + +import io.netty.channel.Channel; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingContext; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.MessageFilter; + +public interface PullMessageResultHandler { + + /** + * Handle result of get message from store. + * + * @param getMessageResult store result + * @param request request + * @param requestHeader request header + * @param channel channel + * @param subscriptionData sub data + * @param subscriptionGroupConfig sub config + * @param brokerAllowSuspend brokerAllowSuspend + * @param messageFilter store message filter + * @param response response + * @return response or null + */ + RemotingCommand handle(final GetMessageResult getMessageResult, + final RemotingCommand request, + final PullMessageRequestHeader requestHeader, + final Channel channel, + final SubscriptionData subscriptionData, + final SubscriptionGroupConfig subscriptionGroupConfig, + final boolean brokerAllowSuspend, + final MessageFilter messageFilter, + final RemotingCommand response, + final TopicQueueMappingContext mappingContext, + final long beginTimeMills); +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/AbstractSendMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/AbstractSendMessageProcessor.java index 410192f3b05..b348ecb8f09 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/AbstractSendMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/AbstractSendMessageProcessor.java @@ -17,12 +17,25 @@ package org.apache.rocketmq.broker.processor; import io.netty.channel.ChannelHandlerContext; +import io.opentelemetry.api.common.Attributes; +import java.net.SocketAddress; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.metrics.BrokerMetricsManager; +import org.apache.rocketmq.broker.mqtrace.ConsumeMessageContext; +import org.apache.rocketmq.broker.mqtrace.ConsumeMessageHook; import org.apache.rocketmq.broker.mqtrace.SendMessageContext; import org.apache.rocketmq.broker.mqtrace.SendMessageHook; +import org.apache.rocketmq.common.AbortProcessException; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MQVersion; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.TopicConfig; import org.apache.rocketmq.common.TopicFilterType; +import org.apache.rocketmq.common.UtilAll; import org.apache.rocketmq.common.constant.DBMsgConstants; import org.apache.rocketmq.common.constant.LoggerName; import org.apache.rocketmq.common.constant.PermName; @@ -30,58 +43,331 @@ import org.apache.rocketmq.common.message.MessageAccessor; import org.apache.rocketmq.common.message.MessageConst; import org.apache.rocketmq.common.message.MessageDecoder; -import org.apache.rocketmq.common.protocol.RequestCode; -import org.apache.rocketmq.common.protocol.ResponseCode; -import org.apache.rocketmq.common.protocol.header.SendMessageRequestHeader; -import org.apache.rocketmq.common.protocol.header.SendMessageRequestHeaderV2; -import org.apache.rocketmq.common.protocol.header.SendMessageResponseHeader; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.message.MessageType; import org.apache.rocketmq.common.sysflag.MessageSysFlag; import org.apache.rocketmq.common.sysflag.TopicSysFlag; -import org.apache.rocketmq.common.utils.ChannelUtil; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.remoting.common.RemotingHelper; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.netty.NettyRemotingAbstract; import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; import org.apache.rocketmq.remoting.protocol.RemotingCommand; -import org.apache.rocketmq.store.MessageExtBrokerInner; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.util.List; -import java.util.Map; -import java.util.Random; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.ConsumerSendMsgBackRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageResponseHeader; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.stats.BrokerStatsManager; + +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_CONSUMER_GROUP; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_IS_SYSTEM; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_TOPIC; public abstract class AbstractSendMessageProcessor implements NettyRequestProcessor { - protected static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + protected static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + protected static final Logger DLQ_LOG = LoggerFactory.getLogger(LoggerName.DLQ_LOGGER_NAME); + + protected List consumeMessageHookList; protected final static int DLQ_NUMS_PER_GROUP = 1; protected final BrokerController brokerController; protected final Random random = new Random(System.currentTimeMillis()); - protected final SocketAddress storeHost; private List sendMessageHookList; public AbstractSendMessageProcessor(final BrokerController brokerController) { this.brokerController = brokerController; - this.storeHost = - new InetSocketAddress(brokerController.getBrokerConfig().getBrokerIP1(), brokerController - .getNettyServerConfig().getListenPort()); } - protected SendMessageContext buildMsgContext(ChannelHandlerContext ctx, - SendMessageRequestHeader requestHeader) { - if (!this.hasSendMessageHook()) { - return null; + public void registerConsumeMessageHook(List consumeMessageHookList) { + this.consumeMessageHookList = consumeMessageHookList; + } + + protected RemotingCommand consumerSendMsgBack(final ChannelHandlerContext ctx, final RemotingCommand request) + throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + final ConsumerSendMsgBackRequestHeader requestHeader = + (ConsumerSendMsgBackRequestHeader) request.decodeCommandCustomHeader(ConsumerSendMsgBackRequestHeader.class); + + // The send back requests sent to SlaveBroker will be forwarded to the master broker beside + final BrokerController masterBroker = this.brokerController.peekMasterBroker(); + if (null == masterBroker) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("no master available along with " + brokerController.getBrokerConfig().getBrokerIP1()); + return response; + } + + // The broker that received the request. + // It may be a master broker or a slave broker + final BrokerController currentBroker = this.brokerController; + + SubscriptionGroupConfig subscriptionGroupConfig = + masterBroker.getSubscriptionGroupManager().findSubscriptionGroupConfig(requestHeader.getGroup()); + if (null == subscriptionGroupConfig) { + response.setCode(ResponseCode.SUBSCRIPTION_GROUP_NOT_EXIST); + response.setRemark("subscription group not exist, " + requestHeader.getGroup() + " " + + FAQUrl.suggestTodo(FAQUrl.SUBSCRIPTION_GROUP_NOT_EXIST)); + return response; + } + + BrokerConfig masterBrokerConfig = masterBroker.getBrokerConfig(); + if (!PermName.isWriteable(masterBrokerConfig.getBrokerPermission())) { + response.setCode(ResponseCode.NO_PERMISSION); + response.setRemark("the broker[" + masterBrokerConfig.getBrokerIP1() + "] sending message is forbidden"); + return response; + } + + if (subscriptionGroupConfig.getRetryQueueNums() <= 0) { + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + String newTopic = MixAll.getRetryTopic(requestHeader.getGroup()); + int queueIdInt = this.random.nextInt(subscriptionGroupConfig.getRetryQueueNums()); + + int topicSysFlag = 0; + if (requestHeader.isUnitMode()) { + topicSysFlag = TopicSysFlag.buildSysFlag(false, true); + } + + // Create retry topic to master broker + TopicConfig topicConfig = masterBroker.getTopicConfigManager().createTopicInSendMessageBackMethod( + newTopic, + subscriptionGroupConfig.getRetryQueueNums(), + PermName.PERM_WRITE | PermName.PERM_READ, topicSysFlag); + if (null == topicConfig) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("topic[" + newTopic + "] not exist"); + return response; + } + + if (!PermName.isWriteable(topicConfig.getPerm())) { + response.setCode(ResponseCode.NO_PERMISSION); + response.setRemark(String.format("the topic[%s] sending message is forbidden", newTopic)); + return response; + } + + // Look message from the origin message store + MessageExt msgExt = currentBroker.getMessageStore().lookMessageByOffset(requestHeader.getOffset()); + if (null == msgExt) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("look message by offset failed, " + requestHeader.getOffset()); + return response; + } + + final String retryTopic = msgExt.getProperty(MessageConst.PROPERTY_RETRY_TOPIC); + if (null == retryTopic) { + MessageAccessor.putProperty(msgExt, MessageConst.PROPERTY_RETRY_TOPIC, msgExt.getTopic()); + } + msgExt.setWaitStoreMsgOK(false); + + int delayLevel = requestHeader.getDelayLevel(); + + int maxReconsumeTimes = subscriptionGroupConfig.getRetryMaxTimes(); + if (request.getVersion() >= MQVersion.Version.V3_4_9.ordinal()) { + Integer times = requestHeader.getMaxReconsumeTimes(); + if (times != null) { + maxReconsumeTimes = times; + } + } + + boolean isDLQ = false; + if (msgExt.getReconsumeTimes() >= maxReconsumeTimes + || delayLevel < 0) { + + Attributes attributes = BrokerMetricsManager.newAttributesBuilder() + .put(LABEL_CONSUMER_GROUP, requestHeader.getGroup()) + .put(LABEL_TOPIC, requestHeader.getOriginTopic()) + .put(LABEL_IS_SYSTEM, BrokerMetricsManager.isSystem(requestHeader.getOriginTopic(), requestHeader.getGroup())) + .build(); + BrokerMetricsManager.sendToDlqMessages.add(1, attributes); + + isDLQ = true; + newTopic = MixAll.getDLQTopic(requestHeader.getGroup()); + queueIdInt = randomQueueId(DLQ_NUMS_PER_GROUP); + + // Create DLQ topic to master broker + topicConfig = masterBroker.getTopicConfigManager().createTopicInSendMessageBackMethod(newTopic, + DLQ_NUMS_PER_GROUP, + PermName.PERM_WRITE | PermName.PERM_READ, 0); + + if (null == topicConfig) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("topic[" + newTopic + "] not exist"); + return response; + } + msgExt.setDelayTimeLevel(0); + } else { + if (0 == delayLevel) { + delayLevel = 3 + msgExt.getReconsumeTimes(); + } + + msgExt.setDelayTimeLevel(delayLevel); } - SendMessageContext mqtraceContext; - mqtraceContext = new SendMessageContext(); - mqtraceContext.setProducerGroup(requestHeader.getProducerGroup()); - mqtraceContext.setTopic(requestHeader.getTopic()); - mqtraceContext.setMsgProps(requestHeader.getProperties()); - mqtraceContext.setBornHost(RemotingHelper.parseChannelRemoteAddr(ctx.channel())); - mqtraceContext.setBrokerAddr(this.brokerController.getBrokerAddr()); - mqtraceContext.setBrokerRegionId(this.brokerController.getBrokerConfig().getRegionId()); - mqtraceContext.setBornTimeStamp(requestHeader.getBornTimestamp()); + + MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); + msgInner.setTopic(newTopic); + msgInner.setBody(msgExt.getBody()); + msgInner.setFlag(msgExt.getFlag()); + MessageAccessor.setProperties(msgInner, msgExt.getProperties()); + msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgExt.getProperties())); + msgInner.setTagsCode(MessageExtBrokerInner.tagsString2tagsCode(null, msgExt.getTags())); + + msgInner.setQueueId(queueIdInt); + msgInner.setSysFlag(msgExt.getSysFlag()); + msgInner.setBornTimestamp(msgExt.getBornTimestamp()); + msgInner.setBornHost(msgExt.getBornHost()); + msgInner.setStoreHost(this.getStoreHost()); + msgInner.setReconsumeTimes(msgExt.getReconsumeTimes() + 1); + + String originMsgId = MessageAccessor.getOriginMessageId(msgExt); + MessageAccessor.setOriginMessageId(msgInner, UtilAll.isBlank(originMsgId) ? msgExt.getMsgId() : originMsgId); + msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgExt.getProperties())); + + boolean succeeded = false; + + // Put retry topic to master message store + PutMessageResult putMessageResult = masterBroker.getMessageStore().putMessage(msgInner); + if (putMessageResult != null) { + String commercialOwner = request.getExtFields().get(BrokerStatsManager.COMMERCIAL_OWNER); + + switch (putMessageResult.getPutMessageStatus()) { + case PUT_OK: + String backTopic = msgExt.getTopic(); + String correctTopic = msgExt.getProperty(MessageConst.PROPERTY_RETRY_TOPIC); + if (correctTopic != null) { + backTopic = correctTopic; + } + if (TopicValidator.RMQ_SYS_SCHEDULE_TOPIC.equals(msgInner.getTopic())) { + masterBroker.getBrokerStatsManager().incTopicPutNums(msgInner.getTopic()); + masterBroker.getBrokerStatsManager().incTopicPutSize(msgInner.getTopic(), putMessageResult.getAppendMessageResult().getWroteBytes()); + masterBroker.getBrokerStatsManager().incQueuePutNums(msgInner.getTopic(), msgInner.getQueueId()); + masterBroker.getBrokerStatsManager().incQueuePutSize(msgInner.getTopic(), msgInner.getQueueId(), putMessageResult.getAppendMessageResult().getWroteBytes()); + } + masterBroker.getBrokerStatsManager().incSendBackNums(requestHeader.getGroup(), backTopic); + + if (isDLQ) { + masterBroker.getBrokerStatsManager().incDLQStatValue( + BrokerStatsManager.SNDBCK2DLQ_TIMES, + commercialOwner, + requestHeader.getGroup(), + requestHeader.getOriginTopic(), + BrokerStatsManager.StatsType.SEND_BACK_TO_DLQ.name(), + 1); + + String uniqKey = msgInner.getProperties().get(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX); + DLQ_LOG.info("send msg to DLQ {}, owner={}, originalTopic={}, consumerId={}, msgUniqKey={}, storeTimestamp={}", + newTopic, + commercialOwner, + requestHeader.getOriginTopic(), + requestHeader.getGroup(), + uniqKey, + putMessageResult.getAppendMessageResult().getStoreTimestamp()); + } + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + + succeeded = true; + break; + default: + break; + } + + if (!succeeded) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(putMessageResult.getPutMessageStatus().name()); + } + } else { + if (isDLQ) { + String owner = request.getExtFields().get(BrokerStatsManager.COMMERCIAL_OWNER); + String uniqKey = msgInner.getProperties().get(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX); + DLQ_LOG.info("failed to send msg to DLQ {}, owner={}, originalTopic={}, consumerId={}, msgUniqKey={}, result={}", + newTopic, + owner, + requestHeader.getOriginTopic(), + requestHeader.getGroup(), + uniqKey, + "null"); + } + + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("putMessageResult is null"); + } + + if (this.hasConsumeMessageHook() && !UtilAll.isBlank(requestHeader.getOriginMsgId())) { + String namespace = NamespaceUtil.getNamespaceFromResource(requestHeader.getGroup()); + ConsumeMessageContext context = new ConsumeMessageContext(); + context.setNamespace(namespace); + context.setTopic(requestHeader.getOriginTopic()); + context.setConsumerGroup(requestHeader.getGroup()); + context.setCommercialRcvStats(BrokerStatsManager.StatsType.SEND_BACK); + context.setCommercialRcvTimes(1); + context.setCommercialOwner(request.getExtFields().get(BrokerStatsManager.COMMERCIAL_OWNER)); + + context.setAccountAuthType(request.getExtFields().get(BrokerStatsManager.ACCOUNT_AUTH_TYPE)); + context.setAccountOwnerParent(request.getExtFields().get(BrokerStatsManager.ACCOUNT_OWNER_PARENT)); + context.setAccountOwnerSelf(request.getExtFields().get(BrokerStatsManager.ACCOUNT_OWNER_SELF)); + context.setRcvStat(isDLQ ? BrokerStatsManager.StatsType.SEND_BACK_TO_DLQ : BrokerStatsManager.StatsType.SEND_BACK); + context.setSuccess(succeeded); + context.setRcvMsgNum(1); + //Set msg body size 0 when sent back by consumer. + context.setRcvMsgSize(0); + context.setCommercialRcvMsgNum(succeeded ? 1 : 0); + + try { + this.executeConsumeMessageHookAfter(context); + } catch (AbortProcessException e) { + response.setCode(e.getResponseCode()); + response.setRemark(e.getErrorMessage()); + } + } + + return response; + } + + public boolean hasConsumeMessageHook() { + return consumeMessageHookList != null && !this.consumeMessageHookList.isEmpty(); + } + + public void executeConsumeMessageHookAfter(final ConsumeMessageContext context) { + if (hasConsumeMessageHook()) { + for (ConsumeMessageHook hook : this.consumeMessageHookList) { + try { + hook.consumeMessageAfter(context); + } catch (Throwable e) { + // Ignore + } + } + } + } + + protected SendMessageContext buildMsgContext(ChannelHandlerContext ctx, + SendMessageRequestHeader requestHeader, RemotingCommand request) { + String namespace = NamespaceUtil.getNamespaceFromResource(requestHeader.getTopic()); + + SendMessageContext sendMessageContext; + sendMessageContext = new SendMessageContext(); + sendMessageContext.setNamespace(namespace); + sendMessageContext.setProducerGroup(requestHeader.getProducerGroup()); + sendMessageContext.setTopic(requestHeader.getTopic()); + sendMessageContext.setBodyLength(request.getBody().length); + sendMessageContext.setMsgProps(requestHeader.getProperties()); + sendMessageContext.setBornHost(RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + sendMessageContext.setBrokerAddr(this.brokerController.getBrokerAddr()); + sendMessageContext.setQueueId(requestHeader.getQueueId()); + sendMessageContext.setBrokerRegionId(this.brokerController.getBrokerConfig().getRegionId()); + sendMessageContext.setBornTimeStamp(requestHeader.getBornTimestamp()); + sendMessageContext.setRequestTimeStamp(System.currentTimeMillis()); + + String owner = request.getExtFields().get(BrokerStatsManager.COMMERCIAL_OWNER); + sendMessageContext.setCommercialOwner(owner); Map properties = MessageDecoder.string2messageProperties(requestHeader.getProperties()); String uniqueKey = properties.get(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX); @@ -92,8 +378,14 @@ protected SendMessageContext buildMsgContext(ChannelHandlerContext ctx, if (uniqueKey == null) { uniqueKey = ""; } - mqtraceContext.setMsgUniqueKey(uniqueKey); - return mqtraceContext; + sendMessageContext.setMsgUniqueKey(uniqueKey); + + if (properties.containsKey(MessageConst.PROPERTY_SHARDING_KEY)) { + sendMessageContext.setMsgType(MessageType.Order_Msg); + } else { + sendMessageContext.setMsgType(MessageType.Normal_Msg); + } + return sendMessageContext; } public boolean hasSendMessageHook() { @@ -104,7 +396,7 @@ protected MessageExtBrokerInner buildInnerMsg(final ChannelHandlerContext ctx, final SendMessageRequestHeader requestHeader, final byte[] body, TopicConfig topicConfig) { int queueIdInt = requestHeader.getQueueId(); if (queueIdInt < 0) { - queueIdInt = Math.abs(this.random.nextInt() % 99999999) % topicConfig.getWriteQueueNums(); + queueIdInt = randomQueueId(topicConfig.getWriteQueueNums()); } int sysFlag = requestHeader.getSysFlag(); @@ -133,25 +425,30 @@ protected MessageExtBrokerInner buildInnerMsg(final ChannelHandlerContext ctx, } public SocketAddress getStoreHost() { - return storeHost; + return brokerController.getStoreHost(); } protected RemotingCommand msgContentCheck(final ChannelHandlerContext ctx, final SendMessageRequestHeader requestHeader, RemotingCommand request, final RemotingCommand response) { - if (requestHeader.getTopic().length() > Byte.MAX_VALUE) { - log.warn("putMessage message topic length too long {}", requestHeader.getTopic().length()); + String topic = requestHeader.getTopic(); + if (topic.length() > Byte.MAX_VALUE) { + LOGGER.warn("msgContentCheck: message topic length is too long, topic={}, topic length={}, threshold={}", + topic, topic.length(), Byte.MAX_VALUE); response.setCode(ResponseCode.MESSAGE_ILLEGAL); return response; } if (requestHeader.getProperties() != null && requestHeader.getProperties().length() > Short.MAX_VALUE) { - log.warn("putMessage message properties length too long {}", requestHeader.getProperties().length()); + LOGGER.warn( + "msgContentCheck: message properties length is too long, topic={}, properties length={}, threshold={}", + topic, requestHeader.getProperties().length(), Short.MAX_VALUE); response.setCode(ResponseCode.MESSAGE_ILLEGAL); return response; } if (request.getBody().length > DBMsgConstants.MAX_BODY_SIZE) { - log.warn(" topic {} msg body size {} from {}", requestHeader.getTopic(), - request.getBody().length, ChannelUtil.getRemoteIp(ctx.channel())); + LOGGER.warn( + "msgContentCheck: message body size exceeds the threshold, topic={}, body size={}, threshold={}bytes", + topic, request.getBody().length, DBMsgConstants.MAX_BODY_SIZE); response.setRemark("msg body must be less 64KB"); response.setCode(ResponseCode.MESSAGE_ILLEGAL); return response; @@ -160,7 +457,8 @@ protected RemotingCommand msgContentCheck(final ChannelHandlerContext ctx, } protected RemotingCommand msgCheck(final ChannelHandlerContext ctx, - final SendMessageRequestHeader requestHeader, final RemotingCommand response) { + final SendMessageRequestHeader requestHeader, final RemotingCommand request, + final RemotingCommand response) { if (!PermName.isWriteable(this.brokerController.getBrokerConfig().getBrokerPermission()) && this.brokerController.getTopicConfigManager().isOrderTopic(requestHeader.getTopic())) { response.setCode(ResponseCode.NO_PERMISSION); @@ -168,11 +466,16 @@ protected RemotingCommand msgCheck(final ChannelHandlerContext ctx, + "] sending message is forbidden"); return response; } - if (!this.brokerController.getTopicConfigManager().isTopicCanSendMessage(requestHeader.getTopic())) { - String errorMsg = "the topic[" + requestHeader.getTopic() + "] is conflict with system reserved words."; - log.warn(errorMsg); + + TopicValidator.ValidateTopicResult result = TopicValidator.validateTopic(requestHeader.getTopic()); + if (!result.isValid()) { response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark(errorMsg); + response.setRemark(result.getRemark()); + return response; + } + if (TopicValidator.isNotAllowedSendTopic(requestHeader.getTopic())) { + response.setCode(ResponseCode.NO_PERMISSION); + response.setRemark("Sending message to topic[" + requestHeader.getTopic() + "] is forbidden."); return response; } @@ -188,7 +491,7 @@ protected RemotingCommand msgCheck(final ChannelHandlerContext ctx, } } - log.warn("the topic {} not exist, producer: {}", requestHeader.getTopic(), ctx.channel().remoteAddress()); + LOGGER.warn("the topic {} not exist, producer: {}", requestHeader.getTopic(), ctx.channel().remoteAddress()); topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageMethod( requestHeader.getTopic(), requestHeader.getDefaultTopic(), @@ -217,10 +520,10 @@ protected RemotingCommand msgCheck(final ChannelHandlerContext ctx, if (queueIdInt >= idValid) { String errorInfo = String.format("request queueId[%d] is illegal, %s Producer: %s", queueIdInt, - topicConfig.toString(), + topicConfig, RemotingHelper.parseChannelRemoteAddr(ctx.channel())); - log.warn(errorInfo); + LOGGER.warn(errorInfo); response.setCode(ResponseCode.SYSTEM_ERROR); response.setRemark(errorInfo); @@ -235,68 +538,29 @@ public void registerSendMessageHook(List sendMessageHookList) { protected void doResponse(ChannelHandlerContext ctx, RemotingCommand request, final RemotingCommand response) { - if (!request.isOnewayRPC()) { - try { - ctx.writeAndFlush(response); - } catch (Throwable e) { - log.error("SendMessageProcessor process request over, but response failed", e); - log.error(request.toString()); - log.error(response.toString()); - } - } + NettyRemotingAbstract.writeResponse(ctx.channel(), request, response); } - public void executeSendMessageHookBefore(final ChannelHandlerContext ctx, final RemotingCommand request, - SendMessageContext context) { + public void executeSendMessageHookBefore(SendMessageContext context) { if (hasSendMessageHook()) { for (SendMessageHook hook : this.sendMessageHookList) { try { - final SendMessageRequestHeader requestHeader = parseRequestHeader(request); - - if (null != requestHeader) { - context.setProducerGroup(requestHeader.getProducerGroup()); - context.setTopic(requestHeader.getTopic()); - context.setBodyLength(request.getBody().length); - context.setMsgProps(requestHeader.getProperties()); - context.setBornHost(RemotingHelper.parseChannelRemoteAddr(ctx.channel())); - context.setBrokerAddr(this.brokerController.getBrokerAddr()); - context.setQueueId(requestHeader.getQueueId()); - } - hook.sendMessageBefore(context); - if (requestHeader != null) { - requestHeader.setProperties(context.getMsgProps()); - } + } catch (AbortProcessException e) { + throw e; } catch (Throwable e) { - // Ignore + //ignore } } } } - protected SendMessageRequestHeader parseRequestHeader(RemotingCommand request) - throws RemotingCommandException { + protected SendMessageRequestHeader parseRequestHeader(RemotingCommand request) throws RemotingCommandException { + return SendMessageRequestHeader.parseRequestHeader(request); + } - SendMessageRequestHeaderV2 requestHeaderV2 = null; - SendMessageRequestHeader requestHeader = null; - switch (request.getCode()) { - case RequestCode.SEND_BATCH_MESSAGE: - case RequestCode.SEND_MESSAGE_V2: - requestHeaderV2 = - (SendMessageRequestHeaderV2) request - .decodeCommandCustomHeader(SendMessageRequestHeaderV2.class); - case RequestCode.SEND_MESSAGE: - if (null == requestHeaderV2) { - requestHeader = - (SendMessageRequestHeader) request - .decodeCommandCustomHeader(SendMessageRequestHeader.class); - } else { - requestHeader = SendMessageRequestHeaderV2.createSendMessageRequestHeaderV1(requestHeaderV2); - } - default: - break; - } - return requestHeader; + protected int randomQueueId(int writeQueueNums) { + return ThreadLocalRandom.current().nextInt(99999999) % writeQueueNums; } public void executeSendMessageHookAfter(final RemotingCommand response, final SendMessageContext context) { @@ -314,7 +578,7 @@ public void executeSendMessageHookAfter(final RemotingCommand response, final Se } hook.sendMessageAfter(context); } catch (Throwable e) { - // Ignore + //ignore } } } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java new file mode 100644 index 00000000000..9a56498632f --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/AckMessageProcessor.java @@ -0,0 +1,323 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.processor; + +import com.alibaba.fastjson.JSON; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import java.util.BitSet; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.metrics.PopMetricsManager; +import org.apache.rocketmq.common.KeyBuilder; +import org.apache.rocketmq.common.PopAckConstants; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.help.FAQUrl; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.utils.DataConverter; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.BatchAck; +import org.apache.rocketmq.remoting.protocol.body.BatchAckMessageRequestBody; +import org.apache.rocketmq.remoting.protocol.header.AckMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ExtraInfoUtil; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.pop.AckMsg; +import org.apache.rocketmq.store.pop.BatchAckMsg; + +public class AckMessageProcessor implements NettyRequestProcessor { + private static final Logger POP_LOGGER = LoggerFactory.getLogger(LoggerName.ROCKETMQ_POP_LOGGER_NAME); + private final BrokerController brokerController; + private final String reviveTopic; + private final PopReviveService[] popReviveServices; + + public AckMessageProcessor(final BrokerController brokerController) { + this.brokerController = brokerController; + this.reviveTopic = PopAckConstants.buildClusterReviveTopic(this.brokerController.getBrokerConfig().getBrokerClusterName()); + this.popReviveServices = new PopReviveService[this.brokerController.getBrokerConfig().getReviveQueueNum()]; + for (int i = 0; i < this.brokerController.getBrokerConfig().getReviveQueueNum(); i++) { + this.popReviveServices[i] = new PopReviveService(brokerController, reviveTopic, i); + this.popReviveServices[i].setShouldRunPopRevive(brokerController.getBrokerConfig().getBrokerId() == 0); + } + } + + public PopReviveService[] getPopReviveServices() { + return popReviveServices; + } + + public void startPopReviveService() { + for (PopReviveService popReviveService : popReviveServices) { + popReviveService.start(); + } + } + + public void shutdownPopReviveService() { + for (PopReviveService popReviveService : popReviveServices) { + popReviveService.shutdown(); + } + } + + public void setPopReviveServiceStatus(boolean shouldStart) { + for (PopReviveService popReviveService : popReviveServices) { + popReviveService.setShouldRunPopRevive(shouldStart); + } + } + + public boolean isPopReviveServiceRunning() { + for (PopReviveService popReviveService : popReviveServices) { + if (popReviveService.isShouldRunPopRevive()) { + return true; + } + } + + return false; + } + + @Override + public RemotingCommand processRequest(final ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + return this.processRequest(ctx.channel(), request, true); + } + + @Override + public boolean rejectRequest() { + return false; + } + + private RemotingCommand processRequest(final Channel channel, RemotingCommand request, + boolean brokerAllowSuspend) throws RemotingCommandException { + AckMessageRequestHeader requestHeader; + BatchAckMessageRequestBody reqBody = null; + final RemotingCommand response = RemotingCommand.createResponseCommand(ResponseCode.SUCCESS, null); + response.setOpaque(request.getOpaque()); + if (request.getCode() == RequestCode.ACK_MESSAGE) { + requestHeader = (AckMessageRequestHeader) request.decodeCommandCustomHeader(AckMessageRequestHeader.class); + + TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic()); + if (null == topicConfig) { + POP_LOGGER.error("The topic {} not exist, consumer: {} ", requestHeader.getTopic(), RemotingHelper.parseChannelRemoteAddr(channel)); + response.setCode(ResponseCode.TOPIC_NOT_EXIST); + response.setRemark(String.format("topic[%s] not exist, apply first please! %s", requestHeader.getTopic(), FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL))); + return response; + } + + if (requestHeader.getQueueId() >= topicConfig.getReadQueueNums() || requestHeader.getQueueId() < 0) { + String errorInfo = String.format("queueId[%d] is illegal, topic:[%s] topicConfig.readQueueNums:[%d] consumer:[%s]", + requestHeader.getQueueId(), requestHeader.getTopic(), topicConfig.getReadQueueNums(), channel.remoteAddress()); + POP_LOGGER.warn(errorInfo); + response.setCode(ResponseCode.MESSAGE_ILLEGAL); + response.setRemark(errorInfo); + return response; + } + + long minOffset = this.brokerController.getMessageStore().getMinOffsetInQueue(requestHeader.getTopic(), requestHeader.getQueueId()); + long maxOffset = this.brokerController.getMessageStore().getMaxOffsetInQueue(requestHeader.getTopic(), requestHeader.getQueueId()); + if (requestHeader.getOffset() < minOffset || requestHeader.getOffset() > maxOffset) { + String errorInfo = String.format("offset is illegal, key:%s@%d, commit:%d, store:%d~%d", + requestHeader.getTopic(), requestHeader.getQueueId(), requestHeader.getOffset(), minOffset, maxOffset); + POP_LOGGER.warn(errorInfo); + response.setCode(ResponseCode.NO_MESSAGE); + response.setRemark(errorInfo); + return response; + } + + appendAck(requestHeader, null, response, channel, null); + } else if (request.getCode() == RequestCode.BATCH_ACK_MESSAGE) { + if (request.getBody() != null) { + reqBody = BatchAckMessageRequestBody.decode(request.getBody(), BatchAckMessageRequestBody.class); + } + if (reqBody == null || reqBody.getAcks() == null || reqBody.getAcks().isEmpty()) { + response.setCode(ResponseCode.NO_MESSAGE); + return response; + } + for (BatchAck bAck : reqBody.getAcks()) { + appendAck(null, bAck, response, channel, reqBody.getBrokerName()); + } + } else { + POP_LOGGER.error("AckMessageProcessor failed to process RequestCode: {}, consumer: {} ", request.getCode(), RemotingHelper.parseChannelRemoteAddr(channel)); + response.setCode(ResponseCode.MESSAGE_ILLEGAL); + response.setRemark(String.format("AckMessageProcessor failed to process RequestCode: %d", request.getCode())); + return response; + } + return response; + } + + private void appendAck(final AckMessageRequestHeader requestHeader, final BatchAck batchAck, final RemotingCommand response, final Channel channel, String brokerName) { + String[] extraInfo; + String consumeGroup, topic; + int qId, rqId; + long startOffset, ackOffset; + long popTime, invisibleTime; + AckMsg ackMsg; + int ackCount = 0; + if (batchAck == null) { + // single ack + extraInfo = ExtraInfoUtil.split(requestHeader.getExtraInfo()); + brokerName = ExtraInfoUtil.getBrokerName(extraInfo); + consumeGroup = requestHeader.getConsumerGroup(); + topic = requestHeader.getTopic(); + qId = requestHeader.getQueueId(); + rqId = ExtraInfoUtil.getReviveQid(extraInfo); + startOffset = ExtraInfoUtil.getCkQueueOffset(extraInfo); + ackOffset = requestHeader.getOffset(); + popTime = ExtraInfoUtil.getPopTime(extraInfo); + invisibleTime = ExtraInfoUtil.getInvisibleTime(extraInfo); + + if (rqId == KeyBuilder.POP_ORDER_REVIVE_QUEUE) { + ackOrderly(topic, consumeGroup, qId, ackOffset, popTime, invisibleTime, channel, response); + return; + } + + ackMsg = new AckMsg(); + ackCount = 1; + } else { + // batch ack + consumeGroup = batchAck.getConsumerGroup(); + topic = ExtraInfoUtil.getRealTopic(batchAck.getTopic(), batchAck.getConsumerGroup(), batchAck.getRetry()); + qId = batchAck.getQueueId(); + rqId = batchAck.getReviveQueueId(); + startOffset = batchAck.getStartOffset(); + ackOffset = -1; + popTime = batchAck.getPopTime(); + invisibleTime = batchAck.getInvisibleTime(); + + long minOffset = this.brokerController.getMessageStore().getMinOffsetInQueue(topic, qId); + long maxOffset = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, qId); + if (minOffset == -1 || maxOffset == -1) { + POP_LOGGER.error("Illegal topic or queue found when batch ack {}", batchAck); + return; + } + + BatchAckMsg batchAckMsg = new BatchAckMsg(); + BitSet bitSet = batchAck.getBitSet(); + for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i + 1)) { + if (i == Integer.MAX_VALUE) { + break; + } + long offset = startOffset + i; + if (offset < minOffset || offset > maxOffset) { + continue; + } + if (rqId == KeyBuilder.POP_ORDER_REVIVE_QUEUE) { + ackOrderly(topic, consumeGroup, qId, offset, popTime, invisibleTime, channel, response); + } else { + batchAckMsg.getAckOffsetList().add(offset); + } + } + if (rqId == KeyBuilder.POP_ORDER_REVIVE_QUEUE || batchAckMsg.getAckOffsetList().isEmpty()) { + return; + } + + ackMsg = batchAckMsg; + ackCount = batchAckMsg.getAckOffsetList().size(); + } + + this.brokerController.getBrokerStatsManager().incBrokerAckNums(ackCount); + this.brokerController.getBrokerStatsManager().incGroupAckNums(consumeGroup, topic, ackCount); + + ackMsg.setConsumerGroup(consumeGroup); + ackMsg.setTopic(topic); + ackMsg.setQueueId(qId); + ackMsg.setStartOffset(startOffset); + ackMsg.setAckOffset(ackOffset); + ackMsg.setPopTime(popTime); + ackMsg.setBrokerName(brokerName); + + if (this.brokerController.getPopMessageProcessor().getPopBufferMergeService().addAk(rqId, ackMsg)) { + brokerController.getPopInflightMessageCounter().decrementInFlightMessageNum(topic, consumeGroup, popTime, qId, ackCount); + return; + } + + MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); + msgInner.setTopic(reviveTopic); + msgInner.setBody(JSON.toJSONString(ackMsg).getBytes(DataConverter.CHARSET_UTF8)); + msgInner.setQueueId(rqId); + if (ackMsg instanceof BatchAckMsg) { + msgInner.setTags(PopAckConstants.BATCH_ACK_TAG); + msgInner.getProperties().put(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, PopMessageProcessor.genBatchAckUniqueId((BatchAckMsg) ackMsg)); + } else { + msgInner.setTags(PopAckConstants.ACK_TAG); + msgInner.getProperties().put(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, PopMessageProcessor.genAckUniqueId(ackMsg)); + } + msgInner.setBornTimestamp(System.currentTimeMillis()); + msgInner.setBornHost(this.brokerController.getStoreHost()); + msgInner.setStoreHost(this.brokerController.getStoreHost()); + msgInner.setDeliverTimeMs(popTime + invisibleTime); + msgInner.getProperties().put(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, PopMessageProcessor.genAckUniqueId(ackMsg)); + msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); + PutMessageResult putMessageResult = this.brokerController.getEscapeBridge().putMessageToSpecificQueue(msgInner); + if (putMessageResult.getPutMessageStatus() != PutMessageStatus.PUT_OK + && putMessageResult.getPutMessageStatus() != PutMessageStatus.FLUSH_DISK_TIMEOUT + && putMessageResult.getPutMessageStatus() != PutMessageStatus.FLUSH_SLAVE_TIMEOUT + && putMessageResult.getPutMessageStatus() != PutMessageStatus.SLAVE_NOT_AVAILABLE) { + POP_LOGGER.error("put ack msg error:" + putMessageResult); + } + PopMetricsManager.incPopReviveAckPutCount(ackMsg, putMessageResult.getPutMessageStatus()); + brokerController.getPopInflightMessageCounter().decrementInFlightMessageNum(topic, consumeGroup, popTime, qId, ackCount); + } + + protected void ackOrderly(String topic, String consumeGroup, int qId, long ackOffset, long popTime, long invisibleTime, Channel channel, RemotingCommand response) { + String lockKey = topic + PopAckConstants.SPLIT + consumeGroup + PopAckConstants.SPLIT + qId; + long oldOffset = this.brokerController.getConsumerOffsetManager().queryOffset(consumeGroup, topic, qId); + if (ackOffset < oldOffset) { + return; + } + while (!this.brokerController.getPopMessageProcessor().getQueueLockManager().tryLock(lockKey)) { + } + try { + oldOffset = this.brokerController.getConsumerOffsetManager().queryOffset(consumeGroup, topic, qId); + if (ackOffset < oldOffset) { + return; + } + long nextOffset = brokerController.getConsumerOrderInfoManager().commitAndNext( + topic, consumeGroup, + qId, ackOffset, + popTime); + if (nextOffset > -1) { + if (!this.brokerController.getConsumerOffsetManager().hasOffsetReset( + topic, consumeGroup, qId)) { + this.brokerController.getConsumerOffsetManager().commitOffset(channel.remoteAddress().toString(), + consumeGroup, topic, qId, nextOffset); + } + if (!this.brokerController.getConsumerOrderInfoManager().checkBlock(null, topic, + consumeGroup, qId, invisibleTime)) { + this.brokerController.getPopMessageProcessor().notifyMessageArriving( + topic, consumeGroup, qId); + } + } else if (nextOffset == -1) { + String errorInfo = String.format("offset is illegal, key:%s, old:%d, commit:%d, next:%d, %s", + lockKey, oldOffset, ackOffset, nextOffset, channel.remoteAddress()); + POP_LOGGER.warn(errorInfo); + response.setCode(ResponseCode.MESSAGE_ILLEGAL); + response.setRemark(errorInfo); + return; + } + } finally { + this.brokerController.getPopMessageProcessor().getQueueLockManager().unLock(lockKey); + } + brokerController.getPopInflightMessageCounter().decrementInFlightMessageNum(topic, consumeGroup, popTime, qId, 1); + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessor.java index d69a78700c9..67a4d45447c 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessor.java @@ -17,11 +17,15 @@ package org.apache.rocketmq.broker.processor; import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.google.common.collect.Sets; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import java.io.UnsupportedEncodingException; import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -29,99 +33,180 @@ import java.util.Map; import java.util.Properties; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.acl.AccessValidator; +import org.apache.rocketmq.acl.plain.PlainAccessValidator; import org.apache.rocketmq.broker.BrokerController; import org.apache.rocketmq.broker.client.ClientChannelInfo; import org.apache.rocketmq.broker.client.ConsumerGroupInfo; +import org.apache.rocketmq.broker.controller.ReplicasManager; import org.apache.rocketmq.broker.filter.ConsumerFilterData; import org.apache.rocketmq.broker.filter.ExpressionMessageFilter; +import org.apache.rocketmq.broker.plugin.BrokerAttachedPlugin; +import org.apache.rocketmq.broker.subscription.SubscriptionGroupManager; +import org.apache.rocketmq.broker.transaction.queue.TransactionalMessageUtil; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.KeyBuilder; +import org.apache.rocketmq.common.LockCallback; import org.apache.rocketmq.common.MQVersion; import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.PlainAccessConfig; import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.UnlockCallback; import org.apache.rocketmq.common.UtilAll; -import org.apache.rocketmq.common.admin.ConsumeStats; -import org.apache.rocketmq.common.admin.OffsetWrapper; -import org.apache.rocketmq.common.admin.TopicOffset; -import org.apache.rocketmq.common.admin.TopicStatsTable; +import org.apache.rocketmq.common.attribute.AttributeParser; +import org.apache.rocketmq.common.attribute.TopicMessageType; +import org.apache.rocketmq.common.constant.ConsumeInitMode; +import org.apache.rocketmq.common.constant.FIleReadaheadMode; import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.constant.PermName; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageConst; import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; import org.apache.rocketmq.common.message.MessageId; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.RequestCode; -import org.apache.rocketmq.common.protocol.ResponseCode; -import org.apache.rocketmq.common.protocol.body.BrokerStatsData; -import org.apache.rocketmq.common.protocol.body.BrokerStatsItem; -import org.apache.rocketmq.common.protocol.body.Connection; -import org.apache.rocketmq.common.protocol.body.ConsumeQueueData; -import org.apache.rocketmq.common.protocol.body.ConsumeStatsList; -import org.apache.rocketmq.common.protocol.body.ConsumerConnection; -import org.apache.rocketmq.common.protocol.body.GroupList; -import org.apache.rocketmq.common.protocol.body.KVTable; -import org.apache.rocketmq.common.protocol.body.LockBatchRequestBody; -import org.apache.rocketmq.common.protocol.body.LockBatchResponseBody; -import org.apache.rocketmq.common.protocol.body.ProducerConnection; -import org.apache.rocketmq.common.protocol.body.QueryConsumeQueueResponseBody; -import org.apache.rocketmq.common.protocol.body.QueryConsumeTimeSpanBody; -import org.apache.rocketmq.common.protocol.body.QueryCorrectionOffsetBody; -import org.apache.rocketmq.common.protocol.body.QueueTimeSpan; -import org.apache.rocketmq.common.protocol.body.TopicList; -import org.apache.rocketmq.common.protocol.body.UnlockBatchRequestBody; -import org.apache.rocketmq.common.protocol.header.CloneGroupOffsetRequestHeader; -import org.apache.rocketmq.common.protocol.header.ConsumeMessageDirectlyResultRequestHeader; -import org.apache.rocketmq.common.protocol.header.CreateTopicRequestHeader; -import org.apache.rocketmq.common.protocol.header.DeleteSubscriptionGroupRequestHeader; -import org.apache.rocketmq.common.protocol.header.DeleteTopicRequestHeader; -import org.apache.rocketmq.common.protocol.header.GetAllTopicConfigResponseHeader; -import org.apache.rocketmq.common.protocol.header.GetBrokerConfigResponseHeader; -import org.apache.rocketmq.common.protocol.header.GetConsumeStatsInBrokerHeader; -import org.apache.rocketmq.common.protocol.header.GetConsumeStatsRequestHeader; -import org.apache.rocketmq.common.protocol.header.GetConsumerConnectionListRequestHeader; -import org.apache.rocketmq.common.protocol.header.GetConsumerRunningInfoRequestHeader; -import org.apache.rocketmq.common.protocol.header.GetConsumerStatusRequestHeader; -import org.apache.rocketmq.common.protocol.header.GetEarliestMsgStoretimeRequestHeader; -import org.apache.rocketmq.common.protocol.header.GetEarliestMsgStoretimeResponseHeader; -import org.apache.rocketmq.common.protocol.header.GetMaxOffsetRequestHeader; -import org.apache.rocketmq.common.protocol.header.GetMaxOffsetResponseHeader; -import org.apache.rocketmq.common.protocol.header.GetMinOffsetRequestHeader; -import org.apache.rocketmq.common.protocol.header.GetMinOffsetResponseHeader; -import org.apache.rocketmq.common.protocol.header.GetProducerConnectionListRequestHeader; -import org.apache.rocketmq.common.protocol.header.GetTopicStatsInfoRequestHeader; -import org.apache.rocketmq.common.protocol.header.QueryConsumeQueueRequestHeader; -import org.apache.rocketmq.common.protocol.header.QueryConsumeTimeSpanRequestHeader; -import org.apache.rocketmq.common.protocol.header.QueryCorrectionOffsetHeader; -import org.apache.rocketmq.common.protocol.header.QueryTopicConsumeByWhoRequestHeader; -import org.apache.rocketmq.common.protocol.header.ResetOffsetRequestHeader; -import org.apache.rocketmq.common.protocol.header.SearchOffsetRequestHeader; -import org.apache.rocketmq.common.protocol.header.SearchOffsetResponseHeader; -import org.apache.rocketmq.common.protocol.header.ViewBrokerStatsDataRequestHeader; -import org.apache.rocketmq.common.protocol.header.filtersrv.RegisterFilterServerRequestHeader; -import org.apache.rocketmq.common.protocol.header.filtersrv.RegisterFilterServerResponseHeader; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; import org.apache.rocketmq.common.stats.StatsItem; import org.apache.rocketmq.common.stats.StatsSnapshot; -import org.apache.rocketmq.common.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.common.topic.TopicValidator; import org.apache.rocketmq.filter.util.BitsArray; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.remoting.common.RemotingHelper; import org.apache.rocketmq.remoting.exception.RemotingCommandException; import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; +import org.apache.rocketmq.remoting.netty.NettyRemotingAbstract; import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; import org.apache.rocketmq.remoting.protocol.LanguageCode; import org.apache.rocketmq.remoting.protocol.RemotingCommand; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; -import org.apache.rocketmq.store.ConsumeQueue; +import org.apache.rocketmq.remoting.protocol.RemotingSysResponseCode; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.admin.ConsumeStats; +import org.apache.rocketmq.remoting.protocol.admin.OffsetWrapper; +import org.apache.rocketmq.remoting.protocol.admin.TopicOffset; +import org.apache.rocketmq.remoting.protocol.admin.TopicStatsTable; +import org.apache.rocketmq.remoting.protocol.body.BrokerMemberGroup; +import org.apache.rocketmq.remoting.protocol.body.BrokerStatsData; +import org.apache.rocketmq.remoting.protocol.body.BrokerStatsItem; +import org.apache.rocketmq.remoting.protocol.body.Connection; +import org.apache.rocketmq.remoting.protocol.body.ConsumeQueueData; +import org.apache.rocketmq.remoting.protocol.body.ConsumeStatsList; +import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection; +import org.apache.rocketmq.remoting.protocol.body.EpochEntryCache; +import org.apache.rocketmq.remoting.protocol.body.GroupList; +import org.apache.rocketmq.remoting.protocol.body.HARuntimeInfo; +import org.apache.rocketmq.remoting.protocol.body.KVTable; +import org.apache.rocketmq.remoting.protocol.body.LockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.body.LockBatchResponseBody; +import org.apache.rocketmq.remoting.protocol.body.ProducerConnection; +import org.apache.rocketmq.remoting.protocol.body.ProducerTableInfo; +import org.apache.rocketmq.remoting.protocol.body.QueryConsumeQueueResponseBody; +import org.apache.rocketmq.remoting.protocol.body.QueryConsumeTimeSpanBody; +import org.apache.rocketmq.remoting.protocol.body.QueryCorrectionOffsetBody; +import org.apache.rocketmq.remoting.protocol.body.QuerySubscriptionResponseBody; +import org.apache.rocketmq.remoting.protocol.body.QueueTimeSpan; +import org.apache.rocketmq.remoting.protocol.body.ResetOffsetBody; +import org.apache.rocketmq.remoting.protocol.body.SyncStateSet; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigAndMappingSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.body.TopicList; +import org.apache.rocketmq.remoting.protocol.body.UnlockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.header.CloneGroupOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ConsumeMessageDirectlyResultRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.CreateAccessConfigRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.CreateTopicRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.DeleteAccessConfigRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.DeleteSubscriptionGroupRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.DeleteTopicRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ExchangeHAInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ExchangeHAInfoResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.GetAllProducerInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetAllTopicConfigResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.GetBrokerAclConfigResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.GetBrokerConfigResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumeStatsInBrokerHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumeStatsRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerConnectionListRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerRunningInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerStatusRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetEarliestMsgStoretimeRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetEarliestMsgStoretimeResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMaxOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMaxOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMinOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMinOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.GetProducerConnectionListRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetSubscriptionGroupConfigRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetTopicConfigRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetTopicStatsInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.NotifyBrokerRoleChangedRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.NotifyMinBrokerIdChangeRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumeQueueRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumeTimeSpanRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryCorrectionOffsetHeader; +import org.apache.rocketmq.remoting.protocol.header.QuerySubscriptionByConsumerRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryTopicConsumeByWhoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryTopicsByConsumerRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ResetMasterFlushOffsetHeader; +import org.apache.rocketmq.remoting.protocol.header.ResetOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ResumeCheckHalfMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SearchOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SearchOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateGlobalWhiteAddrsConfigRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateGroupForbiddenRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ViewBrokerStatsDataRequestHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.remoting.protocol.statictopic.LogicQueueMappingItem; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicConfigAndQueueMapping; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingContext; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingDetail; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingUtils; +import org.apache.rocketmq.remoting.protocol.subscription.GroupForbidden; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.remoting.rpc.RpcClientUtils; +import org.apache.rocketmq.remoting.rpc.RpcException; +import org.apache.rocketmq.remoting.rpc.RpcRequest; +import org.apache.rocketmq.remoting.rpc.RpcResponse; import org.apache.rocketmq.store.ConsumeQueueExt; import org.apache.rocketmq.store.DefaultMessageStore; import org.apache.rocketmq.store.MessageFilter; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; import org.apache.rocketmq.store.SelectMappedBufferResult; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.config.BrokerRole; +import org.apache.rocketmq.store.queue.ConsumeQueueInterface; +import org.apache.rocketmq.store.queue.CqUnit; +import org.apache.rocketmq.store.queue.ReferredIterator; +import org.apache.rocketmq.store.timer.TimerCheckpoint; +import org.apache.rocketmq.store.timer.TimerMessageStore; +import org.apache.rocketmq.store.util.LibC; + +import static org.apache.rocketmq.remoting.protocol.RemotingCommand.buildErrorResponse; public class AdminBrokerProcessor implements NettyRequestProcessor { - private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); - private final BrokerController brokerController; + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + protected final BrokerController brokerController; + protected Set configBlackList = new HashSet<>(); public AdminBrokerProcessor(final BrokerController brokerController) { this.brokerController = brokerController; + initConfigBlackList(); + } + + private void initConfigBlackList() { + configBlackList.add("brokerConfigPath"); + configBlackList.add("rocketmqHome"); + configBlackList.add("configBlackList"); + String[] configArray = brokerController.getBrokerConfig().getConfigBlackList().split(";"); + configBlackList.addAll(Arrays.asList(configArray)); } @Override @@ -134,10 +219,22 @@ public RemotingCommand processRequest(ChannelHandlerContext ctx, return this.deleteTopic(ctx, request); case RequestCode.GET_ALL_TOPIC_CONFIG: return this.getAllTopicConfig(ctx, request); + case RequestCode.GET_TIMER_CHECK_POINT: + return this.getTimerCheckPoint(ctx, request); + case RequestCode.GET_TIMER_METRICS: + return this.getTimerMetrics(ctx, request); case RequestCode.UPDATE_BROKER_CONFIG: return this.updateBrokerConfig(ctx, request); case RequestCode.GET_BROKER_CONFIG: return this.getBrokerConfig(ctx, request); + case RequestCode.UPDATE_COLD_DATA_FLOW_CTR_CONFIG: + return this.updateColdDataFlowCtrGroupConfig(ctx, request); + case RequestCode.REMOVE_COLD_DATA_FLOW_CTR_CONFIG: + return this.removeColdDataFlowCtrGroupConfig(ctx, request); + case RequestCode.GET_COLD_DATA_FLOW_CTR_INFO: + return this.getColdDataFlowCtrInfo(ctx); + case RequestCode.SET_COMMITLOG_READ_MODE: + return this.setCommitLogReadaheadMode(ctx, request); case RequestCode.SEARCH_OFFSET_BY_TIMESTAMP: return this.searchOffsetByTimestamp(ctx, request); case RequestCode.GET_MAX_OFFSET: @@ -164,26 +261,34 @@ public RemotingCommand processRequest(ChannelHandlerContext ctx, return this.getConsumerConnectionList(ctx, request); case RequestCode.GET_PRODUCER_CONNECTION_LIST: return this.getProducerConnectionList(ctx, request); + case RequestCode.GET_ALL_PRODUCER_INFO: + return this.getAllProducerInfo(ctx, request); case RequestCode.GET_CONSUME_STATS: return this.getConsumeStats(ctx, request); case RequestCode.GET_ALL_CONSUMER_OFFSET: return this.getAllConsumerOffset(ctx, request); case RequestCode.GET_ALL_DELAY_OFFSET: return this.getAllDelayOffset(ctx, request); + case RequestCode.GET_ALL_MESSAGE_REQUEST_MODE: + return this.getAllMessageRequestMode(ctx, request); case RequestCode.INVOKE_BROKER_TO_RESET_OFFSET: return this.resetOffset(ctx, request); case RequestCode.INVOKE_BROKER_TO_GET_CONSUMER_STATUS: return this.getConsumerStatus(ctx, request); case RequestCode.QUERY_TOPIC_CONSUME_BY_WHO: return this.queryTopicConsumeByWho(ctx, request); - case RequestCode.REGISTER_FILTER_SERVER: - return this.registerFilterServer(ctx, request); + case RequestCode.QUERY_TOPICS_BY_CONSUMER: + return this.queryTopicsByConsumer(ctx, request); + case RequestCode.QUERY_SUBSCRIPTION_BY_CONSUMER: + return this.querySubscriptionByConsumer(ctx, request); case RequestCode.QUERY_CONSUME_TIME_SPAN: return this.queryConsumeTimeSpan(ctx, request); case RequestCode.GET_SYSTEM_TOPIC_LIST_FROM_BROKER: return this.getSystemTopicListFromBroker(ctx, request); case RequestCode.CLEAN_EXPIRED_CONSUMEQUEUE: return this.cleanExpiredConsumeQueue(); + case RequestCode.DELETE_EXPIRED_COMMITLOG: + return this.deleteExpiredCommitLog(); case RequestCode.CLEAN_UNUSED_TOPIC: return this.cleanUnusedTopic(); case RequestCode.GET_CONSUMER_RUNNING_INFO: @@ -200,11 +305,103 @@ public RemotingCommand processRequest(ChannelHandlerContext ctx, return fetchAllConsumeStatsInBroker(ctx, request); case RequestCode.QUERY_CONSUME_QUEUE: return queryConsumeQueue(ctx, request); + case RequestCode.UPDATE_AND_GET_GROUP_FORBIDDEN: + return this.updateAndGetGroupForbidden(ctx, request); + case RequestCode.GET_SUBSCRIPTIONGROUP_CONFIG: + return this.getSubscriptionGroup(ctx, request); + case RequestCode.UPDATE_AND_CREATE_ACL_CONFIG: + return updateAndCreateAccessConfig(ctx, request); + case RequestCode.DELETE_ACL_CONFIG: + return deleteAccessConfig(ctx, request); + case RequestCode.GET_BROKER_CLUSTER_ACL_INFO: + return getBrokerAclConfigVersion(ctx, request); + case RequestCode.UPDATE_GLOBAL_WHITE_ADDRS_CONFIG: + return updateGlobalWhiteAddrsConfig(ctx, request); + case RequestCode.RESUME_CHECK_HALF_MESSAGE: + return resumeCheckHalfMessage(ctx, request); + case RequestCode.GET_TOPIC_CONFIG: + return getTopicConfig(ctx, request); + case RequestCode.UPDATE_AND_CREATE_STATIC_TOPIC: + return this.updateAndCreateStaticTopic(ctx, request); + case RequestCode.NOTIFY_MIN_BROKER_ID_CHANGE: + return this.notifyMinBrokerIdChange(ctx, request); + case RequestCode.EXCHANGE_BROKER_HA_INFO: + return this.updateBrokerHaInfo(ctx, request); + case RequestCode.GET_BROKER_HA_STATUS: + return this.getBrokerHaStatus(ctx, request); + case RequestCode.RESET_MASTER_FLUSH_OFFSET: + return this.resetMasterFlushOffset(ctx, request); + case RequestCode.GET_BROKER_EPOCH_CACHE: + return this.getBrokerEpochCache(ctx, request); + case RequestCode.NOTIFY_BROKER_ROLE_CHANGED: + return this.notifyBrokerRoleChanged(ctx, request); default: - break; + return getUnknownCmdResponse(ctx, request); } + } - return null; + /** + * @param ctx + * @param request + * @return + * @throws RemotingCommandException + */ + private RemotingCommand getSubscriptionGroup(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + GetSubscriptionGroupConfigRequestHeader requestHeader = (GetSubscriptionGroupConfigRequestHeader) request.decodeCommandCustomHeader(GetSubscriptionGroupConfigRequestHeader.class); + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + SubscriptionGroupConfig groupConfig = this.brokerController.getSubscriptionGroupManager().getSubscriptionGroupTable().get(requestHeader.getGroup()); + if (groupConfig == null) { + LOGGER.error("No group in this broker, client: {} group: {}", ctx.channel().remoteAddress(), requestHeader.getGroup()); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("No group in this broker"); + return response; + } + String content = JSONObject.toJSONString(groupConfig); + try { + response.setBody(content.getBytes(MixAll.DEFAULT_CHARSET)); + } catch (UnsupportedEncodingException e) { + LOGGER.error("UnsupportedEncodingException getSubscriptionGroup: group=" + groupConfig.getGroupName(), e); + + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("UnsupportedEncodingException " + e.getMessage()); + return response; + } + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + + return response; + } + + /** + * @param ctx + * @param request + * @return + */ + private RemotingCommand updateAndGetGroupForbidden(ChannelHandlerContext ctx, RemotingCommand request) + throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + UpdateGroupForbiddenRequestHeader requestHeader = (UpdateGroupForbiddenRequestHeader) // + request.decodeCommandCustomHeader(UpdateGroupForbiddenRequestHeader.class); + String group = requestHeader.getGroup(); + String topic = requestHeader.getTopic(); + LOGGER.info("updateAndGetGroupForbidden called by {} for object {}@{} readable={}",// + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), group, // + topic, requestHeader.getReadable()); + SubscriptionGroupManager groupManager = this.brokerController.getSubscriptionGroupManager(); + if (requestHeader.getReadable() != null) { + groupManager.updateForbidden(group, topic, PermName.INDEX_PERM_READ, !requestHeader.getReadable()); + } + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(""); + GroupForbidden groupForbidden = new GroupForbidden(); + groupForbidden.setGroup(group); + groupForbidden.setTopic(topic); + groupForbidden.setReadable(!groupManager.getForbidden(group, topic, PermName.INDEX_PERM_READ)); + response.setBody(groupForbidden.toJson().getBytes(StandardCharsets.UTF_8)); + return response; } @Override @@ -212,79 +409,347 @@ public boolean rejectRequest() { return false; } - private RemotingCommand updateAndCreateTopic(ChannelHandlerContext ctx, + private synchronized RemotingCommand updateAndCreateTopic(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException { final RemotingCommand response = RemotingCommand.createResponseCommand(null); final CreateTopicRequestHeader requestHeader = (CreateTopicRequestHeader) request.decodeCommandCustomHeader(CreateTopicRequestHeader.class); - log.info("updateAndCreateTopic called by {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel())); - if (requestHeader.getTopic().equals(this.brokerController.getBrokerConfig().getBrokerClusterName())) { - String errorMsg = "the topic[" + requestHeader.getTopic() + "] is conflict with system reserved words."; - log.warn(errorMsg); + LOGGER.info("Broker receive request to update or create topic={}, caller address={}", + requestHeader.getTopic(), RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + + String topic = requestHeader.getTopic(); + + TopicValidator.ValidateTopicResult result = TopicValidator.validateTopic(topic); + if (!result.isValid()) { response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark(errorMsg); + response.setRemark(result.getRemark()); + return response; + } + if (brokerController.getBrokerConfig().isValidateSystemTopicWhenUpdateTopic()) { + if (TopicValidator.isSystemTopic(topic)) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("The topic[" + topic + "] is conflict with system topic."); + return response; + } + } + + TopicConfig topicConfig = new TopicConfig(topic); + topicConfig.setReadQueueNums(requestHeader.getReadQueueNums()); + topicConfig.setWriteQueueNums(requestHeader.getWriteQueueNums()); + topicConfig.setTopicFilterType(requestHeader.getTopicFilterTypeEnum()); + topicConfig.setPerm(requestHeader.getPerm()); + topicConfig.setTopicSysFlag(requestHeader.getTopicSysFlag() == null ? 0 : requestHeader.getTopicSysFlag()); + topicConfig.setOrder(requestHeader.getOrder()); + String attributesModification = requestHeader.getAttributes(); + topicConfig.setAttributes(AttributeParser.parseToMap(attributesModification)); + + if (topicConfig.getTopicMessageType() == TopicMessageType.MIXED + && !brokerController.getBrokerConfig().isEnableMixedMessageType()) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("MIXED message type is not supported."); + return response; + } + + if (topicConfig.equals(this.brokerController.getTopicConfigManager().getTopicConfigTable().get(topic))) { + LOGGER.info("Broker receive request to update or create topic={}, but topicConfig has no changes , so idempotent, caller address={}", + requestHeader.getTopic(), RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + response.setCode(ResponseCode.SUCCESS); return response; } try { + this.brokerController.getTopicConfigManager().updateTopicConfig(topicConfig); + if (brokerController.getBrokerConfig().isEnableSingleTopicRegister()) { + this.brokerController.registerSingleTopicAll(topicConfig); + } else { + this.brokerController.registerIncrementBrokerData(topicConfig, this.brokerController.getTopicConfigManager().getDataVersion()); + } response.setCode(ResponseCode.SUCCESS); - response.setOpaque(request.getOpaque()); - response.markResponseType(); - response.setRemark(null); - ctx.writeAndFlush(response); } catch (Exception e) { - log.error("Failed to produce a proper response", e); + LOGGER.error("Update / create topic failed for [{}]", request, e); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(e.getMessage()); } - TopicConfig topicConfig = new TopicConfig(requestHeader.getTopic()); + return response; + } + + private synchronized RemotingCommand updateAndCreateStaticTopic(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + final CreateTopicRequestHeader requestHeader = + (CreateTopicRequestHeader) request.decodeCommandCustomHeader(CreateTopicRequestHeader.class); + LOGGER.info("Broker receive request to update or create static topic={}, caller address={}", requestHeader.getTopic(), RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + + final TopicQueueMappingDetail topicQueueMappingDetail = RemotingSerializable.decode(request.getBody(), TopicQueueMappingDetail.class); + + String topic = requestHeader.getTopic(); + + TopicValidator.ValidateTopicResult result = TopicValidator.validateTopic(topic); + if (!result.isValid()) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(result.getRemark()); + return response; + } + if (brokerController.getBrokerConfig().isValidateSystemTopicWhenUpdateTopic()) { + if (TopicValidator.isSystemTopic(topic)) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("The topic[" + topic + "] is conflict with system topic."); + return response; + } + } + boolean force = false; + if (requestHeader.getForce() != null && requestHeader.getForce()) { + force = true; + } + + TopicConfig topicConfig = new TopicConfig(topic); topicConfig.setReadQueueNums(requestHeader.getReadQueueNums()); topicConfig.setWriteQueueNums(requestHeader.getWriteQueueNums()); topicConfig.setTopicFilterType(requestHeader.getTopicFilterTypeEnum()); topicConfig.setPerm(requestHeader.getPerm()); topicConfig.setTopicSysFlag(requestHeader.getTopicSysFlag() == null ? 0 : requestHeader.getTopicSysFlag()); - this.brokerController.getTopicConfigManager().updateTopicConfig(topicConfig); - this.brokerController.registerBrokerAll(false, true); + try { + this.brokerController.getTopicConfigManager().updateTopicConfig(topicConfig); - return null; + this.brokerController.getTopicQueueMappingManager().updateTopicQueueMapping(topicQueueMappingDetail, force, false, true); + + this.brokerController.registerIncrementBrokerData(topicConfig, this.brokerController.getTopicConfigManager().getDataVersion()); + response.setCode(ResponseCode.SUCCESS); + } catch (Exception e) { + LOGGER.error("Update static topic failed for [{}]", request, e); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(e.getMessage()); + } + return response; } - private RemotingCommand deleteTopic(ChannelHandlerContext ctx, + private synchronized RemotingCommand deleteTopic(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException { final RemotingCommand response = RemotingCommand.createResponseCommand(null); DeleteTopicRequestHeader requestHeader = (DeleteTopicRequestHeader) request.decodeCommandCustomHeader(DeleteTopicRequestHeader.class); - log.info("deleteTopic called by {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + LOGGER.info("AdminBrokerProcessor#deleteTopic: broker receive request to delete topic={}, caller={}", + requestHeader.getTopic(), RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + + String topic = requestHeader.getTopic(); - this.brokerController.getTopicConfigManager().deleteTopicConfig(requestHeader.getTopic()); - this.brokerController.getMessageStore() - .cleanUnusedTopic(this.brokerController.getTopicConfigManager().getTopicConfigTable().keySet()); + if (UtilAll.isBlank(topic)) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("The specified topic is blank."); + return response; + } + + if (brokerController.getBrokerConfig().isValidateSystemTopicWhenUpdateTopic()) { + if (TopicValidator.isSystemTopic(topic)) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("The topic[" + topic + "] is conflict with system topic."); + return response; + } + } + final Set groups = this.brokerController.getConsumerOffsetManager().whichGroupByTopic(topic); + // delete pop retry topics first + try { + for (String group : groups) { + final String popRetryTopicV2 = KeyBuilder.buildPopRetryTopic(topic, group, true); + if (brokerController.getTopicConfigManager().selectTopicConfig(popRetryTopicV2) != null) { + deleteTopicInBroker(popRetryTopicV2); + } + final String popRetryTopicV1 = KeyBuilder.buildPopRetryTopicV1(topic, group); + if (brokerController.getTopicConfigManager().selectTopicConfig(popRetryTopicV1) != null) { + deleteTopicInBroker(popRetryTopicV1); + } + } + // delete topic + deleteTopicInBroker(topic); + } catch (Throwable t) { + return buildErrorResponse(ResponseCode.SYSTEM_ERROR, t.getMessage()); + } response.setCode(ResponseCode.SUCCESS); response.setRemark(null); return response; } + private void deleteTopicInBroker(String topic) { + this.brokerController.getTopicConfigManager().deleteTopicConfig(topic); + this.brokerController.getTopicQueueMappingManager().delete(topic); + this.brokerController.getConsumerOffsetManager().cleanOffsetByTopic(topic); + this.brokerController.getPopInflightMessageCounter().clearInFlightMessageNumByTopicName(topic); + this.brokerController.getMessageStore().deleteTopics(Sets.newHashSet(topic)); + } + + private synchronized RemotingCommand updateAndCreateAccessConfig(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + final CreateAccessConfigRequestHeader requestHeader = + (CreateAccessConfigRequestHeader) request.decodeCommandCustomHeader(CreateAccessConfigRequestHeader.class); + + PlainAccessConfig accessConfig = new PlainAccessConfig(); + accessConfig.setAccessKey(requestHeader.getAccessKey()); + accessConfig.setSecretKey(requestHeader.getSecretKey()); + accessConfig.setWhiteRemoteAddress(requestHeader.getWhiteRemoteAddress()); + accessConfig.setDefaultTopicPerm(requestHeader.getDefaultTopicPerm()); + accessConfig.setDefaultGroupPerm(requestHeader.getDefaultGroupPerm()); + accessConfig.setTopicPerms(UtilAll.split(requestHeader.getTopicPerms(), ",")); + accessConfig.setGroupPerms(UtilAll.split(requestHeader.getGroupPerms(), ",")); + accessConfig.setAdmin(requestHeader.isAdmin()); + try { + + AccessValidator accessValidator = this.brokerController.getAccessValidatorMap().get(PlainAccessValidator.class); + if (accessValidator.updateAccessConfig(accessConfig)) { + response.setCode(ResponseCode.SUCCESS); + response.setOpaque(request.getOpaque()); + response.markResponseType(); + response.setRemark(null); + NettyRemotingAbstract.writeResponse(ctx.channel(), request, response); + } else { + String errorMsg = "The accessKey[" + requestHeader.getAccessKey() + "] corresponding to accessConfig has been updated failed."; + LOGGER.warn(errorMsg); + response.setCode(ResponseCode.UPDATE_AND_CREATE_ACL_CONFIG_FAILED); + response.setRemark(errorMsg); + return response; + } + } catch (Exception e) { + LOGGER.error("Failed to generate a proper update accessValidator response", e); + response.setCode(ResponseCode.UPDATE_AND_CREATE_ACL_CONFIG_FAILED); + response.setRemark(e.getMessage()); + return response; + } + + return null; + } + + private synchronized RemotingCommand deleteAccessConfig(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + final DeleteAccessConfigRequestHeader requestHeader = + (DeleteAccessConfigRequestHeader) request.decodeCommandCustomHeader(DeleteAccessConfigRequestHeader.class); + LOGGER.info("DeleteAccessConfig called by {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + + try { + String accessKey = requestHeader.getAccessKey(); + AccessValidator accessValidator = this.brokerController.getAccessValidatorMap().get(PlainAccessValidator.class); + if (accessValidator.deleteAccessConfig(accessKey)) { + response.setCode(ResponseCode.SUCCESS); + response.setOpaque(request.getOpaque()); + response.markResponseType(); + response.setRemark(null); + NettyRemotingAbstract.writeResponse(ctx.channel(), request, response); + } else { + String errorMsg = "The accessKey[" + requestHeader.getAccessKey() + "] corresponding to accessConfig has been deleted failed."; + LOGGER.warn(errorMsg); + response.setCode(ResponseCode.DELETE_ACL_CONFIG_FAILED); + response.setRemark(errorMsg); + return response; + } + + } catch (Exception e) { + LOGGER.error("Failed to generate a proper delete accessValidator response", e); + response.setCode(ResponseCode.DELETE_ACL_CONFIG_FAILED); + response.setRemark(e.getMessage()); + return response; + } + + return null; + } + + private synchronized RemotingCommand updateGlobalWhiteAddrsConfig(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + final UpdateGlobalWhiteAddrsConfigRequestHeader requestHeader = + (UpdateGlobalWhiteAddrsConfigRequestHeader) request.decodeCommandCustomHeader(UpdateGlobalWhiteAddrsConfigRequestHeader.class); + + try { + AccessValidator accessValidator = this.brokerController.getAccessValidatorMap().get(PlainAccessValidator.class); + if (accessValidator.updateGlobalWhiteAddrsConfig(UtilAll.split(requestHeader.getGlobalWhiteAddrs(), ","), + requestHeader.getAclFileFullPath())) { + response.setCode(ResponseCode.SUCCESS); + response.setOpaque(request.getOpaque()); + response.markResponseType(); + response.setRemark(null); + NettyRemotingAbstract.writeResponse(ctx.channel(), request, response); + } else { + String errorMsg = "The globalWhiteAddresses[" + requestHeader.getGlobalWhiteAddrs() + "] has been updated failed."; + LOGGER.warn(errorMsg); + response.setCode(ResponseCode.UPDATE_GLOBAL_WHITE_ADDRS_CONFIG_FAILED); + response.setRemark(errorMsg); + return response; + } + } catch (Exception e) { + LOGGER.error("Failed to generate a proper update globalWhiteAddresses response", e); + response.setCode(ResponseCode.UPDATE_GLOBAL_WHITE_ADDRS_CONFIG_FAILED); + response.setRemark(e.getMessage()); + return response; + } + + return null; + } + + private RemotingCommand getBrokerAclConfigVersion(ChannelHandlerContext ctx, RemotingCommand request) { + + final RemotingCommand response = RemotingCommand.createResponseCommand(GetBrokerAclConfigResponseHeader.class); + + final GetBrokerAclConfigResponseHeader responseHeader = (GetBrokerAclConfigResponseHeader) response.readCustomHeader(); + + try { + AccessValidator accessValidator = this.brokerController.getAccessValidatorMap().get(PlainAccessValidator.class); + + responseHeader.setVersion(accessValidator.getAclConfigVersion()); + responseHeader.setBrokerAddr(this.brokerController.getBrokerAddr()); + responseHeader.setBrokerName(this.brokerController.getBrokerConfig().getBrokerName()); + responseHeader.setClusterName(this.brokerController.getBrokerConfig().getBrokerClusterName()); + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } catch (Exception e) { + LOGGER.error("Failed to generate a proper getBrokerAclConfigVersion response", e); + } + + return null; + } + + private RemotingCommand getUnknownCmdResponse(ChannelHandlerContext ctx, RemotingCommand request) { + String error = " request type " + request.getCode() + " not supported"; + final RemotingCommand response = + RemotingCommand.createResponseCommand(RemotingSysResponseCode.REQUEST_CODE_NOT_SUPPORTED, error); + return response; + } + private RemotingCommand getAllTopicConfig(ChannelHandlerContext ctx, RemotingCommand request) { final RemotingCommand response = RemotingCommand.createResponseCommand(GetAllTopicConfigResponseHeader.class); // final GetAllTopicConfigResponseHeader responseHeader = // (GetAllTopicConfigResponseHeader) response.readCustomHeader(); - String content = this.brokerController.getTopicConfigManager().encode(); + TopicConfigAndMappingSerializeWrapper topicConfigAndMappingSerializeWrapper = new TopicConfigAndMappingSerializeWrapper(); + + topicConfigAndMappingSerializeWrapper.setDataVersion(this.brokerController.getTopicConfigManager().getDataVersion()); + topicConfigAndMappingSerializeWrapper.setTopicConfigTable(this.brokerController.getTopicConfigManager().getTopicConfigTable()); + + topicConfigAndMappingSerializeWrapper.setMappingDataVersion(this.brokerController.getTopicQueueMappingManager().getDataVersion()); + topicConfigAndMappingSerializeWrapper.setTopicQueueMappingDetailMap(this.brokerController.getTopicQueueMappingManager().getTopicQueueMappingTable()); + + String content = topicConfigAndMappingSerializeWrapper.toJson(); if (content != null && content.length() > 0) { try { response.setBody(content.getBytes(MixAll.DEFAULT_CHARSET)); } catch (UnsupportedEncodingException e) { - log.error("", e); + LOGGER.error("", e); response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark("UnsupportedEncodingException " + e); + response.setRemark("UnsupportedEncodingException " + e.getMessage()); return response; } } else { - log.error("No topic in this broker, client: {}", ctx.channel().remoteAddress()); + LOGGER.error("No topic in this broker, client: {}", ctx.channel().remoteAddress()); response.setCode(ResponseCode.SYSTEM_ERROR); response.setRemark("No topic in this broker"); return response; @@ -296,10 +761,167 @@ private RemotingCommand getAllTopicConfig(ChannelHandlerContext ctx, RemotingCom return response; } - private RemotingCommand updateBrokerConfig(ChannelHandlerContext ctx, RemotingCommand request) { + private RemotingCommand getTimerCheckPoint(ChannelHandlerContext ctx, RemotingCommand request) { + final RemotingCommand response = RemotingCommand.createResponseCommand(ResponseCode.SYSTEM_ERROR, "Unknown"); + TimerCheckpoint timerCheckpoint = this.brokerController.getTimerCheckpoint(); + if (null == timerCheckpoint) { + LOGGER.error("AdminBrokerProcessor#getTimerCheckPoint: checkpoint is null, caller={}", ctx.channel().remoteAddress()); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("The checkpoint is null"); + return response; + } + response.setBody(TimerCheckpoint.encode(timerCheckpoint).array()); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + private RemotingCommand getTimerMetrics(ChannelHandlerContext ctx, RemotingCommand request) { + final RemotingCommand response = RemotingCommand.createResponseCommand(ResponseCode.SYSTEM_ERROR, "Unknown"); + TimerMessageStore timerMessageStore = this.brokerController.getMessageStore().getTimerMessageStore(); + if (null == timerMessageStore) { + LOGGER.error("The timer message store is null, client: {}", ctx.channel().remoteAddress()); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("The timer message store is null"); + return response; + } + response.setBody(timerMessageStore.getTimerMetrics().encode().getBytes(StandardCharsets.UTF_8)); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + private synchronized RemotingCommand updateColdDataFlowCtrGroupConfig(ChannelHandlerContext ctx, + RemotingCommand request) { final RemotingCommand response = RemotingCommand.createResponseCommand(null); + LOGGER.info("updateColdDataFlowCtrGroupConfig called by {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel())); - log.info("updateBrokerConfig called by {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + byte[] body = request.getBody(); + if (body != null) { + try { + String bodyStr = new String(body, MixAll.DEFAULT_CHARSET); + Properties properties = MixAll.string2Properties(bodyStr); + if (properties != null) { + LOGGER.info("updateColdDataFlowCtrGroupConfig new config: {}, client: {}", properties, ctx.channel().remoteAddress()); + properties.entrySet().stream().forEach(i -> { + try { + String consumerGroup = String.valueOf(i.getKey()); + Long threshold = Long.valueOf(String.valueOf(i.getValue())); + this.brokerController.getColdDataCgCtrService().addOrUpdateGroupConfig(consumerGroup, threshold); + } catch (Exception e) { + LOGGER.error("updateColdDataFlowCtrGroupConfig properties on entry error, key: {}, val: {}", i.getKey(), i.getValue(), e); + } + }); + } else { + LOGGER.error("updateColdDataFlowCtrGroupConfig string2Properties error"); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("string2Properties error"); + return response; + } + } catch (UnsupportedEncodingException e) { + LOGGER.error("updateColdDataFlowCtrGroupConfig UnsupportedEncodingException", e); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("UnsupportedEncodingException " + e); + return response; + } + } + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + private synchronized RemotingCommand removeColdDataFlowCtrGroupConfig(ChannelHandlerContext ctx, + RemotingCommand request) { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + LOGGER.info("removeColdDataFlowCtrGroupConfig called by {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + + byte[] body = request.getBody(); + if (body != null) { + try { + String consumerGroup = new String(body, MixAll.DEFAULT_CHARSET); + if (consumerGroup != null) { + LOGGER.info("removeColdDataFlowCtrGroupConfig, consumerGroup: {} client: {}", consumerGroup, ctx.channel().remoteAddress()); + this.brokerController.getColdDataCgCtrService().removeGroupConfig(consumerGroup); + } else { + LOGGER.error("removeColdDataFlowCtrGroupConfig string parse error"); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("string parse error"); + return response; + } + } catch (UnsupportedEncodingException e) { + LOGGER.error("removeColdDataFlowCtrGroupConfig UnsupportedEncodingException", e); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("UnsupportedEncodingException " + e); + return response; + } + } + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + private RemotingCommand getColdDataFlowCtrInfo(ChannelHandlerContext ctx) { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + LOGGER.info("getColdDataFlowCtrInfo called by {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + + String content = this.brokerController.getColdDataCgCtrService().getColdDataFlowCtrInfo(); + if (content != null) { + try { + response.setBody(content.getBytes(MixAll.DEFAULT_CHARSET)); + } catch (UnsupportedEncodingException e) { + LOGGER.error("getColdDataFlowCtrInfo UnsupportedEncodingException", e); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("UnsupportedEncodingException " + e); + return response; + } + } + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + private RemotingCommand setCommitLogReadaheadMode(ChannelHandlerContext ctx, RemotingCommand request) { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + LOGGER.info("setCommitLogReadaheadMode called by {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + + try { + HashMap extFields = request.getExtFields(); + if (null == extFields) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("set commitlog readahead mode param error"); + return response; + } + int mode = Integer.parseInt(extFields.get(FIleReadaheadMode.READ_AHEAD_MODE)); + if (mode != LibC.MADV_RANDOM && mode != LibC.MADV_NORMAL) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("set commitlog readahead mode param value error"); + return response; + } + MessageStore messageStore = this.brokerController.getMessageStore(); + if (messageStore instanceof DefaultMessageStore) { + DefaultMessageStore defaultMessageStore = (DefaultMessageStore) messageStore; + if (mode == LibC.MADV_NORMAL) { + defaultMessageStore.getMessageStoreConfig().setDataReadAheadEnable(true); + } else { + defaultMessageStore.getMessageStoreConfig().setDataReadAheadEnable(false); + } + defaultMessageStore.getCommitLog().scanFileAndSetReadMode(mode); + } + response.setCode(ResponseCode.SUCCESS); + response.setRemark("set commitlog readahead mode success, mode: " + mode); + } catch (Exception e) { + LOGGER.error("set commitlog readahead mode failed", e); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("set commitlog readahead mode failed"); + } + return response; + } + + private synchronized RemotingCommand updateBrokerConfig(ChannelHandlerContext ctx, RemotingCommand request) { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + final String callerAddress = RemotingHelper.parseChannelRemoteAddr(ctx.channel()); + LOGGER.info("Broker receive request to update config, caller address={}", callerAddress); byte[] body = request.getBody(); if (body != null) { @@ -307,20 +929,29 @@ private RemotingCommand updateBrokerConfig(ChannelHandlerContext ctx, RemotingCo String bodyStr = new String(body, MixAll.DEFAULT_CHARSET); Properties properties = MixAll.string2Properties(bodyStr); if (properties != null) { - log.info("updateBrokerConfig, new config: [{}] client: {} ", properties, ctx.channel().remoteAddress()); + LOGGER.info("updateBrokerConfig, new config: [{}] client: {} ", properties, callerAddress); + if (validateBlackListConfigExist(properties)) { + response.setCode(ResponseCode.NO_PERMISSION); + response.setRemark("Can not update config in black list."); + return response; + } + this.brokerController.getConfiguration().update(properties); if (properties.containsKey("brokerPermission")) { - this.brokerController.registerBrokerAll(false, false); - this.brokerController.getTopicConfigManager().getDataVersion().nextVersion(); + long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0; + this.brokerController.getTopicConfigManager().getDataVersion().nextVersion(stateMachineVersion); + this.brokerController.registerBrokerAll(false, false, true); } + } else { - log.error("string2Properties error"); + LOGGER.error("string2Properties error"); response.setCode(ResponseCode.SYSTEM_ERROR); response.setRemark("string2Properties error"); return response; } } catch (UnsupportedEncodingException e) { - log.error("", e); + LOGGER.error("AdminBrokerProcessor#updateBrokerConfig: unexpected error, caller={}", + callerAddress, e); response.setCode(ResponseCode.SYSTEM_ERROR); response.setRemark("UnsupportedEncodingException " + e); return response; @@ -342,7 +973,8 @@ private RemotingCommand getBrokerConfig(ChannelHandlerContext ctx, RemotingComma try { response.setBody(content.getBytes(MixAll.DEFAULT_CHARSET)); } catch (UnsupportedEncodingException e) { - log.error("", e); + LOGGER.error("AdminBrokerProcessor#getBrokerConfig: unexpected error, caller={}", + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), e); response.setCode(ResponseCode.SYSTEM_ERROR); response.setRemark("UnsupportedEncodingException " + e); @@ -357,6 +989,63 @@ private RemotingCommand getBrokerConfig(ChannelHandlerContext ctx, RemotingComma return response; } + private RemotingCommand rewriteRequestForStaticTopic(SearchOffsetRequestHeader requestHeader, + TopicQueueMappingContext mappingContext) { + try { + if (mappingContext.getMappingDetail() == null) { + return null; + } + + TopicQueueMappingDetail mappingDetail = mappingContext.getMappingDetail(); + List mappingItems = mappingContext.getMappingItemList(); + if (!mappingContext.isLeader()) { + return buildErrorResponse(ResponseCode.NOT_LEADER_FOR_QUEUE, String.format("%s-%d does not exit in request process of current broker %s", mappingContext.getTopic(), mappingContext.getGlobalId(), mappingDetail.getBname())); + } + //TO DO should make sure the timestampOfOffset is equal or bigger than the searched timestamp + Long timestamp = requestHeader.getTimestamp(); + long offset = -1; + for (int i = 0; i < mappingItems.size(); i++) { + LogicQueueMappingItem item = mappingItems.get(i); + if (!item.checkIfLogicoffsetDecided()) { + continue; + } + if (mappingDetail.getBname().equals(item.getBname())) { + offset = this.brokerController.getMessageStore().getOffsetInQueueByTime(mappingContext.getTopic(), item.getQueueId(), timestamp, requestHeader.getBoundaryType()); + if (offset > 0) { + offset = item.computeStaticQueueOffsetStrictly(offset); + break; + } + } else { + requestHeader.setLo(false); + requestHeader.setTimestamp(timestamp); + requestHeader.setQueueId(item.getQueueId()); + requestHeader.setBrokerName(item.getBname()); + RpcRequest rpcRequest = new RpcRequest(RequestCode.SEARCH_OFFSET_BY_TIMESTAMP, requestHeader, null); + RpcResponse rpcResponse = this.brokerController.getBrokerOuterAPI().getRpcClient().invoke(rpcRequest, this.brokerController.getBrokerConfig().getForwardTimeout()).get(); + if (rpcResponse.getException() != null) { + throw rpcResponse.getException(); + } + SearchOffsetResponseHeader offsetResponseHeader = (SearchOffsetResponseHeader) rpcResponse.getHeader(); + if (offsetResponseHeader.getOffset() < 0 + || item.checkIfEndOffsetDecided() && offsetResponseHeader.getOffset() >= item.getEndOffset()) { + continue; + } else { + offset = item.computeStaticQueueOffsetStrictly(offsetResponseHeader.getOffset()); + } + + } + } + final RemotingCommand response = RemotingCommand.createResponseCommand(SearchOffsetResponseHeader.class); + final SearchOffsetResponseHeader responseHeader = (SearchOffsetResponseHeader) response.readCustomHeader(); + responseHeader.setOffset(offset); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } catch (Throwable t) { + return buildErrorResponse(ResponseCode.SYSTEM_ERROR, t.getMessage()); + } + } + private RemotingCommand searchOffsetByTimestamp(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException { final RemotingCommand response = RemotingCommand.createResponseCommand(SearchOffsetResponseHeader.class); @@ -364,8 +1053,15 @@ private RemotingCommand searchOffsetByTimestamp(ChannelHandlerContext ctx, final SearchOffsetRequestHeader requestHeader = (SearchOffsetRequestHeader) request.decodeCommandCustomHeader(SearchOffsetRequestHeader.class); + TopicQueueMappingContext mappingContext = this.brokerController.getTopicQueueMappingManager().buildTopicQueueMappingContext(requestHeader); + + RemotingCommand rewriteResult = rewriteRequestForStaticTopic(requestHeader, mappingContext); + if (rewriteResult != null) { + return rewriteResult; + } + long offset = this.brokerController.getMessageStore().getOffsetInQueueByTime(requestHeader.getTopic(), requestHeader.getQueueId(), - requestHeader.getTimestamp()); + requestHeader.getTimestamp(), requestHeader.getBoundaryType()); responseHeader.setOffset(offset); @@ -374,6 +1070,51 @@ private RemotingCommand searchOffsetByTimestamp(ChannelHandlerContext ctx, return response; } + private RemotingCommand rewriteRequestForStaticTopic(GetMaxOffsetRequestHeader requestHeader, + TopicQueueMappingContext mappingContext) { + if (mappingContext.getMappingDetail() == null) { + return null; + } + + TopicQueueMappingDetail mappingDetail = mappingContext.getMappingDetail(); + LogicQueueMappingItem mappingItem = mappingContext.getLeaderItem(); + if (!mappingContext.isLeader()) { + return buildErrorResponse(ResponseCode.NOT_LEADER_FOR_QUEUE, String.format("%s-%d does not exit in request process of current broker %s", mappingContext.getTopic(), mappingContext.getGlobalId(), mappingDetail.getBname())); + } + + try { + LogicQueueMappingItem maxItem = TopicQueueMappingUtils.findLogicQueueMappingItem(mappingContext.getMappingItemList(), Long.MAX_VALUE, true); + assert maxItem != null; + assert maxItem.getLogicOffset() >= 0; + requestHeader.setBrokerName(maxItem.getBname()); + requestHeader.setLo(false); + requestHeader.setQueueId(mappingItem.getQueueId()); + + long maxPhysicalOffset = Long.MAX_VALUE; + if (maxItem.getBname().equals(mappingDetail.getBname())) { + //current broker + maxPhysicalOffset = this.brokerController.getMessageStore().getMaxOffsetInQueue(mappingContext.getTopic(), mappingItem.getQueueId()); + } else { + RpcRequest rpcRequest = new RpcRequest(RequestCode.GET_MAX_OFFSET, requestHeader, null); + RpcResponse rpcResponse = this.brokerController.getBrokerOuterAPI().getRpcClient().invoke(rpcRequest, this.brokerController.getBrokerConfig().getForwardTimeout()).get(); + if (rpcResponse.getException() != null) { + throw rpcResponse.getException(); + } + GetMaxOffsetResponseHeader offsetResponseHeader = (GetMaxOffsetResponseHeader) rpcResponse.getHeader(); + maxPhysicalOffset = offsetResponseHeader.getOffset(); + } + + final RemotingCommand response = RemotingCommand.createResponseCommand(GetMaxOffsetResponseHeader.class); + final GetMaxOffsetResponseHeader responseHeader = (GetMaxOffsetResponseHeader) response.readCustomHeader(); + responseHeader.setOffset(maxItem.computeStaticQueueOffsetStrictly(maxPhysicalOffset)); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } catch (Throwable t) { + return buildErrorResponse(ResponseCode.SYSTEM_ERROR, t.getMessage()); + } + } + private RemotingCommand getMaxOffset(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException { final RemotingCommand response = RemotingCommand.createResponseCommand(GetMaxOffsetResponseHeader.class); @@ -381,6 +1122,12 @@ private RemotingCommand getMaxOffset(ChannelHandlerContext ctx, final GetMaxOffsetRequestHeader requestHeader = (GetMaxOffsetRequestHeader) request.decodeCommandCustomHeader(GetMaxOffsetRequestHeader.class); + TopicQueueMappingContext mappingContext = this.brokerController.getTopicQueueMappingManager().buildTopicQueueMappingContext(requestHeader); + RemotingCommand rewriteResult = rewriteRequestForStaticTopic(requestHeader, mappingContext); + if (rewriteResult != null) { + return rewriteResult; + } + long offset = this.brokerController.getMessageStore().getMaxOffsetInQueue(requestHeader.getTopic(), requestHeader.getQueueId()); responseHeader.setOffset(offset); @@ -390,19 +1137,107 @@ private RemotingCommand getMaxOffset(ChannelHandlerContext ctx, return response; } + private CompletableFuture handleGetMinOffsetForStaticTopic(RpcRequest request, + TopicQueueMappingContext mappingContext) { + if (mappingContext.getMappingDetail() == null) { + return null; + } + TopicQueueMappingDetail mappingDetail = mappingContext.getMappingDetail(); + if (!mappingContext.isLeader()) { + //this may not + return CompletableFuture.completedFuture(new RpcResponse(new RpcException(ResponseCode.NOT_LEADER_FOR_QUEUE, + String.format("%s-%d is not leader in broker %s, request code %d", mappingContext.getTopic(), mappingContext.getGlobalId(), mappingDetail.getBname(), request.getCode())))); + } + GetMinOffsetRequestHeader requestHeader = (GetMinOffsetRequestHeader) request.getHeader(); + LogicQueueMappingItem mappingItem = TopicQueueMappingUtils.findLogicQueueMappingItem(mappingContext.getMappingItemList(), 0L, true); + assert mappingItem != null; + try { + requestHeader.setBrokerName(mappingItem.getBname()); + requestHeader.setLo(false); + requestHeader.setQueueId(mappingItem.getQueueId()); + long physicalOffset; + //run in local + if (mappingItem.getBname().equals(mappingDetail.getBname())) { + physicalOffset = this.brokerController.getMessageStore().getMinOffsetInQueue(mappingDetail.getTopic(), mappingItem.getQueueId()); + } else { + RpcRequest rpcRequest = new RpcRequest(RequestCode.GET_MIN_OFFSET, requestHeader, null); + RpcResponse rpcResponse = this.brokerController.getBrokerOuterAPI().getRpcClient().invoke(rpcRequest, this.brokerController.getBrokerConfig().getForwardTimeout()).get(); + if (rpcResponse.getException() != null) { + throw rpcResponse.getException(); + } + GetMinOffsetResponseHeader offsetResponseHeader = (GetMinOffsetResponseHeader) rpcResponse.getHeader(); + physicalOffset = offsetResponseHeader.getOffset(); + } + long offset = mappingItem.computeStaticQueueOffsetLoosely(physicalOffset); + + final GetMinOffsetResponseHeader responseHeader = new GetMinOffsetResponseHeader(); + responseHeader.setOffset(offset); + return CompletableFuture.completedFuture(new RpcResponse(ResponseCode.SUCCESS, responseHeader, null)); + } catch (Throwable t) { + LOGGER.error("rewriteRequestForStaticTopic failed", t); + return CompletableFuture.completedFuture(new RpcResponse(new RpcException(ResponseCode.SYSTEM_ERROR, t.getMessage(), t))); + } + } + + private CompletableFuture handleGetMinOffset(RpcRequest request) { + assert request.getCode() == RequestCode.GET_MIN_OFFSET; + GetMinOffsetRequestHeader requestHeader = (GetMinOffsetRequestHeader) request.getHeader(); + TopicQueueMappingContext mappingContext = this.brokerController.getTopicQueueMappingManager().buildTopicQueueMappingContext(requestHeader, false); + CompletableFuture rewriteResult = handleGetMinOffsetForStaticTopic(request, mappingContext); + if (rewriteResult != null) { + return rewriteResult; + } + final GetMinOffsetResponseHeader responseHeader = new GetMinOffsetResponseHeader(); + long offset = this.brokerController.getMessageStore().getMinOffsetInQueue(requestHeader.getTopic(), requestHeader.getQueueId()); + responseHeader.setOffset(offset); + return CompletableFuture.completedFuture(new RpcResponse(ResponseCode.SUCCESS, responseHeader, null)); + } + private RemotingCommand getMinOffset(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException { - final RemotingCommand response = RemotingCommand.createResponseCommand(GetMinOffsetResponseHeader.class); - final GetMinOffsetResponseHeader responseHeader = (GetMinOffsetResponseHeader) response.readCustomHeader(); final GetMinOffsetRequestHeader requestHeader = (GetMinOffsetRequestHeader) request.decodeCommandCustomHeader(GetMinOffsetRequestHeader.class); + try { + CompletableFuture responseFuture = handleGetMinOffset(new RpcRequest(RequestCode.GET_MIN_OFFSET, requestHeader, null)); + RpcResponse rpcResponse = responseFuture.get(); + return RpcClientUtils.createCommandForRpcResponse(rpcResponse); + } catch (Throwable t) { + return buildErrorResponse(ResponseCode.SYSTEM_ERROR, t.getMessage()); + } + } - long offset = this.brokerController.getMessageStore().getMinOffsetInQueue(requestHeader.getTopic(), requestHeader.getQueueId()); + private RemotingCommand rewriteRequestForStaticTopic(GetEarliestMsgStoretimeRequestHeader requestHeader, + TopicQueueMappingContext mappingContext) { + if (mappingContext.getMappingDetail() == null) { + return null; + } - responseHeader.setOffset(offset); - response.setCode(ResponseCode.SUCCESS); - response.setRemark(null); - return response; + TopicQueueMappingDetail mappingDetail = mappingContext.getMappingDetail(); + if (!mappingContext.isLeader()) { + return buildErrorResponse(ResponseCode.NOT_LEADER_FOR_QUEUE, String.format("%s-%d does not exit in request process of current broker %s", mappingContext.getTopic(), mappingContext.getGlobalId(), mappingDetail.getBname())); + } + LogicQueueMappingItem mappingItem = TopicQueueMappingUtils.findLogicQueueMappingItem(mappingContext.getMappingItemList(), 0L, true); + assert mappingItem != null; + try { + requestHeader.setBrokerName(mappingItem.getBname()); + requestHeader.setLo(false); + RpcRequest rpcRequest = new RpcRequest(RequestCode.GET_EARLIEST_MSG_STORETIME, requestHeader, null); + //TO DO check if it is in current broker + RpcResponse rpcResponse = this.brokerController.getBrokerOuterAPI().getRpcClient().invoke(rpcRequest, this.brokerController.getBrokerConfig().getForwardTimeout()).get(); + if (rpcResponse.getException() != null) { + throw rpcResponse.getException(); + } + GetEarliestMsgStoretimeResponseHeader offsetResponseHeader = (GetEarliestMsgStoretimeResponseHeader) rpcResponse.getHeader(); + + final RemotingCommand response = RemotingCommand.createResponseCommand(GetEarliestMsgStoretimeResponseHeader.class); + final GetEarliestMsgStoretimeResponseHeader responseHeader = (GetEarliestMsgStoretimeResponseHeader) response.readCustomHeader(); + responseHeader.setTimestamp(offsetResponseHeader.getTimestamp()); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } catch (Throwable t) { + return buildErrorResponse(ResponseCode.SYSTEM_ERROR, t.getMessage()); + } } private RemotingCommand getEarliestMsgStoretime(ChannelHandlerContext ctx, @@ -412,6 +1247,12 @@ private RemotingCommand getEarliestMsgStoretime(ChannelHandlerContext ctx, final GetEarliestMsgStoretimeRequestHeader requestHeader = (GetEarliestMsgStoretimeRequestHeader) request.decodeCommandCustomHeader(GetEarliestMsgStoretimeRequestHeader.class); + TopicQueueMappingContext mappingContext = this.brokerController.getTopicQueueMappingManager().buildTopicQueueMappingContext(requestHeader, false); + RemotingCommand rewriteResult = rewriteRequestForStaticTopic(requestHeader, mappingContext); + if (rewriteResult != null) { + return rewriteResult; + } + long timestamp = this.brokerController.getMessageStore().getEarliestMessageTime(requestHeader.getTopic(), requestHeader.getQueueId()); @@ -440,10 +1281,78 @@ private RemotingCommand lockBatchMQ(ChannelHandlerContext ctx, final RemotingCommand response = RemotingCommand.createResponseCommand(null); LockBatchRequestBody requestBody = LockBatchRequestBody.decode(request.getBody(), LockBatchRequestBody.class); - Set lockOKMQSet = this.brokerController.getRebalanceLockManager().tryLockBatch( + Set lockOKMQSet = new HashSet<>(); + Set selfLockOKMQSet = this.brokerController.getRebalanceLockManager().tryLockBatch( requestBody.getConsumerGroup(), requestBody.getMqSet(), requestBody.getClientId()); + if (requestBody.isOnlyThisBroker() || !brokerController.getBrokerConfig().isLockInStrictMode()) { + lockOKMQSet = selfLockOKMQSet; + } else { + requestBody.setOnlyThisBroker(true); + int replicaSize = this.brokerController.getMessageStoreConfig().getTotalReplicas(); + + int quorum = replicaSize / 2 + 1; + + if (quorum <= 1) { + lockOKMQSet = selfLockOKMQSet; + } else { + final ConcurrentMap mqLockMap = new ConcurrentHashMap<>(); + for (MessageQueue mq : selfLockOKMQSet) { + if (!mqLockMap.containsKey(mq)) { + mqLockMap.put(mq, 0); + } + mqLockMap.put(mq, mqLockMap.get(mq) + 1); + } + + BrokerMemberGroup memberGroup = this.brokerController.getBrokerMemberGroup(); + + if (memberGroup != null) { + Map addrMap = new HashMap<>(memberGroup.getBrokerAddrs()); + addrMap.remove(this.brokerController.getBrokerConfig().getBrokerId()); + final CountDownLatch countDownLatch = new CountDownLatch(addrMap.size()); + requestBody.setMqSet(selfLockOKMQSet); + requestBody.setOnlyThisBroker(true); + for (Long brokerId : addrMap.keySet()) { + try { + this.brokerController.getBrokerOuterAPI().lockBatchMQAsync(addrMap.get(brokerId), + requestBody, 1000, new LockCallback() { + @Override + public void onSuccess(Set lockOKMQSet) { + for (MessageQueue mq : lockOKMQSet) { + if (!mqLockMap.containsKey(mq)) { + mqLockMap.put(mq, 0); + } + mqLockMap.put(mq, mqLockMap.get(mq) + 1); + } + countDownLatch.countDown(); + } + + @Override + public void onException(Throwable e) { + LOGGER.warn("lockBatchMQAsync on {} failed, {}", addrMap.get(brokerId), e); + countDownLatch.countDown(); + } + }); + } catch (Exception e) { + LOGGER.warn("lockBatchMQAsync on {} failed, {}", addrMap.get(brokerId), e); + countDownLatch.countDown(); + } + } + try { + countDownLatch.await(2000, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + LOGGER.warn("lockBatchMQ exception on {}, {}", this.brokerController.getBrokerConfig().getBrokerName(), e); + } + } + + for (MessageQueue mq : mqLockMap.keySet()) { + if (mqLockMap.get(mq) >= quorum) { + lockOKMQSet.add(mq); + } + } + } + } LockBatchResponseBody responseBody = new LockBatchResponseBody(); responseBody.setLockOKMQSet(lockOKMQSet); @@ -459,10 +1368,36 @@ private RemotingCommand unlockBatchMQ(ChannelHandlerContext ctx, final RemotingCommand response = RemotingCommand.createResponseCommand(null); UnlockBatchRequestBody requestBody = UnlockBatchRequestBody.decode(request.getBody(), UnlockBatchRequestBody.class); - this.brokerController.getRebalanceLockManager().unlockBatch( - requestBody.getConsumerGroup(), - requestBody.getMqSet(), - requestBody.getClientId()); + if (requestBody.isOnlyThisBroker() || !this.brokerController.getBrokerConfig().isLockInStrictMode()) { + this.brokerController.getRebalanceLockManager().unlockBatch( + requestBody.getConsumerGroup(), + requestBody.getMqSet(), + requestBody.getClientId()); + } else { + requestBody.setOnlyThisBroker(true); + BrokerMemberGroup memberGroup = this.brokerController.getBrokerMemberGroup(); + + if (memberGroup != null) { + Map addrMap = memberGroup.getBrokerAddrs(); + for (Long brokerId : addrMap.keySet()) { + try { + this.brokerController.getBrokerOuterAPI().unlockBatchMQAsync(addrMap.get(brokerId), requestBody, 1000, new UnlockCallback() { + @Override + public void onSuccess() { + + } + + @Override + public void onException(Throwable e) { + LOGGER.warn("unlockBatchMQ exception on {}, {}", addrMap.get(brokerId), e); + } + }); + } catch (Exception e) { + LOGGER.warn("unlockBatchMQ exception on {}, {}", addrMap.get(brokerId), e); + } + } + } + } response.setCode(ResponseCode.SUCCESS); response.setRemark(null); @@ -473,7 +1408,8 @@ private RemotingCommand updateAndCreateSubscriptionGroup(ChannelHandlerContext c throws RemotingCommandException { final RemotingCommand response = RemotingCommand.createResponseCommand(null); - log.info("updateAndCreateSubscriptionGroup called by {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + LOGGER.info("AdminBrokerProcessor#updateAndCreateSubscriptionGroup called by {}", + RemotingHelper.parseChannelRemoteAddr(ctx.channel())); SubscriptionGroupConfig config = RemotingSerializable.decode(request.getBody(), SubscriptionGroupConfig.class); if (config != null) { @@ -485,6 +1421,26 @@ private RemotingCommand updateAndCreateSubscriptionGroup(ChannelHandlerContext c return response; } + private void initConsumerOffset(String clientHost, String groupName, int mode, TopicConfig topicConfig) { + String topic = topicConfig.getTopicName(); + for (int queueId = 0; queueId < topicConfig.getReadQueueNums(); queueId++) { + if (this.brokerController.getConsumerOffsetManager().queryOffset(groupName, topic, queueId) > -1) { + continue; + } + long offset = 0; + if (this.brokerController.getMessageStore().getConsumeQueue(topic, queueId) != null) { + if (ConsumeInitMode.MAX == mode) { + offset = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId); + } else if (ConsumeInitMode.MIN == mode) { + offset = this.brokerController.getMessageStore().getMinOffsetInQueue(topic, queueId); + } + } + this.brokerController.getConsumerOffsetManager().commitOffset(clientHost, groupName, topic, queueId, offset); + LOGGER.info("AdminBrokerProcessor#initConsumerOffset: consumerGroup={}, topic={}, queueId={}, offset={}", + groupName, topic, queueId, offset); + } + } + private RemotingCommand getAllSubscriptionGroup(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException { final RemotingCommand response = RemotingCommand.createResponseCommand(null); @@ -493,14 +1449,14 @@ private RemotingCommand getAllSubscriptionGroup(ChannelHandlerContext ctx, try { response.setBody(content.getBytes(MixAll.DEFAULT_CHARSET)); } catch (UnsupportedEncodingException e) { - log.error("", e); + LOGGER.error("UnsupportedEncodingException getAllSubscriptionGroup", e); response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark("UnsupportedEncodingException " + e); + response.setRemark("UnsupportedEncodingException " + e.getMessage()); return response; } } else { - log.error("No subscription group in this broker, client:{} ", ctx.channel().remoteAddress()); + LOGGER.error("No subscription group in this broker, client:{} ", ctx.channel().remoteAddress()); response.setCode(ResponseCode.SYSTEM_ERROR); response.setRemark("No subscription group in this broker"); return response; @@ -518,10 +1474,19 @@ private RemotingCommand deleteSubscriptionGroup(ChannelHandlerContext ctx, DeleteSubscriptionGroupRequestHeader requestHeader = (DeleteSubscriptionGroupRequestHeader) request.decodeCommandCustomHeader(DeleteSubscriptionGroupRequestHeader.class); - log.info("deleteSubscriptionGroup called by {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + LOGGER.info("AdminBrokerProcessor#deleteSubscriptionGroup, caller={}", + RemotingHelper.parseChannelRemoteAddr(ctx.channel())); this.brokerController.getSubscriptionGroupManager().deleteSubscriptionGroupConfig(requestHeader.getGroupName()); + if (requestHeader.isCleanOffset()) { + this.brokerController.getConsumerOffsetManager().removeOffset(requestHeader.getGroupName()); + this.brokerController.getPopInflightMessageCounter().clearInFlightMessageNumByGroupName(requestHeader.getGroupName()); + } + + if (this.brokerController.getBrokerConfig().isAutoDeleteUnusedStats()) { + this.brokerController.getBrokerStatsManager().onGroupDeleted(requestHeader.getGroupName()); + } response.setCode(ResponseCode.SUCCESS); response.setRemark(null); return response; @@ -550,12 +1515,14 @@ private RemotingCommand getTopicStatsInfo(ChannelHandlerContext ctx, TopicOffset topicOffset = new TopicOffset(); long min = this.brokerController.getMessageStore().getMinOffsetInQueue(topic, i); - if (min < 0) + if (min < 0) { min = 0; + } long max = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, i); - if (max < 0) + if (max < 0) { max = 0; + } long timestamp = 0; if (max > 0) { @@ -616,6 +1583,25 @@ private RemotingCommand getConsumerConnectionList(ChannelHandlerContext ctx, return response; } + private RemotingCommand getAllProducerInfo(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + final GetAllProducerInfoRequestHeader requestHeader = + (GetAllProducerInfoRequestHeader) request.decodeCommandCustomHeader(GetAllProducerInfoRequestHeader.class); + + ProducerTableInfo producerTable = this.brokerController.getProducerManager().getProducerTable(); + if (producerTable != null) { + byte[] body = producerTable.encode(); + response.setBody(body); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + response.setCode(ResponseCode.SYSTEM_ERROR); + return response; + } + private RemotingCommand getProducerConnectionList(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException { final RemotingCommand response = RemotingCommand.createResponseCommand(null); @@ -623,7 +1609,7 @@ private RemotingCommand getProducerConnectionList(ChannelHandlerContext ctx, (GetProducerConnectionListRequestHeader) request.decodeCommandCustomHeader(GetProducerConnectionListRequestHeader.class); ProducerConnection bodydata = new ProducerConnection(); - HashMap channelInfoHashMap = + Map channelInfoHashMap = this.brokerController.getProducerManager().getGroupChannelTable().get(requestHeader.getProducerGroup()); if (channelInfoHashMap != null) { Iterator> it = channelInfoHashMap.entrySet().iterator(); @@ -658,7 +1644,7 @@ private RemotingCommand getConsumeStats(ChannelHandlerContext ctx, ConsumeStats consumeStats = new ConsumeStats(); - Set topics = new HashSet(); + Set topics = new HashSet<>(); if (UtilAll.isBlank(requestHeader.getTopic())) { topics = this.brokerController.getConsumerOffsetManager().whichTopicByConsumer(requestHeader.getConsumerGroup()); } else { @@ -668,17 +1654,21 @@ private RemotingCommand getConsumeStats(ChannelHandlerContext ctx, for (String topic : topics) { TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(topic); if (null == topicConfig) { - log.warn("consumeStats, topic config not exist, {}", topic); + LOGGER.warn("AdminBrokerProcessor#getConsumeStats: topic config does not exist, topic={}", topic); continue; } + TopicQueueMappingDetail mappingDetail = this.brokerController.getTopicQueueMappingManager().getTopicQueueMapping(topic); + { SubscriptionData findSubscriptionData = this.brokerController.getConsumerManager().findSubscriptionData(requestHeader.getConsumerGroup(), topic); if (null == findSubscriptionData && this.brokerController.getConsumerManager().findSubscriptionDataCount(requestHeader.getConsumerGroup()) > 0) { - log.warn("consumeStats, the consumer group[{}], topic[{}] not exist", requestHeader.getConsumerGroup(), topic); + LOGGER.warn( + "AdminBrokerProcessor#getConsumeStats: topic does not exist in consumer group's subscription, " + + "topic={}, consumer group={}", topic, requestHeader.getConsumerGroup()); continue; } } @@ -692,18 +1682,28 @@ private RemotingCommand getConsumeStats(ChannelHandlerContext ctx, OffsetWrapper offsetWrapper = new OffsetWrapper(); long brokerOffset = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, i); - if (brokerOffset < 0) + if (brokerOffset < 0) { brokerOffset = 0; + } + + long consumerOffset = this.brokerController.getConsumerOffsetManager().queryOffset( + requestHeader.getConsumerGroup(), topic, i); + + // the consumerOffset cannot be zero for static topic because of the "double read check" strategy + // just remain the logic for dynamic topic + // maybe we should remove it in the future + if (mappingDetail == null) { + if (consumerOffset < 0) { + consumerOffset = 0; + } + } - long consumerOffset = this.brokerController.getConsumerOffsetManager().queryOffset( - requestHeader.getConsumerGroup(), - topic, - i); - if (consumerOffset < 0) - consumerOffset = 0; + long pullOffset = this.brokerController.getConsumerOffsetManager().queryPullOffset( + requestHeader.getConsumerGroup(), topic, i); offsetWrapper.setBrokerOffset(brokerOffset); offsetWrapper.setConsumerOffset(consumerOffset); + offsetWrapper.setPullOffset(Math.max(consumerOffset, pullOffset)); long timeOffset = consumerOffset - 1; if (timeOffset >= 0) { @@ -737,14 +1737,14 @@ private RemotingCommand getAllConsumerOffset(ChannelHandlerContext ctx, Remoting try { response.setBody(content.getBytes(MixAll.DEFAULT_CHARSET)); } catch (UnsupportedEncodingException e) { - log.error("get all consumer offset from master error.", e); + LOGGER.error("get all consumer offset from master error.", e); response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark("UnsupportedEncodingException " + e); + response.setRemark("UnsupportedEncodingException " + e.getMessage()); return response; } } else { - log.error("No consumer offset in this broker, client: {} ", ctx.channel().remoteAddress()); + LOGGER.error("No consumer offset in this broker, client: {} ", ctx.channel().remoteAddress()); response.setCode(ResponseCode.SYSTEM_ERROR); response.setRemark("No consumer offset in this broker"); return response; @@ -759,19 +1759,21 @@ private RemotingCommand getAllConsumerOffset(ChannelHandlerContext ctx, Remoting private RemotingCommand getAllDelayOffset(ChannelHandlerContext ctx, RemotingCommand request) { final RemotingCommand response = RemotingCommand.createResponseCommand(null); - String content = ((DefaultMessageStore) this.brokerController.getMessageStore()).getScheduleMessageService().encode(); + String content = this.brokerController.getScheduleMessageService().encode(); if (content != null && content.length() > 0) { try { response.setBody(content.getBytes(MixAll.DEFAULT_CHARSET)); } catch (UnsupportedEncodingException e) { - log.error("get all delay offset from master error.", e); + LOGGER.error("AdminBrokerProcessor#getAllDelayOffset: unexpected error, caller={}.", + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), e); response.setCode(ResponseCode.SYSTEM_ERROR); response.setRemark("UnsupportedEncodingException " + e); return response; } } else { - log.error("No delay offset in this broker, client: {} ", ctx.channel().remoteAddress()); + LOGGER.error("AdminBrokerProcessor#getAllDelayOffset: no delay offset in this broker, caller={}", + RemotingHelper.parseChannelRemoteAddr(ctx.channel())); response.setCode(ResponseCode.SYSTEM_ERROR); response.setRemark("No delay offset in this broker"); return response; @@ -783,13 +1785,50 @@ private RemotingCommand getAllDelayOffset(ChannelHandlerContext ctx, RemotingCom return response; } + private RemotingCommand getAllMessageRequestMode(ChannelHandlerContext ctx, RemotingCommand request) { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + String content = this.brokerController.getQueryAssignmentProcessor().getMessageRequestModeManager().encode(); + if (content != null && content.length() > 0) { + try { + response.setBody(content.getBytes(MixAll.DEFAULT_CHARSET)); + } catch (UnsupportedEncodingException e) { + LOGGER.error("get all message request mode from master error.", e); + + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("UnsupportedEncodingException " + e); + return response; + } + } else { + LOGGER.error("No message request mode in this broker, client: {} ", ctx.channel().remoteAddress()); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("No message request mode in this broker"); + return response; + } + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + + return response; + } + public RemotingCommand resetOffset(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException { final ResetOffsetRequestHeader requestHeader = (ResetOffsetRequestHeader) request.decodeCommandCustomHeader(ResetOffsetRequestHeader.class); - log.info("[reset-offset] reset offset started by {}. topic={}, group={}, timestamp={}, isForce={}", + LOGGER.info("[reset-offset] reset offset started by {}. topic={}, group={}, timestamp={}, isForce={}", RemotingHelper.parseChannelRemoteAddr(ctx.channel()), requestHeader.getTopic(), requestHeader.getGroup(), requestHeader.getTimestamp(), requestHeader.isForce()); + + if (this.brokerController.getBrokerConfig().isUseServerSideResetOffset()) { + String topic = requestHeader.getTopic(); + String group = requestHeader.getGroup(); + int queueId = requestHeader.getQueueId(); + long timestamp = requestHeader.getTimestamp(); + Long offset = requestHeader.getOffset(); + return resetOffsetInner(topic, group, queueId, timestamp, offset); + } + boolean isC = false; LanguageCode language = request.getLanguage(); switch (language) { @@ -801,12 +1840,105 @@ public RemotingCommand resetOffset(ChannelHandlerContext ctx, requestHeader.getTimestamp(), requestHeader.isForce(), isC); } + private Long searchOffsetByTimestamp(String topic, int queueId, long timestamp) { + if (timestamp < 0) { + return brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId); + } else { + return brokerController.getMessageStore().getOffsetInQueueByTime(topic, queueId, timestamp); + } + } + + /** + * Reset consumer offset. + * + * @param topic Required, not null. + * @param group Required, not null. + * @param queueId if target queue ID is negative, all message queues will be reset; otherwise, only the target queue + * would get reset. + * @param timestamp if timestamp is negative, offset would be reset to broker offset at the time being; otherwise, + * binary search is performed to locate target offset. + * @param offset Target offset to reset to if target queue ID is properly provided. + * @return Affected queues and their new offset + */ + private RemotingCommand resetOffsetInner(String topic, String group, int queueId, long timestamp, Long offset) { + RemotingCommand response = RemotingCommand.createResponseCommand(ResponseCode.SUCCESS, null); + + if (BrokerRole.SLAVE == brokerController.getMessageStoreConfig().getBrokerRole()) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("Can not reset offset in slave broker"); + return response; + } + + Map queueOffsetMap = new HashMap<>(); + + // Reset offset for all queues belonging to the specified topic + TopicConfig topicConfig = brokerController.getTopicConfigManager().getTopicConfigTable().get(topic); + if (null == topicConfig) { + response.setCode(ResponseCode.TOPIC_NOT_EXIST); + response.setRemark("Topic " + topic + " does not exist"); + LOGGER.warn("Reset offset failed, topic does not exist. topic={}, group={}", topic, group); + return response; + } + + if (!brokerController.getSubscriptionGroupManager().containsSubscriptionGroup(group)) { + response.setCode(ResponseCode.SUBSCRIPTION_GROUP_NOT_EXIST); + response.setRemark("Group " + group + " does not exist"); + LOGGER.warn("Reset offset failed, group does not exist. topic={}, group={}", topic, group); + return response; + } + + if (queueId >= 0) { + if (null != offset && -1 != offset) { + long min = brokerController.getMessageStore().getMinOffsetInQueue(topic, queueId); + long max = brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId); + if (min >= 0 && offset < min || offset > max + 1) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark( + String.format("Target offset %d not in consume queue range [%d-%d]", offset, min, max)); + return response; + } + } else { + offset = searchOffsetByTimestamp(topic, queueId, timestamp); + } + queueOffsetMap.put(queueId, offset); + } else { + for (int index = 0; index < topicConfig.getReadQueueNums(); index++) { + offset = searchOffsetByTimestamp(topic, index, timestamp); + queueOffsetMap.put(index, offset); + } + } + + if (queueOffsetMap.isEmpty()) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("No queues to reset."); + LOGGER.warn("Reset offset aborted: no queues to reset"); + return response; + } + + for (Map.Entry entry : queueOffsetMap.entrySet()) { + brokerController.getConsumerOffsetManager() + .assignResetOffset(topic, group, entry.getKey(), entry.getValue()); + } + + // Prepare reset result. + ResetOffsetBody body = new ResetOffsetBody(); + String brokerName = brokerController.getBrokerConfig().getBrokerName(); + for (Map.Entry entry : queueOffsetMap.entrySet()) { + brokerController.getPopInflightMessageCounter().clearInFlightMessageNum(topic, group, entry.getKey()); + body.getOffsetTable().put(new MessageQueue(topic, brokerName, entry.getKey()), entry.getValue()); + } + + LOGGER.info("Reset offset, topic={}, group={}, queues={}", topic, group, body.toJson(false)); + response.setBody(body.encode()); + return response; + } + public RemotingCommand getConsumerStatus(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException { final GetConsumerStatusRequestHeader requestHeader = (GetConsumerStatusRequestHeader) request.decodeCommandCustomHeader(GetConsumerStatusRequestHeader.class); - log.info("[get-consumer-status] get consumer status by {}. topic={}, group={}", + LOGGER.info("[get-consumer-status] get consumer status by {}. topic={}, group={}", RemotingHelper.parseChannelRemoteAddr(ctx.channel()), requestHeader.getTopic(), requestHeader.getGroup()); return this.brokerController.getBroker2Client().getConsumeStatus(requestHeader.getTopic(), requestHeader.getGroup(), @@ -836,21 +1968,45 @@ private RemotingCommand queryTopicConsumeByWho(ChannelHandlerContext ctx, return response; } - private RemotingCommand registerFilterServer(ChannelHandlerContext ctx, + private RemotingCommand queryTopicsByConsumer(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + QueryTopicsByConsumerRequestHeader requestHeader = + (QueryTopicsByConsumerRequestHeader) request.decodeCommandCustomHeader(QueryTopicsByConsumerRequestHeader.class); + + Set topics = this.brokerController.getConsumerOffsetManager().whichTopicByConsumer(requestHeader.getGroup()); + + TopicList topicList = new TopicList(); + topicList.setTopicList(topics); + topicList.setBrokerAddr(brokerController.getBrokerAddr()); + byte[] body = topicList.encode(); + + response.setBody(body); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + private RemotingCommand querySubscriptionByConsumer(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException { - final RemotingCommand response = RemotingCommand.createResponseCommand(RegisterFilterServerResponseHeader.class); - final RegisterFilterServerResponseHeader responseHeader = (RegisterFilterServerResponseHeader) response.readCustomHeader(); - final RegisterFilterServerRequestHeader requestHeader = - (RegisterFilterServerRequestHeader) request.decodeCommandCustomHeader(RegisterFilterServerRequestHeader.class); + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + QuerySubscriptionByConsumerRequestHeader requestHeader = + (QuerySubscriptionByConsumerRequestHeader) request.decodeCommandCustomHeader(QuerySubscriptionByConsumerRequestHeader.class); - this.brokerController.getFilterServerManager().registerFilterServer(ctx.channel(), requestHeader.getFilterServerAddr()); + SubscriptionData subscriptionData = this.brokerController.getConsumerManager() + .findSubscriptionData(requestHeader.getGroup(), requestHeader.getTopic()); - responseHeader.setBrokerId(this.brokerController.getBrokerConfig().getBrokerId()); - responseHeader.setBrokerName(this.brokerController.getBrokerConfig().getBrokerName()); + QuerySubscriptionResponseBody responseBody = new QuerySubscriptionResponseBody(); + responseBody.setGroup(requestHeader.getGroup()); + responseBody.setTopic(requestHeader.getTopic()); + responseBody.setSubscriptionData(subscriptionData); + byte[] body = responseBody.encode(); + response.setBody(body); response.setCode(ResponseCode.SUCCESS); response.setRemark(null); return response; + } private RemotingCommand queryConsumeTimeSpan(ChannelHandlerContext ctx, @@ -867,7 +2023,7 @@ private RemotingCommand queryConsumeTimeSpan(ChannelHandlerContext ctx, return response; } - List timeSpanSet = new ArrayList(); + List timeSpanSet = new ArrayList<>(); for (int i = 0; i < topicConfig.getWriteQueueNums(); i++) { QueueTimeSpan timeSpan = new QueueTimeSpan(); MessageQueue mq = new MessageQueue(); @@ -913,7 +2069,7 @@ private RemotingCommand getSystemTopicListFromBroker(ChannelHandlerContext ctx, throws RemotingCommandException { final RemotingCommand response = RemotingCommand.createResponseCommand(null); - Set topics = this.brokerController.getTopicConfigManager().getSystemTopic(); + Set topics = TopicValidator.getSystemTopicSet(); TopicList topicList = new TopicList(); topicList.setTopicList(topics); response.setBody(topicList.encode()); @@ -923,20 +2079,34 @@ private RemotingCommand getSystemTopicListFromBroker(ChannelHandlerContext ctx, } public RemotingCommand cleanExpiredConsumeQueue() { - log.warn("invoke cleanExpiredConsumeQueue start."); + LOGGER.info("AdminBrokerProcessor#cleanExpiredConsumeQueue: start."); + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + try { + brokerController.getMessageStore().cleanExpiredConsumerQueue(); + } catch (Throwable t) { + return buildErrorResponse(ResponseCode.SYSTEM_ERROR, t.getMessage()); + } + LOGGER.info("AdminBrokerProcessor#cleanExpiredConsumeQueue: end."); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + public RemotingCommand deleteExpiredCommitLog() { + LOGGER.warn("invoke deleteExpiredCommitLog start."); final RemotingCommand response = RemotingCommand.createResponseCommand(null); - brokerController.getMessageStore().cleanExpiredConsumerQueue(); - log.warn("invoke cleanExpiredConsumeQueue end."); + brokerController.getMessageStore().executeDeleteFilesManually(); + LOGGER.warn("invoke deleteExpiredCommitLog end."); response.setCode(ResponseCode.SUCCESS); response.setRemark(null); return response; } public RemotingCommand cleanUnusedTopic() { - log.warn("invoke cleanUnusedTopic start."); + LOGGER.warn("invoke cleanUnusedTopic start."); final RemotingCommand response = RemotingCommand.createResponseCommand(null); brokerController.getMessageStore().cleanUnusedTopic(brokerController.getTopicConfigManager().getTopicConfigTable().keySet()); - log.warn("invoke cleanUnusedTopic end."); + LOGGER.warn("invoke cleanUnusedTopic end."); response.setCode(ResponseCode.SUCCESS); response.setRemark(null); return response; @@ -961,7 +2131,7 @@ private RemotingCommand queryCorrectionOffset(ChannelHandlerContext ctx, .queryMinOffsetInAllGroup(requestHeader.getTopic(), requestHeader.getFilterGroups()); Map compareOffset = - this.brokerController.getConsumerOffsetManager().queryOffset(requestHeader.getTopic(), requestHeader.getCompareGroup()); + this.brokerController.getConsumerOffsetManager().queryOffset(requestHeader.getCompareGroup(), requestHeader.getTopic()); if (compareOffset != null && !compareOffset.isEmpty()) { for (Map.Entry entry : compareOffset.entrySet()) { @@ -984,7 +2154,22 @@ private RemotingCommand consumeMessageDirectly(ChannelHandlerContext ctx, final ConsumeMessageDirectlyResultRequestHeader requestHeader = (ConsumeMessageDirectlyResultRequestHeader) request .decodeCommandCustomHeader(ConsumeMessageDirectlyResultRequestHeader.class); + // brokerName request.getExtFields().put("brokerName", this.brokerController.getBrokerConfig().getBrokerName()); + // topicSysFlag + if (StringUtils.isNotEmpty(requestHeader.getTopic())) { + TopicConfig topicConfig = this.brokerController.getTopicConfigManager().getTopicConfigTable().get(requestHeader.getTopic()); + if (topicConfig != null) { + request.addExtField("topicSysFlag", String.valueOf(topicConfig.getTopicSysFlag())); + } + } + // groupSysFlag + if (StringUtils.isNotEmpty(requestHeader.getConsumerGroup())) { + SubscriptionGroupConfig groupConfig = brokerController.getSubscriptionGroupManager().getSubscriptionGroupTable().get(requestHeader.getConsumerGroup()); + if (groupConfig != null) { + request.addExtField("groupSysFlag", String.valueOf(groupConfig.getGroupSysFlag())); + } + } SelectMappedBufferResult selectMappedBufferResult = null; try { MessageId messageId = MessageDecoder.decodeMessageId(requestHeader.getMsgId()); @@ -1014,14 +2199,14 @@ private RemotingCommand cloneGroupOffset(ChannelHandlerContext ctx, if (UtilAll.isBlank(requestHeader.getTopic())) { topics = this.brokerController.getConsumerOffsetManager().whichTopicByConsumer(requestHeader.getSrcGroup()); } else { - topics = new HashSet(); + topics = new HashSet<>(); topics.add(requestHeader.getTopic()); } for (String topic : topics) { TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(topic); if (null == topicConfig) { - log.warn("[cloneGroupOffset], topic config not exist, {}", topic); + LOGGER.warn("[cloneGroupOffset], topic config not exist, {}", topic); continue; } @@ -1031,7 +2216,9 @@ private RemotingCommand cloneGroupOffset(ChannelHandlerContext ctx, this.brokerController.getConsumerManager().findSubscriptionData(requestHeader.getSrcGroup(), topic); if (this.brokerController.getConsumerManager().findSubscriptionDataCount(requestHeader.getSrcGroup()) > 0 && findSubscriptionData == null) { - log.warn("[cloneGroupOffset], the consumer group[{}], topic[{}] not exist", requestHeader.getSrcGroup(), topic); + LOGGER.warn( + "AdminBrokerProcessor#cloneGroupOffset: topic does not exist in consumer group's " + + "subscription, topic={}, consumer group={}", topic, requestHeader.getSrcGroup()); continue; } } @@ -1050,7 +2237,7 @@ private RemotingCommand ViewBrokerStatsData(ChannelHandlerContext ctx, final ViewBrokerStatsDataRequestHeader requestHeader = (ViewBrokerStatsDataRequestHeader) request.decodeCommandCustomHeader(ViewBrokerStatsDataRequestHeader.class); final RemotingCommand response = RemotingCommand.createResponseCommand(null); - DefaultMessageStore messageStore = (DefaultMessageStore) this.brokerController.getMessageStore(); + MessageStore messageStore = this.brokerController.getMessageStore(); StatsItem statsItem = messageStore.getBrokerStatsManager().getStatsItem(requestHeader.getStatsName(), requestHeader.getStatsKey()); if (null == statsItem) { @@ -1104,19 +2291,21 @@ private RemotingCommand fetchAllConsumeStatsInBroker(ChannelHandlerContext ctx, brokerController.getSubscriptionGroupManager().getSubscriptionGroupTable(); List>> brokerConsumeStatsList = - new ArrayList>>(); + new ArrayList<>(); long totalDiff = 0L; - + long totalInflightDiff = 0L; for (String group : subscriptionGroups.keySet()) { - Map> subscripTopicConsumeMap = new HashMap>(); + Map> subscripTopicConsumeMap = new HashMap<>(); Set topics = this.brokerController.getConsumerOffsetManager().whichTopicByConsumer(group); - List consumeStatsList = new ArrayList(); + List consumeStatsList = new ArrayList<>(); for (String topic : topics) { ConsumeStats consumeStats = new ConsumeStats(); TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(topic); if (null == topicConfig) { - log.warn("consumeStats, topic config not exist, {}", topic); + LOGGER.warn( + "AdminBrokerProcessor#fetchAllConsumeStatsInBroker: topic config does not exist, topic={}", + topic); continue; } @@ -1129,7 +2318,9 @@ private RemotingCommand fetchAllConsumeStatsInBroker(ChannelHandlerContext ctx, if (null == findSubscriptionData && this.brokerController.getConsumerManager().findSubscriptionDataCount(group) > 0) { - log.warn("consumeStats, the consumer group[{}], topic[{}] not exist", group, topic); + LOGGER.warn( + "AdminBrokerProcessor#fetchAllConsumeStatsInBroker: topic does not exist in consumer " + + "group's subscription, topic={}, consumer group={}", topic, group); continue; } } @@ -1141,8 +2332,9 @@ private RemotingCommand fetchAllConsumeStatsInBroker(ChannelHandlerContext ctx, mq.setQueueId(i); OffsetWrapper offsetWrapper = new OffsetWrapper(); long brokerOffset = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, i); - if (brokerOffset < 0) + if (brokerOffset < 0) { brokerOffset = 0; + } long consumerOffset = this.brokerController.getConsumerOffsetManager().queryOffset( group, topic, @@ -1166,6 +2358,7 @@ private RemotingCommand fetchAllConsumeStatsInBroker(ChannelHandlerContext ctx, consumeTps += consumeStats.getConsumeTps(); consumeStats.setConsumeTps(consumeTps); totalDiff += consumeStats.computeTotalDiff(); + totalInflightDiff += consumeStats.computeInflightTotalDiff(); consumeStatsList.add(consumeStats); } subscripTopicConsumeMap.put(group, consumeStatsList); @@ -1175,6 +2368,7 @@ private RemotingCommand fetchAllConsumeStatsInBroker(ChannelHandlerContext ctx, consumeStats.setBrokerAddr(brokerController.getBrokerAddr()); consumeStats.setConsumeStatsList(brokerConsumeStatsList); consumeStats.setTotalDiff(totalDiff); + consumeStats.setTotalInflightDiff(totalInflightDiff); response.setBody(consumeStats.encode()); response.setCode(ResponseCode.SUCCESS); response.setRemark(null); @@ -1183,6 +2377,15 @@ private RemotingCommand fetchAllConsumeStatsInBroker(ChannelHandlerContext ctx, private HashMap prepareRuntimeInfo() { HashMap runtimeInfo = this.brokerController.getMessageStore().getRuntimeInfo(); + + for (BrokerAttachedPlugin brokerAttachedPlugin : brokerController.getBrokerAttachedPlugins()) { + if (brokerAttachedPlugin != null) { + brokerAttachedPlugin.buildRuntimeInfo(runtimeInfo); + } + } + + this.brokerController.getScheduleMessageService().buildRunningStats(runtimeInfo); + runtimeInfo.put("brokerActive", String.valueOf(this.brokerController.isSpecialServiceRunning())); runtimeInfo.put("brokerVersionDesc", MQVersion.getVersionDesc(MQVersion.CURRENT_VERSION)); runtimeInfo.put("brokerVersion", String.valueOf(MQVersion.CURRENT_VERSION)); @@ -1196,8 +2399,38 @@ private HashMap prepareRuntimeInfo() { runtimeInfo.put("msgGetTotalTodayMorning", String.valueOf(this.brokerController.getBrokerStats().getMsgGetTotalTodayMorning())); runtimeInfo.put("msgGetTotalTodayNow", String.valueOf(this.brokerController.getBrokerStats().getMsgGetTotalTodayNow())); - runtimeInfo.put("sendThreadPoolQueueSize", String.valueOf(this.brokerController.getSendThreadPoolQueue().size())); + runtimeInfo.put("dispatchBehindBytes", String.valueOf(this.brokerController.getMessageStore().dispatchBehindBytes())); + runtimeInfo.put("pageCacheLockTimeMills", String.valueOf(this.brokerController.getMessageStore().lockTimeMills())); + + runtimeInfo.put("earliestMessageTimeStamp", String.valueOf(this.brokerController.getMessageStore().getEarliestMessageTime())); + runtimeInfo.put("startAcceptSendRequestTimeStamp", String.valueOf(this.brokerController.getBrokerConfig().getStartAcceptSendRequestTimeStamp())); + + if (this.brokerController.getMessageStoreConfig().isTimerWheelEnable()) { + runtimeInfo.put("timerReadBehind", String.valueOf(this.brokerController.getMessageStore().getTimerMessageStore().getDequeueBehind())); + runtimeInfo.put("timerOffsetBehind", String.valueOf(this.brokerController.getMessageStore().getTimerMessageStore().getEnqueueBehindMessages())); + runtimeInfo.put("timerCongestNum", String.valueOf(this.brokerController.getMessageStore().getTimerMessageStore().getAllCongestNum())); + runtimeInfo.put("timerEnqueueTps", String.valueOf(this.brokerController.getMessageStore().getTimerMessageStore().getEnqueueTps())); + runtimeInfo.put("timerDequeueTps", String.valueOf(this.brokerController.getMessageStore().getTimerMessageStore().getDequeueTps())); + } else { + runtimeInfo.put("timerReadBehind", "0"); + runtimeInfo.put("timerOffsetBehind", "0"); + runtimeInfo.put("timerCongestNum", "0"); + runtimeInfo.put("timerEnqueueTps", "0.0"); + runtimeInfo.put("timerDequeueTps", "0.0"); + } + MessageStore messageStore = this.brokerController.getMessageStore(); + runtimeInfo.put("remainTransientStoreBufferNumbs", String.valueOf(messageStore.remainTransientStoreBufferNumbs())); + if (this.brokerController.getMessageStore() instanceof DefaultMessageStore && ((DefaultMessageStore) this.brokerController.getMessageStore()).isTransientStorePoolEnable()) { + runtimeInfo.put("remainHowManyDataToCommit", MixAll.humanReadableByteCount(messageStore.remainHowManyDataToCommit(), false)); + } + runtimeInfo.put("remainHowManyDataToFlush", MixAll.humanReadableByteCount(messageStore.remainHowManyDataToFlush(), false)); + + java.io.File commitLogDir = new java.io.File(this.brokerController.getMessageStoreConfig().getStorePathRootDir()); + if (commitLogDir.exists()) { + runtimeInfo.put("commitLogDirCapacity", String.format("Total : %s, Free : %s.", MixAll.humanReadableByteCount(commitLogDir.getTotalSpace(), false), MixAll.humanReadableByteCount(commitLogDir.getFreeSpace(), false))); + } + runtimeInfo.put("sendThreadPoolQueueSize", String.valueOf(this.brokerController.getSendThreadPoolQueue().size())); runtimeInfo.put("sendThreadPoolQueueCapacity", String.valueOf(this.brokerController.getBrokerConfig().getSendThreadPoolQueueCapacity())); @@ -1205,32 +2438,22 @@ private HashMap prepareRuntimeInfo() { runtimeInfo.put("pullThreadPoolQueueCapacity", String.valueOf(this.brokerController.getBrokerConfig().getPullThreadPoolQueueCapacity())); + runtimeInfo.put("litePullThreadPoolQueueSize", String.valueOf(brokerController.getLitePullThreadPoolQueue().size())); + runtimeInfo.put("litePullThreadPoolQueueCapacity", + String.valueOf(this.brokerController.getBrokerConfig().getLitePullThreadPoolQueueCapacity())); + runtimeInfo.put("queryThreadPoolQueueSize", String.valueOf(this.brokerController.getQueryThreadPoolQueue().size())); runtimeInfo.put("queryThreadPoolQueueCapacity", String.valueOf(this.brokerController.getBrokerConfig().getQueryThreadPoolQueueCapacity())); - runtimeInfo.put("dispatchBehindBytes", String.valueOf(this.brokerController.getMessageStore().dispatchBehindBytes())); - runtimeInfo.put("pageCacheLockTimeMills", String.valueOf(this.brokerController.getMessageStore().lockTimeMills())); - runtimeInfo.put("sendThreadPoolQueueHeadWaitTimeMills", String.valueOf(this.brokerController.headSlowTimeMills4SendThreadPoolQueue())); - runtimeInfo.put("pullThreadPoolQueueHeadWaitTimeMills", String.valueOf(this.brokerController.headSlowTimeMills4PullThreadPoolQueue())); + runtimeInfo.put("pullThreadPoolQueueHeadWaitTimeMills", String.valueOf(brokerController.headSlowTimeMills4PullThreadPoolQueue())); runtimeInfo.put("queryThreadPoolQueueHeadWaitTimeMills", String.valueOf(this.brokerController.headSlowTimeMills4QueryThreadPoolQueue())); + runtimeInfo.put("litePullThreadPoolQueueHeadWaitTimeMills", String.valueOf(brokerController.headSlowTimeMills4LitePullThreadPoolQueue())); - runtimeInfo.put("earliestMessageTimeStamp", String.valueOf(this.brokerController.getMessageStore().getEarliestMessageTime())); - runtimeInfo.put("startAcceptSendRequestTimeStamp", String.valueOf(this.brokerController.getBrokerConfig().getStartAcceptSendRequestTimeStamp())); - if (this.brokerController.getMessageStore() instanceof DefaultMessageStore) { - DefaultMessageStore defaultMessageStore = (DefaultMessageStore) this.brokerController.getMessageStore(); - runtimeInfo.put("remainTransientStoreBufferNumbs", String.valueOf(defaultMessageStore.remainTransientStoreBufferNumbs())); - if (defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) { - runtimeInfo.put("remainHowManyDataToCommit", MixAll.humanReadableByteCount(defaultMessageStore.getCommitLog().remainHowManyDataToCommit(), false)); - } - runtimeInfo.put("remainHowManyDataToFlush", MixAll.humanReadableByteCount(defaultMessageStore.getCommitLog().remainHowManyDataToFlush(), false)); - } - - java.io.File commitLogDir = new java.io.File(this.brokerController.getMessageStoreConfig().getStorePathRootDir()); - if (commitLogDir.exists()) { - runtimeInfo.put("commitLogDirCapacity", String.format("Total : %s, Free : %s.", MixAll.humanReadableByteCount(commitLogDir.getTotalSpace(), false), MixAll.humanReadableByteCount(commitLogDir.getFreeSpace(), false))); - } + runtimeInfo.put("EndTransactionQueueSize", String.valueOf(this.brokerController.getEndTransactionThreadPoolQueue().size())); + runtimeInfo.put("EndTransactionThreadPoolQueueCapacity", + String.valueOf(this.brokerController.getBrokerConfig().getEndTransactionPoolQueueCapacity())); return runtimeInfo; } @@ -1266,12 +2489,12 @@ private RemotingCommand callConsumer( } catch (RemotingTimeoutException e) { response.setCode(ResponseCode.CONSUME_MSG_TIMEOUT); response - .setRemark(String.format("consumer <%s> <%s> Timeout: %s", consumerGroup, clientId, RemotingHelper.exceptionSimpleDesc(e))); + .setRemark(String.format("consumer <%s> <%s> Timeout: %s", consumerGroup, clientId, UtilAll.exceptionSimpleDesc(e))); return response; } catch (Exception e) { response.setCode(ResponseCode.SYSTEM_ERROR); response.setRemark( - String.format("invoke consumer <%s> <%s> Exception: %s", consumerGroup, clientId, RemotingHelper.exceptionSimpleDesc(e))); + String.format("invoke consumer <%s> <%s> Exception: %s", consumerGroup, clientId, UtilAll.exceptionSimpleDesc(e))); return response; } } @@ -1283,18 +2506,16 @@ private RemotingCommand queryConsumeQueue(ChannelHandlerContext ctx, RemotingCommand response = RemotingCommand.createResponseCommand(null); - ConsumeQueue consumeQueue = this.brokerController.getMessageStore().getConsumeQueue(requestHeader.getTopic(), + ConsumeQueueInterface consumeQueue = this.brokerController.getMessageStore().getConsumeQueue(requestHeader.getTopic(), requestHeader.getQueueId()); if (consumeQueue == null) { response.setCode(ResponseCode.SYSTEM_ERROR); response.setRemark(String.format("%d@%s is not exist!", requestHeader.getQueueId(), requestHeader.getTopic())); return response; } - - QueryConsumeQueueResponseBody body = new QueryConsumeQueueResponseBody(); response.setCode(ResponseCode.SUCCESS); - response.setBody(body.encode()); + QueryConsumeQueueResponseBody body = new QueryConsumeQueueResponseBody(); body.setMaxQueueIndex(consumeQueue.getMaxOffsetInQueue()); body.setMinQueueIndex(consumeQueue.getMinOffsetInQueue()); @@ -1316,26 +2537,31 @@ private RemotingCommand queryConsumeQueue(ChannelHandlerContext ctx, } } - SelectMappedBufferResult result = consumeQueue.getIndexBuffer(requestHeader.getIndex()); + ReferredIterator result = consumeQueue.iterateFrom(requestHeader.getIndex()); if (result == null) { response.setRemark(String.format("Index %d of %d@%s is not exist!", requestHeader.getIndex(), requestHeader.getQueueId(), requestHeader.getTopic())); return response; } try { List queues = new ArrayList<>(); - for (int i = 0; i < result.getSize() && i < requestHeader.getCount() * ConsumeQueue.CQ_STORE_UNIT_SIZE; i += ConsumeQueue.CQ_STORE_UNIT_SIZE) { + while (result.hasNext()) { + CqUnit cqUnit = result.next(); + if (cqUnit.getQueueOffset() - requestHeader.getIndex() >= requestHeader.getCount()) { + break; + } + ConsumeQueueData one = new ConsumeQueueData(); - one.setPhysicOffset(result.getByteBuffer().getLong()); - one.setPhysicSize(result.getByteBuffer().getInt()); - one.setTagsCode(result.getByteBuffer().getLong()); + one.setPhysicOffset(cqUnit.getPos()); + one.setPhysicSize(cqUnit.getSize()); + one.setTagsCode(cqUnit.getTagsCode()); - if (!consumeQueue.isExtAddr(one.getTagsCode())) { + if (cqUnit.getCqExtUnit() == null && cqUnit.isTagsCodeValid()) { queues.add(one); continue; } - ConsumeQueueExt.CqExtUnit cqExtUnit = consumeQueue.getExt(one.getTagsCode()); - if (cqExtUnit != null) { + if (cqUnit.getCqExtUnit() != null) { + ConsumeQueueExt.CqExtUnit cqExtUnit = cqUnit.getCqExtUnit(); one.setExtendDataJson(JSON.toJSONString(cqExtUnit)); if (cqExtUnit.getFilterBitMap() != null) { one.setBitMap(BitsArray.create(cqExtUnit.getFilterBitMap()).toString()); @@ -1353,7 +2579,240 @@ private RemotingCommand queryConsumeQueue(ChannelHandlerContext ctx, } finally { result.release(); } + response.setBody(body.encode()); + return response; + } + + private RemotingCommand resumeCheckHalfMessage(ChannelHandlerContext ctx, + RemotingCommand request) + throws RemotingCommandException { + final ResumeCheckHalfMessageRequestHeader requestHeader = (ResumeCheckHalfMessageRequestHeader) request + .decodeCommandCustomHeader(ResumeCheckHalfMessageRequestHeader.class); + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + SelectMappedBufferResult selectMappedBufferResult = null; + try { + MessageId messageId = MessageDecoder.decodeMessageId(requestHeader.getMsgId()); + selectMappedBufferResult = this.brokerController.getMessageStore() + .selectOneMessageByOffset(messageId.getOffset()); + MessageExt msg = MessageDecoder.decode(selectMappedBufferResult.getByteBuffer()); + msg.putUserProperty(MessageConst.PROPERTY_TRANSACTION_CHECK_TIMES, String.valueOf(0)); + PutMessageResult putMessageResult = this.brokerController.getMessageStore() + .putMessage(toMessageExtBrokerInner(msg)); + if (putMessageResult != null + && putMessageResult.getPutMessageStatus() == PutMessageStatus.PUT_OK) { + LOGGER.info( + "Put message back to RMQ_SYS_TRANS_HALF_TOPIC. real topic={}", + msg.getUserProperty(MessageConst.PROPERTY_REAL_TOPIC)); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + } else { + LOGGER.error("Put message back to RMQ_SYS_TRANS_HALF_TOPIC failed."); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("Put message back to RMQ_SYS_TRANS_HALF_TOPIC failed."); + } + } catch (Exception e) { + LOGGER.error("Exception was thrown when putting message back to RMQ_SYS_TRANS_HALF_TOPIC."); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("Exception was thrown when putting message back to RMQ_SYS_TRANS_HALF_TOPIC."); + } finally { + if (selectMappedBufferResult != null) { + selectMappedBufferResult.release(); + } + } + return response; + } + + private MessageExtBrokerInner toMessageExtBrokerInner(MessageExt msgExt) { + MessageExtBrokerInner inner = new MessageExtBrokerInner(); + inner.setTopic(TransactionalMessageUtil.buildHalfTopic()); + inner.setBody(msgExt.getBody()); + inner.setFlag(msgExt.getFlag()); + MessageAccessor.setProperties(inner, msgExt.getProperties()); + inner.setPropertiesString(MessageDecoder.messageProperties2String(msgExt.getProperties())); + inner.setTagsCode(MessageExtBrokerInner.tagsString2tagsCode(msgExt.getTags())); + inner.setQueueId(0); + inner.setSysFlag(msgExt.getSysFlag()); + inner.setBornHost(msgExt.getBornHost()); + inner.setBornTimestamp(msgExt.getBornTimestamp()); + inner.setStoreHost(msgExt.getStoreHost()); + inner.setReconsumeTimes(msgExt.getReconsumeTimes()); + inner.setMsgId(msgExt.getMsgId()); + inner.setWaitStoreMsgOK(false); + return inner; + } + + private RemotingCommand getTopicConfig(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + GetTopicConfigRequestHeader requestHeader = (GetTopicConfigRequestHeader) request.decodeCommandCustomHeader(GetTopicConfigRequestHeader.class); + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + TopicConfig topicConfig = this.brokerController.getTopicConfigManager().getTopicConfigTable().get(requestHeader.getTopic()); + if (topicConfig == null) { + LOGGER.error("No topic in this broker, client: {} topic: {}", ctx.channel().remoteAddress(), requestHeader.getTopic()); + //be care of the response code, should set "not-exist" explicitly + response.setCode(ResponseCode.TOPIC_NOT_EXIST); + response.setRemark("No topic in this broker. topic: " + requestHeader.getTopic()); + return response; + } + TopicQueueMappingDetail topicQueueMappingDetail = null; + if (Boolean.TRUE.equals(requestHeader.getLo())) { + topicQueueMappingDetail = this.brokerController.getTopicQueueMappingManager().getTopicQueueMapping(requestHeader.getTopic()); + } + String content = JSONObject.toJSONString(new TopicConfigAndQueueMapping(topicConfig, topicQueueMappingDetail)); + try { + response.setBody(content.getBytes(MixAll.DEFAULT_CHARSET)); + } catch (UnsupportedEncodingException e) { + LOGGER.error("UnsupportedEncodingException getTopicConfig: topic=" + topicConfig.getTopicName(), e); + + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("UnsupportedEncodingException " + e.getMessage()); + return response; + } + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + + return response; + } + + private RemotingCommand notifyMinBrokerIdChange(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + NotifyMinBrokerIdChangeRequestHeader requestHeader = (NotifyMinBrokerIdChangeRequestHeader) request.decodeCommandCustomHeader(NotifyMinBrokerIdChangeRequestHeader.class); + + RemotingCommand response = RemotingCommand.createResponseCommand(null); + + LOGGER.warn("min broker id changed, prev {}, new {}", this.brokerController.getMinBrokerIdInGroup(), requestHeader.getMinBrokerId()); + + this.brokerController.updateMinBroker(requestHeader.getMinBrokerId(), requestHeader.getMinBrokerAddr(), + requestHeader.getOfflineBrokerAddr(), + requestHeader.getHaBrokerAddr()); + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + + return response; + } + + private RemotingCommand updateBrokerHaInfo(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + RemotingCommand response = RemotingCommand.createResponseCommand(ExchangeHAInfoResponseHeader.class); + + ExchangeHAInfoRequestHeader requestHeader = (ExchangeHAInfoRequestHeader) request.decodeCommandCustomHeader(ExchangeHAInfoRequestHeader.class); + if (requestHeader.getMasterHaAddress() != null) { + this.brokerController.getMessageStore().updateHaMasterAddress(requestHeader.getMasterHaAddress()); + this.brokerController.getMessageStore().updateMasterAddress(requestHeader.getMasterAddress()); + if (this.brokerController.getMessageStore().getMasterFlushedOffset() == 0 + && this.brokerController.getMessageStoreConfig().isSyncMasterFlushOffsetWhenStartup()) { + LOGGER.info("Set master flush offset in slave to {}", requestHeader.getMasterFlushOffset()); + this.brokerController.getMessageStore().setMasterFlushedOffset(requestHeader.getMasterFlushOffset()); + } + } else if (this.brokerController.getBrokerConfig().getBrokerId() == MixAll.MASTER_ID) { + final ExchangeHAInfoResponseHeader responseHeader = (ExchangeHAInfoResponseHeader) response.readCustomHeader(); + responseHeader.setMasterHaAddress(this.brokerController.getHAServerAddr()); + responseHeader.setMasterFlushOffset(this.brokerController.getMessageStore().getBrokerInitMaxOffset()); + responseHeader.setMasterAddress(this.brokerController.getBrokerAddr()); + } + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + + return response; + } + + private RemotingCommand getBrokerHaStatus(ChannelHandlerContext ctx, RemotingCommand request) { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + HARuntimeInfo runtimeInfo = this.brokerController.getMessageStore().getHARuntimeInfo(); + + if (runtimeInfo != null) { + byte[] body = runtimeInfo.encode(); + response.setBody(body); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + } else { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("Can not get HARuntimeInfo, may be duplicationEnable is true"); + } + + return response; + } + + private RemotingCommand getBrokerEpochCache(ChannelHandlerContext ctx, RemotingCommand request) { + final ReplicasManager replicasManager = this.brokerController.getReplicasManager(); + assert replicasManager != null; + final BrokerConfig brokerConfig = this.brokerController.getBrokerConfig(); + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + if (!brokerConfig.isEnableControllerMode()) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("this request only for controllerMode "); + return response; + } + final EpochEntryCache entryCache = new EpochEntryCache(brokerConfig.getBrokerClusterName(), + brokerConfig.getBrokerName(), brokerConfig.getBrokerId(), replicasManager.getEpochEntries(), this.brokerController.getMessageStore().getMaxPhyOffset()); + + response.setBody(entryCache.encode()); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + private RemotingCommand resetMasterFlushOffset(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + if (this.brokerController.getBrokerConfig().getBrokerId() != MixAll.MASTER_ID) { + + ResetMasterFlushOffsetHeader requestHeader = (ResetMasterFlushOffsetHeader) request.decodeCommandCustomHeader(ResetMasterFlushOffsetHeader.class); + + if (requestHeader.getMasterFlushOffset() != null) { + this.brokerController.getMessageStore().setMasterFlushedOffset(requestHeader.getMasterFlushOffset()); + } + } + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + private RemotingCommand notifyBrokerRoleChanged(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + NotifyBrokerRoleChangedRequestHeader requestHeader = (NotifyBrokerRoleChangedRequestHeader) request.decodeCommandCustomHeader(NotifyBrokerRoleChangedRequestHeader.class); + SyncStateSet syncStateSetInfo = RemotingSerializable.decode(request.getBody(), SyncStateSet.class); + + RemotingCommand response = RemotingCommand.createResponseCommand(null); + + LOGGER.info("Receive notifyBrokerRoleChanged request, try to change brokerRole, request:{}", requestHeader); + + final ReplicasManager replicasManager = this.brokerController.getReplicasManager(); + if (replicasManager != null) { + try { + replicasManager.changeBrokerRole(requestHeader.getMasterBrokerId(), requestHeader.getMasterAddress(), requestHeader.getMasterEpoch(), requestHeader.getSyncStateSetEpoch(), syncStateSetInfo.getSyncStateSet()); + } catch (Exception e) { + throw new RemotingCommandException(e.getMessage()); + } + } + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); return response; } + + private boolean validateSlave(RemotingCommand response) { + if (this.brokerController.getMessageStoreConfig().getBrokerRole().equals(BrokerRole.SLAVE)) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("Can't modify topic or subscription group from slave broker, " + + "please execute it from master broker."); + return true; + } + return false; + } + + private boolean validateBlackListConfigExist(Properties properties) { + for (String blackConfig:configBlackList) { + if (properties.containsKey(blackConfig)) { + return true; + } + } + return false; + } } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java new file mode 100644 index 00000000000..bdfffff096a --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessor.java @@ -0,0 +1,245 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.processor; + +import com.alibaba.fastjson.JSON; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.metrics.PopMetricsManager; +import org.apache.rocketmq.common.PopAckConstants; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.help.FAQUrl; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.utils.DataConverter; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.ChangeInvisibleTimeRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ChangeInvisibleTimeResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.ExtraInfoUtil; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.pop.AckMsg; +import org.apache.rocketmq.store.pop.PopCheckPoint; + +public class ChangeInvisibleTimeProcessor implements NettyRequestProcessor { + private static final Logger POP_LOGGER = LoggerFactory.getLogger(LoggerName.ROCKETMQ_POP_LOGGER_NAME); + private final BrokerController brokerController; + private final String reviveTopic; + + public ChangeInvisibleTimeProcessor(final BrokerController brokerController) { + this.brokerController = brokerController; + this.reviveTopic = PopAckConstants.buildClusterReviveTopic(this.brokerController.getBrokerConfig().getBrokerClusterName()); + } + + @Override + public RemotingCommand processRequest(final ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + return this.processRequest(ctx.channel(), request, true); + } + + @Override + public boolean rejectRequest() { + return false; + } + + private RemotingCommand processRequest(final Channel channel, RemotingCommand request, + boolean brokerAllowSuspend) throws RemotingCommandException { + final ChangeInvisibleTimeRequestHeader requestHeader = (ChangeInvisibleTimeRequestHeader) request.decodeCommandCustomHeader(ChangeInvisibleTimeRequestHeader.class); + RemotingCommand response = RemotingCommand.createResponseCommand(ChangeInvisibleTimeResponseHeader.class); + response.setCode(ResponseCode.SUCCESS); + response.setOpaque(request.getOpaque()); + final ChangeInvisibleTimeResponseHeader responseHeader = (ChangeInvisibleTimeResponseHeader) response.readCustomHeader(); + TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic()); + if (null == topicConfig) { + POP_LOGGER.error("The topic {} not exist, consumer: {} ", requestHeader.getTopic(), RemotingHelper.parseChannelRemoteAddr(channel)); + response.setCode(ResponseCode.TOPIC_NOT_EXIST); + response.setRemark(String.format("topic[%s] not exist, apply first please! %s", requestHeader.getTopic(), FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL))); + return response; + } + + if (requestHeader.getQueueId() >= topicConfig.getReadQueueNums() || requestHeader.getQueueId() < 0) { + String errorInfo = String.format("queueId[%d] is illegal, topic:[%s] topicConfig.readQueueNums:[%d] consumer:[%s]", + requestHeader.getQueueId(), requestHeader.getTopic(), topicConfig.getReadQueueNums(), channel.remoteAddress()); + POP_LOGGER.warn(errorInfo); + response.setCode(ResponseCode.MESSAGE_ILLEGAL); + response.setRemark(errorInfo); + return response; + } + long minOffset = this.brokerController.getMessageStore().getMinOffsetInQueue(requestHeader.getTopic(), requestHeader.getQueueId()); + long maxOffset = this.brokerController.getMessageStore().getMaxOffsetInQueue(requestHeader.getTopic(), requestHeader.getQueueId()); + if (requestHeader.getOffset() < minOffset || requestHeader.getOffset() > maxOffset) { + response.setCode(ResponseCode.NO_MESSAGE); + return response; + } + + String[] extraInfo = ExtraInfoUtil.split(requestHeader.getExtraInfo()); + + if (ExtraInfoUtil.isOrder(extraInfo)) { + return processChangeInvisibleTimeForOrder(requestHeader, extraInfo, response, responseHeader); + } + + // add new ck + long now = System.currentTimeMillis(); + PutMessageResult ckResult = appendCheckPoint(requestHeader, ExtraInfoUtil.getReviveQid(extraInfo), requestHeader.getQueueId(), requestHeader.getOffset(), now, ExtraInfoUtil.getBrokerName(extraInfo)); + + if (ckResult.getPutMessageStatus() != PutMessageStatus.PUT_OK + && ckResult.getPutMessageStatus() != PutMessageStatus.FLUSH_DISK_TIMEOUT + && ckResult.getPutMessageStatus() != PutMessageStatus.FLUSH_SLAVE_TIMEOUT + && ckResult.getPutMessageStatus() != PutMessageStatus.SLAVE_NOT_AVAILABLE) { + POP_LOGGER.error("change Invisible, put new ck error: {}", ckResult); + response.setCode(ResponseCode.SYSTEM_ERROR); + return response; + } + + // ack old msg. + try { + ackOrigin(requestHeader, extraInfo); + } catch (Throwable e) { + POP_LOGGER.error("change Invisible, put ack msg error: {}, {}", requestHeader.getExtraInfo(), e.getMessage()); + // cancel new ck? + } + + responseHeader.setInvisibleTime(requestHeader.getInvisibleTime()); + responseHeader.setPopTime(now); + responseHeader.setReviveQid(ExtraInfoUtil.getReviveQid(extraInfo)); + return response; + } + + protected RemotingCommand processChangeInvisibleTimeForOrder(ChangeInvisibleTimeRequestHeader requestHeader, + String[] extraInfo, RemotingCommand response, ChangeInvisibleTimeResponseHeader responseHeader) { + long popTime = ExtraInfoUtil.getPopTime(extraInfo); + long oldOffset = this.brokerController.getConsumerOffsetManager().queryOffset(requestHeader.getConsumerGroup(), + requestHeader.getTopic(), requestHeader.getQueueId()); + if (requestHeader.getOffset() < oldOffset) { + return response; + } + while (!this.brokerController.getPopMessageProcessor().getQueueLockManager().tryLock(requestHeader.getTopic(), requestHeader.getConsumerGroup(), requestHeader.getQueueId())) { + } + try { + oldOffset = this.brokerController.getConsumerOffsetManager().queryOffset(requestHeader.getConsumerGroup(), + requestHeader.getTopic(), requestHeader.getQueueId()); + if (requestHeader.getOffset() < oldOffset) { + return response; + } + + long nextVisibleTime = System.currentTimeMillis() + requestHeader.getInvisibleTime(); + this.brokerController.getConsumerOrderInfoManager().updateNextVisibleTime( + requestHeader.getTopic(), requestHeader.getConsumerGroup(), requestHeader.getQueueId(), requestHeader.getOffset(), popTime, nextVisibleTime); + + responseHeader.setInvisibleTime(nextVisibleTime - popTime); + responseHeader.setPopTime(popTime); + responseHeader.setReviveQid(ExtraInfoUtil.getReviveQid(extraInfo)); + } finally { + this.brokerController.getPopMessageProcessor().getQueueLockManager().unLock(requestHeader.getTopic(), requestHeader.getConsumerGroup(), requestHeader.getQueueId()); + } + return response; + } + + private void ackOrigin(final ChangeInvisibleTimeRequestHeader requestHeader, String[] extraInfo) { + MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); + AckMsg ackMsg = new AckMsg(); + + ackMsg.setAckOffset(requestHeader.getOffset()); + ackMsg.setStartOffset(ExtraInfoUtil.getCkQueueOffset(extraInfo)); + ackMsg.setConsumerGroup(requestHeader.getConsumerGroup()); + ackMsg.setTopic(requestHeader.getTopic()); + ackMsg.setQueueId(requestHeader.getQueueId()); + ackMsg.setPopTime(ExtraInfoUtil.getPopTime(extraInfo)); + ackMsg.setBrokerName(ExtraInfoUtil.getBrokerName(extraInfo)); + + int rqId = ExtraInfoUtil.getReviveQid(extraInfo); + + this.brokerController.getBrokerStatsManager().incBrokerAckNums(1); + this.brokerController.getBrokerStatsManager().incGroupAckNums(requestHeader.getConsumerGroup(), requestHeader.getTopic(), 1); + + if (brokerController.getPopMessageProcessor().getPopBufferMergeService().addAk(rqId, ackMsg)) { + return; + } + + msgInner.setTopic(reviveTopic); + msgInner.setBody(JSON.toJSONString(ackMsg).getBytes(DataConverter.CHARSET_UTF8)); + msgInner.setQueueId(rqId); + msgInner.setTags(PopAckConstants.ACK_TAG); + msgInner.setBornTimestamp(System.currentTimeMillis()); + msgInner.setBornHost(this.brokerController.getStoreHost()); + msgInner.setStoreHost(this.brokerController.getStoreHost()); + msgInner.setDeliverTimeMs(ExtraInfoUtil.getPopTime(extraInfo) + ExtraInfoUtil.getInvisibleTime(extraInfo)); + msgInner.getProperties().put(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, PopMessageProcessor.genAckUniqueId(ackMsg)); + msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); + PutMessageResult putMessageResult = this.brokerController.getEscapeBridge().putMessageToSpecificQueue(msgInner); + if (putMessageResult.getPutMessageStatus() != PutMessageStatus.PUT_OK + && putMessageResult.getPutMessageStatus() != PutMessageStatus.FLUSH_DISK_TIMEOUT + && putMessageResult.getPutMessageStatus() != PutMessageStatus.FLUSH_SLAVE_TIMEOUT + && putMessageResult.getPutMessageStatus() != PutMessageStatus.SLAVE_NOT_AVAILABLE) { + POP_LOGGER.error("change Invisible, put ack msg fail: {}, {}", ackMsg, putMessageResult); + } + PopMetricsManager.incPopReviveAckPutCount(ackMsg, putMessageResult.getPutMessageStatus()); + } + + private PutMessageResult appendCheckPoint(final ChangeInvisibleTimeRequestHeader requestHeader, int reviveQid, + int queueId, long offset, long popTime, String brokerName) { + // add check point msg to revive log + MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); + msgInner.setTopic(reviveTopic); + PopCheckPoint ck = new PopCheckPoint(); + ck.setBitMap(0); + ck.setNum((byte) 1); + ck.setPopTime(popTime); + ck.setInvisibleTime(requestHeader.getInvisibleTime()); + ck.setStartOffset(offset); + ck.setCId(requestHeader.getConsumerGroup()); + ck.setTopic(requestHeader.getTopic()); + ck.setQueueId(queueId); + ck.addDiff(0); + ck.setBrokerName(brokerName); + + msgInner.setBody(JSON.toJSONString(ck).getBytes(DataConverter.CHARSET_UTF8)); + msgInner.setQueueId(reviveQid); + msgInner.setTags(PopAckConstants.CK_TAG); + msgInner.setBornTimestamp(System.currentTimeMillis()); + msgInner.setBornHost(this.brokerController.getStoreHost()); + msgInner.setStoreHost(this.brokerController.getStoreHost()); + msgInner.setDeliverTimeMs(ck.getReviveTime() - PopAckConstants.ackTimeInterval); + msgInner.getProperties().put(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, PopMessageProcessor.genCkUniqueId(ck)); + msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); + PutMessageResult putMessageResult = this.brokerController.getEscapeBridge().putMessageToSpecificQueue(msgInner); + + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("change Invisible , appendCheckPoint, topic {}, queueId {},reviveId {}, cid {}, startOffset {}, rt {}, result {}", requestHeader.getTopic(), queueId, reviveQid, requestHeader.getConsumerGroup(), offset, + ck.getReviveTime(), putMessageResult); + } + + if (putMessageResult != null) { + PopMetricsManager.incPopReviveCkPutCount(ck, putMessageResult.getPutMessageStatus()); + if (putMessageResult.isOk()) { + this.brokerController.getBrokerStatsManager().incBrokerCkNums(1); + this.brokerController.getBrokerStatsManager().incGroupCkNums(requestHeader.getConsumerGroup(), requestHeader.getTopic(), 1); + } + } + + return putMessageResult; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/ClientManageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/ClientManageProcessor.java index 67807a863da..b51967e184f 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/ClientManageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/ClientManageProcessor.java @@ -16,6 +16,8 @@ */ package org.apache.rocketmq.broker.processor; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import io.netty.channel.ChannelHandlerContext; import org.apache.rocketmq.broker.BrokerController; import org.apache.rocketmq.broker.client.ClientChannelInfo; @@ -23,28 +25,30 @@ import org.apache.rocketmq.common.constant.LoggerName; import org.apache.rocketmq.common.constant.PermName; import org.apache.rocketmq.common.filter.ExpressionType; -import org.apache.rocketmq.common.protocol.RequestCode; -import org.apache.rocketmq.common.protocol.ResponseCode; -import org.apache.rocketmq.common.protocol.body.CheckClientRequestBody; -import org.apache.rocketmq.common.protocol.header.UnregisterClientRequestHeader; -import org.apache.rocketmq.common.protocol.header.UnregisterClientResponseHeader; -import org.apache.rocketmq.common.protocol.heartbeat.ConsumerData; -import org.apache.rocketmq.common.protocol.heartbeat.HeartbeatData; -import org.apache.rocketmq.common.protocol.heartbeat.ProducerData; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; -import org.apache.rocketmq.common.subscription.SubscriptionGroupConfig; import org.apache.rocketmq.common.sysflag.TopicSysFlag; import org.apache.rocketmq.filter.FilterFactory; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.remoting.common.RemotingHelper; import org.apache.rocketmq.remoting.exception.RemotingCommandException; import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; import org.apache.rocketmq.remoting.protocol.RemotingCommand; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.CheckClientRequestBody; +import org.apache.rocketmq.remoting.protocol.header.UnregisterClientRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UnregisterClientResponseHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumerData; +import org.apache.rocketmq.remoting.protocol.heartbeat.HeartbeatData; +import org.apache.rocketmq.remoting.protocol.heartbeat.ProducerData; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; public class ClientManageProcessor implements NettyRequestProcessor { - private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); private final BrokerController brokerController; + private final ConcurrentMap consumerGroupHeartbeatTable = new ConcurrentHashMap<>(); public ClientManageProcessor(final BrokerController brokerController) { this.brokerController = brokerController; @@ -80,41 +84,58 @@ public RemotingCommand heartBeat(ChannelHandlerContext ctx, RemotingCommand requ request.getLanguage(), request.getVersion() ); + int heartbeatFingerprint = heartbeatData.getHeartbeatFingerprint(); + if (heartbeatFingerprint != 0) { + return heartBeatV2(ctx, heartbeatData, clientChannelInfo, response); + } + for (ConsumerData consumerData : heartbeatData.getConsumerDataSet()) { + //Reject the PullConsumer + if (brokerController.getBrokerConfig().isRejectPullConsumerEnable()) { + if (ConsumeType.CONSUME_ACTIVELY == consumerData.getConsumeType()) { + continue; + } + } + consumerGroupHeartbeatTable.put(consumerData.getGroupName(), heartbeatFingerprint); + boolean hasOrderTopicSub = false; - for (ConsumerData data : heartbeatData.getConsumerDataSet()) { - SubscriptionGroupConfig subscriptionGroupConfig = - this.brokerController.getSubscriptionGroupManager().findSubscriptionGroupConfig( - data.getGroupName()); - boolean isNotifyConsumerIdsChangedEnable = true; - if (null != subscriptionGroupConfig) { - isNotifyConsumerIdsChangedEnable = subscriptionGroupConfig.isNotifyConsumerIdsChangedEnable(); - int topicSysFlag = 0; - if (data.isUnitMode()) { - topicSysFlag = TopicSysFlag.buildSysFlag(false, true); + for (final SubscriptionData subscriptionData : consumerData.getSubscriptionDataSet()) { + if (this.brokerController.getTopicConfigManager().isOrderTopic(subscriptionData.getTopic())) { + hasOrderTopicSub = true; + break; } - String newTopic = MixAll.getRetryTopic(data.getGroupName()); - this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod( - newTopic, - subscriptionGroupConfig.getRetryQueueNums(), - PermName.PERM_WRITE | PermName.PERM_READ, topicSysFlag); } + SubscriptionGroupConfig subscriptionGroupConfig = this.brokerController.getSubscriptionGroupManager() + .findSubscriptionGroupConfig(consumerData.getGroupName()); + boolean isNotifyConsumerIdsChangedEnable = true; + + if (null == subscriptionGroupConfig) { + continue; + } + + isNotifyConsumerIdsChangedEnable = subscriptionGroupConfig.isNotifyConsumerIdsChangedEnable(); + int topicSysFlag = 0; + if (consumerData.isUnitMode()) { + topicSysFlag = TopicSysFlag.buildSysFlag(false, true); + } + String newTopic = MixAll.getRetryTopic(consumerData.getGroupName()); + this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(newTopic, subscriptionGroupConfig.getRetryQueueNums(), + PermName.PERM_WRITE | PermName.PERM_READ, hasOrderTopicSub, topicSysFlag); + boolean changed = this.brokerController.getConsumerManager().registerConsumer( - data.getGroupName(), + consumerData.getGroupName(), clientChannelInfo, - data.getConsumeType(), - data.getMessageModel(), - data.getConsumeFromWhere(), - data.getSubscriptionDataSet(), + consumerData.getConsumeType(), + consumerData.getMessageModel(), + consumerData.getConsumeFromWhere(), + consumerData.getSubscriptionDataSet(), isNotifyConsumerIdsChangedEnable ); - if (changed) { - log.info("registerConsumer info changed {} {}", - data.toString(), - RemotingHelper.parseChannelRemoteAddr(ctx.channel()) - ); + LOGGER.info("ClientManageProcessor: registerConsumer info changed, SDK address={}, consumerData={}", + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), consumerData.toString()); } + } for (ProducerData data : heartbeatData.getProducerDataSet()) { @@ -123,6 +144,63 @@ public RemotingCommand heartBeat(ChannelHandlerContext ctx, RemotingCommand requ } response.setCode(ResponseCode.SUCCESS); response.setRemark(null); + response.addExtField(MixAll.IS_SUPPORT_HEART_BEAT_V2, Boolean.TRUE.toString()); + response.addExtField(MixAll.IS_SUB_CHANGE, Boolean.TRUE.toString()); + return response; + } + + private RemotingCommand heartBeatV2(ChannelHandlerContext ctx, HeartbeatData heartbeatData, ClientChannelInfo clientChannelInfo, RemotingCommand response) { + boolean isSubChange = false; + for (ConsumerData consumerData : heartbeatData.getConsumerDataSet()) { + //Reject the PullConsumer + if (brokerController.getBrokerConfig().isRejectPullConsumerEnable()) { + if (ConsumeType.CONSUME_ACTIVELY == consumerData.getConsumeType()) { + continue; + } + } + if (null != consumerGroupHeartbeatTable.get(consumerData.getGroupName()) && consumerGroupHeartbeatTable.get(consumerData.getGroupName()) != heartbeatData.getHeartbeatFingerprint()) { + isSubChange = true; + } + consumerGroupHeartbeatTable.put(consumerData.getGroupName(), heartbeatData.getHeartbeatFingerprint()); + boolean hasOrderTopicSub = false; + + for (final SubscriptionData subscriptionData : consumerData.getSubscriptionDataSet()) { + if (this.brokerController.getTopicConfigManager().isOrderTopic(subscriptionData.getTopic())) { + hasOrderTopicSub = true; + break; + } + } + SubscriptionGroupConfig subscriptionGroupConfig = this.brokerController.getSubscriptionGroupManager().findSubscriptionGroupConfig(consumerData.getGroupName()); + boolean isNotifyConsumerIdsChangedEnable = true; + if (null == subscriptionGroupConfig) { + continue; + } + isNotifyConsumerIdsChangedEnable = subscriptionGroupConfig.isNotifyConsumerIdsChangedEnable(); + int topicSysFlag = 0; + if (consumerData.isUnitMode()) { + topicSysFlag = TopicSysFlag.buildSysFlag(false, true); + } + String newTopic = MixAll.getRetryTopic(consumerData.getGroupName()); + this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(newTopic, subscriptionGroupConfig.getRetryQueueNums(), PermName.PERM_WRITE | PermName.PERM_READ, hasOrderTopicSub, topicSysFlag); + boolean changed = false; + if (heartbeatData.isWithoutSub()) { + changed = this.brokerController.getConsumerManager().registerConsumerWithoutSub(consumerData.getGroupName(), clientChannelInfo, consumerData.getConsumeType(), consumerData.getMessageModel(), consumerData.getConsumeFromWhere(), isNotifyConsumerIdsChangedEnable); + } else { + changed = this.brokerController.getConsumerManager().registerConsumer(consumerData.getGroupName(), clientChannelInfo, consumerData.getConsumeType(), consumerData.getMessageModel(), consumerData.getConsumeFromWhere(), consumerData.getSubscriptionDataSet(), isNotifyConsumerIdsChangedEnable); + } + if (changed) { + LOGGER.info("heartBeatV2 ClientManageProcessor: registerConsumer info changed, SDK address={}, consumerData={}", + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), consumerData.toString()); + } + + } + for (ProducerData data : heartbeatData.getProducerDataSet()) { + this.brokerController.getProducerManager().registerProducer(data.getGroupName(), clientChannelInfo); + } + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + response.addExtField(MixAll.IS_SUPPORT_HEART_BEAT_V2, Boolean.TRUE.toString()); + response.addExtField(MixAll.IS_SUB_CHANGE, Boolean.valueOf(isSubChange).toString()); return response; } @@ -189,7 +267,7 @@ public RemotingCommand checkClientConfig(ChannelHandlerContext ctx, RemotingComm try { FilterFactory.INSTANCE.get(subscriptionData.getExpressionType()).compile(subscriptionData.getSubString()); } catch (Exception e) { - log.warn("Client {}@{} filter message, but failed to compile expression! sub={}, error={}", + LOGGER.warn("Client {}@{} filter message, but failed to compile expression! sub={}, error={}", requestBody.getClientId(), requestBody.getGroup(), requestBody.getSubscriptionData(), e.getMessage()); response.setCode(ResponseCode.SUBSCRIPTION_PARSE_FAILED); response.setRemark(e.getMessage()); diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/ConsumerManageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/ConsumerManageProcessor.java index bb427050d9c..e16a1e9090f 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/ConsumerManageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/ConsumerManageProcessor.java @@ -20,26 +20,35 @@ import java.util.List; import org.apache.rocketmq.broker.BrokerController; import org.apache.rocketmq.broker.client.ConsumerGroupInfo; +import org.apache.rocketmq.broker.offset.ConsumerOffsetManager; import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.common.protocol.RequestCode; -import org.apache.rocketmq.common.protocol.ResponseCode; -import org.apache.rocketmq.common.protocol.header.GetConsumerListByGroupRequestHeader; -import org.apache.rocketmq.common.protocol.header.GetConsumerListByGroupResponseBody; -import org.apache.rocketmq.common.protocol.header.GetConsumerListByGroupResponseHeader; -import org.apache.rocketmq.common.protocol.header.QueryConsumerOffsetRequestHeader; -import org.apache.rocketmq.common.protocol.header.QueryConsumerOffsetResponseHeader; -import org.apache.rocketmq.common.protocol.header.UpdateConsumerOffsetRequestHeader; -import org.apache.rocketmq.common.protocol.header.UpdateConsumerOffsetResponseHeader; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.remoting.common.RemotingHelper; import org.apache.rocketmq.remoting.exception.RemotingCommandException; import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; import org.apache.rocketmq.remoting.protocol.RemotingCommand; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerListByGroupRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerListByGroupResponseBody; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerListByGroupResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumerOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumerOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateConsumerOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateConsumerOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.statictopic.LogicQueueMappingItem; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingContext; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingDetail; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingUtils; +import org.apache.rocketmq.remoting.rpc.RpcClientUtils; +import org.apache.rocketmq.remoting.rpc.RpcRequest; +import org.apache.rocketmq.remoting.rpc.RpcResponse; -public class ConsumerManageProcessor implements NettyRequestProcessor { - private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); +import static org.apache.rocketmq.remoting.protocol.RemotingCommand.buildErrorResponse; +public class ConsumerManageProcessor implements NettyRequestProcessor { + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); private final BrokerController brokerController; public ConsumerManageProcessor(final BrokerController brokerController) { @@ -88,11 +97,11 @@ public RemotingCommand getConsumerListByGroup(ChannelHandlerContext ctx, Remotin response.setRemark(null); return response; } else { - log.warn("getAllClientId failed, {} {}", requestHeader.getConsumerGroup(), + LOGGER.warn("getAllClientId failed, {} {}", requestHeader.getConsumerGroup(), RemotingHelper.parseChannelRemoteAddr(ctx.channel())); } } else { - log.warn("getConsumerGroupInfo failed, {} {}", requestHeader.getConsumerGroup(), + LOGGER.warn("getConsumerGroupInfo failed, {} {}", requestHeader.getConsumerGroup(), RemotingHelper.parseChannelRemoteAddr(ctx.channel())); } @@ -101,20 +110,192 @@ public RemotingCommand getConsumerListByGroup(ChannelHandlerContext ctx, Remotin return response; } + public RemotingCommand rewriteRequestForStaticTopic(final UpdateConsumerOffsetRequestHeader requestHeader, + final TopicQueueMappingContext mappingContext) { + try { + if (mappingContext.getMappingDetail() == null) { + return null; + } + TopicQueueMappingDetail mappingDetail = mappingContext.getMappingDetail(); + if (!mappingContext.isLeader()) { + return buildErrorResponse(ResponseCode.NOT_LEADER_FOR_QUEUE, String.format("%s-%d does not exit in request process of current broker %s", requestHeader.getTopic(), requestHeader.getQueueId(), mappingDetail.getBname())); + } + Long globalOffset = requestHeader.getCommitOffset(); + LogicQueueMappingItem mappingItem = TopicQueueMappingUtils.findLogicQueueMappingItem(mappingContext.getMappingItemList(), globalOffset, true); + requestHeader.setQueueId(mappingItem.getQueueId()); + requestHeader.setLo(false); + requestHeader.setBrokerName(mappingItem.getBname()); + requestHeader.setCommitOffset(mappingItem.computePhysicalQueueOffset(globalOffset)); + //leader, let it go, do not need to rewrite the response + if (mappingDetail.getBname().equals(mappingItem.getBname())) { + return null; + } + RpcRequest rpcRequest = new RpcRequest(RequestCode.UPDATE_CONSUMER_OFFSET, requestHeader, null); + RpcResponse rpcResponse = this.brokerController.getBrokerOuterAPI().getRpcClient().invoke(rpcRequest, this.brokerController.getBrokerConfig().getForwardTimeout()).get(); + if (rpcResponse.getException() != null) { + throw rpcResponse.getException(); + } + return RpcClientUtils.createCommandForRpcResponse(rpcResponse); + } catch (Throwable t) { + return buildErrorResponse(ResponseCode.SYSTEM_ERROR, t.getMessage()); + } + } + private RemotingCommand updateConsumerOffset(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(UpdateConsumerOffsetResponseHeader.class); + final UpdateConsumerOffsetRequestHeader requestHeader = - (UpdateConsumerOffsetRequestHeader) request - .decodeCommandCustomHeader(UpdateConsumerOffsetRequestHeader.class); - this.brokerController.getConsumerOffsetManager().commitOffset(RemotingHelper.parseChannelRemoteAddr(ctx.channel()), requestHeader.getConsumerGroup(), - requestHeader.getTopic(), requestHeader.getQueueId(), requestHeader.getCommitOffset()); + (UpdateConsumerOffsetRequestHeader) + request.decodeCommandCustomHeader(UpdateConsumerOffsetRequestHeader.class); + + TopicQueueMappingContext mappingContext = + this.brokerController.getTopicQueueMappingManager().buildTopicQueueMappingContext(requestHeader); + + RemotingCommand rewriteResult = rewriteRequestForStaticTopic(requestHeader, mappingContext); + if (rewriteResult != null) { + return rewriteResult; + } + + String topic = requestHeader.getTopic(); + String group = requestHeader.getConsumerGroup(); + Integer queueId = requestHeader.getQueueId(); + Long offset = requestHeader.getCommitOffset(); + + if (!this.brokerController.getTopicConfigManager().containsTopic(requestHeader.getTopic())) { + response.setCode(ResponseCode.TOPIC_NOT_EXIST); + response.setRemark("Topic " + topic + " not exist!"); + return response; + } + + if (queueId == null) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("QueueId is null, topic is " + topic); + return response; + } + + if (offset == null) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("Offset is null, topic is " + topic); + return response; + } + + ConsumerOffsetManager consumerOffsetManager = brokerController.getConsumerOffsetManager(); + if (this.brokerController.getBrokerConfig().isUseServerSideResetOffset()) { + // Note, ignoring this update offset request + if (consumerOffsetManager.hasOffsetReset(topic, group, queueId)) { + response.setCode(ResponseCode.SUCCESS); + response.setRemark("Offset has been previously reset"); + LOGGER.info("Update consumer offset is rejected because of previous offset-reset. Group={}, " + + "Topic={}, QueueId={}, Offset={}", group, topic, queueId, offset); + return response; + } + } + + this.brokerController.getConsumerOffsetManager().commitOffset( + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), group, topic, queueId, offset); response.setCode(ResponseCode.SUCCESS); response.setRemark(null); return response; } + public RemotingCommand rewriteRequestForStaticTopic(QueryConsumerOffsetRequestHeader requestHeader, + TopicQueueMappingContext mappingContext) { + try { + if (mappingContext.getMappingDetail() == null) { + return null; + } + TopicQueueMappingDetail mappingDetail = mappingContext.getMappingDetail(); + if (!mappingContext.isLeader()) { + return buildErrorResponse(ResponseCode.NOT_LEADER_FOR_QUEUE, String.format("%s-%d does not exit in request process of current broker %s", requestHeader.getTopic(), requestHeader.getQueueId(), mappingDetail.getBname())); + } + List mappingItemList = mappingContext.getMappingItemList(); + if (mappingItemList.size() == 1 + && mappingItemList.get(0).getLogicOffset() == 0) { + //as physical, just let it go + mappingContext.setCurrentItem(mappingItemList.get(0)); + requestHeader.setQueueId(mappingContext.getLeaderItem().getQueueId()); + return null; + } + //double read check + List itemList = mappingContext.getMappingItemList(); + //by default, it is -1 + long offset = -1; + //double read, first from leader, then from second leader + for (int i = itemList.size() - 1; i >= 0; i--) { + LogicQueueMappingItem mappingItem = itemList.get(i); + mappingContext.setCurrentItem(mappingItem); + if (mappingItem.getBname().equals(mappingDetail.getBname())) { + offset = this.brokerController.getConsumerOffsetManager().queryOffset(requestHeader.getConsumerGroup(), requestHeader.getTopic(), mappingItem.getQueueId()); + if (offset >= 0) { + break; + } else { + //not found + continue; + } + } else { + //maybe we need to reconstruct an object + requestHeader.setBrokerName(mappingItem.getBname()); + requestHeader.setQueueId(mappingItem.getQueueId()); + requestHeader.setLo(false); + requestHeader.setSetZeroIfNotFound(false); + RpcRequest rpcRequest = new RpcRequest(RequestCode.QUERY_CONSUMER_OFFSET, requestHeader, null); + RpcResponse rpcResponse = this.brokerController.getBrokerOuterAPI().getRpcClient().invoke(rpcRequest, this.brokerController.getBrokerConfig().getForwardTimeout()).get(); + if (rpcResponse.getException() != null) { + throw rpcResponse.getException(); + } + if (rpcResponse.getCode() == ResponseCode.SUCCESS) { + offset = ((QueryConsumerOffsetResponseHeader) rpcResponse.getHeader()).getOffset(); + break; + } else if (rpcResponse.getCode() == ResponseCode.QUERY_NOT_FOUND) { + continue; + } else { + //this should not happen + throw new RuntimeException("Unknown response code " + rpcResponse.getCode()); + } + } + } + final RemotingCommand response = RemotingCommand.createResponseCommand(QueryConsumerOffsetResponseHeader.class); + final QueryConsumerOffsetResponseHeader responseHeader = (QueryConsumerOffsetResponseHeader) response.readCustomHeader(); + if (offset >= 0) { + responseHeader.setOffset(offset); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + } else { + response.setCode(ResponseCode.QUERY_NOT_FOUND); + response.setRemark("Not found, maybe this group consumer boot first"); + } + RemotingCommand rewriteResponseResult = rewriteResponseForStaticTopic(requestHeader, responseHeader, mappingContext, response.getCode()); + if (rewriteResponseResult != null) { + return rewriteResponseResult; + } + return response; + } catch (Throwable t) { + return buildErrorResponse(ResponseCode.SYSTEM_ERROR, t.getMessage()); + } + } + + public RemotingCommand rewriteResponseForStaticTopic(final QueryConsumerOffsetRequestHeader requestHeader, + final QueryConsumerOffsetResponseHeader responseHeader, + final TopicQueueMappingContext mappingContext, final int code) { + try { + if (mappingContext.getMappingDetail() == null) { + return null; + } + if (code != ResponseCode.SUCCESS) { + return null; + } + LogicQueueMappingItem item = mappingContext.getCurrentItem(); + responseHeader.setOffset(item.computeStaticQueueOffsetStrictly(responseHeader.getOffset())); + //no need to construct new object + return null; + } catch (Throwable t) { + return buildErrorResponse(ResponseCode.SYSTEM_ERROR, t.getMessage()); + } + } + private RemotingCommand queryConsumerOffset(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException { final RemotingCommand response = @@ -125,6 +306,12 @@ private RemotingCommand queryConsumerOffset(ChannelHandlerContext ctx, RemotingC (QueryConsumerOffsetRequestHeader) request .decodeCommandCustomHeader(QueryConsumerOffsetRequestHeader.class); + TopicQueueMappingContext mappingContext = this.brokerController.getTopicQueueMappingManager().buildTopicQueueMappingContext(requestHeader); + RemotingCommand rewriteResult = rewriteRequestForStaticTopic(requestHeader, mappingContext); + if (rewriteResult != null) { + return rewriteResult; + } + long offset = this.brokerController.getConsumerOffsetManager().queryOffset( requestHeader.getConsumerGroup(), requestHeader.getTopic(), requestHeader.getQueueId()); @@ -137,9 +324,12 @@ private RemotingCommand queryConsumerOffset(ChannelHandlerContext ctx, RemotingC long minOffset = this.brokerController.getMessageStore().getMinOffsetInQueue(requestHeader.getTopic(), requestHeader.getQueueId()); - if (minOffset <= 0 - && !this.brokerController.getMessageStore().checkInDiskByConsumeOffset( - requestHeader.getTopic(), requestHeader.getQueueId(), 0)) { + if (requestHeader.getSetZeroIfNotFound() != null && Boolean.FALSE.equals(requestHeader.getSetZeroIfNotFound())) { + response.setCode(ResponseCode.QUERY_NOT_FOUND); + response.setRemark("Not found, do not set to zero, maybe this group boot first"); + } else if (minOffset <= 0 + && this.brokerController.getMessageStore().checkInMemByConsumeOffset( + requestHeader.getTopic(), requestHeader.getQueueId(), 0, 1)) { responseHeader.setOffset(0L); response.setCode(ResponseCode.SUCCESS); response.setRemark(null); @@ -149,6 +339,11 @@ private RemotingCommand queryConsumerOffset(ChannelHandlerContext ctx, RemotingC } } + RemotingCommand rewriteResponseResult = rewriteResponseForStaticTopic(requestHeader, responseHeader, mappingContext, response.getCode()); + if (rewriteResponseResult != null) { + return rewriteResponseResult; + } + return response; } } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/DefaultPullMessageResultHandler.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/DefaultPullMessageResultHandler.java new file mode 100644 index 00000000000..43b66b4c516 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/DefaultPullMessageResultHandler.java @@ -0,0 +1,296 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.processor; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.FileRegion; +import io.opentelemetry.api.common.Attributes; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.longpolling.PullRequest; +import org.apache.rocketmq.broker.metrics.BrokerMetricsManager; +import org.apache.rocketmq.broker.pagecache.ManyMessageTransfer; +import org.apache.rocketmq.broker.plugin.PullMessageResultHandler; +import org.apache.rocketmq.common.AbortProcessException; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.TopicFilterType; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.common.sysflag.PullSysFlag; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.metrics.RemotingMetricsManager; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PullMessageResponseHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingContext; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.remoting.protocol.topic.OffsetMovedEvent; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.MessageFilter; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.config.BrokerRole; + +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_CONSUMER_GROUP; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_IS_SYSTEM; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_TOPIC; +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.LABEL_REQUEST_CODE; +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.LABEL_RESPONSE_CODE; +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.LABEL_RESULT; + +public class DefaultPullMessageResultHandler implements PullMessageResultHandler { + + protected static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + protected final BrokerController brokerController; + + public DefaultPullMessageResultHandler(final BrokerController brokerController) { + this.brokerController = brokerController; + } + + @Override + public RemotingCommand handle(final GetMessageResult getMessageResult, + final RemotingCommand request, + final PullMessageRequestHeader requestHeader, + final Channel channel, + final SubscriptionData subscriptionData, + final SubscriptionGroupConfig subscriptionGroupConfig, + final boolean brokerAllowSuspend, + final MessageFilter messageFilter, + RemotingCommand response, + TopicQueueMappingContext mappingContext, + long beginTimeMills) { + PullMessageProcessor processor = brokerController.getPullMessageProcessor(); + final String clientAddress = RemotingHelper.parseChannelRemoteAddr(channel); + TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic()); + processor.composeResponseHeader(requestHeader, getMessageResult, topicConfig.getTopicSysFlag(), + subscriptionGroupConfig, response, clientAddress); + try { + processor.executeConsumeMessageHookBefore(request, requestHeader, getMessageResult, brokerAllowSuspend, response.getCode()); + } catch (AbortProcessException e) { + response.setCode(e.getResponseCode()); + response.setRemark(e.getErrorMessage()); + return response; + } + + //rewrite the response for the static topic + final PullMessageResponseHeader responseHeader = (PullMessageResponseHeader) response.readCustomHeader(); + RemotingCommand rewriteResult = processor.rewriteResponseForStaticTopic(requestHeader, responseHeader, mappingContext, response.getCode()); + if (rewriteResult != null) { + response = rewriteResult; + } + + processor.updateBroadcastPulledOffset(requestHeader.getTopic(), requestHeader.getConsumerGroup(), + requestHeader.getQueueId(), requestHeader, channel, response, getMessageResult.getNextBeginOffset()); + processor.tryCommitOffset(brokerAllowSuspend, requestHeader, getMessageResult.getNextBeginOffset(), + clientAddress); + + switch (response.getCode()) { + case ResponseCode.SUCCESS: + this.brokerController.getBrokerStatsManager().incGroupGetNums(requestHeader.getConsumerGroup(), requestHeader.getTopic(), + getMessageResult.getMessageCount()); + + this.brokerController.getBrokerStatsManager().incGroupGetSize(requestHeader.getConsumerGroup(), requestHeader.getTopic(), + getMessageResult.getBufferTotalSize()); + + this.brokerController.getBrokerStatsManager().incBrokerGetNums(requestHeader.getTopic(), getMessageResult.getMessageCount()); + + if (!BrokerMetricsManager.isRetryOrDlqTopic(requestHeader.getTopic())) { + Attributes attributes = BrokerMetricsManager.newAttributesBuilder() + .put(LABEL_TOPIC, requestHeader.getTopic()) + .put(LABEL_CONSUMER_GROUP, requestHeader.getConsumerGroup()) + .put(LABEL_IS_SYSTEM, TopicValidator.isSystemTopic(requestHeader.getTopic()) || MixAll.isSysConsumerGroup(requestHeader.getConsumerGroup())) + .build(); + BrokerMetricsManager.messagesOutTotal.add(getMessageResult.getMessageCount(), attributes); + BrokerMetricsManager.throughputOutTotal.add(getMessageResult.getBufferTotalSize(), attributes); + } + + if (!channelIsWritable(channel, requestHeader)) { + getMessageResult.release(); + //ignore pull request + return null; + } + + if (this.brokerController.getBrokerConfig().isTransferMsgByHeap()) { + final byte[] r = this.readGetMessageResult(getMessageResult, requestHeader.getConsumerGroup(), requestHeader.getTopic(), requestHeader.getQueueId()); + this.brokerController.getBrokerStatsManager().incGroupGetLatency(requestHeader.getConsumerGroup(), + requestHeader.getTopic(), requestHeader.getQueueId(), + (int) (this.brokerController.getMessageStore().now() - beginTimeMills)); + response.setBody(r); + return response; + } else { + try { + FileRegion fileRegion = + new ManyMessageTransfer(response.encodeHeader(getMessageResult.getBufferTotalSize()), getMessageResult); + RemotingCommand finalResponse = response; + channel.writeAndFlush(fileRegion) + .addListener((ChannelFutureListener) future -> { + getMessageResult.release(); + Attributes attributes = RemotingMetricsManager.newAttributesBuilder() + .put(LABEL_REQUEST_CODE, RemotingHelper.getRequestCodeDesc(request.getCode())) + .put(LABEL_RESPONSE_CODE, RemotingHelper.getResponseCodeDesc(finalResponse.getCode())) + .put(LABEL_RESULT, RemotingMetricsManager.getWriteAndFlushResult(future)) + .build(); + RemotingMetricsManager.rpcLatency.record(request.getProcessTimer().elapsed(TimeUnit.MILLISECONDS), attributes); + if (!future.isSuccess()) { + log.error("Fail to transfer messages from page cache to {}", channel.remoteAddress(), future.cause()); + } + }); + } catch (Throwable e) { + log.error("Error occurred when transferring messages from page cache", e); + getMessageResult.release(); + } + return null; + } + case ResponseCode.PULL_NOT_FOUND: + final boolean hasSuspendFlag = PullSysFlag.hasSuspendFlag(requestHeader.getSysFlag()); + final long suspendTimeoutMillisLong = hasSuspendFlag ? requestHeader.getSuspendTimeoutMillis() : 0; + + if (brokerAllowSuspend && hasSuspendFlag) { + long pollingTimeMills = suspendTimeoutMillisLong; + if (!this.brokerController.getBrokerConfig().isLongPollingEnable()) { + pollingTimeMills = this.brokerController.getBrokerConfig().getShortPollingTimeMills(); + } + + String topic = requestHeader.getTopic(); + long offset = requestHeader.getQueueOffset(); + int queueId = requestHeader.getQueueId(); + PullRequest pullRequest = new PullRequest(request, channel, pollingTimeMills, + this.brokerController.getMessageStore().now(), offset, subscriptionData, messageFilter); + this.brokerController.getPullRequestHoldService().suspendPullRequest(topic, queueId, pullRequest); + return null; + } + case ResponseCode.PULL_RETRY_IMMEDIATELY: + break; + case ResponseCode.PULL_OFFSET_MOVED: + if (this.brokerController.getMessageStoreConfig().getBrokerRole() != BrokerRole.SLAVE + || this.brokerController.getMessageStoreConfig().isOffsetCheckInSlave()) { + MessageQueue mq = new MessageQueue(); + mq.setTopic(requestHeader.getTopic()); + mq.setQueueId(requestHeader.getQueueId()); + mq.setBrokerName(this.brokerController.getBrokerConfig().getBrokerName()); + + OffsetMovedEvent event = new OffsetMovedEvent(); + event.setConsumerGroup(requestHeader.getConsumerGroup()); + event.setMessageQueue(mq); + event.setOffsetRequest(requestHeader.getQueueOffset()); + event.setOffsetNew(getMessageResult.getNextBeginOffset()); + log.warn( + "PULL_OFFSET_MOVED:correction offset. topic={}, groupId={}, requestOffset={}, newOffset={}, suggestBrokerId={}", + requestHeader.getTopic(), requestHeader.getConsumerGroup(), event.getOffsetRequest(), event.getOffsetNew(), + responseHeader.getSuggestWhichBrokerId()); + } else { + responseHeader.setSuggestWhichBrokerId(subscriptionGroupConfig.getBrokerId()); + response.setCode(ResponseCode.PULL_RETRY_IMMEDIATELY); + log.warn("PULL_OFFSET_MOVED:none correction. topic={}, groupId={}, requestOffset={}, suggestBrokerId={}", + requestHeader.getTopic(), requestHeader.getConsumerGroup(), requestHeader.getQueueOffset(), + responseHeader.getSuggestWhichBrokerId()); + } + + break; + default: + log.warn("[BUG] impossible result code of get message: {}", response.getCode()); + assert false; + } + + return response; + } + + private boolean channelIsWritable(Channel channel, PullMessageRequestHeader requestHeader) { + if (this.brokerController.getBrokerConfig().isEnableNetWorkFlowControl()) { + if (!channel.isWritable()) { + log.warn("channel {} not writable ,cid {}", channel.remoteAddress(), requestHeader.getConsumerGroup()); + return false; + } + + } + return true; + } + + protected byte[] readGetMessageResult(final GetMessageResult getMessageResult, final String group, + final String topic, + final int queueId) { + final ByteBuffer byteBuffer = ByteBuffer.allocate(getMessageResult.getBufferTotalSize()); + + long storeTimestamp = 0; + try { + List messageBufferList = getMessageResult.getMessageBufferList(); + for (ByteBuffer bb : messageBufferList) { + + byteBuffer.put(bb); + int sysFlag = bb.getInt(MessageDecoder.SYSFLAG_POSITION); +// bornhost has the IPv4 ip if the MessageSysFlag.BORNHOST_V6_FLAG bit of sysFlag is 0 +// IPv4 host = ip(4 byte) + port(4 byte); IPv6 host = ip(16 byte) + port(4 byte) + int bornhostLength = (sysFlag & MessageSysFlag.BORNHOST_V6_FLAG) == 0 ? 8 : 20; + int msgStoreTimePos = 4 // 1 TOTALSIZE + + 4 // 2 MAGICCODE + + 4 // 3 BODYCRC + + 4 // 4 QUEUEID + + 4 // 5 FLAG + + 8 // 6 QUEUEOFFSET + + 8 // 7 PHYSICALOFFSET + + 4 // 8 SYSFLAG + + 8 // 9 BORNTIMESTAMP + + bornhostLength; // 10 BORNHOST + storeTimestamp = bb.getLong(msgStoreTimePos); + } + } finally { + getMessageResult.release(); + } + + this.brokerController.getBrokerStatsManager().recordDiskFallBehindTime(group, topic, queueId, this.brokerController.getMessageStore().now() - storeTimestamp); + return byteBuffer.array(); + } + + protected void generateOffsetMovedEvent(final OffsetMovedEvent event) { + try { + MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); + msgInner.setTopic(TopicValidator.RMQ_SYS_OFFSET_MOVED_EVENT); + msgInner.setTags(event.getConsumerGroup()); + msgInner.setDelayTimeLevel(0); + msgInner.setKeys(event.getConsumerGroup()); + msgInner.setBody(event.encode()); + msgInner.setFlag(0); + msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); + msgInner.setTagsCode(MessageExtBrokerInner.tagsString2tagsCode(TopicFilterType.SINGLE_TAG, msgInner.getTags())); + + msgInner.setQueueId(0); + msgInner.setSysFlag(0); + msgInner.setBornTimestamp(System.currentTimeMillis()); + msgInner.setBornHost(NetworkUtil.string2SocketAddress(this.brokerController.getBrokerAddr())); + msgInner.setStoreHost(msgInner.getBornHost()); + + msgInner.setReconsumeTimes(0); + + PutMessageResult putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner); + } catch (Exception e) { + log.warn(String.format("generateOffsetMovedEvent Exception, %s", event.toString()), e); + } + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/EndTransactionProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/EndTransactionProcessor.java index fee1420a9e7..e812a53ba7c 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/EndTransactionProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/EndTransactionProcessor.java @@ -17,26 +17,35 @@ package org.apache.rocketmq.broker.processor; import io.netty.channel.ChannelHandlerContext; +import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.metrics.BrokerMetricsManager; +import org.apache.rocketmq.broker.transaction.OperationResult; +import org.apache.rocketmq.broker.transaction.queue.TransactionalMessageUtil; import org.apache.rocketmq.common.TopicFilterType; import org.apache.rocketmq.common.constant.LoggerName; import org.apache.rocketmq.common.message.MessageAccessor; import org.apache.rocketmq.common.message.MessageConst; import org.apache.rocketmq.common.message.MessageDecoder; import org.apache.rocketmq.common.message.MessageExt; -import org.apache.rocketmq.common.protocol.ResponseCode; -import org.apache.rocketmq.common.protocol.header.EndTransactionRequestHeader; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.remoting.common.RemotingHelper; import org.apache.rocketmq.remoting.exception.RemotingCommandException; import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; import org.apache.rocketmq.remoting.protocol.RemotingCommand; -import org.apache.rocketmq.store.MessageExtBrokerInner; -import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.EndTransactionRequestHeader; import org.apache.rocketmq.store.PutMessageResult; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.config.BrokerRole; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_TOPIC; + +/** + * EndTransaction processor: process commit and rollback message + */ public class EndTransactionProcessor implements NettyRequestProcessor { private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.TRANSACTION_LOGGER_NAME); private final BrokerController brokerController; @@ -46,16 +55,22 @@ public EndTransactionProcessor(final BrokerController brokerController) { } @Override - public RemotingCommand processRequest(ChannelHandlerContext ctx, - RemotingCommand request) throws RemotingCommandException { + public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) throws + RemotingCommandException { final RemotingCommand response = RemotingCommand.createResponseCommand(null); final EndTransactionRequestHeader requestHeader = (EndTransactionRequestHeader) request.decodeCommandCustomHeader(EndTransactionRequestHeader.class); + LOGGER.debug("Transaction request:{}", requestHeader); + if (BrokerRole.SLAVE == brokerController.getMessageStoreConfig().getBrokerRole()) { + response.setCode(ResponseCode.SLAVE_NOT_AVAILABLE); + LOGGER.warn("Message store is slave mode, so end transaction is forbidden. "); + return response; + } if (requestHeader.getFromTransactionCheck()) { switch (requestHeader.getCommitOrRollback()) { case MessageSysFlag.TRANSACTION_NOT_TYPE: { - LOGGER.warn("check producer[{}] transaction state, but it's pending status." + LOGGER.warn("Check producer[{}] transaction state, but it's pending status." + "RequestHeader: {} Remark: {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel()), requestHeader.toString(), @@ -64,7 +79,7 @@ public RemotingCommand processRequest(ChannelHandlerContext ctx, } case MessageSysFlag.TRANSACTION_COMMIT_TYPE: { - LOGGER.warn("check producer[{}] transaction state, the producer commit the message." + LOGGER.warn("Check producer[{}] transaction state, the producer commit the message." + "RequestHeader: {} Remark: {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel()), requestHeader.toString(), @@ -74,7 +89,7 @@ public RemotingCommand processRequest(ChannelHandlerContext ctx, } case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE: { - LOGGER.warn("check producer[{}] transaction state, the producer rollback the message." + LOGGER.warn("Check producer[{}] transaction state, the producer rollback the message." + "RequestHeader: {} Remark: {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel()), requestHeader.toString(), @@ -87,7 +102,7 @@ public RemotingCommand processRequest(ChannelHandlerContext ctx, } else { switch (requestHeader.getCommitOrRollback()) { case MessageSysFlag.TRANSACTION_NOT_TYPE: { - LOGGER.warn("the producer[{}] end transaction in sending message, and it's pending status." + LOGGER.warn("The producer[{}] end transaction in sending message, and it's pending status." + "RequestHeader: {} Remark: {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel()), requestHeader.toString(), @@ -100,7 +115,7 @@ public RemotingCommand processRequest(ChannelHandlerContext ctx, } case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE: { - LOGGER.warn("the producer[{}] end transaction in sending message, rollback the message." + LOGGER.warn("The producer[{}] end transaction in sending message, rollback the message." + "RequestHeader: {} Remark: {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel()), requestHeader.toString(), @@ -111,122 +126,221 @@ public RemotingCommand processRequest(ChannelHandlerContext ctx, return null; } } + OperationResult result = new OperationResult(); + if (MessageSysFlag.TRANSACTION_COMMIT_TYPE == requestHeader.getCommitOrRollback()) { + result = this.brokerController.getTransactionalMessageService().commitMessage(requestHeader); + if (result.getResponseCode() == ResponseCode.SUCCESS) { + if (rejectCommitOrRollback(requestHeader, result.getPrepareMessage())) { + response.setCode(ResponseCode.ILLEGAL_OPERATION); + LOGGER.warn("Message commit fail [producer end]. currentTimeMillis - bornTime > checkImmunityTime, msgId={},commitLogOffset={}, wait check", + requestHeader.getMsgId(), requestHeader.getCommitLogOffset()); + return response; + } + RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader); + if (res.getCode() == ResponseCode.SUCCESS) { + MessageExtBrokerInner msgInner = endMessageTransaction(result.getPrepareMessage()); + msgInner.setSysFlag(MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), requestHeader.getCommitOrRollback())); + msgInner.setQueueOffset(requestHeader.getTranStateTableOffset()); + msgInner.setPreparedTransactionOffset(requestHeader.getCommitLogOffset()); + msgInner.setStoreTimestamp(result.getPrepareMessage().getStoreTimestamp()); + MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_TRANSACTION_PREPARED); + RemotingCommand sendResult = sendFinalMessage(msgInner); + if (sendResult.getCode() == ResponseCode.SUCCESS) { + this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage()); + // successful committed, then total num of half-messages minus 1 + this.brokerController.getTransactionalMessageService().getTransactionMetrics().addAndGet(msgInner.getTopic(), -1); + BrokerMetricsManager.commitMessagesTotal.add(1, BrokerMetricsManager.newAttributesBuilder() + .put(LABEL_TOPIC, msgInner.getTopic()) + .build()); + // record the commit latency. + Long commitLatency = (System.currentTimeMillis() - result.getPrepareMessage().getBornTimestamp()) / 1000; + BrokerMetricsManager.transactionFinishLatency.record(commitLatency, BrokerMetricsManager.newAttributesBuilder() + .put(LABEL_TOPIC, msgInner.getTopic()) + .build()); + } + return sendResult; + } + return res; + } + } else if (MessageSysFlag.TRANSACTION_ROLLBACK_TYPE == requestHeader.getCommitOrRollback()) { + result = this.brokerController.getTransactionalMessageService().rollbackMessage(requestHeader); + if (result.getResponseCode() == ResponseCode.SUCCESS) { + if (rejectCommitOrRollback(requestHeader, result.getPrepareMessage())) { + response.setCode(ResponseCode.ILLEGAL_OPERATION); + LOGGER.warn("Message rollback fail [producer end]. currentTimeMillis - bornTime > checkImmunityTime, msgId={},commitLogOffset={}, wait check", + requestHeader.getMsgId(), requestHeader.getCommitLogOffset()); + return response; + } + RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader); + if (res.getCode() == ResponseCode.SUCCESS) { + this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage()); + // roll back, then total num of half-messages minus 1 + this.brokerController.getTransactionalMessageService().getTransactionMetrics().addAndGet(result.getPrepareMessage().getProperty(MessageConst.PROPERTY_REAL_TOPIC), -1); + BrokerMetricsManager.rollBackMessagesTotal.add(1, BrokerMetricsManager.newAttributesBuilder() + .put(LABEL_TOPIC, result.getPrepareMessage().getProperty(MessageConst.PROPERTY_REAL_TOPIC)) + .build()); + } + return res; + } + } + response.setCode(result.getResponseCode()); + response.setRemark(result.getResponseRemark()); + return response; + } + + /** + * If you specify a custom first check time CheckImmunityTimeInSeconds, + * And the commit/rollback request whose validity period exceeds CheckImmunityTimeInSeconds and is not checked back will be processed and failed + * returns ILLEGAL_OPERATION 604 error + * @param requestHeader + * @param messageExt + * @return + */ + public boolean rejectCommitOrRollback(EndTransactionRequestHeader requestHeader, MessageExt messageExt) { + if (requestHeader.getFromTransactionCheck()) { + return false; + } + long transactionTimeout = brokerController.getBrokerConfig().getTransactionTimeOut(); - final MessageExt msgExt = this.brokerController.getMessageStore().lookMessageByOffset(requestHeader.getCommitLogOffset()); + String checkImmunityTimeStr = messageExt.getUserProperty(MessageConst.PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS); + if (StringUtils.isNotEmpty(checkImmunityTimeStr)) { + long valueOfCurrentMinusBorn = System.currentTimeMillis() - messageExt.getBornTimestamp(); + long checkImmunityTime = TransactionalMessageUtil.getImmunityTime(checkImmunityTimeStr, transactionTimeout); + //Non-check requests that exceed the specified custom first check time fail to return + return valueOfCurrentMinusBorn > checkImmunityTime; + } + return false; + } + + @Override + public boolean rejectRequest() { + return false; + } + + private RemotingCommand checkPrepareMessage(MessageExt msgExt, EndTransactionRequestHeader requestHeader) { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); if (msgExt != null) { final String pgroupRead = msgExt.getProperty(MessageConst.PROPERTY_PRODUCER_GROUP); if (!pgroupRead.equals(requestHeader.getProducerGroup())) { response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark("the producer group wrong"); + response.setRemark("The producer group wrong"); return response; } if (msgExt.getQueueOffset() != requestHeader.getTranStateTableOffset()) { response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark("the transaction state table offset wrong"); + response.setRemark("The transaction state table offset wrong"); return response; } if (msgExt.getCommitLogOffset() != requestHeader.getCommitLogOffset()) { response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark("the commit log offset wrong"); - return response; - } - - MessageExtBrokerInner msgInner = this.endMessageTransaction(msgExt); - msgInner.setSysFlag(MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), requestHeader.getCommitOrRollback())); - - msgInner.setQueueOffset(requestHeader.getTranStateTableOffset()); - msgInner.setPreparedTransactionOffset(requestHeader.getCommitLogOffset()); - msgInner.setStoreTimestamp(msgExt.getStoreTimestamp()); - if (MessageSysFlag.TRANSACTION_ROLLBACK_TYPE == requestHeader.getCommitOrRollback()) { - msgInner.setBody(null); - } - - final MessageStore messageStore = this.brokerController.getMessageStore(); - final PutMessageResult putMessageResult = messageStore.putMessage(msgInner); - if (putMessageResult != null) { - switch (putMessageResult.getPutMessageStatus()) { - // Success - case PUT_OK: - case FLUSH_DISK_TIMEOUT: - case FLUSH_SLAVE_TIMEOUT: - case SLAVE_NOT_AVAILABLE: - response.setCode(ResponseCode.SUCCESS); - response.setRemark(null); - break; - // Failed - case CREATE_MAPEDFILE_FAILED: - response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark("create mapped file failed."); - break; - case MESSAGE_ILLEGAL: - case PROPERTIES_SIZE_EXCEEDED: - response.setCode(ResponseCode.MESSAGE_ILLEGAL); - response.setRemark("the message is illegal, maybe msg body or properties length not matched. msg body length limit 128k, msg properties length limit 32k."); - break; - case SERVICE_NOT_AVAILABLE: - response.setCode(ResponseCode.SERVICE_NOT_AVAILABLE); - response.setRemark("service not available now."); - break; - case OS_PAGECACHE_BUSY: - response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark("OS page cache busy, please try another machine"); - break; - case UNKNOWN_ERROR: - response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark("UNKNOWN_ERROR"); - break; - default: - response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark("UNKNOWN_ERROR DEFAULT"); - break; - } - + response.setRemark("The commit log offset wrong"); return response; - } else { - response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark("store putMessage return null"); } } else { response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark("find prepared transaction message failed"); + response.setRemark("Find prepared transaction message failed"); return response; } - + response.setCode(ResponseCode.SUCCESS); return response; } - @Override - public boolean rejectRequest() { - return false; - } - private MessageExtBrokerInner endMessageTransaction(MessageExt msgExt) { MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); + msgInner.setTopic(msgExt.getUserProperty(MessageConst.PROPERTY_REAL_TOPIC)); + msgInner.setQueueId(Integer.parseInt(msgExt.getUserProperty(MessageConst.PROPERTY_REAL_QUEUE_ID))); msgInner.setBody(msgExt.getBody()); msgInner.setFlag(msgExt.getFlag()); - MessageAccessor.setProperties(msgInner, msgExt.getProperties()); - + msgInner.setBornTimestamp(msgExt.getBornTimestamp()); + msgInner.setBornHost(msgExt.getBornHost()); + msgInner.setStoreHost(msgExt.getStoreHost()); + msgInner.setReconsumeTimes(msgExt.getReconsumeTimes()); + msgInner.setWaitStoreMsgOK(false); + msgInner.setTransactionId(msgExt.getUserProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX)); + msgInner.setSysFlag(msgExt.getSysFlag()); TopicFilterType topicFilterType = (msgInner.getSysFlag() & MessageSysFlag.MULTI_TAGS_FLAG) == MessageSysFlag.MULTI_TAGS_FLAG ? TopicFilterType.MULTI_TAG : TopicFilterType.SINGLE_TAG; long tagsCodeValue = MessageExtBrokerInner.tagsString2tagsCode(topicFilterType, msgInner.getTags()); msgInner.setTagsCode(tagsCodeValue); + MessageAccessor.setProperties(msgInner, msgExt.getProperties()); msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgExt.getProperties())); - - msgInner.setSysFlag(msgExt.getSysFlag()); - msgInner.setBornTimestamp(msgExt.getBornTimestamp()); - msgInner.setBornHost(msgExt.getBornHost()); - msgInner.setStoreHost(msgExt.getStoreHost()); - msgInner.setReconsumeTimes(msgExt.getReconsumeTimes()); - - msgInner.setWaitStoreMsgOK(false); - MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_DELAY_TIME_LEVEL); - - msgInner.setTopic(msgExt.getTopic()); - msgInner.setQueueId(msgExt.getQueueId()); - + MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_REAL_TOPIC); + MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_REAL_QUEUE_ID); return msgInner; } + + private RemotingCommand sendFinalMessage(MessageExtBrokerInner msgInner) { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + final PutMessageResult putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner); + if (putMessageResult != null) { + switch (putMessageResult.getPutMessageStatus()) { + // Success + case PUT_OK: + case FLUSH_DISK_TIMEOUT: + case FLUSH_SLAVE_TIMEOUT: + case SLAVE_NOT_AVAILABLE: + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + break; + // Failed + case CREATE_MAPPED_FILE_FAILED: + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("Create mapped file failed."); + break; + case MESSAGE_ILLEGAL: + case PROPERTIES_SIZE_EXCEEDED: + response.setCode(ResponseCode.MESSAGE_ILLEGAL); + response.setRemark(String.format("The message is illegal, maybe msg body or properties length not matched. msg body length limit %dB, msg properties length limit 32KB.", + this.brokerController.getMessageStoreConfig().getMaxMessageSize())); + break; + case SERVICE_NOT_AVAILABLE: + response.setCode(ResponseCode.SERVICE_NOT_AVAILABLE); + response.setRemark("Service not available now."); + break; + case OS_PAGE_CACHE_BUSY: + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("OS page cache busy, please try another machine"); + break; + case WHEEL_TIMER_MSG_ILLEGAL: + response.setCode(ResponseCode.MESSAGE_ILLEGAL); + response.setRemark(String.format("timer message illegal, the delay time should not be bigger than the max delay %dms; or if set del msg, the delay time should be bigger than the current time", + this.brokerController.getMessageStoreConfig().getTimerMaxDelaySec() * 1000L)); + break; + case WHEEL_TIMER_FLOW_CONTROL: + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(String.format("timer message is under flow control, max num limit is %d or the current value is greater than %d and less than %d, trigger random flow control", + this.brokerController.getMessageStoreConfig().getTimerCongestNumEachSlot() * 2L, this.brokerController.getMessageStoreConfig().getTimerCongestNumEachSlot(), this.brokerController.getMessageStoreConfig().getTimerCongestNumEachSlot() * 2L)); + break; + case WHEEL_TIMER_NOT_ENABLE: + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(String.format("accurate timer message is not enabled, timerWheelEnable is %s", + this.brokerController.getMessageStoreConfig().isTimerWheelEnable())); + break; + case UNKNOWN_ERROR: + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("UNKNOWN_ERROR"); + break; + case IN_SYNC_REPLICAS_NOT_ENOUGH: + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("in-sync replicas not enough"); + break; + case PUT_TO_REMOTE_BROKER_FAIL: + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("put to remote broker fail"); + break; + default: + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("UNKNOWN_ERROR DEFAULT"); + break; + } + return response; + } else { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("store putMessage return null"); + } + return response; + } } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/ForwardRequestProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/ForwardRequestProcessor.java deleted file mode 100644 index 199aa940d69..00000000000 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/ForwardRequestProcessor.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.broker.processor; - -import io.netty.channel.ChannelHandlerContext; -import org.apache.rocketmq.broker.BrokerController; -import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; -import org.apache.rocketmq.remoting.protocol.RemotingCommand; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class ForwardRequestProcessor implements NettyRequestProcessor { - private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); - - private final BrokerController brokerController; - - public ForwardRequestProcessor(final BrokerController brokerController) { - this.brokerController = brokerController; - } - - @Override - public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) { - return null; - } - - @Override - public boolean rejectRequest() { - return false; - } -} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/NotificationProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/NotificationProcessor.java new file mode 100644 index 00000000000..0deb3ee7077 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/NotificationProcessor.java @@ -0,0 +1,211 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.processor; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import java.util.Objects; +import java.util.Random; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.longpolling.PollingHeader; +import org.apache.rocketmq.broker.longpolling.PollingResult; +import org.apache.rocketmq.broker.longpolling.PopLongPollingService; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.KeyBuilder; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.constant.PermName; +import org.apache.rocketmq.common.help.FAQUrl; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.NotificationRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.NotificationResponseHeader; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; + +public class NotificationProcessor implements NettyRequestProcessor { + private static final Logger POP_LOGGER = LoggerFactory.getLogger(LoggerName.ROCKETMQ_POP_LOGGER_NAME); + private final BrokerController brokerController; + private final Random random = new Random(System.currentTimeMillis()); + private final PopLongPollingService popLongPollingService; + private static final String BORN_TIME = "bornTime"; + + public NotificationProcessor(final BrokerController brokerController) { + this.brokerController = brokerController; + this.popLongPollingService = new PopLongPollingService(brokerController, this); + } + + @Override + public boolean rejectRequest() { + return false; + } + + public void notifyMessageArriving(final String topic, final int queueId) { + popLongPollingService.notifyMessageArrivingWithRetryTopic(topic, queueId); + } + + @Override + public RemotingCommand processRequest(final ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + request.addExtFieldIfNotExist(BORN_TIME, String.valueOf(System.currentTimeMillis())); + if (Objects.equals(request.getExtFields().get(BORN_TIME), "0")) { + request.addExtField(BORN_TIME, String.valueOf(System.currentTimeMillis())); + } + Channel channel = ctx.channel(); + + RemotingCommand response = RemotingCommand.createResponseCommand(NotificationResponseHeader.class); + final NotificationResponseHeader responseHeader = (NotificationResponseHeader) response.readCustomHeader(); + final NotificationRequestHeader requestHeader = + (NotificationRequestHeader) request.decodeCommandCustomHeader(NotificationRequestHeader.class); + + response.setOpaque(request.getOpaque()); + + if (!PermName.isReadable(this.brokerController.getBrokerConfig().getBrokerPermission())) { + response.setCode(ResponseCode.NO_PERMISSION); + response.setRemark(String.format("the broker[%s] peeking message is forbidden", this.brokerController.getBrokerConfig().getBrokerIP1())); + return response; + } + + TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic()); + if (null == topicConfig) { + POP_LOGGER.error("The topic {} not exist, consumer: {} ", requestHeader.getTopic(), RemotingHelper.parseChannelRemoteAddr(channel)); + response.setCode(ResponseCode.TOPIC_NOT_EXIST); + response.setRemark(String.format("topic[%s] not exist, apply first please! %s", requestHeader.getTopic(), FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL))); + return response; + } + + if (!PermName.isReadable(topicConfig.getPerm())) { + response.setCode(ResponseCode.NO_PERMISSION); + response.setRemark("the topic[" + requestHeader.getTopic() + "] peeking message is forbidden"); + return response; + } + + if (requestHeader.getQueueId() >= topicConfig.getReadQueueNums()) { + String errorInfo = String.format("queueId[%d] is illegal, topic:[%s] topicConfig.readQueueNums:[%d] consumer:[%s]", + requestHeader.getQueueId(), requestHeader.getTopic(), topicConfig.getReadQueueNums(), channel.remoteAddress()); + POP_LOGGER.warn(errorInfo); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(errorInfo); + return response; + } + + SubscriptionGroupConfig subscriptionGroupConfig = this.brokerController.getSubscriptionGroupManager().findSubscriptionGroupConfig(requestHeader.getConsumerGroup()); + if (null == subscriptionGroupConfig) { + response.setCode(ResponseCode.SUBSCRIPTION_GROUP_NOT_EXIST); + response.setRemark(String.format("subscription group [%s] does not exist, %s", requestHeader.getConsumerGroup(), FAQUrl.suggestTodo(FAQUrl.SUBSCRIPTION_GROUP_NOT_EXIST))); + return response; + } + + if (!subscriptionGroupConfig.isConsumeEnable()) { + response.setCode(ResponseCode.NO_PERMISSION); + response.setRemark("subscription group no permission, " + requestHeader.getConsumerGroup()); + return response; + } + int randomQ = random.nextInt(100); + boolean hasMsg = false; + boolean needRetry = randomQ % 5 == 0; + BrokerConfig brokerConfig = brokerController.getBrokerConfig(); + if (needRetry) { + String retryTopic = KeyBuilder.buildPopRetryTopic(requestHeader.getTopic(), requestHeader.getConsumerGroup(), brokerConfig.isEnableRetryTopicV2()); + hasMsg = hasMsgFromTopic(retryTopic, randomQ, requestHeader); + if (!hasMsg && brokerConfig.isEnableRetryTopicV2() && brokerConfig.isRetrieveMessageFromPopRetryTopicV1()) { + String retryTopicConfigV1 = KeyBuilder.buildPopRetryTopicV1(requestHeader.getTopic(), requestHeader.getConsumerGroup()); + hasMsg = hasMsgFromTopic(retryTopicConfigV1, randomQ, requestHeader); + } + } + if (!hasMsg) { + if (requestHeader.getQueueId() < 0) { + // read all queue + hasMsg = hasMsgFromTopic(topicConfig, randomQ, requestHeader); + } else { + int queueId = requestHeader.getQueueId(); + hasMsg = hasMsgFromQueue(topicConfig.getTopicName(), requestHeader, queueId); + } + // if it doesn't have message, fetch retry again + if (!needRetry && !hasMsg) { + String retryTopic = KeyBuilder.buildPopRetryTopic(requestHeader.getTopic(), requestHeader.getConsumerGroup(), brokerConfig.isEnableRetryTopicV2()); + hasMsg = hasMsgFromTopic(retryTopic, randomQ, requestHeader); + if (!hasMsg && brokerConfig.isEnableRetryTopicV2() && brokerConfig.isRetrieveMessageFromPopRetryTopicV1()) { + String retryTopicConfigV1 = KeyBuilder.buildPopRetryTopicV1(requestHeader.getTopic(), requestHeader.getConsumerGroup()); + hasMsg = hasMsgFromTopic(retryTopicConfigV1, randomQ, requestHeader); + } + } + } + + if (!hasMsg) { + if (popLongPollingService.polling(ctx, request, new PollingHeader(requestHeader)) == PollingResult.POLLING_SUC) { + return null; + } + } + response.setCode(ResponseCode.SUCCESS); + responseHeader.setHasMsg(hasMsg); + return response; + } + + private boolean hasMsgFromTopic(String topicName, int randomQ, NotificationRequestHeader requestHeader) { + boolean hasMsg; + TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(topicName); + return hasMsgFromTopic(topicConfig, randomQ, requestHeader); + } + + private boolean hasMsgFromTopic(TopicConfig topicConfig, int randomQ, NotificationRequestHeader requestHeader) { + boolean hasMsg; + if (topicConfig != null) { + for (int i = 0; i < topicConfig.getReadQueueNums(); i++) { + int queueId = (randomQ + i) % topicConfig.getReadQueueNums(); + hasMsg = hasMsgFromQueue(topicConfig.getTopicName(), requestHeader, queueId); + if (hasMsg) { + return true; + } + } + } + return false; + } + + private boolean hasMsgFromQueue(String targetTopic, NotificationRequestHeader requestHeader, int queueId) { + if (Boolean.TRUE.equals(requestHeader.getOrder())) { + if (this.brokerController.getConsumerOrderInfoManager().checkBlock(requestHeader.getAttemptId(), requestHeader.getTopic(), requestHeader.getConsumerGroup(), queueId, 0)) { + return false; + } + } + long offset = getPopOffset(targetTopic, requestHeader.getConsumerGroup(), queueId); + long restNum = this.brokerController.getMessageStore().getMaxOffsetInQueue(targetTopic, queueId) - offset; + return restNum > 0; + } + + private long getPopOffset(String topic, String cid, int queueId) { + long offset = this.brokerController.getConsumerOffsetManager().queryOffset(cid, topic, queueId); + if (offset < 0) { + offset = this.brokerController.getMessageStore().getMinOffsetInQueue(topic, queueId); + } + long bufferOffset = this.brokerController.getPopMessageProcessor().getPopBufferMergeService() + .getLatestOffset(topic, cid, queueId); + if (bufferOffset < 0) { + return offset; + } else { + return bufferOffset > offset ? bufferOffset : offset; + } + } + + public PopLongPollingService getPopLongPollingService() { + return popLongPollingService; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PeekMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PeekMessageProcessor.java new file mode 100644 index 00000000000..55552003d80 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PeekMessageProcessor.java @@ -0,0 +1,302 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.processor; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.FileRegion; +import io.opentelemetry.api.common.Attributes; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.metrics.BrokerMetricsManager; +import org.apache.rocketmq.broker.pagecache.ManyMessageTransfer; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.KeyBuilder; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.constant.PermName; +import org.apache.rocketmq.common.help.FAQUrl; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.metrics.RemotingMetricsManager; +import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.PeekMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PopMessageResponseHeader; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.GetMessageStatus; +import org.apache.rocketmq.store.SelectMappedBufferResult; + +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_CONSUMER_GROUP; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_IS_SYSTEM; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_TOPIC; +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.LABEL_REQUEST_CODE; +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.LABEL_RESPONSE_CODE; +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.LABEL_RESULT; + +public class PeekMessageProcessor implements NettyRequestProcessor { + private static final Logger LOG = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + private final BrokerController brokerController; + private Random random = new Random(System.currentTimeMillis()); + + public PeekMessageProcessor(final BrokerController brokerController) { + this.brokerController = brokerController; + } + + @Override + public RemotingCommand processRequest(final ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + return this.processRequest(ctx.channel(), request, true); + } + + @Override + public boolean rejectRequest() { + return false; + } + + private RemotingCommand processRequest(final Channel channel, RemotingCommand request, boolean brokerAllowSuspend) + throws RemotingCommandException { + final long beginTimeMills = this.brokerController.getMessageStore().now(); + RemotingCommand response = RemotingCommand.createResponseCommand(PopMessageResponseHeader.class); + final PopMessageResponseHeader responseHeader = (PopMessageResponseHeader) response.readCustomHeader(); + final PeekMessageRequestHeader requestHeader = + (PeekMessageRequestHeader) request.decodeCommandCustomHeader(PeekMessageRequestHeader.class); + + response.setOpaque(request.getOpaque()); + + if (!PermName.isReadable(this.brokerController.getBrokerConfig().getBrokerPermission())) { + response.setCode(ResponseCode.NO_PERMISSION); + response.setRemark(String.format("the broker[%s] peeking message is forbidden", this.brokerController.getBrokerConfig().getBrokerIP1())); + return response; + } + + TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic()); + if (null == topicConfig) { + LOG.error("The topic {} not exist, consumer: {} ", requestHeader.getTopic(), RemotingHelper.parseChannelRemoteAddr(channel)); + response.setCode(ResponseCode.TOPIC_NOT_EXIST); + response.setRemark(String.format("topic[%s] not exist, apply first please! %s", requestHeader.getTopic(), FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL))); + return response; + } + + if (!PermName.isReadable(topicConfig.getPerm())) { + response.setCode(ResponseCode.NO_PERMISSION); + response.setRemark("the topic[" + requestHeader.getTopic() + "] peeking message is forbidden"); + return response; + } + + if (requestHeader.getQueueId() >= topicConfig.getReadQueueNums()) { + String errorInfo = String.format("queueId[%d] is illegal, topic:[%s] topicConfig.readQueueNums:[%d] consumer:[%s]", + requestHeader.getQueueId(), requestHeader.getTopic(), topicConfig.getReadQueueNums(), channel.remoteAddress()); + LOG.warn(errorInfo); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(errorInfo); + return response; + } + SubscriptionGroupConfig subscriptionGroupConfig = this.brokerController.getSubscriptionGroupManager().findSubscriptionGroupConfig(requestHeader.getConsumerGroup()); + if (null == subscriptionGroupConfig) { + response.setCode(ResponseCode.SUBSCRIPTION_GROUP_NOT_EXIST); + response.setRemark(String.format("subscription group [%s] does not exist, %s", requestHeader.getConsumerGroup(), FAQUrl.suggestTodo(FAQUrl.SUBSCRIPTION_GROUP_NOT_EXIST))); + return response; + } + + if (!subscriptionGroupConfig.isConsumeEnable()) { + response.setCode(ResponseCode.NO_PERMISSION); + response.setRemark("subscription group no permission, " + requestHeader.getConsumerGroup()); + return response; + } + int randomQ = random.nextInt(100); + int reviveQid = randomQ % this.brokerController.getBrokerConfig().getReviveQueueNum(); + GetMessageResult getMessageResult = new GetMessageResult(requestHeader.getMaxMsgNums()); + boolean needRetry = randomQ % 5 == 0; + long popTime = System.currentTimeMillis(); + long restNum = 0; + BrokerConfig brokerConfig = brokerController.getBrokerConfig(); + if (needRetry) { + TopicConfig retryTopicConfig = this.brokerController.getTopicConfigManager() + .selectTopicConfig(KeyBuilder.buildPopRetryTopic(requestHeader.getTopic(), requestHeader.getConsumerGroup(), brokerConfig.isEnableRetryTopicV2())); + if (retryTopicConfig != null) { + for (int i = 0; i < retryTopicConfig.getReadQueueNums(); i++) { + int queueId = (randomQ + i) % retryTopicConfig.getReadQueueNums(); + restNum = peekMsgFromQueue(true, getMessageResult, requestHeader, queueId, restNum, reviveQid, channel, popTime); + } + } + } + if (requestHeader.getQueueId() < 0) { + // read all queue + for (int i = 0; i < topicConfig.getReadQueueNums(); i++) { + int queueId = (randomQ + i) % topicConfig.getReadQueueNums(); + restNum = peekMsgFromQueue(false, getMessageResult, requestHeader, queueId, restNum, reviveQid, channel, popTime); + } + } else { + int queueId = requestHeader.getQueueId(); + restNum = peekMsgFromQueue(false, getMessageResult, requestHeader, queueId, restNum, reviveQid, channel, popTime); + } + // if not full , fetch retry again + if (!needRetry && getMessageResult.getMessageMapedList().size() < requestHeader.getMaxMsgNums()) { + TopicConfig retryTopicConfig = this.brokerController.getTopicConfigManager() + .selectTopicConfig(KeyBuilder.buildPopRetryTopic(requestHeader.getTopic(), requestHeader.getConsumerGroup(), brokerConfig.isEnableRetryTopicV2())); + if (retryTopicConfig != null) { + for (int i = 0; i < retryTopicConfig.getReadQueueNums(); i++) { + int queueId = (randomQ + i) % retryTopicConfig.getReadQueueNums(); + restNum = peekMsgFromQueue(true, getMessageResult, requestHeader, queueId, restNum, reviveQid, channel, popTime); + } + } + } + if (!getMessageResult.getMessageBufferList().isEmpty()) { + response.setCode(ResponseCode.SUCCESS); + getMessageResult.setStatus(GetMessageStatus.FOUND); + } else { + response.setCode(ResponseCode.PULL_NOT_FOUND); + getMessageResult.setStatus(GetMessageStatus.NO_MESSAGE_IN_QUEUE); + + } + responseHeader.setRestNum(restNum); + response.setRemark(getMessageResult.getStatus().name()); + switch (response.getCode()) { + case ResponseCode.SUCCESS: + + this.brokerController.getBrokerStatsManager().incGroupGetNums(requestHeader.getConsumerGroup(), requestHeader.getTopic(), + getMessageResult.getMessageCount()); + + this.brokerController.getBrokerStatsManager().incGroupGetSize(requestHeader.getConsumerGroup(), requestHeader.getTopic(), + getMessageResult.getBufferTotalSize()); + + this.brokerController.getBrokerStatsManager().incBrokerGetNums(requestHeader.getTopic(), getMessageResult.getMessageCount()); + + if (this.brokerController.getBrokerConfig().isTransferMsgByHeap()) { + final byte[] r = this.readGetMessageResult(getMessageResult, requestHeader.getConsumerGroup(), requestHeader.getTopic(), requestHeader.getQueueId()); + this.brokerController.getBrokerStatsManager().incGroupGetLatency(requestHeader.getConsumerGroup(), + requestHeader.getTopic(), requestHeader.getQueueId(), + (int) (this.brokerController.getMessageStore().now() - beginTimeMills)); + response.setBody(r); + } else { + final GetMessageResult tmpGetMessageResult = getMessageResult; + try { + FileRegion fileRegion = + new ManyMessageTransfer(response.encodeHeader(getMessageResult.getBufferTotalSize()), getMessageResult); + RemotingCommand finalResponse = response; + channel.writeAndFlush(fileRegion) + .addListener((ChannelFutureListener) future -> { + tmpGetMessageResult.release(); + Attributes attributes = RemotingMetricsManager.newAttributesBuilder() + .put(LABEL_REQUEST_CODE, RemotingHelper.getRequestCodeDesc(request.getCode())) + .put(LABEL_RESPONSE_CODE, RemotingHelper.getResponseCodeDesc(finalResponse.getCode())) + .put(LABEL_RESULT, RemotingMetricsManager.getWriteAndFlushResult(future)) + .build(); + RemotingMetricsManager.rpcLatency.record(request.getProcessTimer().elapsed(TimeUnit.MILLISECONDS), attributes); + if (!future.isSuccess()) { + LOG.error("Fail to transfer messages from page cache to {}", channel.remoteAddress(), future.cause()); + } + }); + } catch (Throwable e) { + LOG.error("Error occurred when transferring messages from page cache", e); + getMessageResult.release(); + } + + response = null; + } + break; + default: + assert false; + } + return response; + } + + private long peekMsgFromQueue(boolean isRetry, GetMessageResult getMessageResult, + PeekMessageRequestHeader requestHeader, int queueId, long restNum, int reviveQid, Channel channel, + long popTime) { + String topic = isRetry ? + KeyBuilder.buildPopRetryTopic(requestHeader.getTopic(), requestHeader.getConsumerGroup(), brokerController.getBrokerConfig().isEnableRetryTopicV2()) + : requestHeader.getTopic(); + GetMessageResult getMessageTmpResult; + long offset = getPopOffset(topic, requestHeader.getConsumerGroup(), queueId); + restNum = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId) - offset + restNum; + if (getMessageResult.getMessageMapedList().size() >= requestHeader.getMaxMsgNums()) { + return restNum; + } + getMessageTmpResult = this.brokerController.getMessageStore().getMessage(requestHeader.getConsumerGroup(), topic, queueId, offset, + requestHeader.getMaxMsgNums() - getMessageResult.getMessageMapedList().size(), null); + // maybe store offset is not correct. + if (GetMessageStatus.OFFSET_TOO_SMALL.equals(getMessageTmpResult.getStatus()) || GetMessageStatus.OFFSET_OVERFLOW_BADLY.equals(getMessageTmpResult.getStatus())) { + offset = getMessageTmpResult.getNextBeginOffset(); + getMessageTmpResult = this.brokerController.getMessageStore().getMessage(requestHeader.getConsumerGroup(), topic, queueId, offset, + requestHeader.getMaxMsgNums() - getMessageResult.getMessageMapedList().size(), null); + } + if (getMessageTmpResult != null) { + if (!getMessageTmpResult.getMessageMapedList().isEmpty() && !isRetry) { + Attributes attributes = BrokerMetricsManager.newAttributesBuilder() + .put(LABEL_TOPIC, requestHeader.getTopic()) + .put(LABEL_CONSUMER_GROUP, requestHeader.getConsumerGroup()) + .put(LABEL_IS_SYSTEM, TopicValidator.isSystemTopic(requestHeader.getTopic()) || MixAll.isSysConsumerGroup(requestHeader.getConsumerGroup())) + .build(); + BrokerMetricsManager.messagesOutTotal.add(getMessageResult.getMessageCount(), attributes); + BrokerMetricsManager.throughputOutTotal.add(getMessageResult.getBufferTotalSize(), attributes); + } + + for (SelectMappedBufferResult mapedBuffer : getMessageTmpResult.getMessageMapedList()) { + getMessageResult.addMessage(mapedBuffer); + } + } + return restNum; + } + + private long getPopOffset(String topic, String cid, int queueId) { + long offset = this.brokerController.getConsumerOffsetManager().queryOffset(cid, topic, queueId); + if (offset < 0) { + offset = this.brokerController.getMessageStore().getMinOffsetInQueue(topic, queueId); + } + long bufferOffset = this.brokerController.getPopMessageProcessor().getPopBufferMergeService() + .getLatestOffset(topic, cid, queueId); + if (bufferOffset < 0) { + return offset; + } else { + return bufferOffset > offset ? bufferOffset : offset; + } + } + + private byte[] readGetMessageResult(final GetMessageResult getMessageResult, final String group, final String topic, + final int queueId) { + final ByteBuffer byteBuffer = ByteBuffer.allocate(getMessageResult.getBufferTotalSize()); + + long storeTimestamp = 0; + try { + List messageBufferList = getMessageResult.getMessageBufferList(); + for (ByteBuffer bb : messageBufferList) { + + byteBuffer.put(bb); + storeTimestamp = bb.getLong(MessageDecoder.MESSAGE_STORE_TIMESTAMP_POSITION); + } + } finally { + getMessageResult.release(); + } + + this.brokerController.getBrokerStatsManager().recordDiskFallBehindTime(group, topic, queueId, this.brokerController.getMessageStore().now() - storeTimestamp); + return byteBuffer.array(); + } + +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PollingInfoProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PollingInfoProcessor.java new file mode 100644 index 00000000000..65a4d7d7851 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PollingInfoProcessor.java @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.processor; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import java.util.concurrent.ConcurrentSkipListSet; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.longpolling.PopRequest; +import org.apache.rocketmq.common.KeyBuilder; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.constant.PermName; +import org.apache.rocketmq.common.help.FAQUrl; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.PollingInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PollingInfoResponseHeader; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; + +public class PollingInfoProcessor implements NettyRequestProcessor { + private static final Logger POP_LOGGER = LoggerFactory.getLogger(LoggerName.ROCKETMQ_POP_LOGGER_NAME); + private final BrokerController brokerController; + + public PollingInfoProcessor(final BrokerController brokerController) { + this.brokerController = brokerController; + } + + @Override + public RemotingCommand processRequest(final ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + return this.processRequest(ctx.channel(), request); + } + + @Override + public boolean rejectRequest() { + return false; + } + + private RemotingCommand processRequest(final Channel channel, RemotingCommand request) + throws RemotingCommandException { + RemotingCommand response = RemotingCommand.createResponseCommand(PollingInfoResponseHeader.class); + final PollingInfoResponseHeader responseHeader = (PollingInfoResponseHeader) response.readCustomHeader(); + final PollingInfoRequestHeader requestHeader = + (PollingInfoRequestHeader) request.decodeCommandCustomHeader(PollingInfoRequestHeader.class); + + response.setOpaque(request.getOpaque()); + + if (!PermName.isReadable(this.brokerController.getBrokerConfig().getBrokerPermission())) { + response.setCode(ResponseCode.NO_PERMISSION); + response.setRemark(String.format("the broker[%s] peeking message is forbidden", this.brokerController.getBrokerConfig().getBrokerIP1())); + return response; + } + + TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic()); + if (null == topicConfig) { + POP_LOGGER.error("The topic {} not exist, consumer: {} ", requestHeader.getTopic(), RemotingHelper.parseChannelRemoteAddr(channel)); + response.setCode(ResponseCode.TOPIC_NOT_EXIST); + response.setRemark(String.format("topic[%s] not exist, apply first please! %s", requestHeader.getTopic(), FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL))); + return response; + } + + if (!PermName.isReadable(topicConfig.getPerm())) { + response.setCode(ResponseCode.NO_PERMISSION); + response.setRemark("the topic[" + requestHeader.getTopic() + "] peeking message is forbidden"); + return response; + } + + if (requestHeader.getQueueId() >= topicConfig.getReadQueueNums()) { + String errorInfo = String.format("queueId[%d] is illegal, topic:[%s] topicConfig.readQueueNums:[%d] consumer:[%s]", + requestHeader.getQueueId(), requestHeader.getTopic(), topicConfig.getReadQueueNums(), channel.remoteAddress()); + POP_LOGGER.warn(errorInfo); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(errorInfo); + return response; + } + SubscriptionGroupConfig subscriptionGroupConfig = this.brokerController.getSubscriptionGroupManager().findSubscriptionGroupConfig(requestHeader.getConsumerGroup()); + if (null == subscriptionGroupConfig) { + response.setCode(ResponseCode.SUBSCRIPTION_GROUP_NOT_EXIST); + response.setRemark(String.format("subscription group [%s] does not exist, %s", requestHeader.getConsumerGroup(), FAQUrl.suggestTodo(FAQUrl.SUBSCRIPTION_GROUP_NOT_EXIST))); + return response; + } + + if (!subscriptionGroupConfig.isConsumeEnable()) { + response.setCode(ResponseCode.NO_PERMISSION); + response.setRemark("subscription group no permission, " + requestHeader.getConsumerGroup()); + return response; + } + String key = KeyBuilder.buildPollingKey(requestHeader.getTopic(), requestHeader.getConsumerGroup(), requestHeader.getQueueId()); + ConcurrentSkipListSet queue = this.brokerController.getPopMessageProcessor().getPollingMap().get(key); + if (queue != null) { + responseHeader.setPollingNum(queue.size()); + } else { + responseHeader.setPollingNum(0); + } + response.setCode(ResponseCode.SUCCESS); + return response; + } + +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java new file mode 100644 index 00000000000..8a85dd8fec8 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopBufferMergeService.java @@ -0,0 +1,881 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.processor; + +import com.alibaba.fastjson.JSON; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.metrics.PopMetricsManager; +import org.apache.rocketmq.common.KeyBuilder; +import org.apache.rocketmq.common.PopAckConstants; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.utils.DataConverter; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.config.BrokerRole; +import org.apache.rocketmq.store.pop.AckMsg; +import org.apache.rocketmq.store.pop.BatchAckMsg; +import org.apache.rocketmq.store.pop.PopCheckPoint; + +public class PopBufferMergeService extends ServiceThread { + private static final Logger POP_LOGGER = LoggerFactory.getLogger(LoggerName.ROCKETMQ_POP_LOGGER_NAME); + ConcurrentHashMap + buffer = new ConcurrentHashMap<>(1024 * 16); + ConcurrentHashMap> commitOffsets = + new ConcurrentHashMap<>(); + private volatile boolean serving = true; + private AtomicInteger counter = new AtomicInteger(0); + private int scanTimes = 0; + private final BrokerController brokerController; + private final PopMessageProcessor popMessageProcessor; + private final PopMessageProcessor.QueueLockManager queueLockManager; + private final long interval = 5; + private final long minute5 = 5 * 60 * 1000; + private final int countOfMinute1 = (int) (60 * 1000 / interval); + private final int countOfSecond1 = (int) (1000 / interval); + private final int countOfSecond30 = (int) (30 * 1000 / interval); + + private final List batchAckIndexList = new ArrayList(32); + private volatile boolean master = false; + + public PopBufferMergeService(BrokerController brokerController, PopMessageProcessor popMessageProcessor) { + this.brokerController = brokerController; + this.popMessageProcessor = popMessageProcessor; + this.queueLockManager = popMessageProcessor.getQueueLockManager(); + } + + private boolean isShouldRunning() { + if (this.brokerController.getBrokerConfig().isEnableSlaveActingMaster()) { + return true; + } + this.master = brokerController.getMessageStoreConfig().getBrokerRole() != BrokerRole.SLAVE; + return this.master; + } + + @Override + public String getServiceName() { + if (this.brokerController != null && this.brokerController.getBrokerConfig().isInBrokerContainer()) { + return brokerController.getBrokerIdentity().getIdentifier() + PopBufferMergeService.class.getSimpleName(); + } + return PopBufferMergeService.class.getSimpleName(); + } + + @Override + public void run() { + // scan + while (!this.isStopped()) { + try { + if (!isShouldRunning()) { + // slave + this.waitForRunning(interval * 200 * 5); + POP_LOGGER.info("Broker is {}, {}, clear all data", + brokerController.getMessageStoreConfig().getBrokerRole(), this.master); + this.buffer.clear(); + this.commitOffsets.clear(); + continue; + } + + scan(); + if (scanTimes % countOfSecond30 == 0) { + scanGarbage(); + } + + this.waitForRunning(interval); + + if (!this.serving && this.buffer.size() == 0 && getOffsetTotalSize() == 0) { + this.serving = true; + } + } catch (Throwable e) { + POP_LOGGER.error("PopBufferMergeService error", e); + this.waitForRunning(3000); + } + } + + this.serving = false; + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + } + if (!isShouldRunning()) { + return; + } + while (this.buffer.size() > 0 || getOffsetTotalSize() > 0) { + scan(); + } + } + + private int scanCommitOffset() { + Iterator>> iterator = this.commitOffsets.entrySet().iterator(); + int count = 0; + while (iterator.hasNext()) { + Map.Entry> entry = iterator.next(); + LinkedBlockingDeque queue = entry.getValue().get(); + PopCheckPointWrapper pointWrapper; + while ((pointWrapper = queue.peek()) != null) { + // 1. just offset & stored, not processed by scan + // 2. ck is buffer(acked) + // 3. ck is buffer(not all acked), all ak are stored and ck is stored + if (pointWrapper.isJustOffset() && pointWrapper.isCkStored() || isCkDone(pointWrapper) + || isCkDoneForFinish(pointWrapper) && pointWrapper.isCkStored()) { + if (commitOffset(pointWrapper)) { + queue.poll(); + } else { + break; + } + } else { + if (System.currentTimeMillis() - pointWrapper.getCk().getPopTime() + > brokerController.getBrokerConfig().getPopCkStayBufferTime() * 2) { + POP_LOGGER.warn("[PopBuffer] ck offset long time not commit, {}", pointWrapper); + } + break; + } + } + final int qs = queue.size(); + count += qs; + if (qs > 5000 && scanTimes % countOfSecond1 == 0) { + POP_LOGGER.info("[PopBuffer] offset queue size too long, {}, {}", + entry.getKey(), qs); + } + } + return count; + } + + public long getLatestOffset(String lockKey) { + QueueWithTime queue = this.commitOffsets.get(lockKey); + if (queue == null) { + return -1; + } + PopCheckPointWrapper pointWrapper = queue.get().peekLast(); + if (pointWrapper != null) { + return pointWrapper.getNextBeginOffset(); + } + return -1; + } + + public long getLatestOffset(String topic, String group, int queueId) { + return getLatestOffset(KeyBuilder.buildPollingKey(topic, group, queueId)); + } + + private void scanGarbage() { + Iterator>> iterator = commitOffsets.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry> entry = iterator.next(); + if (entry.getKey() == null) { + continue; + } + String[] keyArray = entry.getKey().split(PopAckConstants.SPLIT); + if (keyArray == null || keyArray.length != 3) { + continue; + } + String topic = keyArray[0]; + String cid = keyArray[1]; + if (brokerController.getTopicConfigManager().selectTopicConfig(topic) == null) { + POP_LOGGER.info("[PopBuffer]remove not exit topic {} in buffer!", topic); + iterator.remove(); + continue; + } + if (!brokerController.getSubscriptionGroupManager().getSubscriptionGroupTable().containsKey(cid)) { + POP_LOGGER.info("[PopBuffer]remove not exit sub {} of topic {} in buffer!", cid, topic); + iterator.remove(); + continue; + } + if (System.currentTimeMillis() - entry.getValue().getTime() > minute5) { + POP_LOGGER.info("[PopBuffer]remove long time not used sub {} of topic {} in buffer!", cid, topic); + iterator.remove(); + continue; + } + } + } + + private void scan() { + long startTime = System.currentTimeMillis(); + int count = 0, countCk = 0; + Iterator> iterator = buffer.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + PopCheckPointWrapper pointWrapper = entry.getValue(); + + // just process offset(already stored at pull thread), or buffer ck(not stored and ack finish) + if (pointWrapper.isJustOffset() && pointWrapper.isCkStored() || isCkDone(pointWrapper) + || isCkDoneForFinish(pointWrapper) && pointWrapper.isCkStored()) { + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("[PopBuffer]ck done, {}", pointWrapper); + } + iterator.remove(); + counter.decrementAndGet(); + continue; + } + + PopCheckPoint point = pointWrapper.getCk(); + long now = System.currentTimeMillis(); + + boolean removeCk = !this.serving; + // ck will be timeout + if (point.getReviveTime() - now < brokerController.getBrokerConfig().getPopCkStayBufferTimeOut()) { + removeCk = true; + } + + // the time stayed is too long + if (now - point.getPopTime() > brokerController.getBrokerConfig().getPopCkStayBufferTime()) { + removeCk = true; + } + + if (now - point.getPopTime() > brokerController.getBrokerConfig().getPopCkStayBufferTime() * 2L) { + POP_LOGGER.warn("[PopBuffer]ck finish fail, stay too long, {}", pointWrapper); + } + + // double check + if (isCkDone(pointWrapper)) { + continue; + } else if (pointWrapper.isJustOffset()) { + // just offset should be in store. + if (pointWrapper.getReviveQueueOffset() < 0) { + putCkToStore(pointWrapper, false); + countCk++; + } + continue; + } else if (removeCk) { + // put buffer ak to store + if (pointWrapper.getReviveQueueOffset() < 0) { + putCkToStore(pointWrapper, false); + countCk++; + } + + if (!pointWrapper.isCkStored()) { + continue; + } + + if (brokerController.getBrokerConfig().isEnablePopBatchAck()) { + List indexList = this.batchAckIndexList; + try { + for (byte i = 0; i < point.getNum(); i++) { + // reput buffer ak to store + if (DataConverter.getBit(pointWrapper.getBits().get(), i) + && !DataConverter.getBit(pointWrapper.getToStoreBits().get(), i)) { + indexList.add(i); + } + } + if (indexList.size() > 0) { + if (putBatchAckToStore(pointWrapper, indexList)) { + count += indexList.size(); + for (Byte i : indexList) { + markBitCAS(pointWrapper.getToStoreBits(), i); + } + } + } + } finally { + indexList.clear(); + } + } else { + for (byte i = 0; i < point.getNum(); i++) { + // reput buffer ak to store + if (DataConverter.getBit(pointWrapper.getBits().get(), i) + && !DataConverter.getBit(pointWrapper.getToStoreBits().get(), i)) { + if (putAckToStore(pointWrapper, i)) { + count++; + markBitCAS(pointWrapper.getToStoreBits(), i); + } + } + } + } + + if (isCkDoneForFinish(pointWrapper) && pointWrapper.isCkStored()) { + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("[PopBuffer]ck finish, {}", pointWrapper); + } + iterator.remove(); + counter.decrementAndGet(); + continue; + } + } + } + + int offsetBufferSize = scanCommitOffset(); + + long eclipse = System.currentTimeMillis() - startTime; + if (eclipse > brokerController.getBrokerConfig().getPopCkStayBufferTimeOut() - 1000) { + POP_LOGGER.warn("[PopBuffer]scan stop, because eclipse too long, PopBufferEclipse={}, " + + "PopBufferToStoreAck={}, PopBufferToStoreCk={}, PopBufferSize={}, PopBufferOffsetSize={}", + eclipse, count, countCk, counter.get(), offsetBufferSize); + this.serving = false; + } else { + if (scanTimes % countOfSecond1 == 0) { + POP_LOGGER.info("[PopBuffer]scan, PopBufferEclipse={}, " + + "PopBufferToStoreAck={}, PopBufferToStoreCk={}, PopBufferSize={}, PopBufferOffsetSize={}", + eclipse, count, countCk, counter.get(), offsetBufferSize); + } + } + PopMetricsManager.recordPopBufferScanTimeConsume(eclipse); + scanTimes++; + + if (scanTimes >= countOfMinute1) { + counter.set(this.buffer.size()); + scanTimes = 0; + } + } + + public int getOffsetTotalSize() { + int count = 0; + Iterator>> iterator = this.commitOffsets.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry> entry = iterator.next(); + LinkedBlockingDeque queue = entry.getValue().get(); + count += queue.size(); + } + return count; + } + + public int getBufferedCKSize() { + return this.counter.get(); + } + + private void markBitCAS(AtomicInteger setBits, int index) { + while (true) { + int bits = setBits.get(); + if (DataConverter.getBit(bits, index)) { + break; + } + + int newBits = DataConverter.setBit(bits, index, true); + if (setBits.compareAndSet(bits, newBits)) { + break; + } + } + } + + private boolean commitOffset(final PopCheckPointWrapper wrapper) { + if (wrapper.getNextBeginOffset() < 0) { + return true; + } + + final PopCheckPoint popCheckPoint = wrapper.getCk(); + final String lockKey = wrapper.getLockKey(); + + if (!queueLockManager.tryLock(lockKey)) { + return false; + } + try { + final long offset = brokerController.getConsumerOffsetManager().queryOffset(popCheckPoint.getCId(), popCheckPoint.getTopic(), popCheckPoint.getQueueId()); + if (wrapper.getNextBeginOffset() > offset) { + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("Commit offset, {}, {}", wrapper, offset); + } + } else { + // maybe store offset is not correct. + POP_LOGGER.warn("Commit offset, consumer offset less than store, {}, {}", wrapper, offset); + } + brokerController.getConsumerOffsetManager().commitOffset(getServiceName(), + popCheckPoint.getCId(), popCheckPoint.getTopic(), popCheckPoint.getQueueId(), wrapper.getNextBeginOffset()); + } finally { + queueLockManager.unLock(lockKey); + } + return true; + } + + private boolean putOffsetQueue(PopCheckPointWrapper pointWrapper) { + QueueWithTime queue = this.commitOffsets.get(pointWrapper.getLockKey()); + if (queue == null) { + queue = new QueueWithTime<>(); + QueueWithTime old = this.commitOffsets.putIfAbsent(pointWrapper.getLockKey(), queue); + if (old != null) { + queue = old; + } + } + queue.setTime(pointWrapper.getCk().getPopTime()); + return queue.get().offer(pointWrapper); + } + + private boolean checkQueueOk(PopCheckPointWrapper pointWrapper) { + QueueWithTime queue = this.commitOffsets.get(pointWrapper.getLockKey()); + if (queue == null) { + return true; + } + return queue.get().size() < brokerController.getBrokerConfig().getPopCkOffsetMaxQueueSize(); + } + + /** + * put to store && add to buffer. + * + * @param point + * @param reviveQueueId + * @param reviveQueueOffset + * @param nextBeginOffset + * @return + */ + public boolean addCkJustOffset(PopCheckPoint point, int reviveQueueId, long reviveQueueOffset, long nextBeginOffset) { + PopCheckPointWrapper pointWrapper = new PopCheckPointWrapper(reviveQueueId, reviveQueueOffset, point, nextBeginOffset, true); + + if (this.buffer.containsKey(pointWrapper.getMergeKey())) { + // when mergeKey conflict + // will cause PopBufferMergeService.scanCommitOffset cannot poll PopCheckPointWrapper + POP_LOGGER.warn("[PopBuffer]mergeKey conflict when add ckJustOffset. ck:{}, mergeKey:{}", pointWrapper, pointWrapper.getMergeKey()); + return false; + } + + this.putCkToStore(pointWrapper, !checkQueueOk(pointWrapper)); + + putOffsetQueue(pointWrapper); + this.buffer.put(pointWrapper.getMergeKey(), pointWrapper); + this.counter.incrementAndGet(); + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("[PopBuffer]add ck just offset, {}", pointWrapper); + } + return true; + } + + public void addCkMock(String group, String topic, int queueId, long startOffset, long invisibleTime, + long popTime, int reviveQueueId, long nextBeginOffset, String brokerName) { + final PopCheckPoint ck = new PopCheckPoint(); + ck.setBitMap(0); + ck.setNum((byte) 0); + ck.setPopTime(popTime); + ck.setInvisibleTime(invisibleTime); + ck.setStartOffset(startOffset); + ck.setCId(group); + ck.setTopic(topic); + ck.setQueueId(queueId); + ck.setBrokerName(brokerName); + + PopCheckPointWrapper pointWrapper = new PopCheckPointWrapper(reviveQueueId, Long.MAX_VALUE, ck, nextBeginOffset, true); + pointWrapper.setCkStored(true); + + putOffsetQueue(pointWrapper); + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("[PopBuffer]add ck just offset, mocked, {}", pointWrapper); + } + } + + public boolean addCk(PopCheckPoint point, int reviveQueueId, long reviveQueueOffset, long nextBeginOffset) { + // key: point.getT() + point.getC() + point.getQ() + point.getSo() + point.getPt() + if (!brokerController.getBrokerConfig().isEnablePopBufferMerge()) { + return false; + } + if (!serving) { + return false; + } + + long now = System.currentTimeMillis(); + if (point.getReviveTime() - now < brokerController.getBrokerConfig().getPopCkStayBufferTimeOut() + 1500) { + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.warn("[PopBuffer]add ck, timeout, {}, {}", point, now); + } + return false; + } + + if (this.counter.get() > brokerController.getBrokerConfig().getPopCkMaxBufferSize()) { + POP_LOGGER.warn("[PopBuffer]add ck, max size, {}, {}", point, this.counter.get()); + return false; + } + + PopCheckPointWrapper pointWrapper = new PopCheckPointWrapper(reviveQueueId, reviveQueueOffset, point, nextBeginOffset); + + if (!checkQueueOk(pointWrapper)) { + return false; + } + + if (this.buffer.containsKey(pointWrapper.getMergeKey())) { + // when mergeKey conflict + // will cause PopBufferMergeService.scanCommitOffset cannot poll PopCheckPointWrapper + POP_LOGGER.warn("[PopBuffer]mergeKey conflict when add ck. ck:{}, mergeKey:{}", pointWrapper, pointWrapper.getMergeKey()); + return false; + } + + putOffsetQueue(pointWrapper); + this.buffer.put(pointWrapper.getMergeKey(), pointWrapper); + this.counter.incrementAndGet(); + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("[PopBuffer]add ck, {}", pointWrapper); + } + return true; + } + + public boolean addAk(int reviveQid, AckMsg ackMsg) { + if (!brokerController.getBrokerConfig().isEnablePopBufferMerge()) { + return false; + } + if (!serving) { + return false; + } + try { + PopCheckPointWrapper pointWrapper = this.buffer.get(ackMsg.getTopic() + ackMsg.getConsumerGroup() + ackMsg.getQueueId() + ackMsg.getStartOffset() + ackMsg.getPopTime() + ackMsg.getBrokerName()); + if (pointWrapper == null) { + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.warn("[PopBuffer]add ack fail, rqId={}, no ck, {}", reviveQid, ackMsg); + } + return false; + } + + if (pointWrapper.isJustOffset()) { + return false; + } + + PopCheckPoint point = pointWrapper.getCk(); + long now = System.currentTimeMillis(); + + if (point.getReviveTime() - now < brokerController.getBrokerConfig().getPopCkStayBufferTimeOut() + 1500) { + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.warn("[PopBuffer]add ack fail, rqId={}, almost timeout for revive, {}, {}, {}", reviveQid, pointWrapper, ackMsg, now); + } + return false; + } + + if (now - point.getPopTime() > brokerController.getBrokerConfig().getPopCkStayBufferTime() - 1500) { + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.warn("[PopBuffer]add ack fail, rqId={}, stay too long, {}, {}, {}", reviveQid, pointWrapper, ackMsg, now); + } + return false; + } + + if (ackMsg instanceof BatchAckMsg) { + for (Long ackOffset : ((BatchAckMsg) ackMsg).getAckOffsetList()) { + int indexOfAck = point.indexOfAck(ackOffset); + if (indexOfAck > -1) { + markBitCAS(pointWrapper.getBits(), indexOfAck); + } else { + POP_LOGGER.error("[PopBuffer]Invalid index of ack, reviveQid={}, {}, {}", reviveQid, ackMsg, point); + } + } + } else { + int indexOfAck = point.indexOfAck(ackMsg.getAckOffset()); + if (indexOfAck > -1) { + markBitCAS(pointWrapper.getBits(), indexOfAck); + } else { + POP_LOGGER.error("[PopBuffer]Invalid index of ack, reviveQid={}, {}, {}", reviveQid, ackMsg, point); + return true; + } + } + + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("[PopBuffer]add ack, rqId={}, {}, {}", reviveQid, pointWrapper, ackMsg); + } + +// // check ak done +// if (isCkDone(pointWrapper)) { +// // cancel ck for timer +// cancelCkTimer(pointWrapper); +// } + return true; + } catch (Throwable e) { + POP_LOGGER.error("[PopBuffer]add ack error, rqId=" + reviveQid + ", " + ackMsg, e); + } + + return false; + } + + public void clearOffsetQueue(String lockKey) { + this.commitOffsets.remove(lockKey); + } + + private void putCkToStore(final PopCheckPointWrapper pointWrapper, final boolean runInCurrent) { + if (pointWrapper.getReviveQueueOffset() >= 0) { + return; + } + MessageExtBrokerInner msgInner = popMessageProcessor.buildCkMsg(pointWrapper.getCk(), pointWrapper.getReviveQueueId()); + PutMessageResult putMessageResult = brokerController.getEscapeBridge().putMessageToSpecificQueue(msgInner); + PopMetricsManager.incPopReviveCkPutCount(pointWrapper.getCk(), putMessageResult.getPutMessageStatus()); + if (putMessageResult.getPutMessageStatus() != PutMessageStatus.PUT_OK + && putMessageResult.getPutMessageStatus() != PutMessageStatus.FLUSH_DISK_TIMEOUT + && putMessageResult.getPutMessageStatus() != PutMessageStatus.FLUSH_SLAVE_TIMEOUT + && putMessageResult.getPutMessageStatus() != PutMessageStatus.SLAVE_NOT_AVAILABLE) { + POP_LOGGER.error("[PopBuffer]put ck to store fail: {}, {}", pointWrapper, putMessageResult); + return; + } + pointWrapper.setCkStored(true); + + if (putMessageResult.isRemotePut()) { + //No AppendMessageResult when escaping remotely + pointWrapper.setReviveQueueOffset(0); + } else { + pointWrapper.setReviveQueueOffset(putMessageResult.getAppendMessageResult().getLogicsOffset()); + } + + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("[PopBuffer]put ck to store ok: {}, {}", pointWrapper, putMessageResult); + } + } + + private boolean putAckToStore(final PopCheckPointWrapper pointWrapper, byte msgIndex) { + PopCheckPoint point = pointWrapper.getCk(); + MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); + final AckMsg ackMsg = new AckMsg(); + + ackMsg.setAckOffset(point.ackOffsetByIndex(msgIndex)); + ackMsg.setStartOffset(point.getStartOffset()); + ackMsg.setConsumerGroup(point.getCId()); + ackMsg.setTopic(point.getTopic()); + ackMsg.setQueueId(point.getQueueId()); + ackMsg.setPopTime(point.getPopTime()); + msgInner.setTopic(popMessageProcessor.reviveTopic); + msgInner.setBody(JSON.toJSONString(ackMsg).getBytes(DataConverter.CHARSET_UTF8)); + msgInner.setQueueId(pointWrapper.getReviveQueueId()); + msgInner.setTags(PopAckConstants.ACK_TAG); + msgInner.setBornTimestamp(System.currentTimeMillis()); + msgInner.setBornHost(brokerController.getStoreHost()); + msgInner.setStoreHost(brokerController.getStoreHost()); + msgInner.setDeliverTimeMs(point.getReviveTime()); + msgInner.getProperties().put(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, PopMessageProcessor.genAckUniqueId(ackMsg)); + + msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); + PutMessageResult putMessageResult = brokerController.getEscapeBridge().putMessageToSpecificQueue(msgInner); + PopMetricsManager.incPopReviveAckPutCount(ackMsg, putMessageResult.getPutMessageStatus()); + if (putMessageResult.getPutMessageStatus() != PutMessageStatus.PUT_OK + && putMessageResult.getPutMessageStatus() != PutMessageStatus.FLUSH_DISK_TIMEOUT + && putMessageResult.getPutMessageStatus() != PutMessageStatus.FLUSH_SLAVE_TIMEOUT + && putMessageResult.getPutMessageStatus() != PutMessageStatus.SLAVE_NOT_AVAILABLE) { + POP_LOGGER.error("[PopBuffer]put ack to store fail: {}, {}, {}", pointWrapper, ackMsg, putMessageResult); + return false; + } + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("[PopBuffer]put ack to store ok: {}, {}, {}", pointWrapper, ackMsg, putMessageResult); + } + + return true; + } + + private boolean putBatchAckToStore(final PopCheckPointWrapper pointWrapper, final List msgIndexList) { + PopCheckPoint point = pointWrapper.getCk(); + MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); + final BatchAckMsg batchAckMsg = new BatchAckMsg(); + + for (Byte msgIndex : msgIndexList) { + batchAckMsg.getAckOffsetList().add(point.ackOffsetByIndex(msgIndex)); + } + batchAckMsg.setStartOffset(point.getStartOffset()); + batchAckMsg.setConsumerGroup(point.getCId()); + batchAckMsg.setTopic(point.getTopic()); + batchAckMsg.setQueueId(point.getQueueId()); + batchAckMsg.setPopTime(point.getPopTime()); + msgInner.setTopic(popMessageProcessor.reviveTopic); + msgInner.setBody(JSON.toJSONString(batchAckMsg).getBytes(DataConverter.CHARSET_UTF8)); + msgInner.setQueueId(pointWrapper.getReviveQueueId()); + msgInner.setTags(PopAckConstants.BATCH_ACK_TAG); + msgInner.setBornTimestamp(System.currentTimeMillis()); + msgInner.setBornHost(brokerController.getStoreHost()); + msgInner.setStoreHost(brokerController.getStoreHost()); + msgInner.setDeliverTimeMs(point.getReviveTime()); + msgInner.getProperties().put(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, PopMessageProcessor.genBatchAckUniqueId(batchAckMsg)); + + msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); + PutMessageResult putMessageResult = brokerController.getEscapeBridge().putMessageToSpecificQueue(msgInner); + if (putMessageResult.getPutMessageStatus() != PutMessageStatus.PUT_OK + && putMessageResult.getPutMessageStatus() != PutMessageStatus.FLUSH_DISK_TIMEOUT + && putMessageResult.getPutMessageStatus() != PutMessageStatus.FLUSH_SLAVE_TIMEOUT + && putMessageResult.getPutMessageStatus() != PutMessageStatus.SLAVE_NOT_AVAILABLE) { + POP_LOGGER.error("[PopBuffer]put batch ack to store fail: {}, {}, {}", pointWrapper, batchAckMsg, putMessageResult); + return false; + } + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("[PopBuffer]put batch ack to store ok: {}, {}, {}", pointWrapper, batchAckMsg, putMessageResult); + } + + return true; + } + + private boolean cancelCkTimer(final PopCheckPointWrapper pointWrapper) { + // not stored, no need cancel + if (pointWrapper.getReviveQueueOffset() < 0) { + return true; + } + PopCheckPoint point = pointWrapper.getCk(); + MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); + msgInner.setTopic(popMessageProcessor.reviveTopic); + msgInner.setBody((pointWrapper.getReviveQueueId() + "-" + pointWrapper.getReviveQueueOffset()).getBytes(StandardCharsets.UTF_8)); + msgInner.setQueueId(pointWrapper.getReviveQueueId()); + msgInner.setTags(PopAckConstants.CK_TAG); + msgInner.setBornTimestamp(System.currentTimeMillis()); + msgInner.setBornHost(brokerController.getStoreHost()); + msgInner.setStoreHost(brokerController.getStoreHost()); + + msgInner.setDeliverTimeMs(point.getReviveTime() - PopAckConstants.ackTimeInterval); + msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); + PutMessageResult putMessageResult = brokerController.getEscapeBridge().putMessageToSpecificQueue(msgInner); + if (putMessageResult.getPutMessageStatus() != PutMessageStatus.PUT_OK + && putMessageResult.getPutMessageStatus() != PutMessageStatus.FLUSH_DISK_TIMEOUT + && putMessageResult.getPutMessageStatus() != PutMessageStatus.FLUSH_SLAVE_TIMEOUT + && putMessageResult.getPutMessageStatus() != PutMessageStatus.SLAVE_NOT_AVAILABLE) { + POP_LOGGER.error("[PopBuffer]PutMessageCallback cancelCheckPoint fail, {}, {}", pointWrapper, putMessageResult); + return false; + } + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("[PopBuffer]cancelCheckPoint, {}", pointWrapper); + } + return true; + } + + private boolean isCkDone(PopCheckPointWrapper pointWrapper) { + byte num = pointWrapper.getCk().getNum(); + for (byte i = 0; i < num; i++) { + if (!DataConverter.getBit(pointWrapper.getBits().get(), i)) { + return false; + } + } + return true; + } + + private boolean isCkDoneForFinish(PopCheckPointWrapper pointWrapper) { + byte num = pointWrapper.getCk().getNum(); + int bits = pointWrapper.getBits().get() ^ pointWrapper.getToStoreBits().get(); + for (byte i = 0; i < num; i++) { + if (DataConverter.getBit(bits, i)) { + return false; + } + } + return true; + } + + public class QueueWithTime { + private final LinkedBlockingDeque queue; + private long time; + + public QueueWithTime() { + this.queue = new LinkedBlockingDeque<>(); + this.time = System.currentTimeMillis(); + } + + public void setTime(long popTime) { + this.time = popTime; + } + + public long getTime() { + return time; + } + + public LinkedBlockingDeque get() { + return queue; + } + } + + public class PopCheckPointWrapper { + private final int reviveQueueId; + // -1: not stored, >=0: stored, Long.MAX: storing. + private volatile long reviveQueueOffset; + private final PopCheckPoint ck; + // bit for concurrent + private final AtomicInteger bits; + // bit for stored buffer ak + private final AtomicInteger toStoreBits; + private final long nextBeginOffset; + private final String lockKey; + private final String mergeKey; + private final boolean justOffset; + private volatile boolean ckStored = false; + + public PopCheckPointWrapper(int reviveQueueId, long reviveQueueOffset, PopCheckPoint point, + long nextBeginOffset) { + this.reviveQueueId = reviveQueueId; + this.reviveQueueOffset = reviveQueueOffset; + this.ck = point; + this.bits = new AtomicInteger(0); + this.toStoreBits = new AtomicInteger(0); + this.nextBeginOffset = nextBeginOffset; + this.lockKey = ck.getTopic() + PopAckConstants.SPLIT + ck.getCId() + PopAckConstants.SPLIT + ck.getQueueId(); + this.mergeKey = point.getTopic() + point.getCId() + point.getQueueId() + point.getStartOffset() + point.getPopTime() + point.getBrokerName(); + this.justOffset = false; + } + + public PopCheckPointWrapper(int reviveQueueId, long reviveQueueOffset, PopCheckPoint point, + long nextBeginOffset, + boolean justOffset) { + this.reviveQueueId = reviveQueueId; + this.reviveQueueOffset = reviveQueueOffset; + this.ck = point; + this.bits = new AtomicInteger(0); + this.toStoreBits = new AtomicInteger(0); + this.nextBeginOffset = nextBeginOffset; + this.lockKey = ck.getTopic() + PopAckConstants.SPLIT + ck.getCId() + PopAckConstants.SPLIT + ck.getQueueId(); + this.mergeKey = point.getTopic() + point.getCId() + point.getQueueId() + point.getStartOffset() + point.getPopTime() + point.getBrokerName(); + this.justOffset = justOffset; + } + + public int getReviveQueueId() { + return reviveQueueId; + } + + public long getReviveQueueOffset() { + return reviveQueueOffset; + } + + public boolean isCkStored() { + return ckStored; + } + + public void setReviveQueueOffset(long reviveQueueOffset) { + this.reviveQueueOffset = reviveQueueOffset; + } + + public PopCheckPoint getCk() { + return ck; + } + + public AtomicInteger getBits() { + return bits; + } + + public AtomicInteger getToStoreBits() { + return toStoreBits; + } + + public long getNextBeginOffset() { + return nextBeginOffset; + } + + public String getLockKey() { + return lockKey; + } + + public String getMergeKey() { + return mergeKey; + } + + public boolean isJustOffset() { + return justOffset; + } + + public void setCkStored(boolean ckStored) { + this.ckStored = ckStored; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("CkWrap{"); + sb.append("rq=").append(reviveQueueId); + sb.append(", rqo=").append(reviveQueueOffset); + sb.append(", ck=").append(ck); + sb.append(", bits=").append(bits); + sb.append(", sBits=").append(toStoreBits); + sb.append(", nbo=").append(nextBeginOffset); + sb.append(", cks=").append(ckStored); + sb.append(", jo=").append(justOffset); + sb.append('}'); + return sb.toString(); + } + } + +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopInflightMessageCounter.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopInflightMessageCounter.java new file mode 100644 index 00000000000..6749af3d750 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopInflightMessageCounter.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.processor; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.pop.PopCheckPoint; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +public class PopInflightMessageCounter { + private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + + private static final String TOPIC_GROUP_SEPARATOR = "@"; + private final Map> topicInFlightMessageNum = + new ConcurrentHashMap<>(512); + private final BrokerController brokerController; + + public PopInflightMessageCounter(BrokerController brokerController) { + this.brokerController = brokerController; + } + + public void incrementInFlightMessageNum(String topic, String group, int queueId, int num) { + if (num <= 0) { + return; + } + topicInFlightMessageNum.compute(buildKey(topic, group), (key, queueNum) -> { + if (queueNum == null) { + queueNum = new ConcurrentHashMap<>(8); + } + queueNum.compute(queueId, (queueIdKey, counter) -> { + if (counter == null) { + return new AtomicLong(num); + } + if (counter.addAndGet(num) <= 0) { + return null; + } + return counter; + }); + return queueNum; + }); + } + + public void decrementInFlightMessageNum(String topic, String group, long popTime, int qId, int delta) { + if (popTime < this.brokerController.getShouldStartTime()) { + return; + } + decrementInFlightMessageNum(topic, group, qId, delta); + } + + public void decrementInFlightMessageNum(PopCheckPoint checkPoint) { + if (checkPoint.getPopTime() < this.brokerController.getShouldStartTime()) { + return; + } + decrementInFlightMessageNum(checkPoint.getTopic(), checkPoint.getCId(), checkPoint.getQueueId(), 1); + } + + private void decrementInFlightMessageNum(String topic, String group, int queueId, int delta) { + topicInFlightMessageNum.computeIfPresent(buildKey(topic, group), (key, queueNum) -> { + queueNum.computeIfPresent(queueId, (queueIdKey, counter) -> { + if (counter.addAndGet(-delta) <= 0) { + return null; + } + return counter; + }); + if (queueNum.isEmpty()) { + return null; + } + return queueNum; + }); + } + + public void clearInFlightMessageNumByGroupName(String group) { + Set topicGroupKey = this.topicInFlightMessageNum.keySet(); + for (String key : topicGroupKey) { + if (key.contains(group)) { + Pair topicAndGroup = splitKey(key); + if (topicAndGroup != null && topicAndGroup.getObject2().equals(group)) { + this.topicInFlightMessageNum.remove(key); + log.info("PopInflightMessageCounter#clearInFlightMessageNumByGroupName: clean by group, topic={}, group={}", + topicAndGroup.getObject1(), topicAndGroup.getObject2()); + } + } + } + } + + public void clearInFlightMessageNumByTopicName(String topic) { + Set topicGroupKey = this.topicInFlightMessageNum.keySet(); + for (String key : topicGroupKey) { + if (key.contains(topic)) { + Pair topicAndGroup = splitKey(key); + if (topicAndGroup != null && topicAndGroup.getObject1().equals(topic)) { + this.topicInFlightMessageNum.remove(key); + log.info("PopInflightMessageCounter#clearInFlightMessageNumByTopicName: clean by topic, topic={}, group={}", + topicAndGroup.getObject1(), topicAndGroup.getObject2()); + } + } + } + } + + public void clearInFlightMessageNum(String topic, String group, int queueId) { + topicInFlightMessageNum.computeIfPresent(buildKey(topic, group), (key, queueNum) -> { + queueNum.computeIfPresent(queueId, (queueIdKey, counter) -> null); + if (queueNum.isEmpty()) { + return null; + } + return queueNum; + }); + } + + public long getGroupPopInFlightMessageNum(String topic, String group, int queueId) { + Map queueCounter = topicInFlightMessageNum.get(buildKey(topic, group)); + if (queueCounter == null) { + return 0; + } + AtomicLong counter = queueCounter.get(queueId); + if (counter == null) { + return 0; + } + return Math.max(0, counter.get()); + } + + private static Pair splitKey(String key) { + String[] strings = key.split(TOPIC_GROUP_SEPARATOR); + if (strings.length != 2) { + return null; + } + return new Pair<>(strings[0], strings[1]); + } + + private static String buildKey(String topic, String group) { + return topic + TOPIC_GROUP_SEPARATOR + group; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java new file mode 100644 index 00000000000..59ff2e0fd52 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopMessageProcessor.java @@ -0,0 +1,931 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.processor; + +import com.alibaba.fastjson.JSON; +import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.FileRegion; +import io.opentelemetry.api.common.Attributes; +import java.nio.ByteBuffer; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.filter.ConsumerFilterData; +import org.apache.rocketmq.broker.filter.ConsumerFilterManager; +import org.apache.rocketmq.broker.filter.ExpressionMessageFilter; +import org.apache.rocketmq.broker.longpolling.PollingHeader; +import org.apache.rocketmq.broker.longpolling.PollingResult; +import org.apache.rocketmq.broker.longpolling.PopLongPollingService; +import org.apache.rocketmq.broker.longpolling.PopRequest; +import org.apache.rocketmq.broker.metrics.BrokerMetricsManager; +import org.apache.rocketmq.broker.pagecache.ManyMessageTransfer; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.KeyBuilder; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.PopAckConstants; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.constant.ConsumeInitMode; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.constant.PermName; +import org.apache.rocketmq.common.filter.ExpressionType; +import org.apache.rocketmq.common.help.FAQUrl; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.common.utils.DataConverter; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.metrics.RemotingMetricsManager; +import org.apache.rocketmq.remoting.netty.NettyRemotingAbstract; +import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.filter.FilterAPI; +import org.apache.rocketmq.remoting.protocol.header.ExtraInfoUtil; +import org.apache.rocketmq.remoting.protocol.header.PopMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PopMessageResponseHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.GetMessageStatus; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.pop.AckMsg; +import org.apache.rocketmq.store.pop.BatchAckMsg; +import org.apache.rocketmq.store.pop.PopCheckPoint; + +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_CONSUMER_GROUP; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_IS_RETRY; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_IS_SYSTEM; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_TOPIC; +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.LABEL_REQUEST_CODE; +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.LABEL_RESPONSE_CODE; +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.LABEL_RESULT; + +public class PopMessageProcessor implements NettyRequestProcessor { + private static final Logger POP_LOGGER = + LoggerFactory.getLogger(LoggerName.ROCKETMQ_POP_LOGGER_NAME); + private final BrokerController brokerController; + private final Random random = new Random(System.currentTimeMillis()); + String reviveTopic; + private static final String BORN_TIME = "bornTime"; + + private final PopLongPollingService popLongPollingService; + private final PopBufferMergeService popBufferMergeService; + private final QueueLockManager queueLockManager; + private final AtomicLong ckMessageNumber; + + public PopMessageProcessor(final BrokerController brokerController) { + this.brokerController = brokerController; + this.reviveTopic = PopAckConstants.buildClusterReviveTopic(this.brokerController.getBrokerConfig().getBrokerClusterName()); + this.popLongPollingService = new PopLongPollingService(brokerController, this); + this.queueLockManager = new QueueLockManager(); + this.popBufferMergeService = new PopBufferMergeService(this.brokerController, this); + this.ckMessageNumber = new AtomicLong(); + } + + public PopLongPollingService getPopLongPollingService() { + return popLongPollingService; + } + + public PopBufferMergeService getPopBufferMergeService() { + return this.popBufferMergeService; + } + + public QueueLockManager getQueueLockManager() { + return queueLockManager; + } + + public static String genAckUniqueId(AckMsg ackMsg) { + return ackMsg.getTopic() + + PopAckConstants.SPLIT + ackMsg.getQueueId() + + PopAckConstants.SPLIT + ackMsg.getAckOffset() + + PopAckConstants.SPLIT + ackMsg.getConsumerGroup() + + PopAckConstants.SPLIT + ackMsg.getPopTime() + + PopAckConstants.SPLIT + ackMsg.getBrokerName() + + PopAckConstants.SPLIT + PopAckConstants.ACK_TAG; + } + + public static String genBatchAckUniqueId(BatchAckMsg batchAckMsg) { + return batchAckMsg.getTopic() + + PopAckConstants.SPLIT + batchAckMsg.getQueueId() + + PopAckConstants.SPLIT + batchAckMsg.getAckOffsetList().toString() + + PopAckConstants.SPLIT + batchAckMsg.getConsumerGroup() + + PopAckConstants.SPLIT + batchAckMsg.getPopTime() + + PopAckConstants.SPLIT + PopAckConstants.BATCH_ACK_TAG; + } + + public static String genCkUniqueId(PopCheckPoint ck) { + return ck.getTopic() + + PopAckConstants.SPLIT + ck.getQueueId() + + PopAckConstants.SPLIT + ck.getStartOffset() + + PopAckConstants.SPLIT + ck.getCId() + + PopAckConstants.SPLIT + ck.getPopTime() + + PopAckConstants.SPLIT + ck.getBrokerName() + + PopAckConstants.SPLIT + PopAckConstants.CK_TAG; + } + + @Override + public boolean rejectRequest() { + return false; + } + + public ConcurrentLinkedHashMap> getPollingMap() { + return popLongPollingService.getPollingMap(); + } + + public void notifyLongPollingRequestIfNeed(String topic, String group, int queueId) { + long popBufferOffset = this.brokerController.getPopMessageProcessor().getPopBufferMergeService().getLatestOffset(topic, group, queueId); + long consumerOffset = this.brokerController.getConsumerOffsetManager().queryOffset(group, topic, queueId); + long maxOffset = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId); + long offset = Math.max(popBufferOffset, consumerOffset); + if (maxOffset > offset) { + boolean notifySuccess = popLongPollingService.notifyMessageArriving(topic, group, -1); + if (!notifySuccess) { + // notify pop queue + notifySuccess = popLongPollingService.notifyMessageArriving(topic, group, queueId); + } + this.brokerController.getNotificationProcessor().notifyMessageArriving(topic, queueId); + if (this.brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("notify long polling request. topic:{}, group:{}, queueId:{}, success:{}", + topic, group, queueId, notifySuccess); + } + } + } + + public void notifyMessageArriving(final String topic, final int queueId) { + popLongPollingService.notifyMessageArrivingWithRetryTopic(topic, queueId); + } + + public boolean notifyMessageArriving(final String topic, final String cid, final int queueId) { + return popLongPollingService.notifyMessageArriving(topic, cid, queueId); + } + + @Override + public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingCommand request) + throws RemotingCommandException { + final long beginTimeMills = this.brokerController.getMessageStore().now(); + request.addExtFieldIfNotExist(BORN_TIME, String.valueOf(System.currentTimeMillis())); + if (Objects.equals(request.getExtFields().get(BORN_TIME), "0")) { + request.addExtField(BORN_TIME, String.valueOf(System.currentTimeMillis())); + } + Channel channel = ctx.channel(); + + RemotingCommand response = RemotingCommand.createResponseCommand(PopMessageResponseHeader.class); + final PopMessageResponseHeader responseHeader = (PopMessageResponseHeader) response.readCustomHeader(); + final PopMessageRequestHeader requestHeader = + (PopMessageRequestHeader) request.decodeCommandCustomHeader(PopMessageRequestHeader.class, true); + StringBuilder startOffsetInfo = new StringBuilder(64); + StringBuilder msgOffsetInfo = new StringBuilder(64); + StringBuilder orderCountInfo = null; + if (requestHeader.isOrder()) { + orderCountInfo = new StringBuilder(64); + } + + brokerController.getConsumerManager().compensateBasicConsumerInfo(requestHeader.getConsumerGroup(), + ConsumeType.CONSUME_POP, MessageModel.CLUSTERING); + + response.setOpaque(request.getOpaque()); + + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("receive PopMessage request command, {}", request); + } + + if (requestHeader.isTimeoutTooMuch()) { + response.setCode(ResponseCode.POLLING_TIMEOUT); + response.setRemark(String.format("the broker[%s] pop message is timeout too much", + this.brokerController.getBrokerConfig().getBrokerIP1())); + return response; + } + if (!PermName.isReadable(this.brokerController.getBrokerConfig().getBrokerPermission())) { + response.setCode(ResponseCode.NO_PERMISSION); + response.setRemark(String.format("the broker[%s] pop message is forbidden", + this.brokerController.getBrokerConfig().getBrokerIP1())); + return response; + } + if (requestHeader.getMaxMsgNums() > 32) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(String.format("the broker[%s] pop message's num is greater than 32", + this.brokerController.getBrokerConfig().getBrokerIP1())); + return response; + } + + if (!brokerController.getMessageStore().getMessageStoreConfig().isTimerWheelEnable()) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(String.format("the broker[%s] pop message is forbidden because timerWheelEnable is false", + this.brokerController.getBrokerConfig().getBrokerIP1())); + return response; + } + + TopicConfig topicConfig = + this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic()); + if (null == topicConfig) { + POP_LOGGER.error("The topic {} not exist, consumer: {} ", requestHeader.getTopic(), + RemotingHelper.parseChannelRemoteAddr(channel)); + response.setCode(ResponseCode.TOPIC_NOT_EXIST); + response.setRemark(String.format("topic[%s] not exist, apply first please! %s", requestHeader.getTopic(), + FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL))); + return response; + } + + if (!PermName.isReadable(topicConfig.getPerm())) { + response.setCode(ResponseCode.NO_PERMISSION); + response.setRemark("the topic[" + requestHeader.getTopic() + "] peeking message is forbidden"); + return response; + } + + if (requestHeader.getQueueId() >= topicConfig.getReadQueueNums()) { + String errorInfo = String.format("queueId[%d] is illegal, topic:[%s] topicConfig.readQueueNums:[%d] " + + "consumer:[%s]", + requestHeader.getQueueId(), requestHeader.getTopic(), topicConfig.getReadQueueNums(), + channel.remoteAddress()); + POP_LOGGER.warn(errorInfo); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(errorInfo); + return response; + } + SubscriptionGroupConfig subscriptionGroupConfig = + this.brokerController.getSubscriptionGroupManager().findSubscriptionGroupConfig(requestHeader.getConsumerGroup()); + if (null == subscriptionGroupConfig) { + response.setCode(ResponseCode.SUBSCRIPTION_GROUP_NOT_EXIST); + response.setRemark(String.format("subscription group [%s] does not exist, %s", + requestHeader.getConsumerGroup(), FAQUrl.suggestTodo(FAQUrl.SUBSCRIPTION_GROUP_NOT_EXIST))); + return response; + } + + if (!subscriptionGroupConfig.isConsumeEnable()) { + response.setCode(ResponseCode.NO_PERMISSION); + response.setRemark("subscription group no permission, " + requestHeader.getConsumerGroup()); + return response; + } + + BrokerConfig brokerConfig = brokerController.getBrokerConfig(); + ExpressionMessageFilter messageFilter = null; + if (requestHeader.getExp() != null && requestHeader.getExp().length() > 0) { + try { + SubscriptionData subscriptionData = FilterAPI.build(requestHeader.getTopic(), requestHeader.getExp(), requestHeader.getExpType()); + brokerController.getConsumerManager().compensateSubscribeData(requestHeader.getConsumerGroup(), + requestHeader.getTopic(), subscriptionData); + + String retryTopic = KeyBuilder.buildPopRetryTopic(requestHeader.getTopic(), requestHeader.getConsumerGroup(), brokerConfig.isEnableRetryTopicV2()); + SubscriptionData retrySubscriptionData = FilterAPI.build(retryTopic, SubscriptionData.SUB_ALL, requestHeader.getExpType()); + brokerController.getConsumerManager().compensateSubscribeData(requestHeader.getConsumerGroup(), + retryTopic, retrySubscriptionData); + + ConsumerFilterData consumerFilterData = null; + if (!ExpressionType.isTagType(subscriptionData.getExpressionType())) { + consumerFilterData = ConsumerFilterManager.build( + requestHeader.getTopic(), requestHeader.getConsumerGroup(), requestHeader.getExp(), + requestHeader.getExpType(), System.currentTimeMillis() + ); + if (consumerFilterData == null) { + POP_LOGGER.warn("Parse the consumer's subscription[{}] failed, group: {}", + requestHeader.getExp(), requestHeader.getConsumerGroup()); + response.setCode(ResponseCode.SUBSCRIPTION_PARSE_FAILED); + response.setRemark("parse the consumer's subscription failed"); + return response; + } + } + messageFilter = new ExpressionMessageFilter(subscriptionData, consumerFilterData, + brokerController.getConsumerFilterManager()); + } catch (Exception e) { + POP_LOGGER.warn("Parse the consumer's subscription[{}] error, group: {}", requestHeader.getExp(), + requestHeader.getConsumerGroup()); + response.setCode(ResponseCode.SUBSCRIPTION_PARSE_FAILED); + response.setRemark("parse the consumer's subscription failed"); + return response; + } + } else { + try { + SubscriptionData subscriptionData = FilterAPI.build(requestHeader.getTopic(), "*", ExpressionType.TAG); + brokerController.getConsumerManager().compensateSubscribeData(requestHeader.getConsumerGroup(), + requestHeader.getTopic(), subscriptionData); + + String retryTopic = KeyBuilder.buildPopRetryTopic(requestHeader.getTopic(), requestHeader.getConsumerGroup(), brokerConfig.isEnableRetryTopicV2()); + SubscriptionData retrySubscriptionData = FilterAPI.build(retryTopic, "*", ExpressionType.TAG); + brokerController.getConsumerManager().compensateSubscribeData(requestHeader.getConsumerGroup(), + retryTopic, retrySubscriptionData); + } catch (Exception e) { + POP_LOGGER.warn("Build default subscription error, group: {}", requestHeader.getConsumerGroup()); + } + } + + int randomQ = random.nextInt(100); + int reviveQid; + if (requestHeader.isOrder()) { + reviveQid = KeyBuilder.POP_ORDER_REVIVE_QUEUE; + } else { + reviveQid = (int) Math.abs(ckMessageNumber.getAndIncrement() % this.brokerController.getBrokerConfig().getReviveQueueNum()); + } + + GetMessageResult getMessageResult = new GetMessageResult(requestHeader.getMaxMsgNums()); + ExpressionMessageFilter finalMessageFilter = messageFilter; + StringBuilder finalOrderCountInfo = orderCountInfo; + + // Due to the design of the fields startOffsetInfo, msgOffsetInfo, and orderCountInfo, + // a single POP request could only invoke the popMsgFromQueue method once + // for either a normal topic or a retry topic's queue. Retry topics v1 and v2 are + // considered the same type because they share the same retry flag in previous fields. + // Therefore, needRetryV1 is designed as a subset of needRetry, and within a single request, + // only one type of retry topic is able to call popMsgFromQueue. + boolean needRetry = randomQ % 5 == 0; + boolean needRetryV1 = false; + if (brokerConfig.isEnableRetryTopicV2() && brokerConfig.isRetrieveMessageFromPopRetryTopicV1()) { + needRetryV1 = randomQ % 2 == 0; + } + long popTime = System.currentTimeMillis(); + CompletableFuture getMessageFuture = CompletableFuture.completedFuture(0L); + if (needRetry && !requestHeader.isOrder()) { + if (needRetryV1) { + String retryTopic = KeyBuilder.buildPopRetryTopicV1(requestHeader.getTopic(), requestHeader.getConsumerGroup()); + getMessageFuture = popMsgFromTopic(retryTopic, true, getMessageResult, requestHeader, reviveQid, channel, + popTime, finalMessageFilter, startOffsetInfo, msgOffsetInfo, orderCountInfo, randomQ, getMessageFuture); + } else { + String retryTopic = KeyBuilder.buildPopRetryTopic(requestHeader.getTopic(), requestHeader.getConsumerGroup(), brokerConfig.isEnableRetryTopicV2()); + getMessageFuture = popMsgFromTopic(retryTopic, true, getMessageResult, requestHeader, reviveQid, channel, + popTime, finalMessageFilter, startOffsetInfo, msgOffsetInfo, orderCountInfo, randomQ, getMessageFuture); + } + } + if (requestHeader.getQueueId() < 0) { + // read all queue + getMessageFuture = popMsgFromTopic(topicConfig, false, getMessageResult, requestHeader, reviveQid, channel, + popTime, finalMessageFilter, startOffsetInfo, msgOffsetInfo, orderCountInfo, randomQ, getMessageFuture); + } else { + int queueId = requestHeader.getQueueId(); + getMessageFuture = getMessageFuture.thenCompose(restNum -> + popMsgFromQueue(topicConfig.getTopicName(), requestHeader.getAttemptId(), false, + getMessageResult, requestHeader, queueId, restNum, reviveQid, channel, popTime, finalMessageFilter, + startOffsetInfo, msgOffsetInfo, finalOrderCountInfo)); + } + // if not full , fetch retry again + if (!needRetry && getMessageResult.getMessageMapedList().size() < requestHeader.getMaxMsgNums() && !requestHeader.isOrder()) { + if (needRetryV1) { + String retryTopicV1 = KeyBuilder.buildPopRetryTopicV1(requestHeader.getTopic(), requestHeader.getConsumerGroup()); + getMessageFuture = popMsgFromTopic(retryTopicV1, true, getMessageResult, requestHeader, reviveQid, channel, + popTime, finalMessageFilter, startOffsetInfo, msgOffsetInfo, orderCountInfo, randomQ, getMessageFuture); + } else { + String retryTopic = KeyBuilder.buildPopRetryTopic(requestHeader.getTopic(), requestHeader.getConsumerGroup(), brokerConfig.isEnableRetryTopicV2()); + getMessageFuture = popMsgFromTopic(retryTopic, true, getMessageResult, requestHeader, reviveQid, channel, + popTime, finalMessageFilter, startOffsetInfo, msgOffsetInfo, orderCountInfo, randomQ, getMessageFuture); + } + } + + final RemotingCommand finalResponse = response; + getMessageFuture.thenApply(restNum -> { + if (!getMessageResult.getMessageBufferList().isEmpty()) { + finalResponse.setCode(ResponseCode.SUCCESS); + getMessageResult.setStatus(GetMessageStatus.FOUND); + if (restNum > 0) { + // all queue pop can not notify specified queue pop, and vice versa + popLongPollingService.notifyMessageArriving(requestHeader.getTopic(), requestHeader.getConsumerGroup(), + requestHeader.getQueueId()); + } + } else { + PollingResult pollingResult = popLongPollingService.polling(ctx, request, new PollingHeader(requestHeader)); + if (PollingResult.POLLING_SUC == pollingResult) { + return null; + } else if (PollingResult.POLLING_FULL == pollingResult) { + finalResponse.setCode(ResponseCode.POLLING_FULL); + } else { + finalResponse.setCode(ResponseCode.POLLING_TIMEOUT); + } + getMessageResult.setStatus(GetMessageStatus.NO_MESSAGE_IN_QUEUE); + } + responseHeader.setInvisibleTime(requestHeader.getInvisibleTime()); + responseHeader.setPopTime(popTime); + responseHeader.setReviveQid(reviveQid); + responseHeader.setRestNum(restNum); + responseHeader.setStartOffsetInfo(startOffsetInfo.toString()); + responseHeader.setMsgOffsetInfo(msgOffsetInfo.toString()); + if (requestHeader.isOrder() && finalOrderCountInfo != null) { + responseHeader.setOrderCountInfo(finalOrderCountInfo.toString()); + } + finalResponse.setRemark(getMessageResult.getStatus().name()); + switch (finalResponse.getCode()) { + case ResponseCode.SUCCESS: + if (this.brokerController.getBrokerConfig().isTransferMsgByHeap()) { + final byte[] r = this.readGetMessageResult(getMessageResult, requestHeader.getConsumerGroup(), + requestHeader.getTopic(), requestHeader.getQueueId()); + this.brokerController.getBrokerStatsManager().incGroupGetLatency(requestHeader.getConsumerGroup(), + requestHeader.getTopic(), requestHeader.getQueueId(), + (int) (this.brokerController.getMessageStore().now() - beginTimeMills)); + finalResponse.setBody(r); + } else { + final GetMessageResult tmpGetMessageResult = getMessageResult; + try { + FileRegion fileRegion = + new ManyMessageTransfer(finalResponse.encodeHeader(getMessageResult.getBufferTotalSize()), + getMessageResult); + channel.writeAndFlush(fileRegion) + .addListener((ChannelFutureListener) future -> { + tmpGetMessageResult.release(); + Attributes attributes = RemotingMetricsManager.newAttributesBuilder() + .put(LABEL_REQUEST_CODE, RemotingHelper.getRequestCodeDesc(request.getCode())) + .put(LABEL_RESPONSE_CODE, RemotingHelper.getResponseCodeDesc(finalResponse.getCode())) + .put(LABEL_RESULT, RemotingMetricsManager.getWriteAndFlushResult(future)) + .build(); + RemotingMetricsManager.rpcLatency.record(request.getProcessTimer().elapsed(TimeUnit.MILLISECONDS), attributes); + if (!future.isSuccess()) { + POP_LOGGER.error("Fail to transfer messages from page cache to {}", + channel.remoteAddress(), future.cause()); + } + }); + } catch (Throwable e) { + POP_LOGGER.error("Error occurred when transferring messages from page cache", e); + getMessageResult.release(); + } + + return null; + } + break; + default: + return finalResponse; + } + return finalResponse; + }).thenAccept(result -> NettyRemotingAbstract.writeResponse(channel, request, result)); + return null; + } + + private CompletableFuture popMsgFromTopic(TopicConfig topicConfig, boolean isRetry, GetMessageResult getMessageResult, + PopMessageRequestHeader requestHeader, int reviveQid, Channel channel, long popTime, + ExpressionMessageFilter messageFilter, StringBuilder startOffsetInfo, + StringBuilder msgOffsetInfo, StringBuilder orderCountInfo, int randomQ, CompletableFuture getMessageFuture) { + if (topicConfig != null) { + for (int i = 0; i < topicConfig.getReadQueueNums(); i++) { + int queueId = (randomQ + i) % topicConfig.getReadQueueNums(); + getMessageFuture = getMessageFuture.thenCompose(restNum -> + popMsgFromQueue(topicConfig.getTopicName(), requestHeader.getAttemptId(), isRetry, + getMessageResult, requestHeader, queueId, restNum, reviveQid, channel, popTime, messageFilter, + startOffsetInfo, msgOffsetInfo, orderCountInfo)); + } + } + return getMessageFuture; + } + + private CompletableFuture popMsgFromTopic(String topic, boolean isRetry, GetMessageResult getMessageResult, + PopMessageRequestHeader requestHeader, int reviveQid, Channel channel, long popTime, + ExpressionMessageFilter messageFilter, StringBuilder startOffsetInfo, + StringBuilder msgOffsetInfo, StringBuilder orderCountInfo, int randomQ, CompletableFuture getMessageFuture) { + TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(topic); + return popMsgFromTopic(topicConfig, isRetry, getMessageResult, requestHeader, reviveQid, channel, popTime, + messageFilter, startOffsetInfo, msgOffsetInfo, orderCountInfo, randomQ, getMessageFuture); + } + + private CompletableFuture popMsgFromQueue(String topic, String attemptId, boolean isRetry, GetMessageResult getMessageResult, + PopMessageRequestHeader requestHeader, int queueId, long restNum, int reviveQid, + Channel channel, long popTime, ExpressionMessageFilter messageFilter, StringBuilder startOffsetInfo, + StringBuilder msgOffsetInfo, StringBuilder orderCountInfo) { + String lockKey = + topic + PopAckConstants.SPLIT + requestHeader.getConsumerGroup() + PopAckConstants.SPLIT + queueId; + boolean isOrder = requestHeader.isOrder(); + long offset = getPopOffset(topic, requestHeader.getConsumerGroup(), queueId, requestHeader.getInitMode(), + false, lockKey, false); + CompletableFuture future = new CompletableFuture<>(); + if (!queueLockManager.tryLock(lockKey)) { + restNum = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId) - offset + restNum; + future.complete(restNum); + return future; + } + + if (isPopShouldStop(topic, requestHeader.getConsumerGroup(), queueId)) { + POP_LOGGER.warn("Too much msgs unacked, then stop poping. topic={}, group={}, queueId={}", topic, requestHeader.getConsumerGroup(), queueId); + restNum = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId) - offset + restNum; + future.complete(restNum); + return future; + } + + try { + future.whenComplete((result, throwable) -> queueLockManager.unLock(lockKey)); + offset = getPopOffset(topic, requestHeader.getConsumerGroup(), queueId, requestHeader.getInitMode(), + true, lockKey, true); + if (isOrder && brokerController.getConsumerOrderInfoManager().checkBlock(attemptId, topic, + requestHeader.getConsumerGroup(), queueId, requestHeader.getInvisibleTime())) { + future.complete(this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId) - offset + restNum); + return future; + } + + if (isOrder) { + this.brokerController.getPopInflightMessageCounter().clearInFlightMessageNum( + topic, + requestHeader.getConsumerGroup(), + queueId + ); + } + + if (getMessageResult.getMessageMapedList().size() >= requestHeader.getMaxMsgNums()) { + restNum = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId) - offset + restNum; + future.complete(restNum); + return future; + } + } catch (Exception e) { + POP_LOGGER.error("Exception in popMsgFromQueue", e); + future.complete(restNum); + return future; + } + + AtomicLong atomicRestNum = new AtomicLong(restNum); + AtomicLong atomicOffset = new AtomicLong(offset); + long finalOffset = offset; + return this.brokerController.getMessageStore() + .getMessageAsync(requestHeader.getConsumerGroup(), topic, queueId, offset, + requestHeader.getMaxMsgNums() - getMessageResult.getMessageMapedList().size(), messageFilter) + .thenCompose(result -> { + if (result == null) { + return CompletableFuture.completedFuture(null); + } + // maybe store offset is not correct. + if (GetMessageStatus.OFFSET_TOO_SMALL.equals(result.getStatus()) + || GetMessageStatus.OFFSET_OVERFLOW_BADLY.equals(result.getStatus()) + || GetMessageStatus.OFFSET_FOUND_NULL.equals(result.getStatus())) { + // commit offset, because the offset is not correct + // If offset in store is greater than cq offset, it will cause duplicate messages, + // because offset in PopBuffer is not committed. + POP_LOGGER.warn("Pop initial offset, because store is no correct, {}, {}->{}", + lockKey, atomicOffset.get(), result.getNextBeginOffset()); + this.brokerController.getConsumerOffsetManager().commitOffset(channel.remoteAddress().toString(), requestHeader.getConsumerGroup(), topic, + queueId, result.getNextBeginOffset()); + atomicOffset.set(result.getNextBeginOffset()); + return this.brokerController.getMessageStore().getMessageAsync(requestHeader.getConsumerGroup(), topic, queueId, atomicOffset.get(), + requestHeader.getMaxMsgNums() - getMessageResult.getMessageMapedList().size(), messageFilter); + } + return CompletableFuture.completedFuture(result); + }).thenApply(result -> { + if (result == null) { + atomicRestNum.set(brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId) - atomicOffset.get() + atomicRestNum.get()); + return atomicRestNum.get(); + } + if (!result.getMessageMapedList().isEmpty()) { + this.brokerController.getBrokerStatsManager().incBrokerGetNums(requestHeader.getTopic(), result.getMessageCount()); + this.brokerController.getBrokerStatsManager().incGroupGetNums(requestHeader.getConsumerGroup(), topic, + result.getMessageCount()); + this.brokerController.getBrokerStatsManager().incGroupGetSize(requestHeader.getConsumerGroup(), topic, + result.getBufferTotalSize()); + + Attributes attributes = BrokerMetricsManager.newAttributesBuilder() + .put(LABEL_TOPIC, requestHeader.getTopic()) + .put(LABEL_CONSUMER_GROUP, requestHeader.getConsumerGroup()) + .put(LABEL_IS_SYSTEM, TopicValidator.isSystemTopic(requestHeader.getTopic()) || MixAll.isSysConsumerGroup(requestHeader.getConsumerGroup())) + .put(LABEL_IS_RETRY, isRetry) + .build(); + BrokerMetricsManager.messagesOutTotal.add(result.getMessageCount(), attributes); + BrokerMetricsManager.throughputOutTotal.add(result.getBufferTotalSize(), attributes); + + if (isOrder) { + this.brokerController.getConsumerOrderInfoManager().update(requestHeader.getAttemptId(), isRetry, topic, + requestHeader.getConsumerGroup(), + queueId, popTime, requestHeader.getInvisibleTime(), result.getMessageQueueOffset(), + orderCountInfo); + this.brokerController.getConsumerOffsetManager().commitOffset(channel.remoteAddress().toString(), + requestHeader.getConsumerGroup(), topic, queueId, finalOffset); + } else { + if (!appendCheckPoint(requestHeader, topic, reviveQid, queueId, finalOffset, result, popTime, this.brokerController.getBrokerConfig().getBrokerName())) { + return atomicRestNum.get() + result.getMessageCount(); + } + } + ExtraInfoUtil.buildStartOffsetInfo(startOffsetInfo, topic, queueId, finalOffset); + ExtraInfoUtil.buildMsgOffsetInfo(msgOffsetInfo, topic, queueId, + result.getMessageQueueOffset()); + } else if ((GetMessageStatus.NO_MATCHED_MESSAGE.equals(result.getStatus()) + || GetMessageStatus.OFFSET_FOUND_NULL.equals(result.getStatus()) + || GetMessageStatus.MESSAGE_WAS_REMOVING.equals(result.getStatus()) + || GetMessageStatus.NO_MATCHED_LOGIC_QUEUE.equals(result.getStatus())) + && result.getNextBeginOffset() > -1) { + popBufferMergeService.addCkMock(requestHeader.getConsumerGroup(), topic, queueId, finalOffset, + requestHeader.getInvisibleTime(), popTime, reviveQid, result.getNextBeginOffset(), brokerController.getBrokerConfig().getBrokerName()); +// this.brokerController.getConsumerOffsetManager().commitOffset(channel.remoteAddress().toString(), requestHeader.getConsumerGroup(), topic, +// queueId, getMessageTmpResult.getNextBeginOffset()); + } + + atomicRestNum.set(result.getMaxOffset() - result.getNextBeginOffset() + atomicRestNum.get()); + String brokerName = brokerController.getBrokerConfig().getBrokerName(); + for (SelectMappedBufferResult mapedBuffer : result.getMessageMapedList()) { + // We should not recode buffer when popResponseReturnActualRetryTopic is true or topic is not retry topic + if (brokerController.getBrokerConfig().isPopResponseReturnActualRetryTopic() || !isRetry) { + getMessageResult.addMessage(mapedBuffer); + } else { + List messageExtList = MessageDecoder.decodesBatch(mapedBuffer.getByteBuffer(), + true, false, true); + mapedBuffer.release(); + for (MessageExt messageExt : messageExtList) { + try { + String ckInfo = ExtraInfoUtil.buildExtraInfo(finalOffset, popTime, requestHeader.getInvisibleTime(), + reviveQid, messageExt.getTopic(), brokerName, messageExt.getQueueId(), messageExt.getQueueOffset()); + messageExt.getProperties().putIfAbsent(MessageConst.PROPERTY_POP_CK, ckInfo); + + // Set retry message topic to origin topic and clear message store size to recode + messageExt.setTopic(requestHeader.getTopic()); + messageExt.setStoreSize(0); + + byte[] encode = MessageDecoder.encode(messageExt, false); + ByteBuffer buffer = ByteBuffer.wrap(encode); + SelectMappedBufferResult tmpResult = + new SelectMappedBufferResult(mapedBuffer.getStartOffset(), buffer, encode.length, null); + getMessageResult.addMessage(tmpResult); + } catch (Exception e) { + POP_LOGGER.error("Exception in recode retry message buffer, topic={}", topic, e); + } + } + } + } + this.brokerController.getPopInflightMessageCounter().incrementInFlightMessageNum( + topic, + requestHeader.getConsumerGroup(), + queueId, + result.getMessageCount() + ); + return atomicRestNum.get(); + }).whenComplete((result, throwable) -> { + if (throwable != null) { + POP_LOGGER.error("Pop message error, {}", lockKey, throwable); + } + queueLockManager.unLock(lockKey); + }); + } + + private boolean isPopShouldStop(String topic, String group, int queueId) { + return brokerController.getBrokerConfig().isEnablePopMessageThreshold() && + brokerController.getPopInflightMessageCounter().getGroupPopInFlightMessageNum(topic, group, queueId) > brokerController.getBrokerConfig().getPopInflightMessageThreshold(); + } + + private long getPopOffset(String topic, String group, int queueId, int initMode, boolean init, String lockKey, + boolean checkResetOffset) { + + long offset = this.brokerController.getConsumerOffsetManager().queryOffset(group, topic, queueId); + if (offset < 0) { + offset = this.getInitOffset(topic, group, queueId, initMode, init); + } + + if (checkResetOffset) { + Long resetOffset = resetPopOffset(topic, group, queueId); + if (resetOffset != null) { + return resetOffset; + } + } + + long bufferOffset = this.popBufferMergeService.getLatestOffset(lockKey); + if (bufferOffset < 0) { + return offset; + } else { + return Math.max(bufferOffset, offset); + } + } + + private long getInitOffset(String topic, String group, int queueId, int initMode, boolean init) { + long offset; + if (ConsumeInitMode.MIN == initMode) { + return this.brokerController.getMessageStore().getMinOffsetInQueue(topic, queueId); + } else { + if (this.brokerController.getBrokerConfig().isInitPopOffsetByCheckMsgInMem() && + this.brokerController.getMessageStore().getMinOffsetInQueue(topic, queueId) <= 0 && + this.brokerController.getMessageStore().checkInMemByConsumeOffset(topic, queueId, 0, 1)) { + offset = 0; + } else { + // pop last one,then commit offset. + offset = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId) - 1; + // max & no consumer offset + if (offset < 0) { + offset = 0; + } + } + if (init) { + this.brokerController.getConsumerOffsetManager().commitOffset( + "getPopOffset", group, topic, queueId, offset); + } + } + return offset; + } + + public final MessageExtBrokerInner buildCkMsg(final PopCheckPoint ck, final int reviveQid) { + MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); + + msgInner.setTopic(reviveTopic); + msgInner.setBody(JSON.toJSONString(ck).getBytes(DataConverter.CHARSET_UTF8)); + msgInner.setQueueId(reviveQid); + msgInner.setTags(PopAckConstants.CK_TAG); + msgInner.setBornTimestamp(System.currentTimeMillis()); + msgInner.setBornHost(this.brokerController.getStoreHost()); + msgInner.setStoreHost(this.brokerController.getStoreHost()); + msgInner.setDeliverTimeMs(ck.getReviveTime() - PopAckConstants.ackTimeInterval); + msgInner.getProperties().put(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, genCkUniqueId(ck)); + msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); + + return msgInner; + } + + private boolean appendCheckPoint(final PopMessageRequestHeader requestHeader, + final String topic, final int reviveQid, final int queueId, final long offset, + final GetMessageResult getMessageTmpResult, final long popTime, final String brokerName) { + // add check point msg to revive log + final PopCheckPoint ck = new PopCheckPoint(); + ck.setBitMap(0); + ck.setNum((byte) getMessageTmpResult.getMessageMapedList().size()); + ck.setPopTime(popTime); + ck.setInvisibleTime(requestHeader.getInvisibleTime()); + ck.setStartOffset(offset); + ck.setCId(requestHeader.getConsumerGroup()); + ck.setTopic(topic); + ck.setQueueId(queueId); + ck.setBrokerName(brokerName); + for (Long msgQueueOffset : getMessageTmpResult.getMessageQueueOffset()) { + ck.addDiff((int) (msgQueueOffset - offset)); + } + + final boolean addBufferSuc = this.popBufferMergeService.addCk( + ck, reviveQid, -1, getMessageTmpResult.getNextBeginOffset() + ); + + if (addBufferSuc) { + return true; + } + return this.popBufferMergeService.addCkJustOffset( + ck, reviveQid, -1, getMessageTmpResult.getNextBeginOffset() + ); + } + + private Long resetPopOffset(String topic, String group, int queueId) { + String lockKey = topic + PopAckConstants.SPLIT + group + PopAckConstants.SPLIT + queueId; + Long resetOffset = + this.brokerController.getConsumerOffsetManager().queryThenEraseResetOffset(topic, group, queueId); + if (resetOffset != null) { + this.brokerController.getConsumerOrderInfoManager().clearBlock(topic, group, queueId); + this.getPopBufferMergeService().clearOffsetQueue(lockKey); + this.brokerController.getConsumerOffsetManager() + .commitOffset("ResetPopOffset", group, topic, queueId, resetOffset); + } + return resetOffset; + } + + private byte[] readGetMessageResult(final GetMessageResult getMessageResult, final String group, final String topic, + final int queueId) { + final ByteBuffer byteBuffer = ByteBuffer.allocate(getMessageResult.getBufferTotalSize()); + + long storeTimestamp = 0; + try { + List messageBufferList = getMessageResult.getMessageBufferList(); + for (ByteBuffer bb : messageBufferList) { + + byteBuffer.put(bb); + storeTimestamp = bb.getLong(MessageDecoder.MESSAGE_STORE_TIMESTAMP_POSITION); + } + } finally { + getMessageResult.release(); + } + + this.brokerController.getBrokerStatsManager().recordDiskFallBehindTime(group, topic, queueId, + this.brokerController.getMessageStore().now() - storeTimestamp); + return byteBuffer.array(); + } + + static class TimedLock { + private final AtomicBoolean lock; + private volatile long lockTime; + + public TimedLock() { + this.lock = new AtomicBoolean(true); + this.lockTime = System.currentTimeMillis(); + } + + public boolean tryLock() { + boolean ret = lock.compareAndSet(true, false); + if (ret) { + this.lockTime = System.currentTimeMillis(); + return true; + } else { + return false; + } + } + + public void unLock() { + lock.set(true); + } + + public boolean isLock() { + return !lock.get(); + } + + public long getLockTime() { + return lockTime; + } + } + + public class QueueLockManager extends ServiceThread { + private final ConcurrentHashMap expiredLocalCache = new ConcurrentHashMap<>(100000); + + public String buildLockKey(String topic, String consumerGroup, int queueId) { + return topic + PopAckConstants.SPLIT + consumerGroup + PopAckConstants.SPLIT + queueId; + } + + public boolean tryLock(String topic, String consumerGroup, int queueId) { + return tryLock(buildLockKey(topic, consumerGroup, queueId)); + } + + public boolean tryLock(String key) { + TimedLock timedLock = expiredLocalCache.get(key); + + if (timedLock == null) { + TimedLock old = expiredLocalCache.putIfAbsent(key, new TimedLock()); + if (old != null) { + return false; + } else { + timedLock = expiredLocalCache.get(key); + } + } + + if (timedLock == null) { + return false; + } + + return timedLock.tryLock(); + } + + /** + * is not thread safe, may cause duplicate lock + * + * @param usedExpireMillis the expired time in millisecond + * @return total numbers of TimedLock + */ + public int cleanUnusedLock(final long usedExpireMillis) { + Iterator> iterator = expiredLocalCache.entrySet().iterator(); + + int total = 0; + while (iterator.hasNext()) { + Entry entry = iterator.next(); + + if (System.currentTimeMillis() - entry.getValue().getLockTime() > usedExpireMillis) { + iterator.remove(); + POP_LOGGER.info("Remove unused queue lock: {}, {}, {}", entry.getKey(), + entry.getValue().getLockTime(), + entry.getValue().isLock()); + } + + total++; + } + + return total; + } + + public void unLock(String topic, String consumerGroup, int queueId) { + unLock(buildLockKey(topic, consumerGroup, queueId)); + } + + public void unLock(String key) { + TimedLock timedLock = expiredLocalCache.get(key); + if (timedLock != null) { + timedLock.unLock(); + } + } + + @Override + public String getServiceName() { + if (PopMessageProcessor.this.brokerController.getBrokerConfig().isInBrokerContainer()) { + return PopMessageProcessor.this.brokerController.getBrokerIdentity().getIdentifier() + QueueLockManager.class.getSimpleName(); + } + return QueueLockManager.class.getSimpleName(); + } + + @Override + public void run() { + while (!isStopped()) { + try { + this.waitForRunning(60000); + int count = cleanUnusedLock(60000); + POP_LOGGER.info("QueueLockSize={}", count); + } catch (Exception e) { + PopMessageProcessor.POP_LOGGER.error("QueueLockManager run error", e); + } + } + } + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java new file mode 100644 index 00000000000..104b78d4413 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PopReviveService.java @@ -0,0 +1,689 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.processor; + +import com.alibaba.fastjson.JSON; +import io.opentelemetry.api.common.Attributes; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.TreeMap; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.metrics.BrokerMetricsManager; +import org.apache.rocketmq.broker.metrics.PopMetricsManager; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.consumer.PullStatus; +import org.apache.rocketmq.common.KeyBuilder; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.PopAckConstants; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.TopicFilterType; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.common.utils.DataConverter; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.AppendMessageStatus; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.GetMessageStatus; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.pop.AckMsg; +import org.apache.rocketmq.store.pop.BatchAckMsg; +import org.apache.rocketmq.store.pop.PopCheckPoint; + +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_CONSUMER_GROUP; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_IS_SYSTEM; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_TOPIC; + +public class PopReviveService extends ServiceThread { + private static final Logger POP_LOGGER = LoggerFactory.getLogger(LoggerName.ROCKETMQ_POP_LOGGER_NAME); + + private int queueId; + private BrokerController brokerController; + private String reviveTopic; + private long currentReviveMessageTimestamp = -1; + private volatile boolean shouldRunPopRevive = false; + + private final NavigableMap> inflightReviveRequestMap = Collections.synchronizedNavigableMap(new TreeMap<>()); + private long reviveOffset; + + public PopReviveService(BrokerController brokerController, String reviveTopic, int queueId) { + this.queueId = queueId; + this.brokerController = brokerController; + this.reviveTopic = reviveTopic; + this.reviveOffset = brokerController.getConsumerOffsetManager().queryOffset(PopAckConstants.REVIVE_GROUP, reviveTopic, queueId); + } + + @Override + public String getServiceName() { + if (brokerController != null && brokerController.getBrokerConfig().isInBrokerContainer()) { + return brokerController.getBrokerIdentity().getIdentifier() + "PopReviveService_" + this.queueId; + } + return "PopReviveService_" + this.queueId; + } + + public int getQueueId() { + return queueId; + } + + public void setShouldRunPopRevive(final boolean shouldRunPopRevive) { + this.shouldRunPopRevive = shouldRunPopRevive; + } + + public boolean isShouldRunPopRevive() { + return shouldRunPopRevive; + } + + private boolean reviveRetry(PopCheckPoint popCheckPoint, MessageExt messageExt) { + MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); + if (!popCheckPoint.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + msgInner.setTopic(KeyBuilder.buildPopRetryTopic(popCheckPoint.getTopic(), popCheckPoint.getCId(), brokerController.getBrokerConfig().isEnableRetryTopicV2())); + } else { + msgInner.setTopic(popCheckPoint.getTopic()); + } + msgInner.setBody(messageExt.getBody()); + msgInner.setQueueId(0); + if (messageExt.getTags() != null) { + msgInner.setTags(messageExt.getTags()); + } else { + MessageAccessor.setProperties(msgInner, new HashMap<>()); + } + msgInner.setBornTimestamp(messageExt.getBornTimestamp()); + msgInner.setFlag(messageExt.getFlag()); + msgInner.setSysFlag(messageExt.getSysFlag()); + msgInner.setBornHost(brokerController.getStoreHost()); + msgInner.setStoreHost(brokerController.getStoreHost()); + msgInner.setReconsumeTimes(messageExt.getReconsumeTimes() + 1); + msgInner.getProperties().putAll(messageExt.getProperties()); + if (messageExt.getReconsumeTimes() == 0 || msgInner.getProperties().get(MessageConst.PROPERTY_FIRST_POP_TIME) == null) { + msgInner.getProperties().put(MessageConst.PROPERTY_FIRST_POP_TIME, String.valueOf(popCheckPoint.getPopTime())); + } + msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); + addRetryTopicIfNoExit(msgInner.getTopic(), popCheckPoint.getCId()); + PutMessageResult putMessageResult = brokerController.getEscapeBridge().putMessageToSpecificQueue(msgInner); + PopMetricsManager.incPopReviveRetryMessageCount(popCheckPoint, putMessageResult.getPutMessageStatus()); + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("reviveQueueId={},retry msg, ck={}, msg queueId {}, offset {}, reviveDelay={}, result is {} ", + queueId, popCheckPoint, messageExt.getQueueId(), messageExt.getQueueOffset(), + (System.currentTimeMillis() - popCheckPoint.getReviveTime()) / 1000, putMessageResult); + } + if (putMessageResult.getAppendMessageResult() == null || + putMessageResult.getAppendMessageResult().getStatus() != AppendMessageStatus.PUT_OK) { + POP_LOGGER.error("reviveQueueId={}, revive error, msg is: {}", queueId, msgInner); + return false; + } + this.brokerController.getPopInflightMessageCounter().decrementInFlightMessageNum(popCheckPoint); + this.brokerController.getBrokerStatsManager().incBrokerPutNums(popCheckPoint.getTopic(), 1); + this.brokerController.getBrokerStatsManager().incTopicPutNums(msgInner.getTopic()); + this.brokerController.getBrokerStatsManager().incTopicPutSize(msgInner.getTopic(), putMessageResult.getAppendMessageResult().getWroteBytes()); + return true; + } + + private void initPopRetryOffset(String topic, String consumerGroup) { + long offset = this.brokerController.getConsumerOffsetManager().queryOffset(consumerGroup, topic, 0); + if (offset < 0) { + this.brokerController.getConsumerOffsetManager().commitOffset("initPopRetryOffset", consumerGroup, topic, + 0, 0); + } + } + + private void addRetryTopicIfNoExit(String topic, String consumerGroup) { + if (brokerController != null) { + TopicConfig topicConfig = brokerController.getTopicConfigManager().selectTopicConfig(topic); + if (topicConfig != null) { + return; + } + topicConfig = new TopicConfig(topic); + topicConfig.setReadQueueNums(PopAckConstants.retryQueueNum); + topicConfig.setWriteQueueNums(PopAckConstants.retryQueueNum); + topicConfig.setTopicFilterType(TopicFilterType.SINGLE_TAG); + topicConfig.setPerm(6); + topicConfig.setTopicSysFlag(0); + brokerController.getTopicConfigManager().updateTopicConfig(topicConfig); + + initPopRetryOffset(topic, consumerGroup); + } + } + + protected List getReviveMessage(long offset, int queueId) { + PullResult pullResult = getMessage(PopAckConstants.REVIVE_GROUP, reviveTopic, queueId, offset, 32, true); + if (pullResult == null) { + return null; + } + if (reachTail(pullResult, offset)) { + if (this.brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("reviveQueueId={}, reach tail,offset {}", queueId, offset); + } + } else if (pullResult.getPullStatus() == PullStatus.OFFSET_ILLEGAL || pullResult.getPullStatus() == PullStatus.NO_MATCHED_MSG) { + POP_LOGGER.error("reviveQueueId={}, OFFSET_ILLEGAL {}, result is {}", queueId, offset, pullResult); + if (!shouldRunPopRevive) { + POP_LOGGER.info("slave skip offset correct topic={}, reviveQueueId={}", reviveTopic, queueId); + return null; + } + this.brokerController.getConsumerOffsetManager().commitOffset(PopAckConstants.LOCAL_HOST, PopAckConstants.REVIVE_GROUP, reviveTopic, queueId, pullResult.getNextBeginOffset() - 1); + } + return pullResult.getMsgFoundList(); + } + + private boolean reachTail(PullResult pullResult, long offset) { + return pullResult.getPullStatus() == PullStatus.NO_NEW_MSG + || pullResult.getPullStatus() == PullStatus.OFFSET_ILLEGAL && offset == pullResult.getMaxOffset(); + } + + private CompletableFuture> getBizMessage(String topic, long offset, int queueId, + String brokerName) { + return this.brokerController.getEscapeBridge().getMessageAsync(topic, offset, queueId, brokerName, false); + } + + public PullResult getMessage(String group, String topic, int queueId, long offset, int nums, + boolean deCompressBody) { + GetMessageResult getMessageResult = this.brokerController.getMessageStore().getMessage(group, topic, queueId, offset, nums, null); + + if (getMessageResult != null) { + PullStatus pullStatus = PullStatus.NO_NEW_MSG; + List foundList = null; + switch (getMessageResult.getStatus()) { + case FOUND: + pullStatus = PullStatus.FOUND; + foundList = decodeMsgList(getMessageResult, deCompressBody); + brokerController.getBrokerStatsManager().incGroupGetNums(group, topic, getMessageResult.getMessageCount()); + brokerController.getBrokerStatsManager().incGroupGetSize(group, topic, getMessageResult.getBufferTotalSize()); + brokerController.getBrokerStatsManager().incBrokerGetNums(topic, getMessageResult.getMessageCount()); + brokerController.getBrokerStatsManager().recordDiskFallBehindTime(group, topic, queueId, + brokerController.getMessageStore().now() - foundList.get(foundList.size() - 1).getStoreTimestamp()); + + Attributes attributes = BrokerMetricsManager.newAttributesBuilder() + .put(LABEL_TOPIC, topic) + .put(LABEL_CONSUMER_GROUP, group) + .put(LABEL_IS_SYSTEM, TopicValidator.isSystemTopic(topic) || MixAll.isSysConsumerGroup(group)) + .build(); + BrokerMetricsManager.messagesOutTotal.add(getMessageResult.getMessageCount(), attributes); + BrokerMetricsManager.throughputOutTotal.add(getMessageResult.getBufferTotalSize(), attributes); + + break; + case NO_MATCHED_MESSAGE: + pullStatus = PullStatus.NO_MATCHED_MSG; + POP_LOGGER.debug("no matched message. GetMessageStatus={}, topic={}, groupId={}, requestOffset={}", + getMessageResult.getStatus(), topic, group, offset); + break; + case NO_MESSAGE_IN_QUEUE: + POP_LOGGER.debug("no new message. GetMessageStatus={}, topic={}, groupId={}, requestOffset={}", + getMessageResult.getStatus(), topic, group, offset); + break; + case MESSAGE_WAS_REMOVING: + case NO_MATCHED_LOGIC_QUEUE: + case OFFSET_FOUND_NULL: + case OFFSET_OVERFLOW_BADLY: + case OFFSET_TOO_SMALL: + pullStatus = PullStatus.OFFSET_ILLEGAL; + POP_LOGGER.warn("offset illegal. GetMessageStatus={}, topic={}, groupId={}, requestOffset={}", + getMessageResult.getStatus(), topic, group, offset); + break; + case OFFSET_OVERFLOW_ONE: + // no need to print WARN, because we use "offset + 1" to get the next message + pullStatus = PullStatus.OFFSET_ILLEGAL; + break; + default: + assert false; + break; + } + + return new PullResult(pullStatus, getMessageResult.getNextBeginOffset(), getMessageResult.getMinOffset(), + getMessageResult.getMaxOffset(), foundList); + + } else { + long maxQueueOffset = brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId); + if (maxQueueOffset > offset) { + POP_LOGGER.error("get message from store return null. topic={}, groupId={}, requestOffset={}, maxQueueOffset={}", + topic, group, offset, maxQueueOffset); + } + return null; + } + } + + private List decodeMsgList(GetMessageResult getMessageResult, boolean deCompressBody) { + List foundList = new ArrayList<>(); + try { + List messageBufferList = getMessageResult.getMessageBufferList(); + if (messageBufferList != null) { + for (int i = 0; i < messageBufferList.size(); i++) { + ByteBuffer bb = messageBufferList.get(i); + if (bb == null) { + POP_LOGGER.error("bb is null {}", getMessageResult); + continue; + } + MessageExt msgExt = MessageDecoder.decode(bb, true, deCompressBody); + if (msgExt == null) { + POP_LOGGER.error("decode msgExt is null {}", getMessageResult); + continue; + } + // use CQ offset, not offset in Message + msgExt.setQueueOffset(getMessageResult.getMessageQueueOffset().get(i)); + foundList.add(msgExt); + } + } + } finally { + getMessageResult.release(); + } + + return foundList; + } + + protected void consumeReviveMessage(ConsumeReviveObj consumeReviveObj) { + HashMap map = consumeReviveObj.map; + HashMap mockPointMap = new HashMap<>(); + long startScanTime = System.currentTimeMillis(); + long endTime = 0; + long consumeOffset = this.brokerController.getConsumerOffsetManager().queryOffset(PopAckConstants.REVIVE_GROUP, reviveTopic, queueId); + long oldOffset = Math.max(reviveOffset, consumeOffset); + consumeReviveObj.oldOffset = oldOffset; + POP_LOGGER.info("reviveQueueId={}, old offset is {} ", queueId, oldOffset); + long offset = oldOffset + 1; + int noMsgCount = 0; + long firstRt = 0; + // offset self amend + while (true) { + if (!shouldRunPopRevive) { + POP_LOGGER.info("slave skip scan, revive topic={}, reviveQueueId={}", reviveTopic, queueId); + break; + } + List messageExts = getReviveMessage(offset, queueId); + if (messageExts == null || messageExts.isEmpty()) { + long old = endTime; + long timerDelay = brokerController.getMessageStore().getTimerMessageStore().getDequeueBehind(); + long commitLogDelay = brokerController.getMessageStore().getTimerMessageStore().getEnqueueBehind(); + // move endTime + if (endTime != 0 && System.currentTimeMillis() - endTime > 3 * PopAckConstants.SECOND && timerDelay <= 0 && commitLogDelay <= 0) { + endTime = System.currentTimeMillis(); + } + POP_LOGGER.info("reviveQueueId={}, offset is {}, can not get new msg, old endTime {}, new endTime {}, timerDelay={}, commitLogDelay={} ", + queueId, offset, old, endTime, timerDelay, commitLogDelay); + if (endTime - firstRt > PopAckConstants.ackTimeInterval + PopAckConstants.SECOND) { + break; + } + noMsgCount++; + // Fixme: why sleep is useful here? + try { + Thread.sleep(100); + } catch (Throwable ignore) { + } + if (noMsgCount * 100L > 4 * PopAckConstants.SECOND) { + break; + } else { + continue; + } + } else { + noMsgCount = 0; + } + if (System.currentTimeMillis() - startScanTime > brokerController.getBrokerConfig().getReviveScanTime()) { + POP_LOGGER.info("reviveQueueId={}, scan timeout ", queueId); + break; + } + for (MessageExt messageExt : messageExts) { + if (PopAckConstants.CK_TAG.equals(messageExt.getTags())) { + String raw = new String(messageExt.getBody(), DataConverter.CHARSET_UTF8); + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("reviveQueueId={},find ck, offset:{}, raw : {}", messageExt.getQueueId(), messageExt.getQueueOffset(), raw); + } + PopCheckPoint point = JSON.parseObject(raw, PopCheckPoint.class); + if (point.getTopic() == null || point.getCId() == null) { + continue; + } + map.put(point.getTopic() + point.getCId() + point.getQueueId() + point.getStartOffset() + point.getPopTime(), point); + PopMetricsManager.incPopReviveCkGetCount(point, queueId); + point.setReviveOffset(messageExt.getQueueOffset()); + if (firstRt == 0) { + firstRt = point.getReviveTime(); + } + } else if (PopAckConstants.ACK_TAG.equals(messageExt.getTags())) { + String raw = new String(messageExt.getBody(), DataConverter.CHARSET_UTF8); + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("reviveQueueId={}, find ack, offset:{}, raw : {}", messageExt.getQueueId(), messageExt.getQueueOffset(), raw); + } + AckMsg ackMsg = JSON.parseObject(raw, AckMsg.class); + PopMetricsManager.incPopReviveAckGetCount(ackMsg, queueId); + String mergeKey = ackMsg.getTopic() + ackMsg.getConsumerGroup() + ackMsg.getQueueId() + ackMsg.getStartOffset() + ackMsg.getPopTime(); + PopCheckPoint point = map.get(mergeKey); + if (point == null) { + if (!brokerController.getBrokerConfig().isEnableSkipLongAwaitingAck()) { + continue; + } + if (mockCkForAck(messageExt, ackMsg, mergeKey, mockPointMap) && firstRt == 0) { + firstRt = mockPointMap.get(mergeKey).getReviveTime(); + } + } else { + int indexOfAck = point.indexOfAck(ackMsg.getAckOffset()); + if (indexOfAck > -1) { + point.setBitMap(DataConverter.setBit(point.getBitMap(), indexOfAck, true)); + } else { + POP_LOGGER.error("invalid ack index, {}, {}", ackMsg, point); + } + } + } else if (PopAckConstants.BATCH_ACK_TAG.equals(messageExt.getTags())) { + String raw = new String(messageExt.getBody(), DataConverter.CHARSET_UTF8); + if (brokerController.getBrokerConfig().isEnablePopLog()) { + POP_LOGGER.info("reviveQueueId={}, find batch ack, offset:{}, raw : {}", messageExt.getQueueId(), messageExt.getQueueOffset(), raw); + } + + BatchAckMsg bAckMsg = JSON.parseObject(raw, BatchAckMsg.class); + PopMetricsManager.incPopReviveAckGetCount(bAckMsg, queueId); + String mergeKey = bAckMsg.getTopic() + bAckMsg.getConsumerGroup() + bAckMsg.getQueueId() + bAckMsg.getStartOffset() + bAckMsg.getPopTime(); + PopCheckPoint point = map.get(mergeKey); + if (point == null) { + if (!brokerController.getBrokerConfig().isEnableSkipLongAwaitingAck()) { + continue; + } + if (mockCkForAck(messageExt, bAckMsg, mergeKey, mockPointMap) && firstRt == 0) { + firstRt = mockPointMap.get(mergeKey).getReviveTime(); + } + } else { + List ackOffsetList = bAckMsg.getAckOffsetList(); + for (Long ackOffset : ackOffsetList) { + int indexOfAck = point.indexOfAck(ackOffset); + if (indexOfAck > -1) { + point.setBitMap(DataConverter.setBit(point.getBitMap(), indexOfAck, true)); + } else { + POP_LOGGER.error("invalid batch ack index, {}, {}", bAckMsg, point); + } + } + } + } + long deliverTime = messageExt.getDeliverTimeMs(); + if (deliverTime > endTime) { + endTime = deliverTime; + } + } + offset = offset + messageExts.size(); + } + consumeReviveObj.map.putAll(mockPointMap); + consumeReviveObj.endTime = endTime; + } + + private boolean mockCkForAck(MessageExt messageExt, AckMsg ackMsg, String mergeKey, HashMap mockPointMap) { + long ackWaitTime = System.currentTimeMillis() - messageExt.getDeliverTimeMs(); + long reviveAckWaitMs = brokerController.getBrokerConfig().getReviveAckWaitMs(); + if (ackWaitTime > reviveAckWaitMs) { + // will use the reviveOffset of popCheckPoint to commit offset in mergeAndRevive + PopCheckPoint mockPoint = createMockCkForAck(ackMsg, messageExt.getQueueOffset()); + POP_LOGGER.warn( + "ack wait for {}ms cannot find ck, skip this ack. mergeKey:{}, ack:{}, mockCk:{}", + reviveAckWaitMs, mergeKey, ackMsg, mockPoint); + mockPointMap.put(mergeKey, mockPoint); + return true; + } + return false; + } + + private PopCheckPoint createMockCkForAck(AckMsg ackMsg, long reviveOffset) { + PopCheckPoint point = new PopCheckPoint(); + point.setStartOffset(ackMsg.getStartOffset()); + point.setPopTime(ackMsg.getPopTime()); + point.setQueueId(ackMsg.getQueueId()); + point.setCId(ackMsg.getConsumerGroup()); + point.setTopic(ackMsg.getTopic()); + point.setNum((byte) 0); + point.setBitMap(0); + point.setReviveOffset(reviveOffset); + point.setBrokerName(ackMsg.getBrokerName()); + return point; + } + + protected void mergeAndRevive(ConsumeReviveObj consumeReviveObj) throws Throwable { + ArrayList sortList = consumeReviveObj.genSortList(); + POP_LOGGER.info("reviveQueueId={}, ck listSize={}", queueId, sortList.size()); + if (sortList.size() != 0) { + POP_LOGGER.info("reviveQueueId={}, 1st ck, startOffset={}, reviveOffset={}; last ck, startOffset={}, reviveOffset={}", queueId, sortList.get(0).getStartOffset(), + sortList.get(0).getReviveOffset(), sortList.get(sortList.size() - 1).getStartOffset(), sortList.get(sortList.size() - 1).getReviveOffset()); + } + long newOffset = consumeReviveObj.oldOffset; + for (PopCheckPoint popCheckPoint : sortList) { + if (!shouldRunPopRevive) { + POP_LOGGER.info("slave skip ck process, revive topic={}, reviveQueueId={}", reviveTopic, queueId); + break; + } + if (consumeReviveObj.endTime - popCheckPoint.getReviveTime() <= (PopAckConstants.ackTimeInterval + PopAckConstants.SECOND)) { + break; + } + + // check normal topic, skip ck , if normal topic is not exist + String normalTopic = KeyBuilder.parseNormalTopic(popCheckPoint.getTopic(), popCheckPoint.getCId()); + if (brokerController.getTopicConfigManager().selectTopicConfig(normalTopic) == null) { + POP_LOGGER.warn("reviveQueueId={}, can not get normal topic {}, then continue", queueId, popCheckPoint.getTopic()); + newOffset = popCheckPoint.getReviveOffset(); + continue; + } + if (null == brokerController.getSubscriptionGroupManager().findSubscriptionGroupConfig(popCheckPoint.getCId())) { + POP_LOGGER.warn("reviveQueueId={}, can not get cid {}, then continue", queueId, popCheckPoint.getCId()); + newOffset = popCheckPoint.getReviveOffset(); + continue; + } + + while (inflightReviveRequestMap.size() > 3) { + waitForRunning(100); + Pair pair = inflightReviveRequestMap.firstEntry().getValue(); + if (!pair.getObject2() && System.currentTimeMillis() - pair.getObject1() > 1000 * 30) { + PopCheckPoint oldCK = inflightReviveRequestMap.firstKey(); + rePutCK(oldCK, pair); + inflightReviveRequestMap.remove(oldCK); + } + } + + reviveMsgFromCk(popCheckPoint); + + newOffset = popCheckPoint.getReviveOffset(); + } + if (newOffset > consumeReviveObj.oldOffset) { + if (!shouldRunPopRevive) { + POP_LOGGER.info("slave skip commit, revive topic={}, reviveQueueId={}", reviveTopic, queueId); + return; + } + this.brokerController.getConsumerOffsetManager().commitOffset(PopAckConstants.LOCAL_HOST, PopAckConstants.REVIVE_GROUP, reviveTopic, queueId, newOffset); + } + reviveOffset = newOffset; + consumeReviveObj.newOffset = newOffset; + } + + private void reviveMsgFromCk(PopCheckPoint popCheckPoint) { + if (!shouldRunPopRevive) { + POP_LOGGER.info("slave skip retry, revive topic={}, reviveQueueId={}", reviveTopic, queueId); + return; + } + inflightReviveRequestMap.put(popCheckPoint, new Pair<>(System.currentTimeMillis(), false)); + List>> futureList = new ArrayList<>(popCheckPoint.getNum()); + for (int j = 0; j < popCheckPoint.getNum(); j++) { + if (DataConverter.getBit(popCheckPoint.getBitMap(), j)) { + continue; + } + + // retry msg + long msgOffset = popCheckPoint.ackOffsetByIndex((byte) j); + CompletableFuture> future = getBizMessage(popCheckPoint.getTopic(), msgOffset, popCheckPoint.getQueueId(), popCheckPoint.getBrokerName()) + .thenApply(resultPair -> { + GetMessageStatus getMessageStatus = resultPair.getObject1(); + MessageExt message = resultPair.getObject2(); + if (message == null) { + POP_LOGGER.warn("reviveQueueId={}, can not get biz msg topic is {}, offset is {}, then continue", + queueId, popCheckPoint.getTopic(), msgOffset); + switch (getMessageStatus) { + case MESSAGE_WAS_REMOVING: + case OFFSET_TOO_SMALL: + case NO_MATCHED_LOGIC_QUEUE: + case NO_MESSAGE_IN_QUEUE: + return new Pair<>(msgOffset, true); + default: + return new Pair<>(msgOffset, false); + + } + } + //skip ck from last epoch + if (popCheckPoint.getPopTime() < message.getStoreTimestamp()) { + POP_LOGGER.warn("reviveQueueId={}, skip ck from last epoch {}", queueId, popCheckPoint); + return new Pair<>(msgOffset, true); + } + boolean result = reviveRetry(popCheckPoint, message); + return new Pair<>(msgOffset, result); + }); + futureList.add(future); + } + CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])) + .whenComplete((v, e) -> { + for (CompletableFuture> future : futureList) { + Pair pair = future.getNow(new Pair<>(0L, false)); + if (!pair.getObject2()) { + rePutCK(popCheckPoint, pair); + } + } + + if (inflightReviveRequestMap.containsKey(popCheckPoint)) { + inflightReviveRequestMap.get(popCheckPoint).setObject2(true); + } + for (Map.Entry> entry : inflightReviveRequestMap.entrySet()) { + PopCheckPoint oldCK = entry.getKey(); + Pair pair = entry.getValue(); + if (pair.getObject2()) { + brokerController.getConsumerOffsetManager().commitOffset(PopAckConstants.LOCAL_HOST, PopAckConstants.REVIVE_GROUP, reviveTopic, queueId, oldCK.getReviveOffset()); + inflightReviveRequestMap.remove(oldCK); + } else { + break; + } + } + }); + } + + private void rePutCK(PopCheckPoint oldCK, Pair pair) { + PopCheckPoint newCk = new PopCheckPoint(); + newCk.setBitMap(0); + newCk.setNum((byte) 1); + newCk.setPopTime(oldCK.getPopTime()); + newCk.setInvisibleTime(oldCK.getInvisibleTime()); + newCk.setStartOffset(pair.getObject1()); + newCk.setCId(oldCK.getCId()); + newCk.setTopic(oldCK.getTopic()); + newCk.setQueueId(oldCK.getQueueId()); + newCk.setBrokerName(oldCK.getBrokerName()); + newCk.addDiff(0); + MessageExtBrokerInner ckMsg = brokerController.getPopMessageProcessor().buildCkMsg(newCk, queueId); + brokerController.getMessageStore().putMessage(ckMsg); + } + + public long getReviveBehindMillis() { + if (currentReviveMessageTimestamp <= 0) { + return 0; + } + long maxOffset = brokerController.getMessageStore().getMaxOffsetInQueue(reviveTopic, queueId); + if (maxOffset - reviveOffset > 1) { + return Math.max(0, System.currentTimeMillis() - currentReviveMessageTimestamp); + } + return 0; + } + + public long getReviveBehindMessages() { + if (currentReviveMessageTimestamp <= 0) { + return 0; + } + long diff = brokerController.getMessageStore().getMaxOffsetInQueue(reviveTopic, queueId) - reviveOffset; + return Math.max(0, diff); + } + + @Override + public void run() { + int slow = 1; + while (!this.isStopped()) { + try { + if (System.currentTimeMillis() < brokerController.getShouldStartTime()) { + POP_LOGGER.info("PopReviveService Ready to run after {}", brokerController.getShouldStartTime()); + this.waitForRunning(1000); + continue; + } + this.waitForRunning(brokerController.getBrokerConfig().getReviveInterval()); + if (!shouldRunPopRevive) { + POP_LOGGER.info("skip start revive topic={}, reviveQueueId={}", reviveTopic, queueId); + continue; + } + + if (!brokerController.getMessageStore().getMessageStoreConfig().isTimerWheelEnable()) { + POP_LOGGER.warn("skip revive topic because timerWheelEnable is false"); + continue; + } + + POP_LOGGER.info("start revive topic={}, reviveQueueId={}", reviveTopic, queueId); + ConsumeReviveObj consumeReviveObj = new ConsumeReviveObj(); + consumeReviveMessage(consumeReviveObj); + + if (!shouldRunPopRevive) { + POP_LOGGER.info("slave skip scan, revive topic={}, reviveQueueId={}", reviveTopic, queueId); + continue; + } + + mergeAndRevive(consumeReviveObj); + + ArrayList sortList = consumeReviveObj.sortList; + long delay = 0; + if (sortList != null && !sortList.isEmpty()) { + delay = (System.currentTimeMillis() - sortList.get(0).getReviveTime()) / 1000; + currentReviveMessageTimestamp = sortList.get(0).getReviveTime(); + slow = 1; + } else { + currentReviveMessageTimestamp = System.currentTimeMillis(); + } + + POP_LOGGER.info("reviveQueueId={}, revive finish,old offset is {}, new offset is {}, ckDelay={} ", + queueId, consumeReviveObj.oldOffset, consumeReviveObj.newOffset, delay); + + if (sortList == null || sortList.isEmpty()) { + POP_LOGGER.info("reviveQueueId={}, has no new msg, take a rest {}", queueId, slow); + this.waitForRunning(slow * brokerController.getBrokerConfig().getReviveInterval()); + if (slow < brokerController.getBrokerConfig().getReviveMaxSlow()) { + slow++; + } + } + + } catch (Throwable e) { + POP_LOGGER.error("reviveQueueId={}, revive error", queueId, e); + } + } + } + + static class ConsumeReviveObj { + HashMap map = new HashMap<>(); + ArrayList sortList; + long oldOffset; + long endTime; + long newOffset; + + ArrayList genSortList() { + if (sortList != null) { + return sortList; + } + sortList = new ArrayList<>(map.values()); + sortList.sort((o1, o2) -> (int) (o1.getReviveOffset() - o2.getReviveOffset())); + return sortList; + } + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/PullMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/PullMessageProcessor.java index a46cbff2ebe..d53454f215d 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/PullMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/PullMessageProcessor.java @@ -17,14 +17,14 @@ package org.apache.rocketmq.broker.processor; import io.netty.channel.Channel; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.FileRegion; -import java.nio.ByteBuffer; import java.util.List; +import java.util.Objects; import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.client.ClientChannelInfo; import org.apache.rocketmq.broker.client.ConsumerGroupInfo; +import org.apache.rocketmq.broker.client.ConsumerManager; +import org.apache.rocketmq.broker.coldctr.ColdDataPullRequestHoldService; import org.apache.rocketmq.broker.filter.ConsumerFilterData; import org.apache.rocketmq.broker.filter.ConsumerFilterManager; import org.apache.rocketmq.broker.filter.ExpressionForRetryMessageFilter; @@ -32,62 +32,275 @@ import org.apache.rocketmq.broker.longpolling.PullRequest; import org.apache.rocketmq.broker.mqtrace.ConsumeMessageContext; import org.apache.rocketmq.broker.mqtrace.ConsumeMessageHook; -import org.apache.rocketmq.broker.pagecache.ManyMessageTransfer; +import org.apache.rocketmq.broker.plugin.PullMessageResultHandler; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.TopicConfig; -import org.apache.rocketmq.common.TopicFilterType; import org.apache.rocketmq.common.constant.LoggerName; import org.apache.rocketmq.common.constant.PermName; import org.apache.rocketmq.common.filter.ExpressionType; -import org.apache.rocketmq.common.filter.FilterAPI; import org.apache.rocketmq.common.help.FAQUrl; -import org.apache.rocketmq.common.message.MessageDecoder; -import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.ResponseCode; -import org.apache.rocketmq.common.protocol.header.PullMessageRequestHeader; -import org.apache.rocketmq.common.protocol.header.PullMessageResponseHeader; -import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; -import org.apache.rocketmq.common.protocol.topic.OffsetMovedEvent; -import org.apache.rocketmq.common.subscription.SubscriptionGroupConfig; import org.apache.rocketmq.common.sysflag.PullSysFlag; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.remoting.common.RemotingHelper; -import org.apache.rocketmq.remoting.common.RemotingUtil; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.netty.NettyRemotingAbstract; import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; import org.apache.rocketmq.remoting.netty.RequestTask; +import org.apache.rocketmq.remoting.protocol.ForbiddenType; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.RequestSource; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.filter.FilterAPI; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PullMessageResponseHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.remoting.protocol.statictopic.LogicQueueMappingItem; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingContext; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingDetail; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingUtils; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.remoting.rpc.RpcClientUtils; +import org.apache.rocketmq.remoting.rpc.RpcRequest; +import org.apache.rocketmq.remoting.rpc.RpcResponse; +import org.apache.rocketmq.store.DefaultMessageStore; import org.apache.rocketmq.store.GetMessageResult; -import org.apache.rocketmq.store.MessageExtBrokerInner; +import org.apache.rocketmq.store.GetMessageStatus; import org.apache.rocketmq.store.MessageFilter; -import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.MessageStore; import org.apache.rocketmq.store.config.BrokerRole; import org.apache.rocketmq.store.stats.BrokerStatsManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; + +import static org.apache.rocketmq.remoting.protocol.RemotingCommand.buildErrorResponse; public class PullMessageProcessor implements NettyRequestProcessor { - private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); - private final BrokerController brokerController; + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); private List consumeMessageHookList; + private PullMessageResultHandler pullMessageResultHandler; + private final BrokerController brokerController; public PullMessageProcessor(final BrokerController brokerController) { this.brokerController = brokerController; + this.pullMessageResultHandler = new DefaultPullMessageResultHandler(brokerController); + } + + private RemotingCommand rewriteRequestForStaticTopic(PullMessageRequestHeader requestHeader, + TopicQueueMappingContext mappingContext) { + try { + if (mappingContext.getMappingDetail() == null) { + return null; + } + TopicQueueMappingDetail mappingDetail = mappingContext.getMappingDetail(); + String topic = mappingContext.getTopic(); + Integer globalId = mappingContext.getGlobalId(); + // if the leader? consider the order consumer, which will lock the mq + if (!mappingContext.isLeader()) { + return buildErrorResponse(ResponseCode.NOT_LEADER_FOR_QUEUE, String.format("%s-%d cannot find mapping item in request process of current broker %s", topic, globalId, mappingDetail.getBname())); + } + + Long globalOffset = requestHeader.getQueueOffset(); + LogicQueueMappingItem mappingItem = TopicQueueMappingUtils.findLogicQueueMappingItem(mappingContext.getMappingItemList(), globalOffset, true); + mappingContext.setCurrentItem(mappingItem); + + if (globalOffset < mappingItem.getLogicOffset()) { + //handleOffsetMoved + //If the physical queue is reused, we should handle the PULL_OFFSET_MOVED independently + //Otherwise, we could just transfer it to the physical process + } + //below are physical info + String bname = mappingItem.getBname(); + Integer phyQueueId = mappingItem.getQueueId(); + Long phyQueueOffset = mappingItem.computePhysicalQueueOffset(globalOffset); + requestHeader.setQueueId(phyQueueId); + requestHeader.setQueueOffset(phyQueueOffset); + if (mappingItem.checkIfEndOffsetDecided() + && requestHeader.getMaxMsgNums() != null) { + requestHeader.setMaxMsgNums((int) Math.min(mappingItem.getEndOffset() - mappingItem.getStartOffset(), requestHeader.getMaxMsgNums())); + } + + if (mappingDetail.getBname().equals(bname)) { + //just let it go, do the local pull process + return null; + } + + int sysFlag = requestHeader.getSysFlag(); + requestHeader.setLo(false); + requestHeader.setBrokerName(bname); + sysFlag = PullSysFlag.clearSuspendFlag(sysFlag); + sysFlag = PullSysFlag.clearCommitOffsetFlag(sysFlag); + requestHeader.setSysFlag(sysFlag); + RpcRequest rpcRequest = new RpcRequest(RequestCode.PULL_MESSAGE, requestHeader, null); + RpcResponse rpcResponse = this.brokerController.getBrokerOuterAPI().getRpcClient().invoke(rpcRequest, this.brokerController.getBrokerConfig().getForwardTimeout()).get(); + if (rpcResponse.getException() != null) { + throw rpcResponse.getException(); + } + + PullMessageResponseHeader responseHeader = (PullMessageResponseHeader) rpcResponse.getHeader(); + { + RemotingCommand rewriteResult = rewriteResponseForStaticTopic(requestHeader, responseHeader, mappingContext, rpcResponse.getCode()); + if (rewriteResult != null) { + return rewriteResult; + } + } + return RpcClientUtils.createCommandForRpcResponse(rpcResponse); + } catch (Throwable t) { + LOGGER.warn("", t); + return buildErrorResponse(ResponseCode.SYSTEM_ERROR, t.toString()); + } + } + + protected RemotingCommand rewriteResponseForStaticTopic(PullMessageRequestHeader requestHeader, + PullMessageResponseHeader responseHeader, + TopicQueueMappingContext mappingContext, final int code) { + try { + if (mappingContext.getMappingDetail() == null) { + return null; + } + TopicQueueMappingDetail mappingDetail = mappingContext.getMappingDetail(); + LogicQueueMappingItem leaderItem = mappingContext.getLeaderItem(); + + LogicQueueMappingItem currentItem = mappingContext.getCurrentItem(); + + LogicQueueMappingItem earlistItem = TopicQueueMappingUtils.findLogicQueueMappingItem(mappingContext.getMappingItemList(), 0L, true); + + assert currentItem.getLogicOffset() >= 0; + + long requestOffset = requestHeader.getQueueOffset(); + long nextBeginOffset = responseHeader.getNextBeginOffset(); + long minOffset = responseHeader.getMinOffset(); + long maxOffset = responseHeader.getMaxOffset(); + int responseCode = code; + + //consider the following situations + // 1. read from slave, currently not supported + // 2. the middle queue is truncated because of deleting commitlog + if (code != ResponseCode.SUCCESS) { + //note the currentItem maybe both the leader and the earliest + boolean isRevised = false; + if (leaderItem.getGen() == currentItem.getGen()) { + //read the leader + if (requestOffset > maxOffset) { + //actually, we need do nothing, but keep the code structure here + if (code == ResponseCode.PULL_OFFSET_MOVED) { + responseCode = ResponseCode.PULL_OFFSET_MOVED; + nextBeginOffset = maxOffset; + } else { + //maybe current broker is the slave + responseCode = code; + } + } else if (requestOffset < minOffset) { + nextBeginOffset = minOffset; + responseCode = ResponseCode.PULL_RETRY_IMMEDIATELY; + } else { + responseCode = code; + } + } + //note the currentItem maybe both the leader and the earliest + if (earlistItem.getGen() == currentItem.getGen()) { + //read the earliest one + if (requestOffset < minOffset) { + if (code == ResponseCode.PULL_OFFSET_MOVED) { + responseCode = ResponseCode.PULL_OFFSET_MOVED; + nextBeginOffset = minOffset; + } else { + //maybe read from slave, but we still set it to moved + responseCode = ResponseCode.PULL_OFFSET_MOVED; + nextBeginOffset = minOffset; + } + } else if (requestOffset >= maxOffset) { + //just move to another item + LogicQueueMappingItem nextItem = TopicQueueMappingUtils.findNext(mappingContext.getMappingItemList(), currentItem, true); + if (nextItem != null) { + isRevised = true; + currentItem = nextItem; + nextBeginOffset = currentItem.getStartOffset(); + minOffset = currentItem.getStartOffset(); + maxOffset = minOffset; + responseCode = ResponseCode.PULL_RETRY_IMMEDIATELY; + } else { + //maybe the next one's logic offset is -1 + responseCode = ResponseCode.PULL_NOT_FOUND; + } + } else { + //let it go + responseCode = code; + } + } + + //read from the middle item, ignore the PULL_OFFSET_MOVED + if (!isRevised + && leaderItem.getGen() != currentItem.getGen() + && earlistItem.getGen() != currentItem.getGen()) { + if (requestOffset < minOffset) { + nextBeginOffset = minOffset; + responseCode = ResponseCode.PULL_RETRY_IMMEDIATELY; + } else if (requestOffset >= maxOffset) { + //just move to another item + LogicQueueMappingItem nextItem = TopicQueueMappingUtils.findNext(mappingContext.getMappingItemList(), currentItem, true); + if (nextItem != null) { + currentItem = nextItem; + nextBeginOffset = currentItem.getStartOffset(); + minOffset = currentItem.getStartOffset(); + maxOffset = minOffset; + responseCode = ResponseCode.PULL_RETRY_IMMEDIATELY; + } else { + //maybe the next one's logic offset is -1 + responseCode = ResponseCode.PULL_NOT_FOUND; + } + } else { + responseCode = code; + } + } + } + + //handle nextBeginOffset + //the next begin offset should no more than the end offset + if (currentItem.checkIfEndOffsetDecided() + && nextBeginOffset >= currentItem.getEndOffset()) { + nextBeginOffset = currentItem.getEndOffset(); + } + responseHeader.setNextBeginOffset(currentItem.computeStaticQueueOffsetStrictly(nextBeginOffset)); + //handle min offset + responseHeader.setMinOffset(currentItem.computeStaticQueueOffsetStrictly(Math.max(currentItem.getStartOffset(), minOffset))); + //handle max offset + responseHeader.setMaxOffset(Math.max(currentItem.computeStaticQueueOffsetStrictly(maxOffset), + TopicQueueMappingDetail.computeMaxOffsetFromMapping(mappingDetail, mappingContext.getGlobalId()))); + //set the offsetDelta + responseHeader.setOffsetDelta(currentItem.computeOffsetDelta()); + + if (code != ResponseCode.SUCCESS) { + return RemotingCommand.createResponseCommandWithHeader(responseCode, responseHeader); + } else { + return null; + } + } catch (Throwable t) { + LOGGER.warn("", t); + return buildErrorResponse(ResponseCode.SYSTEM_ERROR, t.toString()); + } } @Override public RemotingCommand processRequest(final ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException { - return this.processRequest(ctx.channel(), request, true); + return this.processRequest(ctx.channel(), request, true, true); } @Override public boolean rejectRequest() { + if (!this.brokerController.getBrokerConfig().isSlaveReadEnable() + && this.brokerController.getMessageStoreConfig().getBrokerRole() == BrokerRole.SLAVE) { + return true; + } return false; } - private RemotingCommand processRequest(final Channel channel, RemotingCommand request, boolean brokerAllowSuspend) + private RemotingCommand processRequest(final Channel channel, RemotingCommand request, boolean brokerAllowSuspend, boolean brokerAllowFlowCtrSuspend) throws RemotingCommandException { + final long beginTimeMills = this.brokerController.getMessageStore().now(); RemotingCommand response = RemotingCommand.createResponseCommand(PullMessageResponseHeader.class); final PullMessageResponseHeader responseHeader = (PullMessageResponseHeader) response.readCustomHeader(); final PullMessageRequestHeader requestHeader = @@ -95,11 +308,21 @@ private RemotingCommand processRequest(final Channel channel, RemotingCommand re response.setOpaque(request.getOpaque()); - log.debug("receive PullMessage request command, {}", request); + LOGGER.debug("receive PullMessage request command, {}", request); if (!PermName.isReadable(this.brokerController.getBrokerConfig().getBrokerPermission())) { response.setCode(ResponseCode.NO_PERMISSION); - response.setRemark(String.format("the broker[%s] pulling message is forbidden", this.brokerController.getBrokerConfig().getBrokerIP1())); + responseHeader.setForbiddenType(ForbiddenType.BROKER_FORBIDDEN); + response.setRemark(String.format("the broker[%s] pulling message is forbidden", + this.brokerController.getBrokerConfig().getBrokerIP1())); + return response; + } + + if (request.getCode() == RequestCode.LITE_PULL_MESSAGE && !this.brokerController.getBrokerConfig().isLitePullMessageEnable()) { + response.setCode(ResponseCode.NO_PERMISSION); + responseHeader.setForbiddenType(ForbiddenType.BROKER_FORBIDDEN); + response.setRemark( + "the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1() + "] for lite pull consumer is forbidden"); return response; } @@ -113,19 +336,14 @@ private RemotingCommand processRequest(final Channel channel, RemotingCommand re if (!subscriptionGroupConfig.isConsumeEnable()) { response.setCode(ResponseCode.NO_PERMISSION); + responseHeader.setForbiddenType(ForbiddenType.GROUP_FORBIDDEN); response.setRemark("subscription group no permission, " + requestHeader.getConsumerGroup()); return response; } - final boolean hasSuspendFlag = PullSysFlag.hasSuspendFlag(requestHeader.getSysFlag()); - final boolean hasCommitOffsetFlag = PullSysFlag.hasCommitOffsetFlag(requestHeader.getSysFlag()); - final boolean hasSubscriptionFlag = PullSysFlag.hasSubscriptionFlag(requestHeader.getSysFlag()); - - final long suspendTimeoutMillisLong = hasSuspendFlag ? requestHeader.getSuspendTimeoutMillis() : 0; - TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic()); if (null == topicConfig) { - log.error("the topic {} not exist, consumer: {}", requestHeader.getTopic(), RemotingHelper.parseChannelRemoteAddr(channel)); + LOGGER.error("the topic {} not exist, consumer: {}", requestHeader.getTopic(), RemotingHelper.parseChannelRemoteAddr(channel)); response.setCode(ResponseCode.TOPIC_NOT_EXIST); response.setRemark(String.format("topic[%s] not exist, apply first please! %s", requestHeader.getTopic(), FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL))); return response; @@ -133,26 +351,52 @@ private RemotingCommand processRequest(final Channel channel, RemotingCommand re if (!PermName.isReadable(topicConfig.getPerm())) { response.setCode(ResponseCode.NO_PERMISSION); + responseHeader.setForbiddenType(ForbiddenType.TOPIC_FORBIDDEN); response.setRemark("the topic[" + requestHeader.getTopic() + "] pulling message is forbidden"); return response; } + TopicQueueMappingContext mappingContext = this.brokerController.getTopicQueueMappingManager().buildTopicQueueMappingContext(requestHeader, false); + + { + RemotingCommand rewriteResult = rewriteRequestForStaticTopic(requestHeader, mappingContext); + if (rewriteResult != null) { + return rewriteResult; + } + } + if (requestHeader.getQueueId() < 0 || requestHeader.getQueueId() >= topicConfig.getReadQueueNums()) { String errorInfo = String.format("queueId[%d] is illegal, topic:[%s] topicConfig.readQueueNums:[%d] consumer:[%s]", requestHeader.getQueueId(), requestHeader.getTopic(), topicConfig.getReadQueueNums(), channel.remoteAddress()); - log.warn(errorInfo); + LOGGER.warn(errorInfo); response.setCode(ResponseCode.SYSTEM_ERROR); response.setRemark(errorInfo); return response; } + ConsumerManager consumerManager = brokerController.getConsumerManager(); + switch (RequestSource.parseInteger(requestHeader.getRequestSource())) { + case PROXY_FOR_BROADCAST: + consumerManager.compensateBasicConsumerInfo(requestHeader.getConsumerGroup(), ConsumeType.CONSUME_PASSIVELY, MessageModel.BROADCASTING); + break; + case PROXY_FOR_STREAM: + consumerManager.compensateBasicConsumerInfo(requestHeader.getConsumerGroup(), ConsumeType.CONSUME_ACTIVELY, MessageModel.CLUSTERING); + break; + default: + consumerManager.compensateBasicConsumerInfo(requestHeader.getConsumerGroup(), ConsumeType.CONSUME_PASSIVELY, MessageModel.CLUSTERING); + break; + } + SubscriptionData subscriptionData = null; ConsumerFilterData consumerFilterData = null; + final boolean hasSubscriptionFlag = PullSysFlag.hasSubscriptionFlag(requestHeader.getSysFlag()); if (hasSubscriptionFlag) { try { subscriptionData = FilterAPI.build( requestHeader.getTopic(), requestHeader.getSubscription(), requestHeader.getExpressionType() ); + consumerManager.compensateSubscribeData(requestHeader.getConsumerGroup(), requestHeader.getTopic(), subscriptionData); + if (!ExpressionType.isTagType(subscriptionData.getExpressionType())) { consumerFilterData = ConsumerFilterManager.build( requestHeader.getTopic(), requestHeader.getConsumerGroup(), requestHeader.getSubscription(), @@ -161,7 +405,7 @@ private RemotingCommand processRequest(final Channel channel, RemotingCommand re assert consumerFilterData != null; } } catch (Exception e) { - log.warn("Parse the consumer's subscription[{}] failed, group: {}", requestHeader.getSubscription(), + LOGGER.warn("Parse the consumer's subscription[{}] failed, group: {}", requestHeader.getSubscription(), requestHeader.getConsumerGroup()); response.setCode(ResponseCode.SUBSCRIPTION_PARSE_FAILED); response.setRemark("parse the consumer's subscription failed"); @@ -171,7 +415,7 @@ private RemotingCommand processRequest(final Channel channel, RemotingCommand re ConsumerGroupInfo consumerGroupInfo = this.brokerController.getConsumerManager().getConsumerGroupInfo(requestHeader.getConsumerGroup()); if (null == consumerGroupInfo) { - log.warn("the consumer's group info not exist, group: {}", requestHeader.getConsumerGroup()); + LOGGER.warn("the consumer's group info not exist, group: {}", requestHeader.getConsumerGroup()); response.setCode(ResponseCode.SUBSCRIPTION_NOT_EXIST); response.setRemark("the consumer's group info not exist" + FAQUrl.suggestTodo(FAQUrl.SAME_GROUP_DIFFERENT_TOPIC)); return response; @@ -180,20 +424,30 @@ private RemotingCommand processRequest(final Channel channel, RemotingCommand re if (!subscriptionGroupConfig.isConsumeBroadcastEnable() && consumerGroupInfo.getMessageModel() == MessageModel.BROADCASTING) { response.setCode(ResponseCode.NO_PERMISSION); + responseHeader.setForbiddenType(ForbiddenType.BROADCASTING_DISABLE_FORBIDDEN); response.setRemark("the consumer group[" + requestHeader.getConsumerGroup() + "] can not consume by broadcast way"); return response; } + boolean readForbidden = this.brokerController.getSubscriptionGroupManager().getForbidden(// + subscriptionGroupConfig.getGroupName(), requestHeader.getTopic(), PermName.INDEX_PERM_READ); + if (readForbidden) { + response.setCode(ResponseCode.NO_PERMISSION); + responseHeader.setForbiddenType(ForbiddenType.SUBSCRIPTION_FORBIDDEN); + response.setRemark("the consumer group[" + requestHeader.getConsumerGroup() + "] is forbidden for topic[" + requestHeader.getTopic() + "]"); + return response; + } + subscriptionData = consumerGroupInfo.findSubscriptionData(requestHeader.getTopic()); if (null == subscriptionData) { - log.warn("the consumer's subscription not exist, group: {}, topic:{}", requestHeader.getConsumerGroup(), requestHeader.getTopic()); + LOGGER.warn("the consumer's subscription not exist, group: {}, topic:{}", requestHeader.getConsumerGroup(), requestHeader.getTopic()); response.setCode(ResponseCode.SUBSCRIPTION_NOT_EXIST); response.setRemark("the consumer's subscription not exist" + FAQUrl.suggestTodo(FAQUrl.SAME_GROUP_DIFFERENT_TOPIC)); return response; } if (subscriptionData.getSubVersion() < requestHeader.getSubVersion()) { - log.warn("The broker's subscription is not latest, group: {} {}", requestHeader.getConsumerGroup(), + LOGGER.warn("The broker's subscription is not latest, group: {} {}", requestHeader.getConsumerGroup(), subscriptionData.getSubString()); response.setCode(ResponseCode.SUBSCRIPTION_NOT_LATEST); response.setRemark("the consumer's subscription not latest"); @@ -208,7 +462,7 @@ private RemotingCommand processRequest(final Channel channel, RemotingCommand re return response; } if (consumerFilterData.getClientVersion() < requestHeader.getSubVersion()) { - log.warn("The broker's consumer filter data is not latest, group: {}, topic: {}, serverV: {}, clientV: {}", + LOGGER.warn("The broker's consumer filter data is not latest, group: {}, topic: {}, serverV: {}, clientV: {}", requestHeader.getConsumerGroup(), requestHeader.getTopic(), consumerFilterData.getClientVersion(), requestHeader.getSubVersion()); response.setCode(ResponseCode.FILTER_DATA_NOT_LATEST); response.setRemark("the consumer's consumer filter data not latest"); @@ -233,341 +487,399 @@ private RemotingCommand processRequest(final Channel channel, RemotingCommand re this.brokerController.getConsumerFilterManager()); } - final GetMessageResult getMessageResult = - this.brokerController.getMessageStore().getMessage(requestHeader.getConsumerGroup(), requestHeader.getTopic(), - requestHeader.getQueueId(), requestHeader.getQueueOffset(), requestHeader.getMaxMsgNums(), messageFilter); - if (getMessageResult != null) { - response.setRemark(getMessageResult.getStatus().name()); - responseHeader.setNextBeginOffset(getMessageResult.getNextBeginOffset()); - responseHeader.setMinOffset(getMessageResult.getMinOffset()); - responseHeader.setMaxOffset(getMessageResult.getMaxOffset()); - - if (getMessageResult.isSuggestPullingFromSlave()) { - responseHeader.setSuggestWhichBrokerId(subscriptionGroupConfig.getWhichBrokerWhenConsumeSlowly()); - } else { - responseHeader.setSuggestWhichBrokerId(MixAll.MASTER_ID); - } - - switch (this.brokerController.getMessageStoreConfig().getBrokerRole()) { - case ASYNC_MASTER: - case SYNC_MASTER: - break; - case SLAVE: - if (!this.brokerController.getBrokerConfig().isSlaveReadEnable()) { - response.setCode(ResponseCode.PULL_RETRY_IMMEDIATELY); - responseHeader.setSuggestWhichBrokerId(MixAll.MASTER_ID); + final MessageStore messageStore = brokerController.getMessageStore(); + if (this.brokerController.getMessageStore() instanceof DefaultMessageStore) { + DefaultMessageStore defaultMessageStore = (DefaultMessageStore)this.brokerController.getMessageStore(); + boolean cgNeedColdDataFlowCtr = brokerController.getColdDataCgCtrService().isCgNeedColdDataFlowCtr(requestHeader.getConsumerGroup()); + if (cgNeedColdDataFlowCtr) { + boolean isMsgLogicCold = defaultMessageStore.getCommitLog() + .getColdDataCheckService().isMsgInColdArea(requestHeader.getConsumerGroup(), + requestHeader.getTopic(), requestHeader.getQueueId(), requestHeader.getQueueOffset()); + if (isMsgLogicCold) { + ConsumeType consumeType = this.brokerController.getConsumerManager().getConsumerGroupInfo(requestHeader.getConsumerGroup()).getConsumeType(); + if (consumeType == ConsumeType.CONSUME_PASSIVELY) { + response.setCode(ResponseCode.SYSTEM_BUSY); + response.setRemark("This consumer group is reading cold data. It has been flow control"); + return response; + } else if (consumeType == ConsumeType.CONSUME_ACTIVELY) { + if (brokerAllowFlowCtrSuspend) { // second arrived, which will not be held + PullRequest pullRequest = new PullRequest(request, channel, 1000, + this.brokerController.getMessageStore().now(), requestHeader.getQueueOffset(), subscriptionData, messageFilter); + this.brokerController.getColdDataPullRequestHoldService().suspendColdDataReadRequest(pullRequest); + return null; + } + requestHeader.setMaxMsgNums(1); } - break; - } - - if (this.brokerController.getBrokerConfig().isSlaveReadEnable()) { - // consume too slow ,redirect to another machine - if (getMessageResult.isSuggestPullingFromSlave()) { - responseHeader.setSuggestWhichBrokerId(subscriptionGroupConfig.getWhichBrokerWhenConsumeSlowly()); } - // consume ok - else { - responseHeader.setSuggestWhichBrokerId(subscriptionGroupConfig.getBrokerId()); - } - } else { - responseHeader.setSuggestWhichBrokerId(MixAll.MASTER_ID); } + } - switch (getMessageResult.getStatus()) { - case FOUND: - response.setCode(ResponseCode.SUCCESS); - break; - case MESSAGE_WAS_REMOVING: - response.setCode(ResponseCode.PULL_RETRY_IMMEDIATELY); - break; - case NO_MATCHED_LOGIC_QUEUE: - case NO_MESSAGE_IN_QUEUE: - if (0 != requestHeader.getQueueOffset()) { - response.setCode(ResponseCode.PULL_OFFSET_MOVED); - - // XXX: warn and notify me - log.info("the broker store no queue data, fix the request offset {} to {}, Topic: {} QueueId: {} Consumer Group: {}", - requestHeader.getQueueOffset(), - getMessageResult.getNextBeginOffset(), - requestHeader.getTopic(), - requestHeader.getQueueId(), - requestHeader.getConsumerGroup() + final boolean useResetOffsetFeature = brokerController.getBrokerConfig().isUseServerSideResetOffset(); + String topic = requestHeader.getTopic(); + String group = requestHeader.getConsumerGroup(); + int queueId = requestHeader.getQueueId(); + Long resetOffset = brokerController.getConsumerOffsetManager().queryThenEraseResetOffset(topic, group, queueId); + + GetMessageResult getMessageResult = null; + if (useResetOffsetFeature && null != resetOffset) { + getMessageResult = new GetMessageResult(); + getMessageResult.setStatus(GetMessageStatus.OFFSET_RESET); + getMessageResult.setNextBeginOffset(resetOffset); + getMessageResult.setMinOffset(messageStore.getMinOffsetInQueue(topic, queueId)); + getMessageResult.setMaxOffset(messageStore.getMaxOffsetInQueue(topic, queueId)); + getMessageResult.setSuggestPullingFromSlave(false); + } else { + long broadcastInitOffset = queryBroadcastPullInitOffset(topic, group, queueId, requestHeader, channel); + if (broadcastInitOffset >= 0) { + getMessageResult = new GetMessageResult(); + getMessageResult.setStatus(GetMessageStatus.OFFSET_RESET); + getMessageResult.setNextBeginOffset(broadcastInitOffset); + } else { + SubscriptionData finalSubscriptionData = subscriptionData; + RemotingCommand finalResponse = response; + messageStore.getMessageAsync(group, topic, queueId, requestHeader.getQueueOffset(), + requestHeader.getMaxMsgNums(), messageFilter) + .thenApply(result -> { + if (null == result) { + finalResponse.setCode(ResponseCode.SYSTEM_ERROR); + finalResponse.setRemark("store getMessage return null"); + return finalResponse; + } + brokerController.getColdDataCgCtrService().coldAcc(requestHeader.getConsumerGroup(), result.getColdDataSum()); + return pullMessageResultHandler.handle( + result, + request, + requestHeader, + channel, + finalSubscriptionData, + subscriptionGroupConfig, + brokerAllowSuspend, + messageFilter, + finalResponse, + mappingContext, + beginTimeMills ); - } else { - response.setCode(ResponseCode.PULL_NOT_FOUND); - } - break; - case NO_MATCHED_MESSAGE: - response.setCode(ResponseCode.PULL_RETRY_IMMEDIATELY); - break; - case OFFSET_FOUND_NULL: - response.setCode(ResponseCode.PULL_NOT_FOUND); - break; - case OFFSET_OVERFLOW_BADLY: - response.setCode(ResponseCode.PULL_OFFSET_MOVED); - // XXX: warn and notify me - log.info("the request offset: {} over flow badly, broker max offset: {}, consumer: {}", - requestHeader.getQueueOffset(), getMessageResult.getMaxOffset(), channel.remoteAddress()); - break; - case OFFSET_OVERFLOW_ONE: - response.setCode(ResponseCode.PULL_NOT_FOUND); - break; - case OFFSET_TOO_SMALL: - response.setCode(ResponseCode.PULL_OFFSET_MOVED); - log.info("the request offset too small. group={}, topic={}, requestOffset={}, brokerMinOffset={}, clientIp={}", - requestHeader.getConsumerGroup(), requestHeader.getTopic(), requestHeader.getQueueOffset(), - getMessageResult.getMinOffset(), channel.remoteAddress()); - break; - default: - assert false; - break; + }) + .thenAccept(result -> NettyRemotingAbstract.writeResponse(channel, request, result)); } + } - if (this.hasConsumeMessageHook()) { - ConsumeMessageContext context = new ConsumeMessageContext(); - context.setConsumerGroup(requestHeader.getConsumerGroup()); - context.setTopic(requestHeader.getTopic()); - context.setQueueId(requestHeader.getQueueId()); - - String owner = request.getExtFields().get(BrokerStatsManager.COMMERCIAL_OWNER); + if (getMessageResult != null) { - switch (response.getCode()) { - case ResponseCode.SUCCESS: - int commercialBaseCount = brokerController.getBrokerConfig().getCommercialBaseCount(); - int incValue = getMessageResult.getMsgCount4Commercial() * commercialBaseCount; + return this.pullMessageResultHandler.handle( + getMessageResult, + request, + requestHeader, + channel, + subscriptionData, + subscriptionGroupConfig, + brokerAllowSuspend, + messageFilter, + response, + mappingContext, + beginTimeMills + ); + } + return null; + } - context.setCommercialRcvStats(BrokerStatsManager.StatsType.RCV_SUCCESS); - context.setCommercialRcvTimes(incValue); - context.setCommercialRcvSize(getMessageResult.getBufferTotalSize()); - context.setCommercialOwner(owner); + public boolean hasConsumeMessageHook() { + return consumeMessageHookList != null && !this.consumeMessageHookList.isEmpty(); + } - break; - case ResponseCode.PULL_NOT_FOUND: - if (!brokerAllowSuspend) { + /** + * Composes the header of the response message to be sent back to the client + * @param requestHeader - the header of the request message + * @param getMessageResult - the result of the GetMessage request + * @param topicSysFlag - the system flag of the topic + * @param subscriptionGroupConfig - configuration of the subscription group + * @param response - the response message to be sent back to the client + * @param clientAddress - the address of the client + */ + protected void composeResponseHeader(PullMessageRequestHeader requestHeader, GetMessageResult getMessageResult, + int topicSysFlag, SubscriptionGroupConfig subscriptionGroupConfig, RemotingCommand response, + String clientAddress) { + final PullMessageResponseHeader responseHeader = (PullMessageResponseHeader) response.readCustomHeader(); + response.setRemark(getMessageResult.getStatus().name()); + responseHeader.setNextBeginOffset(getMessageResult.getNextBeginOffset()); + responseHeader.setMinOffset(getMessageResult.getMinOffset()); + // this does not need to be modified since it's not an accurate value under logical queue. + responseHeader.setMaxOffset(getMessageResult.getMaxOffset()); + responseHeader.setTopicSysFlag(topicSysFlag); + responseHeader.setGroupSysFlag(subscriptionGroupConfig.getGroupSysFlag()); + + switch (getMessageResult.getStatus()) { + case FOUND: + response.setCode(ResponseCode.SUCCESS); + break; + case MESSAGE_WAS_REMOVING: + case NO_MATCHED_MESSAGE: + response.setCode(ResponseCode.PULL_RETRY_IMMEDIATELY); + break; + case NO_MATCHED_LOGIC_QUEUE: + case NO_MESSAGE_IN_QUEUE: + if (0 != requestHeader.getQueueOffset()) { + response.setCode(ResponseCode.PULL_OFFSET_MOVED); + // XXX: warn and notify me + LOGGER.info("the broker stores no queue data, fix the request offset {} to {}, Topic: {} QueueId: {} Consumer Group: {}", + requestHeader.getQueueOffset(), + getMessageResult.getNextBeginOffset(), + requestHeader.getTopic(), + requestHeader.getQueueId(), + requestHeader.getConsumerGroup() + ); + } else { + response.setCode(ResponseCode.PULL_NOT_FOUND); + } + break; + case OFFSET_FOUND_NULL: + case OFFSET_OVERFLOW_ONE: + response.setCode(ResponseCode.PULL_NOT_FOUND); + break; + case OFFSET_OVERFLOW_BADLY: + response.setCode(ResponseCode.PULL_OFFSET_MOVED); + // XXX: warn and notify me + LOGGER.info("the request offset: {} over flow badly, fix to {}, broker max offset: {}, consumer: {}", + requestHeader.getQueueOffset(), getMessageResult.getNextBeginOffset(), getMessageResult.getMaxOffset(), clientAddress); + break; + case OFFSET_RESET: + response.setCode(ResponseCode.PULL_OFFSET_MOVED); + LOGGER.info("The queue under pulling was previously reset to start from {}", + getMessageResult.getNextBeginOffset()); + break; + case OFFSET_TOO_SMALL: + response.setCode(ResponseCode.PULL_OFFSET_MOVED); + LOGGER.info("the request offset too small. group={}, topic={}, requestOffset={}, brokerMinOffset={}, clientIp={}", + requestHeader.getConsumerGroup(), requestHeader.getTopic(), requestHeader.getQueueOffset(), + getMessageResult.getMinOffset(), clientAddress); + break; + default: + assert false; + break; + } - context.setCommercialRcvStats(BrokerStatsManager.StatsType.RCV_EPOLLS); - context.setCommercialRcvTimes(1); - context.setCommercialOwner(owner); + if (this.brokerController.getBrokerConfig().isSlaveReadEnable() && !this.brokerController.getBrokerConfig().isInBrokerContainer()) { + // consume too slow ,redirect to another machine + if (getMessageResult.isSuggestPullingFromSlave()) { + responseHeader.setSuggestWhichBrokerId(subscriptionGroupConfig.getWhichBrokerWhenConsumeSlowly()); + } + // consume ok + else { + responseHeader.setSuggestWhichBrokerId(subscriptionGroupConfig.getBrokerId()); + } + } else { + responseHeader.setSuggestWhichBrokerId(MixAll.MASTER_ID); + } - } - break; - case ResponseCode.PULL_RETRY_IMMEDIATELY: - case ResponseCode.PULL_OFFSET_MOVED: - context.setCommercialRcvStats(BrokerStatsManager.StatsType.RCV_EPOLLS); - context.setCommercialRcvTimes(1); - context.setCommercialOwner(owner); - break; - default: - assert false; - break; + if (this.brokerController.getBrokerConfig().getBrokerId() != MixAll.MASTER_ID && !getMessageResult.isSuggestPullingFromSlave()) { + if (this.brokerController.getMinBrokerIdInGroup() == MixAll.MASTER_ID) { + LOGGER.debug("slave redirect pullRequest to master, topic: {}, queueId: {}, consumer group: {}, next: {}, min: {}, max: {}", + requestHeader.getTopic(), + requestHeader.getQueueId(), + requestHeader.getConsumerGroup(), + responseHeader.getNextBeginOffset(), + responseHeader.getMinOffset(), + responseHeader.getMaxOffset() + ); + responseHeader.setSuggestWhichBrokerId(MixAll.MASTER_ID); + if (!getMessageResult.getStatus().equals(GetMessageStatus.FOUND)) { + response.setCode(ResponseCode.PULL_RETRY_IMMEDIATELY); } - - this.executeConsumeMessageHookBefore(context); } + } - switch (response.getCode()) { + } + + protected void executeConsumeMessageHookBefore(RemotingCommand request, PullMessageRequestHeader requestHeader, + GetMessageResult getMessageResult, boolean brokerAllowSuspend, int responseCode) { + if (this.hasConsumeMessageHook()) { + String owner = request.getExtFields().get(BrokerStatsManager.COMMERCIAL_OWNER); + String authType = request.getExtFields().get(BrokerStatsManager.ACCOUNT_AUTH_TYPE); + String ownerParent = request.getExtFields().get(BrokerStatsManager.ACCOUNT_OWNER_PARENT); + String ownerSelf = request.getExtFields().get(BrokerStatsManager.ACCOUNT_OWNER_SELF); + + ConsumeMessageContext context = new ConsumeMessageContext(); + context.setConsumerGroup(requestHeader.getConsumerGroup()); + context.setTopic(requestHeader.getTopic()); + context.setQueueId(requestHeader.getQueueId()); + context.setAccountAuthType(authType); + context.setAccountOwnerParent(ownerParent); + context.setAccountOwnerSelf(ownerSelf); + context.setNamespace(NamespaceUtil.getNamespaceFromResource(requestHeader.getTopic())); + + switch (responseCode) { case ResponseCode.SUCCESS: + int commercialBaseCount = brokerController.getBrokerConfig().getCommercialBaseCount(); + int incValue = getMessageResult.getMsgCount4Commercial() * commercialBaseCount; - this.brokerController.getBrokerStatsManager().incGroupGetNums(requestHeader.getConsumerGroup(), requestHeader.getTopic(), - getMessageResult.getMessageCount()); + context.setCommercialRcvStats(BrokerStatsManager.StatsType.RCV_SUCCESS); + context.setCommercialRcvTimes(incValue); + context.setCommercialRcvSize(getMessageResult.getBufferTotalSize()); + context.setCommercialOwner(owner); - this.brokerController.getBrokerStatsManager().incGroupGetSize(requestHeader.getConsumerGroup(), requestHeader.getTopic(), - getMessageResult.getBufferTotalSize()); + context.setRcvStat(BrokerStatsManager.StatsType.RCV_SUCCESS); + context.setRcvMsgNum(getMessageResult.getMessageCount()); + context.setRcvMsgSize(getMessageResult.getBufferTotalSize()); + context.setCommercialRcvMsgNum(getMessageResult.getMsgCount4Commercial()); - this.brokerController.getBrokerStatsManager().incBrokerGetNums(getMessageResult.getMessageCount()); - if (this.brokerController.getBrokerConfig().isTransferMsgByHeap()) { - final long beginTimeMills = this.brokerController.getMessageStore().now(); - final byte[] r = this.readGetMessageResult(getMessageResult, requestHeader.getConsumerGroup(), requestHeader.getTopic(), requestHeader.getQueueId()); - this.brokerController.getBrokerStatsManager().incGroupGetLatency(requestHeader.getConsumerGroup(), - requestHeader.getTopic(), requestHeader.getQueueId(), - (int) (this.brokerController.getMessageStore().now() - beginTimeMills)); - response.setBody(r); - } else { - try { - FileRegion fileRegion = - new ManyMessageTransfer(response.encodeHeader(getMessageResult.getBufferTotalSize()), getMessageResult); - channel.writeAndFlush(fileRegion).addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) throws Exception { - getMessageResult.release(); - if (!future.isSuccess()) { - log.error("transfer many message by pagecache failed, {}", channel.remoteAddress(), future.cause()); - } - } - }); - } catch (Throwable e) { - log.error("transfer many message by pagecache exception", e); - getMessageResult.release(); - } - - response = null; - } break; case ResponseCode.PULL_NOT_FOUND: + if (!brokerAllowSuspend) { - if (brokerAllowSuspend && hasSuspendFlag) { - long pollingTimeMills = suspendTimeoutMillisLong; - if (!this.brokerController.getBrokerConfig().isLongPollingEnable()) { - pollingTimeMills = this.brokerController.getBrokerConfig().getShortPollingTimeMills(); - } + context.setCommercialRcvStats(BrokerStatsManager.StatsType.RCV_EPOLLS); + context.setCommercialRcvTimes(1); + context.setCommercialOwner(owner); - String topic = requestHeader.getTopic(); - long offset = requestHeader.getQueueOffset(); - int queueId = requestHeader.getQueueId(); - PullRequest pullRequest = new PullRequest(request, channel, pollingTimeMills, - this.brokerController.getMessageStore().now(), offset, subscriptionData, messageFilter); - this.brokerController.getPullRequestHoldService().suspendPullRequest(topic, queueId, pullRequest); - response = null; - break; + context.setRcvStat(BrokerStatsManager.StatsType.RCV_EPOLLS); + context.setRcvMsgNum(0); + context.setRcvMsgSize(0); + context.setCommercialRcvMsgNum(0); } - - case ResponseCode.PULL_RETRY_IMMEDIATELY: break; + case ResponseCode.PULL_RETRY_IMMEDIATELY: case ResponseCode.PULL_OFFSET_MOVED: - if (this.brokerController.getMessageStoreConfig().getBrokerRole() != BrokerRole.SLAVE - || this.brokerController.getMessageStoreConfig().isOffsetCheckInSlave()) { - MessageQueue mq = new MessageQueue(); - mq.setTopic(requestHeader.getTopic()); - mq.setQueueId(requestHeader.getQueueId()); - mq.setBrokerName(this.brokerController.getBrokerConfig().getBrokerName()); - - OffsetMovedEvent event = new OffsetMovedEvent(); - event.setConsumerGroup(requestHeader.getConsumerGroup()); - event.setMessageQueue(mq); - event.setOffsetRequest(requestHeader.getQueueOffset()); - event.setOffsetNew(getMessageResult.getNextBeginOffset()); - this.generateOffsetMovedEvent(event); - log.warn( - "PULL_OFFSET_MOVED:correction offset. topic={}, groupId={}, requestOffset={}, newOffset={}, suggestBrokerId={}", - requestHeader.getTopic(), requestHeader.getConsumerGroup(), event.getOffsetRequest(), event.getOffsetNew(), - responseHeader.getSuggestWhichBrokerId()); - } else { - responseHeader.setSuggestWhichBrokerId(subscriptionGroupConfig.getBrokerId()); - response.setCode(ResponseCode.PULL_RETRY_IMMEDIATELY); - log.warn("PULL_OFFSET_MOVED:none correction. topic={}, groupId={}, requestOffset={}, suggestBrokerId={}", - requestHeader.getTopic(), requestHeader.getConsumerGroup(), requestHeader.getQueueOffset(), - responseHeader.getSuggestWhichBrokerId()); - } - + context.setCommercialRcvStats(BrokerStatsManager.StatsType.RCV_EPOLLS); + context.setCommercialRcvTimes(1); + context.setCommercialOwner(owner); + + context.setRcvStat(BrokerStatsManager.StatsType.RCV_EPOLLS); + context.setRcvMsgNum(0); + context.setRcvMsgSize(0); + context.setCommercialRcvMsgNum(0); break; default: assert false; + break; + } + + for (ConsumeMessageHook hook : this.consumeMessageHookList) { + try { + hook.consumeMessageBefore(context); + } catch (Throwable ignored) { + } } - } else { - response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark("store getMessage return null"); } + } + + protected void tryCommitOffset(boolean brokerAllowSuspend, PullMessageRequestHeader requestHeader, + long nextOffset, String clientAddress) { + this.brokerController.getConsumerOffsetManager().commitPullOffset(clientAddress, + requestHeader.getConsumerGroup(), requestHeader.getTopic(), requestHeader.getQueueId(), nextOffset); boolean storeOffsetEnable = brokerAllowSuspend; + final boolean hasCommitOffsetFlag = PullSysFlag.hasCommitOffsetFlag(requestHeader.getSysFlag()); storeOffsetEnable = storeOffsetEnable && hasCommitOffsetFlag; - storeOffsetEnable = storeOffsetEnable - && this.brokerController.getMessageStoreConfig().getBrokerRole() != BrokerRole.SLAVE; if (storeOffsetEnable) { - this.brokerController.getConsumerOffsetManager().commitOffset(RemotingHelper.parseChannelRemoteAddr(channel), - requestHeader.getConsumerGroup(), requestHeader.getTopic(), requestHeader.getQueueId(), requestHeader.getCommitOffset()); + this.brokerController.getConsumerOffsetManager().commitOffset(clientAddress, requestHeader.getConsumerGroup(), + requestHeader.getTopic(), requestHeader.getQueueId(), requestHeader.getCommitOffset()); } - return response; } - public boolean hasConsumeMessageHook() { - return consumeMessageHookList != null && !this.consumeMessageHookList.isEmpty(); - } - - public void executeConsumeMessageHookBefore(final ConsumeMessageContext context) { - if (hasConsumeMessageHook()) { - for (ConsumeMessageHook hook : this.consumeMessageHookList) { - try { - hook.consumeMessageBefore(context); - } catch (Throwable e) { + public void executeRequestWhenWakeup(final Channel channel, final RemotingCommand request) { + Runnable run = () -> { + try { + boolean brokerAllowFlowCtrSuspend = !(request.getExtFields() != null && request.getExtFields().containsKey(ColdDataPullRequestHoldService.NO_SUSPEND_KEY)); + final RemotingCommand response = PullMessageProcessor.this.processRequest(channel, request, false, brokerAllowFlowCtrSuspend); + + if (response != null) { + response.setOpaque(request.getOpaque()); + response.markResponseType(); + try { + NettyRemotingAbstract.writeResponse(channel, request, response, future -> { + if (!future.isSuccess()) { + LOGGER.error("processRequestWrapper response to {} failed", channel.remoteAddress(), future.cause()); + LOGGER.error(request.toString()); + LOGGER.error(response.toString()); + } + }); + } catch (Throwable e) { + LOGGER.error("processRequestWrapper process request over, but response failed", e); + LOGGER.error(request.toString()); + LOGGER.error(response.toString()); + } } + } catch (RemotingCommandException e1) { + LOGGER.error("excuteRequestWhenWakeup run", e1); } - } + }; + this.brokerController.getPullMessageExecutor().submit(new RequestTask(run, channel, request)); } - private byte[] readGetMessageResult(final GetMessageResult getMessageResult, final String group, final String topic, - final int queueId) { - final ByteBuffer byteBuffer = ByteBuffer.allocate(getMessageResult.getBufferTotalSize()); + public void registerConsumeMessageHook(List consumeMessageHookList) { + this.consumeMessageHookList = consumeMessageHookList; + } - long storeTimestamp = 0; - try { - List messageBufferList = getMessageResult.getMessageBufferList(); - for (ByteBuffer bb : messageBufferList) { + public void setPullMessageResultHandler(PullMessageResultHandler pullMessageResultHandler) { + this.pullMessageResultHandler = pullMessageResultHandler; + } - byteBuffer.put(bb); - storeTimestamp = bb.getLong(MessageDecoder.MESSAGE_STORE_TIMESTAMP_POSTION); - } - } finally { - getMessageResult.release(); + private boolean isBroadcast(boolean proxyPullBroadcast, ConsumerGroupInfo consumerGroupInfo) { + return proxyPullBroadcast || + consumerGroupInfo != null + && MessageModel.BROADCASTING.equals(consumerGroupInfo.getMessageModel()) + && ConsumeType.CONSUME_PASSIVELY.equals(consumerGroupInfo.getConsumeType()); + } + + protected void updateBroadcastPulledOffset(String topic, String group, int queueId, + PullMessageRequestHeader requestHeader, Channel channel, RemotingCommand response, long nextBeginOffset) { + + if (response == null || !this.brokerController.getBrokerConfig().isEnableBroadcastOffsetStore()) { + return; } - this.brokerController.getBrokerStatsManager().recordDiskFallBehindTime(group, topic, queueId, this.brokerController.getMessageStore().now() - storeTimestamp); - return byteBuffer.array(); - } + boolean proxyPullBroadcast = Objects.equals( + RequestSource.PROXY_FOR_BROADCAST.getValue(), requestHeader.getRequestSource()); + ConsumerGroupInfo consumerGroupInfo = this.brokerController.getConsumerManager().getConsumerGroupInfo(group); - private void generateOffsetMovedEvent(final OffsetMovedEvent event) { - try { - MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); - msgInner.setTopic(MixAll.OFFSET_MOVED_EVENT); - msgInner.setTags(event.getConsumerGroup()); - msgInner.setDelayTimeLevel(0); - msgInner.setKeys(event.getConsumerGroup()); - msgInner.setBody(event.encode()); - msgInner.setFlag(0); - msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); - msgInner.setTagsCode(MessageExtBrokerInner.tagsString2tagsCode(TopicFilterType.SINGLE_TAG, msgInner.getTags())); - - msgInner.setQueueId(0); - msgInner.setSysFlag(0); - msgInner.setBornTimestamp(System.currentTimeMillis()); - msgInner.setBornHost(RemotingUtil.string2SocketAddress(this.brokerController.getBrokerAddr())); - msgInner.setStoreHost(msgInner.getBornHost()); - - msgInner.setReconsumeTimes(0); - - PutMessageResult putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner); - } catch (Exception e) { - log.warn(String.format("generateOffsetMovedEvent Exception, %s", event.toString()), e); + if (isBroadcast(proxyPullBroadcast, consumerGroupInfo)) { + long offset = requestHeader.getQueueOffset(); + if (ResponseCode.PULL_OFFSET_MOVED == response.getCode()) { + offset = nextBeginOffset; + } + String clientId; + if (proxyPullBroadcast) { + clientId = requestHeader.getProxyFrowardClientId(); + } else { + ClientChannelInfo clientChannelInfo = consumerGroupInfo.findChannel(channel); + if (clientChannelInfo == null) { + return; + } + clientId = clientChannelInfo.getClientId(); + } + this.brokerController.getBroadcastOffsetManager() + .updateOffset(topic, group, queueId, offset, clientId, proxyPullBroadcast); } } - public void executeRequestWhenWakeup(final Channel channel, - final RemotingCommand request) throws RemotingCommandException { - Runnable run = new Runnable() { - @Override - public void run() { - try { - final RemotingCommand response = PullMessageProcessor.this.processRequest(channel, request, false); - - if (response != null) { - response.setOpaque(request.getOpaque()); - response.markResponseType(); - try { - channel.writeAndFlush(response).addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) throws Exception { - if (!future.isSuccess()) { - log.error("processRequestWrapper response to {} failed", - future.channel().remoteAddress(), future.cause()); - log.error(request.toString()); - log.error(response.toString()); - } - } - }); - } catch (Throwable e) { - log.error("processRequestWrapper process request over, but response failed", e); - log.error(request.toString()); - log.error(response.toString()); - } - } - } catch (RemotingCommandException e1) { - log.error("excuteRequestWhenWakeup run", e1); + /** + * When pull request is not broadcast or not return -1 + */ + protected long queryBroadcastPullInitOffset(String topic, String group, int queueId, + PullMessageRequestHeader requestHeader, Channel channel) { + + if (!this.brokerController.getBrokerConfig().isEnableBroadcastOffsetStore()) { + return -1L; + } + + ConsumerGroupInfo consumerGroupInfo = this.brokerController.getConsumerManager().getConsumerGroupInfo(group); + boolean proxyPullBroadcast = Objects.equals( + RequestSource.PROXY_FOR_BROADCAST.getValue(), requestHeader.getRequestSource()); + + if (isBroadcast(proxyPullBroadcast, consumerGroupInfo)) { + String clientId; + if (proxyPullBroadcast) { + clientId = requestHeader.getProxyFrowardClientId(); + } else { + ClientChannelInfo clientChannelInfo = consumerGroupInfo.findChannel(channel); + if (clientChannelInfo == null) { + return -1; } + clientId = clientChannelInfo.getClientId(); } - }; - this.brokerController.getPullMessageExecutor().submit(new RequestTask(run, channel, request)); - } - public void registerConsumeMessageHook(List sendMessageHookList) { - this.consumeMessageHookList = sendMessageHookList; + return this.brokerController.getBroadcastOffsetManager() + .queryInitOffset(topic, group, queueId, clientId, requestHeader.getQueueOffset(), proxyPullBroadcast); + } + return -1L; } } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/QueryAssignmentProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/QueryAssignmentProcessor.java new file mode 100644 index 00000000000..d55f1b5b7fb --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/QueryAssignmentProcessor.java @@ -0,0 +1,324 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.processor; + +import io.netty.channel.ChannelHandlerContext; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.client.ConsumerGroupInfo; +import org.apache.rocketmq.broker.loadbalance.MessageRequestModeManager; +import org.apache.rocketmq.broker.topic.TopicRouteInfoManager; +import org.apache.rocketmq.client.consumer.AllocateMessageQueueStrategy; +import org.apache.rocketmq.client.consumer.rebalance.AllocateMessageQueueAveragely; +import org.apache.rocketmq.client.consumer.rebalance.AllocateMessageQueueAveragelyByCircle; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.message.MessageQueueAssignment; +import org.apache.rocketmq.common.message.MessageRequestMode; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.QueryAssignmentRequestBody; +import org.apache.rocketmq.remoting.protocol.body.QueryAssignmentResponseBody; +import org.apache.rocketmq.remoting.protocol.body.SetMessageRequestModeRequestBody; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; + +public class QueryAssignmentProcessor implements NettyRequestProcessor { + private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + + private final BrokerController brokerController; + + private final ConcurrentHashMap name2LoadStrategy = new ConcurrentHashMap<>(); + + private MessageRequestModeManager messageRequestModeManager; + + public QueryAssignmentProcessor(final BrokerController brokerController) { + this.brokerController = brokerController; + + //register strategy + //NOTE: init with broker's log instead of init with ClientLogger.getLog(); + AllocateMessageQueueAveragely allocateMessageQueueAveragely = new AllocateMessageQueueAveragely(); + name2LoadStrategy.put(allocateMessageQueueAveragely.getName(), allocateMessageQueueAveragely); + AllocateMessageQueueAveragelyByCircle allocateMessageQueueAveragelyByCircle = new AllocateMessageQueueAveragelyByCircle(); + name2LoadStrategy.put(allocateMessageQueueAveragelyByCircle.getName(), allocateMessageQueueAveragelyByCircle); + + this.messageRequestModeManager = new MessageRequestModeManager(brokerController); + this.messageRequestModeManager.load(); + } + + @Override + public RemotingCommand processRequest(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + switch (request.getCode()) { + case RequestCode.QUERY_ASSIGNMENT: + return this.queryAssignment(ctx, request); + case RequestCode.SET_MESSAGE_REQUEST_MODE: + return this.setMessageRequestMode(ctx, request); + default: + break; + } + return null; + } + + @Override + public boolean rejectRequest() { + return false; + } + + /** + * + */ + private RemotingCommand queryAssignment(ChannelHandlerContext ctx, RemotingCommand request) + throws RemotingCommandException { + final QueryAssignmentRequestBody requestBody = QueryAssignmentRequestBody.decode(request.getBody(), QueryAssignmentRequestBody.class); + final String topic = requestBody.getTopic(); + final String consumerGroup = requestBody.getConsumerGroup(); + final String clientId = requestBody.getClientId(); + final MessageModel messageModel = requestBody.getMessageModel(); + final String strategyName = requestBody.getStrategyName(); + + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + final QueryAssignmentResponseBody responseBody = new QueryAssignmentResponseBody(); + + SetMessageRequestModeRequestBody setMessageRequestModeRequestBody = this.messageRequestModeManager.getMessageRequestMode(topic, consumerGroup); + + if (setMessageRequestModeRequestBody == null) { + setMessageRequestModeRequestBody = new SetMessageRequestModeRequestBody(); + setMessageRequestModeRequestBody.setTopic(topic); + setMessageRequestModeRequestBody.setConsumerGroup(consumerGroup); + + if (topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + // retry topic must be pull mode + setMessageRequestModeRequestBody.setMode(MessageRequestMode.PULL); + } else { + setMessageRequestModeRequestBody.setMode(brokerController.getBrokerConfig().getDefaultMessageRequestMode()); + } + + if (setMessageRequestModeRequestBody.getMode() == MessageRequestMode.POP) { + setMessageRequestModeRequestBody.setPopShareQueueNum(brokerController.getBrokerConfig().getDefaultPopShareQueueNum()); + } + } + + Set messageQueues = doLoadBalance(topic, consumerGroup, clientId, messageModel, strategyName, setMessageRequestModeRequestBody, ctx); + + Set assignments = null; + if (messageQueues != null) { + assignments = new HashSet<>(); + for (MessageQueue messageQueue : messageQueues) { + MessageQueueAssignment messageQueueAssignment = new MessageQueueAssignment(); + messageQueueAssignment.setMessageQueue(messageQueue); + if (setMessageRequestModeRequestBody != null) { + messageQueueAssignment.setMode(setMessageRequestModeRequestBody.getMode()); + } + assignments.add(messageQueueAssignment); + } + } + + responseBody.setMessageQueueAssignments(assignments); + response.setBody(responseBody.encode()); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + /** + * Returns empty set means the client should clear all load assigned to it before, null means invalid result and the + * client should skip the update logic + * + * @param topic + * @param consumerGroup + * @param clientId + * @param messageModel + * @param strategyName + * @return the MessageQueues assigned to this client + */ + private Set doLoadBalance(final String topic, final String consumerGroup, final String clientId, + final MessageModel messageModel, final String strategyName, + SetMessageRequestModeRequestBody setMessageRequestModeRequestBody, final ChannelHandlerContext ctx) { + Set assignedQueueSet = null; + final TopicRouteInfoManager topicRouteInfoManager = this.brokerController.getTopicRouteInfoManager(); + + switch (messageModel) { + case BROADCASTING: { + assignedQueueSet = topicRouteInfoManager.getTopicSubscribeInfo(topic); + if (assignedQueueSet == null) { + log.warn("QueryLoad: no assignment for group[{}], the topic[{}] does not exist.", consumerGroup, topic); + } + break; + } + case CLUSTERING: { + Set mqSet = topicRouteInfoManager.getTopicSubscribeInfo(topic); + if (null == mqSet) { + if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + log.warn("QueryLoad: no assignment for group[{}], the topic[{}] does not exist.", consumerGroup, topic); + } + return null; + } + + if (!brokerController.getBrokerConfig().isServerLoadBalancerEnable()) { + return mqSet; + } + + List cidAll = null; + ConsumerGroupInfo consumerGroupInfo = this.brokerController.getConsumerManager().getConsumerGroupInfo(consumerGroup); + if (consumerGroupInfo != null) { + cidAll = consumerGroupInfo.getAllClientId(); + } + if (null == cidAll) { + log.warn("QueryLoad: no assignment for group[{}] topic[{}], get consumer id list failed", consumerGroup, topic); + return null; + } + + List mqAll = new ArrayList<>(); + mqAll.addAll(mqSet); + Collections.sort(mqAll); + Collections.sort(cidAll); + List allocateResult = null; + + try { + AllocateMessageQueueStrategy allocateMessageQueueStrategy = name2LoadStrategy.get(strategyName); + if (null == allocateMessageQueueStrategy) { + log.warn("QueryLoad: unsupported strategy [{}], {}", strategyName, RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + return null; + } + + if (setMessageRequestModeRequestBody != null && setMessageRequestModeRequestBody.getMode() == MessageRequestMode.POP) { + allocateResult = allocate4Pop(allocateMessageQueueStrategy, consumerGroup, clientId, mqAll, + cidAll, setMessageRequestModeRequestBody.getPopShareQueueNum()); + + } else { + allocateResult = allocateMessageQueueStrategy.allocate(consumerGroup, clientId, mqAll, cidAll); + } + } catch (Throwable e) { + log.error("QueryLoad: no assignment for group[{}] topic[{}], allocate message queue exception. strategy name: {}, ex: {}", consumerGroup, topic, strategyName, e); + return null; + } + + assignedQueueSet = new HashSet<>(); + if (allocateResult != null) { + assignedQueueSet.addAll(allocateResult); + } + break; + } + default: + break; + } + return assignedQueueSet; + } + + public List allocate4Pop(AllocateMessageQueueStrategy allocateMessageQueueStrategy, + final String consumerGroup, final String clientId, List mqAll, List cidAll, + int popShareQueueNum) { + + List allocateResult; + if (popShareQueueNum <= 0 || popShareQueueNum >= cidAll.size() - 1) { + //each client pop all messagequeue + allocateResult = new ArrayList<>(mqAll.size()); + for (MessageQueue mq : mqAll) { + //must create new MessageQueue in case of change cache in AssignmentManager + MessageQueue newMq = new MessageQueue(mq.getTopic(), mq.getBrokerName(), -1); + allocateResult.add(newMq); + } + + } else { + if (cidAll.size() <= mqAll.size()) { + //consumer working in pop mode could share the MessageQueues assigned to the N (N = popWorkGroupSize) consumer following it in the cid list + allocateResult = allocateMessageQueueStrategy.allocate(consumerGroup, clientId, mqAll, cidAll); + int index = cidAll.indexOf(clientId); + if (index >= 0) { + for (int i = 1; i <= popShareQueueNum; i++) { + index++; + index = index % cidAll.size(); + List tmp = allocateMessageQueueStrategy.allocate(consumerGroup, cidAll.get(index), mqAll, cidAll); + allocateResult.addAll(tmp); + } + } + } else { + //make sure each cid is assigned + allocateResult = allocate(consumerGroup, clientId, mqAll, cidAll); + } + } + + return allocateResult; + } + + private List allocate(String consumerGroup, String currentCID, List mqAll, + List cidAll) { + if (StringUtils.isBlank(currentCID)) { + throw new IllegalArgumentException("currentCID is empty"); + } + + if (CollectionUtils.isEmpty(mqAll)) { + throw new IllegalArgumentException("mqAll is null or mqAll empty"); + } + if (CollectionUtils.isEmpty(cidAll)) { + throw new IllegalArgumentException("cidAll is null or cidAll empty"); + } + + List result = new ArrayList<>(); + if (!cidAll.contains(currentCID)) { + log.info("[BUG] ConsumerGroup: {} The consumerId: {} not in cidAll: {}", + consumerGroup, + currentCID, + cidAll); + return result; + } + + int index = cidAll.indexOf(currentCID); + result.add(mqAll.get(index % mqAll.size())); + return result; + } + + private RemotingCommand setMessageRequestMode(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + final SetMessageRequestModeRequestBody requestBody = SetMessageRequestModeRequestBody.decode(request.getBody(), SetMessageRequestModeRequestBody.class); + + final String topic = requestBody.getTopic(); + if (topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + response.setCode(ResponseCode.NO_PERMISSION); + response.setRemark("retry topic is not allowed to set mode"); + return response; + } + + final String consumerGroup = requestBody.getConsumerGroup(); + + this.messageRequestModeManager.setMessageRequestMode(topic, consumerGroup, requestBody); + this.messageRequestModeManager.persist(); + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + public MessageRequestModeManager getMessageRequestModeManager() { + return messageRequestModeManager; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/QueryMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/QueryMessageProcessor.java index e8f97d0afbb..38385149700 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/QueryMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/QueryMessageProcessor.java @@ -16,31 +16,37 @@ */ package org.apache.rocketmq.broker.processor; -import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.FileRegion; +import io.opentelemetry.api.common.Attributes; +import java.util.concurrent.TimeUnit; import org.apache.rocketmq.broker.BrokerController; import org.apache.rocketmq.broker.pagecache.OneMessageTransfer; import org.apache.rocketmq.broker.pagecache.QueryMessageTransfer; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.common.protocol.RequestCode; -import org.apache.rocketmq.common.protocol.ResponseCode; -import org.apache.rocketmq.common.protocol.header.QueryMessageRequestHeader; -import org.apache.rocketmq.common.protocol.header.QueryMessageResponseHeader; -import org.apache.rocketmq.common.protocol.header.ViewMessageRequestHeader; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.common.RemotingHelper; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.metrics.RemotingMetricsManager; import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.QueryMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryMessageResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.ViewMessageRequestHeader; import org.apache.rocketmq.store.QueryMessageResult; import org.apache.rocketmq.store.SelectMappedBufferResult; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -public class QueryMessageProcessor implements NettyRequestProcessor { - private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.LABEL_REQUEST_CODE; +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.LABEL_RESPONSE_CODE; +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.LABEL_RESULT; +public class QueryMessageProcessor implements NettyRequestProcessor { + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); private final BrokerController brokerController; public QueryMessageProcessor(final BrokerController brokerController) { @@ -101,17 +107,22 @@ public RemotingCommand queryMessage(ChannelHandlerContext ctx, RemotingCommand r FileRegion fileRegion = new QueryMessageTransfer(response.encodeHeader(queryMessageResult .getBufferTotalSize()), queryMessageResult); - ctx.channel().writeAndFlush(fileRegion).addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) throws Exception { + ctx.channel() + .writeAndFlush(fileRegion) + .addListener((ChannelFutureListener) future -> { queryMessageResult.release(); + Attributes attributes = RemotingMetricsManager.newAttributesBuilder() + .put(LABEL_REQUEST_CODE, RemotingHelper.getRequestCodeDesc(request.getCode())) + .put(LABEL_RESPONSE_CODE, RemotingHelper.getResponseCodeDesc(response.getCode())) + .put(LABEL_RESULT, RemotingMetricsManager.getWriteAndFlushResult(future)) + .build(); + RemotingMetricsManager.rpcLatency.record(request.getProcessTimer().elapsed(TimeUnit.MILLISECONDS), attributes); if (!future.isSuccess()) { - log.error("transfer query message by page cache failed, ", future.cause()); + LOGGER.error("transfer query message by page cache failed, ", future.cause()); } - } - }); + }); } catch (Throwable e) { - log.error("", e); + LOGGER.error("", e); queryMessageResult.release(); } @@ -141,17 +152,22 @@ public RemotingCommand viewMessageById(ChannelHandlerContext ctx, RemotingComman FileRegion fileRegion = new OneMessageTransfer(response.encodeHeader(selectMappedBufferResult.getSize()), selectMappedBufferResult); - ctx.channel().writeAndFlush(fileRegion).addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) throws Exception { + ctx.channel() + .writeAndFlush(fileRegion) + .addListener((ChannelFutureListener) future -> { selectMappedBufferResult.release(); + Attributes attributes = RemotingMetricsManager.newAttributesBuilder() + .put(LABEL_REQUEST_CODE, RemotingHelper.getRequestCodeDesc(request.getCode())) + .put(LABEL_RESPONSE_CODE, RemotingHelper.getResponseCodeDesc(response.getCode())) + .put(LABEL_RESULT, RemotingMetricsManager.getWriteAndFlushResult(future)) + .build(); + RemotingMetricsManager.rpcLatency.record(request.getProcessTimer().elapsed(TimeUnit.MILLISECONDS), attributes); if (!future.isSuccess()) { - log.error("Transfer one message from page cache failed, ", future.cause()); + LOGGER.error("Transfer one message from page cache failed, ", future.cause()); } - } - }); + }); } catch (Throwable e) { - log.error("", e); + LOGGER.error("", e); selectMappedBufferResult.release(); } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/ReplyMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/ReplyMessageProcessor.java new file mode 100644 index 00000000000..d3bb048f75d --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/ReplyMessageProcessor.java @@ -0,0 +1,367 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.processor; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.opentelemetry.api.common.Attributes; +import java.net.InetSocketAddress; +import java.util.concurrent.ThreadLocalRandom; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.metrics.BrokerMetricsManager; +import org.apache.rocketmq.broker.mqtrace.SendMessageContext; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.attribute.TopicMessageType; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.ReplyMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeaderV2; +import org.apache.rocketmq.remoting.protocol.header.SendMessageResponseHeader; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.stats.BrokerStatsManager; + +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_IS_SYSTEM; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_MESSAGE_TYPE; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_TOPIC; + +public class ReplyMessageProcessor extends AbstractSendMessageProcessor { + private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + + public ReplyMessageProcessor(final BrokerController brokerController) { + super(brokerController); + } + + @Override + public RemotingCommand processRequest(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + SendMessageContext mqtraceContext = null; + SendMessageRequestHeader requestHeader = parseRequestHeader(request); + if (requestHeader == null) { + return null; + } + + mqtraceContext = buildMsgContext(ctx, requestHeader, request); + this.executeSendMessageHookBefore(mqtraceContext); + + RemotingCommand response = this.processReplyMessageRequest(ctx, request, mqtraceContext, requestHeader); + + this.executeSendMessageHookAfter(response, mqtraceContext); + return response; + } + + @Override + protected SendMessageRequestHeader parseRequestHeader(RemotingCommand request) throws RemotingCommandException { + SendMessageRequestHeaderV2 requestHeaderV2 = null; + SendMessageRequestHeader requestHeader = null; + switch (request.getCode()) { + case RequestCode.SEND_REPLY_MESSAGE_V2: + requestHeaderV2 = + (SendMessageRequestHeaderV2) request + .decodeCommandCustomHeader(SendMessageRequestHeaderV2.class); + case RequestCode.SEND_REPLY_MESSAGE: + if (null == requestHeaderV2) { + requestHeader = + (SendMessageRequestHeader) request + .decodeCommandCustomHeader(SendMessageRequestHeader.class); + } else { + requestHeader = SendMessageRequestHeaderV2.createSendMessageRequestHeaderV1(requestHeaderV2); + } + default: + break; + } + return requestHeader; + } + + private RemotingCommand processReplyMessageRequest(final ChannelHandlerContext ctx, + final RemotingCommand request, + final SendMessageContext sendMessageContext, + final SendMessageRequestHeader requestHeader) { + final RemotingCommand response = RemotingCommand.createResponseCommand(SendMessageResponseHeader.class); + final SendMessageResponseHeader responseHeader = (SendMessageResponseHeader) response.readCustomHeader(); + + response.setOpaque(request.getOpaque()); + + response.addExtField(MessageConst.PROPERTY_MSG_REGION, this.brokerController.getBrokerConfig().getRegionId()); + response.addExtField(MessageConst.PROPERTY_TRACE_SWITCH, String.valueOf(this.brokerController.getBrokerConfig().isTraceOn())); + + log.debug("receive SendReplyMessage request command, {}", request); + final long startTimstamp = this.brokerController.getBrokerConfig().getStartAcceptSendRequestTimeStamp(); + if (this.brokerController.getMessageStore().now() < startTimstamp) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(String.format("broker unable to service, until %s", UtilAll.timeMillisToHumanString2(startTimstamp))); + return response; + } + + response.setCode(-1); + super.msgCheck(ctx, requestHeader, request, response); + if (response.getCode() != -1) { + return response; + } + + final byte[] body = request.getBody(); + + int queueIdInt = requestHeader.getQueueId(); + TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic()); + + if (queueIdInt < 0) { + queueIdInt = ThreadLocalRandom.current().nextInt(99999999) % topicConfig.getWriteQueueNums(); + } + + MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); + msgInner.setTopic(requestHeader.getTopic()); + msgInner.setQueueId(queueIdInt); + msgInner.setBody(body); + msgInner.setFlag(requestHeader.getFlag()); + MessageAccessor.setProperties(msgInner, MessageDecoder.string2messageProperties(requestHeader.getProperties())); + msgInner.setPropertiesString(requestHeader.getProperties()); + msgInner.setBornTimestamp(requestHeader.getBornTimestamp()); + msgInner.setBornHost(ctx.channel().remoteAddress()); + msgInner.setStoreHost(this.getStoreHost()); + msgInner.setReconsumeTimes(requestHeader.getReconsumeTimes() == null ? 0 : requestHeader.getReconsumeTimes()); + + PushReplyResult pushReplyResult = this.pushReplyMessage(ctx, requestHeader, msgInner); + this.handlePushReplyResult(pushReplyResult, response, responseHeader, queueIdInt); + + if (this.brokerController.getBrokerConfig().isStoreReplyMessageEnable()) { + PutMessageResult putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner); + this.handlePutMessageResult(putMessageResult, request, msgInner, responseHeader, sendMessageContext, queueIdInt, BrokerMetricsManager.getMessageType(requestHeader)); + } + + return response; + } + + private PushReplyResult pushReplyMessage(final ChannelHandlerContext ctx, + final SendMessageRequestHeader requestHeader, + final Message msg) { + ReplyMessageRequestHeader replyMessageRequestHeader = new ReplyMessageRequestHeader(); + InetSocketAddress bornAddress = (InetSocketAddress)(ctx.channel().remoteAddress()); + replyMessageRequestHeader.setBornHost(bornAddress.getAddress().getHostAddress() + ":" + bornAddress.getPort()); + InetSocketAddress storeAddress = (InetSocketAddress)(this.getStoreHost()); + replyMessageRequestHeader.setStoreHost(storeAddress.getAddress().getHostAddress() + ":" + storeAddress.getPort()); + replyMessageRequestHeader.setStoreTimestamp(System.currentTimeMillis()); + replyMessageRequestHeader.setProducerGroup(requestHeader.getProducerGroup()); + replyMessageRequestHeader.setTopic(requestHeader.getTopic()); + replyMessageRequestHeader.setDefaultTopic(requestHeader.getDefaultTopic()); + replyMessageRequestHeader.setDefaultTopicQueueNums(requestHeader.getDefaultTopicQueueNums()); + replyMessageRequestHeader.setQueueId(requestHeader.getQueueId()); + replyMessageRequestHeader.setSysFlag(requestHeader.getSysFlag()); + replyMessageRequestHeader.setBornTimestamp(requestHeader.getBornTimestamp()); + replyMessageRequestHeader.setFlag(requestHeader.getFlag()); + replyMessageRequestHeader.setProperties(requestHeader.getProperties()); + replyMessageRequestHeader.setReconsumeTimes(requestHeader.getReconsumeTimes()); + replyMessageRequestHeader.setUnitMode(requestHeader.isUnitMode()); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT, replyMessageRequestHeader); + request.setBody(msg.getBody()); + + String senderId = msg.getProperties().get(MessageConst.PROPERTY_MESSAGE_REPLY_TO_CLIENT); + PushReplyResult pushReplyResult = new PushReplyResult(false); + + if (senderId != null) { + Channel channel = this.brokerController.getProducerManager().findChannel(senderId); + if (channel != null) { + msg.getProperties().put(MessageConst.PROPERTY_PUSH_REPLY_TIME, String.valueOf(System.currentTimeMillis())); + replyMessageRequestHeader.setProperties(MessageDecoder.messageProperties2String(msg.getProperties())); + + try { + RemotingCommand pushResponse = this.brokerController.getBroker2Client().callClient(channel, request); + assert pushResponse != null; + switch (pushResponse.getCode()) { + case ResponseCode.SUCCESS: { + pushReplyResult.setPushOk(true); + break; + } + default: { + pushReplyResult.setPushOk(false); + pushReplyResult.setRemark("push reply message to " + senderId + "fail."); + log.warn("push reply message to <{}> return fail, response remark: {}", senderId, pushResponse.getRemark()); + } + } + } catch (RemotingException | InterruptedException e) { + pushReplyResult.setPushOk(false); + pushReplyResult.setRemark("push reply message to " + senderId + "fail."); + log.warn("push reply message to <{}> fail. {}", senderId, channel, e); + } + } else { + pushReplyResult.setPushOk(false); + pushReplyResult.setRemark("push reply message fail, channel of <" + senderId + "> not found."); + log.warn(pushReplyResult.getRemark()); + } + } else { + log.warn(MessageConst.PROPERTY_MESSAGE_REPLY_TO_CLIENT + " is null, can not reply message"); + pushReplyResult.setPushOk(false); + pushReplyResult.setRemark("reply message properties[" + MessageConst.PROPERTY_MESSAGE_REPLY_TO_CLIENT + "] is null"); + } + return pushReplyResult; + } + + private void handlePushReplyResult(PushReplyResult pushReplyResult, final RemotingCommand response, + final SendMessageResponseHeader responseHeader, int queueIdInt) { + + if (!pushReplyResult.isPushOk()) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(pushReplyResult.getRemark()); + } else { + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + //set to zero to avoid client decoding exception + responseHeader.setMsgId("0"); + responseHeader.setQueueId(queueIdInt); + responseHeader.setQueueOffset(0L); + } + } + + private void handlePutMessageResult(PutMessageResult putMessageResult, + final RemotingCommand request, final MessageExt msg, + final SendMessageResponseHeader responseHeader, SendMessageContext sendMessageContext, + int queueIdInt, TopicMessageType messageType) { + if (putMessageResult == null) { + log.warn("process reply message, store putMessage return null"); + return; + } + boolean putOk = false; + + switch (putMessageResult.getPutMessageStatus()) { + // Success + case PUT_OK: + case FLUSH_DISK_TIMEOUT: + case FLUSH_SLAVE_TIMEOUT: + case SLAVE_NOT_AVAILABLE: + putOk = true; + break; + + // Failed + case CREATE_MAPPED_FILE_FAILED: + log.warn("create mapped file failed, server is busy or broken."); + break; + case MESSAGE_ILLEGAL: + log.warn( + "the message is illegal, maybe msg body or properties length not matched. msg body length limit {}B.", + this.brokerController.getMessageStoreConfig().getMaxMessageSize()); + break; + case PROPERTIES_SIZE_EXCEEDED: + log.warn( + "the message is illegal, maybe msg properties length limit 32KB."); + break; + case SERVICE_NOT_AVAILABLE: + log.warn( + "service not available now. It may be caused by one of the following reasons: " + + "the broker's disk is full, messages are put to the slave, message store has been shut down, etc."); + break; + case OS_PAGE_CACHE_BUSY: + log.warn("[PC_SYNCHRONIZED]broker busy, start flow control for a while"); + break; + case UNKNOWN_ERROR: + log.warn("UNKNOWN_ERROR"); + break; + default: + log.warn("UNKNOWN_ERROR DEFAULT"); + break; + } + + String owner = request.getExtFields().get(BrokerStatsManager.COMMERCIAL_OWNER); + int commercialSizePerMsg = brokerController.getBrokerConfig().getCommercialSizePerMsg(); + if (putOk) { + this.brokerController.getBrokerStatsManager().incTopicPutNums(msg.getTopic(), putMessageResult.getAppendMessageResult().getMsgNum(), 1); + this.brokerController.getBrokerStatsManager().incTopicPutSize(msg.getTopic(), + putMessageResult.getAppendMessageResult().getWroteBytes()); + this.brokerController.getBrokerStatsManager().incBrokerPutNums(msg.getTopic(), putMessageResult.getAppendMessageResult().getMsgNum()); + + if (!BrokerMetricsManager.isRetryOrDlqTopic(msg.getTopic())) { + Attributes attributes = BrokerMetricsManager.newAttributesBuilder() + .put(LABEL_TOPIC, msg.getTopic()) + .put(LABEL_MESSAGE_TYPE, messageType.getMetricsValue()) + .put(LABEL_IS_SYSTEM, TopicValidator.isSystemTopic(msg.getTopic())) + .build(); + BrokerMetricsManager.messagesInTotal.add(putMessageResult.getAppendMessageResult().getMsgNum(), attributes); + BrokerMetricsManager.throughputInTotal.add(putMessageResult.getAppendMessageResult().getWroteBytes(), attributes); + BrokerMetricsManager.messageSize.record(putMessageResult.getAppendMessageResult().getWroteBytes() / putMessageResult.getAppendMessageResult().getMsgNum(), attributes); + } + + responseHeader.setMsgId(putMessageResult.getAppendMessageResult().getMsgId()); + responseHeader.setQueueId(queueIdInt); + responseHeader.setQueueOffset(putMessageResult.getAppendMessageResult().getLogicsOffset()); + + if (hasSendMessageHook()) { + sendMessageContext.setMsgId(responseHeader.getMsgId()); + sendMessageContext.setQueueId(responseHeader.getQueueId()); + sendMessageContext.setQueueOffset(responseHeader.getQueueOffset()); + + int commercialBaseCount = brokerController.getBrokerConfig().getCommercialBaseCount(); + int wroteSize = putMessageResult.getAppendMessageResult().getWroteBytes(); + int incValue = (int) Math.ceil(wroteSize * 1.0 / commercialSizePerMsg) * commercialBaseCount; + + sendMessageContext.setCommercialSendStats(BrokerStatsManager.StatsType.SEND_SUCCESS); + sendMessageContext.setCommercialSendTimes(incValue); + sendMessageContext.setCommercialSendSize(wroteSize); + sendMessageContext.setCommercialOwner(owner); + } + } else { + if (hasSendMessageHook()) { + int wroteSize = request.getBody().length; + int incValue = (int) Math.ceil(wroteSize * 1.0 / commercialSizePerMsg); + + sendMessageContext.setCommercialSendStats(BrokerStatsManager.StatsType.SEND_FAILURE); + sendMessageContext.setCommercialSendTimes(incValue); + sendMessageContext.setCommercialSendSize(wroteSize); + sendMessageContext.setCommercialOwner(owner); + } + } + } + + class PushReplyResult { + boolean pushOk; + String remark; + + public PushReplyResult(boolean pushOk) { + this.pushOk = pushOk; + remark = ""; + } + + public boolean isPushOk() { + return pushOk; + } + + public void setPushOk(boolean pushOk) { + this.pushOk = pushOk; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/SendMessageCallback.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/SendMessageCallback.java new file mode 100644 index 00000000000..a6019dcc734 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/SendMessageCallback.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.processor; + +import org.apache.rocketmq.broker.mqtrace.SendMessageContext; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public interface SendMessageCallback { + /** + * On send complete. + * + * @param ctx send context + * @param response send response + */ + void onComplete(SendMessageContext ctx, RemotingCommand response); +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/processor/SendMessageProcessor.java b/broker/src/main/java/org/apache/rocketmq/broker/processor/SendMessageProcessor.java index 227a23e6b49..912d502eab2 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/processor/SendMessageProcessor.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/processor/SendMessageProcessor.java @@ -17,43 +17,67 @@ package org.apache.rocketmq.broker.processor; import io.netty.channel.ChannelHandlerContext; -import java.net.SocketAddress; -import java.util.List; +import io.opentelemetry.api.common.Attributes; +import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.broker.BrokerController; -import org.apache.rocketmq.broker.mqtrace.ConsumeMessageContext; -import org.apache.rocketmq.broker.mqtrace.ConsumeMessageHook; +import org.apache.rocketmq.broker.metrics.BrokerMetricsManager; import org.apache.rocketmq.broker.mqtrace.SendMessageContext; +import org.apache.rocketmq.common.AbortProcessException; +import org.apache.rocketmq.common.KeyBuilder; import org.apache.rocketmq.common.MQVersion; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.TopicConfig; import org.apache.rocketmq.common.TopicFilterType; import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.attribute.CleanupPolicy; +import org.apache.rocketmq.common.attribute.TopicMessageType; import org.apache.rocketmq.common.constant.PermName; import org.apache.rocketmq.common.help.FAQUrl; import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageClientIDSetter; import org.apache.rocketmq.common.message.MessageConst; import org.apache.rocketmq.common.message.MessageDecoder; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageExtBatch; -import org.apache.rocketmq.common.protocol.RequestCode; -import org.apache.rocketmq.common.protocol.ResponseCode; -import org.apache.rocketmq.common.protocol.header.ConsumerSendMsgBackRequestHeader; -import org.apache.rocketmq.common.protocol.header.SendMessageRequestHeader; -import org.apache.rocketmq.common.protocol.header.SendMessageResponseHeader; -import org.apache.rocketmq.common.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; import org.apache.rocketmq.common.sysflag.MessageSysFlag; -import org.apache.rocketmq.common.sysflag.TopicSysFlag; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.common.utils.CleanupPolicyUtils; +import org.apache.rocketmq.common.utils.MessageUtils; +import org.apache.rocketmq.common.utils.QueueTypeUtils; import org.apache.rocketmq.remoting.exception.RemotingCommandException; import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; import org.apache.rocketmq.remoting.protocol.RemotingCommand; -import org.apache.rocketmq.store.MessageExtBrokerInner; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageResponseHeader; +import org.apache.rocketmq.remoting.protocol.statictopic.LogicQueueMappingItem; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingContext; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingDetail; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.store.AppendMessageResult; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.MessageStore; import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.config.BrokerRole; import org.apache.rocketmq.store.config.StorePathConfigHelper; import org.apache.rocketmq.store.stats.BrokerStatsManager; -public class SendMessageProcessor extends AbstractSendMessageProcessor implements NettyRequestProcessor { +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_CONSUMER_GROUP; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_IS_SYSTEM; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_MESSAGE_TYPE; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_TOPIC; +import static org.apache.rocketmq.remoting.protocol.RemotingCommand.buildErrorResponse; - private List consumeMessageHookList; +public class SendMessageProcessor extends AbstractSendMessageProcessor implements NettyRequestProcessor { public SendMessageProcessor(final BrokerController brokerController) { super(brokerController); @@ -62,7 +86,7 @@ public SendMessageProcessor(final BrokerController brokerController) { @Override public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException { - SendMessageContext mqtraceContext; + SendMessageContext sendMessageContext; switch (request.getCode()) { case RequestCode.CONSUMER_SEND_MSG_BACK: return this.consumerSendMsgBack(ctx, request); @@ -71,187 +95,93 @@ public RemotingCommand processRequest(ChannelHandlerContext ctx, if (requestHeader == null) { return null; } - - mqtraceContext = buildMsgContext(ctx, requestHeader); - this.executeSendMessageHookBefore(ctx, request, mqtraceContext); + TopicQueueMappingContext mappingContext = this.brokerController.getTopicQueueMappingManager().buildTopicQueueMappingContext(requestHeader, true); + RemotingCommand rewriteResult = this.brokerController.getTopicQueueMappingManager().rewriteRequestForStaticTopic(requestHeader, mappingContext); + if (rewriteResult != null) { + return rewriteResult; + } + sendMessageContext = buildMsgContext(ctx, requestHeader, request); + try { + this.executeSendMessageHookBefore(sendMessageContext); + } catch (AbortProcessException e) { + final RemotingCommand errorResponse = RemotingCommand.createResponseCommand(e.getResponseCode(), e.getErrorMessage()); + errorResponse.setOpaque(request.getOpaque()); + return errorResponse; + } RemotingCommand response; + clearReservedProperties(requestHeader); + if (requestHeader.isBatch()) { - response = this.sendBatchMessage(ctx, request, mqtraceContext, requestHeader); + response = this.sendBatchMessage(ctx, request, sendMessageContext, requestHeader, mappingContext, + (ctx1, response1) -> executeSendMessageHookAfter(response1, ctx1)); } else { - response = this.sendMessage(ctx, request, mqtraceContext, requestHeader); + response = this.sendMessage(ctx, request, sendMessageContext, requestHeader, mappingContext, + (ctx12, response12) -> executeSendMessageHookAfter(response12, ctx12)); } - this.executeSendMessageHookAfter(response, mqtraceContext); return response; } } @Override public boolean rejectRequest() { - return this.brokerController.getMessageStore().isOSPageCacheBusy() || - this.brokerController.getMessageStore().isTransientStorePoolDeficient(); - } - - private RemotingCommand consumerSendMsgBack(final ChannelHandlerContext ctx, final RemotingCommand request) - throws RemotingCommandException { - final RemotingCommand response = RemotingCommand.createResponseCommand(null); - final ConsumerSendMsgBackRequestHeader requestHeader = - (ConsumerSendMsgBackRequestHeader) request.decodeCommandCustomHeader(ConsumerSendMsgBackRequestHeader.class); - - if (this.hasConsumeMessageHook() && !UtilAll.isBlank(requestHeader.getOriginMsgId())) { - - ConsumeMessageContext context = new ConsumeMessageContext(); - context.setConsumerGroup(requestHeader.getGroup()); - context.setTopic(requestHeader.getOriginTopic()); - context.setCommercialRcvStats(BrokerStatsManager.StatsType.SEND_BACK); - context.setCommercialRcvTimes(1); - context.setCommercialOwner(request.getExtFields().get(BrokerStatsManager.COMMERCIAL_OWNER)); - - this.executeConsumeMessageHookAfter(context); - } - - SubscriptionGroupConfig subscriptionGroupConfig = - this.brokerController.getSubscriptionGroupManager().findSubscriptionGroupConfig(requestHeader.getGroup()); - if (null == subscriptionGroupConfig) { - response.setCode(ResponseCode.SUBSCRIPTION_GROUP_NOT_EXIST); - response.setRemark("subscription group not exist, " + requestHeader.getGroup() + " " - + FAQUrl.suggestTodo(FAQUrl.SUBSCRIPTION_GROUP_NOT_EXIST)); - return response; + if (!this.brokerController.getBrokerConfig().isEnableSlaveActingMaster() && this.brokerController.getMessageStoreConfig().getBrokerRole() == BrokerRole.SLAVE) { + return true; } - if (!PermName.isWriteable(this.brokerController.getBrokerConfig().getBrokerPermission())) { - response.setCode(ResponseCode.NO_PERMISSION); - response.setRemark("the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1() + "] sending message is forbidden"); - return response; + if (this.brokerController.getMessageStore().isOSPageCacheBusy() || this.brokerController.getMessageStore().isTransientStorePoolDeficient()) { + return true; } - if (subscriptionGroupConfig.getRetryQueueNums() <= 0) { - response.setCode(ResponseCode.SUCCESS); - response.setRemark(null); - return response; - } - - String newTopic = MixAll.getRetryTopic(requestHeader.getGroup()); - int queueIdInt = Math.abs(this.random.nextInt() % 99999999) % subscriptionGroupConfig.getRetryQueueNums(); - - int topicSysFlag = 0; - if (requestHeader.isUnitMode()) { - topicSysFlag = TopicSysFlag.buildSysFlag(false, true); - } - - TopicConfig topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod( - newTopic, - subscriptionGroupConfig.getRetryQueueNums(), - PermName.PERM_WRITE | PermName.PERM_READ, topicSysFlag); - if (null == topicConfig) { - response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark("topic[" + newTopic + "] not exist"); - return response; - } - - if (!PermName.isWriteable(topicConfig.getPerm())) { - response.setCode(ResponseCode.NO_PERMISSION); - response.setRemark(String.format("the topic[%s] sending message is forbidden", newTopic)); - return response; - } - - MessageExt msgExt = this.brokerController.getMessageStore().lookMessageByOffset(requestHeader.getOffset()); - if (null == msgExt) { - response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark("look message by offset failed, " + requestHeader.getOffset()); - return response; - } - - final String retryTopic = msgExt.getProperty(MessageConst.PROPERTY_RETRY_TOPIC); - if (null == retryTopic) { - MessageAccessor.putProperty(msgExt, MessageConst.PROPERTY_RETRY_TOPIC, msgExt.getTopic()); - } - msgExt.setWaitStoreMsgOK(false); - - int delayLevel = requestHeader.getDelayLevel(); - - int maxReconsumeTimes = subscriptionGroupConfig.getRetryMaxTimes(); - if (request.getVersion() >= MQVersion.Version.V3_4_9.ordinal()) { - maxReconsumeTimes = requestHeader.getMaxReconsumeTimes(); - } + return false; + } - if (msgExt.getReconsumeTimes() >= maxReconsumeTimes - || delayLevel < 0) { - newTopic = MixAll.getDLQTopic(requestHeader.getGroup()); - queueIdInt = Math.abs(this.random.nextInt() % 99999999) % DLQ_NUMS_PER_GROUP; + private void clearReservedProperties(SendMessageRequestHeader requestHeader) { + String properties = requestHeader.getProperties(); + properties = MessageUtils.deleteProperty(properties, MessageConst.PROPERTY_POP_CK); + requestHeader.setProperties(properties); + } - topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(newTopic, - DLQ_NUMS_PER_GROUP, - PermName.PERM_WRITE, 0 - ); - if (null == topicConfig) { - response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark("topic[" + newTopic + "] not exist"); - return response; + /** + * If the response is not null, it meets some errors + * + * @return + */ + + private RemotingCommand rewriteResponseForStaticTopic(SendMessageResponseHeader responseHeader, + TopicQueueMappingContext mappingContext) { + try { + if (mappingContext.getMappingDetail() == null) { + return null; } - } else { - if (0 == delayLevel) { - delayLevel = 3 + msgExt.getReconsumeTimes(); - } - - msgExt.setDelayTimeLevel(delayLevel); - } - - MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); - msgInner.setTopic(newTopic); - msgInner.setBody(msgExt.getBody()); - msgInner.setFlag(msgExt.getFlag()); - MessageAccessor.setProperties(msgInner, msgExt.getProperties()); - msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgExt.getProperties())); - msgInner.setTagsCode(MessageExtBrokerInner.tagsString2tagsCode(null, msgExt.getTags())); - - msgInner.setQueueId(queueIdInt); - msgInner.setSysFlag(msgExt.getSysFlag()); - msgInner.setBornTimestamp(msgExt.getBornTimestamp()); - msgInner.setBornHost(msgExt.getBornHost()); - msgInner.setStoreHost(this.getStoreHost()); - msgInner.setReconsumeTimes(msgExt.getReconsumeTimes() + 1); - - String originMsgId = MessageAccessor.getOriginMessageId(msgExt); - MessageAccessor.setOriginMessageId(msgInner, UtilAll.isBlank(originMsgId) ? msgExt.getMsgId() : originMsgId); - - PutMessageResult putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner); - if (putMessageResult != null) { - switch (putMessageResult.getPutMessageStatus()) { - case PUT_OK: - String backTopic = msgExt.getTopic(); - String correctTopic = msgExt.getProperty(MessageConst.PROPERTY_RETRY_TOPIC); - if (correctTopic != null) { - backTopic = correctTopic; - } + TopicQueueMappingDetail mappingDetail = mappingContext.getMappingDetail(); - this.brokerController.getBrokerStatsManager().incSendBackNums(requestHeader.getGroup(), backTopic); - - response.setCode(ResponseCode.SUCCESS); - response.setRemark(null); - - return response; - default: - break; + LogicQueueMappingItem mappingItem = mappingContext.getLeaderItem(); + if (mappingItem == null) { + return buildErrorResponse(ResponseCode.NOT_LEADER_FOR_QUEUE, String.format("%s-%d does not exit in request process of current broker %s", mappingContext.getTopic(), mappingContext.getGlobalId(), mappingDetail.getBname())); } - - response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark(putMessageResult.getPutMessageStatus().name()); - return response; + //no need to care the broker name + long staticLogicOffset = mappingItem.computeStaticQueueOffsetLoosely(responseHeader.getQueueOffset()); + if (staticLogicOffset < 0) { + //if the logic offset is -1, just let it go + //maybe we need a dynamic config + //return buildErrorResponse(ResponseCode.NOT_LEADER_FOR_QUEUE, String.format("%s-%d convert offset error in current broker %s", mappingContext.getTopic(), mappingContext.getGlobalId(), mappingDetail.getBname())); + } + responseHeader.setQueueId(mappingContext.getGlobalId()); + responseHeader.setQueueOffset(staticLogicOffset); + } catch (Throwable t) { + return buildErrorResponse(ResponseCode.SYSTEM_ERROR, t.getMessage()); } - - response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark("putMessageResult is null"); - return response; + return null; } private boolean handleRetryAndDLQ(SendMessageRequestHeader requestHeader, RemotingCommand response, RemotingCommand request, - MessageExt msg, TopicConfig topicConfig) { + MessageExt msg, TopicConfig topicConfig, Map properties) { String newTopic = requestHeader.getTopic(); if (null != newTopic && newTopic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { - String groupName = newTopic.substring(MixAll.RETRY_GROUP_TOPIC_PREFIX.length()); + String groupName = KeyBuilder.parseGroup(newTopic); SubscriptionGroupConfig subscriptionGroupConfig = this.brokerController.getSubscriptionGroupManager().findSubscriptionGroupConfig(groupName); if (null == subscriptionGroupConfig) { @@ -262,19 +192,37 @@ private boolean handleRetryAndDLQ(SendMessageRequestHeader requestHeader, Remoti } int maxReconsumeTimes = subscriptionGroupConfig.getRetryMaxTimes(); - if (request.getVersion() >= MQVersion.Version.V3_4_9.ordinal()) { + if (request.getVersion() >= MQVersion.Version.V3_4_9.ordinal() && requestHeader.getMaxReconsumeTimes() != null) { maxReconsumeTimes = requestHeader.getMaxReconsumeTimes(); } int reconsumeTimes = requestHeader.getReconsumeTimes() == null ? 0 : requestHeader.getReconsumeTimes(); - if (reconsumeTimes >= maxReconsumeTimes) { + + boolean sendRetryMessageToDeadLetterQueueDirectly = false; + if (!brokerController.getRebalanceLockManager().isLockAllExpired(groupName)) { + LOGGER.info("Group has unexpired lock record, which show it is ordered message, send it to DLQ " + + "right now group={}, topic={}, reconsumeTimes={}, maxReconsumeTimes={}.", groupName, + newTopic, reconsumeTimes, maxReconsumeTimes); + sendRetryMessageToDeadLetterQueueDirectly = true; + } + + if (reconsumeTimes > maxReconsumeTimes || sendRetryMessageToDeadLetterQueueDirectly) { + Attributes attributes = BrokerMetricsManager.newAttributesBuilder() + .put(LABEL_CONSUMER_GROUP, requestHeader.getProducerGroup()) + .put(LABEL_TOPIC, requestHeader.getTopic()) + .put(LABEL_IS_SYSTEM, BrokerMetricsManager.isSystem(requestHeader.getTopic(), requestHeader.getProducerGroup())) + .build(); + BrokerMetricsManager.sendToDlqMessages.add(1, attributes); + + properties.put(MessageConst.PROPERTY_DELAY_TIME_LEVEL, "-1"); newTopic = MixAll.getDLQTopic(groupName); - int queueIdInt = Math.abs(this.random.nextInt() % 99999999) % DLQ_NUMS_PER_GROUP; + int queueIdInt = randomQueueId(DLQ_NUMS_PER_GROUP); topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(newTopic, DLQ_NUMS_PER_GROUP, - PermName.PERM_WRITE, 0 + PermName.PERM_WRITE | PermName.PERM_READ, 0 ); msg.setTopic(newTopic); msg.setQueueId(queueIdInt); + msg.setDelayTimeLevel(0); if (null == topicConfig) { response.setCode(ResponseCode.SYSTEM_ERROR); response.setRemark("topic[" + newTopic + "] not exist"); @@ -290,80 +238,135 @@ private boolean handleRetryAndDLQ(SendMessageRequestHeader requestHeader, Remoti return true; } - private RemotingCommand sendMessage(final ChannelHandlerContext ctx, + public RemotingCommand sendMessage(final ChannelHandlerContext ctx, final RemotingCommand request, final SendMessageContext sendMessageContext, - final SendMessageRequestHeader requestHeader) throws RemotingCommandException { - - final RemotingCommand response = RemotingCommand.createResponseCommand(SendMessageResponseHeader.class); - final SendMessageResponseHeader responseHeader = (SendMessageResponseHeader) response.readCustomHeader(); - - response.setOpaque(request.getOpaque()); + final SendMessageRequestHeader requestHeader, + final TopicQueueMappingContext mappingContext, + final SendMessageCallback sendMessageCallback) throws RemotingCommandException { - response.addExtField(MessageConst.PROPERTY_MSG_REGION, this.brokerController.getBrokerConfig().getRegionId()); - response.addExtField(MessageConst.PROPERTY_TRACE_SWITCH, String.valueOf(this.brokerController.getBrokerConfig().isTraceOn())); - - log.debug("receive SendMessage request command, {}", request); - - final long startTimstamp = this.brokerController.getBrokerConfig().getStartAcceptSendRequestTimeStamp(); - if (this.brokerController.getMessageStore().now() < startTimstamp) { - response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark(String.format("broker unable to service, until %s", UtilAll.timeMillisToHumanString2(startTimstamp))); - return response; - } - - response.setCode(-1); - super.msgCheck(ctx, requestHeader, response); + final RemotingCommand response = preSend(ctx, request, requestHeader); if (response.getCode() != -1) { return response; } + final SendMessageResponseHeader responseHeader = (SendMessageResponseHeader) response.readCustomHeader(); + final byte[] body = request.getBody(); int queueIdInt = requestHeader.getQueueId(); TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic()); if (queueIdInt < 0) { - queueIdInt = Math.abs(this.random.nextInt() % 99999999) % topicConfig.getWriteQueueNums(); + queueIdInt = randomQueueId(topicConfig.getWriteQueueNums()); } MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); msgInner.setTopic(requestHeader.getTopic()); msgInner.setQueueId(queueIdInt); - if (!handleRetryAndDLQ(requestHeader, response, request, msgInner, topicConfig)) { + Map oriProps = MessageDecoder.string2messageProperties(requestHeader.getProperties()); + if (!handleRetryAndDLQ(requestHeader, response, request, msgInner, topicConfig, oriProps)) { return response; } msgInner.setBody(body); msgInner.setFlag(requestHeader.getFlag()); - MessageAccessor.setProperties(msgInner, MessageDecoder.string2messageProperties(requestHeader.getProperties())); - msgInner.setPropertiesString(requestHeader.getProperties()); + + String uniqKey = oriProps.get(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX); + if (uniqKey == null || uniqKey.length() <= 0) { + uniqKey = MessageClientIDSetter.createUniqID(); + oriProps.put(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, uniqKey); + } + + MessageAccessor.setProperties(msgInner, oriProps); + + CleanupPolicy cleanupPolicy = CleanupPolicyUtils.getDeletePolicy(Optional.of(topicConfig)); + if (Objects.equals(cleanupPolicy, CleanupPolicy.COMPACTION)) { + if (StringUtils.isBlank(msgInner.getKeys())) { + response.setCode(ResponseCode.MESSAGE_ILLEGAL); + response.setRemark("Required message key is missing"); + return response; + } + } + + msgInner.setTagsCode(MessageExtBrokerInner.tagsString2tagsCode(topicConfig.getTopicFilterType(), msgInner.getTags())); msgInner.setBornTimestamp(requestHeader.getBornTimestamp()); msgInner.setBornHost(ctx.channel().remoteAddress()); msgInner.setStoreHost(this.getStoreHost()); msgInner.setReconsumeTimes(requestHeader.getReconsumeTimes() == null ? 0 : requestHeader.getReconsumeTimes()); + String clusterName = this.brokerController.getBrokerConfig().getBrokerClusterName(); + MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_CLUSTER, clusterName); + + msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); - if (this.brokerController.getBrokerConfig().isRejectTransactionMessage()) { - String traFlag = msgInner.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED); - if (traFlag != null) { + // Map oriProps = MessageDecoder.string2messageProperties(requestHeader.getProperties()); + String traFlag = oriProps.get(MessageConst.PROPERTY_TRANSACTION_PREPARED); + boolean sendTransactionPrepareMessage; + if (Boolean.parseBoolean(traFlag) + && !(msgInner.getReconsumeTimes() > 0 && msgInner.getDelayTimeLevel() > 0)) { //For client under version 4.6.1 + if (this.brokerController.getBrokerConfig().isRejectTransactionMessage()) { response.setCode(ResponseCode.NO_PERMISSION); response.setRemark( - "the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1() + "] sending transaction message is forbidden"); + "the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1() + + "] sending transaction message is forbidden"); return response; } + sendTransactionPrepareMessage = true; + } else { + sendTransactionPrepareMessage = false; } - PutMessageResult putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner); + long beginTimeMillis = this.brokerController.getMessageStore().now(); + + if (brokerController.getBrokerConfig().isAsyncSendEnable()) { + CompletableFuture asyncPutMessageFuture; + if (sendTransactionPrepareMessage) { + asyncPutMessageFuture = this.brokerController.getTransactionalMessageService().asyncPrepareMessage(msgInner); + } else { + asyncPutMessageFuture = this.brokerController.getMessageStore().asyncPutMessage(msgInner); + } + + final int finalQueueIdInt = queueIdInt; + final MessageExtBrokerInner finalMsgInner = msgInner; + asyncPutMessageFuture.thenAcceptAsync(putMessageResult -> { + RemotingCommand responseFuture = + handlePutMessageResult(putMessageResult, response, request, finalMsgInner, responseHeader, sendMessageContext, + ctx, finalQueueIdInt, beginTimeMillis, mappingContext, BrokerMetricsManager.getMessageType(requestHeader)); + if (responseFuture != null) { + doResponse(ctx, request, responseFuture); + } - return handlePutMessageResult(putMessageResult, response, request, msgInner, responseHeader, sendMessageContext, ctx, queueIdInt); + // record the transaction metrics, responseFuture == null means put successfully + if (sendTransactionPrepareMessage && (responseFuture == null || responseFuture.getCode() == ResponseCode.SUCCESS)) { + this.brokerController.getTransactionalMessageService().getTransactionMetrics().addAndGet(msgInner.getProperty(MessageConst.PROPERTY_REAL_TOPIC), 1); + } + sendMessageCallback.onComplete(sendMessageContext, response); + }, this.brokerController.getPutMessageFutureExecutor()); + // Returns null to release the send message thread + return null; + } else { + PutMessageResult putMessageResult = null; + if (sendTransactionPrepareMessage) { + putMessageResult = this.brokerController.getTransactionalMessageService().prepareMessage(msgInner); + } else { + putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner); + } + handlePutMessageResult(putMessageResult, response, request, msgInner, responseHeader, sendMessageContext, ctx, queueIdInt, beginTimeMillis, mappingContext, BrokerMetricsManager.getMessageType(requestHeader)); + // record the transaction metrics + if (putMessageResult.getPutMessageStatus() == PutMessageStatus.PUT_OK && putMessageResult.getAppendMessageResult().isOk()) { + this.brokerController.getTransactionalMessageService().getTransactionMetrics().addAndGet(msgInner.getProperty(MessageConst.PROPERTY_REAL_TOPIC), 1); + } + sendMessageCallback.onComplete(sendMessageContext, response); + return response; + } } private RemotingCommand handlePutMessageResult(PutMessageResult putMessageResult, RemotingCommand response, - RemotingCommand request, MessageExt msg, - SendMessageResponseHeader responseHeader, SendMessageContext sendMessageContext, ChannelHandlerContext ctx, - int queueIdInt) { + RemotingCommand request, MessageExt msg, SendMessageResponseHeader responseHeader, + SendMessageContext sendMessageContext, ChannelHandlerContext ctx, int queueIdInt, long beginTimeMillis, + TopicQueueMappingContext mappingContext, TopicMessageType messageType) { if (putMessageResult == null) { response.setCode(ResponseCode.SYSTEM_ERROR); response.setRemark("store putMessage return null"); @@ -391,25 +394,49 @@ private RemotingCommand handlePutMessageResult(PutMessageResult putMessageResult break; // Failed - case CREATE_MAPEDFILE_FAILED: + case IN_SYNC_REPLICAS_NOT_ENOUGH: + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("in-sync replicas not enough"); + break; + case CREATE_MAPPED_FILE_FAILED: response.setCode(ResponseCode.SYSTEM_ERROR); response.setRemark("create mapped file failed, server is busy or broken."); break; case MESSAGE_ILLEGAL: case PROPERTIES_SIZE_EXCEEDED: response.setCode(ResponseCode.MESSAGE_ILLEGAL); - response.setRemark( - "the message is illegal, maybe msg body or properties length not matched. msg body length limit 128k, msg properties length limit 32k."); + response.setRemark(String.format("the message is illegal, maybe msg body or properties length not matched. msg body length limit %dB, msg properties length limit 32KB.", + this.brokerController.getMessageStoreConfig().getMaxMessageSize())); + break; + case WHEEL_TIMER_MSG_ILLEGAL: + response.setCode(ResponseCode.MESSAGE_ILLEGAL); + response.setRemark(String.format("timer message illegal, the delay time should not be bigger than the max delay %dms; or if set del msg, the delay time should be bigger than the current time", + this.brokerController.getMessageStoreConfig().getTimerMaxDelaySec() * 1000L)); + break; + case WHEEL_TIMER_FLOW_CONTROL: + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(String.format("timer message is under flow control, max num limit is %d or the current value is greater than %d and less than %d, trigger random flow control", + this.brokerController.getMessageStoreConfig().getTimerCongestNumEachSlot() * 2L, this.brokerController.getMessageStoreConfig().getTimerCongestNumEachSlot(), this.brokerController.getMessageStoreConfig().getTimerCongestNumEachSlot() * 2L)); + break; + case WHEEL_TIMER_NOT_ENABLE: + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(String.format("accurate timer message is not enabled, timerWheelEnable is %s", + this.brokerController.getMessageStoreConfig().isTimerWheelEnable())); break; case SERVICE_NOT_AVAILABLE: response.setCode(ResponseCode.SERVICE_NOT_AVAILABLE); response.setRemark( - "service not available now, maybe disk full, " + diskUtil() + ", maybe your broker machine memory too small."); + "service not available now. It may be caused by one of the following reasons: " + + "the broker's disk is full [" + diskUtil() + "], messages are put to the slave, message store has been shut down, etc."); break; - case OS_PAGECACHE_BUSY: + case OS_PAGE_CACHE_BUSY: response.setCode(ResponseCode.SYSTEM_ERROR); response.setRemark("[PC_SYNCHRONIZED]broker busy, start flow control for a while"); break; + case LMQ_CONSUME_QUEUE_NUM_EXCEEDED: + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("[LMQ_CONSUME_QUEUE_NUM_EXCEEDED]broker config enableLmq and enableMultiDispatch, lmq consumeQueue num exceed maxLmqConsumeQueueNum config num, default limit 2w."); + break; case UNKNOWN_ERROR: response.setCode(ResponseCode.SYSTEM_ERROR); response.setRemark("UNKNOWN_ERROR"); @@ -421,18 +448,46 @@ private RemotingCommand handlePutMessageResult(PutMessageResult putMessageResult } String owner = request.getExtFields().get(BrokerStatsManager.COMMERCIAL_OWNER); + String authType = request.getExtFields().get(BrokerStatsManager.ACCOUNT_AUTH_TYPE); + String ownerParent = request.getExtFields().get(BrokerStatsManager.ACCOUNT_OWNER_PARENT); + String ownerSelf = request.getExtFields().get(BrokerStatsManager.ACCOUNT_OWNER_SELF); + int commercialSizePerMsg = brokerController.getBrokerConfig().getCommercialSizePerMsg(); if (sendOK) { + if (TopicValidator.RMQ_SYS_SCHEDULE_TOPIC.equals(msg.getTopic())) { + this.brokerController.getBrokerStatsManager().incQueuePutNums(msg.getTopic(), msg.getQueueId(), putMessageResult.getAppendMessageResult().getMsgNum(), 1); + this.brokerController.getBrokerStatsManager().incQueuePutSize(msg.getTopic(), msg.getQueueId(), putMessageResult.getAppendMessageResult().getWroteBytes()); + } + this.brokerController.getBrokerStatsManager().incTopicPutNums(msg.getTopic(), putMessageResult.getAppendMessageResult().getMsgNum(), 1); this.brokerController.getBrokerStatsManager().incTopicPutSize(msg.getTopic(), putMessageResult.getAppendMessageResult().getWroteBytes()); - this.brokerController.getBrokerStatsManager().incBrokerPutNums(putMessageResult.getAppendMessageResult().getMsgNum()); + this.brokerController.getBrokerStatsManager().incBrokerPutNums(msg.getTopic(), putMessageResult.getAppendMessageResult().getMsgNum()); + this.brokerController.getBrokerStatsManager().incTopicPutLatency(msg.getTopic(), queueIdInt, + (int) (this.brokerController.getMessageStore().now() - beginTimeMillis)); + + if (!BrokerMetricsManager.isRetryOrDlqTopic(msg.getTopic())) { + Attributes attributes = BrokerMetricsManager.newAttributesBuilder() + .put(LABEL_TOPIC, msg.getTopic()) + .put(LABEL_MESSAGE_TYPE, messageType.getMetricsValue()) + .put(LABEL_IS_SYSTEM, TopicValidator.isSystemTopic(msg.getTopic())) + .build(); + BrokerMetricsManager.messagesInTotal.add(putMessageResult.getAppendMessageResult().getMsgNum(), attributes); + BrokerMetricsManager.throughputInTotal.add(putMessageResult.getAppendMessageResult().getWroteBytes(), attributes); + BrokerMetricsManager.messageSize.record(putMessageResult.getAppendMessageResult().getWroteBytes() / putMessageResult.getAppendMessageResult().getMsgNum(), attributes); + } response.setRemark(null); responseHeader.setMsgId(putMessageResult.getAppendMessageResult().getMsgId()); responseHeader.setQueueId(queueIdInt); responseHeader.setQueueOffset(putMessageResult.getAppendMessageResult().getLogicsOffset()); + responseHeader.setTransactionId(MessageClientIDSetter.getUniqID(msg)); + + RemotingCommand rewriteResult = rewriteResponseForStaticTopic(responseHeader, mappingContext); + if (rewriteResult != null) { + return rewriteResult; + } doResponse(ctx, request, response); @@ -443,23 +498,45 @@ private RemotingCommand handlePutMessageResult(PutMessageResult putMessageResult int commercialBaseCount = brokerController.getBrokerConfig().getCommercialBaseCount(); int wroteSize = putMessageResult.getAppendMessageResult().getWroteBytes(); - int incValue = (int) Math.ceil(wroteSize / BrokerStatsManager.SIZE_PER_COUNT) * commercialBaseCount; + int msgNum = putMessageResult.getAppendMessageResult().getMsgNum(); + int commercialMsgNum = (int) Math.ceil(wroteSize / (double) commercialSizePerMsg); + int incValue = commercialMsgNum * commercialBaseCount; sendMessageContext.setCommercialSendStats(BrokerStatsManager.StatsType.SEND_SUCCESS); sendMessageContext.setCommercialSendTimes(incValue); sendMessageContext.setCommercialSendSize(wroteSize); sendMessageContext.setCommercialOwner(owner); + + sendMessageContext.setSendStat(BrokerStatsManager.StatsType.SEND_SUCCESS); + sendMessageContext.setCommercialSendMsgNum(commercialMsgNum); + sendMessageContext.setAccountAuthType(authType); + sendMessageContext.setAccountOwnerParent(ownerParent); + sendMessageContext.setAccountOwnerSelf(ownerSelf); + sendMessageContext.setSendMsgSize(wroteSize); + sendMessageContext.setSendMsgNum(msgNum); } return null; } else { if (hasSendMessageHook()) { + AppendMessageResult appendMessageResult = putMessageResult.getAppendMessageResult(); + + // TODO process partial failures of batch message int wroteSize = request.getBody().length; - int incValue = (int) Math.ceil(wroteSize / BrokerStatsManager.SIZE_PER_COUNT); + int msgNum = Math.max(appendMessageResult != null ? appendMessageResult.getMsgNum() : 1, 1); + int commercialMsgNum = (int) Math.ceil(wroteSize / (double) commercialSizePerMsg); sendMessageContext.setCommercialSendStats(BrokerStatsManager.StatsType.SEND_FAILURE); - sendMessageContext.setCommercialSendTimes(incValue); + sendMessageContext.setCommercialSendTimes(commercialMsgNum); sendMessageContext.setCommercialSendSize(wroteSize); sendMessageContext.setCommercialOwner(owner); + + sendMessageContext.setSendStat(BrokerStatsManager.StatsType.SEND_FAILURE); + sendMessageContext.setCommercialSendMsgNum(commercialMsgNum); + sendMessageContext.setAccountAuthType(authType); + sendMessageContext.setAccountOwnerParent(ownerParent); + sendMessageContext.setAccountOwnerSelf(ownerSelf); + sendMessageContext.setSendMsgSize(wroteSize); + sendMessageContext.setSendMsgNum(msgNum); } } return response; @@ -468,27 +545,12 @@ private RemotingCommand handlePutMessageResult(PutMessageResult putMessageResult private RemotingCommand sendBatchMessage(final ChannelHandlerContext ctx, final RemotingCommand request, final SendMessageContext sendMessageContext, - final SendMessageRequestHeader requestHeader) throws RemotingCommandException { - - final RemotingCommand response = RemotingCommand.createResponseCommand(SendMessageResponseHeader.class); + final SendMessageRequestHeader requestHeader, + TopicQueueMappingContext mappingContext, + final SendMessageCallback sendMessageCallback) { + final RemotingCommand response = preSend(ctx, request, requestHeader); final SendMessageResponseHeader responseHeader = (SendMessageResponseHeader) response.readCustomHeader(); - response.setOpaque(request.getOpaque()); - - response.addExtField(MessageConst.PROPERTY_MSG_REGION, this.brokerController.getBrokerConfig().getRegionId()); - response.addExtField(MessageConst.PROPERTY_TRACE_SWITCH, String.valueOf(this.brokerController.getBrokerConfig().isTraceOn())); - - log.debug("Receive SendMessage request command {}", request); - - final long startTimstamp = this.brokerController.getBrokerConfig().getStartAcceptSendRequestTimeStamp(); - if (this.brokerController.getMessageStore().now() < startTimstamp) { - response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark(String.format("broker unable to service, until %s", UtilAll.timeMillisToHumanString2(startTimstamp))); - return response; - } - - response.setCode(-1); - super.msgCheck(ctx, requestHeader, response); if (response.getCode() != -1) { return response; } @@ -497,7 +559,7 @@ private RemotingCommand sendBatchMessage(final ChannelHandlerContext ctx, TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic()); if (queueIdInt < 0) { - queueIdInt = Math.abs(this.random.nextInt() % 99999999) % topicConfig.getWriteQueueNums(); + queueIdInt = randomQueueId(topicConfig.getWriteQueueNums()); } if (requestHeader.getTopic().length() > Byte.MAX_VALUE) { @@ -528,35 +590,76 @@ private RemotingCommand sendBatchMessage(final ChannelHandlerContext ctx, messageExtBatch.setBornHost(ctx.channel().remoteAddress()); messageExtBatch.setStoreHost(this.getStoreHost()); messageExtBatch.setReconsumeTimes(requestHeader.getReconsumeTimes() == null ? 0 : requestHeader.getReconsumeTimes()); + String clusterName = this.brokerController.getBrokerConfig().getBrokerClusterName(); + MessageAccessor.putProperty(messageExtBatch, MessageConst.PROPERTY_CLUSTER, clusterName); - PutMessageResult putMessageResult = this.brokerController.getMessageStore().putMessages(messageExtBatch); + boolean isInnerBatch = false; - return handlePutMessageResult(putMessageResult, response, request, messageExtBatch, responseHeader, sendMessageContext, ctx, queueIdInt); - } + if (QueueTypeUtils.isBatchCq(Optional.of(topicConfig)) && MessageClientIDSetter.getUniqID(messageExtBatch) != null) { + // newly introduced inner-batch message + messageExtBatch.setSysFlag(messageExtBatch.getSysFlag() | MessageSysFlag.NEED_UNWRAP_FLAG); + messageExtBatch.setSysFlag(messageExtBatch.getSysFlag() | MessageSysFlag.INNER_BATCH_FLAG); + messageExtBatch.setInnerBatch(true); - public boolean hasConsumeMessageHook() { - return consumeMessageHookList != null && !this.consumeMessageHookList.isEmpty(); - } + int innerNum = MessageDecoder.countInnerMsgNum(ByteBuffer.wrap(messageExtBatch.getBody())); - public void executeConsumeMessageHookAfter(final ConsumeMessageContext context) { - if (hasConsumeMessageHook()) { - for (ConsumeMessageHook hook : this.consumeMessageHookList) { - try { - hook.consumeMessageAfter(context); - } catch (Throwable e) { - // Ignore + MessageAccessor.putProperty(messageExtBatch, MessageConst.PROPERTY_INNER_NUM, String.valueOf(innerNum)); + messageExtBatch.setPropertiesString(MessageDecoder.messageProperties2String(messageExtBatch.getProperties())); + + // tell the producer that it's an inner-batch message response. + responseHeader.setBatchUniqId(MessageClientIDSetter.getUniqID(messageExtBatch)); + + isInnerBatch = true; + } + + long beginTimeMillis = this.brokerController.getMessageStore().now(); + + if (this.brokerController.getBrokerConfig().isAsyncSendEnable()) { + CompletableFuture asyncPutMessageFuture; + if (isInnerBatch) { + asyncPutMessageFuture = this.brokerController.getMessageStore().asyncPutMessage(messageExtBatch); + } else { + asyncPutMessageFuture = this.brokerController.getMessageStore().asyncPutMessages(messageExtBatch); + } + final int finalQueueIdInt = queueIdInt; + asyncPutMessageFuture.thenAcceptAsync(putMessageResult -> { + RemotingCommand responseFuture = + handlePutMessageResult(putMessageResult, response, request, messageExtBatch, responseHeader, + sendMessageContext, ctx, finalQueueIdInt, beginTimeMillis, mappingContext, BrokerMetricsManager.getMessageType(requestHeader)); + if (responseFuture != null) { + doResponse(ctx, request, responseFuture); } + sendMessageCallback.onComplete(sendMessageContext, response); + }, this.brokerController.getSendMessageExecutor()); + // Returns null to release the send message thread + return null; + } else { + PutMessageResult putMessageResult; + if (isInnerBatch) { + putMessageResult = this.brokerController.getMessageStore().putMessage(messageExtBatch); + } else { + putMessageResult = this.brokerController.getMessageStore().putMessages(messageExtBatch); } + handlePutMessageResult(putMessageResult, response, request, messageExtBatch, responseHeader, + sendMessageContext, ctx, queueIdInt, beginTimeMillis, mappingContext, BrokerMetricsManager.getMessageType(requestHeader)); + sendMessageCallback.onComplete(sendMessageContext, response); + return response; } } - public SocketAddress getStoreHost() { - return storeHost; - } - private String diskUtil() { - String storePathPhysic = this.brokerController.getMessageStoreConfig().getStorePathCommitLog(); - double physicRatio = UtilAll.getDiskPartitionSpaceUsedPercent(storePathPhysic); + double physicRatio = 100; + String storePath; + MessageStore messageStore = this.brokerController.getMessageStore(); + if (messageStore instanceof DefaultMessageStore) { + storePath = ((DefaultMessageStore) messageStore).getStorePathPhysic(); + } else { + storePath = this.brokerController.getMessageStoreConfig().getStorePathCommitLog(); + } + String[] paths = storePath.trim().split(MixAll.MULTI_PATH_SPLITTER); + for (String storePathPhysic : paths) { + physicRatio = Math.min(physicRatio, UtilAll.getDiskPartitionSpaceUsedPercent(storePathPhysic)); + } String storePathLogis = StorePathConfigHelper.getStorePathConsumeQueue(this.brokerController.getMessageStoreConfig().getStorePathRootDir()); @@ -569,7 +672,29 @@ private String diskUtil() { return String.format("CL: %5.2f CQ: %5.2f INDEX: %5.2f", physicRatio, logisRatio, indexRatio); } - public void registerConsumeMessageHook(List consumeMessageHookList) { - this.consumeMessageHookList = consumeMessageHookList; + private RemotingCommand preSend(ChannelHandlerContext ctx, RemotingCommand request, + SendMessageRequestHeader requestHeader) { + final RemotingCommand response = RemotingCommand.createResponseCommand(SendMessageResponseHeader.class); + + response.setOpaque(request.getOpaque()); + + response.addExtField(MessageConst.PROPERTY_MSG_REGION, this.brokerController.getBrokerConfig().getRegionId()); + response.addExtField(MessageConst.PROPERTY_TRACE_SWITCH, String.valueOf(this.brokerController.getBrokerConfig().isTraceOn())); + + LOGGER.debug("Receive SendMessage request command {}", request); + + final long startTimestamp = this.brokerController.getBrokerConfig().getStartAcceptSendRequestTimeStamp(); + + if (this.brokerController.getMessageStore().now() < startTimestamp) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(String.format("broker unable to service, until %s", UtilAll.timeMillisToHumanString2(startTimestamp))); + return response; + } + + response.setCode(-1); + super.msgCheck(ctx, requestHeader, request, response); + + return response; } + } diff --git a/store/src/main/java/org/apache/rocketmq/store/schedule/DelayOffsetSerializeWrapper.java b/broker/src/main/java/org/apache/rocketmq/broker/schedule/DelayOffsetSerializeWrapper.java similarity index 78% rename from store/src/main/java/org/apache/rocketmq/store/schedule/DelayOffsetSerializeWrapper.java rename to broker/src/main/java/org/apache/rocketmq/broker/schedule/DelayOffsetSerializeWrapper.java index 7021992c598..2485c9a9bcf 100644 --- a/store/src/main/java/org/apache/rocketmq/store/schedule/DelayOffsetSerializeWrapper.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/schedule/DelayOffsetSerializeWrapper.java @@ -14,15 +14,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.rocketmq.store.schedule; +package org.apache.rocketmq.broker.schedule; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import org.apache.rocketmq.remoting.protocol.DataVersion; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; public class DelayOffsetSerializeWrapper extends RemotingSerializable { private ConcurrentMap offsetTable = - new ConcurrentHashMap(32); + new ConcurrentHashMap<>(32); + + private DataVersion dataVersion; public ConcurrentMap getOffsetTable() { return offsetTable; @@ -31,4 +34,12 @@ public ConcurrentMap getOffsetTable() { public void setOffsetTable(ConcurrentMap offsetTable) { this.offsetTable = offsetTable; } + + public DataVersion getDataVersion() { + return dataVersion; + } + + public void setDataVersion(DataVersion dataVersion) { + this.dataVersion = dataVersion; + } } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/schedule/ScheduleMessageService.java b/broker/src/main/java/org/apache/rocketmq/broker/schedule/ScheduleMessageService.java new file mode 100644 index 00000000000..ef7e4f67894 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/schedule/ScheduleMessageService.java @@ -0,0 +1,851 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.schedule; + +import io.opentelemetry.api.common.Attributes; +import java.util.HashMap; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.metrics.BrokerMetricsManager; +import org.apache.rocketmq.common.ConfigManager; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.TopicFilterType; +import org.apache.rocketmq.common.attribute.TopicMessageType; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.running.RunningStats; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.config.StorePathConfigHelper; +import org.apache.rocketmq.store.queue.ConsumeQueueInterface; +import org.apache.rocketmq.store.queue.CqUnit; +import org.apache.rocketmq.store.queue.ReferredIterator; + +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_CONSUMER_GROUP; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_IS_SYSTEM; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_MESSAGE_TYPE; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_TOPIC; + +public class ScheduleMessageService extends ConfigManager { + private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + + private static final long FIRST_DELAY_TIME = 1000L; + private static final long DELAY_FOR_A_WHILE = 100L; + private static final long DELAY_FOR_A_PERIOD = 10000L; + private static final long WAIT_FOR_SHUTDOWN = 5000L; + private static final long DELAY_FOR_A_SLEEP = 10L; + + private final ConcurrentSkipListMap delayLevelTable = + new ConcurrentSkipListMap<>(); + + private final ConcurrentMap offsetTable = + new ConcurrentHashMap<>(32); + private final AtomicBoolean started = new AtomicBoolean(false); + private ScheduledExecutorService deliverExecutorService; + private int maxDelayLevel; + private DataVersion dataVersion = new DataVersion(); + private boolean enableAsyncDeliver = false; + private ScheduledExecutorService handleExecutorService; + private final ScheduledExecutorService scheduledPersistService; + private final Map> deliverPendingTable = + new ConcurrentHashMap<>(32); + private final BrokerController brokerController; + private final transient AtomicLong versionChangeCounter = new AtomicLong(0); + + public ScheduleMessageService(final BrokerController brokerController) { + this.brokerController = brokerController; + this.enableAsyncDeliver = brokerController.getMessageStoreConfig().isEnableScheduleAsyncDeliver(); + scheduledPersistService = ThreadUtils.newScheduledThreadPool(1, + new ThreadFactoryImpl("ScheduleMessageServicePersistThread", true, brokerController.getBrokerConfig())); + } + + public static int queueId2DelayLevel(final int queueId) { + return queueId + 1; + } + + public static int delayLevel2QueueId(final int delayLevel) { + return delayLevel - 1; + } + + public void buildRunningStats(HashMap stats) { + for (Map.Entry next : this.offsetTable.entrySet()) { + int queueId = delayLevel2QueueId(next.getKey()); + long delayOffset = next.getValue(); + long maxOffset = this.brokerController.getMessageStore().getMaxOffsetInQueue(TopicValidator.RMQ_SYS_SCHEDULE_TOPIC, queueId); + String value = String.format("%d,%d", delayOffset, maxOffset); + String key = String.format("%s_%d", RunningStats.scheduleMessageOffset.name(), next.getKey()); + stats.put(key, value); + } + } + + private void updateOffset(int delayLevel, long offset) { + this.offsetTable.put(delayLevel, offset); + if (versionChangeCounter.incrementAndGet() % brokerController.getBrokerConfig().getDelayOffsetUpdateVersionStep() == 0) { + long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0; + dataVersion.nextVersion(stateMachineVersion); + } + } + + public long computeDeliverTimestamp(final int delayLevel, final long storeTimestamp) { + Long time = this.delayLevelTable.get(delayLevel); + if (time != null) { + return time + storeTimestamp; + } + + return storeTimestamp + 1000; + } + + public void start() { + if (started.compareAndSet(false, true)) { + this.load(); + this.deliverExecutorService = ThreadUtils.newScheduledThreadPool(this.maxDelayLevel, new ThreadFactoryImpl("ScheduleMessageTimerThread_")); + if (this.enableAsyncDeliver) { + this.handleExecutorService = ThreadUtils.newScheduledThreadPool(this.maxDelayLevel, new ThreadFactoryImpl("ScheduleMessageExecutorHandleThread_")); + } + for (Map.Entry entry : this.delayLevelTable.entrySet()) { + Integer level = entry.getKey(); + Long timeDelay = entry.getValue(); + Long offset = this.offsetTable.get(level); + if (null == offset) { + offset = 0L; + } + + if (timeDelay != null) { + if (this.enableAsyncDeliver) { + this.handleExecutorService.schedule(new HandlePutResultTask(level), FIRST_DELAY_TIME, TimeUnit.MILLISECONDS); + } + this.deliverExecutorService.schedule(new DeliverDelayedMessageTimerTask(level, offset), FIRST_DELAY_TIME, TimeUnit.MILLISECONDS); + } + } + + scheduledPersistService.scheduleAtFixedRate(() -> { + try { + ScheduleMessageService.this.persist(); + } catch (Throwable e) { + log.error("scheduleAtFixedRate flush exception", e); + } + }, 10000, this.brokerController.getMessageStoreConfig().getFlushDelayOffsetInterval(), TimeUnit.MILLISECONDS); + } + } + + public void shutdown() { + stop(); + ThreadUtils.shutdown(scheduledPersistService); + } + + public boolean stop() { + if (this.started.compareAndSet(true, false) && null != this.deliverExecutorService) { + this.deliverExecutorService.shutdown(); + try { + this.deliverExecutorService.awaitTermination(WAIT_FOR_SHUTDOWN, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + log.error("deliverExecutorService awaitTermination error", e); + } + + if (this.handleExecutorService != null) { + this.handleExecutorService.shutdown(); + try { + this.handleExecutorService.awaitTermination(WAIT_FOR_SHUTDOWN, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + log.error("handleExecutorService awaitTermination error", e); + } + } + + for (int i = 1; i <= this.deliverPendingTable.size(); i++) { + log.warn("deliverPendingTable level: {}, size: {}", i, this.deliverPendingTable.get(i).size()); + } + + this.persist(); + } + return true; + } + + public boolean isStarted() { + return started.get(); + } + + public int getMaxDelayLevel() { + return maxDelayLevel; + } + + public DataVersion getDataVersion() { + return dataVersion; + } + + public void setDataVersion(DataVersion dataVersion) { + this.dataVersion = dataVersion; + } + + @Override + public String encode() { + return this.encode(false); + } + + @Override + public boolean load() { + boolean result = super.load(); + result = result && this.parseDelayLevel(); + result = result && this.correctDelayOffset(); + return result; + } + + public boolean loadWhenSyncDelayOffset() { + boolean result = super.load(); + result = result && this.parseDelayLevel(); + return result; + } + + public boolean correctDelayOffset() { + try { + for (int delayLevel : delayLevelTable.keySet()) { + ConsumeQueueInterface cq = + brokerController.getMessageStore().getQueueStore().findOrCreateConsumeQueue(TopicValidator.RMQ_SYS_SCHEDULE_TOPIC, + delayLevel2QueueId(delayLevel)); + Long currentDelayOffset = offsetTable.get(delayLevel); + if (currentDelayOffset == null || cq == null) { + continue; + } + long correctDelayOffset = currentDelayOffset; + long cqMinOffset = cq.getMinOffsetInQueue(); + long cqMaxOffset = cq.getMaxOffsetInQueue(); + if (currentDelayOffset < cqMinOffset) { + correctDelayOffset = cqMinOffset; + log.error("schedule CQ offset invalid. offset={}, cqMinOffset={}, cqMaxOffset={}, queueId={}", + currentDelayOffset, cqMinOffset, cqMaxOffset, cq.getQueueId()); + } + + if (currentDelayOffset > cqMaxOffset) { + correctDelayOffset = cqMaxOffset; + log.error("schedule CQ offset invalid. offset={}, cqMinOffset={}, cqMaxOffset={}, queueId={}", + currentDelayOffset, cqMinOffset, cqMaxOffset, cq.getQueueId()); + } + if (correctDelayOffset != currentDelayOffset) { + log.error("correct delay offset [ delayLevel {} ] from {} to {}", delayLevel, currentDelayOffset, correctDelayOffset); + offsetTable.put(delayLevel, correctDelayOffset); + } + } + } catch (Exception e) { + log.error("correctDelayOffset exception", e); + return false; + } + return true; + } + + @Override + public String configFilePath() { + return StorePathConfigHelper.getDelayOffsetStorePath(this.brokerController.getMessageStore().getMessageStoreConfig() + .getStorePathRootDir()); + } + + @Override + public void decode(String jsonString) { + if (jsonString != null) { + DelayOffsetSerializeWrapper delayOffsetSerializeWrapper = + DelayOffsetSerializeWrapper.fromJson(jsonString, DelayOffsetSerializeWrapper.class); + if (delayOffsetSerializeWrapper != null) { + this.offsetTable.putAll(delayOffsetSerializeWrapper.getOffsetTable()); + // For compatible + if (delayOffsetSerializeWrapper.getDataVersion() != null) { + this.dataVersion.assignNewOne(delayOffsetSerializeWrapper.getDataVersion()); + } + } + } + } + + @Override + public String encode(final boolean prettyFormat) { + DelayOffsetSerializeWrapper delayOffsetSerializeWrapper = new DelayOffsetSerializeWrapper(); + delayOffsetSerializeWrapper.setOffsetTable(this.offsetTable); + delayOffsetSerializeWrapper.setDataVersion(this.dataVersion); + return delayOffsetSerializeWrapper.toJson(prettyFormat); + } + + public boolean parseDelayLevel() { + HashMap timeUnitTable = new HashMap<>(); + timeUnitTable.put("s", 1000L); + timeUnitTable.put("m", 1000L * 60); + timeUnitTable.put("h", 1000L * 60 * 60); + timeUnitTable.put("d", 1000L * 60 * 60 * 24); + + String levelString = this.brokerController.getMessageStoreConfig().getMessageDelayLevel(); + try { + String[] levelArray = levelString.split(" "); + for (int i = 0; i < levelArray.length; i++) { + String value = levelArray[i]; + String ch = value.substring(value.length() - 1); + Long tu = timeUnitTable.get(ch); + + int level = i + 1; + if (level > this.maxDelayLevel) { + this.maxDelayLevel = level; + } + long num = Long.parseLong(value.substring(0, value.length() - 1)); + long delayTimeMillis = tu * num; + this.delayLevelTable.put(level, delayTimeMillis); + if (this.enableAsyncDeliver) { + this.deliverPendingTable.put(level, new LinkedBlockingQueue<>()); + } + } + } catch (Exception e) { + log.error("parse message delay level failed. messageDelayLevel = {}", levelString, e); + return false; + } + + return true; + } + + private MessageExtBrokerInner messageTimeUp(MessageExt msgExt) { + MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); + msgInner.setBody(msgExt.getBody()); + msgInner.setFlag(msgExt.getFlag()); + MessageAccessor.setProperties(msgInner, msgExt.getProperties()); + + TopicFilterType topicFilterType = MessageExt.parseTopicFilterType(msgInner.getSysFlag()); + long tagsCodeValue = + MessageExtBrokerInner.tagsString2tagsCode(topicFilterType, msgInner.getTags()); + msgInner.setTagsCode(tagsCodeValue); + msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgExt.getProperties())); + + msgInner.setSysFlag(msgExt.getSysFlag()); + msgInner.setBornTimestamp(msgExt.getBornTimestamp()); + msgInner.setBornHost(msgExt.getBornHost()); + msgInner.setStoreHost(msgExt.getStoreHost()); + msgInner.setReconsumeTimes(msgExt.getReconsumeTimes()); + + msgInner.setWaitStoreMsgOK(false); + MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_DELAY_TIME_LEVEL); + MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_TIMER_DELIVER_MS); + MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_TIMER_DELAY_SEC); + + msgInner.setTopic(msgInner.getProperty(MessageConst.PROPERTY_REAL_TOPIC)); + + String queueIdStr = msgInner.getProperty(MessageConst.PROPERTY_REAL_QUEUE_ID); + int queueId = Integer.parseInt(queueIdStr); + msgInner.setQueueId(queueId); + + return msgInner; + } + + class DeliverDelayedMessageTimerTask implements Runnable { + private final int delayLevel; + private final long offset; + + public DeliverDelayedMessageTimerTask(int delayLevel, long offset) { + this.delayLevel = delayLevel; + this.offset = offset; + } + + @Override + public void run() { + try { + if (isStarted()) { + this.executeOnTimeUp(); + } + } catch (Exception e) { + // XXX: warn and notify me + log.error("ScheduleMessageService, executeOnTimeUp exception", e); + this.scheduleNextTimerTask(this.offset, DELAY_FOR_A_PERIOD); + } + } + + private long correctDeliverTimestamp(final long now, final long deliverTimestamp) { + + long result = deliverTimestamp; + + long maxTimestamp = now + ScheduleMessageService.this.delayLevelTable.get(this.delayLevel); + if (deliverTimestamp > maxTimestamp) { + result = now; + } + + return result; + } + + public void executeOnTimeUp() { + ConsumeQueueInterface cq = + ScheduleMessageService.this.brokerController.getMessageStore().getConsumeQueue(TopicValidator.RMQ_SYS_SCHEDULE_TOPIC, + delayLevel2QueueId(delayLevel)); + + if (cq == null) { + this.scheduleNextTimerTask(this.offset, DELAY_FOR_A_WHILE); + return; + } + + ReferredIterator bufferCQ = cq.iterateFrom(this.offset); + if (bufferCQ == null) { + long resetOffset; + if ((resetOffset = cq.getMinOffsetInQueue()) > this.offset) { + log.error("schedule CQ offset invalid. offset={}, cqMinOffset={}, queueId={}", + this.offset, resetOffset, cq.getQueueId()); + } else if ((resetOffset = cq.getMaxOffsetInQueue()) < this.offset) { + log.error("schedule CQ offset invalid. offset={}, cqMaxOffset={}, queueId={}", + this.offset, resetOffset, cq.getQueueId()); + } else { + resetOffset = this.offset; + } + + this.scheduleNextTimerTask(resetOffset, DELAY_FOR_A_WHILE); + return; + } + + long nextOffset = this.offset; + try { + while (bufferCQ.hasNext() && isStarted()) { + CqUnit cqUnit = bufferCQ.next(); + long offsetPy = cqUnit.getPos(); + int sizePy = cqUnit.getSize(); + long tagsCode = cqUnit.getTagsCode(); + + if (!cqUnit.isTagsCodeValid()) { + //can't find ext content.So re compute tags code. + log.error("[BUG] can't find consume queue extend file content!addr={}, offsetPy={}, sizePy={}", + tagsCode, offsetPy, sizePy); + long msgStoreTime = ScheduleMessageService.this.brokerController.getMessageStore().getCommitLog().pickupStoreTimestamp(offsetPy, sizePy); + tagsCode = computeDeliverTimestamp(delayLevel, msgStoreTime); + } + + long now = System.currentTimeMillis(); + long deliverTimestamp = this.correctDeliverTimestamp(now, tagsCode); + + long currOffset = cqUnit.getQueueOffset(); + assert cqUnit.getBatchNum() == 1; + nextOffset = currOffset + cqUnit.getBatchNum(); + + long countdown = deliverTimestamp - now; + if (countdown > 0) { + this.scheduleNextTimerTask(currOffset, DELAY_FOR_A_WHILE); + ScheduleMessageService.this.updateOffset(this.delayLevel, currOffset); + return; + } + + MessageExt msgExt = ScheduleMessageService.this.brokerController.getMessageStore().lookMessageByOffset(offsetPy, sizePy); + if (msgExt == null) { + continue; + } + + MessageExtBrokerInner msgInner = ScheduleMessageService.this.messageTimeUp(msgExt); + if (TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC.equals(msgInner.getTopic())) { + log.error("[BUG] the real topic of schedule msg is {}, discard the msg. msg={}", + msgInner.getTopic(), msgInner); + continue; + } + + boolean deliverSuc; + if (ScheduleMessageService.this.enableAsyncDeliver) { + deliverSuc = this.asyncDeliver(msgInner, msgExt.getMsgId(), currOffset, offsetPy, sizePy); + } else { + deliverSuc = this.syncDeliver(msgInner, msgExt.getMsgId(), currOffset, offsetPy, sizePy); + } + + if (!deliverSuc) { + this.scheduleNextTimerTask(nextOffset, DELAY_FOR_A_WHILE); + return; + } + } + } catch (Exception e) { + log.error("ScheduleMessageService, messageTimeUp execute error, offset = {}", nextOffset, e); + } finally { + bufferCQ.release(); + } + + this.scheduleNextTimerTask(nextOffset, DELAY_FOR_A_WHILE); + } + + public void scheduleNextTimerTask(long offset, long delay) { + ScheduleMessageService.this.deliverExecutorService.schedule(new DeliverDelayedMessageTimerTask( + this.delayLevel, offset), delay, TimeUnit.MILLISECONDS); + } + + private boolean syncDeliver(MessageExtBrokerInner msgInner, String msgId, long offset, long offsetPy, + int sizePy) { + PutResultProcess resultProcess = deliverMessage(msgInner, msgId, offset, offsetPy, sizePy, false); + PutMessageResult result = resultProcess.get(); + boolean sendStatus = result != null && result.getPutMessageStatus() == PutMessageStatus.PUT_OK; + if (sendStatus) { + ScheduleMessageService.this.updateOffset(this.delayLevel, resultProcess.getNextOffset()); + } + return sendStatus; + } + + private boolean asyncDeliver(MessageExtBrokerInner msgInner, String msgId, long offset, long offsetPy, + int sizePy) { + Queue processesQueue = ScheduleMessageService.this.deliverPendingTable.get(this.delayLevel); + + //Flow Control + int currentPendingNum = processesQueue.size(); + int maxPendingLimit = brokerController.getMessageStoreConfig() + .getScheduleAsyncDeliverMaxPendingLimit(); + if (currentPendingNum > maxPendingLimit) { + log.warn("Asynchronous deliver triggers flow control, " + + "currentPendingNum={}, maxPendingLimit={}", currentPendingNum, maxPendingLimit); + return false; + } + + //Blocked + PutResultProcess firstProcess = processesQueue.peek(); + if (firstProcess != null && firstProcess.need2Blocked()) { + log.warn("Asynchronous deliver block. info={}", firstProcess.toString()); + return false; + } + + PutResultProcess resultProcess = deliverMessage(msgInner, msgId, offset, offsetPy, sizePy, true); + processesQueue.add(resultProcess); + return true; + } + + private PutResultProcess deliverMessage(MessageExtBrokerInner msgInner, String msgId, long offset, + long offsetPy, int sizePy, boolean autoResend) { + CompletableFuture future = + brokerController.getEscapeBridge().asyncPutMessage(msgInner); + return new PutResultProcess() + .setTopic(msgInner.getTopic()) + .setDelayLevel(this.delayLevel) + .setOffset(offset) + .setPhysicOffset(offsetPy) + .setPhysicSize(sizePy) + .setMsgId(msgId) + .setAutoResend(autoResend) + .setFuture(future) + .thenProcess(); + } + } + + public class HandlePutResultTask implements Runnable { + private final int delayLevel; + + public HandlePutResultTask(int delayLevel) { + this.delayLevel = delayLevel; + } + + @Override + public void run() { + LinkedBlockingQueue pendingQueue = + ScheduleMessageService.this.deliverPendingTable.get(this.delayLevel); + + PutResultProcess putResultProcess; + while ((putResultProcess = pendingQueue.peek()) != null) { + try { + switch (putResultProcess.getStatus()) { + case SUCCESS: + ScheduleMessageService.this.updateOffset(this.delayLevel, putResultProcess.getNextOffset()); + pendingQueue.remove(); + break; + case RUNNING: + scheduleNextTask(); + return; + case EXCEPTION: + if (!isStarted()) { + log.warn("HandlePutResultTask shutdown, info={}", putResultProcess.toString()); + return; + } + log.warn("putResultProcess error, info={}", putResultProcess.toString()); + putResultProcess.doResend(); + break; + case SKIP: + log.warn("putResultProcess skip, info={}", putResultProcess.toString()); + pendingQueue.remove(); + break; + } + } catch (Exception e) { + log.error("HandlePutResultTask exception. info={}", putResultProcess.toString(), e); + putResultProcess.doResend(); + } + } + + scheduleNextTask(); + } + + private void scheduleNextTask() { + if (isStarted()) { + ScheduleMessageService.this.handleExecutorService + .schedule(new HandlePutResultTask(this.delayLevel), DELAY_FOR_A_SLEEP, TimeUnit.MILLISECONDS); + } + } + } + + public class PutResultProcess { + private String topic; + private long offset; + private long physicOffset; + private int physicSize; + private int delayLevel; + private String msgId; + private boolean autoResend = false; + private CompletableFuture future; + + private volatile AtomicInteger resendCount = new AtomicInteger(0); + private volatile ProcessStatus status = ProcessStatus.RUNNING; + + public PutResultProcess setTopic(String topic) { + this.topic = topic; + return this; + } + + public PutResultProcess setOffset(long offset) { + this.offset = offset; + return this; + } + + public PutResultProcess setPhysicOffset(long physicOffset) { + this.physicOffset = physicOffset; + return this; + } + + public PutResultProcess setPhysicSize(int physicSize) { + this.physicSize = physicSize; + return this; + } + + public PutResultProcess setDelayLevel(int delayLevel) { + this.delayLevel = delayLevel; + return this; + } + + public PutResultProcess setMsgId(String msgId) { + this.msgId = msgId; + return this; + } + + public PutResultProcess setAutoResend(boolean autoResend) { + this.autoResend = autoResend; + return this; + } + + public PutResultProcess setFuture(CompletableFuture future) { + this.future = future; + return this; + } + + public String getTopic() { + return topic; + } + + public long getOffset() { + return offset; + } + + public long getNextOffset() { + return offset + 1; + } + + public long getPhysicOffset() { + return physicOffset; + } + + public int getPhysicSize() { + return physicSize; + } + + public Integer getDelayLevel() { + return delayLevel; + } + + public String getMsgId() { + return msgId; + } + + public boolean isAutoResend() { + return autoResend; + } + + public CompletableFuture getFuture() { + return future; + } + + public AtomicInteger getResendCount() { + return resendCount; + } + + public PutResultProcess thenProcess() { + this.future.thenAccept(this::handleResult); + + this.future.exceptionally(e -> { + log.error("ScheduleMessageService put message exceptionally, info: {}", + PutResultProcess.this.toString(), e); + + onException(); + return null; + }); + return this; + } + + private void handleResult(PutMessageResult result) { + if (result != null && result.getPutMessageStatus() == PutMessageStatus.PUT_OK) { + onSuccess(result); + } else { + log.warn("ScheduleMessageService put message failed. info: {}.", result); + onException(); + } + } + + public void onSuccess(PutMessageResult result) { + this.status = ProcessStatus.SUCCESS; + if (ScheduleMessageService.this.brokerController.getMessageStore().getMessageStoreConfig().isEnableScheduleMessageStats() && !result.isRemotePut()) { + ScheduleMessageService.this.brokerController.getBrokerStatsManager().incQueueGetNums(MixAll.SCHEDULE_CONSUMER_GROUP, TopicValidator.RMQ_SYS_SCHEDULE_TOPIC, delayLevel - 1, result.getAppendMessageResult().getMsgNum()); + ScheduleMessageService.this.brokerController.getBrokerStatsManager().incQueueGetSize(MixAll.SCHEDULE_CONSUMER_GROUP, TopicValidator.RMQ_SYS_SCHEDULE_TOPIC, delayLevel - 1, result.getAppendMessageResult().getWroteBytes()); + ScheduleMessageService.this.brokerController.getBrokerStatsManager().incGroupGetNums(MixAll.SCHEDULE_CONSUMER_GROUP, TopicValidator.RMQ_SYS_SCHEDULE_TOPIC, result.getAppendMessageResult().getMsgNum()); + ScheduleMessageService.this.brokerController.getBrokerStatsManager().incGroupGetSize(MixAll.SCHEDULE_CONSUMER_GROUP, TopicValidator.RMQ_SYS_SCHEDULE_TOPIC, result.getAppendMessageResult().getWroteBytes()); + + Attributes attributes = BrokerMetricsManager.newAttributesBuilder() + .put(LABEL_TOPIC, TopicValidator.RMQ_SYS_SCHEDULE_TOPIC) + .put(LABEL_CONSUMER_GROUP, MixAll.SCHEDULE_CONSUMER_GROUP) + .put(LABEL_IS_SYSTEM, true) + .build(); + BrokerMetricsManager.messagesOutTotal.add(result.getAppendMessageResult().getMsgNum(), attributes); + BrokerMetricsManager.throughputOutTotal.add(result.getAppendMessageResult().getWroteBytes(), attributes); + + ScheduleMessageService.this.brokerController.getBrokerStatsManager().incTopicPutNums(this.topic, result.getAppendMessageResult().getMsgNum(), 1); + ScheduleMessageService.this.brokerController.getBrokerStatsManager().incTopicPutSize(this.topic, result.getAppendMessageResult().getWroteBytes()); + ScheduleMessageService.this.brokerController.getBrokerStatsManager().incBrokerPutNums(this.topic, result.getAppendMessageResult().getMsgNum()); + + attributes = BrokerMetricsManager.newAttributesBuilder() + .put(LABEL_TOPIC, topic) + .put(LABEL_MESSAGE_TYPE, TopicMessageType.DELAY.getMetricsValue()) + .put(LABEL_IS_SYSTEM, TopicValidator.isSystemTopic(topic)) + .build(); + BrokerMetricsManager.messagesInTotal.add(result.getAppendMessageResult().getMsgNum(), attributes); + BrokerMetricsManager.throughputInTotal.add(result.getAppendMessageResult().getWroteBytes(), attributes); + BrokerMetricsManager.messageSize.record(result.getAppendMessageResult().getWroteBytes() / result.getAppendMessageResult().getMsgNum(), attributes); + } + } + + public void onException() { + log.warn("ScheduleMessageService onException, info: {}", this.toString()); + if (this.autoResend) { + this.status = ProcessStatus.EXCEPTION; + } else { + this.status = ProcessStatus.SKIP; + } + } + + public ProcessStatus getStatus() { + return this.status; + } + + public PutMessageResult get() { + try { + return this.future.get(); + } catch (InterruptedException | ExecutionException e) { + return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, null); + } + } + + public void doResend() { + log.info("Resend message, info: {}", this.toString()); + + // Gradually increase the resend interval. + try { + Thread.sleep(Math.min(this.resendCount.incrementAndGet() * 100, 60 * 1000)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + try { + MessageExt msgExt = ScheduleMessageService.this.brokerController.getMessageStore().lookMessageByOffset(this.physicOffset, this.physicSize); + if (msgExt == null) { + log.warn("ScheduleMessageService resend not found message. info: {}", this.toString()); + this.status = need2Skip() ? ProcessStatus.SKIP : ProcessStatus.EXCEPTION; + return; + } + + MessageExtBrokerInner msgInner = ScheduleMessageService.this.messageTimeUp(msgExt); + PutMessageResult result = ScheduleMessageService.this.brokerController.getEscapeBridge().putMessage(msgInner); + this.handleResult(result); + if (result != null && result.getPutMessageStatus() == PutMessageStatus.PUT_OK) { + log.info("Resend message success, info: {}", this.toString()); + } + } catch (Exception e) { + this.status = ProcessStatus.EXCEPTION; + log.error("Resend message error, info: {}", this.toString(), e); + } + } + + public boolean need2Blocked() { + int maxResendNum2Blocked = ScheduleMessageService.this.brokerController.getMessageStore().getMessageStoreConfig() + .getScheduleAsyncDeliverMaxResendNum2Blocked(); + return this.resendCount.get() > maxResendNum2Blocked; + } + + public boolean need2Skip() { + int maxResendNum2Blocked = ScheduleMessageService.this.brokerController.getMessageStore().getMessageStoreConfig() + .getScheduleAsyncDeliverMaxResendNum2Blocked(); + return this.resendCount.get() > maxResendNum2Blocked * 2; + } + + @Override + public String toString() { + return "PutResultProcess{" + + "topic='" + topic + '\'' + + ", offset=" + offset + + ", physicOffset=" + physicOffset + + ", physicSize=" + physicSize + + ", delayLevel=" + delayLevel + + ", msgId='" + msgId + '\'' + + ", autoResend=" + autoResend + + ", resendCount=" + resendCount + + ", status=" + status + + '}'; + } + } + + public enum ProcessStatus { + /** + * In process, the processing result has not yet been returned. + */ + RUNNING, + + /** + * Put message success. + */ + SUCCESS, + + /** + * Put message exception. When autoResend is true, the message will be resend. + */ + EXCEPTION, + + /** + * Skip put message. When the message cannot be looked, the message will be skipped. + */ + SKIP, + } + + public ConcurrentMap getOffsetTable() { + return offsetTable; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/slave/SlaveSynchronize.java b/broker/src/main/java/org/apache/rocketmq/broker/slave/SlaveSynchronize.java index 44c8264f30d..7f802adb938 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/slave/SlaveSynchronize.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/slave/SlaveSynchronize.java @@ -17,19 +17,29 @@ package org.apache.rocketmq.broker.slave; import java.io.IOException; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentMap; + +import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.loadbalance.MessageRequestModeManager; import org.apache.rocketmq.broker.subscription.SubscriptionGroupManager; import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.common.protocol.body.ConsumerOffsetSerializeWrapper; -import org.apache.rocketmq.common.protocol.body.SubscriptionGroupWrapper; -import org.apache.rocketmq.common.protocol.body.TopicConfigSerializeWrapper; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.body.ConsumerOffsetSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.body.MessageRequestModeSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.body.SubscriptionGroupWrapper; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigAndMappingSerializeWrapper; import org.apache.rocketmq.store.config.StorePathConfigHelper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.timer.TimerCheckpoint; +import org.apache.rocketmq.store.timer.TimerMetrics; public class SlaveSynchronize { - private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); private final BrokerController brokerController; private volatile String masterAddr = null; @@ -42,7 +52,10 @@ public String getMasterAddr() { } public void setMasterAddr(String masterAddr) { - this.masterAddr = masterAddr; + if (!StringUtils.equals(this.masterAddr, masterAddr)) { + LOGGER.info("Update master address from {} to {}", this.masterAddr, masterAddr); + this.masterAddr = masterAddr; + } } public void syncAll() { @@ -50,94 +63,189 @@ public void syncAll() { this.syncConsumerOffset(); this.syncDelayOffset(); this.syncSubscriptionGroupConfig(); + this.syncMessageRequestMode(); + + if (brokerController.getMessageStoreConfig().isTimerWheelEnable()) { + this.syncTimerMetrics(); + } } private void syncTopicConfig() { String masterAddrBak = this.masterAddr; - if (masterAddrBak != null) { + if (masterAddrBak != null && !masterAddrBak.equals(brokerController.getBrokerAddr())) { try { - TopicConfigSerializeWrapper topicWrapper = - this.brokerController.getBrokerOuterAPI().getAllTopicConfig(masterAddrBak); + TopicConfigAndMappingSerializeWrapper topicWrapper = + this.brokerController.getBrokerOuterAPI().getAllTopicConfig(masterAddrBak); if (!this.brokerController.getTopicConfigManager().getDataVersion() - .equals(topicWrapper.getDataVersion())) { + .equals(topicWrapper.getDataVersion())) { this.brokerController.getTopicConfigManager().getDataVersion() - .assignNewOne(topicWrapper.getDataVersion()); - this.brokerController.getTopicConfigManager().getTopicConfigTable().clear(); - this.brokerController.getTopicConfigManager().getTopicConfigTable() - .putAll(topicWrapper.getTopicConfigTable()); + .assignNewOne(topicWrapper.getDataVersion()); + + ConcurrentMap newTopicConfigTable = topicWrapper.getTopicConfigTable(); + //delete + ConcurrentMap topicConfigTable = this.brokerController.getTopicConfigManager().getTopicConfigTable(); + for (Iterator> it = topicConfigTable.entrySet().iterator(); it.hasNext(); ) { + Map.Entry item = it.next(); + if (!newTopicConfigTable.containsKey(item.getKey())) { + it.remove(); + } + } + //update + topicConfigTable.putAll(newTopicConfigTable); + this.brokerController.getTopicConfigManager().persist(); + } + if (topicWrapper.getTopicQueueMappingDetailMap() != null + && !topicWrapper.getMappingDataVersion().equals(this.brokerController.getTopicQueueMappingManager().getDataVersion())) { + this.brokerController.getTopicQueueMappingManager().getDataVersion() + .assignNewOne(topicWrapper.getMappingDataVersion()); + + ConcurrentMap newTopicConfigTable = topicWrapper.getTopicConfigTable(); + //delete + ConcurrentMap topicConfigTable = this.brokerController.getTopicConfigManager().getTopicConfigTable(); + for (Iterator> it = topicConfigTable.entrySet().iterator(); it.hasNext(); ) { + Map.Entry item = it.next(); + if (!newTopicConfigTable.containsKey(item.getKey())) { + it.remove(); + } + } + //update + topicConfigTable.putAll(newTopicConfigTable); - log.info("Update slave topic config from master, {}", masterAddrBak); + this.brokerController.getTopicQueueMappingManager().persist(); } + LOGGER.info("Update slave topic config from master, {}", masterAddrBak); } catch (Exception e) { - log.error("SyncTopicConfig Exception, {}", masterAddrBak, e); + LOGGER.error("SyncTopicConfig Exception, {}", masterAddrBak, e); } } } private void syncConsumerOffset() { String masterAddrBak = this.masterAddr; - if (masterAddrBak != null) { + if (masterAddrBak != null && !masterAddrBak.equals(brokerController.getBrokerAddr())) { try { ConsumerOffsetSerializeWrapper offsetWrapper = - this.brokerController.getBrokerOuterAPI().getAllConsumerOffset(masterAddrBak); + this.brokerController.getBrokerOuterAPI().getAllConsumerOffset(masterAddrBak); this.brokerController.getConsumerOffsetManager().getOffsetTable() - .putAll(offsetWrapper.getOffsetTable()); + .putAll(offsetWrapper.getOffsetTable()); + this.brokerController.getConsumerOffsetManager().getDataVersion().assignNewOne(offsetWrapper.getDataVersion()); this.brokerController.getConsumerOffsetManager().persist(); - log.info("Update slave consumer offset from master, {}", masterAddrBak); + LOGGER.info("Update slave consumer offset from master, {}", masterAddrBak); } catch (Exception e) { - log.error("SyncConsumerOffset Exception, {}", masterAddrBak, e); + LOGGER.error("SyncConsumerOffset Exception, {}", masterAddrBak, e); } } } private void syncDelayOffset() { String masterAddrBak = this.masterAddr; - if (masterAddrBak != null) { + if (masterAddrBak != null && !masterAddrBak.equals(brokerController.getBrokerAddr())) { try { String delayOffset = - this.brokerController.getBrokerOuterAPI().getAllDelayOffset(masterAddrBak); + this.brokerController.getBrokerOuterAPI().getAllDelayOffset(masterAddrBak); if (delayOffset != null) { String fileName = - StorePathConfigHelper.getDelayOffsetStorePath(this.brokerController - .getMessageStoreConfig().getStorePathRootDir()); + StorePathConfigHelper.getDelayOffsetStorePath(this.brokerController + .getMessageStoreConfig().getStorePathRootDir()); try { MixAll.string2File(delayOffset, fileName); + this.brokerController.getScheduleMessageService().loadWhenSyncDelayOffset(); } catch (IOException e) { - log.error("Persist file Exception, {}", fileName, e); + LOGGER.error("Persist file Exception, {}", fileName, e); } } - log.info("Update slave delay offset from master, {}", masterAddrBak); + LOGGER.info("Update slave delay offset from master, {}", masterAddrBak); } catch (Exception e) { - log.error("SyncDelayOffset Exception, {}", masterAddrBak, e); + LOGGER.error("SyncDelayOffset Exception, {}", masterAddrBak, e); } } } private void syncSubscriptionGroupConfig() { String masterAddrBak = this.masterAddr; - if (masterAddrBak != null) { + if (masterAddrBak != null && !masterAddrBak.equals(brokerController.getBrokerAddr())) { try { SubscriptionGroupWrapper subscriptionWrapper = - this.brokerController.getBrokerOuterAPI() - .getAllSubscriptionGroupConfig(masterAddrBak); + this.brokerController.getBrokerOuterAPI() + .getAllSubscriptionGroupConfig(masterAddrBak); if (!this.brokerController.getSubscriptionGroupManager().getDataVersion() - .equals(subscriptionWrapper.getDataVersion())) { + .equals(subscriptionWrapper.getDataVersion())) { SubscriptionGroupManager subscriptionGroupManager = - this.brokerController.getSubscriptionGroupManager(); + this.brokerController.getSubscriptionGroupManager(); subscriptionGroupManager.getDataVersion().assignNewOne( - subscriptionWrapper.getDataVersion()); + subscriptionWrapper.getDataVersion()); subscriptionGroupManager.getSubscriptionGroupTable().clear(); subscriptionGroupManager.getSubscriptionGroupTable().putAll( - subscriptionWrapper.getSubscriptionGroupTable()); + subscriptionWrapper.getSubscriptionGroupTable()); subscriptionGroupManager.persist(); - log.info("Update slave Subscription Group from master, {}", masterAddrBak); + LOGGER.info("Update slave Subscription Group from master, {}", masterAddrBak); + } + } catch (Exception e) { + LOGGER.error("SyncSubscriptionGroup Exception, {}", masterAddrBak, e); + } + } + } + + private void syncMessageRequestMode() { + String masterAddrBak = this.masterAddr; + if (masterAddrBak != null && !masterAddrBak.equals(brokerController.getBrokerAddr())) { + try { + MessageRequestModeSerializeWrapper messageRequestModeSerializeWrapper = + this.brokerController.getBrokerOuterAPI().getAllMessageRequestMode(masterAddrBak); + + MessageRequestModeManager messageRequestModeManager = + this.brokerController.getQueryAssignmentProcessor().getMessageRequestModeManager(); + messageRequestModeManager.getMessageRequestModeMap().clear(); + messageRequestModeManager.getMessageRequestModeMap().putAll( + messageRequestModeSerializeWrapper.getMessageRequestModeMap() + ); + messageRequestModeManager.persist(); + LOGGER.info("Update slave Message Request Mode from master, {}", masterAddrBak); + } catch (Exception e) { + LOGGER.error("SyncMessageRequestMode Exception, {}", masterAddrBak, e); + } + } + } + + public void syncTimerCheckPoint() { + String masterAddrBak = this.masterAddr; + if (masterAddrBak != null) { + try { + if (null != brokerController.getMessageStore().getTimerMessageStore() && + !brokerController.getTimerMessageStore().isShouldRunningDequeue()) { + TimerCheckpoint checkpoint = this.brokerController.getBrokerOuterAPI().getTimerCheckPoint(masterAddrBak); + if (null != this.brokerController.getTimerCheckpoint()) { + this.brokerController.getTimerCheckpoint().setLastReadTimeMs(checkpoint.getLastReadTimeMs()); + this.brokerController.getTimerCheckpoint().setMasterTimerQueueOffset(checkpoint.getMasterTimerQueueOffset()); + this.brokerController.getTimerCheckpoint().getDataVersion().assignNewOne(checkpoint.getDataVersion()); + } + } + } catch (Exception e) { + LOGGER.error("syncTimerCheckPoint Exception, {}", masterAddrBak, e); + } + } + } + + private void syncTimerMetrics() { + String masterAddrBak = this.masterAddr; + if (masterAddrBak != null) { + try { + if (null != brokerController.getMessageStore().getTimerMessageStore()) { + TimerMetrics.TimerMetricsSerializeWrapper metricsSerializeWrapper = + this.brokerController.getBrokerOuterAPI().getTimerMetrics(masterAddrBak); + if (!brokerController.getMessageStore().getTimerMessageStore().getTimerMetrics().getDataVersion().equals(metricsSerializeWrapper.getDataVersion())) { + this.brokerController.getMessageStore().getTimerMessageStore().getTimerMetrics().getDataVersion().assignNewOne(metricsSerializeWrapper.getDataVersion()); + this.brokerController.getMessageStore().getTimerMessageStore().getTimerMetrics().getTimingCount().clear(); + this.brokerController.getMessageStore().getTimerMessageStore().getTimerMetrics().getTimingCount().putAll(metricsSerializeWrapper.getTimingCount()); + this.brokerController.getMessageStore().getTimerMessageStore().getTimerMetrics().persist(); + } } } catch (Exception e) { - log.error("SyncSubscriptionGroup Exception, {}", masterAddrBak, e); + LOGGER.error("SyncTimerMetrics Exception, {}", masterAddrBak, e); } } } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/subscription/LmqSubscriptionGroupManager.java b/broker/src/main/java/org/apache/rocketmq/broker/subscription/LmqSubscriptionGroupManager.java new file mode 100644 index 00000000000..018083811e8 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/subscription/LmqSubscriptionGroupManager.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.subscription; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; + +public class LmqSubscriptionGroupManager extends SubscriptionGroupManager { + + public LmqSubscriptionGroupManager(BrokerController brokerController) { + super(brokerController); + } + + @Override + public SubscriptionGroupConfig findSubscriptionGroupConfig(final String group) { + if (MixAll.isLmq(group)) { + SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); + subscriptionGroupConfig.setGroupName(group); + return subscriptionGroupConfig; + } + return super.findSubscriptionGroupConfig(group); + } + + @Override + public void updateSubscriptionGroupConfig(final SubscriptionGroupConfig config) { + if (config == null || MixAll.isLmq(config.getGroupName())) { + return; + } + super.updateSubscriptionGroupConfig(config); + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/subscription/RocksDBLmqSubscriptionGroupManager.java b/broker/src/main/java/org/apache/rocketmq/broker/subscription/RocksDBLmqSubscriptionGroupManager.java new file mode 100644 index 00000000000..8c05d0bd98f --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/subscription/RocksDBLmqSubscriptionGroupManager.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.subscription; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; + +public class RocksDBLmqSubscriptionGroupManager extends RocksDBSubscriptionGroupManager { + + public RocksDBLmqSubscriptionGroupManager(BrokerController brokerController) { + super(brokerController); + } + + @Override + public SubscriptionGroupConfig findSubscriptionGroupConfig(final String group) { + if (MixAll.isLmq(group)) { + SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); + subscriptionGroupConfig.setGroupName(group); + return subscriptionGroupConfig; + } + return super.findSubscriptionGroupConfig(group); + } + + @Override + public void updateSubscriptionGroupConfig(final SubscriptionGroupConfig config) { + if (config == null || MixAll.isLmq(config.getGroupName())) { + return; + } + super.updateSubscriptionGroupConfig(config); + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/subscription/RocksDBSubscriptionGroupManager.java b/broker/src/main/java/org/apache/rocketmq/broker/subscription/RocksDBSubscriptionGroupManager.java new file mode 100644 index 00000000000..e9a81a8d686 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/subscription/RocksDBSubscriptionGroupManager.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.subscription; + +import java.io.File; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.config.RocksDBConfigManager; +import org.apache.rocketmq.common.utils.DataConverter; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.serializer.SerializerFeature; + +public class RocksDBSubscriptionGroupManager extends SubscriptionGroupManager { + + public RocksDBSubscriptionGroupManager(BrokerController brokerController) { + super(brokerController, false); + this.rocksDBConfigManager = new RocksDBConfigManager(brokerController.getMessageStoreConfig().getMemTableFlushIntervalMs()); + } + + @Override + public boolean load() { + if (!this.rocksDBConfigManager.load(configFilePath(), this::decode0)) { + return false; + } + this.init(); + return true; + } + + @Override + public boolean stop() { + return this.rocksDBConfigManager.stop(); + } + + @Override + protected SubscriptionGroupConfig putSubscriptionGroupConfig(SubscriptionGroupConfig subscriptionGroupConfig) { + String groupName = subscriptionGroupConfig.getGroupName(); + SubscriptionGroupConfig oldConfig = this.subscriptionGroupTable.put(groupName, subscriptionGroupConfig); + + try { + byte[] keyBytes = groupName.getBytes(DataConverter.CHARSET_UTF8); + byte[] valueBytes = JSON.toJSONBytes(subscriptionGroupConfig, SerializerFeature.BrowserCompatible); + this.rocksDBConfigManager.put(keyBytes, keyBytes.length, valueBytes); + } catch (Exception e) { + log.error("kv put sub Failed, {}", subscriptionGroupConfig.toString()); + } + return oldConfig; + } + + @Override + protected SubscriptionGroupConfig putSubscriptionGroupConfigIfAbsent(SubscriptionGroupConfig subscriptionGroupConfig) { + String groupName = subscriptionGroupConfig.getGroupName(); + SubscriptionGroupConfig oldConfig = this.subscriptionGroupTable.putIfAbsent(groupName, subscriptionGroupConfig); + if (oldConfig == null) { + try { + byte[] keyBytes = groupName.getBytes(DataConverter.CHARSET_UTF8); + byte[] valueBytes = JSON.toJSONBytes(subscriptionGroupConfig, SerializerFeature.BrowserCompatible); + this.rocksDBConfigManager.put(keyBytes, keyBytes.length, valueBytes); + } catch (Exception e) { + log.error("kv put sub Failed, {}", subscriptionGroupConfig.toString()); + } + } + return oldConfig; + } + + @Override + protected SubscriptionGroupConfig removeSubscriptionGroupConfig(String groupName) { + SubscriptionGroupConfig subscriptionGroupConfig = this.subscriptionGroupTable.remove(groupName); + try { + this.rocksDBConfigManager.delete(groupName.getBytes(DataConverter.CHARSET_UTF8)); + } catch (Exception e) { + log.error("kv delete sub Failed, {}", subscriptionGroupConfig.toString()); + } + return subscriptionGroupConfig; + } + + @Override + protected void decode0(byte[] key, byte[] body) { + String groupName = new String(key, DataConverter.CHARSET_UTF8); + SubscriptionGroupConfig subscriptionGroupConfig = JSON.parseObject(body, SubscriptionGroupConfig.class); + + this.subscriptionGroupTable.put(groupName, subscriptionGroupConfig); + log.info("load exist local sub, {}", subscriptionGroupConfig.toString()); + } + + @Override + public synchronized void persist() { + if (brokerController.getMessageStoreConfig().isRealTimePersistRocksDBConfig()) { + this.rocksDBConfigManager.flushWAL(); + } + } + + @Override + public String configFilePath() { + return this.brokerController.getMessageStoreConfig().getStorePathRootDir() + File.separator + "config" + File.separator + "subscriptionGroups" + File.separator; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/subscription/SubscriptionGroupManager.java b/broker/src/main/java/org/apache/rocketmq/broker/subscription/SubscriptionGroupManager.java index 0cbb76172ca..e63b9305868 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/subscription/SubscriptionGroupManager.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/subscription/SubscriptionGroupManager.java @@ -16,118 +16,253 @@ */ package org.apache.rocketmq.broker.subscription; +import com.google.common.collect.ImmutableMap; +import java.util.HashMap; import java.util.Iterator; +import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.broker.BrokerController; import org.apache.rocketmq.broker.BrokerPathConfigHelper; +import org.apache.rocketmq.client.Validators; import org.apache.rocketmq.common.ConfigManager; -import org.apache.rocketmq.common.DataVersion; import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.SubscriptionGroupAttributes; +import org.apache.rocketmq.common.attribute.AttributeUtil; import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.common.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.DataVersion; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; public class SubscriptionGroupManager extends ConfigManager { - private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + protected static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + + protected ConcurrentMap subscriptionGroupTable = + new ConcurrentHashMap<>(1024); + + private ConcurrentMap> forbiddenTable = + new ConcurrentHashMap<>(4); - private final ConcurrentMap subscriptionGroupTable = - new ConcurrentHashMap(1024); private final DataVersion dataVersion = new DataVersion(); - private transient BrokerController brokerController; + protected transient BrokerController brokerController; public SubscriptionGroupManager() { this.init(); } public SubscriptionGroupManager(BrokerController brokerController) { + this(brokerController, true); + } + + public SubscriptionGroupManager(BrokerController brokerController, boolean init) { this.brokerController = brokerController; - this.init(); + if (init) { + init(); + } } - private void init() { + protected void init() { { SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); subscriptionGroupConfig.setGroupName(MixAll.TOOLS_CONSUMER_GROUP); - this.subscriptionGroupTable.put(MixAll.TOOLS_CONSUMER_GROUP, subscriptionGroupConfig); + putSubscriptionGroupConfig(subscriptionGroupConfig); } { SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); subscriptionGroupConfig.setGroupName(MixAll.FILTERSRV_CONSUMER_GROUP); - this.subscriptionGroupTable.put(MixAll.FILTERSRV_CONSUMER_GROUP, subscriptionGroupConfig); + putSubscriptionGroupConfig(subscriptionGroupConfig); } { SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); subscriptionGroupConfig.setGroupName(MixAll.SELF_TEST_CONSUMER_GROUP); - this.subscriptionGroupTable.put(MixAll.SELF_TEST_CONSUMER_GROUP, subscriptionGroupConfig); + putSubscriptionGroupConfig(subscriptionGroupConfig); } { SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); subscriptionGroupConfig.setGroupName(MixAll.ONS_HTTP_PROXY_GROUP); subscriptionGroupConfig.setConsumeBroadcastEnable(true); - this.subscriptionGroupTable.put(MixAll.ONS_HTTP_PROXY_GROUP, subscriptionGroupConfig); + putSubscriptionGroupConfig(subscriptionGroupConfig); } { SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); subscriptionGroupConfig.setGroupName(MixAll.CID_ONSAPI_PULL_GROUP); subscriptionGroupConfig.setConsumeBroadcastEnable(true); - this.subscriptionGroupTable.put(MixAll.CID_ONSAPI_PULL_GROUP, subscriptionGroupConfig); + putSubscriptionGroupConfig(subscriptionGroupConfig); } { SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); subscriptionGroupConfig.setGroupName(MixAll.CID_ONSAPI_PERMISSION_GROUP); subscriptionGroupConfig.setConsumeBroadcastEnable(true); - this.subscriptionGroupTable.put(MixAll.CID_ONSAPI_PERMISSION_GROUP, subscriptionGroupConfig); + putSubscriptionGroupConfig(subscriptionGroupConfig); } { SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); subscriptionGroupConfig.setGroupName(MixAll.CID_ONSAPI_OWNER_GROUP); subscriptionGroupConfig.setConsumeBroadcastEnable(true); - this.subscriptionGroupTable.put(MixAll.CID_ONSAPI_OWNER_GROUP, subscriptionGroupConfig); + putSubscriptionGroupConfig(subscriptionGroupConfig); } + + { + SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); + subscriptionGroupConfig.setGroupName(MixAll.CID_SYS_RMQ_TRANS); + subscriptionGroupConfig.setConsumeBroadcastEnable(true); + putSubscriptionGroupConfig(subscriptionGroupConfig); + } + } + + protected SubscriptionGroupConfig putSubscriptionGroupConfig(SubscriptionGroupConfig subscriptionGroupConfig) { + return this.subscriptionGroupTable.put(subscriptionGroupConfig.getGroupName(), subscriptionGroupConfig); + } + + protected SubscriptionGroupConfig putSubscriptionGroupConfigIfAbsent(SubscriptionGroupConfig subscriptionGroupConfig) { + return this.subscriptionGroupTable.putIfAbsent(subscriptionGroupConfig.getGroupName(), subscriptionGroupConfig); + } + + protected SubscriptionGroupConfig getSubscriptionGroupConfig(String groupName) { + return this.subscriptionGroupTable.get(groupName); + } + + protected SubscriptionGroupConfig removeSubscriptionGroupConfig(String groupName) { + return this.subscriptionGroupTable.remove(groupName); } public void updateSubscriptionGroupConfig(final SubscriptionGroupConfig config) { - SubscriptionGroupConfig old = this.subscriptionGroupTable.put(config.getGroupName(), config); + Map newAttributes = request(config); + Map currentAttributes = current(config.getGroupName()); + + Map finalAttributes = AttributeUtil.alterCurrentAttributes( + this.subscriptionGroupTable.get(config.getGroupName()) == null, + SubscriptionGroupAttributes.ALL, + ImmutableMap.copyOf(currentAttributes), + ImmutableMap.copyOf(newAttributes)); + + config.setAttributes(finalAttributes); + + SubscriptionGroupConfig old = putSubscriptionGroupConfig(config); if (old != null) { log.info("update subscription group config, old: {} new: {}", old, config); } else { log.info("create new subscription group, {}", config); } - this.dataVersion.nextVersion(); + long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0; + dataVersion.nextVersion(stateMachineVersion); + + this.persist(); + } + + public void updateForbidden(String group, String topic, int forbiddenIndex, boolean setOrClear) { + if (setOrClear) { + setForbidden(group, topic, forbiddenIndex); + } else { + clearForbidden(group, topic, forbiddenIndex); + } + } + + /** + * set the bit value to 1 at the specific index (from 0) + * + * @param group + * @param topic + * @param forbiddenIndex from 0 + */ + public void setForbidden(String group, String topic, int forbiddenIndex) { + int topicForbidden = getForbidden(group, topic); + topicForbidden |= 1 << forbiddenIndex; + updateForbiddenValue(group, topic, topicForbidden); + } + + /** + * clear the bit value to 0 at the specific index (from 0) + * + * @param group + * @param topic + * @param forbiddenIndex from 0 + */ + public void clearForbidden(String group, String topic, int forbiddenIndex) { + int topicForbidden = getForbidden(group, topic); + topicForbidden &= ~(1 << forbiddenIndex); + updateForbiddenValue(group, topic, topicForbidden); + } + + public boolean getForbidden(String group, String topic, int forbiddenIndex) { + int topicForbidden = getForbidden(group, topic); + int bitForbidden = 1 << forbiddenIndex; + return (topicForbidden & bitForbidden) == bitForbidden; + } + + public int getForbidden(String group, String topic) { + ConcurrentMap topicForbiddens = this.forbiddenTable.get(group); + if (topicForbiddens == null) { + return 0; + } + Integer topicForbidden = topicForbiddens.get(topic); + if (topicForbidden == null || topicForbidden < 0) { + topicForbidden = 0; + } + return topicForbidden; + } + + private void updateForbiddenValue(String group, String topic, Integer forbidden) { + if (forbidden == null || forbidden <= 0) { + this.forbiddenTable.remove(group); + log.info("clear group forbidden, {}@{} ", group, topic); + return; + } + + ConcurrentMap topicsPermMap = this.forbiddenTable.get(group); + if (topicsPermMap == null) { + this.forbiddenTable.putIfAbsent(group, new ConcurrentHashMap<>()); + topicsPermMap = this.forbiddenTable.get(group); + } + Integer old = topicsPermMap.put(topic, forbidden); + if (old != null) { + log.info("set group forbidden, {}@{} old: {} new: {}", group, topic, old, forbidden); + } else { + log.info("set group forbidden, {}@{} old: {} new: {}", group, topic, 0, forbidden); + } + + long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0; + dataVersion.nextVersion(stateMachineVersion); this.persist(); } public void disableConsume(final String groupName) { - SubscriptionGroupConfig old = this.subscriptionGroupTable.get(groupName); + SubscriptionGroupConfig old = getSubscriptionGroupConfig(groupName); if (old != null) { old.setConsumeEnable(false); - this.dataVersion.nextVersion(); + long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0; + dataVersion.nextVersion(stateMachineVersion); } } public SubscriptionGroupConfig findSubscriptionGroupConfig(final String group) { - SubscriptionGroupConfig subscriptionGroupConfig = this.subscriptionGroupTable.get(group); + SubscriptionGroupConfig subscriptionGroupConfig = getSubscriptionGroupConfig(group); if (null == subscriptionGroupConfig) { if (brokerController.getBrokerConfig().isAutoCreateSubscriptionGroup() || MixAll.isSysConsumerGroup(group)) { + if (group.length() > Validators.CHARACTER_MAX_LENGTH || TopicValidator.isTopicOrGroupIllegal(group)) { + return null; + } subscriptionGroupConfig = new SubscriptionGroupConfig(); subscriptionGroupConfig.setGroupName(group); - SubscriptionGroupConfig preConfig = this.subscriptionGroupTable.putIfAbsent(group, subscriptionGroupConfig); + SubscriptionGroupConfig preConfig = putSubscriptionGroupConfigIfAbsent(subscriptionGroupConfig); if (null == preConfig) { log.info("auto create a subscription group, {}", subscriptionGroupConfig.toString()); } - this.dataVersion.nextVersion(); + long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0; + dataVersion.nextVersion(stateMachineVersion); this.persist(); } } @@ -152,12 +287,16 @@ public void decode(String jsonString) { SubscriptionGroupManager obj = RemotingSerializable.fromJson(jsonString, SubscriptionGroupManager.class); if (obj != null) { this.subscriptionGroupTable.putAll(obj.subscriptionGroupTable); + if (obj.forbiddenTable != null) { + this.forbiddenTable.putAll(obj.forbiddenTable); + } this.dataVersion.assignNewOne(obj.dataVersion); this.printLoadDataWhenFirstBoot(obj); } } } + @Override public String encode(final boolean prettyFormat) { return RemotingSerializable.toJson(this, prettyFormat); } @@ -174,18 +313,60 @@ public ConcurrentMap getSubscriptionGroupTable( return subscriptionGroupTable; } + public ConcurrentMap> getForbiddenTable() { + return forbiddenTable; + } + + public void setForbiddenTable( + ConcurrentMap> forbiddenTable) { + this.forbiddenTable = forbiddenTable; + } + public DataVersion getDataVersion() { return dataVersion; } public void deleteSubscriptionGroupConfig(final String groupName) { - SubscriptionGroupConfig old = this.subscriptionGroupTable.remove(groupName); + SubscriptionGroupConfig old = removeSubscriptionGroupConfig(groupName); + this.forbiddenTable.remove(groupName); if (old != null) { log.info("delete subscription group OK, subscription group:{}", old); - this.dataVersion.nextVersion(); + long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0; + dataVersion.nextVersion(stateMachineVersion); this.persist(); } else { log.warn("delete subscription group failed, subscription groupName: {} not exist", groupName); } } + + + public void setSubscriptionGroupTable(ConcurrentMap subscriptionGroupTable) { + this.subscriptionGroupTable = subscriptionGroupTable; + } + + public boolean containsSubscriptionGroup(String group) { + if (StringUtils.isBlank(group)) { + return false; + } + + return subscriptionGroupTable.containsKey(group); + } + + private Map request(SubscriptionGroupConfig subscriptionGroupConfig) { + return subscriptionGroupConfig.getAttributes() == null ? new HashMap<>() : subscriptionGroupConfig.getAttributes(); + } + + private Map current(String groupName) { + SubscriptionGroupConfig subscriptionGroupConfig = this.subscriptionGroupTable.get(groupName); + if (subscriptionGroupConfig == null) { + return new HashMap<>(); + } else { + Map attributes = subscriptionGroupConfig.getAttributes(); + if (attributes == null) { + return new HashMap<>(); + } else { + return attributes; + } + } + } } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/topic/LmqTopicConfigManager.java b/broker/src/main/java/org/apache/rocketmq/broker/topic/LmqTopicConfigManager.java new file mode 100644 index 00000000000..ca5a94a901b --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/topic/LmqTopicConfigManager.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.topic; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.constant.PermName; + +public class LmqTopicConfigManager extends TopicConfigManager { + public LmqTopicConfigManager(BrokerController brokerController) { + super(brokerController); + } + + @Override + public TopicConfig selectTopicConfig(final String topic) { + if (MixAll.isLmq(topic)) { + return simpleLmqTopicConfig(topic); + } + return super.selectTopicConfig(topic); + } + + @Override + public void updateTopicConfig(final TopicConfig topicConfig) { + if (topicConfig == null || MixAll.isLmq(topicConfig.getTopicName())) { + return; + } + super.updateTopicConfig(topicConfig); + } + + private TopicConfig simpleLmqTopicConfig(String topic) { + return new TopicConfig(topic, 1, 1, PermName.PERM_READ | PermName.PERM_WRITE); + } + + @Override + public boolean containsTopic(String topic) { + if (MixAll.isLmq(topic)) { + return true; + } + return super.containsTopic(topic); + } + +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/topic/RocksDBLmqTopicConfigManager.java b/broker/src/main/java/org/apache/rocketmq/broker/topic/RocksDBLmqTopicConfigManager.java new file mode 100644 index 00000000000..d049a8dbcde --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/topic/RocksDBLmqTopicConfigManager.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.topic; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.constant.PermName; + +public class RocksDBLmqTopicConfigManager extends RocksDBTopicConfigManager { + + public RocksDBLmqTopicConfigManager(BrokerController brokerController) { + super(brokerController); + } + + @Override + public TopicConfig selectTopicConfig(final String topic) { + if (MixAll.isLmq(topic)) { + return simpleLmqTopicConfig(topic); + } + return super.selectTopicConfig(topic); + } + + @Override + public void updateTopicConfig(final TopicConfig topicConfig) { + if (topicConfig == null || MixAll.isLmq(topicConfig.getTopicName())) { + return; + } + super.updateTopicConfig(topicConfig); + } + + @Override + public boolean containsTopic(String topic) { + if (MixAll.isLmq(topic)) { + return true; + } + return super.containsTopic(topic); + } + + private TopicConfig simpleLmqTopicConfig(String topic) { + return new TopicConfig(topic, 1, 1, PermName.PERM_READ | PermName.PERM_WRITE); + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/topic/RocksDBTopicConfigManager.java b/broker/src/main/java/org/apache/rocketmq/broker/topic/RocksDBTopicConfigManager.java new file mode 100644 index 00000000000..fddecf2d92a --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/topic/RocksDBTopicConfigManager.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.topic; + +import java.io.File; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.config.RocksDBConfigManager; +import org.apache.rocketmq.common.utils.DataConverter; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.serializer.SerializerFeature; + +public class RocksDBTopicConfigManager extends TopicConfigManager { + + public RocksDBTopicConfigManager(BrokerController brokerController) { + super(brokerController, false); + this.rocksDBConfigManager = new RocksDBConfigManager(brokerController.getMessageStoreConfig().getMemTableFlushIntervalMs()); + } + + @Override + public boolean load() { + if (!this.rocksDBConfigManager.load(configFilePath(), this::decode0)) { + return false; + } + this.init(); + return true; + } + + @Override + public boolean stop() { + return this.rocksDBConfigManager.stop(); + } + + @Override + protected void decode0(byte[] key, byte[] body) { + String topicName = new String(key, DataConverter.CHARSET_UTF8); + TopicConfig topicConfig = JSON.parseObject(body, TopicConfig.class); + + this.topicConfigTable.put(topicName, topicConfig); + log.info("load exist local topic, {}", topicConfig.toString()); + } + + @Override + public String configFilePath() { + return this.brokerController.getMessageStoreConfig().getStorePathRootDir() + File.separator + "config" + File.separator + "topics" + File.separator; + } + + @Override + protected TopicConfig putTopicConfig(TopicConfig topicConfig) { + String topicName = topicConfig.getTopicName(); + TopicConfig oldTopicConfig = this.topicConfigTable.put(topicName, topicConfig); + try { + byte[] keyBytes = topicName.getBytes(DataConverter.CHARSET_UTF8); + byte[] valueBytes = JSON.toJSONBytes(topicConfig, SerializerFeature.BrowserCompatible); + this.rocksDBConfigManager.put(keyBytes, keyBytes.length, valueBytes); + } catch (Exception e) { + log.error("kv put topic Failed, {}", topicConfig.toString(), e); + } + return oldTopicConfig; + } + + @Override + protected TopicConfig removeTopicConfig(String topicName) { + TopicConfig topicConfig = this.topicConfigTable.remove(topicName); + try { + this.rocksDBConfigManager.delete(topicName.getBytes(DataConverter.CHARSET_UTF8)); + } catch (Exception e) { + log.error("kv remove topic Failed, {}", topicConfig.toString()); + } + return topicConfig; + } + + @Override + public synchronized void persist() { + if (brokerController.getMessageStoreConfig().isRealTimePersistRocksDBConfig()) { + this.rocksDBConfigManager.flushWAL(); + } + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/topic/TopicConfigManager.java b/broker/src/main/java/org/apache/rocketmq/broker/topic/TopicConfigManager.java index cd30a089b7a..511d29e12ad 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/topic/TopicConfigManager.java +++ b/broker/src/main/java/org/apache/rocketmq/broker/topic/TopicConfigManager.java @@ -16,7 +16,7 @@ */ package org.apache.rocketmq.broker.topic; -import java.util.HashSet; +import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; @@ -26,86 +26,106 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; + +import com.google.common.collect.ImmutableMap; + +import com.google.common.collect.Maps; +import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.broker.BrokerController; import org.apache.rocketmq.broker.BrokerPathConfigHelper; import org.apache.rocketmq.common.ConfigManager; -import org.apache.rocketmq.common.DataVersion; import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.PopAckConstants; +import org.apache.rocketmq.common.TopicAttributes; import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.attribute.Attribute; +import org.apache.rocketmq.common.attribute.AttributeUtil; import org.apache.rocketmq.common.constant.LoggerName; import org.apache.rocketmq.common.constant.PermName; -import org.apache.rocketmq.common.protocol.body.KVTable; -import org.apache.rocketmq.common.protocol.body.TopicConfigSerializeWrapper; import org.apache.rocketmq.common.sysflag.TopicSysFlag; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.remoting.protocol.body.KVTable; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigAndMappingSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingInfo; + +import static com.google.common.base.Preconditions.checkNotNull; public class TopicConfigManager extends ConfigManager { - private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + protected static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); private static final long LOCK_TIMEOUT_MILLIS = 3000; - private transient final Lock lockTopicConfigTable = new ReentrantLock(); + private static final int SCHEDULE_TOPIC_QUEUE_NUM = 18; - private final ConcurrentMap topicConfigTable = - new ConcurrentHashMap(1024); - private final DataVersion dataVersion = new DataVersion(); - private final Set systemTopicList = new HashSet(); - private transient BrokerController brokerController; + private transient final Lock topicConfigTableLock = new ReentrantLock(); + protected ConcurrentMap topicConfigTable = new ConcurrentHashMap<>(1024); + private DataVersion dataVersion = new DataVersion(); + protected transient BrokerController brokerController; public TopicConfigManager() { + } public TopicConfigManager(BrokerController brokerController) { + this(brokerController, true); + } + + public TopicConfigManager(BrokerController brokerController, boolean init) { this.brokerController = brokerController; + if (init) { + init(); + } + } + + protected void init() { { - // MixAll.SELF_TEST_TOPIC - String topic = MixAll.SELF_TEST_TOPIC; + String topic = TopicValidator.RMQ_SYS_SELF_TEST_TOPIC; TopicConfig topicConfig = new TopicConfig(topic); - this.systemTopicList.add(topic); + TopicValidator.addSystemTopic(topic); topicConfig.setReadQueueNums(1); topicConfig.setWriteQueueNums(1); - this.topicConfigTable.put(topicConfig.getTopicName(), topicConfig); + putTopicConfig(topicConfig); } { - // MixAll.DEFAULT_TOPIC if (this.brokerController.getBrokerConfig().isAutoCreateTopicEnable()) { - String topic = MixAll.DEFAULT_TOPIC; + String topic = TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC; TopicConfig topicConfig = new TopicConfig(topic); - this.systemTopicList.add(topic); + TopicValidator.addSystemTopic(topic); topicConfig.setReadQueueNums(this.brokerController.getBrokerConfig() .getDefaultTopicQueueNums()); topicConfig.setWriteQueueNums(this.brokerController.getBrokerConfig() .getDefaultTopicQueueNums()); int perm = PermName.PERM_INHERIT | PermName.PERM_READ | PermName.PERM_WRITE; topicConfig.setPerm(perm); - this.topicConfigTable.put(topicConfig.getTopicName(), topicConfig); + putTopicConfig(topicConfig); } } { - // MixAll.BENCHMARK_TOPIC - String topic = MixAll.BENCHMARK_TOPIC; + String topic = TopicValidator.RMQ_SYS_BENCHMARK_TOPIC; TopicConfig topicConfig = new TopicConfig(topic); - this.systemTopicList.add(topic); + TopicValidator.addSystemTopic(topic); topicConfig.setReadQueueNums(1024); topicConfig.setWriteQueueNums(1024); - this.topicConfigTable.put(topicConfig.getTopicName(), topicConfig); + putTopicConfig(topicConfig); } { - String topic = this.brokerController.getBrokerConfig().getBrokerClusterName(); TopicConfig topicConfig = new TopicConfig(topic); - this.systemTopicList.add(topic); + TopicValidator.addSystemTopic(topic); int perm = PermName.PERM_INHERIT; if (this.brokerController.getBrokerConfig().isClusterTopicEnable()) { perm |= PermName.PERM_READ | PermName.PERM_WRITE; } topicConfig.setPerm(perm); - this.topicConfigTable.put(topicConfig.getTopicName(), topicConfig); + putTopicConfig(topicConfig); } { String topic = this.brokerController.getBrokerConfig().getBrokerName(); TopicConfig topicConfig = new TopicConfig(topic); - this.systemTopicList.add(topic); + TopicValidator.addSystemTopic(topic); int perm = PermName.PERM_INHERIT; if (this.brokerController.getBrokerConfig().isBrokerTopicEnable()) { perm |= PermName.PERM_READ | PermName.PERM_WRITE; @@ -113,33 +133,96 @@ public TopicConfigManager(BrokerController brokerController) { topicConfig.setReadQueueNums(1); topicConfig.setWriteQueueNums(1); topicConfig.setPerm(perm); - this.topicConfigTable.put(topicConfig.getTopicName(), topicConfig); + putTopicConfig(topicConfig); + } + { + String topic = TopicValidator.RMQ_SYS_OFFSET_MOVED_EVENT; + TopicConfig topicConfig = new TopicConfig(topic); + TopicValidator.addSystemTopic(topic); + topicConfig.setReadQueueNums(1); + topicConfig.setWriteQueueNums(1); + putTopicConfig(topicConfig); + } + { + String topic = TopicValidator.RMQ_SYS_SCHEDULE_TOPIC; + TopicConfig topicConfig = new TopicConfig(topic); + TopicValidator.addSystemTopic(topic); + topicConfig.setReadQueueNums(SCHEDULE_TOPIC_QUEUE_NUM); + topicConfig.setWriteQueueNums(SCHEDULE_TOPIC_QUEUE_NUM); + putTopicConfig(topicConfig); } { - // MixAll.OFFSET_MOVED_EVENT - String topic = MixAll.OFFSET_MOVED_EVENT; + if (this.brokerController.getBrokerConfig().isTraceTopicEnable()) { + String topic = this.brokerController.getBrokerConfig().getMsgTraceTopicName(); + TopicConfig topicConfig = new TopicConfig(topic); + TopicValidator.addSystemTopic(topic); + topicConfig.setReadQueueNums(1); + topicConfig.setWriteQueueNums(1); + putTopicConfig(topicConfig); + } + } + { + String topic = this.brokerController.getBrokerConfig().getBrokerClusterName() + "_" + MixAll.REPLY_TOPIC_POSTFIX; + TopicConfig topicConfig = new TopicConfig(topic); + TopicValidator.addSystemTopic(topic); + topicConfig.setReadQueueNums(1); + topicConfig.setWriteQueueNums(1); + putTopicConfig(topicConfig); + } + { + // PopAckConstants.REVIVE_TOPIC + String topic = PopAckConstants.buildClusterReviveTopic(this.brokerController.getBrokerConfig().getBrokerClusterName()); TopicConfig topicConfig = new TopicConfig(topic); - this.systemTopicList.add(topic); + TopicValidator.addSystemTopic(topic); + topicConfig.setReadQueueNums(this.brokerController.getBrokerConfig().getReviveQueueNum()); + topicConfig.setWriteQueueNums(this.brokerController.getBrokerConfig().getReviveQueueNum()); + putTopicConfig(topicConfig); + } + { + // sync broker member group topic + String topic = TopicValidator.SYNC_BROKER_MEMBER_GROUP_PREFIX + this.brokerController.getBrokerConfig().getBrokerName(); + TopicConfig topicConfig = new TopicConfig(topic); + TopicValidator.addSystemTopic(topic); topicConfig.setReadQueueNums(1); topicConfig.setWriteQueueNums(1); - this.topicConfigTable.put(topicConfig.getTopicName(), topicConfig); + topicConfig.setPerm(PermName.PERM_INHERIT); + putTopicConfig(topicConfig); + } + { + // TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC + String topic = TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC; + TopicConfig topicConfig = new TopicConfig(topic); + TopicValidator.addSystemTopic(topic); + topicConfig.setReadQueueNums(1); + topicConfig.setWriteQueueNums(1); + putTopicConfig(topicConfig); + } + + { + // TopicValidator.RMQ_SYS_TRANS_OP_HALF_TOPIC + String topic = TopicValidator.RMQ_SYS_TRANS_OP_HALF_TOPIC; + TopicConfig topicConfig = new TopicConfig(topic); + TopicValidator.addSystemTopic(topic); + topicConfig.setReadQueueNums(1); + topicConfig.setWriteQueueNums(1); + putTopicConfig(topicConfig); } } - public boolean isSystemTopic(final String topic) { - return this.systemTopicList.contains(topic); + protected TopicConfig putTopicConfig(TopicConfig topicConfig) { + return this.topicConfigTable.put(topicConfig.getTopicName(), topicConfig); } - public Set getSystemTopic() { - return this.systemTopicList; + protected TopicConfig getTopicConfig(String topicName) { + return this.topicConfigTable.get(topicName); } - public boolean isTopicCanSendMessage(final String topic) { - return !topic.equals(MixAll.DEFAULT_TOPIC); + protected TopicConfig removeTopicConfig(String topicName) { + return this.topicConfigTable.remove(topicName); } public TopicConfig selectTopicConfig(final String topic) { - return this.topicConfigTable.get(topic); + return getTopicConfig(topic); } public TopicConfig createTopicInSendMessageMethod(final String topic, final String defaultTopic, @@ -148,15 +231,16 @@ public TopicConfig createTopicInSendMessageMethod(final String topic, final Stri boolean createNew = false; try { - if (this.lockTopicConfigTable.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { + if (this.topicConfigTableLock.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { try { - topicConfig = this.topicConfigTable.get(topic); - if (topicConfig != null) + topicConfig = getTopicConfig(topic); + if (topicConfig != null) { return topicConfig; + } - TopicConfig defaultTopicConfig = this.topicConfigTable.get(defaultTopic); + TopicConfig defaultTopicConfig = getTopicConfig(defaultTopic); if (defaultTopicConfig != null) { - if (defaultTopic.equals(MixAll.DEFAULT_TOPIC)) { + if (defaultTopic.equals(TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC)) { if (!this.brokerController.getBrokerConfig().isAutoCreateTopicEnable()) { defaultTopicConfig.setPerm(PermName.PERM_READ | PermName.PERM_WRITE); } @@ -165,9 +249,7 @@ public TopicConfig createTopicInSendMessageMethod(final String topic, final Stri if (PermName.isInherited(defaultTopicConfig.getPerm())) { topicConfig = new TopicConfig(topic); - int queueNums = - clientDefaultTopicQueueNums > defaultTopicConfig.getWriteQueueNums() ? defaultTopicConfig - .getWriteQueueNums() : clientDefaultTopicQueueNums; + int queueNums = Math.min(clientDefaultTopicQueueNums, defaultTopicConfig.getWriteQueueNums()); if (queueNums < 0) { queueNums = 0; @@ -193,16 +275,17 @@ public TopicConfig createTopicInSendMessageMethod(final String topic, final Stri log.info("Create new topic by default topic:[{}] config:[{}] producer:[{}]", defaultTopic, topicConfig, remoteAddress); - this.topicConfigTable.put(topic, topicConfig); + putTopicConfig(topicConfig); - this.dataVersion.nextVersion(); + long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0; + dataVersion.nextVersion(stateMachineVersion); createNew = true; this.persist(); } } finally { - this.lockTopicConfigTable.unlock(); + this.topicConfigTableLock.unlock(); } } } catch (InterruptedException e) { @@ -210,43 +293,99 @@ public TopicConfig createTopicInSendMessageMethod(final String topic, final Stri } if (createNew) { - this.brokerController.registerBrokerAll(false, true); + registerBrokerData(topicConfig); } return topicConfig; } + public TopicConfig createTopicIfAbsent(TopicConfig topicConfig) { + return createTopicIfAbsent(topicConfig, true); + } + + public TopicConfig createTopicIfAbsent(TopicConfig topicConfig, boolean register) { + boolean createNew = false; + if (topicConfig == null) { + throw new NullPointerException("TopicConfig"); + } + if (StringUtils.isEmpty(topicConfig.getTopicName())) { + throw new IllegalArgumentException("TopicName"); + } + + try { + if (this.topicConfigTableLock.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { + try { + TopicConfig existedTopicConfig = getTopicConfig(topicConfig.getTopicName()); + if (existedTopicConfig != null) { + return existedTopicConfig; + } + log.info("Create new topic [{}] config:[{}]", topicConfig.getTopicName(), topicConfig); + putTopicConfig(topicConfig); + long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0; + dataVersion.nextVersion(stateMachineVersion); + createNew = true; + this.persist(); + } finally { + this.topicConfigTableLock.unlock(); + } + } + } catch (InterruptedException e) { + log.error("createTopicIfAbsent ", e); + } + if (createNew && register) { + registerBrokerData(topicConfig); + } + return getTopicConfig(topicConfig.getTopicName()); + } + public TopicConfig createTopicInSendMessageBackMethod( final String topic, final int clientDefaultTopicQueueNums, final int perm, final int topicSysFlag) { - TopicConfig topicConfig = this.topicConfigTable.get(topic); - if (topicConfig != null) + return createTopicInSendMessageBackMethod(topic, clientDefaultTopicQueueNums, perm, false, topicSysFlag); + } + + public TopicConfig createTopicInSendMessageBackMethod( + final String topic, + final int clientDefaultTopicQueueNums, + final int perm, + final boolean isOrder, + final int topicSysFlag) { + TopicConfig topicConfig = getTopicConfig(topic); + if (topicConfig != null) { + if (isOrder != topicConfig.isOrder()) { + topicConfig.setOrder(isOrder); + this.updateTopicConfig(topicConfig); + } return topicConfig; + } boolean createNew = false; try { - if (this.lockTopicConfigTable.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { + if (this.topicConfigTableLock.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { try { - topicConfig = this.topicConfigTable.get(topic); - if (topicConfig != null) + topicConfig = getTopicConfig(topic); + if (topicConfig != null) { return topicConfig; + } topicConfig = new TopicConfig(topic); topicConfig.setReadQueueNums(clientDefaultTopicQueueNums); topicConfig.setWriteQueueNums(clientDefaultTopicQueueNums); topicConfig.setPerm(perm); topicConfig.setTopicSysFlag(topicSysFlag); + topicConfig.setOrder(isOrder); log.info("create new topic {}", topicConfig); - this.topicConfigTable.put(topic, topicConfig); + putTopicConfig(topicConfig); createNew = true; - this.dataVersion.nextVersion(); + long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0; + dataVersion.nextVersion(stateMachineVersion); this.persist(); } finally { - this.lockTopicConfigTable.unlock(); + this.topicConfigTableLock.unlock(); } } } catch (InterruptedException e) { @@ -254,7 +393,48 @@ public TopicConfig createTopicInSendMessageBackMethod( } if (createNew) { - this.brokerController.registerBrokerAll(false, true); + registerBrokerData(topicConfig); + } + + return topicConfig; + } + + public TopicConfig createTopicOfTranCheckMaxTime(final int clientDefaultTopicQueueNums, final int perm) { + TopicConfig topicConfig = getTopicConfig(TopicValidator.RMQ_SYS_TRANS_CHECK_MAX_TIME_TOPIC); + if (topicConfig != null) + return topicConfig; + + boolean createNew = false; + + try { + if (this.topicConfigTableLock.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { + try { + topicConfig = getTopicConfig(TopicValidator.RMQ_SYS_TRANS_CHECK_MAX_TIME_TOPIC); + if (topicConfig != null) + return topicConfig; + + topicConfig = new TopicConfig(TopicValidator.RMQ_SYS_TRANS_CHECK_MAX_TIME_TOPIC); + topicConfig.setReadQueueNums(clientDefaultTopicQueueNums); + topicConfig.setWriteQueueNums(clientDefaultTopicQueueNums); + topicConfig.setPerm(perm); + topicConfig.setTopicSysFlag(0); + + log.info("create new topic {}", topicConfig); + putTopicConfig(topicConfig); + createNew = true; + long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0; + dataVersion.nextVersion(stateMachineVersion); + this.persist(); + } finally { + this.topicConfigTableLock.unlock(); + } + } + } catch (InterruptedException e) { + log.error("create TRANS_CHECK_MAX_TIME_TOPIC exception", e); + } + + if (createNew) { + registerBrokerData(topicConfig); } return topicConfig; @@ -262,7 +442,7 @@ public TopicConfig createTopicInSendMessageBackMethod( public void updateTopicUnitFlag(final String topic, final boolean unit) { - TopicConfig topicConfig = this.topicConfigTable.get(topic); + TopicConfig topicConfig = getTopicConfig(topic); if (topicConfig != null) { int oldTopicSysFlag = topicConfig.getTopicSysFlag(); if (unit) { @@ -271,49 +451,68 @@ public void updateTopicUnitFlag(final String topic, final boolean unit) { topicConfig.setTopicSysFlag(TopicSysFlag.clearUnitFlag(oldTopicSysFlag)); } - log.info("update topic sys flag. oldTopicSysFlag={}, newTopicSysFlag", oldTopicSysFlag, + log.info("update topic sys flag. oldTopicSysFlag={}, newTopicSysFlag={}", oldTopicSysFlag, topicConfig.getTopicSysFlag()); - this.topicConfigTable.put(topic, topicConfig); + putTopicConfig(topicConfig); - this.dataVersion.nextVersion(); + long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0; + dataVersion.nextVersion(stateMachineVersion); this.persist(); - this.brokerController.registerBrokerAll(false, true); + registerBrokerData(topicConfig); } } public void updateTopicUnitSubFlag(final String topic, final boolean hasUnitSub) { - TopicConfig topicConfig = this.topicConfigTable.get(topic); + TopicConfig topicConfig = getTopicConfig(topic); if (topicConfig != null) { int oldTopicSysFlag = topicConfig.getTopicSysFlag(); if (hasUnitSub) { topicConfig.setTopicSysFlag(TopicSysFlag.setUnitSubFlag(oldTopicSysFlag)); + } else { + topicConfig.setTopicSysFlag(TopicSysFlag.clearUnitSubFlag(oldTopicSysFlag)); } - log.info("update topic sys flag. oldTopicSysFlag={}, newTopicSysFlag", oldTopicSysFlag, + log.info("update topic sys flag. oldTopicSysFlag={}, newTopicSysFlag={}", oldTopicSysFlag, topicConfig.getTopicSysFlag()); - this.topicConfigTable.put(topic, topicConfig); + putTopicConfig(topicConfig); - this.dataVersion.nextVersion(); + long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0; + dataVersion.nextVersion(stateMachineVersion); this.persist(); - this.brokerController.registerBrokerAll(false, true); + registerBrokerData(topicConfig); } } public void updateTopicConfig(final TopicConfig topicConfig) { - TopicConfig old = this.topicConfigTable.put(topicConfig.getTopicName(), topicConfig); + checkNotNull(topicConfig, "topicConfig shouldn't be null"); + + Map newAttributes = request(topicConfig); + Map currentAttributes = current(topicConfig.getTopicName()); + + + Map finalAttributes = AttributeUtil.alterCurrentAttributes( + this.topicConfigTable.get(topicConfig.getTopicName()) == null, + TopicAttributes.ALL, + ImmutableMap.copyOf(currentAttributes), + ImmutableMap.copyOf(newAttributes)); + + topicConfig.setAttributes(finalAttributes); + + TopicConfig old = putTopicConfig(topicConfig); if (old != null) { log.info("update topic config, old:[{}] new:[{}]", old, topicConfig); } else { log.info("create new topic [{}]", topicConfig); } - this.dataVersion.nextVersion(); + long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0; + dataVersion.nextVersion(stateMachineVersion); - this.persist(); + this.persist(topicConfig.getTopicName(), topicConfig); } public void updateOrderTopicConfig(final KVTable orderKVTableFromNs) { @@ -322,7 +521,7 @@ public void updateOrderTopicConfig(final KVTable orderKVTableFromNs) { boolean isChange = false; Set orderTopics = orderKVTableFromNs.getTable().keySet(); for (String topic : orderTopics) { - TopicConfig topicConfig = this.topicConfigTable.get(topic); + TopicConfig topicConfig = getTopicConfig(topic); if (topicConfig != null && !topicConfig.isOrder()) { topicConfig.setOrder(true); isChange = true; @@ -330,7 +529,11 @@ public void updateOrderTopicConfig(final KVTable orderKVTableFromNs) { } } - for (Map.Entry entry : this.topicConfigTable.entrySet()) { + // We don't have a mandatory rule to maintain the validity of order conf in NameServer, + // so we may overwrite the order field mistakenly. + // To avoid the above case, we comment the below codes, please use mqadmin API to update + // the order filed. + /*for (Map.Entry entry : this.topicConfigTable.entrySet()) { String topic = entry.getKey(); if (!orderTopics.contains(topic)) { TopicConfig topicConfig = entry.getValue(); @@ -340,17 +543,23 @@ public void updateOrderTopicConfig(final KVTable orderKVTableFromNs) { log.info("update order topic config, topic={}, order={}", topic, false); } } - } + }*/ if (isChange) { - this.dataVersion.nextVersion(); + long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0; + dataVersion.nextVersion(stateMachineVersion); this.persist(); } } } + // make it testable + public Map allAttributes() { + return TopicAttributes.ALL; + } + public boolean isOrderTopic(final String topic) { - TopicConfig topicConfig = this.topicConfigTable.get(topic); + TopicConfig topicConfig = getTopicConfig(topic); if (topicConfig == null) { return false; } else { @@ -359,10 +568,11 @@ public boolean isOrderTopic(final String topic) { } public void deleteTopicConfig(final String topic) { - TopicConfig old = this.topicConfigTable.remove(topic); + TopicConfig old = removeTopicConfig(topic); if (old != null) { log.info("delete topic config OK, topic: {}", old); - this.dataVersion.nextVersion(); + long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0; + dataVersion.nextVersion(stateMachineVersion); this.persist(); } else { log.warn("delete topic config failed, topic: {} not exists", topic); @@ -372,10 +582,30 @@ public void deleteTopicConfig(final String topic) { public TopicConfigSerializeWrapper buildTopicConfigSerializeWrapper() { TopicConfigSerializeWrapper topicConfigSerializeWrapper = new TopicConfigSerializeWrapper(); topicConfigSerializeWrapper.setTopicConfigTable(this.topicConfigTable); - topicConfigSerializeWrapper.setDataVersion(this.dataVersion); + DataVersion dataVersionCopy = new DataVersion(); + dataVersionCopy.assignNewOne(this.dataVersion); + topicConfigSerializeWrapper.setDataVersion(dataVersionCopy); return topicConfigSerializeWrapper; } + public TopicConfigAndMappingSerializeWrapper buildSerializeWrapper(final ConcurrentMap topicConfigTable) { + return buildSerializeWrapper(topicConfigTable, Maps.newHashMap()); + } + + public TopicConfigAndMappingSerializeWrapper buildSerializeWrapper( + final ConcurrentMap topicConfigTable, + final Map topicQueueMappingInfoMap + ) { + TopicConfigAndMappingSerializeWrapper topicConfigWrapper = new TopicConfigAndMappingSerializeWrapper(); + topicConfigWrapper.setTopicConfigTable(topicConfigTable); + topicConfigWrapper.setTopicQueueMappingInfoMap(topicQueueMappingInfoMap); + topicConfigWrapper.setDataVersion(this.getDataVersion()); + if (this.brokerController.getBrokerConfig().isEnableSplitRegistration()) { + this.getDataVersion().nextVersion(); + } + return topicConfigWrapper; + } + @Override public String encode() { return encode(false); @@ -383,8 +613,7 @@ public String encode() { @Override public String configFilePath() { - return BrokerPathConfigHelper.getTopicConfigPath(this.brokerController.getMessageStoreConfig() - .getStorePathRootDir()); + return BrokerPathConfigHelper.getTopicConfigPath(this.brokerController.getMessageStoreConfig().getStorePathRootDir()); } @Override @@ -419,7 +648,44 @@ public DataVersion getDataVersion() { return dataVersion; } + public void setTopicConfigTable( + ConcurrentMap topicConfigTable) { + this.topicConfigTable = topicConfigTable; + } + public ConcurrentMap getTopicConfigTable() { return topicConfigTable; } + + private Map request(TopicConfig topicConfig) { + return topicConfig.getAttributes() == null ? new HashMap<>() : topicConfig.getAttributes(); + } + + private Map current(String topic) { + TopicConfig topicConfig = getTopicConfig(topic); + if (topicConfig == null) { + return new HashMap<>(); + } else { + Map attributes = topicConfig.getAttributes(); + if (attributes == null) { + return new HashMap<>(); + } else { + return attributes; + } + } + } + + private void registerBrokerData(TopicConfig topicConfig) { + if (brokerController.getBrokerConfig().isEnableSingleTopicRegister()) { + this.brokerController.registerSingleTopicAll(topicConfig); + } else { + this.brokerController.registerIncrementBrokerData(topicConfig, dataVersion); + } + } + + public boolean containsTopic(String topic) { + return topicConfigTable.containsKey(topic); + } + + } diff --git a/broker/src/main/java/org/apache/rocketmq/broker/topic/TopicQueueMappingCleanService.java b/broker/src/main/java/org/apache/rocketmq/broker/topic/TopicQueueMappingCleanService.java new file mode 100644 index 00000000000..c4fd4c62082 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/topic/TopicQueueMappingCleanService.java @@ -0,0 +1,336 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.topic; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.out.BrokerOuterAPI; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.admin.TopicOffset; +import org.apache.rocketmq.remoting.protocol.admin.TopicStatsTable; +import org.apache.rocketmq.remoting.protocol.header.GetTopicConfigRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetTopicStatsInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.apache.rocketmq.remoting.protocol.statictopic.LogicQueueMappingItem; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicConfigAndQueueMapping; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingDetail; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingUtils; +import org.apache.rocketmq.remoting.rpc.ClientMetadata; +import org.apache.rocketmq.remoting.rpc.RpcClient; +import org.apache.rocketmq.remoting.rpc.RpcRequest; +import org.apache.rocketmq.remoting.rpc.RpcResponse; +import org.apache.rocketmq.store.config.MessageStoreConfig; + +public class TopicQueueMappingCleanService extends ServiceThread { + private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + + private TopicQueueMappingManager topicQueueMappingManager; + private BrokerOuterAPI brokerOuterAPI; + private RpcClient rpcClient; + private MessageStoreConfig messageStoreConfig; + private BrokerConfig brokerConfig; + private BrokerController brokerController; + + public TopicQueueMappingCleanService(BrokerController brokerController) { + this.brokerController = brokerController; + this.topicQueueMappingManager = brokerController.getTopicQueueMappingManager(); + this.rpcClient = brokerController.getBrokerOuterAPI().getRpcClient(); + this.messageStoreConfig = brokerController.getMessageStoreConfig(); + this.brokerConfig = brokerController.getBrokerConfig(); + this.brokerOuterAPI = brokerController.getBrokerOuterAPI(); + } + + @Override + public String getServiceName() { + if (this.brokerConfig.isInBrokerContainer()) { + return this.brokerController.getBrokerIdentity().getIdentifier() + TopicQueueMappingCleanService.class.getSimpleName(); + } + return TopicQueueMappingCleanService.class.getSimpleName(); + } + + @Override + public void run() { + log.info("Start topic queue mapping clean service thread!"); + while (!this.isStopped()) { + try { + this.waitForRunning(5L * 60 * 1000); + } catch (Throwable ignored) { + } + try { + cleanItemExpired(); + } catch (Throwable t) { + log.error("topic queue mapping cleanItemExpired failed", t); + } + try { + cleanItemListMoreThanSecondGen(); + } catch (Throwable t) { + log.error("topic queue mapping cleanItemListMoreThanSecondGen failed", t); + } + + } + log.info("End topic queue mapping clean service thread!"); + } + + + + public void cleanItemExpired() { + String when = messageStoreConfig.getDeleteWhen(); + if (!UtilAll.isItTimeToDo(when)) { + return; + } + boolean changed = false; + long start = System.currentTimeMillis(); + try { + for (String topic : this.topicQueueMappingManager.getTopicQueueMappingTable().keySet()) { + try { + if (isStopped()) { + break; + } + TopicQueueMappingDetail mappingDetail = this.topicQueueMappingManager.getTopicQueueMappingTable().get(topic); + if (mappingDetail == null + || mappingDetail.getHostedQueues().isEmpty()) { + continue; + } + if (!mappingDetail.getBname().equals(brokerConfig.getBrokerName())) { + log.warn("The TopicQueueMappingDetail [{}] should not exist in this broker", mappingDetail); + continue; + } + Set brokers = new HashSet<>(); + for (List items: mappingDetail.getHostedQueues().values()) { + if (items.size() <= 1) { + continue; + } + if (!TopicQueueMappingUtils.checkIfLeader(items, mappingDetail)) { + continue; + } + LogicQueueMappingItem earlistItem = items.get(0); + brokers.add(earlistItem.getBname()); + } + Map statsTable = new HashMap<>(); + for (String broker: brokers) { + GetTopicStatsInfoRequestHeader header = new GetTopicStatsInfoRequestHeader(); + header.setTopic(topic); + header.setBrokerName(broker); + header.setLo(false); + try { + RpcRequest rpcRequest = new RpcRequest(RequestCode.GET_TOPIC_STATS_INFO, header, null); + RpcResponse rpcResponse = rpcClient.invoke(rpcRequest, brokerConfig.getForwardTimeout()).get(); + if (rpcResponse.getException() != null) { + throw rpcResponse.getException(); + } + statsTable.put(broker, (TopicStatsTable) rpcResponse.getBody()); + } catch (Throwable rt) { + log.error("Get remote topic {} state info failed from broker {}", topic, broker, rt); + } + } + Map> newHostedQueues = new HashMap<>(); + boolean changedForTopic = false; + for (Map.Entry> entry : mappingDetail.getHostedQueues().entrySet()) { + Integer qid = entry.getKey(); + List items = entry.getValue(); + if (items.size() <= 1) { + continue; + } + if (!TopicQueueMappingUtils.checkIfLeader(items, mappingDetail)) { + continue; + } + LogicQueueMappingItem earlistItem = items.get(0); + TopicStatsTable topicStats = statsTable.get(earlistItem.getBname()); + if (topicStats == null) { + continue; + } + TopicOffset topicOffset = topicStats.getOffsetTable().get(new MessageQueue(topic, earlistItem.getBname(), earlistItem.getQueueId())); + if (topicOffset == null) { + //this may should not happen + log.error("Get null topicOffset for {} {}",topic, earlistItem); + continue; + } + //ignore the maxOffset < 0, which may in case of some error + if (topicOffset.getMaxOffset() == topicOffset.getMinOffset() + || topicOffset.getMaxOffset() == 0) { + List newItems = new ArrayList<>(items); + boolean result = newItems.remove(earlistItem); + if (result) { + changedForTopic = true; + newHostedQueues.put(qid, newItems); + } + log.info("The logic queue item {} {} is removed {} because of {}", topic, earlistItem, result, topicOffset); + } + } + if (changedForTopic) { + TopicQueueMappingDetail newMappingDetail = new TopicQueueMappingDetail(mappingDetail.getTopic(), mappingDetail.getTotalQueues(), mappingDetail.getBname(), mappingDetail.getEpoch()); + newMappingDetail.getHostedQueues().putAll(mappingDetail.getHostedQueues()); + newMappingDetail.getHostedQueues().putAll(newHostedQueues); + this.topicQueueMappingManager.updateTopicQueueMapping(newMappingDetail, false, true, false); + changed = true; + } + } catch (Throwable tt) { + log.error("Try CleanItemExpired failed for {}", topic, tt); + } finally { + UtilAll.sleep(10); + } + } + } catch (Throwable t) { + log.error("Try cleanItemExpired failed", t); + } finally { + if (changed) { + this.topicQueueMappingManager.getDataVersion().nextVersion(); + this.topicQueueMappingManager.persist(); + log.info("CleanItemExpired changed"); + } + log.info("cleanItemExpired cost {} ms", System.currentTimeMillis() - start); + } + } + + public void cleanItemListMoreThanSecondGen() { + String when = messageStoreConfig.getDeleteWhen(); + if (!UtilAll.isItTimeToDo(when)) { + return; + } + boolean changed = false; + long start = System.currentTimeMillis(); + try { + ClientMetadata clientMetadata = new ClientMetadata(); + for (String topic : this.topicQueueMappingManager.getTopicQueueMappingTable().keySet()) { + try { + if (isStopped()) { + break; + } + TopicQueueMappingDetail mappingDetail = this.topicQueueMappingManager.getTopicQueueMappingTable().get(topic); + if (mappingDetail == null + || mappingDetail.getHostedQueues().isEmpty()) { + continue; + } + if (!mappingDetail.getBname().equals(brokerConfig.getBrokerName())) { + log.warn("The TopicQueueMappingDetail [{}] should not exist in this broker", mappingDetail); + continue; + } + Map qid2CurrLeaderBroker = new HashMap<>(); + for (Map.Entry> entry : mappingDetail.getHostedQueues().entrySet()) { + Integer qId = entry.getKey(); + List items = entry.getValue(); + if (items.isEmpty()) { + continue; + } + LogicQueueMappingItem leaderItem = items.get(items.size() - 1); + if (!leaderItem.getBname().equals(mappingDetail.getBname())) { + qid2CurrLeaderBroker.put(qId, leaderItem.getBname()); + } + } + if (qid2CurrLeaderBroker.isEmpty()) { + continue; + } + //find the topic route + TopicRouteData topicRouteData = brokerOuterAPI.getTopicRouteInfoFromNameServer(topic, brokerConfig.getForwardTimeout()); + clientMetadata.freshTopicRoute(topic, topicRouteData); + Map qid2RealLeaderBroker = new HashMap<>(); + //fine the real leader + for (Map.Entry entry : qid2CurrLeaderBroker.entrySet()) { + qid2RealLeaderBroker.put(entry.getKey(), clientMetadata.getBrokerNameFromMessageQueue(new MessageQueue(topic, TopicQueueMappingUtils.getMockBrokerName(mappingDetail.getScope()), entry.getKey()))); + } + + //find the mapping detail of real leader + Map mappingDetailMap = new HashMap<>(); + for (Map.Entry entry : qid2RealLeaderBroker.entrySet()) { + if (entry.getValue().startsWith(MixAll.LOGICAL_QUEUE_MOCK_BROKER_PREFIX)) { + continue; + } + String broker = entry.getValue(); + GetTopicConfigRequestHeader header = new GetTopicConfigRequestHeader(); + header.setTopic(topic); + header.setBrokerName(broker); + header.setLo(true); + try { + RpcRequest rpcRequest = new RpcRequest(RequestCode.GET_TOPIC_CONFIG, header, null); + RpcResponse rpcResponse = rpcClient.invoke(rpcRequest, brokerConfig.getForwardTimeout()).get(); + if (rpcResponse.getException() != null) { + throw rpcResponse.getException(); + } + TopicQueueMappingDetail mappingDetailRemote = ((TopicConfigAndQueueMapping) rpcResponse.getBody()).getMappingDetail(); + if (broker.equals(mappingDetailRemote.getBname())) { + mappingDetailMap.put(broker, mappingDetailRemote); + } + } catch (Throwable rt) { + log.error("Get remote topic {} state info failed from broker {}", topic, broker, rt); + } + } + //check all the info + Set ids2delete = new HashSet<>(); + for (Map.Entry entry : qid2CurrLeaderBroker.entrySet()) { + Integer qId = entry.getKey(); + String currLeaderBroker = entry.getValue(); + String realLeaderBroker = qid2RealLeaderBroker.get(qId); + TopicQueueMappingDetail remoteMappingDetail = mappingDetailMap.get(realLeaderBroker); + if (remoteMappingDetail == null + || remoteMappingDetail.getTotalQueues() != mappingDetail.getTotalQueues() + || remoteMappingDetail.getEpoch() != mappingDetail.getEpoch()) { + continue; + } + List items = remoteMappingDetail.getHostedQueues().get(qId); + if (items.isEmpty()) { + continue; + } + LogicQueueMappingItem leaderItem = items.get(items.size() - 1); + if (!realLeaderBroker.equals(leaderItem.getBname())) { + continue; + } + //all the check is ok + if (!realLeaderBroker.equals(currLeaderBroker)) { + ids2delete.add(qId); + } + } + for (Integer qid : ids2delete) { + List items = mappingDetail.getHostedQueues().remove(qid); + changed = true; + if (items != null) { + log.info("Remove the ItemListMoreThanSecondGen topic {} qid {} items {}", topic, qid, items); + } + } + } catch (Throwable tt) { + log.error("Try cleanItemListMoreThanSecondGen failed for topic {}", topic, tt); + } finally { + UtilAll.sleep(10); + } + } + } catch (Throwable t) { + log.error("Try cleanItemListMoreThanSecondGen failed", t); + } finally { + if (changed) { + this.topicQueueMappingManager.getDataVersion().nextVersion(); + this.topicQueueMappingManager.persist(); + } + log.info("Try cleanItemListMoreThanSecondGen cost {} ms", System.currentTimeMillis() - start); + } + } + + + + +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/topic/TopicQueueMappingManager.java b/broker/src/main/java/org/apache/rocketmq/broker/topic/TopicQueueMappingManager.java new file mode 100644 index 00000000000..6b9cf159383 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/topic/TopicQueueMappingManager.java @@ -0,0 +1,258 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.topic; + +import com.alibaba.fastjson.JSON; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.BrokerPathConfigHelper; +import org.apache.rocketmq.common.ConfigManager; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.TopicQueueMappingSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.statictopic.LogicQueueMappingItem; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingContext; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingDetail; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingUtils; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; + +import static org.apache.rocketmq.remoting.protocol.RemotingCommand.buildErrorResponse; + +public class TopicQueueMappingManager extends ConfigManager { + private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + private static final long LOCK_TIMEOUT_MILLIS = 3000; + private transient final Lock lock = new ReentrantLock(); + + //this data version should be equal to the TopicConfigManager + private final DataVersion dataVersion = new DataVersion(); + private transient BrokerController brokerController; + + private final ConcurrentMap topicQueueMappingTable = new ConcurrentHashMap<>(); + + + public TopicQueueMappingManager(BrokerController brokerController) { + this.brokerController = brokerController; + } + + public void updateTopicQueueMapping(TopicQueueMappingDetail newDetail, boolean force, boolean isClean, boolean flush) throws Exception { + boolean locked = false; + boolean updated = false; + TopicQueueMappingDetail oldDetail = null; + try { + + if (lock.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { + locked = true; + } else { + return; + } + if (newDetail == null) { + return; + } + assert newDetail.getBname().equals(this.brokerController.getBrokerConfig().getBrokerName()); + + newDetail.getHostedQueues().forEach((queueId, items) -> { + TopicQueueMappingUtils.checkLogicQueueMappingItemOffset(items); + }); + + oldDetail = topicQueueMappingTable.get(newDetail.getTopic()); + if (oldDetail == null) { + topicQueueMappingTable.put(newDetail.getTopic(), newDetail); + updated = true; + return; + } + if (force) { + //bakeup the old items + oldDetail.getHostedQueues().forEach((queueId, items) -> { + newDetail.getHostedQueues().putIfAbsent(queueId, items); + }); + topicQueueMappingTable.put(newDetail.getTopic(), newDetail); + updated = true; + return; + } + //do more check + if (newDetail.getEpoch() < oldDetail.getEpoch()) { + throw new RuntimeException(String.format("Can't accept data with small epoch %d < %d", newDetail.getEpoch(), oldDetail.getEpoch())); + } + if (!newDetail.getScope().equals(oldDetail.getScope())) { + throw new RuntimeException(String.format("Can't accept data with unmatched scope %s != %s", newDetail.getScope(), oldDetail.getScope())); + } + boolean epochEqual = newDetail.getEpoch() == oldDetail.getEpoch(); + for (Integer globalId : oldDetail.getHostedQueues().keySet()) { + List oldItems = oldDetail.getHostedQueues().get(globalId); + List newItems = newDetail.getHostedQueues().get(globalId); + if (newItems == null) { + if (epochEqual) { + throw new RuntimeException("Cannot accept equal epoch with null data"); + } else { + newDetail.getHostedQueues().put(globalId, oldItems); + } + } else { + TopicQueueMappingUtils.makeSureLogicQueueMappingItemImmutable(oldItems, newItems, epochEqual, isClean); + } + } + topicQueueMappingTable.put(newDetail.getTopic(), newDetail); + updated = true; + } finally { + if (locked) { + this.lock.unlock(); + } + if (updated && flush) { + this.dataVersion.nextVersion(); + this.persist(); + log.info("Update topic queue mapping from [{}] to [{}], force {}", oldDetail, newDetail, force); + } + } + + } + + public void delete(final String topic) { + TopicQueueMappingDetail old = this.topicQueueMappingTable.remove(topic); + if (old != null) { + log.info("delete topic queue mapping OK, static topic queue mapping: {}", old); + this.dataVersion.nextVersion(); + this.persist(); + } else { + log.warn("delete topic queue mapping failed, static topic: {} not exists", topic); + } + } + + public TopicQueueMappingDetail getTopicQueueMapping(String topic) { + return topicQueueMappingTable.get(topic); + } + + @Override + public String encode(boolean pretty) { + TopicQueueMappingSerializeWrapper wrapper = new TopicQueueMappingSerializeWrapper(); + wrapper.setTopicQueueMappingInfoMap(topicQueueMappingTable); + wrapper.setDataVersion(this.dataVersion); + return JSON.toJSONString(wrapper, pretty); + } + + @Override + public String encode() { + return encode(false); + } + + @Override + public String configFilePath() { + return BrokerPathConfigHelper.getTopicQueueMappingPath(this.brokerController.getMessageStoreConfig() + .getStorePathRootDir()); + } + + @Override + public void decode(String jsonString) { + if (jsonString != null) { + TopicQueueMappingSerializeWrapper wrapper = TopicQueueMappingSerializeWrapper.fromJson(jsonString, TopicQueueMappingSerializeWrapper.class); + if (wrapper != null) { + this.topicQueueMappingTable.putAll(wrapper.getTopicQueueMappingInfoMap()); + this.dataVersion.assignNewOne(wrapper.getDataVersion()); + } + } + } + + public ConcurrentMap getTopicQueueMappingTable() { + return topicQueueMappingTable; + } + + public DataVersion getDataVersion() { + return dataVersion; + } + + public TopicQueueMappingContext buildTopicQueueMappingContext(TopicRequestHeader requestHeader) { + return buildTopicQueueMappingContext(requestHeader, false); + } + + //Do not return a null context + public TopicQueueMappingContext buildTopicQueueMappingContext(TopicRequestHeader requestHeader, boolean selectOneWhenMiss) { + // if lo is set to false explicitly, it maybe the forwarded request + if (requestHeader.getLo() != null + && Boolean.FALSE.equals(requestHeader.getLo())) { + return new TopicQueueMappingContext(requestHeader.getTopic(), null, null, null, null); + } + String topic = requestHeader.getTopic(); + Integer globalId = null; + if (requestHeader instanceof TopicQueueRequestHeader) { + globalId = ((TopicQueueRequestHeader) requestHeader).getQueueId(); + } + + TopicQueueMappingDetail mappingDetail = getTopicQueueMapping(topic); + if (mappingDetail == null) { + //it is not static topic + return new TopicQueueMappingContext(topic, null, null, null, null); + } + assert mappingDetail.getBname().equals(this.brokerController.getBrokerConfig().getBrokerName()); + + if (globalId == null) { + return new TopicQueueMappingContext(topic, null, mappingDetail, null, null); + } + + //If not find mappingItem, it encounters some errors + if (globalId < 0 && !selectOneWhenMiss) { + return new TopicQueueMappingContext(topic, globalId, mappingDetail, null, null); + } + + if (globalId < 0) { + try { + if (!mappingDetail.getHostedQueues().isEmpty()) { + //do not check + globalId = mappingDetail.getHostedQueues().keySet().iterator().next(); + } + } catch (Throwable ignored) { + } + } + if (globalId < 0) { + return new TopicQueueMappingContext(topic, globalId, mappingDetail, null, null); + } + + List mappingItemList = TopicQueueMappingDetail.getMappingInfo(mappingDetail, globalId); + LogicQueueMappingItem leaderItem = null; + if (mappingItemList != null + && mappingItemList.size() > 0) { + leaderItem = mappingItemList.get(mappingItemList.size() - 1); + } + return new TopicQueueMappingContext(topic, globalId, mappingDetail, mappingItemList, leaderItem); + } + + + public RemotingCommand rewriteRequestForStaticTopic(TopicQueueRequestHeader requestHeader, TopicQueueMappingContext mappingContext) { + try { + if (mappingContext.getMappingDetail() == null) { + return null; + } + TopicQueueMappingDetail mappingDetail = mappingContext.getMappingDetail(); + if (!mappingContext.isLeader()) { + return buildErrorResponse(ResponseCode.NOT_LEADER_FOR_QUEUE, String.format("%s-%d does not exit in request process of current broker %s", requestHeader.getTopic(), requestHeader.getQueueId(), mappingDetail.getBname())); + } + LogicQueueMappingItem mappingItem = mappingContext.getLeaderItem(); + requestHeader.setQueueId(mappingItem.getQueueId()); + return null; + } catch (Throwable t) { + return buildErrorResponse(ResponseCode.SYSTEM_ERROR, t.getMessage()); + } + } + +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/topic/TopicRouteInfoManager.java b/broker/src/main/java/org/apache/rocketmq/broker/topic/TopicRouteInfoManager.java new file mode 100644 index 00000000000..11bde5f5fe2 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/topic/TopicRouteInfoManager.java @@ -0,0 +1,258 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.topic; + +import com.google.common.collect.Sets; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.client.impl.producer.TopicPublishInfo; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; + +public class TopicRouteInfoManager { + + private static final long GET_TOPIC_ROUTE_TIMEOUT = 3000L; + private static final long LOCK_TIMEOUT_MILLIS = 3000L; + private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + + private final Lock lockNamesrv = new ReentrantLock(); + private final ConcurrentMap topicRouteTable = new ConcurrentHashMap<>(); + private final ConcurrentMap> brokerAddrTable = + new ConcurrentHashMap<>(); + private final ConcurrentMap topicPublishInfoTable = new ConcurrentHashMap<>(); + + private final ConcurrentHashMap> topicSubscribeInfoTable = new ConcurrentHashMap<>(); + + private ScheduledExecutorService scheduledExecutorService; + private BrokerController brokerController; + + public TopicRouteInfoManager(BrokerController brokerController) { + this.brokerController = brokerController; + } + + public void start() { + this.scheduledExecutorService = ThreadUtils.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("TopicRouteInfoManagerScheduledThread")); + + this.scheduledExecutorService.scheduleAtFixedRate(() -> { + try { + updateTopicRouteInfoFromNameServer(); + } catch (Exception e) { + log.error("ScheduledTask: failed to pull TopicRouteData from NameServer", e); + } + }, 1000, this.brokerController.getBrokerConfig().getLoadBalancePollNameServerInterval(), TimeUnit.MILLISECONDS); + } + + private void updateTopicRouteInfoFromNameServer() { + final Set topicSetForPopAssignment = this.topicSubscribeInfoTable.keySet(); + final Set topicSetForEscapeBridge = this.topicRouteTable.keySet(); + final Set topicsAll = Sets.union(topicSetForPopAssignment, topicSetForEscapeBridge); + + for (String topic : topicsAll) { + boolean isNeedUpdatePublishInfo = topicSetForEscapeBridge.contains(topic); + boolean isNeedUpdateSubscribeInfo = topicSetForPopAssignment.contains(topic); + updateTopicRouteInfoFromNameServer(topic, isNeedUpdatePublishInfo, isNeedUpdateSubscribeInfo); + } + } + + public void updateTopicRouteInfoFromNameServer(String topic, boolean isNeedUpdatePublishInfo, + boolean isNeedUpdateSubscribeInfo) { + try { + if (this.lockNamesrv.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { + try { + final TopicRouteData topicRouteData = this.brokerController.getBrokerOuterAPI() + .getTopicRouteInfoFromNameServer(topic, GET_TOPIC_ROUTE_TIMEOUT); + if (null == topicRouteData) { + log.warn("TopicRouteInfoManager: updateTopicRouteInfoFromNameServer, getTopicRouteInfoFromNameServer return null, Topic: {}.", topic); + return; + } + + if (isNeedUpdateSubscribeInfo) { + this.updateSubscribeInfoTable(topicRouteData, topic); + } + + if (isNeedUpdatePublishInfo) { + this.updateTopicRouteTable(topic, topicRouteData); + } + } catch (RemotingException e) { + log.error("updateTopicRouteInfoFromNameServer Exception", e); + } catch (MQBrokerException e) { + log.error("updateTopicRouteInfoFromNameServer Exception", e); + if (!NamespaceUtil.isRetryTopic(topic) + && ResponseCode.TOPIC_NOT_EXIST == e.getResponseCode()) { + // clean no used topic + cleanNoneRouteTopic(topic); + } + } finally { + this.lockNamesrv.unlock(); + } + } + } catch (InterruptedException e) { + log.warn("updateTopicRouteInfoFromNameServer Exception", e); + } + } + + private boolean updateTopicRouteTable(String topic, TopicRouteData topicRouteData) { + TopicRouteData old = this.topicRouteTable.get(topic); + boolean changed = topicRouteData.topicRouteDataChanged(old); + if (!changed) { + if (!this.isNeedUpdateTopicRouteInfo(topic)) { + return false; + } + } else { + log.info("the topic[{}] route info changed, old[{}] ,new[{}]", topic, old, topicRouteData); + } + + for (BrokerData bd : topicRouteData.getBrokerDatas()) { + this.brokerAddrTable.put(bd.getBrokerName(), bd.getBrokerAddrs()); + } + + TopicPublishInfo publishInfo = MQClientInstance.topicRouteData2TopicPublishInfo(topic, topicRouteData); + publishInfo.setHaveTopicRouterInfo(true); + this.updateTopicPublishInfo(topic, publishInfo); + + TopicRouteData cloneTopicRouteData = new TopicRouteData(topicRouteData); + log.info("topicRouteTable.put. Topic = {}, TopicRouteData[{}]", topic, cloneTopicRouteData); + this.topicRouteTable.put(topic, cloneTopicRouteData); + + return true; + } + + private boolean updateSubscribeInfoTable(TopicRouteData topicRouteData, String topic) { + final TopicRouteData tmp = new TopicRouteData(topicRouteData); + tmp.setTopicQueueMappingByBroker(null); + Set newSubscribeInfo = MQClientInstance.topicRouteData2TopicSubscribeInfo(topic, tmp); + Set oldSubscribeInfo = topicSubscribeInfoTable.get(topic); + + if (Objects.equals(newSubscribeInfo, oldSubscribeInfo)) { + return false; + } + + log.info("the topic[{}] subscribe message queue changed, old[{}] ,new[{}]", topic, oldSubscribeInfo, newSubscribeInfo); + topicSubscribeInfoTable.put(topic, newSubscribeInfo); + return true; + + } + + private boolean isNeedUpdateTopicRouteInfo(final String topic) { + final TopicPublishInfo prev = this.topicPublishInfoTable.get(topic); + return null == prev || !prev.ok(); + } + + private void cleanNoneRouteTopic(String topic) { + // clean no used topic + topicSubscribeInfoTable.remove(topic); + } + + private void updateTopicPublishInfo(final String topic, final TopicPublishInfo info) { + if (info != null && topic != null) { + TopicPublishInfo prev = this.topicPublishInfoTable.put(topic, info); + if (prev != null) { + log.info("updateTopicPublishInfo prev is not null, " + prev); + } + } + } + + public void shutdown() { + if (null != this.scheduledExecutorService) { + this.scheduledExecutorService.shutdown(); + } + } + + public TopicPublishInfo tryToFindTopicPublishInfo(final String topic) { + TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic); + if (null == topicPublishInfo || !topicPublishInfo.ok()) { + this.updateTopicRouteInfoFromNameServer(topic, true, false); + topicPublishInfo = this.topicPublishInfoTable.get(topic); + } + return topicPublishInfo; + } + + public String findBrokerAddressInPublish(String brokerName) { + if (brokerName == null) { + return null; + } + Map map = this.brokerAddrTable.get(brokerName); + if (map != null && !map.isEmpty()) { + return map.get(MixAll.MASTER_ID); + } + + return null; + } + + public String findBrokerAddressInSubscribe( + final String brokerName, + final long brokerId, + final boolean onlyThisBroker + ) { + if (brokerName == null) { + return null; + } + String brokerAddr = null; + boolean found = false; + + Map map = this.brokerAddrTable.get(brokerName); + if (map != null && !map.isEmpty()) { + brokerAddr = map.get(brokerId); + boolean slave = brokerId != MixAll.MASTER_ID; + found = brokerAddr != null; + + if (!found && slave) { + brokerAddr = map.get(brokerId + 1); + found = brokerAddr != null; + } + + if (!found && !onlyThisBroker) { + Map.Entry entry = map.entrySet().iterator().next(); + brokerAddr = entry.getValue(); + found = true; + } + } + + return brokerAddr; + + } + + public Set getTopicSubscribeInfo(String topic) { + Set queues = topicSubscribeInfoTable.get(topic); + if (null == queues || queues.isEmpty()) { + this.updateTopicRouteInfoFromNameServer(topic, false, true); + queues = this.topicSubscribeInfoTable.get(topic); + } + return queues; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/transaction/AbstractTransactionalMessageCheckListener.java b/broker/src/main/java/org/apache/rocketmq/broker/transaction/AbstractTransactionalMessageCheckListener.java new file mode 100644 index 00000000000..b5a86d3d083 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/transaction/AbstractTransactionalMessageCheckListener.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.transaction; + +import io.netty.channel.Channel; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.header.CheckTransactionStateRequestHeader; + +public abstract class AbstractTransactionalMessageCheckListener { + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.TRANSACTION_LOGGER_NAME); + + private BrokerController brokerController; + + //queue nums of topic TRANS_CHECK_MAX_TIME_TOPIC + protected final static int TCMT_QUEUE_NUMS = 1; + + private static volatile ExecutorService executorService; + + public AbstractTransactionalMessageCheckListener() { + } + + public AbstractTransactionalMessageCheckListener(BrokerController brokerController) { + this.brokerController = brokerController; + } + + public void sendCheckMessage(MessageExt msgExt) throws Exception { + CheckTransactionStateRequestHeader checkTransactionStateRequestHeader = new CheckTransactionStateRequestHeader(); + checkTransactionStateRequestHeader.setCommitLogOffset(msgExt.getCommitLogOffset()); + checkTransactionStateRequestHeader.setOffsetMsgId(msgExt.getMsgId()); + checkTransactionStateRequestHeader.setMsgId(msgExt.getUserProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX)); + checkTransactionStateRequestHeader.setTransactionId(checkTransactionStateRequestHeader.getMsgId()); + checkTransactionStateRequestHeader.setTranStateTableOffset(msgExt.getQueueOffset()); + checkTransactionStateRequestHeader.setBrokerName(brokerController.getBrokerConfig().getBrokerName()); + msgExt.setTopic(msgExt.getUserProperty(MessageConst.PROPERTY_REAL_TOPIC)); + msgExt.setQueueId(Integer.parseInt(msgExt.getUserProperty(MessageConst.PROPERTY_REAL_QUEUE_ID))); + msgExt.setStoreSize(0); + String groupId = msgExt.getProperty(MessageConst.PROPERTY_PRODUCER_GROUP); + Channel channel = brokerController.getProducerManager().getAvailableChannel(groupId); + if (channel != null) { + brokerController.getBroker2Client().checkProducerTransactionState(groupId, channel, checkTransactionStateRequestHeader, msgExt); + } else { + LOGGER.warn("Check transaction failed, channel is null. groupId={}", groupId); + } + } + + public void resolveHalfMsg(final MessageExt msgExt) { + if (executorService != null) { + executorService.execute(new Runnable() { + @Override + public void run() { + try { + sendCheckMessage(msgExt); + } catch (Exception e) { + LOGGER.error("Send check message error!", e); + } + } + }); + } else { + LOGGER.error("TransactionalMessageCheckListener not init"); + } + } + + public BrokerController getBrokerController() { + return brokerController; + } + + public void shutDown() { + if (executorService != null) { + executorService.shutdown(); + } + } + + public synchronized void initExecutorService() { + if (executorService == null) { + executorService = ThreadUtils.newThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2000), + new ThreadFactoryImpl("Transaction-msg-check-thread", brokerController.getBrokerIdentity()), new CallerRunsPolicy()); + } + } + + /** + * Inject brokerController for this listener + * + * @param brokerController + */ + public void setBrokerController(BrokerController brokerController) { + this.brokerController = brokerController; + initExecutorService(); + } + + /** + * In order to avoid check back unlimited, we will discard the message that have been checked more than a certain + * number of times. + * + * @param msgExt Message to be discarded. + */ + public abstract void resolveDiscardMsg(MessageExt msgExt); +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/transaction/OperationResult.java b/broker/src/main/java/org/apache/rocketmq/broker/transaction/OperationResult.java new file mode 100644 index 00000000000..d23b7617e15 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/transaction/OperationResult.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.transaction; + +import org.apache.rocketmq.common.message.MessageExt; + +public class OperationResult { + private MessageExt prepareMessage; + + private int responseCode; + + private String responseRemark; + + public void setPrepareMessage(MessageExt prepareMessage) { + this.prepareMessage = prepareMessage; + } + + public int getResponseCode() { + return responseCode; + } + + public void setResponseCode(int responseCode) { + this.responseCode = responseCode; + } + + public String getResponseRemark() { + return responseRemark; + } + + public void setResponseRemark(String responseRemark) { + this.responseRemark = responseRemark; + } + + public MessageExt getPrepareMessage() { + return prepareMessage; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/transaction/TransactionMetrics.java b/broker/src/main/java/org/apache/rocketmq/broker/transaction/TransactionMetrics.java new file mode 100644 index 00000000000..ad30c73c608 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/transaction/TransactionMetrics.java @@ -0,0 +1,259 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.transaction; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.serializer.SerializerFeature; +import com.google.common.io.Files; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.rocketmq.common.ConfigManager; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class TransactionMetrics extends ConfigManager { + private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + + private ConcurrentMap transactionCounts = + new ConcurrentHashMap<>(1024); + + private DataVersion dataVersion = new DataVersion(); + + private final String configPath; + + public TransactionMetrics(String configPath) { + this.configPath = configPath; + } + + public long addAndGet(String topic, int value) { + Metric pair = getTopicPair(topic); + getDataVersion().nextVersion(); + pair.setTimeStamp(System.currentTimeMillis()); + return pair.getCount().addAndGet(value); + } + + public Metric getTopicPair(String topic) { + Metric pair = transactionCounts.get(topic); + if (null != pair) { + return pair; + } + pair = new Metric(); + final Metric previous = transactionCounts.putIfAbsent(topic, pair); + if (null != previous) { + return previous; + } + return pair; + } + public long getTransactionCount(String topic) { + Metric pair = transactionCounts.get(topic); + if (null == pair) { + return 0; + } else { + return pair.getCount().get(); + } + } + + public Map getTransactionCounts() { + return transactionCounts; + } + public void setTransactionCounts(ConcurrentMap transactionCounts) { + this.transactionCounts = transactionCounts; + } + + protected void write0(Writer writer) { + TransactionMetricsSerializeWrapper wrapper = new TransactionMetricsSerializeWrapper(); + wrapper.setTransactionCount(transactionCounts); + wrapper.setDataVersion(dataVersion); + JSON.writeJSONString(writer, wrapper, SerializerFeature.BrowserCompatible); + } + + @Override + public String encode() { + return encode(false); + } + + @Override + public String configFilePath() { + return configPath; + } + + @Override + public void decode(String jsonString) { + if (jsonString != null) { + TransactionMetricsSerializeWrapper transactionMetricsSerializeWrapper = + TransactionMetricsSerializeWrapper.fromJson(jsonString, TransactionMetricsSerializeWrapper.class); + if (transactionMetricsSerializeWrapper != null) { + this.transactionCounts.putAll(transactionMetricsSerializeWrapper.getTransactionCount()); + this.dataVersion.assignNewOne(transactionMetricsSerializeWrapper.getDataVersion()); + } + } + } + + @Override + public String encode(boolean prettyFormat) { + TransactionMetricsSerializeWrapper metricsSerializeWrapper = new TransactionMetricsSerializeWrapper(); + metricsSerializeWrapper.setDataVersion(this.dataVersion); + metricsSerializeWrapper.setTransactionCount(this.transactionCounts); + return metricsSerializeWrapper.toJson(prettyFormat); + } + + public DataVersion getDataVersion() { + return dataVersion; + } + + public void setDataVersion(DataVersion dataVersion) { + this.dataVersion = dataVersion; + } + + public void cleanMetrics(Set topics) { + if (topics == null || topics.isEmpty()) { + return; + } + Iterator> iterator = transactionCounts.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + final String topic = entry.getKey(); + if (topic.startsWith(TopicValidator.SYSTEM_TOPIC_PREFIX)) { + continue; + } + if (!topics.contains(topic)) { + continue; + } + // in the input topics set, then remove it. + iterator.remove(); + } + } + + public static class TransactionMetricsSerializeWrapper extends RemotingSerializable { + private ConcurrentMap transactionCount = + new ConcurrentHashMap<>(1024); + private DataVersion dataVersion = new DataVersion(); + + public ConcurrentMap getTransactionCount() { + return transactionCount; + } + + public void setTransactionCount( + ConcurrentMap transactionCount) { + this.transactionCount = transactionCount; + } + + public DataVersion getDataVersion() { + return dataVersion; + } + + public void setDataVersion(DataVersion dataVersion) { + this.dataVersion = dataVersion; + } + } + + @Override + public synchronized void persist() { + String config = configFilePath(); + String temp = config + ".tmp"; + String backup = config + ".bak"; + BufferedWriter bufferedWriter = null; + try { + File tmpFile = new File(temp); + File parentDirectory = tmpFile.getParentFile(); + if (!parentDirectory.exists()) { + if (!parentDirectory.mkdirs()) { + log.error("Failed to create directory: {}", parentDirectory.getCanonicalPath()); + return; + } + } + + if (!tmpFile.exists()) { + if (!tmpFile.createNewFile()) { + log.error("Failed to create file: {}", tmpFile.getCanonicalPath()); + return; + } + } + bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(tmpFile, false), + StandardCharsets.UTF_8)); + write0(bufferedWriter); + bufferedWriter.flush(); + bufferedWriter.close(); + log.debug("Finished writing tmp file: {}", temp); + + File configFile = new File(config); + if (configFile.exists()) { + Files.copy(configFile, new File(backup)); + configFile.delete(); + } + + tmpFile.renameTo(configFile); + } catch (IOException e) { + log.error("Failed to persist {}", temp, e); + } finally { + if (null != bufferedWriter) { + try { + bufferedWriter.close(); + } catch (IOException ignore) { + } + } + } + } + + public static class Metric { + private AtomicLong count; + private long timeStamp; + + public Metric() { + count = new AtomicLong(0); + timeStamp = System.currentTimeMillis(); + } + + public AtomicLong getCount() { + return count; + } + + public void setCount(AtomicLong count) { + this.count = count; + } + + public long getTimeStamp() { + return timeStamp; + } + + public void setTimeStamp(long timeStamp) { + this.timeStamp = timeStamp; + } + + @Override + public String toString() { + return String.format("[%d,%d]", count.get(), timeStamp); + } + } + +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/transaction/TransactionMetricsFlushService.java b/broker/src/main/java/org/apache/rocketmq/broker/transaction/TransactionMetricsFlushService.java new file mode 100644 index 00000000000..948f9fbc8ed --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/transaction/TransactionMetricsFlushService.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.transaction; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class TransactionMetricsFlushService extends ServiceThread { + private static final Logger log = LoggerFactory.getLogger(LoggerName.TRANSACTION_LOGGER_NAME); + private BrokerController brokerController; + public TransactionMetricsFlushService(BrokerController brokerController) { + this.brokerController = brokerController; + } + + @Override + public String getServiceName() { + return "TransactionFlushService"; + } + + @Override + public void run() { + log.info(this.getServiceName() + " service start"); + long start = System.currentTimeMillis(); + while (!this.isStopped()) { + try { + if (System.currentTimeMillis() - start > brokerController.getBrokerConfig().getTransactionMetricFlushInterval()) { + start = System.currentTimeMillis(); + brokerController.getTransactionalMessageService().getTransactionMetrics().persist(); + waitForRunning(brokerController.getBrokerConfig().getTransactionMetricFlushInterval()); + } + } catch (Throwable e) { + log.error("Error occurred in " + getServiceName(), e); + } + } + log.info(this.getServiceName() + " service end"); + } +} \ No newline at end of file diff --git a/broker/src/main/java/org/apache/rocketmq/broker/transaction/TransactionRecord.java b/broker/src/main/java/org/apache/rocketmq/broker/transaction/TransactionRecord.java deleted file mode 100644 index 5f81ebb3303..00000000000 --- a/broker/src/main/java/org/apache/rocketmq/broker/transaction/TransactionRecord.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.broker.transaction; - -public class TransactionRecord { - // Commit Log Offset - private long offset; - private String producerGroup; - - public long getOffset() { - return offset; - } - - public void setOffset(long offset) { - this.offset = offset; - } - - public String getProducerGroup() { - return producerGroup; - } - - public void setProducerGroup(String producerGroup) { - this.producerGroup = producerGroup; - } -} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/transaction/TransactionStore.java b/broker/src/main/java/org/apache/rocketmq/broker/transaction/TransactionStore.java deleted file mode 100644 index 3decc01f5eb..00000000000 --- a/broker/src/main/java/org/apache/rocketmq/broker/transaction/TransactionStore.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.broker.transaction; - -import java.util.List; - -public interface TransactionStore { - boolean open(); - - void close(); - - boolean put(final List trs); - - void remove(final List pks); - - List traverse(final long pk, final int nums); - - long totalRecords(); - - long minPK(); - - long maxPK(); -} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/transaction/TransactionalMessageCheckService.java b/broker/src/main/java/org/apache/rocketmq/broker/transaction/TransactionalMessageCheckService.java new file mode 100644 index 00000000000..52209c3fbdb --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/transaction/TransactionalMessageCheckService.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.transaction; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class TransactionalMessageCheckService extends ServiceThread { + private static final Logger log = LoggerFactory.getLogger(LoggerName.TRANSACTION_LOGGER_NAME); + + private BrokerController brokerController; + + public TransactionalMessageCheckService(BrokerController brokerController) { + this.brokerController = brokerController; + } + + @Override + public String getServiceName() { + if (brokerController != null && brokerController.getBrokerConfig().isInBrokerContainer()) { + return brokerController.getBrokerIdentity().getIdentifier() + TransactionalMessageCheckService.class.getSimpleName(); + } + return TransactionalMessageCheckService.class.getSimpleName(); + } + + @Override + public void run() { + log.info("Start transaction check service thread!"); + while (!this.isStopped()) { + long checkInterval = brokerController.getBrokerConfig().getTransactionCheckInterval(); + this.waitForRunning(checkInterval); + } + log.info("End transaction check service thread!"); + } + + @Override + protected void onWaitEnd() { + long timeout = brokerController.getBrokerConfig().getTransactionTimeOut(); + int checkMax = brokerController.getBrokerConfig().getTransactionCheckMax(); + long begin = System.currentTimeMillis(); + log.info("Begin to check prepare message, begin time:{}", begin); + this.brokerController.getTransactionalMessageService().check(timeout, checkMax, this.brokerController.getTransactionalMessageCheckListener()); + log.info("End to check prepare message, consumed time:{}", System.currentTimeMillis() - begin); + } + +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/transaction/TransactionalMessageService.java b/broker/src/main/java/org/apache/rocketmq/broker/transaction/TransactionalMessageService.java new file mode 100644 index 00000000000..849e64024b5 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/transaction/TransactionalMessageService.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.transaction; + +import java.util.concurrent.CompletableFuture; + +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.remoting.protocol.header.EndTransactionRequestHeader; +import org.apache.rocketmq.store.PutMessageResult; + +public interface TransactionalMessageService { + + /** + * Process prepare message, in common, we should put this message to storage service. + * + * @param messageInner Prepare(Half) message. + * @return Prepare message storage result. + */ + PutMessageResult prepareMessage(MessageExtBrokerInner messageInner); + + /** + * Process prepare message in async manner, we should put this message to storage service + * + * @param messageInner Prepare(Half) message. + * @return CompletableFuture of put result, will be completed at put success(flush and replica done) + */ + CompletableFuture asyncPrepareMessage(MessageExtBrokerInner messageInner); + + /** + * Delete prepare message when this message has been committed or rolled back. + * + * @param messageExt + */ + boolean deletePrepareMessage(MessageExt messageExt); + + /** + * Invoked to process commit prepare message. + * + * @param requestHeader Commit message request header. + * @return Operate result contains prepare message and relative error code. + */ + OperationResult commitMessage(EndTransactionRequestHeader requestHeader); + + /** + * Invoked to roll back prepare message. + * + * @param requestHeader Prepare message request header. + * @return Operate result contains prepare message and relative error code. + */ + OperationResult rollbackMessage(EndTransactionRequestHeader requestHeader); + + /** + * Traverse uncommitted/unroll back half message and send check back request to producer to obtain transaction + * status. + * + * @param transactionTimeout The minimum time of the transactional message to be checked firstly, one message only + * exceed this time interval that can be checked. + * @param transactionCheckMax The maximum number of times the message was checked, if exceed this value, this + * message will be discarded. + * @param listener When the message is considered to be checked or discarded, the relative method of this class will + * be invoked. + */ + void check(long transactionTimeout, int transactionCheckMax, AbstractTransactionalMessageCheckListener listener); + + /** + * Open transaction service. + * + * @return If open success, return true. + */ + boolean open(); + + /** + * Close transaction service. + */ + void close(); + + TransactionMetrics getTransactionMetrics(); + + void setTransactionMetrics(TransactionMetrics transactionMetrics); +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/transaction/jdbc/JDBCTransactionStore.java b/broker/src/main/java/org/apache/rocketmq/broker/transaction/jdbc/JDBCTransactionStore.java deleted file mode 100644 index 056d93902fc..00000000000 --- a/broker/src/main/java/org/apache/rocketmq/broker/transaction/jdbc/JDBCTransactionStore.java +++ /dev/null @@ -1,242 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.broker.transaction.jdbc; - -import java.net.URL; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.List; -import java.util.Properties; -import java.util.concurrent.atomic.AtomicLong; -import org.apache.rocketmq.broker.transaction.TransactionRecord; -import org.apache.rocketmq.broker.transaction.TransactionStore; -import org.apache.rocketmq.common.MixAll; -import org.apache.rocketmq.common.constant.LoggerName; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class JDBCTransactionStore implements TransactionStore { - private static final Logger log = LoggerFactory.getLogger(LoggerName.TRANSACTION_LOGGER_NAME); - private final JDBCTransactionStoreConfig jdbcTransactionStoreConfig; - private Connection connection; - private AtomicLong totalRecordsValue = new AtomicLong(0); - - public JDBCTransactionStore(JDBCTransactionStoreConfig jdbcTransactionStoreConfig) { - this.jdbcTransactionStoreConfig = jdbcTransactionStoreConfig; - } - - @Override - public boolean open() { - if (this.loadDriver()) { - Properties props = new Properties(); - props.put("user", jdbcTransactionStoreConfig.getJdbcUser()); - props.put("password", jdbcTransactionStoreConfig.getJdbcPassword()); - - try { - this.connection = - DriverManager.getConnection(this.jdbcTransactionStoreConfig.getJdbcURL(), props); - - this.connection.setAutoCommit(false); - - if (!this.computeTotalRecords()) { - return this.createDB(); - } - - return true; - } catch (SQLException e) { - log.info("Create JDBC Connection Exception", e); - } - } - - return false; - } - - private boolean loadDriver() { - try { - Class.forName(this.jdbcTransactionStoreConfig.getJdbcDriverClass()).newInstance(); - log.info("Loaded the appropriate driver, {}", - this.jdbcTransactionStoreConfig.getJdbcDriverClass()); - return true; - } catch (Exception e) { - log.info("Loaded the appropriate driver Exception", e); - } - - return false; - } - - private boolean computeTotalRecords() { - Statement statement = null; - ResultSet resultSet = null; - try { - statement = this.connection.createStatement(); - - resultSet = statement.executeQuery("select count(offset) as total from t_transaction"); - if (!resultSet.next()) { - log.warn("computeTotalRecords ResultSet is empty"); - return false; - } - - this.totalRecordsValue.set(resultSet.getLong(1)); - } catch (Exception e) { - log.warn("computeTotalRecords Exception", e); - return false; - } finally { - if (null != statement) { - try { - statement.close(); - } catch (SQLException e) { - } - } - - if (null != resultSet) { - try { - resultSet.close(); - } catch (SQLException e) { - } - } - } - - return true; - } - - private boolean createDB() { - Statement statement = null; - try { - statement = this.connection.createStatement(); - - String sql = this.createTableSql(); - log.info("createDB SQL:\n {}", sql); - statement.execute(sql); - this.connection.commit(); - return true; - } catch (Exception e) { - log.warn("createDB Exception", e); - return false; - } finally { - if (null != statement) { - try { - statement.close(); - } catch (SQLException e) { - log.warn("Close statement exception", e); - } - } - } - } - - private String createTableSql() { - URL resource = JDBCTransactionStore.class.getClassLoader().getResource("transaction.sql"); - String fileContent = MixAll.file2String(resource); - return fileContent; - } - - @Override - public void close() { - try { - if (this.connection != null) { - this.connection.close(); - } - } catch (SQLException e) { - } - } - - @Override - public boolean put(List trs) { - PreparedStatement statement = null; - try { - this.connection.setAutoCommit(false); - statement = this.connection.prepareStatement("insert into t_transaction values (?, ?)"); - for (TransactionRecord tr : trs) { - statement.setLong(1, tr.getOffset()); - statement.setString(2, tr.getProducerGroup()); - statement.addBatch(); - } - int[] executeBatch = statement.executeBatch(); - this.connection.commit(); - this.totalRecordsValue.addAndGet(updatedRows(executeBatch)); - return true; - } catch (Exception e) { - log.warn("createDB Exception", e); - return false; - } finally { - if (null != statement) { - try { - statement.close(); - } catch (SQLException e) { - log.warn("Close statement exception", e); - } - } - } - } - - private long updatedRows(int[] rows) { - long res = 0; - for (int i : rows) { - res += i; - } - - return res; - } - - @Override - public void remove(List pks) { - PreparedStatement statement = null; - try { - this.connection.setAutoCommit(false); - statement = this.connection.prepareStatement("DELETE FROM t_transaction WHERE offset = ?"); - for (long pk : pks) { - statement.setLong(1, pk); - statement.addBatch(); - } - int[] executeBatch = statement.executeBatch(); - this.connection.commit(); - } catch (Exception e) { - log.warn("createDB Exception", e); - } finally { - if (null != statement) { - try { - statement.close(); - } catch (SQLException e) { - } - } - } - } - - @Override - public List traverse(long pk, int nums) { - return null; - } - - @Override - public long totalRecords() { - return this.totalRecordsValue.get(); - } - - @Override - public long minPK() { - return 0; - } - - @Override - public long maxPK() { - return 0; - } -} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/transaction/jdbc/JDBCTransactionStoreConfig.java b/broker/src/main/java/org/apache/rocketmq/broker/transaction/jdbc/JDBCTransactionStoreConfig.java deleted file mode 100644 index 4b079597c4d..00000000000 --- a/broker/src/main/java/org/apache/rocketmq/broker/transaction/jdbc/JDBCTransactionStoreConfig.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.broker.transaction.jdbc; - -public class JDBCTransactionStoreConfig { - private String jdbcDriverClass = "com.mysql.jdbc.Driver"; - private String jdbcURL = "jdbc:mysql://xxx.xxx.xxx.xxx:1000/xxx?useUnicode=true&characterEncoding=UTF-8"; - private String jdbcUser = "xxx"; - private String jdbcPassword = "xxx"; - - public String getJdbcDriverClass() { - return jdbcDriverClass; - } - - public void setJdbcDriverClass(String jdbcDriverClass) { - this.jdbcDriverClass = jdbcDriverClass; - } - - public String getJdbcURL() { - return jdbcURL; - } - - public void setJdbcURL(String jdbcURL) { - this.jdbcURL = jdbcURL; - } - - public String getJdbcUser() { - return jdbcUser; - } - - public void setJdbcUser(String jdbcUser) { - this.jdbcUser = jdbcUser; - } - - public String getJdbcPassword() { - return jdbcPassword; - } - - public void setJdbcPassword(String jdbcPassword) { - this.jdbcPassword = jdbcPassword; - } -} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/DefaultTransactionalMessageCheckListener.java b/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/DefaultTransactionalMessageCheckListener.java new file mode 100644 index 00000000000..8e2b679b405 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/DefaultTransactionalMessageCheckListener.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.transaction.queue; + +import org.apache.rocketmq.broker.transaction.AbstractTransactionalMessageCheckListener; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.constant.PermName; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; + +import java.util.concurrent.ThreadLocalRandom; + +public class DefaultTransactionalMessageCheckListener extends AbstractTransactionalMessageCheckListener { + private static final Logger log = LoggerFactory.getLogger(LoggerName.TRANSACTION_LOGGER_NAME); + + public DefaultTransactionalMessageCheckListener() { + super(); + } + + @Override + public void resolveDiscardMsg(MessageExt msgExt) { + log.error("MsgExt:{} has been checked too many times, so discard it by moving it to system topic TRANS_CHECK_MAXTIME_TOPIC", msgExt); + + try { + MessageExtBrokerInner brokerInner = toMessageExtBrokerInner(msgExt); + PutMessageResult putMessageResult = this.getBrokerController().getMessageStore().putMessage(brokerInner); + if (putMessageResult != null && putMessageResult.getPutMessageStatus() == PutMessageStatus.PUT_OK) { + log.info("Put checked-too-many-time half message to TRANS_CHECK_MAXTIME_TOPIC OK. Restored in queueOffset={}, " + + "commitLogOffset={}, real topic={}", msgExt.getQueueOffset(), msgExt.getCommitLogOffset(), msgExt.getUserProperty(MessageConst.PROPERTY_REAL_TOPIC)); + // discarded, then the num of half-messages minus 1 + this.getBrokerController().getTransactionalMessageService().getTransactionMetrics().addAndGet(msgExt.getUserProperty(MessageConst.PROPERTY_REAL_TOPIC), -1); + } else { + log.error("Put checked-too-many-time half message to TRANS_CHECK_MAXTIME_TOPIC failed, real topic={}, msgId={}", msgExt.getTopic(), msgExt.getMsgId()); + } + } catch (Exception e) { + log.warn("Put checked-too-many-time message to TRANS_CHECK_MAXTIME_TOPIC error. {}", e); + } + + } + + private MessageExtBrokerInner toMessageExtBrokerInner(MessageExt msgExt) { + TopicConfig topicConfig = this.getBrokerController().getTopicConfigManager().createTopicOfTranCheckMaxTime(TCMT_QUEUE_NUMS, PermName.PERM_READ | PermName.PERM_WRITE); + int queueId = ThreadLocalRandom.current().nextInt(99999999) % TCMT_QUEUE_NUMS; + MessageExtBrokerInner inner = new MessageExtBrokerInner(); + inner.setTopic(topicConfig.getTopicName()); + inner.setBody(msgExt.getBody()); + inner.setFlag(msgExt.getFlag()); + MessageAccessor.setProperties(inner, msgExt.getProperties()); + inner.setPropertiesString(MessageDecoder.messageProperties2String(msgExt.getProperties())); + inner.setTagsCode(MessageExtBrokerInner.tagsString2tagsCode(msgExt.getTags())); + inner.setQueueId(queueId); + inner.setSysFlag(msgExt.getSysFlag()); + inner.setBornHost(msgExt.getBornHost()); + inner.setBornTimestamp(msgExt.getBornTimestamp()); + inner.setStoreHost(msgExt.getStoreHost()); + inner.setReconsumeTimes(msgExt.getReconsumeTimes()); + inner.setMsgId(msgExt.getMsgId()); + inner.setWaitStoreMsgOK(false); + return inner; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/GetResult.java b/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/GetResult.java new file mode 100644 index 00000000000..a78970e83ad --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/GetResult.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.transaction.queue; + +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.common.message.MessageExt; + +public class GetResult { + private MessageExt msg; + private PullResult pullResult; + + public MessageExt getMsg() { + return msg; + } + + public void setMsg(MessageExt msg) { + this.msg = msg; + } + + public PullResult getPullResult() { + return pullResult; + } + + public void setPullResult(PullResult pullResult) { + this.pullResult = pullResult; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/MessageQueueOpContext.java b/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/MessageQueueOpContext.java new file mode 100644 index 00000000000..e8e5f13de6b --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/MessageQueueOpContext.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.transaction.queue; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; + +public class MessageQueueOpContext { + private AtomicInteger totalSize = new AtomicInteger(0); + private volatile long lastWriteTimestamp; + private LinkedBlockingQueue contextQueue; + + public MessageQueueOpContext(long timestamp, int queueLength) { + this.lastWriteTimestamp = timestamp; + contextQueue = new LinkedBlockingQueue(queueLength); + } + + public LinkedBlockingQueue getContextQueue() { + return contextQueue; + } + + + public AtomicInteger getTotalSize() { + return totalSize; + } + + + public long getLastWriteTimestamp() { + return lastWriteTimestamp; + } + + + public void setLastWriteTimestamp(long lastWriteTimestamp) { + this.lastWriteTimestamp = lastWriteTimestamp; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageBridge.java b/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageBridge.java new file mode 100644 index 00000000000..2383f4f917c --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageBridge.java @@ -0,0 +1,364 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.transaction.queue; + +import io.opentelemetry.api.common.Attributes; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.metrics.BrokerMetricsManager; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.consumer.PullStatus; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.constant.PermName; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; + +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_CONSUMER_GROUP; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_IS_SYSTEM; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_TOPIC; + +public class TransactionalMessageBridge { + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.TRANSACTION_LOGGER_NAME); + + private final ConcurrentHashMap opQueueMap = new ConcurrentHashMap<>(); + private final BrokerController brokerController; + private final MessageStore store; + private final SocketAddress storeHost; + + public TransactionalMessageBridge(BrokerController brokerController, MessageStore store) { + try { + this.brokerController = brokerController; + this.store = store; + this.storeHost = + new InetSocketAddress(brokerController.getBrokerConfig().getBrokerIP1(), + brokerController.getNettyServerConfig().getListenPort()); + } catch (Exception e) { + LOGGER.error("Init TransactionBridge error", e); + throw new RuntimeException(e); + } + + } + + public long fetchConsumeOffset(MessageQueue mq) { + long offset = brokerController.getConsumerOffsetManager().queryOffset(TransactionalMessageUtil.buildConsumerGroup(), + mq.getTopic(), mq.getQueueId()); + if (offset == -1) { + offset = store.getMinOffsetInQueue(mq.getTopic(), mq.getQueueId()); + } + return offset; + } + + public Set fetchMessageQueues(String topic) { + Set mqSet = new HashSet<>(); + TopicConfig topicConfig = selectTopicConfig(topic); + if (topicConfig != null && topicConfig.getReadQueueNums() > 0) { + for (int i = 0; i < topicConfig.getReadQueueNums(); i++) { + MessageQueue mq = new MessageQueue(); + mq.setTopic(topic); + mq.setBrokerName(brokerController.getBrokerConfig().getBrokerName()); + mq.setQueueId(i); + mqSet.add(mq); + } + } + return mqSet; + } + + public void updateConsumeOffset(MessageQueue mq, long offset) { + this.brokerController.getConsumerOffsetManager().commitOffset( + RemotingHelper.parseSocketAddressAddr(this.storeHost), TransactionalMessageUtil.buildConsumerGroup(), mq.getTopic(), + mq.getQueueId(), offset); + } + + public PullResult getHalfMessage(int queueId, long offset, int nums) { + String group = TransactionalMessageUtil.buildConsumerGroup(); + String topic = TransactionalMessageUtil.buildHalfTopic(); + SubscriptionData sub = new SubscriptionData(topic, "*"); + return getMessage(group, topic, queueId, offset, nums, sub); + } + + public PullResult getOpMessage(int queueId, long offset, int nums) { + String group = TransactionalMessageUtil.buildConsumerGroup(); + String topic = TransactionalMessageUtil.buildOpTopic(); + SubscriptionData sub = new SubscriptionData(topic, "*"); + return getMessage(group, topic, queueId, offset, nums, sub); + } + + private PullResult getMessage(String group, String topic, int queueId, long offset, int nums, + SubscriptionData sub) { + GetMessageResult getMessageResult = store.getMessage(group, topic, queueId, offset, nums, null); + + if (getMessageResult != null) { + PullStatus pullStatus = PullStatus.NO_NEW_MSG; + List foundList = null; + switch (getMessageResult.getStatus()) { + case FOUND: + pullStatus = PullStatus.FOUND; + foundList = decodeMsgList(getMessageResult); + this.brokerController.getBrokerStatsManager().incGroupGetNums(group, topic, + getMessageResult.getMessageCount()); + this.brokerController.getBrokerStatsManager().incGroupGetSize(group, topic, + getMessageResult.getBufferTotalSize()); + this.brokerController.getBrokerStatsManager().incBrokerGetNums(topic, getMessageResult.getMessageCount()); + if (foundList == null || foundList.size() == 0) { + break; + } + this.brokerController.getBrokerStatsManager().recordDiskFallBehindTime(group, topic, queueId, + this.brokerController.getMessageStore().now() - foundList.get(foundList.size() - 1) + .getStoreTimestamp()); + + Attributes attributes = BrokerMetricsManager.newAttributesBuilder() + .put(LABEL_TOPIC, topic) + .put(LABEL_CONSUMER_GROUP, group) + .put(LABEL_IS_SYSTEM, TopicValidator.isSystemTopic(topic) || MixAll.isSysConsumerGroup(group)) + .build(); + BrokerMetricsManager.messagesOutTotal.add(getMessageResult.getMessageCount(), attributes); + BrokerMetricsManager.throughputOutTotal.add(getMessageResult.getBufferTotalSize(), attributes); + + break; + case NO_MATCHED_MESSAGE: + pullStatus = PullStatus.NO_MATCHED_MSG; + LOGGER.warn("No matched message. GetMessageStatus={}, topic={}, groupId={}, requestOffset={}", + getMessageResult.getStatus(), topic, group, offset); + break; + case NO_MESSAGE_IN_QUEUE: + case OFFSET_OVERFLOW_ONE: + pullStatus = PullStatus.NO_NEW_MSG; + LOGGER.warn("No new message. GetMessageStatus={}, topic={}, groupId={}, requestOffset={}", + getMessageResult.getStatus(), topic, group, offset); + break; + case MESSAGE_WAS_REMOVING: + case NO_MATCHED_LOGIC_QUEUE: + case OFFSET_FOUND_NULL: + case OFFSET_OVERFLOW_BADLY: + case OFFSET_TOO_SMALL: + pullStatus = PullStatus.OFFSET_ILLEGAL; + LOGGER.warn("Offset illegal. GetMessageStatus={}, topic={}, groupId={}, requestOffset={}", + getMessageResult.getStatus(), topic, group, offset); + break; + default: + assert false; + break; + } + + return new PullResult(pullStatus, getMessageResult.getNextBeginOffset(), getMessageResult.getMinOffset(), + getMessageResult.getMaxOffset(), foundList); + + } else { + LOGGER.error("Get message from store return null. topic={}, groupId={}, requestOffset={}", topic, group, + offset); + return null; + } + } + + private List decodeMsgList(GetMessageResult getMessageResult) { + List foundList = new ArrayList<>(); + try { + List messageBufferList = getMessageResult.getMessageBufferList(); + for (ByteBuffer bb : messageBufferList) { + MessageExt msgExt = MessageDecoder.decode(bb, true, false); + if (msgExt != null) { + foundList.add(msgExt); + } + } + + } finally { + getMessageResult.release(); + } + + return foundList; + } + + public PutMessageResult putHalfMessage(MessageExtBrokerInner messageInner) { + return store.putMessage(parseHalfMessageInner(messageInner)); + } + + public CompletableFuture asyncPutHalfMessage(MessageExtBrokerInner messageInner) { + return store.asyncPutMessage(parseHalfMessageInner(messageInner)); + } + + private MessageExtBrokerInner parseHalfMessageInner(MessageExtBrokerInner msgInner) { + String uniqId = msgInner.getUserProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX); + if (uniqId != null && !uniqId.isEmpty()) { + MessageAccessor.putProperty(msgInner, TransactionalMessageUtil.TRANSACTION_ID, uniqId); + } + MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_TOPIC, msgInner.getTopic()); + MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_QUEUE_ID, + String.valueOf(msgInner.getQueueId())); + msgInner.setSysFlag( + MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), MessageSysFlag.TRANSACTION_NOT_TYPE)); + msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic()); + msgInner.setQueueId(0); + msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); + return msgInner; + } + + public PutMessageResult putMessageReturnResult(MessageExtBrokerInner messageInner) { + LOGGER.debug("[BUG-TO-FIX] Thread:{} msgID:{}", Thread.currentThread().getName(), messageInner.getMsgId()); + PutMessageResult result = store.putMessage(messageInner); + if (result != null && result.getPutMessageStatus() == PutMessageStatus.PUT_OK) { + this.brokerController.getBrokerStatsManager().incTopicPutNums(messageInner.getTopic()); + this.brokerController.getBrokerStatsManager().incTopicPutSize(messageInner.getTopic(), + result.getAppendMessageResult().getWroteBytes()); + this.brokerController.getBrokerStatsManager().incBrokerPutNums(); + } + return result; + } + + public boolean putMessage(MessageExtBrokerInner messageInner) { + PutMessageResult putMessageResult = store.putMessage(messageInner); + if (putMessageResult != null + && putMessageResult.getPutMessageStatus() == PutMessageStatus.PUT_OK) { + return true; + } else { + LOGGER.error("Put message failed, topic: {}, queueId: {}, msgId: {}", + messageInner.getTopic(), messageInner.getQueueId(), messageInner.getMsgId()); + return false; + } + } + + public MessageExtBrokerInner renewImmunityHalfMessageInner(MessageExt msgExt) { + MessageExtBrokerInner msgInner = renewHalfMessageInner(msgExt); + String queueOffsetFromPrepare = msgExt.getUserProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED_QUEUE_OFFSET); + if (null != queueOffsetFromPrepare) { + MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_TRANSACTION_PREPARED_QUEUE_OFFSET, + queueOffsetFromPrepare); + } else { + MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_TRANSACTION_PREPARED_QUEUE_OFFSET, + String.valueOf(msgExt.getQueueOffset())); + } + + msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); + + return msgInner; + } + + public MessageExtBrokerInner renewHalfMessageInner(MessageExt msgExt) { + MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); + msgInner.setTopic(msgExt.getTopic()); + msgInner.setBody(msgExt.getBody()); + msgInner.setQueueId(msgExt.getQueueId()); + msgInner.setMsgId(msgExt.getMsgId()); + msgInner.setSysFlag(msgExt.getSysFlag()); + msgInner.setTags(msgExt.getTags()); + msgInner.setTagsCode(MessageExtBrokerInner.tagsString2tagsCode(msgInner.getTags())); + MessageAccessor.setProperties(msgInner, msgExt.getProperties()); + msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgExt.getProperties())); + msgInner.setBornTimestamp(msgExt.getBornTimestamp()); + msgInner.setBornHost(msgExt.getBornHost()); + msgInner.setStoreHost(msgExt.getStoreHost()); + msgInner.setWaitStoreMsgOK(false); + return msgInner; + } + + private MessageExtBrokerInner makeOpMessageInner(Message message, MessageQueue messageQueue) { + MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); + msgInner.setTopic(message.getTopic()); + msgInner.setBody(message.getBody()); + msgInner.setQueueId(messageQueue.getQueueId()); + msgInner.setTags(message.getTags()); + msgInner.setTagsCode(MessageExtBrokerInner.tagsString2tagsCode(msgInner.getTags())); + msgInner.setSysFlag(0); + MessageAccessor.setProperties(msgInner, message.getProperties()); + msgInner.setPropertiesString(MessageDecoder.messageProperties2String(message.getProperties())); + msgInner.setBornTimestamp(System.currentTimeMillis()); + msgInner.setBornHost(this.storeHost); + msgInner.setStoreHost(this.storeHost); + msgInner.setWaitStoreMsgOK(false); + MessageClientIDSetter.setUniqID(msgInner); + return msgInner; + } + + private TopicConfig selectTopicConfig(String topic) { + TopicConfig topicConfig = brokerController.getTopicConfigManager().selectTopicConfig(topic); + if (topicConfig == null) { + topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod( + topic, 1, PermName.PERM_WRITE | PermName.PERM_READ, 0); + } + return topicConfig; + } + + public boolean writeOp(Integer queueId,Message message) { + MessageQueue opQueue = opQueueMap.get(queueId); + if (opQueue == null) { + opQueue = getOpQueueByHalf(queueId, this.brokerController.getBrokerConfig().getBrokerName()); + MessageQueue oldQueue = opQueueMap.putIfAbsent(queueId, opQueue); + if (oldQueue != null) { + opQueue = oldQueue; + } + } + + PutMessageResult result = putMessageReturnResult(makeOpMessageInner(message, opQueue)); + if (result != null && result.getPutMessageStatus() == PutMessageStatus.PUT_OK) { + return true; + } + + return false; + } + + private MessageQueue getOpQueueByHalf(Integer queueId, String brokerName) { + MessageQueue opQueue = new MessageQueue(); + opQueue.setTopic(TransactionalMessageUtil.buildOpTopic()); + opQueue.setBrokerName(brokerName); + opQueue.setQueueId(queueId); + return opQueue; + } + + public MessageExt lookMessageByOffset(final long commitLogOffset) { + return this.store.lookMessageByOffset(commitLogOffset); + } + + public BrokerController getBrokerController() { + return brokerController; + } + + public boolean escapeMessage(MessageExtBrokerInner messageInner) { + PutMessageResult putMessageResult = this.brokerController.getEscapeBridge().putMessage(messageInner); + if (putMessageResult != null && putMessageResult.isOk()) { + return true; + } else { + LOGGER.error("Escaping message failed, topic: {}, queueId: {}, msgId: {}", + messageInner.getTopic(), messageInner.getQueueId(), messageInner.getMsgId()); + return false; + } + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageServiceImpl.java b/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageServiceImpl.java new file mode 100644 index 00000000000..9fdfd0a7101 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageServiceImpl.java @@ -0,0 +1,757 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.transaction.queue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +import org.apache.rocketmq.broker.BrokerPathConfigHelper; +import org.apache.rocketmq.broker.transaction.AbstractTransactionalMessageCheckListener; +import org.apache.rocketmq.broker.transaction.OperationResult; +import org.apache.rocketmq.broker.transaction.TransactionMetrics; +import org.apache.rocketmq.broker.transaction.TransactionalMessageService; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.consumer.PullStatus; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.EndTransactionRequestHeader; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.config.BrokerRole; + +public class TransactionalMessageServiceImpl implements TransactionalMessageService { + private static final Logger log = LoggerFactory.getLogger(LoggerName.TRANSACTION_LOGGER_NAME); + + private TransactionalMessageBridge transactionalMessageBridge; + + private static final int PULL_MSG_RETRY_NUMBER = 1; + + private static final int MAX_PROCESS_TIME_LIMIT = 60000; + private static final int MAX_RETRY_TIMES_FOR_ESCAPE = 10; + + private static final int MAX_RETRY_COUNT_WHEN_HALF_NULL = 1; + + private static final int OP_MSG_PULL_NUMS = 32; + + private static final int SLEEP_WHILE_NO_OP = 1000; + + private final ConcurrentHashMap deleteContext = new ConcurrentHashMap<>(); + + private ServiceThread transactionalOpBatchService; + + private ConcurrentHashMap opQueueMap = new ConcurrentHashMap<>(); + + private TransactionMetrics transactionMetrics; + + public TransactionalMessageServiceImpl(TransactionalMessageBridge transactionBridge) { + this.transactionalMessageBridge = transactionBridge; + transactionalOpBatchService = new TransactionalOpBatchService(transactionalMessageBridge.getBrokerController(), this); + transactionalOpBatchService.start(); + transactionMetrics = new TransactionMetrics(BrokerPathConfigHelper.getTransactionMetricsPath( + transactionalMessageBridge.getBrokerController().getMessageStoreConfig().getStorePathRootDir())); + transactionMetrics.load(); + } + + @Override + public TransactionMetrics getTransactionMetrics() { + return transactionMetrics; + } + + @Override + public void setTransactionMetrics(TransactionMetrics transactionMetrics) { + this.transactionMetrics = transactionMetrics; + } + + + @Override + public CompletableFuture asyncPrepareMessage(MessageExtBrokerInner messageInner) { + return transactionalMessageBridge.asyncPutHalfMessage(messageInner); + } + + @Override + public PutMessageResult prepareMessage(MessageExtBrokerInner messageInner) { + return transactionalMessageBridge.putHalfMessage(messageInner); + } + + private boolean needDiscard(MessageExt msgExt, int transactionCheckMax) { + String checkTimes = msgExt.getProperty(MessageConst.PROPERTY_TRANSACTION_CHECK_TIMES); + int checkTime = 1; + if (null != checkTimes) { + checkTime = getInt(checkTimes); + if (checkTime >= transactionCheckMax) { + return true; + } else { + checkTime++; + } + } + msgExt.putUserProperty(MessageConst.PROPERTY_TRANSACTION_CHECK_TIMES, String.valueOf(checkTime)); + return false; + } + + private boolean needSkip(MessageExt msgExt) { + long valueOfCurrentMinusBorn = System.currentTimeMillis() - msgExt.getBornTimestamp(); + if (valueOfCurrentMinusBorn + > transactionalMessageBridge.getBrokerController().getMessageStoreConfig().getFileReservedTime() + * 3600L * 1000) { + log.info("Half message exceed file reserved time ,so skip it.messageId {},bornTime {}", + msgExt.getMsgId(), msgExt.getBornTimestamp()); + return true; + } + return false; + } + + private boolean putBackHalfMsgQueue(MessageExt msgExt, long offset) { + PutMessageResult putMessageResult = putBackToHalfQueueReturnResult(msgExt); + if (putMessageResult != null + && putMessageResult.getPutMessageStatus() == PutMessageStatus.PUT_OK) { + msgExt.setQueueOffset( + putMessageResult.getAppendMessageResult().getLogicsOffset()); + msgExt.setCommitLogOffset( + putMessageResult.getAppendMessageResult().getWroteOffset()); + msgExt.setMsgId(putMessageResult.getAppendMessageResult().getMsgId()); + log.debug( + "Send check message, the offset={} restored in queueOffset={} " + + "commitLogOffset={} " + + "newMsgId={} realMsgId={} topic={}", + offset, msgExt.getQueueOffset(), msgExt.getCommitLogOffset(), msgExt.getMsgId(), + msgExt.getUserProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX), + msgExt.getTopic()); + return true; + } else { + log.error( + "PutBackToHalfQueueReturnResult write failed, topic: {}, queueId: {}, " + + "msgId: {}", + msgExt.getTopic(), msgExt.getQueueId(), msgExt.getMsgId()); + return false; + } + } + + @Override + public void check(long transactionTimeout, int transactionCheckMax, + AbstractTransactionalMessageCheckListener listener) { + try { + String topic = TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC; + Set msgQueues = transactionalMessageBridge.fetchMessageQueues(topic); + if (msgQueues == null || msgQueues.size() == 0) { + log.warn("The queue of topic is empty :" + topic); + return; + } + log.debug("Check topic={}, queues={}", topic, msgQueues); + for (MessageQueue messageQueue : msgQueues) { + long startTime = System.currentTimeMillis(); + MessageQueue opQueue = getOpQueue(messageQueue); + long halfOffset = transactionalMessageBridge.fetchConsumeOffset(messageQueue); + long opOffset = transactionalMessageBridge.fetchConsumeOffset(opQueue); + log.info("Before check, the queue={} msgOffset={} opOffset={}", messageQueue, halfOffset, opOffset); + if (halfOffset < 0 || opOffset < 0) { + log.error("MessageQueue: {} illegal offset read: {}, op offset: {},skip this queue", messageQueue, + halfOffset, opOffset); + continue; + } + + List doneOpOffset = new ArrayList<>(); + HashMap removeMap = new HashMap<>(); + HashMap> opMsgMap = new HashMap>(); + PullResult pullResult = fillOpRemoveMap(removeMap, opQueue, opOffset, halfOffset, opMsgMap, doneOpOffset); + if (null == pullResult) { + log.error("The queue={} check msgOffset={} with opOffset={} failed, pullResult is null", + messageQueue, halfOffset, opOffset); + continue; + } + // single thread + int getMessageNullCount = 1; + long newOffset = halfOffset; + long i = halfOffset; + long nextOpOffset = pullResult.getNextBeginOffset(); + int putInQueueCount = 0; + int escapeFailCnt = 0; + + while (true) { + if (System.currentTimeMillis() - startTime > MAX_PROCESS_TIME_LIMIT) { + log.info("Queue={} process time reach max={}", messageQueue, MAX_PROCESS_TIME_LIMIT); + break; + } + if (removeMap.containsKey(i)) { + log.debug("Half offset {} has been committed/rolled back", i); + Long removedOpOffset = removeMap.remove(i); + opMsgMap.get(removedOpOffset).remove(i); + if (opMsgMap.get(removedOpOffset).size() == 0) { + opMsgMap.remove(removedOpOffset); + doneOpOffset.add(removedOpOffset); + } + } else { + GetResult getResult = getHalfMsg(messageQueue, i); + MessageExt msgExt = getResult.getMsg(); + if (msgExt == null) { + if (getMessageNullCount++ > MAX_RETRY_COUNT_WHEN_HALF_NULL) { + break; + } + if (getResult.getPullResult().getPullStatus() == PullStatus.NO_NEW_MSG) { + log.debug("No new msg, the miss offset={} in={}, continue check={}, pull result={}", i, + messageQueue, getMessageNullCount, getResult.getPullResult()); + break; + } else { + log.info("Illegal offset, the miss offset={} in={}, continue check={}, pull result={}", + i, messageQueue, getMessageNullCount, getResult.getPullResult()); + i = getResult.getPullResult().getNextBeginOffset(); + newOffset = i; + continue; + } + } + + if (this.transactionalMessageBridge.getBrokerController().getBrokerConfig().isEnableSlaveActingMaster() + && this.transactionalMessageBridge.getBrokerController().getMinBrokerIdInGroup() + == this.transactionalMessageBridge.getBrokerController().getBrokerIdentity().getBrokerId() + && BrokerRole.SLAVE.equals(this.transactionalMessageBridge.getBrokerController().getMessageStoreConfig().getBrokerRole()) + ) { + final MessageExtBrokerInner msgInner = this.transactionalMessageBridge.renewHalfMessageInner(msgExt); + final boolean isSuccess = this.transactionalMessageBridge.escapeMessage(msgInner); + + if (isSuccess) { + escapeFailCnt = 0; + newOffset = i + 1; + i++; + } else { + log.warn("Escaping transactional message failed {} times! msgId(offsetId)={}, UNIQ_KEY(transactionId)={}", + escapeFailCnt + 1, + msgExt.getMsgId(), + msgExt.getUserProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX)); + if (escapeFailCnt < MAX_RETRY_TIMES_FOR_ESCAPE) { + escapeFailCnt++; + Thread.sleep(100L * (2 ^ escapeFailCnt)); + } else { + escapeFailCnt = 0; + newOffset = i + 1; + i++; + } + } + continue; + } + + if (needDiscard(msgExt, transactionCheckMax) || needSkip(msgExt)) { + listener.resolveDiscardMsg(msgExt); + newOffset = i + 1; + i++; + continue; + } + if (msgExt.getStoreTimestamp() >= startTime) { + log.debug("Fresh stored. the miss offset={}, check it later, store={}", i, + new Date(msgExt.getStoreTimestamp())); + break; + } + + long valueOfCurrentMinusBorn = System.currentTimeMillis() - msgExt.getBornTimestamp(); + long checkImmunityTime = transactionTimeout; + String checkImmunityTimeStr = msgExt.getUserProperty(MessageConst.PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS); + if (null != checkImmunityTimeStr) { + checkImmunityTime = getImmunityTime(checkImmunityTimeStr, transactionTimeout); + if (valueOfCurrentMinusBorn < checkImmunityTime) { + if (checkPrepareQueueOffset(removeMap, doneOpOffset, msgExt, checkImmunityTimeStr)) { + newOffset = i + 1; + i++; + continue; + } + } + } else { + if (0 <= valueOfCurrentMinusBorn && valueOfCurrentMinusBorn < checkImmunityTime) { + log.debug("New arrived, the miss offset={}, check it later checkImmunity={}, born={}", i, + checkImmunityTime, new Date(msgExt.getBornTimestamp())); + break; + } + } + List opMsg = pullResult == null ? null : pullResult.getMsgFoundList(); + boolean isNeedCheck = opMsg == null && valueOfCurrentMinusBorn > checkImmunityTime + || opMsg != null && opMsg.get(opMsg.size() - 1).getBornTimestamp() - startTime > transactionTimeout + || valueOfCurrentMinusBorn <= -1; + + if (isNeedCheck) { + + if (!putBackHalfMsgQueue(msgExt, i)) { + continue; + } + putInQueueCount++; + log.info("Check transaction. real_topic={},uniqKey={},offset={},commitLogOffset={}", + msgExt.getUserProperty(MessageConst.PROPERTY_REAL_TOPIC), + msgExt.getUserProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX), + msgExt.getQueueOffset(), msgExt.getCommitLogOffset()); + listener.resolveHalfMsg(msgExt); + } else { + nextOpOffset = pullResult != null ? pullResult.getNextBeginOffset() : nextOpOffset; + pullResult = fillOpRemoveMap(removeMap, opQueue, nextOpOffset, + halfOffset, opMsgMap, doneOpOffset); + if (pullResult == null || pullResult.getPullStatus() == PullStatus.NO_NEW_MSG + || pullResult.getPullStatus() == PullStatus.OFFSET_ILLEGAL + || pullResult.getPullStatus() == PullStatus.NO_MATCHED_MSG) { + + try { + Thread.sleep(SLEEP_WHILE_NO_OP); + } catch (Throwable ignored) { + } + + } else { + log.info("The miss message offset:{}, pullOffsetOfOp:{}, miniOffset:{} get more opMsg.", i, nextOpOffset, halfOffset); + } + + continue; + } + } + newOffset = i + 1; + i++; + } + if (newOffset != halfOffset) { + transactionalMessageBridge.updateConsumeOffset(messageQueue, newOffset); + } + long newOpOffset = calculateOpOffset(doneOpOffset, opOffset); + if (newOpOffset != opOffset) { + transactionalMessageBridge.updateConsumeOffset(opQueue, newOpOffset); + } + GetResult getResult = getHalfMsg(messageQueue, newOffset); + pullResult = pullOpMsg(opQueue, newOpOffset, 1); + long maxMsgOffset = getResult.getPullResult() == null ? newOffset : getResult.getPullResult().getMaxOffset(); + long maxOpOffset = pullResult == null ? newOpOffset : pullResult.getMaxOffset(); + long msgTime = getResult.getMsg() == null ? System.currentTimeMillis() : getResult.getMsg().getStoreTimestamp(); + + log.info("After check, {} opOffset={} opOffsetDiff={} msgOffset={} msgOffsetDiff={} msgTime={} msgTimeDelayInMs={} putInQueueCount={}", + messageQueue, newOpOffset, maxOpOffset - newOpOffset, newOffset, maxMsgOffset - newOffset, new Date(msgTime), + System.currentTimeMillis() - msgTime, putInQueueCount); + } + } catch (Throwable e) { + log.error("Check error", e); + } + + } + + private long getImmunityTime(String checkImmunityTimeStr, long transactionTimeout) { + long checkImmunityTime; + + checkImmunityTime = getLong(checkImmunityTimeStr); + if (-1 == checkImmunityTime) { + checkImmunityTime = transactionTimeout; + } else { + checkImmunityTime *= 1000; + } + return checkImmunityTime; + } + + /** + * Read op message, parse op message, and fill removeMap + * + * @param removeMap Half message to be remove, key:halfOffset, value: opOffset. + * @param opQueue Op message queue. + * @param pullOffsetOfOp The begin offset of op message queue. + * @param miniOffset The current minimum offset of half message queue. + * @param opMsgMap Half message offset in op message + * @param doneOpOffset Stored op messages that have been processed. + * @return Op message result. + */ + private PullResult fillOpRemoveMap(HashMap removeMap, MessageQueue opQueue, + long pullOffsetOfOp, long miniOffset, Map> opMsgMap, List doneOpOffset) { + PullResult pullResult = pullOpMsg(opQueue, pullOffsetOfOp, OP_MSG_PULL_NUMS); + if (null == pullResult) { + return null; + } + if (pullResult.getPullStatus() == PullStatus.OFFSET_ILLEGAL + || pullResult.getPullStatus() == PullStatus.NO_MATCHED_MSG) { + log.warn("The miss op offset={} in queue={} is illegal, pullResult={}", pullOffsetOfOp, opQueue, + pullResult); + transactionalMessageBridge.updateConsumeOffset(opQueue, pullResult.getNextBeginOffset()); + return pullResult; + } else if (pullResult.getPullStatus() == PullStatus.NO_NEW_MSG) { + log.warn("The miss op offset={} in queue={} is NO_NEW_MSG, pullResult={}", pullOffsetOfOp, opQueue, + pullResult); + return pullResult; + } + List opMsg = pullResult.getMsgFoundList(); + if (opMsg == null) { + log.warn("The miss op offset={} in queue={} is empty, pullResult={}", pullOffsetOfOp, opQueue, pullResult); + return pullResult; + } + for (MessageExt opMessageExt : opMsg) { + if (opMessageExt.getBody() == null) { + log.error("op message body is null. queueId={}, offset={}", opMessageExt.getQueueId(), + opMessageExt.getQueueOffset()); + doneOpOffset.add(opMessageExt.getQueueOffset()); + continue; + } + HashSet set = new HashSet(); + String queueOffsetBody = new String(opMessageExt.getBody(), TransactionalMessageUtil.CHARSET); + + log.debug("Topic: {} tags: {}, OpOffset: {}, HalfOffset: {}", opMessageExt.getTopic(), + opMessageExt.getTags(), opMessageExt.getQueueOffset(), queueOffsetBody); + if (TransactionalMessageUtil.REMOVE_TAG.equals(opMessageExt.getTags())) { + String[] offsetArray = queueOffsetBody.split(TransactionalMessageUtil.OFFSET_SEPARATOR); + for (String offset : offsetArray) { + Long offsetValue = getLong(offset); + if (offsetValue < miniOffset) { + continue; + } + + removeMap.put(offsetValue, opMessageExt.getQueueOffset()); + set.add(offsetValue); + } + } else { + log.error("Found a illegal tag in opMessageExt= {} ", opMessageExt); + } + + if (set.size() > 0) { + opMsgMap.put(opMessageExt.getQueueOffset(), set); + } else { + doneOpOffset.add(opMessageExt.getQueueOffset()); + } + } + + log.debug("Remove map: {}", removeMap); + log.debug("Done op list: {}", doneOpOffset); + log.debug("opMsg map: {}", opMsgMap); + return pullResult; + } + + /** + * If return true, skip this msg + * + * @param removeMap Op message map to determine whether a half message was responded by producer. + * @param doneOpOffset Op Message which has been checked. + * @param msgExt Half message + * @return Return true if put success, otherwise return false. + */ + private boolean checkPrepareQueueOffset(HashMap removeMap, List doneOpOffset, + MessageExt msgExt, String checkImmunityTimeStr) { + String prepareQueueOffsetStr = msgExt.getUserProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED_QUEUE_OFFSET); + if (null == prepareQueueOffsetStr) { + return putImmunityMsgBackToHalfQueue(msgExt); + } else { + long prepareQueueOffset = getLong(prepareQueueOffsetStr); + if (-1 == prepareQueueOffset) { + return false; + } else { + if (removeMap.containsKey(prepareQueueOffset)) { + long tmpOpOffset = removeMap.remove(prepareQueueOffset); + doneOpOffset.add(tmpOpOffset); + log.info("removeMap contain prepareQueueOffset. real_topic={},uniqKey={},immunityTime={},offset={}", + msgExt.getUserProperty(MessageConst.PROPERTY_REAL_TOPIC), + msgExt.getUserProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX), + checkImmunityTimeStr, + msgExt.getQueueOffset()); + return true; + } else { + return putImmunityMsgBackToHalfQueue(msgExt); + } + } + } + } + + /** + * Write messageExt to Half topic again + * + * @param messageExt Message will be write back to queue + * @return Put result can used to determine the specific results of storage. + */ + private PutMessageResult putBackToHalfQueueReturnResult(MessageExt messageExt) { + PutMessageResult putMessageResult = null; + try { + MessageExtBrokerInner msgInner = transactionalMessageBridge.renewHalfMessageInner(messageExt); + putMessageResult = transactionalMessageBridge.putMessageReturnResult(msgInner); + } catch (Exception e) { + log.warn("PutBackToHalfQueueReturnResult error", e); + } + return putMessageResult; + } + + private boolean putImmunityMsgBackToHalfQueue(MessageExt messageExt) { + MessageExtBrokerInner msgInner = transactionalMessageBridge.renewImmunityHalfMessageInner(messageExt); + return transactionalMessageBridge.putMessage(msgInner); + } + + /** + * Read half message from Half Topic + * + * @param mq Target message queue, in this method, it means the half message queue. + * @param offset Offset in the message queue. + * @param nums Pull message number. + * @return Messages pulled from half message queue. + */ + private PullResult pullHalfMsg(MessageQueue mq, long offset, int nums) { + return transactionalMessageBridge.getHalfMessage(mq.getQueueId(), offset, nums); + } + + /** + * Read op message from Op Topic + * + * @param mq Target Message Queue + * @param offset Offset in the message queue + * @param nums Pull message number + * @return Messages pulled from operate message queue. + */ + private PullResult pullOpMsg(MessageQueue mq, long offset, int nums) { + return transactionalMessageBridge.getOpMessage(mq.getQueueId(), offset, nums); + } + + private Long getLong(String s) { + long v = -1; + try { + v = Long.parseLong(s); + } catch (Exception e) { + log.error("GetLong error", e); + } + return v; + + } + + private Integer getInt(String s) { + int v = -1; + try { + v = Integer.parseInt(s); + } catch (Exception e) { + log.error("GetInt error", e); + } + return v; + + } + + private long calculateOpOffset(List doneOffset, long oldOffset) { + Collections.sort(doneOffset); + long newOffset = oldOffset; + for (int i = 0; i < doneOffset.size(); i++) { + if (doneOffset.get(i) == newOffset) { + newOffset++; + } else { + break; + } + } + return newOffset; + + } + + private MessageQueue getOpQueue(MessageQueue messageQueue) { + MessageQueue opQueue = opQueueMap.get(messageQueue); + if (opQueue == null) { + opQueue = new MessageQueue(TransactionalMessageUtil.buildOpTopic(), messageQueue.getBrokerName(), + messageQueue.getQueueId()); + opQueueMap.put(messageQueue, opQueue); + } + return opQueue; + + } + + private GetResult getHalfMsg(MessageQueue messageQueue, long offset) { + GetResult getResult = new GetResult(); + + PullResult result = pullHalfMsg(messageQueue, offset, PULL_MSG_RETRY_NUMBER); + if (result != null) { + getResult.setPullResult(result); + List messageExts = result.getMsgFoundList(); + if (messageExts == null || messageExts.size() == 0) { + return getResult; + } + getResult.setMsg(messageExts.get(0)); + } + return getResult; + } + + private OperationResult getHalfMessageByOffset(long commitLogOffset) { + OperationResult response = new OperationResult(); + MessageExt messageExt = this.transactionalMessageBridge.lookMessageByOffset(commitLogOffset); + if (messageExt != null) { + response.setPrepareMessage(messageExt); + response.setResponseCode(ResponseCode.SUCCESS); + } else { + response.setResponseCode(ResponseCode.SYSTEM_ERROR); + response.setResponseRemark("Find prepared transaction message failed"); + } + return response; + } + + @Override + public boolean deletePrepareMessage(MessageExt messageExt) { + Integer queueId = messageExt.getQueueId(); + MessageQueueOpContext mqContext = deleteContext.get(queueId); + if (mqContext == null) { + mqContext = new MessageQueueOpContext(System.currentTimeMillis(), 20000); + MessageQueueOpContext old = deleteContext.putIfAbsent(queueId, mqContext); + if (old != null) { + mqContext = old; + } + } + + String data = messageExt.getQueueOffset() + TransactionalMessageUtil.OFFSET_SEPARATOR; + try { + boolean res = mqContext.getContextQueue().offer(data, 100, TimeUnit.MILLISECONDS); + if (res) { + int totalSize = mqContext.getTotalSize().addAndGet(data.length()); + if (totalSize > transactionalMessageBridge.getBrokerController().getBrokerConfig().getTransactionOpMsgMaxSize()) { + this.transactionalOpBatchService.wakeup(); + } + return true; + } else { + this.transactionalOpBatchService.wakeup(); + } + } catch (InterruptedException ignore) { + } + + Message msg = getOpMessage(queueId, data); + if (this.transactionalMessageBridge.writeOp(queueId, msg)) { + log.warn("Force add remove op data. queueId={}", queueId); + return true; + } else { + log.error("Transaction op message write failed. messageId is {}, queueId is {}", messageExt.getMsgId(), messageExt.getQueueId()); + return false; + } + } + + @Override + public OperationResult commitMessage(EndTransactionRequestHeader requestHeader) { + return getHalfMessageByOffset(requestHeader.getCommitLogOffset()); + } + + @Override + public OperationResult rollbackMessage(EndTransactionRequestHeader requestHeader) { + return getHalfMessageByOffset(requestHeader.getCommitLogOffset()); + } + + @Override + public boolean open() { + return true; + } + + @Override + public void close() { + if (this.transactionalOpBatchService != null) { + this.transactionalOpBatchService.shutdown(); + } + this.getTransactionMetrics().persist(); + } + + public Message getOpMessage(int queueId, String moreData) { + String opTopic = TransactionalMessageUtil.buildOpTopic(); + MessageQueueOpContext mqContext = deleteContext.get(queueId); + + int moreDataLength = moreData != null ? moreData.length() : 0; + int length = moreDataLength; + int maxSize = transactionalMessageBridge.getBrokerController().getBrokerConfig().getTransactionOpMsgMaxSize(); + if (length < maxSize) { + int sz = mqContext.getTotalSize().get(); + if (sz > maxSize || length + sz > maxSize) { + length = maxSize + 100; + } else { + length += sz; + } + } + + StringBuilder sb = new StringBuilder(length); + + if (moreData != null) { + sb.append(moreData); + } + + while (!mqContext.getContextQueue().isEmpty()) { + if (sb.length() >= maxSize) { + break; + } + String data = mqContext.getContextQueue().poll(); + if (data != null) { + sb.append(data); + } + } + + if (sb.length() == 0) { + return null; + } + + int l = sb.length() - moreDataLength; + mqContext.getTotalSize().addAndGet(-l); + mqContext.setLastWriteTimestamp(System.currentTimeMillis()); + return new Message(opTopic, TransactionalMessageUtil.REMOVE_TAG, + sb.toString().getBytes(TransactionalMessageUtil.CHARSET)); + } + public long batchSendOpMessage() { + long startTime = System.currentTimeMillis(); + try { + long firstTimestamp = startTime; + Map sendMap = null; + long interval = transactionalMessageBridge.getBrokerController().getBrokerConfig().getTransactionOpBatchInterval(); + int maxSize = transactionalMessageBridge.getBrokerController().getBrokerConfig().getTransactionOpMsgMaxSize(); + boolean overSize = false; + for (Map.Entry entry : deleteContext.entrySet()) { + MessageQueueOpContext mqContext = entry.getValue(); + //no msg in contextQueue + if (mqContext.getTotalSize().get() <= 0 || mqContext.getContextQueue().size() == 0 || + // wait for the interval + mqContext.getTotalSize().get() < maxSize && + startTime - mqContext.getLastWriteTimestamp() < interval) { + continue; + } + + if (sendMap == null) { + sendMap = new HashMap<>(); + } + + Message opMsg = getOpMessage(entry.getKey(), null); + if (opMsg == null) { + continue; + } + sendMap.put(entry.getKey(), opMsg); + firstTimestamp = Math.min(firstTimestamp, mqContext.getLastWriteTimestamp()); + if (mqContext.getTotalSize().get() >= maxSize) { + overSize = true; + } + } + + if (sendMap != null) { + for (Map.Entry entry : sendMap.entrySet()) { + if (!this.transactionalMessageBridge.writeOp(entry.getKey(), entry.getValue())) { + log.error("Transaction batch op message write failed. body is {}, queueId is {}", + new String(entry.getValue().getBody(), TransactionalMessageUtil.CHARSET), entry.getKey()); + } + } + } + + log.debug("Send op message queueIds={}", sendMap == null ? null : sendMap.keySet()); + + //wait for next batch remove + long wakeupTimestamp = firstTimestamp + interval; + if (!overSize && wakeupTimestamp > startTime) { + return wakeupTimestamp; + } + } catch (Throwable t) { + log.error("batchSendOp error.", t); + } + + return 0L; + } + + public Map getDeleteContext() { + return this.deleteContext; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageUtil.java b/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageUtil.java new file mode 100644 index 00000000000..555ae4d2940 --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageUtil.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.transaction.queue; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.common.topic.TopicValidator; + +public class TransactionalMessageUtil { + public static final String REMOVE_TAG = "d"; + public static final Charset CHARSET = StandardCharsets.UTF_8; + public static final String OFFSET_SEPARATOR = ","; + public static final String TRANSACTION_ID = "__transactionId__"; + + public static String buildOpTopic() { + return TopicValidator.RMQ_SYS_TRANS_OP_HALF_TOPIC; + } + + public static String buildHalfTopic() { + return TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC; + } + + public static String buildConsumerGroup() { + return MixAll.CID_SYS_RMQ_TRANS; + } + + public static MessageExtBrokerInner buildTransactionalMessageFromHalfMessage(MessageExt msgExt) { + final MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); + msgInner.setWaitStoreMsgOK(false); + msgInner.setMsgId(msgExt.getMsgId()); + msgInner.setTopic(msgExt.getProperty(MessageConst.PROPERTY_REAL_TOPIC)); + msgInner.setBody(msgExt.getBody()); + final String realQueueIdStr = msgExt.getProperty(MessageConst.PROPERTY_REAL_QUEUE_ID); + if (StringUtils.isNumeric(realQueueIdStr)) { + msgInner.setQueueId(Integer.parseInt(realQueueIdStr)); + } + msgInner.setFlag(msgExt.getFlag()); + msgInner.setTagsCode(MessageExtBrokerInner.tagsString2tagsCode(msgInner.getTags())); + msgInner.setBornTimestamp(msgExt.getBornTimestamp()); + msgInner.setBornHost(msgExt.getBornHost()); + msgInner.setTransactionId(msgExt.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX)); + + MessageAccessor.setProperties(msgInner, msgExt.getProperties()); + MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true"); + MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_TRANSACTION_PREPARED_QUEUE_OFFSET); + MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_REAL_QUEUE_ID); + msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); + + int sysFlag = msgExt.getSysFlag(); + sysFlag |= MessageSysFlag.TRANSACTION_PREPARED_TYPE; + msgInner.setSysFlag(sysFlag); + + return msgInner; + } + + public static long getImmunityTime(String checkImmunityTimeStr, long transactionTimeout) { + long checkImmunityTime = 0; + + try { + checkImmunityTime = Long.parseLong(checkImmunityTimeStr) * 1000; + } catch (Throwable ignored) { + } + + //If a custom first check time is set, the minimum check time; + //The default check protection period is transactionTimeout + if (checkImmunityTime < transactionTimeout) { + checkImmunityTime = transactionTimeout; + } + return checkImmunityTime; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/TransactionalOpBatchService.java b/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/TransactionalOpBatchService.java new file mode 100644 index 00000000000..fb6e9e8ce1a --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/transaction/queue/TransactionalOpBatchService.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.transaction.queue; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class TransactionalOpBatchService extends ServiceThread { + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.TRANSACTION_LOGGER_NAME); + + private BrokerController brokerController; + private TransactionalMessageServiceImpl transactionalMessageService; + + private long wakeupTimestamp = 0; + + + public TransactionalOpBatchService(BrokerController brokerController, + TransactionalMessageServiceImpl transactionalMessageService) { + this.brokerController = brokerController; + this.transactionalMessageService = transactionalMessageService; + } + + @Override + public String getServiceName() { + return TransactionalOpBatchService.class.getSimpleName(); + } + + @Override + public void run() { + LOGGER.info("Start transaction op batch thread!"); + long checkInterval = brokerController.getBrokerConfig().getTransactionOpBatchInterval(); + wakeupTimestamp = System.currentTimeMillis() + checkInterval; + while (!this.isStopped()) { + long interval = wakeupTimestamp - System.currentTimeMillis(); + if (interval <= 0) { + interval = 0; + wakeup(); + } + this.waitForRunning(interval); + } + LOGGER.info("End transaction op batch thread!"); + } + + @Override + protected void onWaitEnd() { + wakeupTimestamp = transactionalMessageService.batchSendOpMessage(); + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/util/HookUtils.java b/broker/src/main/java/org/apache/rocketmq/broker/util/HookUtils.java new file mode 100644 index 00000000000..dec42351d9f --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/util/HookUtils.java @@ -0,0 +1,253 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.util; + +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.schedule.ScheduleMessageService; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.common.utils.QueueTypeUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.config.BrokerRole; +import org.apache.rocketmq.store.timer.TimerMessageStore; + +public class HookUtils { + + protected static final Logger LOG = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + + private static final AtomicLong PRINT_TIMES = new AtomicLong(0); + + /** + * On Linux: The maximum length for a file name is 255 bytes. + * The maximum combined length of both the file name and path name is 4096 bytes. + * This length matches the PATH_MAX that is supported by the operating system. + * The Unicode representation of a character can occupy several bytes, + * so the maximum number of characters that comprises a path and file name can vary. + * The actual limitation is the number of bytes in the path and file components, + * which might correspond to an equal number of characters. + */ + private static final Integer MAX_TOPIC_LENGTH = 255; + + public static PutMessageResult checkBeforePutMessage(BrokerController brokerController, final MessageExt msg) { + if (brokerController.getMessageStore().isShutdown()) { + LOG.warn("message store has shutdown, so putMessage is forbidden"); + return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null); + } + + if (!brokerController.getMessageStoreConfig().isDuplicationEnable() && BrokerRole.SLAVE == brokerController.getMessageStoreConfig().getBrokerRole()) { + long value = PRINT_TIMES.getAndIncrement(); + if ((value % 50000) == 0) { + LOG.warn("message store is in slave mode, so putMessage is forbidden "); + } + + return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null); + } + + if (!brokerController.getMessageStore().getRunningFlags().isWriteable()) { + long value = PRINT_TIMES.getAndIncrement(); + if ((value % 50000) == 0) { + LOG.warn("message store is not writeable, so putMessage is forbidden " + brokerController.getMessageStore().getRunningFlags().getFlagBits()); + } + + return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null); + } else { + PRINT_TIMES.set(0); + } + + final byte[] topicData = msg.getTopic().getBytes(MessageDecoder.CHARSET_UTF8); + boolean retryTopic = msg.getTopic() != null && msg.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX); + if (!retryTopic && topicData.length > Byte.MAX_VALUE) { + LOG.warn("putMessage message topic[{}] length too long {}, but it is not supported by broker", + msg.getTopic(), topicData.length); + return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null); + } + + if (topicData.length > MAX_TOPIC_LENGTH) { + LOG.warn("putMessage message topic[{}] length too long {}, but it is not supported by broker", + msg.getTopic(), topicData.length); + return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null); + } + + if (msg.getBody() == null) { + LOG.warn("putMessage message topic[{}], but message body is null", msg.getTopic()); + return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null); + } + + if (brokerController.getMessageStore().isOSPageCacheBusy()) { + return new PutMessageResult(PutMessageStatus.OS_PAGE_CACHE_BUSY, null); + } + return null; + } + + public static PutMessageResult checkInnerBatch(BrokerController brokerController, final MessageExt msg) { + if (msg.getProperties().containsKey(MessageConst.PROPERTY_INNER_NUM) + && !MessageSysFlag.check(msg.getSysFlag(), MessageSysFlag.INNER_BATCH_FLAG)) { + LOG.warn("[BUG]The message had property {} but is not an inner batch", MessageConst.PROPERTY_INNER_NUM); + return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null); + } + + if (MessageSysFlag.check(msg.getSysFlag(), MessageSysFlag.INNER_BATCH_FLAG)) { + Optional topicConfig = Optional.ofNullable(brokerController.getTopicConfigManager().getTopicConfigTable().get(msg.getTopic())); + if (!QueueTypeUtils.isBatchCq(topicConfig)) { + LOG.error("[BUG]The message is an inner batch but cq type is not batch cq"); + return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null); + } + } + + return null; + } + + public static PutMessageResult handleScheduleMessage(BrokerController brokerController, + final MessageExtBrokerInner msg) { + final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag()); + if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE + || tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) { + if (!isRolledTimerMessage(msg)) { + if (checkIfTimerMessage(msg)) { + if (!brokerController.getMessageStoreConfig().isTimerWheelEnable()) { + //wheel timer is not enabled, reject the message + return new PutMessageResult(PutMessageStatus.WHEEL_TIMER_NOT_ENABLE, null); + } + PutMessageResult transformRes = transformTimerMessage(brokerController, msg); + if (null != transformRes) { + return transformRes; + } + } + } + // Delay Delivery + if (msg.getDelayTimeLevel() > 0) { + transformDelayLevelMessage(brokerController, msg); + } + } + return null; + } + + private static boolean isRolledTimerMessage(MessageExtBrokerInner msg) { + return TimerMessageStore.TIMER_TOPIC.equals(msg.getTopic()); + } + + public static boolean checkIfTimerMessage(MessageExtBrokerInner msg) { + if (msg.getDelayTimeLevel() > 0) { + if (null != msg.getProperty(MessageConst.PROPERTY_TIMER_DELIVER_MS)) { + MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_TIMER_DELIVER_MS); + } + if (null != msg.getProperty(MessageConst.PROPERTY_TIMER_DELAY_SEC)) { + MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_TIMER_DELAY_SEC); + } + if (null != msg.getProperty(MessageConst.PROPERTY_TIMER_DELAY_MS)) { + MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_TIMER_DELAY_MS); + } + return false; + //return this.defaultMessageStore.getMessageStoreConfig().isTimerInterceptDelayLevel(); + } + //double check + if (TimerMessageStore.TIMER_TOPIC.equals(msg.getTopic()) || null != msg.getProperty(MessageConst.PROPERTY_TIMER_OUT_MS)) { + return false; + } + return null != msg.getProperty(MessageConst.PROPERTY_TIMER_DELIVER_MS) || null != msg.getProperty(MessageConst.PROPERTY_TIMER_DELAY_MS) || null != msg.getProperty(MessageConst.PROPERTY_TIMER_DELAY_SEC); + } + + private static PutMessageResult transformTimerMessage(BrokerController brokerController, + MessageExtBrokerInner msg) { + //do transform + int delayLevel = msg.getDelayTimeLevel(); + long deliverMs; + try { + if (msg.getProperty(MessageConst.PROPERTY_TIMER_DELAY_SEC) != null) { + deliverMs = System.currentTimeMillis() + Long.parseLong(msg.getProperty(MessageConst.PROPERTY_TIMER_DELAY_SEC)) * 1000; + } else if (msg.getProperty(MessageConst.PROPERTY_TIMER_DELAY_MS) != null) { + deliverMs = System.currentTimeMillis() + Long.parseLong(msg.getProperty(MessageConst.PROPERTY_TIMER_DELAY_MS)); + } else { + deliverMs = Long.parseLong(msg.getProperty(MessageConst.PROPERTY_TIMER_DELIVER_MS)); + } + } catch (Exception e) { + return new PutMessageResult(PutMessageStatus.WHEEL_TIMER_MSG_ILLEGAL, null); + } + if (deliverMs > System.currentTimeMillis()) { + if (delayLevel <= 0 && deliverMs - System.currentTimeMillis() > brokerController.getMessageStoreConfig().getTimerMaxDelaySec() * 1000L) { + return new PutMessageResult(PutMessageStatus.WHEEL_TIMER_MSG_ILLEGAL, null); + } + + int timerPrecisionMs = brokerController.getMessageStoreConfig().getTimerPrecisionMs(); + if (deliverMs % timerPrecisionMs == 0) { + deliverMs -= timerPrecisionMs; + } else { + deliverMs = deliverMs / timerPrecisionMs * timerPrecisionMs; + } + + if (brokerController.getTimerMessageStore().isReject(deliverMs)) { + return new PutMessageResult(PutMessageStatus.WHEEL_TIMER_FLOW_CONTROL, null); + } + MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TIMER_OUT_MS, deliverMs + ""); + MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic()); + MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId())); + msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); + msg.setTopic(TimerMessageStore.TIMER_TOPIC); + msg.setQueueId(0); + } else if (null != msg.getProperty(MessageConst.PROPERTY_TIMER_DEL_UNIQKEY)) { + return new PutMessageResult(PutMessageStatus.WHEEL_TIMER_MSG_ILLEGAL, null); + } + return null; + } + + public static void transformDelayLevelMessage(BrokerController brokerController, MessageExtBrokerInner msg) { + + if (msg.getDelayTimeLevel() > brokerController.getScheduleMessageService().getMaxDelayLevel()) { + msg.setDelayTimeLevel(brokerController.getScheduleMessageService().getMaxDelayLevel()); + } + + // Backup real topic, queueId + MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic()); + MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId())); + msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); + + msg.setTopic(TopicValidator.RMQ_SYS_SCHEDULE_TOPIC); + msg.setQueueId(ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel())); + } + + public static boolean sendMessageBack(BrokerController brokerController, List msgList, + String brokerName, String brokerAddr) { + try { + Iterator it = msgList.iterator(); + while (it.hasNext()) { + MessageExt msg = it.next(); + msg.setWaitStoreMsgOK(false); + brokerController.getBrokerOuterAPI().sendMessageToSpecificBroker(brokerAddr, brokerName, msg, "InnerSendMessageBackGroup", 3000); + it.remove(); + } + } catch (Exception e) { + LOG.error("send message back to broker {} addr {} failed", brokerName, brokerAddr, e); + return false; + } + return true; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/util/PositiveAtomicCounter.java b/broker/src/main/java/org/apache/rocketmq/broker/util/PositiveAtomicCounter.java new file mode 100644 index 00000000000..8d92f43085e --- /dev/null +++ b/broker/src/main/java/org/apache/rocketmq/broker/util/PositiveAtomicCounter.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.util; + +import java.util.concurrent.atomic.AtomicInteger; + +public class PositiveAtomicCounter { + private static final int MASK = 0x7FFFFFFF; + private final AtomicInteger atom; + + + public PositiveAtomicCounter() { + atom = new AtomicInteger(0); + } + + + public final int incrementAndGet() { + final int rt = atom.incrementAndGet(); + return rt & MASK; + } + + + public int intValue() { + return atom.intValue(); + } +} diff --git a/broker/src/main/resources/rmq.broker.logback.xml b/broker/src/main/resources/rmq.broker.logback.xml new file mode 100644 index 00000000000..32dc297360e --- /dev/null +++ b/broker/src/main/resources/rmq.broker.logback.xml @@ -0,0 +1,687 @@ + + + + + + + + brokerContainerLogDir + ${file.separator} + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}broker_default.log + true + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}otherdays${file.separator}broker_default.%i.log.gz + + 1 + 10 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + + + + brokerContainerLogDir + ${file.separator} + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}broker.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}otherdays${file.separator}broker.%i.log.gz + + 1 + 20 + + + 128MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + + + + brokerContainerLogDir + ${file.separator} + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}protection.log + + true + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}otherdays${file.separator}protection.%i.log.gz + + 1 + 10 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} - %m%n + UTF-8 + + + + + + + + + + + brokerContainerLogDir + ${file.separator} + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}watermark.log + + true + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}otherdays${file.separator}watermark.%i.log.gz + + 1 + 10 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} - %m%n + UTF-8 + + + + + + + + + + + brokerContainerLogDir + ${file.separator} + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}rocksdb.log + + true + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}otherdays${file.separator}rocksdb.%i.log.gz + + 1 + 10 + + + 128MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + + + + brokerContainerLogDir + ${file.separator} + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}store.log + + true + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}otherdays${file.separator}store.%i.log.gz + + 1 + 10 + + + 128MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + + + + brokerContainerLogDir + ${file.separator} + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}tiered_store.log + + true + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}otherdays${file.separator}tiered_store.%i.log.gz + + 1 + 10 + + + 128MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + + + + brokerContainerLogDir + ${file.separator} + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}broker_traffic.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}broker_traffic.%i.log.gz + 1 + 10 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + + + + brokerContainerLogDir + ${file.separator} + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}remoting.log + + true + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}otherdays${file.separator}remoting.%i.log.gz + + 1 + 10 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + + + + brokerContainerLogDir + ${file.separator} + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}storeerror.log + + true + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}otherdays${file.separator}storeerror.%i.log.gz + + 1 + 10 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + + + + brokerContainerLogDir + ${file.separator} + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}transaction.log + + true + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}otherdays${file.separator}transaction.%i.log.gz + + 1 + 10 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + + + + brokerContainerLogDir + ${file.separator} + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}lock.log + + true + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}otherdays${file.separator}lock.%i.log.gz + + 1 + 5 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + + + + brokerContainerLogDir + ${file.separator} + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}filter.log + + true + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}otherdays${file.separator}filter.%i.log.gz + + 1 + 10 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + + + + brokerContainerLogDir + ${file.separator} + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}stats.log + + true + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}otherdays${file.separator}stats.%i.log.gz + + 1 + 5 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p - %m%n + UTF-8 + + + + + + + + + + + brokerContainerLogDir + ${file.separator} + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}commercial.log + + true + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}otherdays${file.separator}commercial.%i.log.gz + + 1 + 10 + + + 500MB + + + + + + + + + + + brokerContainerLogDir + ${file.separator} + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}pop.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}pop.%i.log + + 1 + 20 + + + 128MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + + + + brokerContainerLogDir + ${file.separator} + + + + ${user.home}/logs/rocketmqlogs/coldctr.log + true + + ${user.home}/logs/rocketmqlogs/otherdays/coldctr.%i.log + + 1 + 10 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + brokerContainerLogDir + ${file.separator} + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}broker_metrics.log + + true + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${brokerLogDir:-${file.separator}}${brokerContainerLogDir}${file.separator}otherdays${file.separator}broker_metrics.%i.log.gz + + 1 + 3 + + + 512MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/broker/src/test/java/org/apache/rocketmq/broker/BrokerControllerTest.java b/broker/src/test/java/org/apache/rocketmq/broker/BrokerControllerTest.java index 797e0d882a2..6035a20acb2 100644 --- a/broker/src/test/java/org/apache/rocketmq/broker/BrokerControllerTest.java +++ b/broker/src/test/java/org/apache/rocketmq/broker/BrokerControllerTest.java @@ -17,38 +17,81 @@ package org.apache.rocketmq.broker; +import java.io.File; +import java.util.UUID; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + import org.apache.rocketmq.common.BrokerConfig; import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.future.FutureTaskExt; import org.apache.rocketmq.remoting.netty.NettyClientConfig; import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.netty.RequestTask; import org.apache.rocketmq.store.config.MessageStoreConfig; import org.junit.After; +import org.junit.Before; import org.junit.Test; -import java.io.File; - import static org.assertj.core.api.Assertions.assertThat; public class BrokerControllerTest { - /** - * Tests if the controller can be properly stopped and started. - * - * @throws Exception If fails. - */ + + private MessageStoreConfig messageStoreConfig; + + private BrokerConfig brokerConfig; + + private NettyServerConfig nettyServerConfig; + + + @Before + public void setUp() { + messageStoreConfig = new MessageStoreConfig(); + String storePathRootDir = System.getProperty("java.io.tmpdir") + File.separator + "store-" + + UUID.randomUUID().toString(); + messageStoreConfig.setStorePathRootDir(storePathRootDir); + + brokerConfig = new BrokerConfig(); + + nettyServerConfig = new NettyServerConfig(); + nettyServerConfig.setListenPort(0); + + } + @Test public void testBrokerRestart() throws Exception { - BrokerController brokerController = new BrokerController( - new BrokerConfig(), - new NettyServerConfig(), - new NettyClientConfig(), - new MessageStoreConfig()); - assertThat(brokerController.initialize()); + BrokerController brokerController = new BrokerController(brokerConfig, nettyServerConfig, new NettyClientConfig(), messageStoreConfig); + assertThat(brokerController.initialize()).isTrue(); brokerController.start(); brokerController.shutdown(); } @After - public void destory() { - UtilAll.deleteFile(new File(new MessageStoreConfig().getStorePathRootDir())); + public void destroy() { + UtilAll.deleteFile(new File(messageStoreConfig.getStorePathRootDir())); + } + + @Test + public void testHeadSlowTimeMills() throws Exception { + BrokerController brokerController = new BrokerController(brokerConfig, nettyServerConfig, new NettyClientConfig(), messageStoreConfig); + brokerController.initialize(); + BlockingQueue queue = new LinkedBlockingQueue<>(); + + //create task is not instance of FutureTaskExt; + Runnable runnable = new Runnable() { + @Override + public void run() { + + } + }; + + RequestTask requestTask = new RequestTask(runnable, null, null); + // the requestTask is not the head of queue; + queue.add(new FutureTaskExt<>(requestTask, null)); + + long headSlowTimeMills = 100; + TimeUnit.MILLISECONDS.sleep(headSlowTimeMills); + assertThat(brokerController.headSlowTimeMills(queue)).isGreaterThanOrEqualTo(headSlowTimeMills); } } diff --git a/broker/src/test/java/org/apache/rocketmq/broker/BrokerOuterAPITest.java b/broker/src/test/java/org/apache/rocketmq/broker/BrokerOuterAPITest.java new file mode 100644 index 00000000000..2541e755e39 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/BrokerOuterAPITest.java @@ -0,0 +1,253 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker; + +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import io.netty.channel.ChannelHandlerContext; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.rocketmq.broker.out.BrokerOuterAPI; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.BrokerIdentity; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyRemotingClient; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.header.namesrv.QueryDataVersionResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.RegisterBrokerResponseHeader; +import org.apache.rocketmq.remoting.protocol.namesrv.RegisterBrokerResult; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.AdditionalMatchers.or; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class BrokerOuterAPITest { + @Mock + private ChannelHandlerContext handlerContext; + @Spy + private BrokerController brokerController = new BrokerController(new BrokerConfig(), new NettyServerConfig(), new NettyClientConfig(), new MessageStoreConfig()); + @Mock + private MessageStore messageStore; + private String clusterName = "clusterName"; + private String brokerName = "brokerName"; + private String brokerAddr = "brokerAddr"; + private long brokerId = 0L; + private String nameserver1 = "127.0.0.1"; + private String nameserver2 = "127.0.0.2"; + private String nameserver3 = "127.0.0.3"; + private int timeOut = 3000; + + @Mock + private NettyRemotingClient nettyRemotingClient; + + private BrokerOuterAPI brokerOuterAPI; + + public void init() throws Exception { + brokerOuterAPI = new BrokerOuterAPI(new NettyClientConfig()); + Field field = BrokerOuterAPI.class.getDeclaredField("remotingClient"); + field.setAccessible(true); + field.set(brokerOuterAPI, nettyRemotingClient); + } + + @Test + public void test_needRegister_normal() throws Exception { + init(); + brokerOuterAPI.start(); + final RemotingCommand response = buildResponse(Boolean.TRUE); + + TopicConfigSerializeWrapper topicConfigSerializeWrapper = new TopicConfigSerializeWrapper(); + + when(nettyRemotingClient.getNameServerAddressList()).thenReturn(Lists.asList(nameserver1, nameserver2, new String[] {nameserver3})); + when(nettyRemotingClient.invokeSync(anyString(), any(RemotingCommand.class), anyLong())).thenReturn(response); + List booleanList = brokerOuterAPI.needRegister(clusterName, brokerAddr, brokerName, brokerId, topicConfigSerializeWrapper, timeOut, false); + assertTrue(booleanList.size() > 0); + assertFalse(booleanList.contains(Boolean.FALSE)); + } + + @Test + public void test_needRegister_timeout() throws Exception { + init(); + brokerOuterAPI.start(); + + TopicConfigSerializeWrapper topicConfigSerializeWrapper = new TopicConfigSerializeWrapper(); + + when(nettyRemotingClient.getNameServerAddressList()).thenReturn(Lists.asList(nameserver1, nameserver2, new String[] {nameserver3})); + + when(nettyRemotingClient.invokeSync(anyString(), any(RemotingCommand.class), anyLong())).thenAnswer(new Answer() { + @Override + public RemotingCommand answer(InvocationOnMock invocation) throws Throwable { + if (invocation.getArgument(0) == nameserver1) { + return buildResponse(Boolean.TRUE); + } else if (invocation.getArgument(0) == nameserver2) { + return buildResponse(Boolean.FALSE); + } else if (invocation.getArgument(0) == nameserver3) { + TimeUnit.MILLISECONDS.sleep(timeOut + 20); + return buildResponse(Boolean.TRUE); + } + return buildResponse(Boolean.TRUE); + } + }); + List booleanList = brokerOuterAPI.needRegister(clusterName, brokerAddr, brokerName, brokerId, topicConfigSerializeWrapper, timeOut, false); + assertEquals(2, booleanList.size()); + boolean success = Iterables.any(booleanList, + new Predicate() { + public boolean apply(Boolean input) { + return input; + } + }); + + assertTrue(success); + + } + + @Test + public void test_register_normal() throws Exception { + init(); + brokerOuterAPI.start(); + + final RemotingCommand response = RemotingCommand.createResponseCommand(RegisterBrokerResponseHeader.class); + final RegisterBrokerResponseHeader responseHeader = (RegisterBrokerResponseHeader) response.readCustomHeader(); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + + TopicConfigSerializeWrapper topicConfigSerializeWrapper = new TopicConfigSerializeWrapper(); + + when(nettyRemotingClient.getAvailableNameSrvList()).thenReturn(Lists.asList(nameserver1, nameserver2, new String[] {nameserver3})); + when(nettyRemotingClient.invokeSync(anyString(), any(RemotingCommand.class), anyLong())).then(new Answer() { + @Override + public RemotingCommand answer(InvocationOnMock mock) throws Throwable { + RemotingCommand request = mock.getArgument(1); + return response; + } + }); + List registerBrokerResultList = brokerOuterAPI.registerBrokerAll(clusterName, brokerAddr, + brokerName, + brokerId, + "hasServerAddr", + topicConfigSerializeWrapper, + Lists.newArrayList(), + false, + timeOut, + false, + true, + new BrokerIdentity()); + + assertTrue(registerBrokerResultList.size() > 0); + } + + @Test + public void test_register_timeout() throws Exception { + init(); + brokerOuterAPI.start(); + + final RemotingCommand response = RemotingCommand.createResponseCommand(RegisterBrokerResponseHeader.class); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + + TopicConfigSerializeWrapper topicConfigSerializeWrapper = new TopicConfigSerializeWrapper(); + + when(nettyRemotingClient.getAvailableNameSrvList()).thenReturn(Lists.asList(nameserver1, nameserver2, new String[] {nameserver3})); + final ArgumentCaptor timeoutMillisCaptor = ArgumentCaptor.forClass(Long.class); + when(nettyRemotingClient.invokeSync(or(ArgumentMatchers.eq(nameserver1), ArgumentMatchers.eq(nameserver2)), any(RemotingCommand.class), + timeoutMillisCaptor.capture())).thenReturn(response); + when(nettyRemotingClient.invokeSync(ArgumentMatchers.eq(nameserver3), any(RemotingCommand.class), anyLong())).thenThrow(RemotingTimeoutException.class); + List registerBrokerResultList = brokerOuterAPI.registerBrokerAll(clusterName, brokerAddr, brokerName, brokerId, "hasServerAddr", topicConfigSerializeWrapper, Lists.newArrayList(), false, timeOut, false, true, new BrokerIdentity()); + + assertEquals(2, registerBrokerResultList.size()); + } + + @Test + public void testGetBrokerClusterInfo() throws Exception { + init(); + brokerOuterAPI.start(); + + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + + ClusterInfo want = new ClusterInfo(); + want.setBrokerAddrTable(new HashMap<>(Collections.singletonMap("key", new BrokerData("cluster", "broker", new HashMap<>(Collections.singletonMap(MixAll.MASTER_ID, "127.0.0.1:10911")))))); + response.setBody(RemotingSerializable.encode(want)); + + when(nettyRemotingClient.invokeSync(isNull(), argThat(argument -> argument.getCode() == RequestCode.GET_BROKER_CLUSTER_INFO), anyLong())).thenReturn(response); + ClusterInfo got = brokerOuterAPI.getBrokerClusterInfo(); + + assertEquals(want, got); + } + + private RemotingCommand buildResponse(Boolean changed) { + final RemotingCommand response = RemotingCommand.createResponseCommand(QueryDataVersionResponseHeader.class); + final QueryDataVersionResponseHeader responseHeader = (QueryDataVersionResponseHeader) response.readCustomHeader(); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + responseHeader.setChanged(changed); + return response; + } + + @Test + public void testLookupAddressByDomain() throws Exception { + init(); + brokerOuterAPI.start(); + Class clazz = BrokerOuterAPI.class; + Method method = clazz.getDeclaredMethod("dnsLookupAddressByDomain", String.class); + method.setAccessible(true); + List addressList = (List) method.invoke(brokerOuterAPI, "localhost:6789"); + AtomicBoolean result = new AtomicBoolean(false); + addressList.forEach(s -> { + if (s.contains("127.0.0.1:6789")) { + result.set(true); + } + }); + Assert.assertTrue(result.get()); + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/BrokerPathConfigHelperTest.java b/broker/src/test/java/org/apache/rocketmq/broker/BrokerPathConfigHelperTest.java new file mode 100644 index 00000000000..3b260540838 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/BrokerPathConfigHelperTest.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker; + +import java.io.File; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class BrokerPathConfigHelperTest { + + @Test + public void testGetLmqConsumerOffsetPath() { + String lmqConsumerOffsetPath = BrokerPathConfigHelper.getLmqConsumerOffsetPath("/home/admin/store".replace("/", File.separator)); + assertEquals("/home/admin/store/config/lmqConsumerOffset.json".replace("/", File.separator), lmqConsumerOffsetPath); + + String consumerOffsetPath = BrokerPathConfigHelper.getConsumerOffsetPath("/home/admin/store".replace("/", File.separator)); + assertEquals("/home/admin/store/config/consumerOffset.json".replace("/", File.separator), consumerOffsetPath); + + String topicConfigPath = BrokerPathConfigHelper.getTopicConfigPath("/home/admin/store".replace("/", File.separator)); + assertEquals("/home/admin/store/config/topics.json".replace("/", File.separator), topicConfigPath); + + String subscriptionGroupPath = BrokerPathConfigHelper.getSubscriptionGroupPath("/home/admin/store".replace("/", File.separator)); + assertEquals("/home/admin/store/config/subscriptionGroup.json".replace("/", File.separator), subscriptionGroupPath); + + } +} \ No newline at end of file diff --git a/broker/src/test/java/org/apache/rocketmq/broker/BrokerStartupTest.java b/broker/src/test/java/org/apache/rocketmq/broker/BrokerStartupTest.java index c8da08d28df..ce370a39c13 100644 --- a/broker/src/test/java/org/apache/rocketmq/broker/BrokerStartupTest.java +++ b/broker/src/test/java/org/apache/rocketmq/broker/BrokerStartupTest.java @@ -20,6 +20,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Properties; +import org.apache.rocketmq.common.MixAll; import org.junit.Assert; import org.junit.Test; @@ -34,8 +35,21 @@ public void testProperties2SystemEnv() throws NoSuchMethodException, InvocationT Class clazz = BrokerStartup.class; Method method = clazz.getDeclaredMethod("properties2SystemEnv", Properties.class); method.setAccessible(true); - System.setProperty("rocketmq.namesrv.domain", "value"); - method.invoke(null, properties); - Assert.assertEquals("value", System.getProperty("rocketmq.namesrv.domain")); + { + properties.put("rmqAddressServerDomain", "value1"); + properties.put("rmqAddressServerSubGroup", "value2"); + method.invoke(null, properties); + Assert.assertEquals("value1", System.getProperty("rocketmq.namesrv.domain")); + Assert.assertEquals("value2", System.getProperty("rocketmq.namesrv.domain.subgroup")); + } + { + properties.put("rmqAddressServerDomain", MixAll.WS_DOMAIN_NAME); + properties.put("rmqAddressServerSubGroup", MixAll.WS_DOMAIN_SUBGROUP); + method.invoke(null, properties); + Assert.assertEquals(MixAll.WS_DOMAIN_NAME, System.getProperty("rocketmq.namesrv.domain")); + Assert.assertEquals(MixAll.WS_DOMAIN_SUBGROUP, System.getProperty("rocketmq.namesrv.domain.subgroup")); + } + + } } \ No newline at end of file diff --git a/broker/src/test/java/org/apache/rocketmq/broker/client/ConsumerManagerScannerTest.java b/broker/src/test/java/org/apache/rocketmq/broker/client/ConsumerManagerScannerTest.java new file mode 100644 index 00000000000..d190c0daceb --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/client/ConsumerManagerScannerTest.java @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.client; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.remoting.protocol.LanguageCode; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ConsumerManagerScannerTest { + private ConsumerManager consumerManager; + private String group = "FooBar"; + private String clientId = "clientId"; + private ClientChannelInfo clientInfo; + private Map> groupEventListMap = new HashMap<>(); + + @Mock + private Channel channel; + + @Before + public void init() { + clientInfo = new ClientChannelInfo(channel, clientId, LanguageCode.JAVA, 0); + + consumerManager = new ConsumerManager(new ConsumerIdsChangeListener() { + @Override + public void handle(ConsumerGroupEvent event, String group, Object... args) { + groupEventListMap.compute(event, (eventKey, dataListVal) -> { + if (dataListVal == null) { + dataListVal = new ArrayList<>(); + } + dataListVal.add(new ConsumerIdsChangeListenerData(event, group, args)); + return dataListVal; + }); + } + + @Override + public void shutdown() { + + } + }, 1000 * 120); + } + + private static class ConsumerIdsChangeListenerData { + private ConsumerGroupEvent event; + private String group; + private Object[] args; + + public ConsumerIdsChangeListenerData(ConsumerGroupEvent event, String group, Object[] args) { + this.event = event; + this.group = group; + this.args = args; + } + } + + @Test + public void testClientUnregisterEventInDoChannelCloseEvent() { + assertThat(consumerManager.registerConsumer( + group, + clientInfo, + ConsumeType.CONSUME_PASSIVELY, + MessageModel.CLUSTERING, + ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET, + new HashSet<>(), + false + )).isTrue(); + + consumerManager.doChannelCloseEvent("remoteAddr", channel); + + assertThat(groupEventListMap.get(ConsumerGroupEvent.CLIENT_UNREGISTER).size()).isEqualTo(1); + assertThat(groupEventListMap.get(ConsumerGroupEvent.CLIENT_UNREGISTER).get(0).args[0]).isInstanceOf(ClientChannelInfo.class); + ClientChannelInfo clientChannelInfo = (ClientChannelInfo) groupEventListMap.get(ConsumerGroupEvent.CLIENT_UNREGISTER).get(0).args[0]; + assertThat(clientChannelInfo).isSameAs(clientInfo); + } + + @Test + public void testClientUnregisterEventInUnregisterConsumer() { + assertThat(consumerManager.registerConsumer( + group, + clientInfo, + ConsumeType.CONSUME_PASSIVELY, + MessageModel.CLUSTERING, + ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET, + new HashSet<>(), + false + )).isTrue(); + + consumerManager.unregisterConsumer(group, clientInfo, false); + + assertThat(groupEventListMap.get(ConsumerGroupEvent.CLIENT_UNREGISTER).size()).isEqualTo(1); + assertThat(groupEventListMap.get(ConsumerGroupEvent.CLIENT_UNREGISTER).get(0).args[0]).isInstanceOf(ClientChannelInfo.class); + ClientChannelInfo clientChannelInfo = (ClientChannelInfo) groupEventListMap.get(ConsumerGroupEvent.CLIENT_UNREGISTER).get(0).args[0]; + assertThat(clientChannelInfo).isSameAs(clientInfo); + } + + @Test + public void testClientUnregisterEventInScanNotActiveChannel() { + assertThat(consumerManager.registerConsumer( + group, + clientInfo, + ConsumeType.CONSUME_PASSIVELY, + MessageModel.CLUSTERING, + ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET, + new HashSet<>(), + false + )).isTrue(); + clientInfo.setLastUpdateTimestamp(0); + when(channel.close()).thenReturn(mock(ChannelFuture.class)); + + consumerManager.scanNotActiveChannel(); + assertThat(groupEventListMap.get(ConsumerGroupEvent.CLIENT_UNREGISTER).size()).isEqualTo(1); + assertThat(groupEventListMap.get(ConsumerGroupEvent.CLIENT_UNREGISTER).get(0).args[0]).isInstanceOf(ClientChannelInfo.class); + ClientChannelInfo clientChannelInfo = (ClientChannelInfo) groupEventListMap.get(ConsumerGroupEvent.CLIENT_UNREGISTER).get(0).args[0]; + assertThat(clientChannelInfo).isSameAs(clientInfo); + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/client/ConsumerManagerTest.java b/broker/src/test/java/org/apache/rocketmq/broker/client/ConsumerManagerTest.java new file mode 100644 index 00000000000..8c909824348 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/client/ConsumerManagerTest.java @@ -0,0 +1,210 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.client; + +import io.netty.channel.Channel; +import java.util.HashSet; +import java.util.Set; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.client.net.Broker2Client; +import org.apache.rocketmq.broker.filter.ConsumerFilterManager; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.remoting.protocol.LanguageCode; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.store.stats.BrokerStatsManager; +import org.assertj.core.api.Assertions; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ConsumerManagerTest { + + private ClientChannelInfo clientChannelInfo; + + @Mock + private Channel channel; + + private ConsumerManager consumerManager; + + private DefaultConsumerIdsChangeListener defaultConsumerIdsChangeListener; + + @Mock + private BrokerController brokerController; + + @Mock + private ConsumerFilterManager consumerFilterManager; + + private BrokerConfig brokerConfig = new BrokerConfig(); + + private Broker2Client broker2Client; + + private BrokerStatsManager brokerStatsManager; + + private static final String GROUP = "DEFAULT_GROUP"; + + private static final String CLIENT_ID = "1"; + + private static final int VERSION = 1; + + private static final String TOPIC = "DEFAULT_TOPIC"; + + @Before + public void before() { + clientChannelInfo = new ClientChannelInfo(channel, CLIENT_ID, LanguageCode.JAVA, VERSION); + defaultConsumerIdsChangeListener = new DefaultConsumerIdsChangeListener(brokerController); + brokerStatsManager = new BrokerStatsManager(brokerConfig); + consumerManager = new ConsumerManager(defaultConsumerIdsChangeListener, brokerStatsManager, brokerConfig); + broker2Client = new Broker2Client(brokerController); + when(brokerController.getConsumerFilterManager()).thenReturn(consumerFilterManager); + when(brokerController.getBrokerConfig()).thenReturn(brokerConfig); + when(brokerController.getBroker2Client()).thenReturn(broker2Client); + } + + @Test + public void compensateBasicConsumerInfoTest() { + ConsumerGroupInfo consumerGroupInfo = consumerManager.getConsumerGroupInfo(GROUP, true); + Assertions.assertThat(consumerGroupInfo).isNull(); + + consumerManager.compensateBasicConsumerInfo(GROUP, ConsumeType.CONSUME_ACTIVELY, MessageModel.BROADCASTING); + consumerGroupInfo = consumerManager.getConsumerGroupInfo(GROUP, true); + Assertions.assertThat(consumerGroupInfo).isNotNull(); + Assertions.assertThat(consumerGroupInfo.getConsumeType()).isEqualTo(ConsumeType.CONSUME_ACTIVELY); + Assertions.assertThat(consumerGroupInfo.getMessageModel()).isEqualTo(MessageModel.BROADCASTING); + } + + @Test + public void compensateSubscribeDataTest() { + ConsumerGroupInfo consumerGroupInfo = consumerManager.getConsumerGroupInfo(GROUP, true); + Assertions.assertThat(consumerGroupInfo).isNull(); + + consumerManager.compensateSubscribeData(GROUP, TOPIC, new SubscriptionData(TOPIC, SubscriptionData.SUB_ALL)); + consumerGroupInfo = consumerManager.getConsumerGroupInfo(GROUP, true); + Assertions.assertThat(consumerGroupInfo).isNotNull(); + Assertions.assertThat(consumerGroupInfo.getSubscriptionTable().size()).isEqualTo(1); + SubscriptionData subscriptionData = consumerGroupInfo.getSubscriptionTable().get(TOPIC); + Assertions.assertThat(subscriptionData).isNotNull(); + Assertions.assertThat(subscriptionData.getTopic()).isEqualTo(TOPIC); + Assertions.assertThat(subscriptionData.getSubString()).isEqualTo(SubscriptionData.SUB_ALL); + } + + @Test + public void registerConsumerTest() { + register(); + final Set subList = new HashSet<>(); + SubscriptionData subscriptionData = new SubscriptionData(TOPIC, "*"); + subList.add(subscriptionData); + consumerManager.registerConsumer(GROUP, clientChannelInfo, ConsumeType.CONSUME_PASSIVELY, + MessageModel.BROADCASTING, ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET, subList, true); + Assertions.assertThat(consumerManager.getConsumerTable().get(GROUP)).isNotNull(); + } + + @Test + public void unregisterConsumerTest() { + // register + register(); + + // unregister + consumerManager.unregisterConsumer(GROUP, clientChannelInfo, true); + Assertions.assertThat(consumerManager.getConsumerTable().get(GROUP)).isNull(); + } + + @Test + public void findChannelTest() { + register(); + final ClientChannelInfo consumerManagerChannel = consumerManager.findChannel(GROUP, CLIENT_ID); + Assertions.assertThat(consumerManagerChannel).isNotNull(); + } + + @Test + public void findSubscriptionDataTest() { + register(); + final SubscriptionData subscriptionData = consumerManager.findSubscriptionData(GROUP, TOPIC); + Assertions.assertThat(subscriptionData).isNotNull(); + } + + @Test + public void findSubscriptionDataCountTest() { + register(); + final int count = consumerManager.findSubscriptionDataCount(GROUP); + assert count > 0; + } + + @Test + public void findSubscriptionTest() { + SubscriptionData subscriptionData = consumerManager.findSubscriptionData(GROUP, TOPIC, true); + Assertions.assertThat(subscriptionData).isNull(); + + consumerManager.compensateSubscribeData(GROUP, TOPIC, new SubscriptionData(TOPIC, SubscriptionData.SUB_ALL)); + subscriptionData = consumerManager.findSubscriptionData(GROUP, TOPIC, true); + Assertions.assertThat(subscriptionData).isNotNull(); + Assertions.assertThat(subscriptionData.getTopic()).isEqualTo(TOPIC); + Assertions.assertThat(subscriptionData.getSubString()).isEqualTo(SubscriptionData.SUB_ALL); + + subscriptionData = consumerManager.findSubscriptionData(GROUP, TOPIC, false); + Assertions.assertThat(subscriptionData).isNull(); + } + + @Test + public void scanNotActiveChannelTest() { + clientChannelInfo.setLastUpdateTimestamp(System.currentTimeMillis() - brokerConfig.getChannelExpiredTimeout() * 2); + consumerManager.scanNotActiveChannel(); + Assertions.assertThat(consumerManager.getConsumerTable().size()).isEqualTo(0); + } + + @Test + public void queryTopicConsumeByWhoTest() { + register(); + final HashSet consumeGroup = consumerManager.queryTopicConsumeByWho(TOPIC); + assert consumeGroup.size() > 0; + } + + @Test + public void doChannelCloseEventTest() { + consumerManager.doChannelCloseEvent("127.0.0.1", channel); + assert consumerManager.getConsumerTable().size() == 0; + } + + private void register() { + // register + final Set subList = new HashSet<>(); + SubscriptionData subscriptionData = new SubscriptionData(TOPIC, "*"); + subList.add(subscriptionData); + consumerManager.registerConsumer(GROUP, clientChannelInfo, ConsumeType.CONSUME_PASSIVELY, + MessageModel.BROADCASTING, ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET, subList, true); + } + + @Test + public void removeExpireConsumerGroupInfo() { + SubscriptionData subscriptionData = new SubscriptionData(TOPIC, SubscriptionData.SUB_ALL); + subscriptionData.setSubVersion(System.currentTimeMillis() - brokerConfig.getSubscriptionExpiredTimeout() * 2); + consumerManager.compensateSubscribeData(GROUP, TOPIC, subscriptionData); + consumerManager.compensateSubscribeData(GROUP, TOPIC + "_1", new SubscriptionData(TOPIC, SubscriptionData.SUB_ALL)); + consumerManager.removeExpireConsumerGroupInfo(); + Assertions.assertThat(consumerManager.getConsumerGroupInfo(GROUP, true)).isNotNull(); + Assertions.assertThat(consumerManager.findSubscriptionData(GROUP, TOPIC)).isNull(); + Assertions.assertThat(consumerManager.findSubscriptionData(GROUP, TOPIC + "_1")).isNotNull(); + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/client/ProducerManagerTest.java b/broker/src/test/java/org/apache/rocketmq/broker/client/ProducerManagerTest.java index 08dbb9c754c..3d6091e02fb 100644 --- a/broker/src/test/java/org/apache/rocketmq/broker/client/ProducerManagerTest.java +++ b/broker/src/test/java/org/apache/rocketmq/broker/client/ProducerManagerTest.java @@ -19,11 +19,15 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import java.lang.reflect.Field; -import java.util.HashMap; +import java.util.Map; + +import java.util.concurrent.atomic.AtomicReference; +import org.apache.rocketmq.remoting.protocol.LanguageCode; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import static org.assertj.core.api.Assertions.assertThat; @@ -42,49 +46,168 @@ public class ProducerManagerTest { @Before public void init() { producerManager = new ProducerManager(); - clientInfo = new ClientChannelInfo(channel); + clientInfo = new ClientChannelInfo(channel, "clientId", LanguageCode.JAVA, 0); } @Test public void scanNotActiveChannel() throws Exception { producerManager.registerProducer(group, clientInfo); + AtomicReference groupRef = new AtomicReference<>(); + AtomicReference clientChannelInfoRef = new AtomicReference<>(); + producerManager.appendProducerChangeListener((event, group, clientChannelInfo) -> { + switch (event) { + case GROUP_UNREGISTER: + groupRef.set(group); + break; + case CLIENT_UNREGISTER: + clientChannelInfoRef.set(clientChannelInfo); + break; + default: + break; + } + }); assertThat(producerManager.getGroupChannelTable().get(group).get(channel)).isNotNull(); + assertThat(producerManager.findChannel("clientId")).isNotNull(); + Field field = ProducerManager.class.getDeclaredField("CHANNEL_EXPIRED_TIMEOUT"); + field.setAccessible(true); + long channelExpiredTimeout = field.getLong(producerManager); + clientInfo.setLastUpdateTimestamp(System.currentTimeMillis() - channelExpiredTimeout - 10); + when(channel.close()).thenReturn(mock(ChannelFuture.class)); + producerManager.scanNotActiveChannel(); + assertThat(producerManager.getGroupChannelTable().get(group)).isNull(); + assertThat(groupRef.get()).isEqualTo(group); + assertThat(clientChannelInfoRef.get()).isSameAs(clientInfo); + assertThat(producerManager.findChannel("clientId")).isNull(); + } + @Test + public void scanNotActiveChannelWithSameClientId() throws Exception { + producerManager.registerProducer(group, clientInfo); + Channel channel1 = Mockito.mock(Channel.class); + ClientChannelInfo clientInfo1 = new ClientChannelInfo(channel1, clientInfo.getClientId(), LanguageCode.JAVA, 0); + producerManager.registerProducer(group, clientInfo1); + AtomicReference groupRef = new AtomicReference<>(); + AtomicReference clientChannelInfoRef = new AtomicReference<>(); + producerManager.appendProducerChangeListener((event, group, clientChannelInfo) -> { + switch (event) { + case GROUP_UNREGISTER: + groupRef.set(group); + break; + case CLIENT_UNREGISTER: + clientChannelInfoRef.set(clientChannelInfo); + break; + default: + break; + } + }); + assertThat(producerManager.getGroupChannelTable().get(group).get(channel)).isNotNull(); + assertThat(producerManager.getGroupChannelTable().get(group).get(channel1)).isNotNull(); + assertThat(producerManager.findChannel("clientId")).isNotNull(); Field field = ProducerManager.class.getDeclaredField("CHANNEL_EXPIRED_TIMEOUT"); field.setAccessible(true); - long CHANNEL_EXPIRED_TIMEOUT = field.getLong(producerManager); - clientInfo.setLastUpdateTimestamp(System.currentTimeMillis() - CHANNEL_EXPIRED_TIMEOUT - 10); + long channelExpiredTimeout = field.getLong(producerManager); + clientInfo.setLastUpdateTimestamp(System.currentTimeMillis() - channelExpiredTimeout - 10); when(channel.close()).thenReturn(mock(ChannelFuture.class)); producerManager.scanNotActiveChannel(); - assertThat(producerManager.getGroupChannelTable().get(group).get(channel)).isNull(); + assertThat(producerManager.getGroupChannelTable().get(group).get(channel1)).isNotNull(); + assertThat(producerManager.findChannel("clientId")).isNotNull(); } @Test public void doChannelCloseEvent() throws Exception { producerManager.registerProducer(group, clientInfo); + AtomicReference groupRef = new AtomicReference<>(); + AtomicReference clientChannelInfoRef = new AtomicReference<>(); + producerManager.appendProducerChangeListener((event, group, clientChannelInfo) -> { + switch (event) { + case GROUP_UNREGISTER: + groupRef.set(group); + break; + case CLIENT_UNREGISTER: + clientChannelInfoRef.set(clientChannelInfo); + break; + default: + break; + } + }); assertThat(producerManager.getGroupChannelTable().get(group).get(channel)).isNotNull(); + assertThat(producerManager.findChannel("clientId")).isNotNull(); producerManager.doChannelCloseEvent("127.0.0.1", channel); - assertThat(producerManager.getGroupChannelTable().get(group).get(channel)).isNull(); + assertThat(producerManager.getGroupChannelTable().get(group)).isNull(); + assertThat(groupRef.get()).isEqualTo(group); + assertThat(clientChannelInfoRef.get()).isSameAs(clientInfo); + assertThat(producerManager.findChannel("clientId")).isNull(); } @Test public void testRegisterProducer() throws Exception { producerManager.registerProducer(group, clientInfo); - HashMap channelMap = producerManager.getGroupChannelTable().get(group); + Map channelMap = producerManager.getGroupChannelTable().get(group); + Channel channel1 = producerManager.findChannel("clientId"); assertThat(channelMap).isNotNull(); + assertThat(channel1).isNotNull(); assertThat(channelMap.get(channel)).isEqualTo(clientInfo); + assertThat(channel1).isEqualTo(channel); } @Test public void unregisterProducer() throws Exception { producerManager.registerProducer(group, clientInfo); - HashMap channelMap = producerManager.getGroupChannelTable().get(group); + AtomicReference groupRef = new AtomicReference<>(); + AtomicReference clientChannelInfoRef = new AtomicReference<>(); + producerManager.appendProducerChangeListener((event, group, clientChannelInfo) -> { + switch (event) { + case GROUP_UNREGISTER: + groupRef.set(group); + break; + case CLIENT_UNREGISTER: + clientChannelInfoRef.set(clientChannelInfo); + break; + default: + break; + } + }); + Map channelMap = producerManager.getGroupChannelTable().get(group); assertThat(channelMap).isNotNull(); assertThat(channelMap.get(channel)).isEqualTo(clientInfo); - + Channel channel1 = producerManager.findChannel("clientId"); + assertThat(channel1).isNotNull(); + assertThat(channel1).isEqualTo(channel); producerManager.unregisterProducer(group, clientInfo); channelMap = producerManager.getGroupChannelTable().get(group); + channel1 = producerManager.findChannel("clientId"); + assertThat(groupRef.get()).isEqualTo(group); + assertThat(clientChannelInfoRef.get()).isSameAs(clientInfo); assertThat(channelMap).isNull(); + assertThat(channel1).isNull(); + + } + + @Test + public void testGetGroupChannelTable() throws Exception { + producerManager.registerProducer(group, clientInfo); + Map oldMap = producerManager.getGroupChannelTable().get(group); + + producerManager.unregisterProducer(group, clientInfo); + assertThat(oldMap.size()).isEqualTo(0); + } + + @Test + public void testGetAvailableChannel() { + producerManager.registerProducer(group, clientInfo); + + when(channel.isActive()).thenReturn(true); + when(channel.isWritable()).thenReturn(true); + Channel c = producerManager.getAvailableChannel(group); + assertThat(c).isSameAs(channel); + + when(channel.isWritable()).thenReturn(false); + c = producerManager.getAvailableChannel(group); + assertThat(c).isSameAs(channel); + + when(channel.isActive()).thenReturn(false); + c = producerManager.getAvailableChannel(group); + assertThat(c).isNull(); } } \ No newline at end of file diff --git a/broker/src/test/java/org/apache/rocketmq/broker/controller/ReplicasManagerRegisterTest.java b/broker/src/test/java/org/apache/rocketmq/broker/controller/ReplicasManagerRegisterTest.java new file mode 100644 index 00000000000..d01a6f76f5e --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/controller/ReplicasManagerRegisterTest.java @@ -0,0 +1,352 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.controller; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.out.BrokerOuterAPI; +import org.apache.rocketmq.broker.slave.SlaveSynchronize; +import org.apache.rocketmq.broker.topic.TopicConfigManager; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetMetaDataResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.ApplyBrokerIdResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.GetNextBrokerIdResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.RegisterBrokerToControllerResponseHeader; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.RunningFlags; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.ha.autoswitch.AutoSwitchHAService; +import org.apache.rocketmq.store.ha.autoswitch.BrokerMetadata; +import org.apache.rocketmq.store.ha.autoswitch.TempBrokerMetadata; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.io.File; +import java.time.Duration; +import java.util.Arrays; +import java.util.HashSet; +import java.util.UUID; + +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(ReplicasManager.class) +public class ReplicasManagerRegisterTest { + + public static final String STORE_BASE_PATH = System.getProperty("java.io.tmpdir") + File.separator + "ReplicasManagerRegisterTest"; + + public static final String STORE_PATH = STORE_BASE_PATH + File.separator + UUID.randomUUID(); + + public static final String BROKER_NAME = "default-broker"; + + public static final String CLUSTER_NAME = "default-cluster"; + + public static final String NAME_SRV_ADDR = "127.0.0.1:9999"; + + public static final String CONTROLLER_ADDR = "127.0.0.1:8888"; + + public static final BrokerConfig BROKER_CONFIG; + private final HashSet syncStateSet = new HashSet<>(Arrays.asList(1L)); + + static { + BROKER_CONFIG = new BrokerConfig(); + BROKER_CONFIG.setListenPort(21030); + BROKER_CONFIG.setNamesrvAddr(NAME_SRV_ADDR); + BROKER_CONFIG.setControllerAddr(CONTROLLER_ADDR); + BROKER_CONFIG.setSyncControllerMetadataPeriod(2 * 1000); + BROKER_CONFIG.setEnableControllerMode(true); + BROKER_CONFIG.setBrokerName(BROKER_NAME); + BROKER_CONFIG.setBrokerClusterName(CLUSTER_NAME); + } + + private MessageStoreConfig buildMessageStoreConfig(int id) { + MessageStoreConfig config = new MessageStoreConfig(); + config.setStorePathRootDir(STORE_PATH + File.separator + id); + config.setStorePathCommitLog(config.getStorePathRootDir() + File.separator + "commitLog"); + config.setStorePathEpochFile(config.getStorePathRootDir() + File.separator + "epochFileCache"); + config.setStorePathBrokerIdentity(config.getStorePathRootDir() + File.separator + "brokerIdentity"); + return config; + } + + private BrokerController mockedBrokerController; + + private DefaultMessageStore mockedMessageStore; + + private BrokerOuterAPI mockedBrokerOuterAPI; + + private AutoSwitchHAService mockedAutoSwitchHAService; + + private RunningFlags runningFlags = new RunningFlags(); + + @Before + public void setUp() throws Exception { + UtilAll.deleteFile(new File(STORE_BASE_PATH)); + this.mockedBrokerController = Mockito.mock(BrokerController.class); + this.mockedMessageStore = Mockito.mock(DefaultMessageStore.class); + this.mockedBrokerOuterAPI = Mockito.mock(BrokerOuterAPI.class); + this.mockedAutoSwitchHAService = Mockito.mock(AutoSwitchHAService.class); + TopicConfigManager mockedTopicConfigManager = new TopicConfigManager(); + when(mockedBrokerController.getBrokerOuterAPI()).thenReturn(mockedBrokerOuterAPI); + when(mockedBrokerController.getMessageStore()).thenReturn(mockedMessageStore); + when(mockedBrokerController.getBrokerConfig()).thenReturn(BROKER_CONFIG); + when(mockedBrokerController.getTopicConfigManager()).thenReturn(mockedTopicConfigManager); + when(mockedMessageStore.getHaService()).thenReturn(mockedAutoSwitchHAService); + when(mockedMessageStore.getRunningFlags()).thenReturn(runningFlags); + when(mockedBrokerController.getSlaveSynchronize()).thenReturn(new SlaveSynchronize(mockedBrokerController)); + when(mockedBrokerOuterAPI.getControllerMetaData(any())).thenReturn( + new GetMetaDataResponseHeader("default-group", "dledger-a", CONTROLLER_ADDR, true, CONTROLLER_ADDR)); + when(mockedBrokerOuterAPI.checkAddressReachable(any())).thenReturn(true); + when(mockedBrokerController.getMessageStoreConfig()).thenReturn(buildMessageStoreConfig(0)); + } + + @Test + public void testBrokerRegisterSuccess() throws Exception { + + when(mockedBrokerOuterAPI.getNextBrokerId(any(), any(), any())).thenReturn(new GetNextBrokerIdResponseHeader(CLUSTER_NAME, BROKER_NAME, 1L)); + when(mockedBrokerOuterAPI.applyBrokerId(any(), any(), anyLong(), any(), any())).thenReturn(new ApplyBrokerIdResponseHeader()); + when(mockedBrokerOuterAPI.registerBrokerToController(any(), any(), anyLong(), any(), any())).thenReturn(new Pair<>(new RegisterBrokerToControllerResponseHeader(), syncStateSet)); + when(mockedBrokerOuterAPI.brokerElect(any(), any(), any(), anyLong())).thenReturn(new Pair<>(new ElectMasterResponseHeader(1L, "127.0.0.1:13131", 1, 1), syncStateSet)); + + ReplicasManager replicasManager0 = new ReplicasManager(mockedBrokerController); + replicasManager0.start(); + await().atMost(Duration.ofMillis(1000)).until(() -> + replicasManager0.getState() == ReplicasManager.State.RUNNING + ); + Assert.assertEquals(ReplicasManager.RegisterState.REGISTERED, replicasManager0.getRegisterState()); + Assert.assertEquals(1L, replicasManager0.getBrokerControllerId().longValue()); + checkMetadataFile(replicasManager0.getBrokerMetadata(), 1L); + Assert.assertFalse(replicasManager0.getTempBrokerMetadata().isLoaded()); + Assert.assertFalse(replicasManager0.getTempBrokerMetadata().fileExists()); + replicasManager0.shutdown(); + } + + @Test + public void testBrokerRegisterSuccessAndRestartWithChangedBrokerConfig() throws Exception { + when(mockedBrokerOuterAPI.getNextBrokerId(any(), any(), any())).thenReturn(new GetNextBrokerIdResponseHeader(CLUSTER_NAME, BROKER_NAME, 1L)); + when(mockedBrokerOuterAPI.applyBrokerId(any(), any(), anyLong(), any(), any())).thenReturn(new ApplyBrokerIdResponseHeader()); + when(mockedBrokerOuterAPI.registerBrokerToController(any(), any(), anyLong(), any(), any())).thenReturn(new Pair<>(new RegisterBrokerToControllerResponseHeader(), syncStateSet)); + when(mockedBrokerOuterAPI.brokerElect(any(), any(), any(), anyLong())).thenReturn(new Pair<>(new ElectMasterResponseHeader(1L, "127.0.0.1:13131", 1, 1), syncStateSet)); + + ReplicasManager replicasManager0 = new ReplicasManager(mockedBrokerController); + replicasManager0.start(); + await().atMost(Duration.ofMillis(1000)).until(() -> + replicasManager0.getState() == ReplicasManager.State.RUNNING + ); + Assert.assertEquals(ReplicasManager.RegisterState.REGISTERED, replicasManager0.getRegisterState()); + Assert.assertEquals(1L, replicasManager0.getBrokerControllerId().longValue()); + checkMetadataFile(replicasManager0.getBrokerMetadata(), 1L); + Assert.assertFalse(replicasManager0.getTempBrokerMetadata().isLoaded()); + Assert.assertFalse(replicasManager0.getTempBrokerMetadata().fileExists()); + replicasManager0.shutdown(); + + // change broker name in broker config + mockedBrokerController.getBrokerConfig().setBrokerName(BROKER_NAME + "1"); + ReplicasManager replicasManagerRestart = new ReplicasManager(mockedBrokerController); + replicasManagerRestart.start(); + Assert.assertEquals(ReplicasManager.RegisterState.CREATE_METADATA_FILE_DONE, replicasManagerRestart.getRegisterState()); + mockedBrokerController.getBrokerConfig().setBrokerName(BROKER_NAME); + replicasManagerRestart.shutdown(); + + // change cluster name in broker config + mockedBrokerController.getBrokerConfig().setBrokerClusterName(CLUSTER_NAME + "1"); + replicasManagerRestart = new ReplicasManager(mockedBrokerController); + replicasManagerRestart.start(); + Assert.assertEquals(ReplicasManager.RegisterState.CREATE_METADATA_FILE_DONE, replicasManagerRestart.getRegisterState()); + mockedBrokerController.getBrokerConfig().setBrokerClusterName(CLUSTER_NAME); + replicasManagerRestart.shutdown(); + } + + @Test + public void testRegisterFailedAtGetNextBrokerId() throws Exception { + ReplicasManager replicasManager = new ReplicasManager(mockedBrokerController); + when(mockedBrokerOuterAPI.getNextBrokerId(any(), any(), any())).thenThrow(new RuntimeException()); + + replicasManager.start(); + + Assert.assertEquals(ReplicasManager.State.FIRST_TIME_SYNC_CONTROLLER_METADATA_DONE, replicasManager.getState()); + Assert.assertEquals(ReplicasManager.RegisterState.INITIAL, replicasManager.getRegisterState()); + Assert.assertFalse(replicasManager.getTempBrokerMetadata().fileExists()); + Assert.assertFalse(replicasManager.getBrokerMetadata().fileExists()); + Assert.assertNull(replicasManager.getBrokerControllerId()); + replicasManager.shutdown(); + } + + @Test + public void testRegisterFailedAtCreateTempFile() throws Exception { + ReplicasManager replicasManager = new ReplicasManager(mockedBrokerController); + when(mockedBrokerOuterAPI.getNextBrokerId(any(), any(), any())).thenReturn(new GetNextBrokerIdResponseHeader(CLUSTER_NAME, BROKER_NAME, 1L)); + when(mockedBrokerOuterAPI.applyBrokerId(any(), any(), anyLong(), any(), any())).thenReturn(new ApplyBrokerIdResponseHeader()); + when(mockedBrokerOuterAPI.registerBrokerToController(any(), any(), anyLong(), any(), any())).thenReturn(new Pair<>(new RegisterBrokerToControllerResponseHeader(), syncStateSet)); + when(mockedBrokerOuterAPI.brokerElect(any(), any(), any(), anyLong())).thenReturn(new Pair<>(new ElectMasterResponseHeader(1L, "127.0.0.1:13131", 1, 1), syncStateSet)); + ReplicasManager spyReplicasManager = PowerMockito.spy(replicasManager); + PowerMockito.doReturn(false).when(spyReplicasManager, "createTempMetadataFile", anyLong()); + + spyReplicasManager.start(); + + Assert.assertEquals(ReplicasManager.State.FIRST_TIME_SYNC_CONTROLLER_METADATA_DONE, spyReplicasManager.getState()); + Assert.assertEquals(ReplicasManager.RegisterState.INITIAL, spyReplicasManager.getRegisterState()); + Assert.assertFalse(spyReplicasManager.getTempBrokerMetadata().fileExists()); + Assert.assertFalse(spyReplicasManager.getBrokerMetadata().fileExists()); + Assert.assertNull(spyReplicasManager.getBrokerControllerId()); + spyReplicasManager.shutdown(); + } + + @Test + public void testRegisterFailedAtApplyBrokerIdFailed() throws Exception { + ReplicasManager replicasManager = new ReplicasManager(mockedBrokerController); + when(mockedBrokerOuterAPI.getNextBrokerId(any(), any(), any())).thenReturn(new GetNextBrokerIdResponseHeader(CLUSTER_NAME, BROKER_NAME, 1L)); + when(mockedBrokerOuterAPI.applyBrokerId(any(), any(), anyLong(), any(), any())).thenThrow(new RuntimeException()); + when(mockedBrokerOuterAPI.registerBrokerToController(any(), any(), anyLong(), any(), any())).thenReturn(new Pair<>(new RegisterBrokerToControllerResponseHeader(), syncStateSet)); + when(mockedBrokerOuterAPI.brokerElect(any(), any(), any(), anyLong())).thenReturn(new Pair<>(new ElectMasterResponseHeader(1L, "127.0.0.1:13131", 1, 1), syncStateSet)); + + replicasManager.start(); + + Assert.assertEquals(ReplicasManager.State.FIRST_TIME_SYNC_CONTROLLER_METADATA_DONE, replicasManager.getState()); + Assert.assertNotEquals(ReplicasManager.RegisterState.CREATE_METADATA_FILE_DONE, replicasManager.getRegisterState()); + Assert.assertNotEquals(ReplicasManager.RegisterState.REGISTERED, replicasManager.getRegisterState()); + + replicasManager.shutdown(); + + Assert.assertFalse(replicasManager.getBrokerMetadata().fileExists()); + Assert.assertNull(replicasManager.getBrokerControllerId()); + } + + @Test + public void testRegisterFailedAtCreateMetadataFileAndDeleteTemp() throws Exception { + ReplicasManager replicasManager = new ReplicasManager(mockedBrokerController); + when(mockedBrokerOuterAPI.getNextBrokerId(any(), any(), any())).thenReturn(new GetNextBrokerIdResponseHeader(CLUSTER_NAME, BROKER_NAME, 1L)); + when(mockedBrokerOuterAPI.applyBrokerId(any(), any(), anyLong(), any(), any())).thenReturn(new ApplyBrokerIdResponseHeader()); + when(mockedBrokerOuterAPI.registerBrokerToController(any(), any(), anyLong(), any(), any())).thenReturn(new Pair<>(new RegisterBrokerToControllerResponseHeader(), syncStateSet)); + when(mockedBrokerOuterAPI.brokerElect(any(), any(), any(), anyLong())).thenReturn(new Pair<>(new ElectMasterResponseHeader(1L, "127.0.0.1:13131", 1, 1), syncStateSet)); + + ReplicasManager spyReplicasManager = PowerMockito.spy(replicasManager); + PowerMockito.doReturn(false).when(spyReplicasManager, "createMetadataFileAndDeleteTemp"); + + spyReplicasManager.start(); + + Assert.assertEquals(ReplicasManager.State.FIRST_TIME_SYNC_CONTROLLER_METADATA_DONE, spyReplicasManager.getState()); + Assert.assertEquals(ReplicasManager.RegisterState.CREATE_TEMP_METADATA_FILE_DONE, spyReplicasManager.getRegisterState()); + TempBrokerMetadata tempBrokerMetadata = spyReplicasManager.getTempBrokerMetadata(); + Assert.assertTrue(tempBrokerMetadata.fileExists()); + Assert.assertTrue(tempBrokerMetadata.isLoaded()); + Assert.assertFalse(spyReplicasManager.getBrokerMetadata().fileExists()); + Assert.assertNull(spyReplicasManager.getBrokerControllerId()); + + spyReplicasManager.shutdown(); + + // restart, we expect that this replicasManager still keep the tempMetadata and still try to finish its registering + ReplicasManager replicasManagerNew = new ReplicasManager(mockedBrokerController); + // because apply brokerId: 1 has succeeded, so now next broker id is 2 + when(mockedBrokerOuterAPI.getNextBrokerId(any(), any(), any())).thenReturn(new GetNextBrokerIdResponseHeader(CLUSTER_NAME, BROKER_NAME, 2L)); + + replicasManagerNew.start(); + + Assert.assertEquals(ReplicasManager.State.RUNNING, replicasManagerNew.getState()); + Assert.assertEquals(ReplicasManager.RegisterState.REGISTERED, replicasManagerNew.getRegisterState()); + // tempMetadata has been cleared + Assert.assertFalse(replicasManagerNew.getTempBrokerMetadata().fileExists()); + Assert.assertFalse(replicasManagerNew.getTempBrokerMetadata().isLoaded()); + // metadata has been persisted + Assert.assertTrue(replicasManagerNew.getBrokerMetadata().fileExists()); + Assert.assertTrue(replicasManagerNew.getBrokerMetadata().isLoaded()); + Assert.assertEquals(1L, replicasManagerNew.getBrokerMetadata().getBrokerId().longValue()); + Assert.assertEquals(1L, replicasManagerNew.getBrokerControllerId().longValue()); + replicasManagerNew.shutdown(); + } + + @Test + public void testRegisterFailedAtRegisterSuccess() throws Exception { + ReplicasManager replicasManager = new ReplicasManager(mockedBrokerController); + when(mockedBrokerOuterAPI.getNextBrokerId(any(), any(), any())).thenReturn(new GetNextBrokerIdResponseHeader(CLUSTER_NAME, BROKER_NAME, 1L)); + when(mockedBrokerOuterAPI.applyBrokerId(any(), any(), anyLong(), any(), any())).thenReturn(new ApplyBrokerIdResponseHeader()); + when(mockedBrokerOuterAPI.registerBrokerToController(any(), any(), anyLong(), any(), any())).thenThrow(new RuntimeException()); + when(mockedBrokerOuterAPI.brokerElect(any(), any(), any(), anyLong())).thenReturn(new Pair<>(new ElectMasterResponseHeader(1L, "127.0.0.1:13131", 1, 1), syncStateSet)); + + replicasManager.start(); + + Assert.assertEquals(ReplicasManager.State.FIRST_TIME_SYNC_CONTROLLER_METADATA_DONE, replicasManager.getState()); + Assert.assertEquals(ReplicasManager.RegisterState.CREATE_METADATA_FILE_DONE, replicasManager.getRegisterState()); + TempBrokerMetadata tempBrokerMetadata = replicasManager.getTempBrokerMetadata(); + // temp metadata has been cleared + Assert.assertFalse(tempBrokerMetadata.fileExists()); + Assert.assertFalse(tempBrokerMetadata.isLoaded()); + // metadata has been persisted + Assert.assertTrue(replicasManager.getBrokerMetadata().fileExists()); + Assert.assertTrue(replicasManager.getBrokerMetadata().isLoaded()); + Assert.assertEquals(1L, replicasManager.getBrokerMetadata().getBrokerId().longValue()); + Assert.assertEquals(1L, replicasManager.getBrokerControllerId().longValue()); + + replicasManager.shutdown(); + + Mockito.reset(mockedBrokerOuterAPI); + when(mockedBrokerOuterAPI.brokerElect(any(), any(), any(), anyLong())).thenReturn(new Pair<>(new ElectMasterResponseHeader(1L, "127.0.0.1:13131", 1, 1), syncStateSet)); + when(mockedBrokerOuterAPI.getControllerMetaData(any())).thenReturn( + new GetMetaDataResponseHeader("default-group", "dledger-a", CONTROLLER_ADDR, true, CONTROLLER_ADDR)); + when(mockedBrokerOuterAPI.checkAddressReachable(any())).thenReturn(true); + + // restart, we expect that this replicasManager still keep the metadata and still try to finish its registering + ReplicasManager replicasManagerNew = new ReplicasManager(mockedBrokerController); + // because apply brokerId: 1 has succeeded, so now next broker id is 2 + when(mockedBrokerOuterAPI.getNextBrokerId(any(), any(), any())).thenReturn(new GetNextBrokerIdResponseHeader(CLUSTER_NAME, BROKER_NAME, 2L)); + // because apply brokerId: 1 has succeeded, so next request which try to apply brokerId: 1 will be failed + when(mockedBrokerOuterAPI.applyBrokerId(any(), any(), eq(1L), any(), any())).thenThrow(new RuntimeException()); + when(mockedBrokerOuterAPI.registerBrokerToController(any(), any(), anyLong(), any(), any())).thenReturn(new Pair<>(new RegisterBrokerToControllerResponseHeader(), syncStateSet)); + replicasManagerNew.start(); + + Assert.assertEquals(ReplicasManager.State.RUNNING, replicasManagerNew.getState()); + Assert.assertEquals(ReplicasManager.RegisterState.REGISTERED, replicasManagerNew.getRegisterState()); + // tempMetadata has been cleared + Assert.assertFalse(replicasManagerNew.getTempBrokerMetadata().fileExists()); + Assert.assertFalse(replicasManagerNew.getTempBrokerMetadata().isLoaded()); + // metadata has been persisted + Assert.assertTrue(replicasManagerNew.getBrokerMetadata().fileExists()); + Assert.assertTrue(replicasManagerNew.getBrokerMetadata().isLoaded()); + Assert.assertEquals(1L, replicasManagerNew.getBrokerMetadata().getBrokerId().longValue()); + Assert.assertEquals(1L, replicasManagerNew.getBrokerControllerId().longValue()); + replicasManagerNew.shutdown(); + } + + + private void checkMetadataFile(BrokerMetadata brokerMetadata0 ,Long brokerId) throws Exception { + Assert.assertEquals(brokerId, brokerMetadata0.getBrokerId()); + Assert.assertTrue(brokerMetadata0.fileExists()); + BrokerMetadata brokerMetadata = new BrokerMetadata(brokerMetadata0.getFilePath()); + brokerMetadata.readFromFile(); + Assert.assertEquals(brokerMetadata0, brokerMetadata); + } + + @After + public void clear() { + UtilAll.deleteFile(new File(STORE_BASE_PATH)); + } + + +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/controller/ReplicasManagerTest.java b/broker/src/test/java/org/apache/rocketmq/broker/controller/ReplicasManagerTest.java new file mode 100644 index 00000000000..c863f7ac96c --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/controller/ReplicasManagerTest.java @@ -0,0 +1,211 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.controller; + +import java.io.File; +import java.util.Arrays; +import java.util.HashSet; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.out.BrokerOuterAPI; +import org.apache.rocketmq.broker.slave.SlaveSynchronize; +import org.apache.rocketmq.broker.topic.TopicConfigManager; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.remoting.protocol.body.SyncStateSet; +import org.apache.rocketmq.remoting.protocol.header.controller.GetMetaDataResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetReplicaInfoResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.ApplyBrokerIdResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.GetNextBrokerIdResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.RegisterBrokerToControllerResponseHeader; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.RunningFlags; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.ha.autoswitch.AutoSwitchHAService; +import org.assertj.core.api.Assertions; +import org.assertj.core.util.Sets; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ReplicasManagerTest { + + public static final String STORE_BASE_PATH = System.getProperty("java.io.tmpdir") + File.separator + "ReplicasManagerTest"; + + public static final String STORE_PATH = STORE_BASE_PATH + File.separator + UUID.randomUUID(); + + @Mock + private BrokerController brokerController; + + private ReplicasManager replicasManager; + + @Mock + private DefaultMessageStore defaultMessageStore; + + private SlaveSynchronize slaveSynchronize; + + private AutoSwitchHAService autoSwitchHAService; + + private MessageStoreConfig messageStoreConfig; + + private GetMetaDataResponseHeader getMetaDataResponseHeader; + + private BrokerConfig brokerConfig; + + @Mock + private BrokerOuterAPI brokerOuterAPI; + + private GetNextBrokerIdResponseHeader getNextBrokerIdResponseHeader; + + private ApplyBrokerIdResponseHeader applyBrokerIdResponseHeader; + + private RegisterBrokerToControllerResponseHeader registerBrokerToControllerResponseHeader; + + private ElectMasterResponseHeader brokerTryElectResponseHeader; + + private Pair result; + + private GetReplicaInfoResponseHeader getReplicaInfoResponseHeader; + + private SyncStateSet syncStateSet; + + private RunningFlags runningFlags = new RunningFlags(); + + private static final String OLD_MASTER_ADDRESS = "192.168.1.1"; + + private static final String NEW_MASTER_ADDRESS = "192.168.1.2"; + + private static final long BROKER_ID_1 = 1; + + private static final long BROKER_ID_2 = 2; + + private static final int OLD_MASTER_EPOCH = 2; + private static final int NEW_MASTER_EPOCH = 3; + + private static final String GROUP = "DEFAULT_GROUP"; + + private static final String LEADER_ID = "leader-1"; + + private static final Boolean IS_LEADER = true; + + private static final String PEERS = "1.1.1.1"; + + private static final long SCHEDULE_SERVICE_EXEC_PERIOD = 5; + + private static final Long SYNC_STATE = 1L; + + private static final HashSet SYNC_STATE_SET_1 = new HashSet(Arrays.asList(BROKER_ID_1)); + + private static final HashSet SYNC_STATE_SET_2 = new HashSet(Arrays.asList(BROKER_ID_2)); + + @Before + public void before() throws Exception { + UtilAll.deleteFile(new File(STORE_BASE_PATH)); + autoSwitchHAService = new AutoSwitchHAService(); + messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setStorePathRootDir(STORE_PATH); + brokerConfig = new BrokerConfig(); + slaveSynchronize = new SlaveSynchronize(brokerController); + getMetaDataResponseHeader = new GetMetaDataResponseHeader(GROUP, LEADER_ID, OLD_MASTER_ADDRESS, IS_LEADER, PEERS); + getNextBrokerIdResponseHeader = new GetNextBrokerIdResponseHeader(); + getNextBrokerIdResponseHeader.setNextBrokerId(BROKER_ID_1); + applyBrokerIdResponseHeader = new ApplyBrokerIdResponseHeader(); + registerBrokerToControllerResponseHeader = new RegisterBrokerToControllerResponseHeader(); + brokerTryElectResponseHeader = new ElectMasterResponseHeader(); + brokerTryElectResponseHeader.setMasterBrokerId(BROKER_ID_1); + brokerTryElectResponseHeader.setMasterAddress(OLD_MASTER_ADDRESS); + brokerTryElectResponseHeader.setMasterEpoch(OLD_MASTER_EPOCH); + brokerTryElectResponseHeader.setSyncStateSetEpoch(OLD_MASTER_EPOCH); + getReplicaInfoResponseHeader = new GetReplicaInfoResponseHeader(); + getReplicaInfoResponseHeader.setMasterAddress(OLD_MASTER_ADDRESS); + getReplicaInfoResponseHeader.setMasterBrokerId(BROKER_ID_1); + getReplicaInfoResponseHeader.setMasterEpoch(NEW_MASTER_EPOCH); + syncStateSet = new SyncStateSet(Sets.newLinkedHashSet(SYNC_STATE), NEW_MASTER_EPOCH); + result = new Pair<>(getReplicaInfoResponseHeader, syncStateSet); + TopicConfigManager topicConfigManager = new TopicConfigManager(); + when(defaultMessageStore.getMessageStoreConfig()).thenReturn(messageStoreConfig); + when(brokerController.getMessageStore()).thenReturn(defaultMessageStore); + when(brokerController.getMessageStore().getHaService()).thenReturn(autoSwitchHAService); + when(brokerController.getMessageStore().getRunningFlags()).thenReturn(runningFlags); + when(brokerController.getBrokerConfig()).thenReturn(brokerConfig); + when(brokerController.getMessageStoreConfig()).thenReturn(messageStoreConfig); + when(brokerController.getSlaveSynchronize()).thenReturn(slaveSynchronize); + when(brokerController.getBrokerOuterAPI()).thenReturn(brokerOuterAPI); + when(brokerController.getBrokerAddr()).thenReturn(OLD_MASTER_ADDRESS); + when(brokerController.getTopicConfigManager()).thenReturn(topicConfigManager); + when(brokerOuterAPI.getControllerMetaData(any())).thenReturn(getMetaDataResponseHeader); + when(brokerOuterAPI.checkAddressReachable(any())).thenReturn(true); + when(brokerOuterAPI.getNextBrokerId(any(), any(), any())).thenReturn(getNextBrokerIdResponseHeader); + when(brokerOuterAPI.applyBrokerId(any(), any(), anyLong(), any(), any())).thenReturn(applyBrokerIdResponseHeader); + when(brokerOuterAPI.registerBrokerToController(any(), any(), anyLong(), any(), any())).thenReturn(new Pair<>(new RegisterBrokerToControllerResponseHeader(), SYNC_STATE_SET_1)); + when(brokerOuterAPI.getReplicaInfo(any(), any())).thenReturn(result); + when(brokerOuterAPI.brokerElect(any(), any(), any(), any())).thenReturn(new Pair<>(brokerTryElectResponseHeader, SYNC_STATE_SET_1)); + replicasManager = new ReplicasManager(brokerController); + autoSwitchHAService.init(defaultMessageStore); + replicasManager.start(); + // execute schedulingSyncBrokerMetadata() + TimeUnit.SECONDS.sleep(SCHEDULE_SERVICE_EXEC_PERIOD); + } + + @After + public void after() { + replicasManager.shutdown(); + brokerController.shutdown(); + UtilAll.deleteFile(new File(STORE_BASE_PATH)); + } + + @Test + public void changeBrokerRoleTest() { + HashSet syncStateSetA = new HashSet<>(); + syncStateSetA.add(BROKER_ID_1); + HashSet syncStateSetB = new HashSet<>(); + syncStateSetA.add(BROKER_ID_2); + // not equal to localAddress + Assertions.assertThatCode(() -> replicasManager.changeBrokerRole(BROKER_ID_2, NEW_MASTER_ADDRESS, NEW_MASTER_EPOCH, OLD_MASTER_EPOCH, syncStateSetB)) + .doesNotThrowAnyException(); + + // equal to localAddress + Assertions.assertThatCode(() -> replicasManager.changeBrokerRole(BROKER_ID_1, OLD_MASTER_ADDRESS, NEW_MASTER_EPOCH, OLD_MASTER_EPOCH, syncStateSetA)) + .doesNotThrowAnyException(); + } + + @Test + public void changeToMasterTest() { + HashSet syncStateSet = new HashSet<>(); + syncStateSet.add(BROKER_ID_1); + Assertions.assertThatCode(() -> replicasManager.changeToMaster(NEW_MASTER_EPOCH, OLD_MASTER_EPOCH, syncStateSet)).doesNotThrowAnyException(); + } + + @Test + public void changeToSlaveTest() { + Assertions.assertThatCode(() -> replicasManager.changeToSlave(NEW_MASTER_ADDRESS, NEW_MASTER_EPOCH, BROKER_ID_2)) + .doesNotThrowAnyException(); + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/failover/EscapeBridgeTest.java b/broker/src/test/java/org/apache/rocketmq/broker/failover/EscapeBridgeTest.java new file mode 100644 index 00000000000..d7bd753d776 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/failover/EscapeBridgeTest.java @@ -0,0 +1,202 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.failover; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.out.BrokerOuterAPI; +import org.apache.rocketmq.broker.topic.TopicRouteInfoManager; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.consumer.PullStatus; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.logfile.DefaultMappedFile; +import org.apache.rocketmq.store.logfile.MappedFile; +import org.assertj.core.api.Assertions; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class EscapeBridgeTest { + + private EscapeBridge escapeBridge; + + @Mock + private BrokerController brokerController; + + @Mock + private MessageExtBrokerInner messageExtBrokerInner; + + private BrokerConfig brokerConfig; + + @Mock + private DefaultMessageStore defaultMessageStore; + + private GetMessageResult getMessageResult; + + @Mock + private DefaultMQProducer defaultMQProducer; + + private static final String BROKER_NAME = "broker_a"; + + private static final String TEST_TOPIC = "TEST_TOPIC"; + + private static final int DEFAULT_QUEUE_ID = 0; + + + @Before + public void before() throws Exception { + brokerConfig = new BrokerConfig(); + getMessageResult = new GetMessageResult(); + brokerConfig.setBrokerName(BROKER_NAME); + when(brokerController.getBrokerConfig()).thenReturn(brokerConfig); + + escapeBridge = new EscapeBridge(brokerController); + messageExtBrokerInner = new MessageExtBrokerInner(); + when(brokerController.getMessageStore()).thenReturn(defaultMessageStore); + when(defaultMessageStore.getMessageAsync(anyString(), anyString(), anyInt(), anyLong(), anyInt(), any())).thenReturn(CompletableFuture.completedFuture(getMessageResult)); + + TopicRouteInfoManager topicRouteInfoManager = mock(TopicRouteInfoManager.class); + when(brokerController.getTopicRouteInfoManager()).thenReturn(topicRouteInfoManager); + when(topicRouteInfoManager.findBrokerAddressInSubscribe(anyString(), anyLong(), anyBoolean())).thenReturn(""); + + BrokerOuterAPI brokerOuterAPI = mock(BrokerOuterAPI.class); + when(brokerController.getBrokerOuterAPI()).thenReturn(brokerOuterAPI); + when(brokerOuterAPI.pullMessageFromSpecificBrokerAsync(anyString(), anyString(), anyString(), anyString(), anyInt(), anyLong(), anyInt(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(new PullResult(PullStatus.FOUND, -1, -1, -1, new ArrayList<>()))); + + brokerConfig.setEnableSlaveActingMaster(true); + brokerConfig.setEnableRemoteEscape(true); + escapeBridge.start(); + defaultMQProducer.start(); + } + + @After + public void after() { + escapeBridge.shutdown(); + brokerController.shutdown(); + defaultMQProducer.shutdown(); + } + + @Test + public void putMessageTest() { + messageExtBrokerInner.setTopic(TEST_TOPIC); + messageExtBrokerInner.setQueueId(DEFAULT_QUEUE_ID); + messageExtBrokerInner.setBody("Hello World".getBytes(StandardCharsets.UTF_8)); + // masterBroker is null + final PutMessageResult result1 = escapeBridge.putMessage(messageExtBrokerInner); + assert result1 != null; + assert PutMessageStatus.PUT_TO_REMOTE_BROKER_FAIL.equals(result1.getPutMessageStatus()); + + // masterBroker is not null + messageExtBrokerInner.setBody("Hello World2".getBytes(StandardCharsets.UTF_8)); + when(brokerController.peekMasterBroker()).thenReturn(brokerController); + Assertions.assertThatCode(() -> escapeBridge.putMessage(messageExtBrokerInner)).doesNotThrowAnyException(); + + when(brokerController.peekMasterBroker()).thenReturn(null); + final PutMessageResult result3 = escapeBridge.putMessage(messageExtBrokerInner); + assert result3 != null; + assert PutMessageStatus.PUT_TO_REMOTE_BROKER_FAIL.equals(result3.getPutMessageStatus()); + } + + @Test + public void asyncPutMessageTest() { + + // masterBroker is null + Assertions.assertThatCode(() -> escapeBridge.asyncPutMessage(messageExtBrokerInner)).doesNotThrowAnyException(); + + // masterBroker is not null + when(brokerController.peekMasterBroker()).thenReturn(brokerController); + Assertions.assertThatCode(() -> escapeBridge.asyncPutMessage(messageExtBrokerInner)).doesNotThrowAnyException(); + + when(brokerController.peekMasterBroker()).thenReturn(null); + Assertions.assertThatCode(() -> escapeBridge.asyncPutMessage(messageExtBrokerInner)).doesNotThrowAnyException(); + } + + @Test + public void putMessageToSpecificQueueTest() { + // masterBroker is null + final PutMessageResult result1 = escapeBridge.putMessageToSpecificQueue(messageExtBrokerInner); + assert result1 != null; + assert PutMessageStatus.PUT_TO_REMOTE_BROKER_FAIL.equals(result1.getPutMessageStatus()); + + // masterBroker is not null + when(brokerController.peekMasterBroker()).thenReturn(brokerController); + Assertions.assertThatCode(() -> escapeBridge.putMessageToSpecificQueue(messageExtBrokerInner)).doesNotThrowAnyException(); + } + + @Test + public void getMessageTest() { + when(brokerController.peekMasterBroker()).thenReturn(brokerController); + when(brokerController.getMessageStoreByBrokerName(any())).thenReturn(defaultMessageStore); + Assertions.assertThatCode(() -> escapeBridge.putMessage(messageExtBrokerInner)).doesNotThrowAnyException(); + + Assertions.assertThatCode(() -> escapeBridge.getMessage(TEST_TOPIC, 0, DEFAULT_QUEUE_ID, BROKER_NAME, false)).doesNotThrowAnyException(); + } + + @Test + public void getMessageAsyncTest() { + when(brokerController.peekMasterBroker()).thenReturn(brokerController); + when(brokerController.getMessageStoreByBrokerName(any())).thenReturn(defaultMessageStore); + Assertions.assertThatCode(() -> escapeBridge.putMessage(messageExtBrokerInner)).doesNotThrowAnyException(); + + Assertions.assertThatCode(() -> escapeBridge.getMessageAsync(TEST_TOPIC, 0, DEFAULT_QUEUE_ID, BROKER_NAME, false)).doesNotThrowAnyException(); + } + + @Test + public void getMessageFromRemoteTest() { + Assertions.assertThatCode(() -> escapeBridge.getMessageFromRemote(TEST_TOPIC, 1, DEFAULT_QUEUE_ID, BROKER_NAME)).doesNotThrowAnyException(); + } + + @Test + public void getMessageFromRemoteAsyncTest() { + Assertions.assertThatCode(() -> escapeBridge.getMessageFromRemoteAsync(TEST_TOPIC, 1, DEFAULT_QUEUE_ID, BROKER_NAME)).doesNotThrowAnyException(); + } + + @Test + public void decodeMsgListTest() { + ByteBuffer byteBuffer = ByteBuffer.allocate(10); + MappedFile mappedFile = new DefaultMappedFile(); + SelectMappedBufferResult result = new SelectMappedBufferResult(0, byteBuffer, 10, mappedFile); + + getMessageResult.addMessage(result); + Assertions.assertThatCode(() -> escapeBridge.decodeMsgList(getMessageResult, false)).doesNotThrowAnyException(); + } + +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/filter/CommitLogDispatcherCalcBitMapTest.java b/broker/src/test/java/org/apache/rocketmq/broker/filter/CommitLogDispatcherCalcBitMapTest.java index 8f28832c0c7..af1d06ea28d 100644 --- a/broker/src/test/java/org/apache/rocketmq/broker/filter/CommitLogDispatcherCalcBitMapTest.java +++ b/broker/src/test/java/org/apache/rocketmq/broker/filter/CommitLogDispatcherCalcBitMapTest.java @@ -55,7 +55,7 @@ public void testDispatch_filterDataIllegal() { filterManager); for (int i = 0; i < 1; i++) { - Map properties = new HashMap(4); + Map properties = new HashMap<>(4); properties.put("a", String.valueOf(i * 10 + 5)); String topic = "topic" + i; @@ -99,7 +99,7 @@ public void testDispatch_blankFilterData() { filterManager); for (int i = 0; i < 10; i++) { - Map properties = new HashMap(4); + Map properties = new HashMap<>(4); properties.put("a", String.valueOf(i * 10 + 5)); String topic = "topic" + i; @@ -136,7 +136,7 @@ public void testDispatch() { filterManager); for (int i = 0; i < 10; i++) { - Map properties = new HashMap(4); + Map properties = new HashMap<>(4); properties.put("a", String.valueOf(i * 10 + 5)); String topic = "topic" + i; diff --git a/broker/src/test/java/org/apache/rocketmq/broker/filter/ConsumerFilterManagerTest.java b/broker/src/test/java/org/apache/rocketmq/broker/filter/ConsumerFilterManagerTest.java index 68d60092d8b..c01d8299dcf 100644 --- a/broker/src/test/java/org/apache/rocketmq/broker/filter/ConsumerFilterManagerTest.java +++ b/broker/src/test/java/org/apache/rocketmq/broker/filter/ConsumerFilterManagerTest.java @@ -17,17 +17,16 @@ package org.apache.rocketmq.broker.filter; -import org.apache.rocketmq.common.UtilAll; -import org.apache.rocketmq.common.filter.ExpressionType; -import org.apache.rocketmq.common.filter.FilterAPI; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; -import org.junit.Test; - import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.filter.ExpressionType; +import org.apache.rocketmq.remoting.protocol.filter.FilterAPI; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -81,9 +80,7 @@ public void testRegister_newExpressionCompileErrorAndRemoveOld() { public void testRegister_change() { ConsumerFilterManager filterManager = gen(10, 10); - ConsumerFilterData filterData = filterManager.get("topic9", "CID_9"); - - System.out.println(filterData.getCompiledExpression()); + ConsumerFilterData filterData; String newExpr = "a > 0 and a < 10"; @@ -92,8 +89,6 @@ public void testRegister_change() { filterData = filterManager.get("topic9", "CID_9"); assertThat(newExpr).isEqualTo(filterData.getExpression()); - - System.out.println(filterData.toString()); } @Test diff --git a/broker/src/test/java/org/apache/rocketmq/broker/filter/MessageStoreWithFilterTest.java b/broker/src/test/java/org/apache/rocketmq/broker/filter/MessageStoreWithFilterTest.java index e544d90a124..84bca916998 100644 --- a/broker/src/test/java/org/apache/rocketmq/broker/filter/MessageStoreWithFilterTest.java +++ b/broker/src/test/java/org/apache/rocketmq/broker/filter/MessageStoreWithFilterTest.java @@ -17,12 +17,25 @@ package org.apache.rocketmq.broker.filter; +import java.io.File; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.UtilAll; import org.apache.rocketmq.common.filter.ExpressionType; import org.apache.rocketmq.common.message.MessageDecoder; import org.apache.rocketmq.common.message.MessageExt; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; import org.apache.rocketmq.store.CommitLogDispatcher; import org.apache.rocketmq.store.ConsumeQueueExt; import org.apache.rocketmq.store.DefaultMessageStore; @@ -30,42 +43,34 @@ import org.apache.rocketmq.store.GetMessageResult; import org.apache.rocketmq.store.GetMessageStatus; import org.apache.rocketmq.store.MessageArrivingListener; -import org.apache.rocketmq.store.MessageExtBrokerInner; import org.apache.rocketmq.store.MessageFilter; import org.apache.rocketmq.store.PutMessageResult; import org.apache.rocketmq.store.config.MessageStoreConfig; import org.apache.rocketmq.store.stats.BrokerStatsManager; +import org.awaitility.core.ThrowingRunnable; import org.junit.After; +import org.junit.Assume; import org.junit.Before; import org.junit.Test; -import java.io.File; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.net.UnknownHostException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; public class MessageStoreWithFilterTest { - private static final String msg = "Once, there was a chance for me!"; - private static final byte[] msgBody = msg.getBytes(); + private static final String MSG = "Once, there was a chance for me!"; + private static final byte[] MSG_BODY = MSG.getBytes(); - private static final String topic = "topic"; - private static final int queueId = 0; - private static final String storePath = "." + File.separator + "unit_test_store"; - private static final int commitLogFileSize = 1024 * 1024 * 256; - private static final int cqFileSize = 300000 * 20; - private static final int cqExtFileSize = 300000 * 128; + private static final String TOPIC = "topic"; + private static final int QUEUE_ID = 0; + private static final String STORE_PATH = System.getProperty("java.io.tmpdir") + File.separator + "unit_test_store"; + private static final int COMMIT_LOG_FILE_SIZE = 1024 * 1024 * 256; + private static final int CQ_FILE_SIZE = 300000 * 20; + private static final int CQ_EXT_FILE_SIZE = 300000 * 128; - private static SocketAddress BornHost; + private static SocketAddress bornHost; - private static SocketAddress StoreHost; + private static SocketAddress storeHost; private DefaultMessageStore master; @@ -77,11 +82,11 @@ public class MessageStoreWithFilterTest { static { try { - StoreHost = new InetSocketAddress(InetAddress.getLocalHost(), 8123); + storeHost = new InetSocketAddress(InetAddress.getLocalHost(), 8123); } catch (UnknownHostException e) { } try { - BornHost = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0); + bornHost = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0); } catch (UnknownHostException e) { } } @@ -94,23 +99,25 @@ public void init() throws Exception { @After public void destroy() { - master.shutdown(); - master.destroy(); - UtilAll.deleteFile(new File(storePath)); + if (master != null) { + master.shutdown(); + master.destroy(); + } + UtilAll.deleteFile(new File(STORE_PATH)); } public MessageExtBrokerInner buildMessage() { MessageExtBrokerInner msg = new MessageExtBrokerInner(); - msg.setTopic(topic); + msg.setTopic(TOPIC); msg.setTags(System.currentTimeMillis() + "TAG"); msg.setKeys("Hello"); - msg.setBody(msgBody); + msg.setBody(MSG_BODY); msg.setKeys(String.valueOf(System.currentTimeMillis())); - msg.setQueueId(queueId); + msg.setQueueId(QUEUE_ID); msg.setSysFlag(0); msg.setBornTimestamp(System.currentTimeMillis()); - msg.setStoreHost(StoreHost); - msg.setBornHost(BornHost); + msg.setStoreHost(storeHost); + msg.setBornHost(bornHost); for (int i = 1; i < 3; i++) { msg.putUserProperty(String.valueOf(i), "imagoodperson" + i); } @@ -122,21 +129,21 @@ public MessageExtBrokerInner buildMessage() { public MessageStoreConfig buildStoreConfig(int commitLogFileSize, int cqFileSize, boolean enableCqExt, int cqExtFileSize) { MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); - messageStoreConfig.setMapedFileSizeCommitLog(commitLogFileSize); - messageStoreConfig.setMapedFileSizeConsumeQueue(cqFileSize); + messageStoreConfig.setMappedFileSizeCommitLog(commitLogFileSize); + messageStoreConfig.setMappedFileSizeConsumeQueue(cqFileSize); messageStoreConfig.setMappedFileSizeConsumeQueueExt(cqExtFileSize); messageStoreConfig.setMessageIndexEnable(false); messageStoreConfig.setEnableConsumeQueueExt(enableCqExt); - messageStoreConfig.setStorePathRootDir(storePath); - messageStoreConfig.setStorePathCommitLog(storePath + File.separator + "commitlog"); + messageStoreConfig.setStorePathRootDir(STORE_PATH); + messageStoreConfig.setStorePathCommitLog(STORE_PATH + File.separator + "commitlog"); return messageStoreConfig; } protected DefaultMessageStore gen(ConsumerFilterManager filterManager) throws Exception { MessageStoreConfig messageStoreConfig = buildStoreConfig( - commitLogFileSize, cqFileSize, true, cqExtFileSize + COMMIT_LOG_FILE_SIZE, CQ_FILE_SIZE, true, CQ_EXT_FILE_SIZE ); BrokerConfig brokerConfig = new BrokerConfig(); @@ -146,14 +153,14 @@ protected DefaultMessageStore gen(ConsumerFilterManager filterManager) throws Ex DefaultMessageStore master = new DefaultMessageStore( messageStoreConfig, - new BrokerStatsManager(brokerConfig.getBrokerClusterName()), + new BrokerStatsManager(brokerConfig.getBrokerClusterName(), brokerConfig.isEnableDetailStat()), new MessageArrivingListener() { @Override public void arriving(String topic, int queueId, long logicOffset, long tagsCode, long msgStoreTime, byte[] filterBitMap, Map properties) { } } - , brokerConfig); + , brokerConfig, new ConcurrentHashMap<>()); master.getDispatcherList().addFirst(new CommitLogDispatcher() { @Override @@ -166,7 +173,11 @@ public void dispatch(DispatchRequest request) { }); master.getDispatcherList().addFirst(new CommitLogDispatcherCalcBitMap(brokerConfig, filterManager)); - assertThat(master.load()).isTrue(); + if (MixAll.isWindows()) { + Assume.assumeTrue(master.load()); + } else { + assertThat(master.load()).isTrue(); + } master.start(); @@ -175,9 +186,9 @@ public void dispatch(DispatchRequest request) { protected List putMsg(DefaultMessageStore master, int topicCount, int msgCountPerTopic) throws Exception { - List msgs = new ArrayList(); + List msgs = new ArrayList<>(); for (int i = 0; i < topicCount; i++) { - String realTopic = topic + i; + String realTopic = TOPIC + i; for (int j = 0; j < msgCountPerTopic; j++) { MessageExtBrokerInner msg = buildMessage(); msg.setTopic(realTopic); @@ -196,7 +207,7 @@ protected List putMsg(DefaultMessageStore master, int top } protected List filtered(List msgs, ConsumerFilterData filterData) { - List filteredMsgs = new ArrayList(); + List filteredMsgs = new ArrayList<>(); for (MessageExtBrokerInner messageExtBrokerInner : msgs) { @@ -242,7 +253,7 @@ public void testGetMessage_withFilterBitMapAndConsumerChanged() throws Exception resetGroup, resetSubData.getSubString(), resetSubData.getExpressionType(), System.currentTimeMillis()); - GetMessageResult resetGetResult = master.getMessage(resetGroup, topic, queueId, 0, 1000, + GetMessageResult resetGetResult = master.getMessage(resetGroup, topic, QUEUE_ID, 0, 1000, new ExpressionMessageFilter(resetSubData, resetFilterData, filterManager)); try { @@ -269,7 +280,7 @@ public void testGetMessage_withFilterBitMapAndConsumerChanged() throws Exception List filteredMsgs = filtered(msgs, normalFilterData); - GetMessageResult normalGetResult = master.getMessage(normalGroup, topic, queueId, 0, 1000, + GetMessageResult normalGetResult = master.getMessage(normalGroup, topic, QUEUE_ID, 0, 1000, new ExpressionMessageFilter(normalSubData, normalFilterData, filterManager)); try { @@ -288,7 +299,7 @@ public void testGetMessage_withFilterBitMap() throws Exception { Thread.sleep(100); for (int i = 0; i < topicCount; i++) { - String realTopic = topic + i; + String realTopic = TOPIC + i; for (int j = 0; j < msgPerTopic; j++) { String group = "CID_" + j; @@ -304,7 +315,7 @@ public void testGetMessage_withFilterBitMap() throws Exception { subscriptionData.setClassFilterMode(false); subscriptionData.setSubString(filterData.getExpression()); - GetMessageResult getMessageResult = master.getMessage(group, realTopic, queueId, 0, 10000, + GetMessageResult getMessageResult = master.getMessage(group, realTopic, QUEUE_ID, 0, 10000, new ExpressionMessageFilter(subscriptionData, filterData, filterManager)); String assertMsg = group + "-" + realTopic; try { @@ -347,27 +358,31 @@ public void testGetMessage_withFilterBitMap() throws Exception { public void testGetMessage_withFilter_checkTagsCode() throws Exception { putMsg(master, topicCount, msgPerTopic); - Thread.sleep(200); - - for (int i = 0; i < topicCount; i++) { - String realTopic = topic + i; - - GetMessageResult getMessageResult = master.getMessage("test", realTopic, queueId, 0, 10000, - new MessageFilter() { - @Override - public boolean isMatchedByConsumeQueue(Long tagsCode, ConsumeQueueExt.CqExtUnit cqExtUnit) { - if (tagsCode != null && tagsCode <= ConsumeQueueExt.MAX_ADDR) { - return false; - } - return true; - } + await().atMost(3, TimeUnit.SECONDS).untilAsserted(new ThrowingRunnable() { + @Override + public void run() throws Throwable { + for (int i = 0; i < topicCount; i++) { + final String realTopic = TOPIC + i; + GetMessageResult getMessageResult = master.getMessage("test", realTopic, QUEUE_ID, 0, 10000, + new MessageFilter() { + @Override + public boolean isMatchedByConsumeQueue(Long tagsCode, + ConsumeQueueExt.CqExtUnit cqExtUnit) { + if (tagsCode != null && tagsCode <= ConsumeQueueExt.MAX_ADDR) { + return false; + } + return true; + } - @Override - public boolean isMatchedByCommitLog(ByteBuffer msgBuffer, Map properties) { - return true; - } - }); - assertThat(getMessageResult.getMessageCount()).isEqualTo(msgPerTopic); - } + @Override + public boolean isMatchedByCommitLog(ByteBuffer msgBuffer, + Map properties) { + return true; + } + }); + assertThat(getMessageResult.getMessageCount()).isEqualTo(msgPerTopic); + } + } + }); } } diff --git a/broker/src/test/java/org/apache/rocketmq/broker/latency/BrokerFastFailureTest.java b/broker/src/test/java/org/apache/rocketmq/broker/latency/BrokerFastFailureTest.java index 5d0f7f9d72b..31b547cf1be 100644 --- a/broker/src/test/java/org/apache/rocketmq/broker/latency/BrokerFastFailureTest.java +++ b/broker/src/test/java/org/apache/rocketmq/broker/latency/BrokerFastFailureTest.java @@ -19,6 +19,7 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.common.future.FutureTaskExt; import org.apache.rocketmq.remoting.netty.RequestTask; import org.junit.Test; diff --git a/broker/src/test/java/org/apache/rocketmq/broker/longpolling/PullRequestHoldServiceTest.java b/broker/src/test/java/org/apache/rocketmq/broker/longpolling/PullRequestHoldServiceTest.java new file mode 100644 index 00000000000..6eeb4adbe34 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/longpolling/PullRequestHoldServiceTest.java @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.longpolling; + +import io.netty.channel.Channel; +import java.util.HashMap; +import java.util.concurrent.Executors; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.processor.PullMessageProcessor; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.store.DefaultMessageFilter; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.assertj.core.api.Assertions; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class PullRequestHoldServiceTest { + + @Mock + private BrokerController brokerController; + + private PullRequestHoldService pullRequestHoldService; + + @Mock + private PullRequest pullRequest; + + private BrokerConfig brokerConfig = new BrokerConfig(); + + @Mock + private DefaultMessageStore defaultMessageStore; + + @Mock + private DefaultMessageFilter defaultMessageFilter; + + @Mock + private RemotingCommand remotingCommand; + + @Mock + private Channel channel; + + private SubscriptionData subscriptionData; + + private static final String TEST_TOPIC = "TEST_TOPIC"; + + private static final int DEFAULT_QUEUE_ID = 0; + + private static final long MAX_OFFSET = 100L; + + @Before + public void before() { + when(brokerController.getBrokerConfig()).thenReturn(brokerConfig); + when(brokerController.getPullMessageProcessor()).thenReturn(new PullMessageProcessor(brokerController)); + when(brokerController.getPullMessageExecutor()).thenReturn(Executors.newCachedThreadPool()); + pullRequestHoldService = new PullRequestHoldService(brokerController); + subscriptionData = new SubscriptionData(TEST_TOPIC, "*"); + pullRequest = new PullRequest(remotingCommand, channel, 3000, 3000, 0L, subscriptionData, defaultMessageFilter); + pullRequestHoldService.start(); + } + + @After + public void after() { + pullRequestHoldService.shutdown(); + } + + @Test + public void suspendPullRequestTest() { + Assertions.assertThatCode(() -> pullRequestHoldService.suspendPullRequest(TEST_TOPIC, DEFAULT_QUEUE_ID, pullRequest)).doesNotThrowAnyException(); + } + + @Test + public void getServiceNameTest() { + final String name = pullRequestHoldService.getServiceName(); + assert StringUtils.isNotEmpty(name); + } + + @Test + public void checkHoldRequestTest() { + Assertions.assertThatCode(() -> pullRequestHoldService.checkHoldRequest()).doesNotThrowAnyException(); + } + + @Test + public void notifyMessageArrivingTest() { + Assertions.assertThatCode(() -> pullRequestHoldService.notifyMessageArriving(TEST_TOPIC, DEFAULT_QUEUE_ID, MAX_OFFSET)).doesNotThrowAnyException(); + + Assertions.assertThatCode(() -> pullRequestHoldService.suspendPullRequest(TEST_TOPIC, DEFAULT_QUEUE_ID, pullRequest)).doesNotThrowAnyException(); + + Assertions.assertThatCode(() -> pullRequestHoldService.notifyMessageArriving(TEST_TOPIC, DEFAULT_QUEUE_ID, MAX_OFFSET, + 1L, System.currentTimeMillis(), new byte[10], new HashMap<>())).doesNotThrowAnyException(); + } + + @Test + public void notifyMasterOnlineTest() { + Assertions.assertThatCode(() -> pullRequestHoldService.suspendPullRequest(TEST_TOPIC, DEFAULT_QUEUE_ID, pullRequest)).doesNotThrowAnyException(); + + Assertions.assertThatCode(() -> pullRequestHoldService.notifyMasterOnline()).doesNotThrowAnyException(); + } + +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/metrics/BrokerMetricsManagerTest.java b/broker/src/test/java/org/apache/rocketmq/broker/metrics/BrokerMetricsManagerTest.java new file mode 100644 index 00000000000..11f7ae8215a --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/metrics/BrokerMetricsManagerTest.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.metrics; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BrokerMetricsManagerTest { + + @Test + public void testNewAttributesBuilder() { + Attributes attributes = BrokerMetricsManager.newAttributesBuilder().put("a", "b") + .build(); + assertThat(attributes.get(AttributeKey.stringKey("a"))).isEqualTo("b"); + } + + @Test + public void testCustomizedAttributesBuilder() { + BrokerMetricsManager.attributesBuilderSupplier = () -> new AttributesBuilder() { + private AttributesBuilder attributesBuilder = Attributes.builder(); + @Override + public Attributes build() { + return attributesBuilder.put("customized", "value").build(); + } + + @Override + public AttributesBuilder put(AttributeKey key, int value) { + attributesBuilder.put(key, value); + return this; + } + + @Override + public AttributesBuilder put(AttributeKey key, T value) { + attributesBuilder.put(key, value); + return this; + } + + @Override + public AttributesBuilder putAll(Attributes attributes) { + attributesBuilder.putAll(attributes); + return this; + } + }; + Attributes attributes = BrokerMetricsManager.newAttributesBuilder().put("a", "b") + .build(); + assertThat(attributes.get(AttributeKey.stringKey("a"))).isEqualTo("b"); + assertThat(attributes.get(AttributeKey.stringKey("customized"))).isEqualTo("value"); + } +} \ No newline at end of file diff --git a/broker/src/test/java/org/apache/rocketmq/broker/offset/BroadcastOffsetManagerTest.java b/broker/src/test/java/org/apache/rocketmq/broker/offset/BroadcastOffsetManagerTest.java new file mode 100644 index 00000000000..9dc00f9d6b1 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/offset/BroadcastOffsetManagerTest.java @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.offset; + +import java.time.Duration; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.client.ClientChannelInfo; +import org.apache.rocketmq.broker.client.ConsumerManager; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.store.MessageStore; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.stubbing.Answer; + +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class BroadcastOffsetManagerTest { + + private final AtomicLong maxOffset = new AtomicLong(10L); + private final AtomicLong commitOffset = new AtomicLong(-1); + + private final ConsumerOffsetManager consumerOffsetManager = mock(ConsumerOffsetManager.class); + private final ConsumerManager consumerManager = mock(ConsumerManager.class); + private final BrokerConfig brokerConfig = new BrokerConfig(); + private final Set onlineClientIdSet = new HashSet<>(); + private BroadcastOffsetManager broadcastOffsetManager; + + @Before + public void before() { + brokerConfig.setEnableBroadcastOffsetStore(true); + brokerConfig.setBroadcastOffsetExpireSecond(1); + brokerConfig.setBroadcastOffsetExpireMaxSecond(5); + BrokerController brokerController = mock(BrokerController.class); + when(brokerController.getBrokerConfig()).thenReturn(brokerConfig); + + when(brokerController.getConsumerManager()).thenReturn(consumerManager); + doAnswer((Answer) mock -> { + String clientId = mock.getArgument(1); + if (onlineClientIdSet.contains(clientId)) { + return new ClientChannelInfo(null); + } + return null; + }).when(consumerManager).findChannel(anyString(), anyString()); + + doAnswer((Answer) mock -> commitOffset.get()) + .when(consumerOffsetManager).queryOffset(anyString(), anyString(), anyInt()); + doAnswer((Answer) mock -> { + commitOffset.set(mock.getArgument(4)); + return null; + }).when(consumerOffsetManager).commitOffset(anyString(), anyString(), anyString(), anyInt(), anyLong()); + when(brokerController.getConsumerOffsetManager()).thenReturn(consumerOffsetManager); + + MessageStore messageStore = mock(MessageStore.class); + doAnswer((Answer) mock -> maxOffset.get()) + .when(messageStore).getMaxOffsetInQueue(anyString(), anyInt(), anyBoolean()); + when(brokerController.getMessageStore()).thenReturn(messageStore); + + broadcastOffsetManager = new BroadcastOffsetManager(brokerController); + } + + @Test + public void testBroadcastOffsetSwitch() { + // client1 connect to broker + onlineClientIdSet.add("client1"); + long offset = broadcastOffsetManager.queryInitOffset("group", "topic", 0, "client1", 0, false); + Assert.assertEquals(-1, offset); + broadcastOffsetManager.updateOffset("group", "topic", 0, 10, "client1", false); + offset = broadcastOffsetManager.queryInitOffset("group", "topic", 0, "client1", 11, false); + Assert.assertEquals(-1, offset); + broadcastOffsetManager.updateOffset("group", "topic", 0, 11, "client1", false); + + // client1 connect to proxy + offset = broadcastOffsetManager.queryInitOffset("group", "topic", 0, "client1", -1, true); + Assert.assertEquals(11, offset); + broadcastOffsetManager.updateOffset("group", "topic", 0, 11, "client1", true); + offset = broadcastOffsetManager.queryInitOffset("group", "topic", 0, "client1", 11, true); + Assert.assertEquals(-1, offset); + broadcastOffsetManager.updateOffset("group", "topic", 0, 12, "client1", true); + + broadcastOffsetManager.scanOffsetData(); + Assert.assertEquals(12L, commitOffset.get()); + + // client2 connect to proxy + onlineClientIdSet.add("client2"); + offset = broadcastOffsetManager.queryInitOffset("group", "topic", 0, "client2", -1, true); + Assert.assertEquals(12, offset); + broadcastOffsetManager.updateOffset("group", "topic", 0, 12, "client2", true); + offset = broadcastOffsetManager.queryInitOffset("group", "topic", 0, "client2", 11, true); + Assert.assertEquals(-1, offset); + broadcastOffsetManager.updateOffset("group", "topic", 0, 13, "client2", true); + + broadcastOffsetManager.scanOffsetData(); + Assert.assertEquals(12L, commitOffset.get()); + + // client1 connect to broker + offset = broadcastOffsetManager.queryInitOffset("group", "topic", 0, "client1", 20, false); + Assert.assertEquals(12, offset); + broadcastOffsetManager.updateOffset("group", "topic", 0, 12, "client1", false); + offset = broadcastOffsetManager.queryInitOffset("group", "topic", 0, "client1", 12, false); + Assert.assertEquals(-1, offset); + + onlineClientIdSet.clear(); + + maxOffset.set(30L); + + // client3 connect to broker + onlineClientIdSet.add("client3"); + offset = broadcastOffsetManager.queryInitOffset("group", "topic", 0, "client3", 30, false); + Assert.assertEquals(-1, offset); + broadcastOffsetManager.updateOffset("group", "topic", 0, 30, "client3", false); + + await().atMost(Duration.ofSeconds(brokerConfig.getBroadcastOffsetExpireSecond() + 1)).until(() -> { + broadcastOffsetManager.scanOffsetData(); + return commitOffset.get() == 30L; + }); + } + + @Test + public void testBroadcastOffsetExpire() { + onlineClientIdSet.add("client1"); + broadcastOffsetManager.updateOffset( + "group", "topic", 0, 10, "client1", false); + onlineClientIdSet.clear(); + + await().atMost(Duration.ofSeconds(brokerConfig.getBroadcastOffsetExpireSecond() + 1)).until(() -> { + broadcastOffsetManager.scanOffsetData(); + return broadcastOffsetManager.offsetStoreMap.isEmpty(); + }); + + onlineClientIdSet.add("client1"); + broadcastOffsetManager.updateOffset( + "group", "topic", 0, 10, "client1", false); + await().atMost(Duration.ofSeconds(brokerConfig.getBroadcastOffsetExpireMaxSecond() + 1)).until(() -> { + broadcastOffsetManager.scanOffsetData(); + return broadcastOffsetManager.offsetStoreMap.isEmpty(); + }); + } +} \ No newline at end of file diff --git a/broker/src/test/java/org/apache/rocketmq/broker/offset/BroadcastOffsetStoreTest.java b/broker/src/test/java/org/apache/rocketmq/broker/offset/BroadcastOffsetStoreTest.java new file mode 100644 index 00000000000..ef830b9e9c4 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/offset/BroadcastOffsetStoreTest.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.offset; + +import org.junit.Assert; +import org.junit.Test; + +public class BroadcastOffsetStoreTest { + + @Test + public void testBasicOffsetStore() { + BroadcastOffsetStore offsetStore = new BroadcastOffsetStore(); + offsetStore.updateOffset(0, 100L, false); + offsetStore.updateOffset(1, 200L, false); + Assert.assertEquals(100L, offsetStore.readOffset(0)); + } +} \ No newline at end of file diff --git a/broker/src/test/java/org/apache/rocketmq/broker/offset/ConsumerOffsetManagerTest.java b/broker/src/test/java/org/apache/rocketmq/broker/offset/ConsumerOffsetManagerTest.java new file mode 100644 index 00000000000..7bd289a6f11 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/offset/ConsumerOffsetManagerTest.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.offset; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.mockito.Mockito; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ConsumerOffsetManagerTest { + + private static final String KEY = "FooBar@FooBarGroup"; + + private BrokerController brokerController; + + private ConsumerOffsetManager consumerOffsetManager; + + @Before + public void init() { + brokerController = Mockito.mock(BrokerController.class); + consumerOffsetManager = new ConsumerOffsetManager(brokerController); + + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + Mockito.when(brokerController.getMessageStoreConfig()).thenReturn(messageStoreConfig); + + ConcurrentHashMap> offsetTable = new ConcurrentHashMap<>(512); + offsetTable.put(KEY,new ConcurrentHashMap() {{ + put(1,2L); + put(2,3L); + }}); + consumerOffsetManager.setOffsetTable(offsetTable); + } + + @Test + public void cleanOffsetByTopic_NotExist() { + consumerOffsetManager.cleanOffsetByTopic("InvalidTopic"); + assertThat(consumerOffsetManager.getOffsetTable().containsKey(KEY)).isTrue(); + } + + @Test + public void cleanOffsetByTopic_Exist() { + consumerOffsetManager.cleanOffsetByTopic("FooBar"); + assertThat(!consumerOffsetManager.getOffsetTable().containsKey(KEY)).isTrue(); + } + + @Test + public void testOffsetPersistInMemory() { + ConcurrentMap> offsetTable = consumerOffsetManager.getOffsetTable(); + ConcurrentMap table = new ConcurrentHashMap<>(); + table.put(0, 1L); + table.put(1, 3L); + String group = "G1"; + offsetTable.put(group, table); + + consumerOffsetManager.persist(); + ConsumerOffsetManager manager = new ConsumerOffsetManager(brokerController); + manager.load(); + + ConcurrentMap offsetTableLoaded = manager.getOffsetTable().get(group); + Assert.assertEquals(table, offsetTableLoaded); + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/offset/ConsumerOrderInfoManagerLockFreeNotifyTest.java b/broker/src/test/java/org/apache/rocketmq/broker/offset/ConsumerOrderInfoManagerLockFreeNotifyTest.java new file mode 100644 index 00000000000..93689efa586 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/offset/ConsumerOrderInfoManagerLockFreeNotifyTest.java @@ -0,0 +1,178 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.offset; + +import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.processor.PopMessageProcessor; +import org.apache.rocketmq.common.BrokerConfig; +import org.assertj.core.util.Lists; +import org.junit.Before; +import org.junit.Test; +import org.mockito.stubbing.Answer; + +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ConsumerOrderInfoManagerLockFreeNotifyTest { + + private static final String TOPIC = "topic"; + private static final String GROUP = "group"; + private static final int QUEUE_ID_0 = 0; + + private long popTime; + private ConsumerOrderInfoManager consumerOrderInfoManager; + private AtomicBoolean notified; + + private final BrokerConfig brokerConfig = new BrokerConfig(); + private final PopMessageProcessor popMessageProcessor = mock(PopMessageProcessor.class); + private final BrokerController brokerController = mock(BrokerController.class); + + @Before + public void before() { + notified = new AtomicBoolean(false); + brokerConfig.setEnableNotifyAfterPopOrderLockRelease(true); + when(brokerController.getBrokerConfig()).thenReturn(brokerConfig); + when(brokerController.getPopMessageProcessor()).thenReturn(popMessageProcessor); + doAnswer((Answer) mock -> { + notified.set(true); + return null; + }).when(popMessageProcessor).notifyLongPollingRequestIfNeed(anyString(), anyString(), anyInt()); + + consumerOrderInfoManager = new ConsumerOrderInfoManager(brokerController); + popTime = System.currentTimeMillis(); + } + + @Test + public void testConsumeMessageThenNoAck() { + consumerOrderInfoManager.update( + null, + false, + TOPIC, + GROUP, + QUEUE_ID_0, + popTime, + 3000, + Lists.newArrayList(1L), + new StringBuilder() + ); + await().atLeast(Duration.ofSeconds(2)).atMost(Duration.ofSeconds(4)).until(notified::get); + assertTrue(consumerOrderInfoManager.getConsumerOrderInfoLockManager().getTimeoutMap().isEmpty()); + } + + @Test + public void testConsumeMessageThenAck() { + consumerOrderInfoManager.update( + null, + false, + TOPIC, + GROUP, + QUEUE_ID_0, + popTime, + 3000, + Lists.newArrayList(1L), + new StringBuilder() + ); + consumerOrderInfoManager.commitAndNext( + TOPIC, + GROUP, + QUEUE_ID_0, + 1, + popTime + ); + await().atMost(Duration.ofSeconds(1)).until(notified::get); + assertTrue(consumerOrderInfoManager.getConsumerOrderInfoLockManager().getTimeoutMap().isEmpty()); + } + + @Test + public void testConsumeTheChangeInvisibleLonger() { + consumerOrderInfoManager.update( + null, + false, + TOPIC, + GROUP, + QUEUE_ID_0, + popTime, + 3000, + Lists.newArrayList(1L), + new StringBuilder() + ); + consumerOrderInfoManager.updateNextVisibleTime( + TOPIC, + GROUP, + QUEUE_ID_0, + 1, + popTime, + popTime + 5000 + ); + await().atLeast(Duration.ofSeconds(4)).atMost(Duration.ofSeconds(6)).until(notified::get); + assertTrue(consumerOrderInfoManager.getConsumerOrderInfoLockManager().getTimeoutMap().isEmpty()); + } + + @Test + public void testConsumeTheChangeInvisibleShorter() { + consumerOrderInfoManager.update( + null, + false, + TOPIC, + GROUP, + QUEUE_ID_0, + popTime, + 3000, + Lists.newArrayList(1L), + new StringBuilder() + ); + consumerOrderInfoManager.updateNextVisibleTime( + TOPIC, + GROUP, + QUEUE_ID_0, + 1, + popTime, + popTime + 1000 + ); + await().atLeast(Duration.ofMillis(500)).atMost(Duration.ofSeconds(2)).until(notified::get); + assertTrue(consumerOrderInfoManager.getConsumerOrderInfoLockManager().getTimeoutMap().isEmpty()); + } + + @Test + public void testRecover() { + ConsumerOrderInfoManager savedConsumerOrderInfoManager = new ConsumerOrderInfoManager(); + savedConsumerOrderInfoManager.update( + null, + false, + TOPIC, + GROUP, + QUEUE_ID_0, + popTime, + 3000, + Lists.newArrayList(1L), + new StringBuilder() + ); + String encodedData = savedConsumerOrderInfoManager.encode(); + + consumerOrderInfoManager.decode(encodedData); + await().atLeast(Duration.ofSeconds(2)).atMost(Duration.ofSeconds(4)).until(notified::get); + assertTrue(consumerOrderInfoManager.getConsumerOrderInfoLockManager().getTimeoutMap().isEmpty()); + } +} \ No newline at end of file diff --git a/broker/src/test/java/org/apache/rocketmq/broker/offset/ConsumerOrderInfoManagerTest.java b/broker/src/test/java/org/apache/rocketmq/broker/offset/ConsumerOrderInfoManagerTest.java new file mode 100644 index 00000000000..25b418c9344 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/offset/ConsumerOrderInfoManagerTest.java @@ -0,0 +1,537 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.offset; + +import java.time.Duration; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.subscription.SubscriptionGroupManager; +import org.apache.rocketmq.broker.topic.TopicConfigManager; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.remoting.protocol.header.ExtraInfoUtil; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.assertj.core.util.Lists; +import org.junit.Before; +import org.junit.Test; + +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ConsumerOrderInfoManagerTest { + + private static final String TOPIC = "topic"; + private static final String GROUP = "group"; + private static final int QUEUE_ID_0 = 0; + private static final int QUEUE_ID_1 = 1; + + private long popTime; + private ConsumerOrderInfoManager consumerOrderInfoManager; + + @Before + public void before() { + consumerOrderInfoManager = new ConsumerOrderInfoManager(); + popTime = System.currentTimeMillis(); + } + + @Test + public void testCommitAndNext() { + consumerOrderInfoManager.update( + null, + false, + TOPIC, + GROUP, + QUEUE_ID_0, + popTime, + 3000, + Lists.newArrayList(1L), + new StringBuilder() + ); + assertEncodeAndDecode(); + assertEquals(-2, consumerOrderInfoManager.commitAndNext( + TOPIC, + GROUP, + QUEUE_ID_0, + 1L, + popTime - 10 + )); + assertEncodeAndDecode(); + assertTrue(consumerOrderInfoManager.checkBlock( + null, + TOPIC, + GROUP, + QUEUE_ID_0, + TimeUnit.SECONDS.toMillis(3) + )); + + assertEquals(2, consumerOrderInfoManager.commitAndNext( + TOPIC, + GROUP, + QUEUE_ID_0, + 1L, + popTime + )); + assertEncodeAndDecode(); + assertFalse(consumerOrderInfoManager.checkBlock( + null, + TOPIC, + GROUP, + QUEUE_ID_0, + TimeUnit.SECONDS.toMillis(3) + )); + } + + @Test + public void testConsumedCount() { + { + // consume three new messages + StringBuilder orderInfoBuilder = new StringBuilder(); + consumerOrderInfoManager.update( + null, + false, + TOPIC, + GROUP, + QUEUE_ID_0, + popTime, + 3000, + Lists.newArrayList(1L, 2L, 3L), + orderInfoBuilder + ); + assertEncodeAndDecode(); + Map orderInfoMap = ExtraInfoUtil.parseOrderCountInfo(orderInfoBuilder.toString()); + assertEquals(1, orderInfoMap.size()); + assertEquals(0, orderInfoMap.get(ExtraInfoUtil.getStartOffsetInfoMapKey(TOPIC, QUEUE_ID_0)).intValue()); + } + + { + // reconsume same messages + StringBuilder orderInfoBuilder = new StringBuilder(); + consumerOrderInfoManager.update( + null, + false, + TOPIC, + GROUP, + QUEUE_ID_0, + popTime, + 3000, + Lists.newArrayList(1L, 2L, 3L), + orderInfoBuilder + ); + assertEncodeAndDecode(); + Map orderInfoMap = ExtraInfoUtil.parseOrderCountInfo(orderInfoBuilder.toString()); + assertEquals(4, orderInfoMap.size()); + assertEquals(1, orderInfoMap.get(ExtraInfoUtil.getStartOffsetInfoMapKey(TOPIC, QUEUE_ID_0)).intValue()); + for (int i = 1; i <= 3; i++) { + assertEquals(1, orderInfoMap.get(ExtraInfoUtil.getQueueOffsetMapKey(TOPIC, QUEUE_ID_0, i)).intValue()); + } + } + + { + // reconsume last two message + StringBuilder orderInfoBuilder = new StringBuilder(); + consumerOrderInfoManager.update( + null, + false, + TOPIC, + GROUP, + QUEUE_ID_0, + popTime, + 3000, + Lists.newArrayList(2L, 3L), + orderInfoBuilder + ); + assertEncodeAndDecode(); + Map orderInfoMap = ExtraInfoUtil.parseOrderCountInfo(orderInfoBuilder.toString()); + assertEquals(3, orderInfoMap.size()); + assertEquals(2, orderInfoMap.get(ExtraInfoUtil.getStartOffsetInfoMapKey(TOPIC, QUEUE_ID_0)).intValue()); + for (int i = 2; i <= 3; i++) { + assertEquals(2, orderInfoMap.get(ExtraInfoUtil.getQueueOffsetMapKey(TOPIC, QUEUE_ID_0, i)).intValue()); + } + } + + { + // consume a new message and reconsume last message + StringBuilder orderInfoBuilder = new StringBuilder(); + consumerOrderInfoManager.update( + null, + false, + TOPIC, + GROUP, + QUEUE_ID_0, + popTime, + 3000, + Lists.newArrayList(3L, 4L), + orderInfoBuilder + ); + assertEncodeAndDecode(); + Map orderInfoMap = ExtraInfoUtil.parseOrderCountInfo(orderInfoBuilder.toString()); + assertEquals(2, orderInfoMap.size()); + assertEquals(0, orderInfoMap.get(ExtraInfoUtil.getStartOffsetInfoMapKey(TOPIC, QUEUE_ID_0)).intValue()); + assertEquals(3, orderInfoMap.get(ExtraInfoUtil.getQueueOffsetMapKey(TOPIC, QUEUE_ID_0, 3)).intValue()); + } + + { + // consume two new messages + StringBuilder orderInfoBuilder = new StringBuilder(); + consumerOrderInfoManager.update( + null, + false, + TOPIC, + GROUP, + QUEUE_ID_0, + popTime, + 3000, + Lists.newArrayList(5L, 6L), + orderInfoBuilder + ); + assertEncodeAndDecode(); + Map orderInfoMap = ExtraInfoUtil.parseOrderCountInfo(orderInfoBuilder.toString()); + assertEquals(1, orderInfoMap.size()); + assertEquals(0, orderInfoMap.get(ExtraInfoUtil.getStartOffsetInfoMapKey(TOPIC, QUEUE_ID_0)).intValue()); + } + } + + @Test + public void testConsumedCountForMultiQueue() { + { + // consume two new messages + StringBuilder orderInfoBuilder = new StringBuilder(); + consumerOrderInfoManager.update( + null, + false, + TOPIC, + GROUP, + QUEUE_ID_0, + popTime, + 3000, + Lists.newArrayList(0L), + orderInfoBuilder + ); + consumerOrderInfoManager.update( + null, + false, + TOPIC, + GROUP, + QUEUE_ID_1, + popTime, + 3000, + Lists.newArrayList(0L), + orderInfoBuilder + ); + assertEncodeAndDecode(); + Map orderInfoMap = ExtraInfoUtil.parseOrderCountInfo(orderInfoBuilder.toString()); + assertEquals(2, orderInfoMap.size()); + assertEquals(0, orderInfoMap.get(ExtraInfoUtil.getStartOffsetInfoMapKey(TOPIC, QUEUE_ID_0)).intValue()); + assertEquals(0, orderInfoMap.get(ExtraInfoUtil.getStartOffsetInfoMapKey(TOPIC, QUEUE_ID_1)).intValue()); + } + { + // reconsume two message + StringBuilder orderInfoBuilder = new StringBuilder(); + consumerOrderInfoManager.update( + null, + false, + TOPIC, + GROUP, + QUEUE_ID_0, + popTime, + 3000, + Lists.newArrayList(0L), + orderInfoBuilder + ); + consumerOrderInfoManager.update( + null, + false, + TOPIC, + GROUP, + QUEUE_ID_1, + popTime, + 3000, + Lists.newArrayList(0L), + orderInfoBuilder + ); + assertEncodeAndDecode(); + Map orderInfoMap = ExtraInfoUtil.parseOrderCountInfo(orderInfoBuilder.toString()); + assertEquals(4, orderInfoMap.size()); + assertEquals(1, orderInfoMap.get(ExtraInfoUtil.getStartOffsetInfoMapKey(TOPIC, QUEUE_ID_0)).intValue()); + assertEquals(1, orderInfoMap.get(ExtraInfoUtil.getStartOffsetInfoMapKey(TOPIC, QUEUE_ID_1)).intValue()); + assertEquals(1, orderInfoMap.get(ExtraInfoUtil.getQueueOffsetMapKey(TOPIC, QUEUE_ID_0, 0L)).intValue()); + assertEquals(1, orderInfoMap.get(ExtraInfoUtil.getQueueOffsetMapKey(TOPIC, QUEUE_ID_1, 0L)).intValue()); + } + { + // reconsume with a new message + StringBuilder orderInfoBuilder = new StringBuilder(); + consumerOrderInfoManager.update( + null, + false, + TOPIC, + GROUP, + QUEUE_ID_0, + popTime, + 3000, + Lists.newArrayList(0L, 1L), + orderInfoBuilder + ); + consumerOrderInfoManager.update( + null, + false, + TOPIC, + GROUP, + QUEUE_ID_1, + popTime, + 3000, + Lists.newArrayList(0L), + orderInfoBuilder + ); + assertEncodeAndDecode(); + Map orderInfoMap = ExtraInfoUtil.parseOrderCountInfo(orderInfoBuilder.toString()); + assertEquals(4, orderInfoMap.size()); + assertEquals(0, orderInfoMap.get(ExtraInfoUtil.getStartOffsetInfoMapKey(TOPIC, QUEUE_ID_0)).intValue()); + assertEquals(2, orderInfoMap.get(ExtraInfoUtil.getStartOffsetInfoMapKey(TOPIC, QUEUE_ID_1)).intValue()); + assertEquals(2, orderInfoMap.get(ExtraInfoUtil.getQueueOffsetMapKey(TOPIC, QUEUE_ID_0, 0L)).intValue()); + assertNull(orderInfoMap.get(ExtraInfoUtil.getQueueOffsetMapKey(TOPIC, QUEUE_ID_0, 1L))); + assertEquals(2, orderInfoMap.get(ExtraInfoUtil.getQueueOffsetMapKey(TOPIC, QUEUE_ID_1, 0L)).intValue()); + } + } + + @Test + public void testUpdateNextVisibleTime() { + long invisibleTime = 3000; + + StringBuilder orderInfoBuilder = new StringBuilder(); + consumerOrderInfoManager.update( + null, + false, + TOPIC, + GROUP, + QUEUE_ID_0, + popTime, + 1, + Lists.newArrayList(1L, 2L, 3L), + orderInfoBuilder + ); + + consumerOrderInfoManager.updateNextVisibleTime(TOPIC, GROUP, QUEUE_ID_0, 2L, popTime, System.currentTimeMillis() + invisibleTime); + assertEncodeAndDecode(); + + assertEquals(2, consumerOrderInfoManager.commitAndNext(TOPIC, GROUP, QUEUE_ID_0, 1L, popTime)); + assertEncodeAndDecode(); + assertEquals(2, consumerOrderInfoManager.commitAndNext(TOPIC, GROUP, QUEUE_ID_0, 3L, popTime)); + assertEncodeAndDecode(); + + await().atMost(Duration.ofSeconds(invisibleTime + 1)).until(() -> !consumerOrderInfoManager.checkBlock(null, TOPIC, GROUP, QUEUE_ID_0, invisibleTime)); + + orderInfoBuilder = new StringBuilder(); + consumerOrderInfoManager.update( + null, + false, + TOPIC, + GROUP, + QUEUE_ID_0, + popTime, + 1, + Lists.newArrayList(2L, 3L, 4L), + orderInfoBuilder + ); + + consumerOrderInfoManager.updateNextVisibleTime(TOPIC, GROUP, QUEUE_ID_0, 2L, popTime, System.currentTimeMillis() + invisibleTime); + assertEncodeAndDecode(); + + assertEquals(2, consumerOrderInfoManager.commitAndNext(TOPIC, GROUP, QUEUE_ID_0, 3L, popTime)); + assertEncodeAndDecode(); + assertEquals(2, consumerOrderInfoManager.commitAndNext(TOPIC, GROUP, QUEUE_ID_0, 4L, popTime)); + assertEncodeAndDecode(); + assertTrue(consumerOrderInfoManager.checkBlock(null, TOPIC, GROUP, QUEUE_ID_0, invisibleTime)); + + assertEquals(5L, consumerOrderInfoManager.commitAndNext(TOPIC, GROUP, QUEUE_ID_0, 2L, popTime)); + assertEncodeAndDecode(); + assertFalse(consumerOrderInfoManager.checkBlock(null, TOPIC, GROUP, QUEUE_ID_0, invisibleTime)); + } + + @Test + public void testAutoCleanAndEncode() { + BrokerConfig brokerConfig = new BrokerConfig(); + BrokerController brokerController = mock(BrokerController.class); + TopicConfigManager topicConfigManager = mock(TopicConfigManager.class); + when(brokerController.getBrokerConfig()).thenReturn(brokerConfig); + when(brokerController.getTopicConfigManager()).thenReturn(topicConfigManager); + + SubscriptionGroupManager subscriptionGroupManager = mock(SubscriptionGroupManager.class); + when(brokerController.getSubscriptionGroupManager()).thenReturn(subscriptionGroupManager); + ConcurrentMap subscriptionGroupConfigConcurrentMap = new ConcurrentHashMap<>(); + subscriptionGroupConfigConcurrentMap.put(GROUP, new SubscriptionGroupConfig()); + when(subscriptionGroupManager.getSubscriptionGroupTable()).thenReturn(subscriptionGroupConfigConcurrentMap); + + TopicConfig topicConfig = new TopicConfig(TOPIC); + when(topicConfigManager.selectTopicConfig(eq(TOPIC))).thenReturn(topicConfig); + + ConsumerOrderInfoManager consumerOrderInfoManager = new ConsumerOrderInfoManager(brokerController); + + { + consumerOrderInfoManager.update(null, false, + "errTopic", + "errGroup", + QUEUE_ID_0, + popTime, + 1, + Lists.newArrayList(2L, 3L, 4L), + new StringBuilder()); + + consumerOrderInfoManager.autoClean(); + assertEquals(0, consumerOrderInfoManager.getTable().size()); + } + { + consumerOrderInfoManager.update(null, false, + TOPIC, + "errGroup", + QUEUE_ID_0, + popTime, + 1, + Lists.newArrayList(2L, 3L, 4L), + new StringBuilder()); + + consumerOrderInfoManager.autoClean(); + assertEquals(0, consumerOrderInfoManager.getTable().size()); + } + { + topicConfig.setReadQueueNums(0); + consumerOrderInfoManager.update(null, false, + TOPIC, + GROUP, + QUEUE_ID_0, + popTime, + 1, + Lists.newArrayList(2L, 3L, 4L), + new StringBuilder()); + + await().atMost(Duration.ofSeconds(1)).until(() -> { + consumerOrderInfoManager.autoClean(); + return consumerOrderInfoManager.getTable().size() == 0; + }); + } + { + topicConfig.setReadQueueNums(8); + consumerOrderInfoManager.update(null, false, + TOPIC, + GROUP, + QUEUE_ID_0, + popTime, + 1, + Lists.newArrayList(2L, 3L, 4L), + new StringBuilder()); + + consumerOrderInfoManager.autoClean(); + assertEquals(1, consumerOrderInfoManager.getTable().size()); + for (ConcurrentHashMap orderInfoMap : consumerOrderInfoManager.getTable().values()) { + assertEquals(1, orderInfoMap.size()); + assertNotNull(orderInfoMap.get(QUEUE_ID_0)); + break; + } + } + } + + private void assertEncodeAndDecode() { + ConsumerOrderInfoManager.OrderInfo prevOrderInfo = consumerOrderInfoManager.getTable().values().stream().findFirst() + .get().get(QUEUE_ID_0); + + String dataEncoded = consumerOrderInfoManager.encode(); + + consumerOrderInfoManager.decode(dataEncoded); + ConsumerOrderInfoManager.OrderInfo newOrderInfo = consumerOrderInfoManager.getTable().values().stream().findFirst() + .get().get(QUEUE_ID_0); + + assertNotSame(prevOrderInfo, newOrderInfo); + assertEquals(prevOrderInfo.getPopTime(), newOrderInfo.getPopTime()); + assertEquals(prevOrderInfo.getInvisibleTime(), newOrderInfo.getInvisibleTime()); + assertEquals(prevOrderInfo.getOffsetList(), newOrderInfo.getOffsetList()); + assertEquals(prevOrderInfo.getOffsetConsumedCount(), newOrderInfo.getOffsetConsumedCount()); + assertEquals(prevOrderInfo.getOffsetNextVisibleTime(), newOrderInfo.getOffsetNextVisibleTime()); + assertEquals(prevOrderInfo.getLastConsumeTimestamp(), newOrderInfo.getLastConsumeTimestamp()); + assertEquals(prevOrderInfo.getCommitOffsetBit(), newOrderInfo.getCommitOffsetBit()); + } + + @Test + public void testLoadFromOldVersionOrderInfoData() { + consumerOrderInfoManager.update(null, false, + TOPIC, + GROUP, + QUEUE_ID_0, + popTime, + 1, + Lists.newArrayList(2L, 3L, 4L), + new StringBuilder()); + ConsumerOrderInfoManager.OrderInfo orderInfo = consumerOrderInfoManager.getTable().values().stream().findFirst() + .get().get(QUEUE_ID_0); + + orderInfo.setInvisibleTime(null); + orderInfo.setOffsetConsumedCount(null); + orderInfo.setOffsetNextVisibleTime(null); + + String dataEncoded = consumerOrderInfoManager.encode(); + + consumerOrderInfoManager.decode(dataEncoded); + assertTrue(consumerOrderInfoManager.checkBlock(null, TOPIC, GROUP, QUEUE_ID_0, 3000)); + + StringBuilder orderInfoBuilder = new StringBuilder(); + consumerOrderInfoManager.update(null, false, + TOPIC, + GROUP, + QUEUE_ID_0, + popTime, + 1, + Lists.newArrayList(3L, 4L, 5L), + orderInfoBuilder); + assertEncodeAndDecode(); + Map orderInfoMap = ExtraInfoUtil.parseOrderCountInfo(orderInfoBuilder.toString()); + assertEquals(3, orderInfoMap.size()); + assertEquals(0, orderInfoMap.get(ExtraInfoUtil.getStartOffsetInfoMapKey(TOPIC, QUEUE_ID_0)).intValue()); + assertEquals(1, orderInfoMap.get(ExtraInfoUtil.getQueueOffsetMapKey(TOPIC, QUEUE_ID_0, 3)).intValue()); + assertEquals(1, orderInfoMap.get(ExtraInfoUtil.getQueueOffsetMapKey(TOPIC, QUEUE_ID_0, 4)).intValue()); + } + + @Test + public void testReentrant() { + StringBuilder orderInfoBuilder = new StringBuilder(); + String attemptId = UUID.randomUUID().toString(); + consumerOrderInfoManager.update( + attemptId, + false, + TOPIC, + GROUP, + QUEUE_ID_0, + popTime, + 3000, + Lists.newArrayList(1L, 2L, 3L), + orderInfoBuilder + ); + + assertTrue(consumerOrderInfoManager.checkBlock(null, TOPIC, GROUP, QUEUE_ID_0, 3000)); + assertFalse(consumerOrderInfoManager.checkBlock(attemptId, TOPIC, GROUP, QUEUE_ID_0, 3000)); + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/offset/LmqConsumerOffsetManagerTest.java b/broker/src/test/java/org/apache/rocketmq/broker/offset/LmqConsumerOffsetManagerTest.java new file mode 100644 index 00000000000..9626bcaaeeb --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/offset/LmqConsumerOffsetManagerTest.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.offset; + +import java.io.File; +import java.util.Map; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.subscription.LmqSubscriptionGroupManager; +import org.apache.rocketmq.broker.topic.LmqTopicConfigManager; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.After; +import org.junit.Test; +import org.mockito.Spy; + +import static org.assertj.core.api.Assertions.assertThat; + +public class LmqConsumerOffsetManagerTest { + + @Spy + private BrokerController brokerController = new BrokerController(new BrokerConfig(), new NettyServerConfig(), + new NettyClientConfig(), new MessageStoreConfig()); + + @Test + public void testOffsetManage() { + LmqConsumerOffsetManager lmqConsumerOffsetManager = new LmqConsumerOffsetManager(brokerController); + LmqTopicConfigManager lmqTopicConfigManager = new LmqTopicConfigManager(brokerController); + LmqSubscriptionGroupManager lmqSubscriptionGroupManager = new LmqSubscriptionGroupManager(brokerController); + + String lmqTopicName = "%LMQ%1111"; + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName(lmqTopicName); + lmqTopicConfigManager.updateTopicConfig(topicConfig); + TopicConfig topicConfig1 = lmqTopicConfigManager.selectTopicConfig(lmqTopicName); + assertThat(topicConfig1.getTopicName()).isEqualTo(topicConfig.getTopicName()); + + String lmqGroupName = "%LMQ%GID_test"; + SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); + subscriptionGroupConfig.setGroupName(lmqGroupName); + lmqSubscriptionGroupManager.updateSubscriptionGroupConfig(subscriptionGroupConfig); + SubscriptionGroupConfig subscriptionGroupConfig1 = lmqSubscriptionGroupManager.findSubscriptionGroupConfig( + lmqGroupName); + assertThat(subscriptionGroupConfig1.getGroupName()).isEqualTo(subscriptionGroupConfig.getGroupName()); + + lmqConsumerOffsetManager.commitOffset("127.0.0.1", lmqGroupName, lmqTopicName, 0, 10L); + Map integerLongMap = lmqConsumerOffsetManager.queryOffset(lmqGroupName, lmqTopicName); + assertThat(integerLongMap.get(0)).isEqualTo(10L); + long offset = lmqConsumerOffsetManager.queryOffset(lmqGroupName, lmqTopicName, 0); + assertThat(offset).isEqualTo(10L); + + long offset1 = lmqConsumerOffsetManager.queryOffset(lmqGroupName, lmqTopicName + "test", 0); + assertThat(offset1).isEqualTo(-1L); + } + + @Test + public void testOffsetManage1() { + LmqConsumerOffsetManager lmqConsumerOffsetManager = new LmqConsumerOffsetManager(brokerController); + + String lmqTopicName = "%LMQ%1111"; + + String lmqGroupName = "%LMQ%GID_test"; + + lmqConsumerOffsetManager.commitOffset("127.0.0.1", lmqGroupName, lmqTopicName, 0, 10L); + + lmqTopicName = "%LMQ%1222"; + + lmqGroupName = "%LMQ%GID_test222"; + + lmqConsumerOffsetManager.commitOffset("127.0.0.1", lmqGroupName, lmqTopicName, 0, 10L); + lmqConsumerOffsetManager.commitOffset("127.0.0.1","GID_test1", "MqttTest",0, 10L); + + String json = lmqConsumerOffsetManager.encode(true); + + LmqConsumerOffsetManager lmqConsumerOffsetManager1 = new LmqConsumerOffsetManager(brokerController); + + lmqConsumerOffsetManager1.decode(json); + + assertThat(lmqConsumerOffsetManager1.getOffsetTable().size()).isEqualTo(1); + assertThat(lmqConsumerOffsetManager1.getLmqOffsetTable().size()).isEqualTo(2); + } + + @After + public void destroy() { + UtilAll.deleteFile(new File(new MessageStoreConfig().getStorePathRootDir())); + } + +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/offset/RocksDBConsumerOffsetManagerTest.java b/broker/src/test/java/org/apache/rocketmq/broker/offset/RocksDBConsumerOffsetManagerTest.java new file mode 100644 index 00000000000..58b690c9a34 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/offset/RocksDBConsumerOffsetManagerTest.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.offset; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RocksDBConsumerOffsetManagerTest { + + private static final String KEY = "FooBar@FooBarGroup"; + + private BrokerController brokerController; + + private ConsumerOffsetManager consumerOffsetManager; + + @Before + public void init() { + if (notToBeExecuted()) { + return; + } + brokerController = Mockito.mock(BrokerController.class); + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + Mockito.when(brokerController.getMessageStoreConfig()).thenReturn(messageStoreConfig); + + consumerOffsetManager = new RocksDBConsumerOffsetManager(brokerController); + consumerOffsetManager.load(); + + ConcurrentHashMap> offsetTable = new ConcurrentHashMap<>(512); + offsetTable.put(KEY,new ConcurrentHashMap() {{ + put(1,2L); + put(2,3L); + }}); + consumerOffsetManager.setOffsetTable(offsetTable); + } + + @After + public void destroy() { + if (notToBeExecuted()) { + return; + } + if (consumerOffsetManager != null) { + consumerOffsetManager.stop(); + } + } + + @Test + public void cleanOffsetByTopic_NotExist() { + if (notToBeExecuted()) { + return; + } + consumerOffsetManager.cleanOffsetByTopic("InvalidTopic"); + assertThat(consumerOffsetManager.getOffsetTable().containsKey(KEY)).isTrue(); + } + + @Test + public void cleanOffsetByTopic_Exist() { + if (notToBeExecuted()) { + return; + } + consumerOffsetManager.cleanOffsetByTopic("FooBar"); + assertThat(!consumerOffsetManager.getOffsetTable().containsKey(KEY)).isTrue(); + } + + @Test + public void testOffsetPersistInMemory() { + if (notToBeExecuted()) { + return; + } + ConcurrentMap> offsetTable = consumerOffsetManager.getOffsetTable(); + ConcurrentMap table = new ConcurrentHashMap<>(); + table.put(0, 1L); + table.put(1, 3L); + String group = "G1"; + offsetTable.put(group, table); + + consumerOffsetManager.persist(); + consumerOffsetManager.stop(); + consumerOffsetManager.load(); + + ConcurrentMap offsetTableLoaded = consumerOffsetManager.getOffsetTable().get(group); + Assert.assertEquals(table, offsetTableLoaded); + } + + private boolean notToBeExecuted() { + return MixAll.isMac(); + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/pagecache/ManyMessageTransferTest.java b/broker/src/test/java/org/apache/rocketmq/broker/pagecache/ManyMessageTransferTest.java new file mode 100644 index 00000000000..2617b5cee9f --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/pagecache/ManyMessageTransferTest.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.pagecache; + +import java.nio.ByteBuffer; +import org.apache.rocketmq.store.GetMessageResult; +import org.junit.Assert; +import org.junit.Test; + +public class ManyMessageTransferTest { + + @Test + public void ManyMessageTransferBuilderTest() { + ByteBuffer byteBuffer = ByteBuffer.allocate(20); + byteBuffer.putInt(20); + GetMessageResult getMessageResult = new GetMessageResult(); + ManyMessageTransfer manyMessageTransfer = new ManyMessageTransfer(byteBuffer,getMessageResult); + } + + @Test + public void ManyMessageTransferPosTest() { + ByteBuffer byteBuffer = ByteBuffer.allocate(20); + byteBuffer.putInt(20); + GetMessageResult getMessageResult = new GetMessageResult(); + ManyMessageTransfer manyMessageTransfer = new ManyMessageTransfer(byteBuffer,getMessageResult); + Assert.assertEquals(manyMessageTransfer.position(),4); + } + + @Test + public void ManyMessageTransferCountTest() { + ByteBuffer byteBuffer = ByteBuffer.allocate(20); + byteBuffer.putInt(20); + GetMessageResult getMessageResult = new GetMessageResult(); + ManyMessageTransfer manyMessageTransfer = new ManyMessageTransfer(byteBuffer,getMessageResult); + + Assert.assertEquals(manyMessageTransfer.count(),20); + + } + + @Test + public void ManyMessageTransferCloseTest() { + ByteBuffer byteBuffer = ByteBuffer.allocate(20); + byteBuffer.putInt(20); + GetMessageResult getMessageResult = new GetMessageResult(); + ManyMessageTransfer manyMessageTransfer = new ManyMessageTransfer(byteBuffer,getMessageResult); + manyMessageTransfer.close(); + manyMessageTransfer.deallocate(); + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/pagecache/OneMessageTransferTest.java b/broker/src/test/java/org/apache/rocketmq/broker/pagecache/OneMessageTransferTest.java new file mode 100644 index 00000000000..1930641d7b6 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/pagecache/OneMessageTransferTest.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.pagecache; + +import java.nio.ByteBuffer; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.logfile.DefaultMappedFile; +import org.junit.Assert; +import org.junit.Test; + +public class OneMessageTransferTest { + + @Test + public void OneMessageTransferTest() { + ByteBuffer byteBuffer = ByteBuffer.allocate(20); + byteBuffer.putInt(20); + SelectMappedBufferResult selectMappedBufferResult = new SelectMappedBufferResult(0,byteBuffer,20,new DefaultMappedFile()); + OneMessageTransfer manyMessageTransfer = new OneMessageTransfer(byteBuffer,selectMappedBufferResult); + } + + @Test + public void OneMessageTransferCountTest() { + ByteBuffer byteBuffer = ByteBuffer.allocate(20); + byteBuffer.putInt(20); + SelectMappedBufferResult selectMappedBufferResult = new SelectMappedBufferResult(0,byteBuffer,20,new DefaultMappedFile()); + OneMessageTransfer manyMessageTransfer = new OneMessageTransfer(byteBuffer,selectMappedBufferResult); + Assert.assertEquals(manyMessageTransfer.count(),40); + } + + @Test + public void OneMessageTransferPosTest() { + ByteBuffer byteBuffer = ByteBuffer.allocate(20); + byteBuffer.putInt(20); + SelectMappedBufferResult selectMappedBufferResult = new SelectMappedBufferResult(0,byteBuffer,20,new DefaultMappedFile()); + OneMessageTransfer manyMessageTransfer = new OneMessageTransfer(byteBuffer,selectMappedBufferResult); + Assert.assertEquals(manyMessageTransfer.position(),8); + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/processor/AckMessageProcessorTest.java b/broker/src/test/java/org/apache/rocketmq/broker/processor/AckMessageProcessorTest.java new file mode 100644 index 00000000000..c0afb46c330 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/AckMessageProcessorTest.java @@ -0,0 +1,361 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.processor; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.client.ClientChannelInfo; +import org.apache.rocketmq.broker.client.net.Broker2Client; +import org.apache.rocketmq.broker.failover.EscapeBridge; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.exception.RemotingSendRequestException; +import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.protocol.LanguageCode; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.BatchAck; +import org.apache.rocketmq.remoting.protocol.body.BatchAckMessageRequestBody; +import org.apache.rocketmq.remoting.protocol.header.AckMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ExtraInfoUtil; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumerData; +import org.apache.rocketmq.store.AppendMessageResult; +import org.apache.rocketmq.store.AppendMessageStatus; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collections; + +import static org.apache.rocketmq.broker.processor.PullMessageProcessorTest.createConsumerData; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class AckMessageProcessorTest { + private AckMessageProcessor ackMessageProcessor; + @Mock + private PopMessageProcessor popMessageProcessor; + @Spy + private BrokerController brokerController = new BrokerController(new BrokerConfig(), new NettyServerConfig(), new NettyClientConfig(), new MessageStoreConfig()); + @Mock + private ChannelHandlerContext handlerContext; + @Mock + private DefaultMessageStore messageStore; + @Mock + private Channel channel; + + private String topic = "FooBar"; + private String group = "FooBarGroup"; + private ClientChannelInfo clientInfo; + @Mock + private Broker2Client broker2Client; + + private static final long MIN_OFFSET_IN_QUEUE = 100; + private static final long MAX_OFFSET_IN_QUEUE = 999; + + @Before + public void init() throws IllegalAccessException, NoSuchFieldException { + clientInfo = new ClientChannelInfo(channel, "127.0.0.1", LanguageCode.JAVA, 0); + brokerController.setMessageStore(messageStore); + Field field = BrokerController.class.getDeclaredField("broker2Client"); + field.setAccessible(true); + field.set(brokerController, broker2Client); + EscapeBridge escapeBridge = new EscapeBridge(brokerController); + Mockito.when(brokerController.getEscapeBridge()).thenReturn(escapeBridge); + Channel mockChannel = mock(Channel.class); + when(handlerContext.channel()).thenReturn(mockChannel); + brokerController.getTopicConfigManager().getTopicConfigTable().put(topic, new TopicConfig()); + ConsumerData consumerData = createConsumerData(group, topic); + brokerController.getConsumerManager().registerConsumer( + consumerData.getGroupName(), + clientInfo, + consumerData.getConsumeType(), + consumerData.getMessageModel(), + consumerData.getConsumeFromWhere(), + consumerData.getSubscriptionDataSet(), + false); + ackMessageProcessor = new AckMessageProcessor(brokerController); + + when(messageStore.getMinOffsetInQueue(anyString(), anyInt())).thenReturn(MIN_OFFSET_IN_QUEUE); + when(messageStore.getMaxOffsetInQueue(anyString(), anyInt())).thenReturn(MAX_OFFSET_IN_QUEUE); + + when(brokerController.getPopMessageProcessor()).thenReturn(popMessageProcessor); + } + + @Test + public void testProcessRequest_Success() throws RemotingCommandException, InterruptedException, RemotingTimeoutException, RemotingSendRequestException { + when(messageStore.putMessage(any(MessageExtBrokerInner.class))).thenReturn(new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK))); + PopBufferMergeService popBufferMergeService = mock(PopBufferMergeService.class); + when(popBufferMergeService.addAk(anyInt(), any())).thenReturn(false); + when(popMessageProcessor.getPopBufferMergeService()).thenReturn(popBufferMergeService); + + int queueId = 0; + long queueOffset = 0; + long popTime = System.currentTimeMillis() - 1_000; + long invisibleTime = 30_000; + int reviveQid = 0; + String brokerName = "test_broker"; + String extraInfo = ExtraInfoUtil.buildExtraInfo(queueOffset, popTime, invisibleTime, reviveQid, + topic, brokerName, queueId) + MessageConst.KEY_SEPARATOR + queueOffset; + AckMessageRequestHeader requestHeader = new AckMessageRequestHeader(); + requestHeader.setTopic(topic); + requestHeader.setQueueId(0); + requestHeader.setOffset(MIN_OFFSET_IN_QUEUE + 1); + requestHeader.setConsumerGroup(group); + requestHeader.setExtraInfo(extraInfo); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.ACK_MESSAGE, requestHeader); + request.makeCustomHeaderToNet(); + RemotingCommand responseToReturn = ackMessageProcessor.processRequest(handlerContext, request); + assertThat(responseToReturn.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(responseToReturn.getOpaque()).isEqualTo(request.getOpaque()); + } + + @Test + public void testProcessRequest_WrongRequestCode() throws Exception { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, null); + RemotingCommand response = ackMessageProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.MESSAGE_ILLEGAL); + assertThat(response.getRemark()).isEqualTo("AckMessageProcessor failed to process RequestCode: " + RequestCode.SEND_MESSAGE); + } + + @Test + public void testSingleAck_TopicCheck() throws RemotingCommandException { + AckMessageRequestHeader requestHeader = new AckMessageRequestHeader(); + requestHeader.setTopic("wrongTopic"); + requestHeader.setQueueId(0); + requestHeader.setOffset(0L); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.ACK_MESSAGE, requestHeader); + request.makeCustomHeaderToNet(); + RemotingCommand response = ackMessageProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.TOPIC_NOT_EXIST); + assertThat(response.getRemark()).contains("not exist, apply first"); + } + + @Test + public void testSingleAck_QueueCheck() throws RemotingCommandException { + { + int qId = -1; + AckMessageRequestHeader requestHeader = new AckMessageRequestHeader(); + requestHeader.setTopic(topic); + requestHeader.setQueueId(qId); + requestHeader.setOffset(0L); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.ACK_MESSAGE, requestHeader); + request.makeCustomHeaderToNet(); + RemotingCommand response = ackMessageProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.MESSAGE_ILLEGAL); + assertThat(response.getRemark()).contains("queueId[" + qId + "] is illegal"); + } + + { + int qId = 17; + AckMessageRequestHeader requestHeader = new AckMessageRequestHeader(); + requestHeader.setTopic(topic); + requestHeader.setQueueId(qId); + requestHeader.setOffset(0L); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.ACK_MESSAGE, requestHeader); + request.makeCustomHeaderToNet(); + RemotingCommand response = ackMessageProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.MESSAGE_ILLEGAL); + assertThat(response.getRemark()).contains("queueId[" + qId + "] is illegal"); + } + } + + @Test + public void testSingleAck_OffsetCheck() throws RemotingCommandException { + { + AckMessageRequestHeader requestHeader = new AckMessageRequestHeader(); + requestHeader.setTopic(topic); + requestHeader.setQueueId(0); + requestHeader.setOffset(MIN_OFFSET_IN_QUEUE - 1); + //requestHeader.setOffset(maxOffsetInQueue + 1); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.ACK_MESSAGE, requestHeader); + request.makeCustomHeaderToNet(); + RemotingCommand response = ackMessageProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.NO_MESSAGE); + assertThat(response.getRemark()).contains("offset is illegal"); + } + + { + AckMessageRequestHeader requestHeader = new AckMessageRequestHeader(); + requestHeader.setTopic(topic); + requestHeader.setQueueId(0); + //requestHeader.setOffset(minOffsetInQueue - 1); + requestHeader.setOffset(MAX_OFFSET_IN_QUEUE + 1); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.ACK_MESSAGE, requestHeader); + request.makeCustomHeaderToNet(); + RemotingCommand response = ackMessageProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.NO_MESSAGE); + assertThat(response.getRemark()).contains("offset is illegal"); + } + } + + @Test + public void testBatchAck_NoMessage() throws RemotingCommandException { + { + //reqBody == null + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.BATCH_ACK_MESSAGE, null); + RemotingCommand response = ackMessageProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.NO_MESSAGE); + } + + { + //reqBody.getAcks() == null + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.BATCH_ACK_MESSAGE, null); + BatchAckMessageRequestBody reqBody = new BatchAckMessageRequestBody(); + request.setBody(reqBody.encode()); + RemotingCommand response = ackMessageProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.NO_MESSAGE); + } + + { + //reqBody.getAcks().isEmpty() + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.BATCH_ACK_MESSAGE, null); + BatchAckMessageRequestBody reqBody = new BatchAckMessageRequestBody(); + reqBody.setAcks(new ArrayList<>()); + request.setBody(reqBody.encode()); + RemotingCommand response = ackMessageProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.NO_MESSAGE); + } + } + + @Test + public void testSingleAck_appendAck() throws RemotingCommandException { + { + // buffer addAk OK + PopBufferMergeService popBufferMergeService = mock(PopBufferMergeService.class); + when(popBufferMergeService.addAk(anyInt(), any())).thenReturn(true); + when(popMessageProcessor.getPopBufferMergeService()).thenReturn(popBufferMergeService); + + AckMessageRequestHeader requestHeader = new AckMessageRequestHeader(); + long ackOffset = MIN_OFFSET_IN_QUEUE + 10; + requestHeader.setTopic(topic); + requestHeader.setQueueId(0); + requestHeader.setOffset(ackOffset); + requestHeader.setConsumerGroup(MixAll.DEFAULT_CONSUMER_GROUP); + requestHeader.setExtraInfo("64 1666860736757 60000 4 0 broker-a 0 " + ackOffset); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.ACK_MESSAGE, requestHeader); + request.makeCustomHeaderToNet(); + RemotingCommand response = ackMessageProcessor.processRequest(handlerContext, request); + + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + { + // buffer addAk fail + PopBufferMergeService popBufferMergeService = mock(PopBufferMergeService.class); + when(popBufferMergeService.addAk(anyInt(), any())).thenReturn(false); + when(popMessageProcessor.getPopBufferMergeService()).thenReturn(popBufferMergeService); + // store putMessage OK + PutMessageResult putMessageResult = new PutMessageResult(PutMessageStatus.PUT_OK, null); + when(messageStore.putMessage(any())).thenReturn(putMessageResult); + + AckMessageRequestHeader requestHeader = new AckMessageRequestHeader(); + long ackOffset = MIN_OFFSET_IN_QUEUE + 10; + requestHeader.setTopic(topic); + requestHeader.setQueueId(0); + requestHeader.setOffset(ackOffset); + requestHeader.setConsumerGroup(MixAll.DEFAULT_CONSUMER_GROUP); + requestHeader.setExtraInfo("64 1666860736757 60000 4 0 broker-a 0 " + ackOffset); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.ACK_MESSAGE, requestHeader); + request.makeCustomHeaderToNet(); + RemotingCommand response = ackMessageProcessor.processRequest(handlerContext, request); + + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + } + + @Test + public void testBatchAck_appendAck() throws RemotingCommandException { + { + // buffer addAk OK + PopBufferMergeService popBufferMergeService = mock(PopBufferMergeService.class); + when(popBufferMergeService.addAk(anyInt(), any())).thenReturn(true); + when(popMessageProcessor.getPopBufferMergeService()).thenReturn(popBufferMergeService); + + BatchAck bAck1 = new BatchAck(); + bAck1.setConsumerGroup(MixAll.DEFAULT_CONSUMER_GROUP); + bAck1.setTopic(topic); + bAck1.setStartOffset(MIN_OFFSET_IN_QUEUE); + bAck1.setBitSet(new BitSet()); + bAck1.getBitSet().set(1); + bAck1.setRetry("0"); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.BATCH_ACK_MESSAGE, null); + BatchAckMessageRequestBody reqBody = new BatchAckMessageRequestBody(); + reqBody.setAcks(Collections.singletonList(bAck1)); + request.setBody(reqBody.encode()); + request.makeCustomHeaderToNet(); + RemotingCommand response = ackMessageProcessor.processRequest(handlerContext, request); + + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + { + // buffer addAk fail + PopBufferMergeService popBufferMergeService = mock(PopBufferMergeService.class); + when(popBufferMergeService.addAk(anyInt(), any())).thenReturn(false); + when(popMessageProcessor.getPopBufferMergeService()).thenReturn(popBufferMergeService); + // store putMessage OK + PutMessageResult putMessageResult = new PutMessageResult(PutMessageStatus.PUT_OK, null); + when(messageStore.putMessage(any())).thenReturn(putMessageResult); + + BatchAck bAck1 = new BatchAck(); + bAck1.setConsumerGroup(MixAll.DEFAULT_CONSUMER_GROUP); + bAck1.setTopic(topic); + bAck1.setStartOffset(MIN_OFFSET_IN_QUEUE); + bAck1.setBitSet(new BitSet()); + bAck1.getBitSet().set(1); + bAck1.setRetry("0"); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.BATCH_ACK_MESSAGE, null); + BatchAckMessageRequestBody reqBody = new BatchAckMessageRequestBody(); + reqBody.setAcks(Arrays.asList(bAck1)); + request.setBody(reqBody.encode()); + request.makeCustomHeaderToNet(); + RemotingCommand response = ackMessageProcessor.processRequest(handlerContext, request); + + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + } + +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessorTest.java b/broker/src/test/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessorTest.java new file mode 100644 index 00000000000..e4fcc690d11 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessorTest.java @@ -0,0 +1,698 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.processor; + +import com.alibaba.fastjson.JSON; +import com.google.common.collect.Sets; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.LongAdder; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.client.ConsumerGroupInfo; +import org.apache.rocketmq.broker.client.ConsumerManager; +import org.apache.rocketmq.broker.offset.ConsumerOffsetManager; +import org.apache.rocketmq.broker.schedule.ScheduleMessageService; +import org.apache.rocketmq.broker.subscription.RocksDBSubscriptionGroupManager; +import org.apache.rocketmq.broker.topic.RocksDBTopicConfigManager; +import org.apache.rocketmq.broker.topic.TopicConfigManager; +import org.apache.rocketmq.common.BoundaryType; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.KeyBuilder; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.TopicFilterType; +import org.apache.rocketmq.common.TopicQueueId; +import org.apache.rocketmq.common.constant.PermName; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.LockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.body.UnlockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.header.CreateTopicRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.DeleteTopicRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetAllTopicConfigResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.GetEarliestMsgStoretimeRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMaxOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMinOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetTopicConfigRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ResumeCheckHalfMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SearchOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.logfile.DefaultMappedFile; +import org.apache.rocketmq.store.stats.BrokerStats; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class AdminBrokerProcessorTest { + + private AdminBrokerProcessor adminBrokerProcessor; + + @Mock + private ChannelHandlerContext handlerContext; + + @Mock + private Channel channel; + + @Spy + private BrokerController + brokerController = new BrokerController(new BrokerConfig(), new NettyServerConfig(), new NettyClientConfig(), + new MessageStoreConfig()); + + @Mock + private MessageStore messageStore; + + @Mock + private SendMessageProcessor sendMessageProcessor; + + @Mock + private ConcurrentMap inFlyWritingCounterMap; + + private Set systemTopicSet; + private String topic; + + @Mock + private SocketAddress socketAddress; + @Mock + private BrokerStats brokerStats; + @Mock + private TopicConfigManager topicConfigManager; + @Mock + private ConsumerManager consumerManager; + @Mock + private ConsumerOffsetManager consumerOffsetManager; + @Mock + private DefaultMessageStore defaultMessageStore; + @Mock + private ScheduleMessageService scheduleMessageService; + + @Before + public void init() throws Exception { + brokerController.setMessageStore(messageStore); + + //doReturn(sendMessageProcessor).when(brokerController).getSendMessageProcessor(); + + adminBrokerProcessor = new AdminBrokerProcessor(brokerController); + + systemTopicSet = Sets.newHashSet( + TopicValidator.RMQ_SYS_SELF_TEST_TOPIC, + TopicValidator.RMQ_SYS_BENCHMARK_TOPIC, + TopicValidator.RMQ_SYS_SCHEDULE_TOPIC, + TopicValidator.RMQ_SYS_OFFSET_MOVED_EVENT, + TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC, + this.brokerController.getBrokerConfig().getBrokerClusterName(), + this.brokerController.getBrokerConfig().getBrokerClusterName() + "_" + MixAll.REPLY_TOPIC_POSTFIX); + if (this.brokerController.getBrokerConfig().isTraceTopicEnable()) { + systemTopicSet.add(this.brokerController.getBrokerConfig().getMsgTraceTopicName()); + } + when(handlerContext.channel()).thenReturn(channel); + when(channel.remoteAddress()).thenReturn(new InetSocketAddress("127.0.0.1", 12345)); + + topic = "FooBar" + System.nanoTime(); + + brokerController.getTopicConfigManager().getTopicConfigTable().put(topic, new TopicConfig(topic)); + brokerController.getMessageStoreConfig().setTimerWheelEnable(false); + } + + @After + public void destroy() { + if (notToBeExecuted()) { + return; + } + if (brokerController.getSubscriptionGroupManager() != null) { + brokerController.getSubscriptionGroupManager().stop(); + } + if (brokerController.getTopicConfigManager() != null) { + brokerController.getTopicConfigManager().stop(); + } + if (brokerController.getConsumerOffsetManager() != null) { + brokerController.getConsumerOffsetManager().stop(); + } + } + + private void initRocksdbTopicManager() { + if (notToBeExecuted()) { + return; + } + RocksDBTopicConfigManager rocksDBTopicConfigManager = new RocksDBTopicConfigManager(brokerController); + brokerController.setTopicConfigManager(rocksDBTopicConfigManager); + rocksDBTopicConfigManager.load(); + } + + private void initRocksdbSubscriptionManager() { + if (notToBeExecuted()) { + return; + } + RocksDBSubscriptionGroupManager rocksDBSubscriptionGroupManager = new RocksDBSubscriptionGroupManager(brokerController); + brokerController.setSubscriptionGroupManager(rocksDBSubscriptionGroupManager); + rocksDBSubscriptionGroupManager.load(); + } + + @Test + public void testProcessRequest_success() throws RemotingCommandException, UnknownHostException { + RemotingCommand request = createUpdateBrokerConfigCommand(); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testProcessRequest_fail() throws RemotingCommandException, UnknownHostException { + RemotingCommand request = createResumeCheckHalfMessageCommand(); + when(messageStore.selectOneMessageByOffset(any(Long.class))).thenReturn(createSelectMappedBufferResult()); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SYSTEM_ERROR); + } + + @Test + public void testUpdateAndCreateTopicInRocksdb() throws Exception { + if (notToBeExecuted()) { + return; + } + initRocksdbTopicManager(); + testUpdateAndCreateTopic(); + } + + @Test + public void testUpdateAndCreateTopic() throws Exception { + //test system topic + for (String topic : systemTopicSet) { + RemotingCommand request = buildCreateTopicRequest(topic); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SYSTEM_ERROR); + assertThat(response.getRemark()).isEqualTo("The topic[" + topic + "] is conflict with system topic."); + } + + //test validate error topic + String topic = ""; + RemotingCommand request = buildCreateTopicRequest(topic); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SYSTEM_ERROR); + + topic = "TEST_CREATE_TOPIC"; + request = buildCreateTopicRequest(topic); + response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testDeleteTopicInRocksdb() throws Exception { + if (notToBeExecuted()) { + return; + } + initRocksdbTopicManager(); + testDeleteTopic(); + } + + @Test + public void testDeleteTopic() throws Exception { + //test system topic + for (String topic : systemTopicSet) { + RemotingCommand request = buildDeleteTopicRequest(topic); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SYSTEM_ERROR); + assertThat(response.getRemark()).isEqualTo("The topic[" + topic + "] is conflict with system topic."); + } + + String topic = "TEST_DELETE_TOPIC"; + RemotingCommand request = buildDeleteTopicRequest(topic); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testDeleteWithPopRetryTopic() throws Exception { + String topic = "topicA"; + String anotherTopic = "another_topicA"; + BrokerConfig brokerConfig = new BrokerConfig(); + + topicConfigManager = mock(TopicConfigManager.class); + when(brokerController.getTopicConfigManager()).thenReturn(topicConfigManager); + final ConcurrentHashMap topicConfigTable = new ConcurrentHashMap<>(); + topicConfigTable.put(topic, new TopicConfig()); + topicConfigTable.put(KeyBuilder.buildPopRetryTopic(topic, "cid1", brokerConfig.isEnableRetryTopicV2()), new TopicConfig()); + + topicConfigTable.put(anotherTopic, new TopicConfig()); + topicConfigTable.put(KeyBuilder.buildPopRetryTopic(anotherTopic, "cid2", brokerConfig.isEnableRetryTopicV2()), new TopicConfig()); + when(topicConfigManager.getTopicConfigTable()).thenReturn(topicConfigTable); + when(topicConfigManager.selectTopicConfig(anyString())).thenAnswer(invocation -> { + final String selectTopic = invocation.getArgument(0); + return topicConfigManager.getTopicConfigTable().get(selectTopic); + }); + + when(brokerController.getConsumerOffsetManager()).thenReturn(consumerOffsetManager); + when(consumerOffsetManager.whichGroupByTopic(topic)).thenReturn(Sets.newHashSet("cid1")); + + RemotingCommand request = buildDeleteTopicRequest(topic); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + verify(topicConfigManager).deleteTopicConfig(topic); + verify(topicConfigManager).deleteTopicConfig(KeyBuilder.buildPopRetryTopic(topic, "cid1", brokerConfig.isEnableRetryTopicV2())); + verify(messageStore, times(2)).deleteTopics(anySet()); + } + + @Test + public void testGetAllTopicConfigInRocksdb() throws Exception { + if (notToBeExecuted()) { + return; + } + initRocksdbTopicManager(); + testGetAllTopicConfig(); + } + + @Test + public void testGetAllTopicConfig() throws Exception { + GetAllTopicConfigResponseHeader getAllTopicConfigResponseHeader = new GetAllTopicConfigResponseHeader(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ALL_TOPIC_CONFIG, getAllTopicConfigResponseHeader); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testUpdateBrokerConfig() throws Exception { + handlerContext = mock(ChannelHandlerContext.class); + channel = mock(Channel.class); + when(handlerContext.channel()).thenReturn(channel); + socketAddress = mock(SocketAddress.class); + when(channel.remoteAddress()).thenReturn(socketAddress); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_BROKER_CONFIG, null); + Map bodyMap = new HashMap<>(); + bodyMap.put("key", "value"); + request.setBody(bodyMap.toString().getBytes()); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetBrokerConfig() throws Exception { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_BROKER_CONFIG, null); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testProcessRequest_UpdateConfigPath() throws RemotingCommandException { + final RemotingCommand updateConfigRequest = RemotingCommand.createRequestCommand(RequestCode.UPDATE_BROKER_CONFIG, null); + Properties properties = new Properties(); + + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + when(ctx.channel()).thenReturn(null); + + // Update allowed value + properties.setProperty("allAckInSyncStateSet", "true"); + updateConfigRequest.setBody(MixAll.properties2String(properties).getBytes(StandardCharsets.UTF_8)); + + RemotingCommand response = adminBrokerProcessor.processRequest(ctx, updateConfigRequest); + + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + //update disallowed value + properties.clear(); + properties.setProperty("brokerConfigPath", "test/path"); + updateConfigRequest.setBody(MixAll.properties2String(properties).getBytes(StandardCharsets.UTF_8)); + + response = adminBrokerProcessor.processRequest(ctx, updateConfigRequest); + + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.NO_PERMISSION); + assertThat(response.getRemark()).contains("Can not update config in black list."); + + //update disallowed value + properties.clear(); + properties.setProperty("configBlackList", "test;path"); + updateConfigRequest.setBody(MixAll.properties2String(properties).getBytes(StandardCharsets.UTF_8)); + + response = adminBrokerProcessor.processRequest(ctx, updateConfigRequest); + + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.NO_PERMISSION); + assertThat(response.getRemark()).contains("Can not update config in black list."); + } + + @Test + public void testSearchOffsetByTimestamp() throws Exception { + messageStore = mock(MessageStore.class); + when(messageStore.getOffsetInQueueByTime(anyString(), anyInt(), anyLong(), any(BoundaryType.class))).thenReturn(Long.MIN_VALUE); + when(brokerController.getMessageStore()).thenReturn(messageStore); + SearchOffsetRequestHeader searchOffsetRequestHeader = new SearchOffsetRequestHeader(); + searchOffsetRequestHeader.setTopic("topic"); + searchOffsetRequestHeader.setQueueId(0); + searchOffsetRequestHeader.setTimestamp(System.currentTimeMillis()); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.SEARCH_OFFSET_BY_TIMESTAMP, searchOffsetRequestHeader); + request.addExtField("topic", "topic"); + request.addExtField("queueId", "0"); + request.addExtField("timestamp", System.currentTimeMillis() + ""); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetMaxOffset() throws Exception { + messageStore = mock(MessageStore.class); + when(messageStore.getMaxOffsetInQueue(anyString(), anyInt())).thenReturn(Long.MIN_VALUE); + when(brokerController.getMessageStore()).thenReturn(messageStore); + GetMaxOffsetRequestHeader getMaxOffsetRequestHeader = new GetMaxOffsetRequestHeader(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_MAX_OFFSET, getMaxOffsetRequestHeader); + request.addExtField("topic", "topic"); + request.addExtField("queueId", "0"); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetMinOffset() throws Exception { + messageStore = mock(MessageStore.class); + when(messageStore.getMinOffsetInQueue(anyString(), anyInt())).thenReturn(Long.MIN_VALUE); + when(brokerController.getMessageStore()).thenReturn(messageStore); + GetMinOffsetRequestHeader getMinOffsetRequestHeader = new GetMinOffsetRequestHeader(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_MIN_OFFSET, getMinOffsetRequestHeader); + request.addExtField("topic", "topic"); + request.addExtField("queueId", "0"); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetEarliestMsgStoretime() throws Exception { + messageStore = mock(MessageStore.class); + when(brokerController.getMessageStore()).thenReturn(messageStore); + GetEarliestMsgStoretimeRequestHeader getEarliestMsgStoretimeRequestHeader = new GetEarliestMsgStoretimeRequestHeader(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_EARLIEST_MSG_STORETIME, getEarliestMsgStoretimeRequestHeader); + request.addExtField("topic", "topic"); + request.addExtField("queueId", "0"); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetBrokerRuntimeInfo() throws Exception { + brokerStats = mock(BrokerStats.class); + when(brokerController.getBrokerStats()).thenReturn(brokerStats); + when(brokerStats.getMsgPutTotalYesterdayMorning()).thenReturn(Long.MIN_VALUE); + when(brokerStats.getMsgPutTotalTodayMorning()).thenReturn(Long.MIN_VALUE); + when(brokerStats.getMsgPutTotalTodayNow()).thenReturn(Long.MIN_VALUE); + when(brokerStats.getMsgGetTotalTodayMorning()).thenReturn(Long.MIN_VALUE); + when(brokerStats.getMsgGetTotalTodayNow()).thenReturn(Long.MIN_VALUE); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_BROKER_RUNTIME_INFO, null); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testLockBatchMQ() throws Exception { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.LOCK_BATCH_MQ, null); + LockBatchRequestBody lockBatchRequestBody = new LockBatchRequestBody(); + lockBatchRequestBody.setClientId("1111"); + lockBatchRequestBody.setConsumerGroup("group"); + request.setBody(JSON.toJSON(lockBatchRequestBody).toString().getBytes()); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testUnlockBatchMQ() throws Exception { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UNLOCK_BATCH_MQ, null); + UnlockBatchRequestBody unlockBatchRequestBody = new UnlockBatchRequestBody(); + unlockBatchRequestBody.setClientId("11111"); + unlockBatchRequestBody.setConsumerGroup("group"); + request.setBody(JSON.toJSON(unlockBatchRequestBody).toString().getBytes()); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testUpdateAndCreateSubscriptionGroupInRocksdb() throws Exception { + initRocksdbSubscriptionManager(); + testUpdateAndCreateSubscriptionGroup(); + } + + @Test + public void testUpdateAndCreateSubscriptionGroup() throws RemotingCommandException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_AND_CREATE_SUBSCRIPTIONGROUP, null); + SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); + subscriptionGroupConfig.setBrokerId(1); + subscriptionGroupConfig.setGroupName("groupId"); + subscriptionGroupConfig.setConsumeEnable(Boolean.TRUE); + subscriptionGroupConfig.setConsumeBroadcastEnable(Boolean.TRUE); + subscriptionGroupConfig.setRetryMaxTimes(111); + subscriptionGroupConfig.setConsumeFromMinEnable(Boolean.TRUE); + request.setBody(JSON.toJSON(subscriptionGroupConfig).toString().getBytes()); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetAllSubscriptionGroupInRocksdb() throws Exception { + initRocksdbSubscriptionManager(); + testGetAllSubscriptionGroup(); + } + + @Test + public void testGetAllSubscriptionGroup() throws RemotingCommandException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ALL_SUBSCRIPTIONGROUP_CONFIG, null); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testDeleteSubscriptionGroupInRocksdb() throws Exception { + initRocksdbSubscriptionManager(); + testDeleteSubscriptionGroup(); + } + + @Test + public void testDeleteSubscriptionGroup() throws RemotingCommandException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.DELETE_SUBSCRIPTIONGROUP, null); + request.addExtField("groupName", "GID-Group-Name"); + request.addExtField("removeOffset", "true"); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetTopicStatsInfo() throws RemotingCommandException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_TOPIC_STATS_INFO, null); + request.addExtField("topic", "topicTest"); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.TOPIC_NOT_EXIST); + topicConfigManager = mock(TopicConfigManager.class); + when(brokerController.getTopicConfigManager()).thenReturn(topicConfigManager); + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName("topicTest"); + when(topicConfigManager.selectTopicConfig(anyString())).thenReturn(topicConfig); + RemotingCommand responseSuccess = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(responseSuccess.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetConsumerConnectionList() throws RemotingCommandException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_CONSUMER_CONNECTION_LIST, null); + request.addExtField("consumerGroup", "GID-group-test"); + consumerManager = mock(ConsumerManager.class); + when(brokerController.getConsumerManager()).thenReturn(consumerManager); + ConsumerGroupInfo consumerGroupInfo = new ConsumerGroupInfo("GID-group-test", ConsumeType.CONSUME_ACTIVELY, MessageModel.CLUSTERING, ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + when(consumerManager.getConsumerGroupInfo(anyString())).thenReturn(consumerGroupInfo); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetProducerConnectionList() throws RemotingCommandException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_PRODUCER_CONNECTION_LIST, null); + request.addExtField("producerGroup", "ProducerGroupId"); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SYSTEM_ERROR); + } + + @Test + public void testGetAllProducerInfo() throws RemotingCommandException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ALL_PRODUCER_INFO, null); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetConsumeStats() throws RemotingCommandException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_CONSUME_STATS, null); + request.addExtField("topic", "topicTest"); + request.addExtField("consumerGroup", "GID-test"); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetAllConsumerOffset() throws RemotingCommandException { + consumerOffsetManager = mock(ConsumerOffsetManager.class); + when(brokerController.getConsumerOffsetManager()).thenReturn(consumerOffsetManager); + ConsumerOffsetManager consumerOffset = new ConsumerOffsetManager(); + when(consumerOffsetManager.encode()).thenReturn(JSON.toJSONString(consumerOffset, false)); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ALL_CONSUMER_OFFSET, null); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetAllDelayOffset() throws Exception { + defaultMessageStore = mock(DefaultMessageStore.class); + scheduleMessageService = mock(ScheduleMessageService.class); +// when(brokerController.getMessageStore()).thenReturn(defaultMessageStore); + when(brokerController.getScheduleMessageService()).thenReturn(scheduleMessageService); + when(scheduleMessageService.encode()).thenReturn("content"); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ALL_DELAY_OFFSET, null); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetTopicConfigInRocksdb() throws Exception { + if (notToBeExecuted()) { + return; + } + initRocksdbTopicManager(); + testGetTopicConfig(); + } + + @Test + public void testGetTopicConfig() throws Exception { + String topic = "foobar"; + + brokerController.getTopicConfigManager().getTopicConfigTable().put(topic, new TopicConfig(topic)); + + { + GetTopicConfigRequestHeader requestHeader = new GetTopicConfigRequestHeader(); + requestHeader.setTopic(topic); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_TOPIC_CONFIG, requestHeader); + request.makeCustomHeaderToNet(); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(response.getBody()).isNotEmpty(); + } + { + GetTopicConfigRequestHeader requestHeader = new GetTopicConfigRequestHeader(); + requestHeader.setTopic("aaaaaaa"); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_TOPIC_CONFIG, requestHeader); + request.makeCustomHeaderToNet(); + RemotingCommand response = adminBrokerProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.TOPIC_NOT_EXIST); + assertThat(response.getRemark()).contains("No topic in this broker."); + } + } + + private RemotingCommand buildCreateTopicRequest(String topic) { + CreateTopicRequestHeader requestHeader = new CreateTopicRequestHeader(); + requestHeader.setTopic(topic); + requestHeader.setTopicFilterType(TopicFilterType.SINGLE_TAG.name()); + requestHeader.setReadQueueNums(8); + requestHeader.setWriteQueueNums(8); + requestHeader.setPerm(PermName.PERM_READ | PermName.PERM_WRITE); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_AND_CREATE_TOPIC, requestHeader); + request.makeCustomHeaderToNet(); + return request; + } + + private RemotingCommand buildDeleteTopicRequest(String topic) { + DeleteTopicRequestHeader requestHeader = new DeleteTopicRequestHeader(); + requestHeader.setTopic(topic); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.DELETE_TOPIC_IN_BROKER, requestHeader); + request.makeCustomHeaderToNet(); + return request; + } + + private MessageExt createDefaultMessageExt() { + MessageExt messageExt = new MessageExt(); + messageExt.setMsgId("12345678"); + messageExt.setQueueId(0); + messageExt.setCommitLogOffset(123456789L); + messageExt.setQueueOffset(1234); + MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_REAL_QUEUE_ID, "0"); + MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_REAL_TOPIC, "testTopic"); + MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_TRANSACTION_CHECK_TIMES, "15"); + return messageExt; + } + + private SelectMappedBufferResult createSelectMappedBufferResult() { + SelectMappedBufferResult result = new SelectMappedBufferResult(0, ByteBuffer.allocate(1024), 0, new DefaultMappedFile()); + return result; + } + + private ResumeCheckHalfMessageRequestHeader createResumeCheckHalfMessageRequestHeader() { + ResumeCheckHalfMessageRequestHeader header = new ResumeCheckHalfMessageRequestHeader(); + header.setMsgId("C0A803CA00002A9F0000000000031367"); + return header; + } + + private RemotingCommand createResumeCheckHalfMessageCommand() { + ResumeCheckHalfMessageRequestHeader header = createResumeCheckHalfMessageRequestHeader(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.RESUME_CHECK_HALF_MESSAGE, header); + request.makeCustomHeaderToNet(); + return request; + } + + private RemotingCommand createUpdateBrokerConfigCommand() { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_BROKER_CONFIG, null); + request.makeCustomHeaderToNet(); + return request; + } + + private boolean notToBeExecuted() { + return MixAll.isMac(); + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessorTest.java b/broker/src/test/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessorTest.java new file mode 100644 index 00000000000..e51e110a7a8 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/ChangeInvisibleTimeProcessorTest.java @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.processor; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import java.lang.reflect.Field; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.client.ClientChannelInfo; +import org.apache.rocketmq.broker.client.net.Broker2Client; +import org.apache.rocketmq.broker.failover.EscapeBridge; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.exception.RemotingSendRequestException; +import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.protocol.LanguageCode; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.ChangeInvisibleTimeRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ExtraInfoUtil; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumerData; +import org.apache.rocketmq.store.AppendMessageResult; +import org.apache.rocketmq.store.AppendMessageStatus; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.apache.rocketmq.broker.processor.PullMessageProcessorTest.createConsumerData; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ChangeInvisibleTimeProcessorTest { + private ChangeInvisibleTimeProcessor changeInvisibleTimeProcessor; + @Spy + private BrokerController brokerController = new BrokerController(new BrokerConfig(), new NettyServerConfig(), new NettyClientConfig(), new MessageStoreConfig()); + @Mock + private ChannelHandlerContext handlerContext; + @Mock + private DefaultMessageStore messageStore; + @Mock + private Channel channel; + + private String topic = "FooBar"; + private String group = "FooBarGroup"; + private ClientChannelInfo clientInfo; + @Mock + private Broker2Client broker2Client; + + @Mock + private EscapeBridge escapeBridge = new EscapeBridge(this.brokerController); + + @Before + public void init() throws IllegalAccessException, NoSuchFieldException { + brokerController.setMessageStore(messageStore); + Field field = BrokerController.class.getDeclaredField("broker2Client"); + field.setAccessible(true); + field.set(brokerController, broker2Client); + + Field ebField = BrokerController.class.getDeclaredField("escapeBridge"); + ebField.setAccessible(true); + ebField.set(brokerController, this.escapeBridge); + + Channel mockChannel = mock(Channel.class); + when(handlerContext.channel()).thenReturn(mockChannel); + brokerController.getTopicConfigManager().getTopicConfigTable().put(topic, new TopicConfig()); + ConsumerData consumerData = createConsumerData(group, topic); + clientInfo = new ClientChannelInfo(channel, "127.0.0.1", LanguageCode.JAVA, 0); + brokerController.getConsumerManager().registerConsumer( + consumerData.getGroupName(), + clientInfo, + consumerData.getConsumeType(), + consumerData.getMessageModel(), + consumerData.getConsumeFromWhere(), + consumerData.getSubscriptionDataSet(), + false); + changeInvisibleTimeProcessor = new ChangeInvisibleTimeProcessor(brokerController); + } + + @Test + public void testProcessRequest_Success() throws RemotingCommandException, InterruptedException, RemotingTimeoutException, RemotingSendRequestException { + when(escapeBridge.putMessageToSpecificQueue(any(MessageExtBrokerInner.class))).thenReturn(new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK))); + int queueId = 0; + long queueOffset = 0; + long popTime = System.currentTimeMillis() - 1_000; + long invisibleTime = 30_000; + int reviveQid = 0; + String brokerName = "test_broker"; + String extraInfo = ExtraInfoUtil.buildExtraInfo(queueOffset, popTime, invisibleTime, reviveQid, + topic, brokerName, queueId) + MessageConst.KEY_SEPARATOR + queueOffset; + + ChangeInvisibleTimeRequestHeader requestHeader = new ChangeInvisibleTimeRequestHeader(); + requestHeader.setTopic(topic); + requestHeader.setQueueId(queueId); + requestHeader.setOffset(queueOffset); + requestHeader.setConsumerGroup(group); + requestHeader.setExtraInfo(extraInfo); + requestHeader.setInvisibleTime(invisibleTime); + + final RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CHANGE_MESSAGE_INVISIBLETIME, requestHeader); + request.makeCustomHeaderToNet(); + RemotingCommand responseToReturn = changeInvisibleTimeProcessor.processRequest(handlerContext, request); + assertThat(responseToReturn.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(responseToReturn.getOpaque()).isEqualTo(request.getOpaque()); + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/processor/ClientManageProcessorTest.java b/broker/src/test/java/org/apache/rocketmq/broker/processor/ClientManageProcessorTest.java index 147c7323803..874adb4d5fa 100644 --- a/broker/src/test/java/org/apache/rocketmq/broker/processor/ClientManageProcessorTest.java +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/ClientManageProcessorTest.java @@ -18,21 +18,31 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; -import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; import java.util.UUID; +import java.util.List; +import java.util.ArrayList; import org.apache.rocketmq.broker.BrokerController; import org.apache.rocketmq.broker.client.ClientChannelInfo; import org.apache.rocketmq.broker.client.ConsumerGroupInfo; import org.apache.rocketmq.common.BrokerConfig; -import org.apache.rocketmq.common.protocol.RequestCode; -import org.apache.rocketmq.common.protocol.ResponseCode; -import org.apache.rocketmq.common.protocol.header.UnregisterClientRequestHeader; -import org.apache.rocketmq.common.protocol.heartbeat.ConsumerData; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; import org.apache.rocketmq.remoting.exception.RemotingCommandException; import org.apache.rocketmq.remoting.netty.NettyClientConfig; import org.apache.rocketmq.remoting.netty.NettyServerConfig; import org.apache.rocketmq.remoting.protocol.LanguageCode; import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.UnregisterClientRequestHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumerData; +import org.apache.rocketmq.remoting.protocol.heartbeat.HeartbeatData; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; import org.apache.rocketmq.store.config.MessageStoreConfig; import org.junit.Before; import org.junit.Test; @@ -41,7 +51,6 @@ import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; -import static org.apache.rocketmq.broker.processor.PullMessageProcessorTest.createConsumerData; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; @@ -67,7 +76,7 @@ public void init() { clientChannelInfo = new ClientChannelInfo(channel, clientId, LanguageCode.JAVA, 100); brokerController.getProducerManager().registerProducer(group, clientChannelInfo); - ConsumerData consumerData = createConsumerData(group, topic); + ConsumerData consumerData = PullMessageProcessorTest.createConsumerData(group, topic); brokerController.getConsumerManager().registerConsumer( consumerData.getGroupName(), clientChannelInfo, @@ -81,7 +90,7 @@ public void init() { @Test public void processRequest_UnRegisterProducer() throws Exception { brokerController.getProducerManager().registerProducer(group, clientChannelInfo); - HashMap channelMap = brokerController.getProducerManager().getGroupChannelTable().get(group); + Map channelMap = brokerController.getProducerManager().getGroupChannelTable().get(group); assertThat(channelMap).isNotNull(); assertThat(channelMap.get(channel)).isEqualTo(clientChannelInfo); @@ -108,6 +117,48 @@ public void processRequest_UnRegisterConsumer() throws RemotingCommandException assertThat(consumerGroupInfo).isNull(); } + @Test + public void processRequest_heartbeat() throws RemotingCommandException { + RemotingCommand request = createHeartbeatCommand(false, "topicA"); + RemotingCommand response = clientManageProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(Boolean.parseBoolean(response.getExtFields().get(MixAll.IS_SUB_CHANGE))).isFalse(); + ConsumerGroupInfo consumerGroupInfo = brokerController.getConsumerManager().getConsumerGroupInfo(group); + + RemotingCommand requestSimple = createHeartbeatCommand(true, "topicA"); + RemotingCommand responseSimple = clientManageProcessor.processRequest(handlerContext, requestSimple); + assertThat(responseSimple.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(Boolean.parseBoolean(responseSimple.getExtFields().get(MixAll.IS_SUB_CHANGE))).isFalse(); + ConsumerGroupInfo consumerGroupInfoSimple = brokerController.getConsumerManager().getConsumerGroupInfo(group); + assertThat(consumerGroupInfoSimple).isEqualTo(consumerGroupInfo); + + request = createHeartbeatCommand(false, "topicB"); + response = clientManageProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(Boolean.parseBoolean(response.getExtFields().get(MixAll.IS_SUB_CHANGE))).isTrue(); + consumerGroupInfo = brokerController.getConsumerManager().getConsumerGroupInfo(group); + + requestSimple = createHeartbeatCommand(true, "topicB"); + responseSimple = clientManageProcessor.processRequest(handlerContext, requestSimple); + assertThat(responseSimple.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(Boolean.parseBoolean(responseSimple.getExtFields().get(MixAll.IS_SUB_CHANGE))).isFalse(); + consumerGroupInfoSimple = brokerController.getConsumerManager().getConsumerGroupInfo(group); + assertThat(consumerGroupInfoSimple).isEqualTo(consumerGroupInfo); + } + + @Test + public void test_heartbeat_costTime() { + String topic = "TOPIC_TEST"; + List topicList = new ArrayList<>(); + for (int i = 0; i < 500; i ++) { + topicList.add(topic + i); + } + HeartbeatData heartbeatData = prepareHeartbeatData(false, topicList); + long time = System.currentTimeMillis(); + heartbeatData.computeHeartbeatFingerprint(); + System.out.print("computeHeartbeatFingerprint cost time : " + (System.currentTimeMillis() - time) + " ms \n"); + } + private RemotingCommand createUnRegisterProducerCommand() { UnregisterClientRequestHeader requestHeader = new UnregisterClientRequestHeader(); requestHeader.setClientID(clientId); @@ -129,4 +180,50 @@ private RemotingCommand createUnRegisterConsumerCommand() { request.makeCustomHeaderToNet(); return request; } -} \ No newline at end of file + + private RemotingCommand createHeartbeatCommand(boolean isWithoutSub, String topic) { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.HEART_BEAT, null); + request.setLanguage(LanguageCode.JAVA); + HeartbeatData heartbeatDataWithSub = prepareHeartbeatData(false, topic); + int heartbeatFingerprint = heartbeatDataWithSub.computeHeartbeatFingerprint(); + HeartbeatData heartbeatData = prepareHeartbeatData(isWithoutSub, topic); + heartbeatData.setHeartbeatFingerprint(heartbeatFingerprint); + request.setBody(heartbeatData.encode()); + return request; + } + + private HeartbeatData prepareHeartbeatData(boolean isWithoutSub, String topic) { + List list = new ArrayList<>(); + list.add(topic); + return prepareHeartbeatData(isWithoutSub, list); + } + + private HeartbeatData prepareHeartbeatData(boolean isWithoutSub, List topicList) { + HeartbeatData heartbeatData = new HeartbeatData(); + heartbeatData.setClientID(this.clientId); + ConsumerData consumerData = createConsumerData(group); + if (!isWithoutSub) { + Set subscriptionDataSet = new HashSet<>(); + for (String topic : topicList) { + SubscriptionData subscriptionData = new SubscriptionData(); + subscriptionData.setTopic(topic); + subscriptionData.setSubString("*"); + subscriptionData.setSubVersion(100L); + subscriptionDataSet.add(subscriptionData); + } + consumerData.getSubscriptionDataSet().addAll(subscriptionDataSet); + } + heartbeatData.getConsumerDataSet().add(consumerData); + heartbeatData.setWithoutSub(isWithoutSub); + return heartbeatData; + } + + static ConsumerData createConsumerData(String group) { + ConsumerData consumerData = new ConsumerData(); + consumerData.setGroupName(group); + consumerData.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + consumerData.setConsumeType(ConsumeType.CONSUME_PASSIVELY); + consumerData.setMessageModel(MessageModel.CLUSTERING); + return consumerData; + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/processor/ConsumerManageProcessorTest.java b/broker/src/test/java/org/apache/rocketmq/broker/processor/ConsumerManageProcessorTest.java new file mode 100644 index 00000000000..dd7584b5276 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/ConsumerManageProcessorTest.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.processor; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.topic.TopicConfigManager; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ConsumerManageProcessorTest { + private ConsumerManageProcessor consumerManageProcessor; + @Mock + private ChannelHandlerContext handlerContext; + @Spy + private BrokerController brokerController = new BrokerController(new BrokerConfig(), new NettyServerConfig(), new NettyClientConfig(), new MessageStoreConfig()); + @Mock + private MessageStore messageStore; + + private String topic = "FooBar"; + private String group = "FooBarGroup"; + + @Before + public void init() { + brokerController.setMessageStore(messageStore); + TopicConfigManager topicConfigManager = new TopicConfigManager(brokerController); + topicConfigManager.getTopicConfigTable().put(topic, new TopicConfig(topic)); + when(brokerController.getTopicConfigManager()).thenReturn(topicConfigManager); + consumerManageProcessor = new ConsumerManageProcessor(brokerController); + } + + @Test + public void testUpdateConsumerOffset_InvalidTopic() throws Exception { + RemotingCommand request = createConsumerManageCommand(RequestCode.UPDATE_CONSUMER_OFFSET); + request.addExtField("topic", "InvalidTopic"); + RemotingCommand response = consumerManageProcessor.processRequest(handlerContext, request); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.TOPIC_NOT_EXIST); + } + + private RemotingCommand createConsumerManageCommand(int requestCode) { + SendMessageRequestHeader requestHeader = new SendMessageRequestHeader(); + requestHeader.setProducerGroup(group); + requestHeader.setTopic(topic); + requestHeader.setDefaultTopic(TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC); + requestHeader.setDefaultTopicQueueNums(3); + requestHeader.setQueueId(1); + requestHeader.setSysFlag(0); + requestHeader.setBornTimestamp(System.currentTimeMillis()); + requestHeader.setFlag(124); + requestHeader.setReconsumeTimes(0); + + RemotingCommand request = RemotingCommand.createRequestCommand(requestCode, requestHeader); + request.setBody(new byte[] {'a'}); + request.makeCustomHeaderToNet(); + return request; + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/processor/EndTransactionProcessorTest.java b/broker/src/test/java/org/apache/rocketmq/broker/processor/EndTransactionProcessorTest.java new file mode 100644 index 00000000000..a364a1bbeeb --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/EndTransactionProcessorTest.java @@ -0,0 +1,198 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.processor; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.transaction.OperationResult; +import org.apache.rocketmq.broker.transaction.TransactionMetrics; +import org.apache.rocketmq.broker.transaction.TransactionalMessageService; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.EndTransactionRequestHeader; +import org.apache.rocketmq.store.AppendMessageResult; +import org.apache.rocketmq.store.AppendMessageStatus; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class EndTransactionProcessorTest { + + private EndTransactionProcessor endTransactionProcessor; + + @Mock + private ChannelHandlerContext handlerContext; + + @Spy + private BrokerController + brokerController = new BrokerController(new BrokerConfig(), new NettyServerConfig(), new NettyClientConfig(), + new MessageStoreConfig()); + + @Mock + private MessageStore messageStore; + + @Mock + private TransactionalMessageService transactionMsgService; + + @Mock + private TransactionMetrics transactionMetrics; + + @Before + public void init() { + when(transactionMsgService.getTransactionMetrics()).thenReturn(transactionMetrics); + brokerController.setMessageStore(messageStore); + brokerController.setTransactionalMessageService(transactionMsgService); + endTransactionProcessor = new EndTransactionProcessor(brokerController); + } + + private OperationResult createResponse(int status) { + OperationResult response = new OperationResult(); + response.setPrepareMessage(createDefaultMessageExt()); + response.setResponseCode(status); + response.setResponseRemark(null); + return response; + } + + @Test + public void testProcessRequest() throws RemotingCommandException { + when(transactionMsgService.commitMessage(any(EndTransactionRequestHeader.class))).thenReturn(createResponse(ResponseCode.SUCCESS)); + when(messageStore.putMessage(any(MessageExtBrokerInner.class))) + .thenReturn(new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK))); + RemotingCommand request = createEndTransactionMsgCommand(MessageSysFlag.TRANSACTION_COMMIT_TYPE, false); + RemotingCommand response = endTransactionProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testProcessRequest_CheckMessage() throws RemotingCommandException { + when(transactionMsgService.commitMessage(any(EndTransactionRequestHeader.class))).thenReturn(createResponse(ResponseCode.SUCCESS)); + when(messageStore.putMessage(any(MessageExtBrokerInner.class))) + .thenReturn(new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK))); + RemotingCommand request = createEndTransactionMsgCommand(MessageSysFlag.TRANSACTION_COMMIT_TYPE, true); + RemotingCommand response = endTransactionProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testProcessRequest_NotType() throws RemotingCommandException { + RemotingCommand request = createEndTransactionMsgCommand(MessageSysFlag.TRANSACTION_NOT_TYPE, true); + RemotingCommand response = endTransactionProcessor.processRequest(handlerContext, request); + assertThat(response).isNull(); + } + + @Test + public void testProcessRequest_RollBack() throws RemotingCommandException { + when(transactionMsgService.rollbackMessage(any(EndTransactionRequestHeader.class))).thenReturn(createResponse(ResponseCode.SUCCESS)); + RemotingCommand request = createEndTransactionMsgCommand(MessageSysFlag.TRANSACTION_ROLLBACK_TYPE, true); + RemotingCommand response = endTransactionProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testProcessRequest_RejectCommitMessage() throws RemotingCommandException { + when(transactionMsgService.commitMessage(any(EndTransactionRequestHeader.class))).thenReturn(createRejectResponse()); + RemotingCommand request = createEndTransactionMsgCommand(MessageSysFlag.TRANSACTION_COMMIT_TYPE, false); + RemotingCommand response = endTransactionProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.ILLEGAL_OPERATION); + } + + @Test + public void testProcessRequest_RejectRollBackMessage() throws RemotingCommandException { + when(transactionMsgService.rollbackMessage(any(EndTransactionRequestHeader.class))).thenReturn(createRejectResponse()); + RemotingCommand request = createEndTransactionMsgCommand(MessageSysFlag.TRANSACTION_ROLLBACK_TYPE, false); + RemotingCommand response = endTransactionProcessor.processRequest(handlerContext, request); + assertThat(response.getCode()).isEqualTo(ResponseCode.ILLEGAL_OPERATION); + } + + private MessageExt createDefaultMessageExt() { + MessageExt messageExt = new MessageExt(); + messageExt.setMsgId("12345678"); + messageExt.setQueueId(0); + messageExt.setCommitLogOffset(123456789L); + messageExt.setQueueOffset(1234); + MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_REAL_QUEUE_ID, "0"); + MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true"); + MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_PRODUCER_GROUP, "testTransactionGroup"); + return messageExt; + } + + private EndTransactionRequestHeader createEndTransactionRequestHeader(int status, boolean isCheckMsg) { + EndTransactionRequestHeader header = new EndTransactionRequestHeader(); + header.setCommitLogOffset(123456789L); + header.setFromTransactionCheck(isCheckMsg); + header.setCommitOrRollback(status); + header.setMsgId("12345678"); + header.setTransactionId("123"); + header.setProducerGroup("testTransactionGroup"); + header.setTranStateTableOffset(1234L); + return header; + } + + private RemotingCommand createEndTransactionMsgCommand(int status, boolean isCheckMsg) { + EndTransactionRequestHeader header = createEndTransactionRequestHeader(status, isCheckMsg); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.END_TRANSACTION, header); + request.makeCustomHeaderToNet(); + return request; + } + + private OperationResult createRejectResponse() { + OperationResult response = new OperationResult(); + response.setPrepareMessage(createRejectMessageExt()); + response.setResponseCode(ResponseCode.SUCCESS); + response.setResponseRemark(null); + return response; + } + private MessageExt createRejectMessageExt() { + MessageExt messageExt = new MessageExt(); + messageExt.setMsgId("12345678"); + messageExt.setQueueId(0); + messageExt.setCommitLogOffset(123456789L); + messageExt.setQueueOffset(1234); + messageExt.setBody("body".getBytes(StandardCharsets.UTF_8)); + messageExt.setBornTimestamp(System.currentTimeMillis() - 65 * 1000); + MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_REAL_QUEUE_ID, "0"); + MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true"); + MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_PRODUCER_GROUP, "testTransactionGroup"); + MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_REAL_TOPIC, "TEST"); + MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS, "60"); + return messageExt; + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/processor/PopBufferMergeServiceTest.java b/broker/src/test/java/org/apache/rocketmq/broker/processor/PopBufferMergeServiceTest.java new file mode 100644 index 00000000000..acc7a3da74a --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/PopBufferMergeServiceTest.java @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.processor; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.client.ClientChannelInfo; +import org.apache.rocketmq.broker.schedule.ScheduleMessageService; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumerData; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.pop.AckMsg; +import org.apache.rocketmq.store.pop.PopCheckPoint; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.apache.rocketmq.broker.processor.PullMessageProcessorTest.createConsumerData; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +@RunWith(MockitoJUnitRunner.Silent.class) +public class PopBufferMergeServiceTest { + @Spy + private BrokerController brokerController = new BrokerController(new BrokerConfig(), new NettyServerConfig(), new NettyClientConfig(), new MessageStoreConfig()); + @Mock + private PopMessageProcessor popMessageProcessor; + @Mock + private ChannelHandlerContext handlerContext; + @Mock + private DefaultMessageStore messageStore; + private ScheduleMessageService scheduleMessageService; + private ClientChannelInfo clientChannelInfo; + private String group = "FooBarGroup"; + private String topic = "FooBar"; + + @Before + public void init() throws Exception { + FieldUtils.writeField(brokerController.getBrokerConfig(), "enablePopBufferMerge", true, true); + brokerController.setMessageStore(messageStore); + popMessageProcessor = new PopMessageProcessor(brokerController); + scheduleMessageService = new ScheduleMessageService(brokerController); + scheduleMessageService.parseDelayLevel(); + Channel mockChannel = mock(Channel.class); + brokerController.getTopicConfigManager().getTopicConfigTable().put(topic, new TopicConfig()); + clientChannelInfo = new ClientChannelInfo(mockChannel); + ConsumerData consumerData = createConsumerData(group, topic); + brokerController.getConsumerManager().registerConsumer( + consumerData.getGroupName(), + clientChannelInfo, + consumerData.getConsumeType(), + consumerData.getMessageModel(), + consumerData.getConsumeFromWhere(), + consumerData.getSubscriptionDataSet(), + false); + } + + @Test(timeout = 10_000) + public void testBasic() throws Exception { + // This test case fails on Windows in CI pipeline + // Disable it for later fix + Assume.assumeFalse(MixAll.isWindows()); + PopBufferMergeService popBufferMergeService = new PopBufferMergeService(brokerController, popMessageProcessor); + popBufferMergeService.start(); + PopCheckPoint ck = new PopCheckPoint(); + ck.setBitMap(0); + int msgCnt = 1; + ck.setNum((byte) msgCnt); + long popTime = System.currentTimeMillis() - 1000; + ck.setPopTime(popTime); + int invisibleTime = 30_000; + ck.setInvisibleTime(invisibleTime); + int offset = 100; + ck.setStartOffset(offset); + ck.setCId(group); + ck.setTopic(topic); + int queueId = 0; + ck.setQueueId(queueId); + + int reviveQid = 0; + long nextBeginOffset = 101L; + long ackOffset = offset; + AckMsg ackMsg = new AckMsg(); + ackMsg.setAckOffset(ackOffset); + ackMsg.setStartOffset(offset); + ackMsg.setConsumerGroup(group); + ackMsg.setTopic(topic); + ackMsg.setQueueId(queueId); + ackMsg.setPopTime(popTime); + try { + assertThat(popBufferMergeService.addCk(ck, reviveQid, ackOffset, nextBeginOffset)).isTrue(); + assertThat(popBufferMergeService.getLatestOffset(topic, group, queueId)).isEqualTo(nextBeginOffset); + Thread.sleep(1000); // wait background threads of PopBufferMergeService run for some time + assertThat(popBufferMergeService.addAk(reviveQid, ackMsg)).isTrue(); + assertThat(popBufferMergeService.getLatestOffset(topic, group, queueId)).isEqualTo(nextBeginOffset); + } finally { + popBufferMergeService.shutdown(true); + } + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/processor/PopInflightMessageCounterTest.java b/broker/src/test/java/org/apache/rocketmq/broker/processor/PopInflightMessageCounterTest.java new file mode 100644 index 00000000000..dea59fc99e6 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/PopInflightMessageCounterTest.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.processor; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.store.pop.PopCheckPoint; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class PopInflightMessageCounterTest { + + @Test + public void testNum() { + BrokerController brokerController = mock(BrokerController.class); + long brokerStartTime = System.currentTimeMillis(); + when(brokerController.getShouldStartTime()).thenReturn(brokerStartTime); + PopInflightMessageCounter counter = new PopInflightMessageCounter(brokerController); + + final String topic = "topic"; + final String group = "group"; + + assertEquals(0, counter.getGroupPopInFlightMessageNum(topic, group, 0)); + + counter.incrementInFlightMessageNum(topic, group, 0, 3); + assertEquals(3, counter.getGroupPopInFlightMessageNum(topic, group, 0)); + + counter.decrementInFlightMessageNum(topic, group, System.currentTimeMillis(), 0, 1); + assertEquals(2, counter.getGroupPopInFlightMessageNum(topic, group, 0)); + + counter.decrementInFlightMessageNum(topic, group, System.currentTimeMillis() - 1000, 0, 1); + assertEquals(2, counter.getGroupPopInFlightMessageNum(topic, group, 0)); + + PopCheckPoint popCheckPoint = new PopCheckPoint(); + popCheckPoint.setTopic(topic); + popCheckPoint.setCId(group); + popCheckPoint.setQueueId(0); + popCheckPoint.setPopTime(System.currentTimeMillis()); + + counter.decrementInFlightMessageNum(popCheckPoint); + assertEquals(1, counter.getGroupPopInFlightMessageNum(topic, group, 0)); + + counter.decrementInFlightMessageNum(topic, group, System.currentTimeMillis(), 0 ,1); + assertEquals(0, counter.getGroupPopInFlightMessageNum(topic, group, 0)); + + counter.decrementInFlightMessageNum(topic, group, System.currentTimeMillis(), 0, 1); + assertEquals(0, counter.getGroupPopInFlightMessageNum(topic, group, 0)); + } + + @Test + public void testClearInFlightMessageNum() { + BrokerController brokerController = mock(BrokerController.class); + long brokerStartTime = System.currentTimeMillis(); + when(brokerController.getShouldStartTime()).thenReturn(brokerStartTime); + PopInflightMessageCounter counter = new PopInflightMessageCounter(brokerController); + + final String topic = "topic"; + final String group = "group"; + + assertEquals(0, counter.getGroupPopInFlightMessageNum(topic, group, 0)); + + counter.incrementInFlightMessageNum(topic, group, 0, 3); + assertEquals(3, counter.getGroupPopInFlightMessageNum(topic, group, 0)); + + counter.clearInFlightMessageNumByTopicName("errorTopic"); + assertEquals(3, counter.getGroupPopInFlightMessageNum(topic, group, 0)); + + counter.clearInFlightMessageNumByTopicName(topic); + assertEquals(0, counter.getGroupPopInFlightMessageNum(topic, group, 0)); + + counter.incrementInFlightMessageNum(topic, group, 0, 3); + assertEquals(3, counter.getGroupPopInFlightMessageNum(topic, group, 0)); + + counter.clearInFlightMessageNumByGroupName("errorGroup"); + assertEquals(3, counter.getGroupPopInFlightMessageNum(topic, group, 0)); + + counter.clearInFlightMessageNumByGroupName(group); + assertEquals(0, counter.getGroupPopInFlightMessageNum(topic, group, 0)); + } +} \ No newline at end of file diff --git a/broker/src/test/java/org/apache/rocketmq/broker/processor/PopMessageProcessorTest.java b/broker/src/test/java/org/apache/rocketmq/broker/processor/PopMessageProcessorTest.java new file mode 100644 index 00000000000..d8c8fa1034e --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/PopMessageProcessorTest.java @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.processor; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.embedded.EmbeddedChannel; +import java.nio.ByteBuffer; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.client.ClientChannelInfo; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.constant.ConsumeInitMode; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.PopMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumerData; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.GetMessageStatus; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.logfile.DefaultMappedFile; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.apache.rocketmq.broker.processor.PullMessageProcessorTest.createConsumerData; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class PopMessageProcessorTest { + private PopMessageProcessor popMessageProcessor; + + @Spy + private BrokerController brokerController = new BrokerController(new BrokerConfig(), new NettyServerConfig(), new NettyClientConfig(), new MessageStoreConfig()); + @Mock + private ChannelHandlerContext handlerContext; + private final EmbeddedChannel embeddedChannel = new EmbeddedChannel(); + @Mock + private DefaultMessageStore messageStore; + private ClientChannelInfo clientChannelInfo; + private String group = "FooBarGroup"; + private String topic = "FooBar"; + + @Before + public void init() { + brokerController.setMessageStore(messageStore); + brokerController.getBrokerConfig().setEnablePopBufferMerge(true); + popMessageProcessor = new PopMessageProcessor(brokerController); + when(handlerContext.channel()).thenReturn(embeddedChannel); + brokerController.getTopicConfigManager().getTopicConfigTable().put(topic, new TopicConfig(topic)); + clientChannelInfo = new ClientChannelInfo(embeddedChannel); + ConsumerData consumerData = createConsumerData(group, topic); + brokerController.getConsumerManager().registerConsumer( + consumerData.getGroupName(), + clientChannelInfo, + consumerData.getConsumeType(), + consumerData.getMessageModel(), + consumerData.getConsumeFromWhere(), + consumerData.getSubscriptionDataSet(), + false); + } + + @Test + public void testProcessRequest_TopicNotExist() throws RemotingCommandException { + when(messageStore.getMessageStoreConfig()).thenReturn(new MessageStoreConfig()); + brokerController.getTopicConfigManager().getTopicConfigTable().remove(topic); + final RemotingCommand request = createPopMsgCommand(); + RemotingCommand response = popMessageProcessor.processRequest(handlerContext, request); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.TOPIC_NOT_EXIST); + assertThat(response.getRemark()).contains("topic[" + topic + "] not exist"); + } + + @Test + public void testProcessRequest_Found() throws RemotingCommandException, InterruptedException { + GetMessageResult getMessageResult = createGetMessageResult(1); + when(messageStore.getMessageStoreConfig()).thenReturn(new MessageStoreConfig()); + when(messageStore.getMessageAsync(anyString(), anyString(), anyInt(), anyLong(), anyInt(), any())).thenReturn(CompletableFuture.completedFuture(getMessageResult)); + + final RemotingCommand request = createPopMsgCommand(); + popMessageProcessor.processRequest(handlerContext, request); + RemotingCommand response = embeddedChannel.readOutbound(); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testProcessRequest_MsgWasRemoving() throws RemotingCommandException { + GetMessageResult getMessageResult = createGetMessageResult(1); + getMessageResult.setStatus(GetMessageStatus.MESSAGE_WAS_REMOVING); + when(messageStore.getMessageStoreConfig()).thenReturn(new MessageStoreConfig()); + when(messageStore.getMessageAsync(anyString(), anyString(), anyInt(), anyLong(), anyInt(), any())).thenReturn(CompletableFuture.completedFuture(getMessageResult)); + + final RemotingCommand request = createPopMsgCommand(); + popMessageProcessor.processRequest(handlerContext, request); + RemotingCommand response = embeddedChannel.readOutbound(); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testProcessRequest_NoMsgInQueue() throws RemotingCommandException { + GetMessageResult getMessageResult = createGetMessageResult(0); + getMessageResult.setStatus(GetMessageStatus.NO_MESSAGE_IN_QUEUE); + when(messageStore.getMessageStoreConfig()).thenReturn(new MessageStoreConfig()); + when(messageStore.getMessageAsync(anyString(), anyString(), anyInt(), anyLong(), anyInt(), any())).thenReturn(CompletableFuture.completedFuture(getMessageResult)); + + final RemotingCommand request = createPopMsgCommand(); + RemotingCommand response = popMessageProcessor.processRequest(handlerContext, request); + assertThat(response).isNull(); + } + + @Test + public void testProcessRequest_whenTimerWheelIsFalse() throws RemotingCommandException { + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setTimerWheelEnable(false); + when(messageStore.getMessageStoreConfig()).thenReturn(messageStoreConfig); + final RemotingCommand request = createPopMsgCommand(); + RemotingCommand response = popMessageProcessor.processRequest(handlerContext, request); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.SYSTEM_ERROR); + assertThat(response.getRemark()).contains("pop message is forbidden because timerWheelEnable is false"); + } + + private RemotingCommand createPopMsgCommand() { + PopMessageRequestHeader requestHeader = new PopMessageRequestHeader(); + requestHeader.setConsumerGroup(group); + requestHeader.setMaxMsgNums(30); + requestHeader.setQueueId(-1); + requestHeader.setTopic(topic); + requestHeader.setInvisibleTime(10_000); + requestHeader.setInitMode(ConsumeInitMode.MAX); + requestHeader.setOrder(false); + requestHeader.setPollTime(15_000); + requestHeader.setBornTime(System.currentTimeMillis()); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.POP_MESSAGE, requestHeader); + request.makeCustomHeaderToNet(); + return request; + } + + private GetMessageResult createGetMessageResult(int msgCnt) { + GetMessageResult getMessageResult = new GetMessageResult(); + getMessageResult.setStatus(GetMessageStatus.FOUND); + getMessageResult.setMinOffset(100); + getMessageResult.setMaxOffset(1024); + getMessageResult.setNextBeginOffset(516); + for (int i = 0; i < msgCnt; i++) { + ByteBuffer bb = ByteBuffer.allocate(64); + bb.putLong(MessageDecoder.MESSAGE_STORE_TIMESTAMP_POSITION, System.currentTimeMillis()); + getMessageResult.addMessage(new SelectMappedBufferResult(200, bb, 64, new DefaultMappedFile())); + } + return getMessageResult; + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/processor/PopReviveServiceTest.java b/broker/src/test/java/org/apache/rocketmq/broker/processor/PopReviveServiceTest.java new file mode 100644 index 00000000000..78b76264fef --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/PopReviveServiceTest.java @@ -0,0 +1,284 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.processor; + +import com.alibaba.fastjson.JSON; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.offset.ConsumerOffsetManager; +import org.apache.rocketmq.broker.subscription.SubscriptionGroupManager; +import org.apache.rocketmq.broker.topic.TopicConfigManager; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.PopAckConstants; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.utils.DataConverter; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.pop.AckMsg; +import org.apache.rocketmq.store.pop.PopCheckPoint; +import org.apache.rocketmq.store.timer.TimerMessageStore; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.Silent.class) +public class PopReviveServiceTest { + + private static final String REVIVE_TOPIC = PopAckConstants.REVIVE_TOPIC + "test"; + private static final int REVIVE_QUEUE_ID = 0; + private static final String GROUP = "group"; + private static final String TOPIC = "topic"; + private static final SocketAddress STORE_HOST = NetworkUtil.string2SocketAddress("127.0.0.1:8080"); + + @Mock + private MessageStore messageStore; + @Mock + private ConsumerOffsetManager consumerOffsetManager; + @Mock + private TopicConfigManager topicConfigManager; + @Mock + private TimerMessageStore timerMessageStore; + @Mock + private SubscriptionGroupManager subscriptionGroupManager; + @Mock + private BrokerController brokerController; + + private BrokerConfig brokerConfig; + private PopReviveService popReviveService; + + @Before + public void before() { + brokerConfig = new BrokerConfig(); + + when(brokerController.getBrokerConfig()).thenReturn(brokerConfig); + when(brokerController.getConsumerOffsetManager()).thenReturn(consumerOffsetManager); + when(brokerController.getMessageStore()).thenReturn(messageStore); + when(brokerController.getTopicConfigManager()).thenReturn(topicConfigManager); + when(brokerController.getSubscriptionGroupManager()).thenReturn(subscriptionGroupManager); + when(messageStore.getTimerMessageStore()).thenReturn(timerMessageStore); + when(timerMessageStore.getDequeueBehind()).thenReturn(0L); + when(timerMessageStore.getEnqueueBehind()).thenReturn(0L); + + when(topicConfigManager.selectTopicConfig(anyString())).thenReturn(new TopicConfig()); + when(subscriptionGroupManager.findSubscriptionGroupConfig(anyString())).thenReturn(new SubscriptionGroupConfig()); + + popReviveService = spy(new PopReviveService(brokerController, REVIVE_TOPIC, REVIVE_QUEUE_ID)); + popReviveService.setShouldRunPopRevive(true); + } + + @Test + public void testWhenAckMoreThanCk() throws Throwable { + brokerConfig.setEnableSkipLongAwaitingAck(true); + long maxReviveOffset = 4; + + when(consumerOffsetManager.queryOffset(PopAckConstants.REVIVE_GROUP, REVIVE_TOPIC, REVIVE_QUEUE_ID)) + .thenReturn(0L); + List reviveMessageExtList = new ArrayList<>(); + long basePopTime = System.currentTimeMillis(); + { + // put a pair of ck and ack + PopCheckPoint ck = buildPopCheckPoint(1, basePopTime, 1); + reviveMessageExtList.add(buildCkMsg(ck)); + reviveMessageExtList.add(buildAckMsg(buildAckMsg(1, basePopTime), ck.getReviveTime(), 1, basePopTime)); + } + { + for (int i = 2; i <= maxReviveOffset; i++) { + long popTime = basePopTime + i; + PopCheckPoint ck = buildPopCheckPoint(i, popTime, i); + reviveMessageExtList.add(buildAckMsg(buildAckMsg(i, popTime), ck.getReviveTime(), i, popTime)); + } + } + doReturn(reviveMessageExtList, new ArrayList<>()).when(popReviveService).getReviveMessage(anyLong(), anyInt()); + + PopReviveService.ConsumeReviveObj consumeReviveObj = new PopReviveService.ConsumeReviveObj(); + popReviveService.consumeReviveMessage(consumeReviveObj); + + assertEquals(1, consumeReviveObj.map.size()); + + ArgumentCaptor commitOffsetCaptor = ArgumentCaptor.forClass(Long.class); + doNothing().when(consumerOffsetManager).commitOffset(anyString(), anyString(), anyString(), anyInt(), commitOffsetCaptor.capture()); + popReviveService.mergeAndRevive(consumeReviveObj); + + assertEquals(1, commitOffsetCaptor.getValue().longValue()); + } + + @Test + public void testSkipLongWaiteAck() throws Throwable { + brokerConfig.setEnableSkipLongAwaitingAck(true); + brokerConfig.setReviveAckWaitMs(TimeUnit.SECONDS.toMillis(2)); + long maxReviveOffset = 4; + + when(consumerOffsetManager.queryOffset(PopAckConstants.REVIVE_GROUP, REVIVE_TOPIC, REVIVE_QUEUE_ID)) + .thenReturn(0L); + List reviveMessageExtList = new ArrayList<>(); + long basePopTime = System.currentTimeMillis() - brokerConfig.getReviveAckWaitMs() * 2; + { + // put a pair of ck and ack + PopCheckPoint ck = buildPopCheckPoint(1, basePopTime, 1); + reviveMessageExtList.add(buildCkMsg(ck)); + reviveMessageExtList.add(buildAckMsg(buildAckMsg(1, basePopTime), ck.getReviveTime(), 1, basePopTime)); + } + { + for (int i = 2; i <= maxReviveOffset; i++) { + long popTime = basePopTime + i; + PopCheckPoint ck = buildPopCheckPoint(i, popTime, i); + reviveMessageExtList.add(buildAckMsg(buildAckMsg(i, popTime), ck.getReviveTime(), i, popTime)); + } + } + doReturn(reviveMessageExtList, new ArrayList<>()).when(popReviveService).getReviveMessage(anyLong(), anyInt()); + + PopReviveService.ConsumeReviveObj consumeReviveObj = new PopReviveService.ConsumeReviveObj(); + popReviveService.consumeReviveMessage(consumeReviveObj); + + assertEquals(4, consumeReviveObj.map.size()); + + ArgumentCaptor commitOffsetCaptor = ArgumentCaptor.forClass(Long.class); + doNothing().when(consumerOffsetManager).commitOffset(anyString(), anyString(), anyString(), anyInt(), commitOffsetCaptor.capture()); + popReviveService.mergeAndRevive(consumeReviveObj); + + assertEquals(maxReviveOffset, commitOffsetCaptor.getValue().longValue()); + } + + @Test + public void testSkipLongWaiteAckWithSameAck() throws Throwable { + brokerConfig.setEnableSkipLongAwaitingAck(true); + brokerConfig.setReviveAckWaitMs(TimeUnit.SECONDS.toMillis(2)); + long maxReviveOffset = 4; + + when(consumerOffsetManager.queryOffset(PopAckConstants.REVIVE_GROUP, REVIVE_TOPIC, REVIVE_QUEUE_ID)) + .thenReturn(0L); + List reviveMessageExtList = new ArrayList<>(); + long basePopTime = System.currentTimeMillis() - brokerConfig.getReviveAckWaitMs() * 2; + { + for (int i = 2; i <= maxReviveOffset; i++) { + long popTime = basePopTime + i; + PopCheckPoint ck = buildPopCheckPoint(0, basePopTime, i); + reviveMessageExtList.add(buildAckMsg(buildAckMsg(0, basePopTime), ck.getReviveTime(), i, popTime)); + } + } + doReturn(reviveMessageExtList, new ArrayList<>()).when(popReviveService).getReviveMessage(anyLong(), anyInt()); + + PopReviveService.ConsumeReviveObj consumeReviveObj = new PopReviveService.ConsumeReviveObj(); + popReviveService.consumeReviveMessage(consumeReviveObj); + + assertEquals(1, consumeReviveObj.map.size()); + + ArgumentCaptor commitOffsetCaptor = ArgumentCaptor.forClass(Long.class); + doNothing().when(consumerOffsetManager).commitOffset(anyString(), anyString(), anyString(), anyInt(), commitOffsetCaptor.capture()); + popReviveService.mergeAndRevive(consumeReviveObj); + + assertEquals(maxReviveOffset, commitOffsetCaptor.getValue().longValue()); + } + + public static PopCheckPoint buildPopCheckPoint(long startOffset, long popTime, long reviveOffset) { + PopCheckPoint ck = new PopCheckPoint(); + ck.setStartOffset(startOffset); + ck.setPopTime(popTime); + ck.setQueueId(0); + ck.setCId(GROUP); + ck.setTopic(TOPIC); + ck.setNum((byte) 1); + ck.setBitMap(0); + ck.setReviveOffset(reviveOffset); + ck.setInvisibleTime(1000); + return ck; + } + + public static AckMsg buildAckMsg(long offset, long popTime) { + AckMsg ackMsg = new AckMsg(); + ackMsg.setAckOffset(offset); + ackMsg.setStartOffset(offset); + ackMsg.setConsumerGroup(GROUP); + ackMsg.setTopic(TOPIC); + ackMsg.setQueueId(0); + ackMsg.setPopTime(popTime); + + return ackMsg; + } + + public static MessageExtBrokerInner buildCkMsg(PopCheckPoint ck) { + MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); + + msgInner.setTopic(REVIVE_TOPIC); + msgInner.setBody(JSON.toJSONString(ck).getBytes(DataConverter.CHARSET_UTF8)); + msgInner.setQueueId(REVIVE_QUEUE_ID); + msgInner.setTags(PopAckConstants.CK_TAG); + msgInner.setBornTimestamp(System.currentTimeMillis()); + msgInner.setBornHost(STORE_HOST); + msgInner.setStoreHost(STORE_HOST); + msgInner.setDeliverTimeMs(ck.getReviveTime() - PopAckConstants.ackTimeInterval); + msgInner.getProperties().put(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, PopMessageProcessor.genCkUniqueId(ck)); + msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); + + msgInner.setQueueOffset(ck.getReviveOffset()); + + return msgInner; + } + + public static MessageExtBrokerInner buildAckMsg(AckMsg ackMsg, long deliverMs, long reviveOffset, + long deliverTime) { + MessageExtBrokerInner messageExtBrokerInner = buildAckInnerMessage( + REVIVE_TOPIC, + ackMsg, + REVIVE_QUEUE_ID, + STORE_HOST, + deliverMs, + PopMessageProcessor.genAckUniqueId(ackMsg) + ); + messageExtBrokerInner.setQueueOffset(reviveOffset); + messageExtBrokerInner.setDeliverTimeMs(deliverMs); + messageExtBrokerInner.setStoreTimestamp(deliverTime); + return messageExtBrokerInner; + } + + public static MessageExtBrokerInner buildAckInnerMessage(String reviveTopic, AckMsg ackMsg, int reviveQid, + SocketAddress host, long deliverMs, String ackUniqueId) { + MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); + msgInner.setTopic(reviveTopic); + msgInner.setBody(JSON.toJSONString(ackMsg).getBytes(DataConverter.CHARSET_UTF8)); + msgInner.setQueueId(reviveQid); + msgInner.setTags(PopAckConstants.ACK_TAG); + msgInner.setBornTimestamp(System.currentTimeMillis()); + msgInner.setBornHost(host); + msgInner.setStoreHost(host); + msgInner.setDeliverTimeMs(deliverMs); + msgInner.getProperties().put(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, ackUniqueId); + msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); + + return msgInner; + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/processor/PullMessageProcessorTest.java b/broker/src/test/java/org/apache/rocketmq/broker/processor/PullMessageProcessorTest.java index c96f708e854..83c30111854 100644 --- a/broker/src/test/java/org/apache/rocketmq/broker/processor/PullMessageProcessorTest.java +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/PullMessageProcessorTest.java @@ -16,36 +16,40 @@ */ package org.apache.rocketmq.broker.processor; -import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; -import java.net.InetSocketAddress; +import io.netty.channel.embedded.EmbeddedChannel; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.CompletableFuture; import org.apache.rocketmq.broker.BrokerController; import org.apache.rocketmq.broker.client.ClientChannelInfo; +import org.apache.rocketmq.broker.client.ConsumerGroupInfo; import org.apache.rocketmq.broker.filter.ExpressionMessageFilter; import org.apache.rocketmq.broker.mqtrace.ConsumeMessageContext; import org.apache.rocketmq.broker.mqtrace.ConsumeMessageHook; +import org.apache.rocketmq.broker.subscription.SubscriptionGroupManager; import org.apache.rocketmq.common.BrokerConfig; import org.apache.rocketmq.common.TopicConfig; import org.apache.rocketmq.common.consumer.ConsumeFromWhere; -import org.apache.rocketmq.common.protocol.RequestCode; -import org.apache.rocketmq.common.protocol.ResponseCode; -import org.apache.rocketmq.common.protocol.header.PullMessageRequestHeader; -import org.apache.rocketmq.common.protocol.heartbeat.ConsumeType; -import org.apache.rocketmq.common.protocol.heartbeat.ConsumerData; -import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; import org.apache.rocketmq.remoting.exception.RemotingCommandException; import org.apache.rocketmq.remoting.netty.NettyClientConfig; import org.apache.rocketmq.remoting.netty.NettyServerConfig; import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumerData; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; import org.apache.rocketmq.store.GetMessageResult; import org.apache.rocketmq.store.GetMessageStatus; import org.apache.rocketmq.store.MessageStore; import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -58,16 +62,17 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class PullMessageProcessorTest { private PullMessageProcessor pullMessageProcessor; @Spy - private BrokerController brokerController = new BrokerController(new BrokerConfig(), new NettyServerConfig(), new NettyClientConfig(), new MessageStoreConfig()); + private BrokerController brokerController = new BrokerController(new BrokerConfig(), new NettyServerConfig(), + new NettyClientConfig(), new MessageStoreConfig()); @Mock private ChannelHandlerContext handlerContext; + private final EmbeddedChannel embeddedChannel = new EmbeddedChannel(); @Mock private MessageStore messageStore; private ClientChannelInfo clientChannelInfo; @@ -77,12 +82,13 @@ public class PullMessageProcessorTest { @Before public void init() { brokerController.setMessageStore(messageStore); + SubscriptionGroupManager subscriptionGroupManager = new SubscriptionGroupManager(brokerController); pullMessageProcessor = new PullMessageProcessor(brokerController); - Channel mockChannel = mock(Channel.class); - when(mockChannel.remoteAddress()).thenReturn(new InetSocketAddress(1024)); - when(handlerContext.channel()).thenReturn(mockChannel); + when(brokerController.getPullMessageProcessor()).thenReturn(pullMessageProcessor); + when(handlerContext.channel()).thenReturn(embeddedChannel); + when(brokerController.getSubscriptionGroupManager()).thenReturn(subscriptionGroupManager); brokerController.getTopicConfigManager().getTopicConfigTable().put(topic, new TopicConfig()); - clientChannelInfo = new ClientChannelInfo(mockChannel); + clientChannelInfo = new ClientChannelInfo(embeddedChannel); ConsumerData consumerData = createConsumerData(group, topic); brokerController.getConsumerManager().registerConsumer( consumerData.getGroupName(), @@ -127,10 +133,11 @@ public void testProcessRequest_SubNotLatest() throws RemotingCommandException { @Test public void testProcessRequest_Found() throws RemotingCommandException { GetMessageResult getMessageResult = createGetMessageResult(); - when(messageStore.getMessage(anyString(), anyString(), anyInt(), anyLong(), anyInt(), any(ExpressionMessageFilter.class))).thenReturn(getMessageResult); + when(messageStore.getMessageAsync(anyString(), anyString(), anyInt(), anyLong(), anyInt(), any(ExpressionMessageFilter.class))).thenReturn(CompletableFuture.completedFuture(getMessageResult)); final RemotingCommand request = createPullMsgCommand(RequestCode.PULL_MESSAGE); - RemotingCommand response = pullMessageProcessor.processRequest(handlerContext, request); + pullMessageProcessor.processRequest(handlerContext, request); + RemotingCommand response = embeddedChannel.readOutbound(); assertThat(response).isNotNull(); assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); } @@ -138,7 +145,7 @@ public void testProcessRequest_Found() throws RemotingCommandException { @Test public void testProcessRequest_FoundWithHook() throws RemotingCommandException { GetMessageResult getMessageResult = createGetMessageResult(); - when(messageStore.getMessage(anyString(), anyString(), anyInt(), anyLong(), anyInt(), any(ExpressionMessageFilter.class))).thenReturn(getMessageResult); + when(messageStore.getMessageAsync(anyString(), anyString(), anyInt(), anyLong(), anyInt(), any(ExpressionMessageFilter.class))).thenReturn(CompletableFuture.completedFuture(getMessageResult)); List consumeMessageHookList = new ArrayList<>(); final ConsumeMessageContext[] messageContext = new ConsumeMessageContext[1]; ConsumeMessageHook consumeMessageHook = new ConsumeMessageHook() { @@ -159,7 +166,8 @@ public void consumeMessageAfter(ConsumeMessageContext context) { consumeMessageHookList.add(consumeMessageHook); pullMessageProcessor.registerConsumeMessageHook(consumeMessageHookList); final RemotingCommand request = createPullMsgCommand(RequestCode.PULL_MESSAGE); - RemotingCommand response = pullMessageProcessor.processRequest(handlerContext, request); + pullMessageProcessor.processRequest(handlerContext, request); + RemotingCommand response = embeddedChannel.readOutbound(); assertThat(response).isNotNull(); assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); assertThat(messageContext[0]).isNotNull(); @@ -172,10 +180,11 @@ public void consumeMessageAfter(ConsumeMessageContext context) { public void testProcessRequest_MsgWasRemoving() throws RemotingCommandException { GetMessageResult getMessageResult = createGetMessageResult(); getMessageResult.setStatus(GetMessageStatus.MESSAGE_WAS_REMOVING); - when(messageStore.getMessage(anyString(), anyString(), anyInt(), anyLong(), anyInt(), any(ExpressionMessageFilter.class))).thenReturn(getMessageResult); + when(messageStore.getMessageAsync(anyString(), anyString(), anyInt(), anyLong(), anyInt(), any(ExpressionMessageFilter.class))).thenReturn(CompletableFuture.completedFuture(getMessageResult)); final RemotingCommand request = createPullMsgCommand(RequestCode.PULL_MESSAGE); - RemotingCommand response = pullMessageProcessor.processRequest(handlerContext, request); + pullMessageProcessor.processRequest(handlerContext, request); + RemotingCommand response = embeddedChannel.readOutbound(); assertThat(response).isNotNull(); assertThat(response.getCode()).isEqualTo(ResponseCode.PULL_RETRY_IMMEDIATELY); } @@ -184,14 +193,57 @@ public void testProcessRequest_MsgWasRemoving() throws RemotingCommandException public void testProcessRequest_NoMsgInQueue() throws RemotingCommandException { GetMessageResult getMessageResult = createGetMessageResult(); getMessageResult.setStatus(GetMessageStatus.NO_MESSAGE_IN_QUEUE); - when(messageStore.getMessage(anyString(), anyString(), anyInt(), anyLong(), anyInt(), any(ExpressionMessageFilter.class))).thenReturn(getMessageResult); + when(messageStore.getMessageAsync(anyString(), anyString(), anyInt(), anyLong(), anyInt(), any(ExpressionMessageFilter.class))).thenReturn(CompletableFuture.completedFuture(getMessageResult)); final RemotingCommand request = createPullMsgCommand(RequestCode.PULL_MESSAGE); - RemotingCommand response = pullMessageProcessor.processRequest(handlerContext, request); + pullMessageProcessor.processRequest(handlerContext, request); + RemotingCommand response = embeddedChannel.readOutbound(); assertThat(response).isNotNull(); assertThat(response.getCode()).isEqualTo(ResponseCode.PULL_OFFSET_MOVED); } + @Test + public void test_LitePullRequestForbidden() throws Exception { + brokerController.getBrokerConfig().setLitePullMessageEnable(false); + RemotingCommand remotingCommand = createPullMsgCommand(RequestCode.LITE_PULL_MESSAGE); + RemotingCommand response = pullMessageProcessor.processRequest(handlerContext, remotingCommand); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.NO_PERMISSION); + } + + @Test + public void testIfBroadcast() throws Exception { + Class clazz = pullMessageProcessor.getClass(); + Method method = clazz.getDeclaredMethod("isBroadcast", boolean.class, ConsumerGroupInfo.class); + method.setAccessible(true); + + ConsumerGroupInfo consumerGroupInfo = new ConsumerGroupInfo("GID-1", + ConsumeType.CONSUME_PASSIVELY, MessageModel.CLUSTERING, ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + Assert.assertTrue((Boolean) method.invoke(pullMessageProcessor, true, consumerGroupInfo)); + + ConsumerGroupInfo consumerGroupInfo2 = new ConsumerGroupInfo("GID-2", + ConsumeType.CONSUME_ACTIVELY, MessageModel.BROADCASTING, ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + Assert.assertFalse((Boolean) method.invoke(pullMessageProcessor, false, consumerGroupInfo2)); + + ConsumerGroupInfo consumerGroupInfo3 = new ConsumerGroupInfo("GID-3", + ConsumeType.CONSUME_PASSIVELY, MessageModel.BROADCASTING, ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + Assert.assertTrue((Boolean) method.invoke(pullMessageProcessor, false, consumerGroupInfo3)); + } + + @Test + public void testCommitPullOffset() throws RemotingCommandException { + GetMessageResult getMessageResult = createGetMessageResult(); + when(messageStore.getMessageAsync(anyString(), anyString(), anyInt(), anyLong(), anyInt(), any(ExpressionMessageFilter.class))).thenReturn(CompletableFuture.completedFuture(getMessageResult)); + + final RemotingCommand request = createPullMsgCommand(RequestCode.PULL_MESSAGE); + pullMessageProcessor.processRequest(handlerContext, request); + RemotingCommand response = embeddedChannel.readOutbound(); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(this.brokerController.getConsumerOffsetManager().queryPullOffset(group, topic, 1)) + .isEqualTo(getMessageResult.getNextBeginOffset()); + } + private RemotingCommand createPullMsgCommand(int requestCode) { PullMessageRequestHeader requestHeader = new PullMessageRequestHeader(); requestHeader.setCommitOffset(123L); @@ -232,4 +284,4 @@ private GetMessageResult createGetMessageResult() { getMessageResult.setNextBeginOffset(516); return getMessageResult; } -} \ No newline at end of file +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/processor/QueryAssignmentProcessorTest.java b/broker/src/test/java/org/apache/rocketmq/broker/processor/QueryAssignmentProcessorTest.java new file mode 100644 index 00000000000..e91c1a09617 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/QueryAssignmentProcessorTest.java @@ -0,0 +1,227 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.processor; + +import com.google.common.collect.ImmutableSet; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import java.util.ArrayList; +import java.util.List; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.client.ClientChannelInfo; +import org.apache.rocketmq.broker.topic.TopicRouteInfoManager; +import org.apache.rocketmq.client.consumer.AllocateMessageQueueStrategy; +import org.apache.rocketmq.client.consumer.rebalance.AllocateMessageQueueAveragely; +import org.apache.rocketmq.client.consumer.rebalance.AllocateMessageQueueAveragelyByCircle; +import org.apache.rocketmq.client.consumer.rebalance.AllocateMessageQueueConsistentHash; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.message.MessageRequestMode; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.protocol.LanguageCode; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.QueryAssignmentRequestBody; +import org.apache.rocketmq.remoting.protocol.body.QueryAssignmentResponseBody; +import org.apache.rocketmq.remoting.protocol.body.SetMessageRequestModeRequestBody; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumerData; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.apache.rocketmq.broker.processor.PullMessageProcessorTest.createConsumerData; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class QueryAssignmentProcessorTest { + private QueryAssignmentProcessor queryAssignmentProcessor; + @Spy + private BrokerController brokerController = new BrokerController(new BrokerConfig(), new NettyServerConfig(), new NettyClientConfig(), new MessageStoreConfig()); + + @Mock + private TopicRouteInfoManager topicRouteInfoManager; + @Mock + private ChannelHandlerContext handlerContext; + @Mock + private MessageStore messageStore; + @Mock + private Channel channel; + + private String broker = "defaultBroker"; + private String topic = "FooBar"; + private String group = "FooBarGroup"; + private String clientId = "127.0.0.1"; + private ClientChannelInfo clientInfo; + + @Before + public void init() throws IllegalAccessException, NoSuchFieldException { + clientInfo = new ClientChannelInfo(channel, "127.0.0.1", LanguageCode.JAVA, 0); + brokerController.setMessageStore(messageStore); + doReturn(topicRouteInfoManager).when(brokerController).getTopicRouteInfoManager(); + when(topicRouteInfoManager.getTopicSubscribeInfo(topic)).thenReturn(ImmutableSet.of(new MessageQueue(topic, "broker-1", 0), new MessageQueue(topic, "broker-2", 1))); + queryAssignmentProcessor = new QueryAssignmentProcessor(brokerController); + brokerController.getTopicConfigManager().getTopicConfigTable().put(topic, new TopicConfig()); + ConsumerData consumerData = createConsumerData(group, topic); + brokerController.getConsumerManager().registerConsumer( + consumerData.getGroupName(), + clientInfo, + consumerData.getConsumeType(), + consumerData.getMessageModel(), + consumerData.getConsumeFromWhere(), + consumerData.getSubscriptionDataSet(), + false); + } + + @Test + public void testQueryAssignment() throws Exception { + brokerController.getProducerManager().registerProducer(group, clientInfo); + final RemotingCommand request = createQueryAssignmentRequest(); + RemotingCommand responseToReturn = queryAssignmentProcessor.processRequest(handlerContext, request); + assertThat(responseToReturn.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(responseToReturn.getBody()).isNotNull(); + QueryAssignmentResponseBody responseBody = QueryAssignmentResponseBody.decode(responseToReturn.getBody(), QueryAssignmentResponseBody.class); + assertThat(responseBody.getMessageQueueAssignments()).size().isEqualTo(2); + } + + @Test + public void testSetMessageRequestMode_Success() throws Exception { + brokerController.getProducerManager().registerProducer(group, clientInfo); + final RemotingCommand request = createSetMessageRequestModeRequest(topic); + RemotingCommand responseToReturn = queryAssignmentProcessor.processRequest(handlerContext, request); + assertThat(responseToReturn.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testSetMessageRequestMode_RetryTopic() throws Exception { + brokerController.getProducerManager().registerProducer(group, clientInfo); + final RemotingCommand request = createSetMessageRequestModeRequest(MixAll.RETRY_GROUP_TOPIC_PREFIX + topic); + RemotingCommand responseToReturn = queryAssignmentProcessor.processRequest(handlerContext, request); + assertThat(responseToReturn.getCode()).isEqualTo(ResponseCode.NO_PERMISSION); + } + + @Test + public void testAllocate4Pop() { + testAllocate4Pop(new AllocateMessageQueueAveragely()); + testAllocate4Pop(new AllocateMessageQueueAveragelyByCircle()); + testAllocate4Pop(new AllocateMessageQueueConsistentHash()); + } + + private void testAllocate4Pop(AllocateMessageQueueStrategy strategy) { + int testNum = 16; + List mqAll = new ArrayList<>(); + for (int mqSize = 0; mqSize < testNum; mqSize++) { + mqAll.add(new MessageQueue(topic, broker, mqSize)); + + List cidAll = new ArrayList<>(); + for (int cidSize = 0; cidSize < testNum; cidSize++) { + String clientId = String.valueOf(cidSize); + cidAll.add(clientId); + + for (int popShareQueueNum = 0; popShareQueueNum < testNum; popShareQueueNum++) { + List allocateResult = + queryAssignmentProcessor.allocate4Pop(strategy, group, clientId, mqAll, cidAll, popShareQueueNum); + Assert.assertTrue(checkAllocateResult(popShareQueueNum, mqAll.size(), cidAll.size(), allocateResult.size(), strategy)); + } + } + } + } + + private boolean checkAllocateResult(int popShareQueueNum, int mqSize, int cidSize, int allocateSize, + AllocateMessageQueueStrategy strategy) { + + //The maximum size of allocations will not exceed mqSize. + if (allocateSize > mqSize) { + return false; + } + + //It is not allowed that the client is not assigned to the consumeQueue. + if (allocateSize <= 0) { + return false; + } + + if (popShareQueueNum <= 0 || popShareQueueNum >= cidSize - 1) { + return allocateSize == mqSize; + } else if (mqSize < cidSize) { + return allocateSize == 1; + } + + if (strategy instanceof AllocateMessageQueueAveragely + || strategy instanceof AllocateMessageQueueAveragelyByCircle) { + + if (mqSize % cidSize == 0) { + return allocateSize == (mqSize / cidSize) * (popShareQueueNum + 1); + } else { + int avgSize = mqSize / cidSize; + return allocateSize >= avgSize * (popShareQueueNum + 1) + && allocateSize <= (avgSize + 1) * (popShareQueueNum + 1); + } + } + + if (strategy instanceof AllocateMessageQueueConsistentHash) { + //Just skip + return true; + } + + return false; + } + + private RemotingCommand createQueryAssignmentRequest() { + QueryAssignmentRequestBody requestBody = new QueryAssignmentRequestBody(); + requestBody.setTopic(topic); + requestBody.setConsumerGroup(group); + requestBody.setClientId(clientId); + requestBody.setMessageModel(MessageModel.CLUSTERING); + requestBody.setStrategyName("AVG"); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_ASSIGNMENT, null); + request.setBody(requestBody.encode()); + return request; + } + + private RemotingCommand createSetMessageRequestModeRequest(String topic) { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.SET_MESSAGE_REQUEST_MODE, null); + + SetMessageRequestModeRequestBody requestBody = new SetMessageRequestModeRequestBody(); + requestBody.setTopic(topic); + requestBody.setConsumerGroup(group); + requestBody.setMode(MessageRequestMode.POP); + requestBody.setPopShareQueueNum(0); + request.setBody(requestBody.encode()); + + return request; + } + + private RemotingCommand createResponse(int code, RemotingCommand request) { + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(code); + response.setOpaque(request.getOpaque()); + return response; + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/processor/ReplyMessageProcessorTest.java b/broker/src/test/java/org/apache/rocketmq/broker/processor/ReplyMessageProcessorTest.java new file mode 100644 index 00000000000..266c8491cbf --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/ReplyMessageProcessorTest.java @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.processor; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.util.HashMap; +import java.util.Map; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.client.ClientChannelInfo; +import org.apache.rocketmq.broker.client.net.Broker2Client; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.exception.RemotingSendRequestException; +import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.protocol.LanguageCode; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageResponseHeader; +import org.apache.rocketmq.store.AppendMessageResult; +import org.apache.rocketmq.store.AppendMessageStatus; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ReplyMessageProcessorTest { + private ReplyMessageProcessor replyMessageProcessor; + @Spy + private BrokerController brokerController = new BrokerController(new BrokerConfig(), new NettyServerConfig(), new NettyClientConfig(), new MessageStoreConfig()); + @Mock + private ChannelHandlerContext handlerContext; + @Mock + private MessageStore messageStore; + @Mock + private Channel channel; + + private String topic = "FooBar"; + private String group = "FooBarGroup"; + private ClientChannelInfo clientInfo; + @Mock + private Broker2Client broker2Client; + + @Before + public void init() throws IllegalAccessException, NoSuchFieldException { + clientInfo = new ClientChannelInfo(channel, "127.0.0.1", LanguageCode.JAVA, 0); + brokerController.setMessageStore(messageStore); + Field field = BrokerController.class.getDeclaredField("broker2Client"); + field.setAccessible(true); + field.set(brokerController, broker2Client); + when(messageStore.now()).thenReturn(System.currentTimeMillis()); + Channel mockChannel = mock(Channel.class); + when(mockChannel.remoteAddress()).thenReturn(new InetSocketAddress(1024)); + when(handlerContext.channel()).thenReturn(mockChannel); + replyMessageProcessor = new ReplyMessageProcessor(brokerController); + } + + @Test + public void testProcessRequest_Success() throws RemotingCommandException, InterruptedException, RemotingTimeoutException, RemotingSendRequestException { + when(messageStore.putMessage(any(MessageExtBrokerInner.class))).thenReturn(new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK))); + brokerController.getProducerManager().registerProducer(group, clientInfo); + final RemotingCommand request = createSendMessageRequestHeaderCommand(RequestCode.SEND_REPLY_MESSAGE); + when(brokerController.getBroker2Client().callClient(any(), any(RemotingCommand.class))).thenReturn(createResponse(ResponseCode.SUCCESS, request)); + RemotingCommand responseToReturn = replyMessageProcessor.processRequest(handlerContext, request); + assertThat(responseToReturn.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(responseToReturn.getOpaque()).isEqualTo(request.getOpaque()); + } + + private RemotingCommand createSendMessageRequestHeaderCommand(int requestCode) { + SendMessageRequestHeader requestHeader = createSendMessageRequestHeader(); + RemotingCommand request = RemotingCommand.createRequestCommand(requestCode, requestHeader); + request.setBody(new byte[] {'a'}); + request.makeCustomHeaderToNet(); + return request; + } + + private SendMessageRequestHeader createSendMessageRequestHeader() { + SendMessageRequestHeader requestHeader = new SendMessageRequestHeader(); + requestHeader.setProducerGroup(group); + requestHeader.setTopic(topic); + requestHeader.setDefaultTopic(TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC); + requestHeader.setDefaultTopicQueueNums(3); + requestHeader.setQueueId(1); + requestHeader.setSysFlag(0); + requestHeader.setBornTimestamp(System.currentTimeMillis()); + requestHeader.setFlag(124); + requestHeader.setReconsumeTimes(0); + Map map = new HashMap<>(); + map.put(MessageConst.PROPERTY_MESSAGE_REPLY_TO_CLIENT, "127.0.0.1"); + requestHeader.setProperties(MessageDecoder.messageProperties2String(map)); + return requestHeader; + } + + private RemotingCommand createResponse(int code, RemotingCommand request) { + RemotingCommand response = RemotingCommand.createResponseCommand(SendMessageResponseHeader.class); + response.setCode(code); + response.setOpaque(request.getOpaque()); + return response; + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/processor/SendMessageProcessorTest.java b/broker/src/test/java/org/apache/rocketmq/broker/processor/SendMessageProcessorTest.java index 7828e7a91cd..e046c888438 100644 --- a/broker/src/test/java/org/apache/rocketmq/broker/processor/SendMessageProcessorTest.java +++ b/broker/src/test/java/org/apache/rocketmq/broker/processor/SendMessageProcessorTest.java @@ -19,25 +19,39 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import java.net.InetSocketAddress; +import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.AbortProcessException; +import org.apache.rocketmq.broker.mqtrace.ConsumeMessageContext; +import org.apache.rocketmq.broker.mqtrace.ConsumeMessageHook; import org.apache.rocketmq.broker.mqtrace.SendMessageContext; import org.apache.rocketmq.broker.mqtrace.SendMessageHook; +import org.apache.rocketmq.broker.subscription.SubscriptionGroupManager; +import org.apache.rocketmq.broker.topic.TopicConfigManager; +import org.apache.rocketmq.broker.transaction.TransactionalMessageService; import org.apache.rocketmq.common.BrokerConfig; -import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; import org.apache.rocketmq.common.message.MessageExt; -import org.apache.rocketmq.common.protocol.RequestCode; -import org.apache.rocketmq.common.protocol.ResponseCode; -import org.apache.rocketmq.common.protocol.header.ConsumerSendMsgBackRequestHeader; -import org.apache.rocketmq.common.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.common.topic.TopicValidator; import org.apache.rocketmq.remoting.exception.RemotingCommandException; import org.apache.rocketmq.remoting.netty.NettyClientConfig; import org.apache.rocketmq.remoting.netty.NettyServerConfig; import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.ConsumerSendMsgBackRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; import org.apache.rocketmq.store.AppendMessageResult; import org.apache.rocketmq.store.AppendMessageStatus; -import org.apache.rocketmq.store.MessageExtBrokerInner; import org.apache.rocketmq.store.MessageStore; import org.apache.rocketmq.store.PutMessageResult; import org.apache.rocketmq.store.PutMessageStatus; @@ -47,15 +61,13 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Spy; -import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.MockitoJUnitRunner; -import org.mockito.stubbing.Answer; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) @@ -63,34 +75,47 @@ public class SendMessageProcessorTest { private SendMessageProcessor sendMessageProcessor; @Mock private ChannelHandlerContext handlerContext; + @Mock + private Channel channel; @Spy - private BrokerController brokerController = new BrokerController(new BrokerConfig(), new NettyServerConfig(), new NettyClientConfig(), new MessageStoreConfig()); + private BrokerController brokerController = new BrokerController(new BrokerConfig(), new NettyServerConfig(), + new NettyClientConfig(), new MessageStoreConfig()); @Mock private MessageStore messageStore; + @Mock + private TransactionalMessageService transactionMsgService; + private String topic = "FooBar"; private String group = "FooBarGroup"; @Before public void init() { brokerController.setMessageStore(messageStore); + TopicConfigManager topicConfigManager = new TopicConfigManager(brokerController); + topicConfigManager.getTopicConfigTable().put(topic, new TopicConfig(topic)); + SubscriptionGroupManager subscriptionGroupManager = new SubscriptionGroupManager(brokerController); + when(brokerController.getSubscriptionGroupManager()).thenReturn(subscriptionGroupManager); + when(brokerController.getTopicConfigManager()).thenReturn(topicConfigManager); + when(brokerController.getPutMessageFutureExecutor()).thenReturn(Executors.newSingleThreadExecutor()); when(messageStore.now()).thenReturn(System.currentTimeMillis()); - Channel mockChannel = mock(Channel.class); - when(mockChannel.remoteAddress()).thenReturn(new InetSocketAddress(1024)); - when(handlerContext.channel()).thenReturn(mockChannel); + when(channel.remoteAddress()).thenReturn(new InetSocketAddress(1024)); + when(handlerContext.channel()).thenReturn(channel); when(messageStore.lookMessageByOffset(anyLong())).thenReturn(new MessageExt()); sendMessageProcessor = new SendMessageProcessor(brokerController); } @Test - public void testProcessRequest() throws RemotingCommandException { - when(messageStore.putMessage(any(MessageExtBrokerInner.class))).thenReturn(new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK))); + public void testProcessRequest() throws Exception { + when(messageStore.asyncPutMessage(any(MessageExtBrokerInner.class))). + thenReturn(CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK)))); assertPutResult(ResponseCode.SUCCESS); } @Test - public void testProcessRequest_WithHook() throws RemotingCommandException { - when(messageStore.putMessage(any(MessageExtBrokerInner.class))).thenReturn(new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK))); + public void testProcessRequest_WithHook() throws Exception { + when(messageStore.asyncPutMessage(any(MessageExtBrokerInner.class))). + thenReturn(CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK)))); List sendMessageHookList = new ArrayList<>(); final SendMessageContext[] sendMessageContext = new SendMessageContext[1]; SendMessageHook sendMessageHook = new SendMessageHook() { @@ -112,63 +137,71 @@ public void sendMessageAfter(SendMessageContext context) { sendMessageHookList.add(sendMessageHook); sendMessageProcessor.registerSendMessageHook(sendMessageHookList); assertPutResult(ResponseCode.SUCCESS); - System.out.println(sendMessageContext[0]); assertThat(sendMessageContext[0]).isNotNull(); assertThat(sendMessageContext[0].getTopic()).isEqualTo(topic); assertThat(sendMessageContext[0].getProducerGroup()).isEqualTo(group); } @Test - public void testProcessRequest_FlushTimeOut() throws RemotingCommandException { - when(messageStore.putMessage(any(MessageExtBrokerInner.class))).thenReturn(new PutMessageResult(PutMessageStatus.FLUSH_DISK_TIMEOUT, new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR))); + public void testProcessRequest_FlushTimeOut() throws Exception { + when(messageStore.asyncPutMessage(any(MessageExtBrokerInner.class))). + thenReturn(CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.FLUSH_DISK_TIMEOUT, new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR)))); assertPutResult(ResponseCode.FLUSH_DISK_TIMEOUT); } @Test - public void testProcessRequest_MessageIllegal() throws RemotingCommandException { - when(messageStore.putMessage(any(MessageExtBrokerInner.class))).thenReturn(new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR))); + public void testProcessRequest_MessageIllegal() throws Exception { + when(messageStore.asyncPutMessage(any(MessageExtBrokerInner.class))). + thenReturn(CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR)))); assertPutResult(ResponseCode.MESSAGE_ILLEGAL); } @Test - public void testProcessRequest_CreateMappedFileFailed() throws RemotingCommandException { - when(messageStore.putMessage(any(MessageExtBrokerInner.class))).thenReturn(new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR))); + public void testProcessRequest_CreateMappedFileFailed() throws Exception { + when(messageStore.asyncPutMessage(any(MessageExtBrokerInner.class))). + thenReturn(CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.CREATE_MAPPED_FILE_FAILED, new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR)))); assertPutResult(ResponseCode.SYSTEM_ERROR); } @Test - public void testProcessRequest_FlushSlaveTimeout() throws RemotingCommandException { - when(messageStore.putMessage(any(MessageExtBrokerInner.class))).thenReturn(new PutMessageResult(PutMessageStatus.FLUSH_SLAVE_TIMEOUT, new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR))); + public void testProcessRequest_FlushSlaveTimeout() throws Exception { + when(messageStore.asyncPutMessage(any(MessageExtBrokerInner.class))). + thenReturn(CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.FLUSH_SLAVE_TIMEOUT, new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR)))); assertPutResult(ResponseCode.FLUSH_SLAVE_TIMEOUT); } @Test - public void testProcessRequest_PageCacheBusy() throws RemotingCommandException { - when(messageStore.putMessage(any(MessageExtBrokerInner.class))).thenReturn(new PutMessageResult(PutMessageStatus.OS_PAGECACHE_BUSY, new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR))); + public void testProcessRequest_PageCacheBusy() throws Exception { + when(messageStore.asyncPutMessage(any(MessageExtBrokerInner.class))). + thenReturn(CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.OS_PAGE_CACHE_BUSY, new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR)))); assertPutResult(ResponseCode.SYSTEM_ERROR); } @Test - public void testProcessRequest_PropertiesTooLong() throws RemotingCommandException { - when(messageStore.putMessage(any(MessageExtBrokerInner.class))).thenReturn(new PutMessageResult(PutMessageStatus.PROPERTIES_SIZE_EXCEEDED, new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR))); + public void testProcessRequest_PropertiesTooLong() throws Exception { + when(messageStore.asyncPutMessage(any(MessageExtBrokerInner.class))). + thenReturn(CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.PROPERTIES_SIZE_EXCEEDED, new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR)))); assertPutResult(ResponseCode.MESSAGE_ILLEGAL); } @Test - public void testProcessRequest_ServiceNotAvailable() throws RemotingCommandException { - when(messageStore.putMessage(any(MessageExtBrokerInner.class))).thenReturn(new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR))); + public void testProcessRequest_ServiceNotAvailable() throws Exception { + when(messageStore.asyncPutMessage(any(MessageExtBrokerInner.class))). + thenReturn(CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR)))); assertPutResult(ResponseCode.SERVICE_NOT_AVAILABLE); } @Test - public void testProcessRequest_SlaveNotAvailable() throws RemotingCommandException { - when(messageStore.putMessage(any(MessageExtBrokerInner.class))).thenReturn(new PutMessageResult(PutMessageStatus.SLAVE_NOT_AVAILABLE, new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR))); + public void testProcessRequest_SlaveNotAvailable() throws Exception { + when(messageStore.asyncPutMessage(any(MessageExtBrokerInner.class))). + thenReturn(CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.SLAVE_NOT_AVAILABLE, new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR)))); assertPutResult(ResponseCode.SLAVE_NOT_AVAILABLE); } @Test - public void testProcessRequest_WithMsgBack() throws RemotingCommandException { - when(messageStore.putMessage(any(MessageExtBrokerInner.class))).thenReturn(new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK))); + public void testProcessRequest_WithMsgBack() throws Exception { + when(messageStore.putMessage(any(MessageExtBrokerInner.class))). + thenReturn(new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK))); final RemotingCommand request = createSendMsgBackCommand(RequestCode.CONSUMER_SEND_MSG_BACK); sendMessageProcessor = new SendMessageProcessor(brokerController); @@ -177,17 +210,125 @@ public void testProcessRequest_WithMsgBack() throws RemotingCommandException { assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); } - private RemotingCommand createSendMsgCommand(int requestCode) { + @Test + public void testProcessRequest_Transaction() throws RemotingCommandException { + brokerController.setTransactionalMessageService(transactionMsgService); + when(brokerController.getTransactionalMessageService().asyncPrepareMessage(any(MessageExtBrokerInner.class))) + .thenReturn(CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK)))); + RemotingCommand request = createSendTransactionMsgCommand(RequestCode.SEND_MESSAGE); + final RemotingCommand[] response = new RemotingCommand[1]; + doAnswer(invocation -> { + response[0] = invocation.getArgument(0); + return null; + }).when(channel).writeAndFlush(any(Object.class)); + await().atMost(Duration.ofSeconds(10)).until(() -> { + RemotingCommand responseToReturn = sendMessageProcessor.processRequest(handlerContext, request); + if (responseToReturn != null) { + assertThat(response[0]).isNull(); + response[0] = responseToReturn; + } + + if (response[0] == null) { + return false; + } + assertThat(response[0].getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(response[0].getOpaque()).isEqualTo(request.getOpaque()); + return true; + }); + } + + @Test + public void testProcessRequest_WithAbortProcessSendMessageBeforeHook() throws Exception { + List sendMessageHookList = new ArrayList<>(); + final SendMessageContext[] sendMessageContext = new SendMessageContext[1]; + SendMessageHook sendMessageHook = new SendMessageHook() { + @Override + public String hookName() { + return null; + } + + @Override + public void sendMessageBefore(SendMessageContext context) { + sendMessageContext[0] = context; + throw new AbortProcessException(ResponseCode.FLOW_CONTROL, "flow control test"); + } + + @Override + public void sendMessageAfter(SendMessageContext context) { + + } + }; + sendMessageHookList.add(sendMessageHook); + sendMessageProcessor.registerSendMessageHook(sendMessageHookList); + assertPutResult(ResponseCode.FLOW_CONTROL); + assertThat(sendMessageContext[0]).isNotNull(); + assertThat(sendMessageContext[0].getTopic()).isEqualTo(topic); + assertThat(sendMessageContext[0].getProducerGroup()).isEqualTo(group); + } + + @Test + public void testProcessRequest_WithMsgBackWithConsumeMessageAfterHook() throws Exception { + when(messageStore.putMessage(any(MessageExtBrokerInner.class))). + thenReturn(new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK))); + final RemotingCommand request = createSendMsgBackCommand(RequestCode.CONSUMER_SEND_MSG_BACK); + + sendMessageProcessor = new SendMessageProcessor(brokerController); + List consumeMessageHookList = new ArrayList<>(); + final ConsumeMessageContext[] messageContext = new ConsumeMessageContext[1]; + ConsumeMessageHook consumeMessageHook = new ConsumeMessageHook() { + @Override + public String hookName() { + return "TestHook"; + } + + @Override + public void consumeMessageBefore(ConsumeMessageContext context) { + + } + + @Override + public void consumeMessageAfter(ConsumeMessageContext context) { + messageContext[0] = context; + throw new AbortProcessException(ResponseCode.FLOW_CONTROL, "flow control test"); + } + }; + consumeMessageHookList.add(consumeMessageHook); + sendMessageProcessor.registerConsumeMessageHook(consumeMessageHookList); + final RemotingCommand response = sendMessageProcessor.processRequest(handlerContext, request); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + private RemotingCommand createSendTransactionMsgCommand(int requestCode) { + SendMessageRequestHeader header = createSendMsgRequestHeader(); + int sysFlag = header.getSysFlag(); + Map oriProps = MessageDecoder.string2messageProperties(header.getProperties()); + oriProps.put(MessageConst.PROPERTY_TRANSACTION_PREPARED, "true"); + header.setProperties(MessageDecoder.messageProperties2String(oriProps)); + sysFlag |= MessageSysFlag.TRANSACTION_PREPARED_TYPE; + header.setSysFlag(sysFlag); + RemotingCommand request = RemotingCommand.createRequestCommand(requestCode, header); + request.setBody(new byte[] {'a'}); + request.makeCustomHeaderToNet(); + return request; + } + + private SendMessageRequestHeader createSendMsgRequestHeader() { SendMessageRequestHeader requestHeader = new SendMessageRequestHeader(); requestHeader.setProducerGroup(group); requestHeader.setTopic(topic); - requestHeader.setDefaultTopic(MixAll.DEFAULT_TOPIC); + requestHeader.setDefaultTopic(TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC); requestHeader.setDefaultTopicQueueNums(3); requestHeader.setQueueId(1); requestHeader.setSysFlag(0); requestHeader.setBornTimestamp(System.currentTimeMillis()); requestHeader.setFlag(124); requestHeader.setReconsumeTimes(0); + return requestHeader; + } + + private RemotingCommand createSendMsgCommand(int requestCode) { + SendMessageRequestHeader requestHeader = createSendMsgRequestHeader(); RemotingCommand request = RemotingCommand.createRequestCommand(requestCode, requestHeader); request.setBody(new byte[] {'a'}); @@ -208,22 +349,34 @@ private RemotingCommand createSendMsgBackCommand(int requestCode) { return request; } + /** + * We will explain the logic of this method so you can get a better feeling of how to use it: This method assumes + * that if responseToReturn is not null, then there would be an error, which means the writeAndFlush are never + * reached. If responseToReturn is null, means everything ok, so writeAndFlush should record the actual response. + * + * @param responseCode + * @throws RemotingCommandException + */ private void assertPutResult(int responseCode) throws RemotingCommandException { final RemotingCommand request = createSendMsgCommand(RequestCode.SEND_MESSAGE); final RemotingCommand[] response = new RemotingCommand[1]; - doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - response[0] = invocation.getArgument(0); - return null; + doAnswer(invocation -> { + response[0] = invocation.getArgument(0); + return null; + }).when(channel).writeAndFlush(any(Object.class)); + await().atMost(Duration.ofSeconds(10)).until(() -> { + RemotingCommand responseToReturn = sendMessageProcessor.processRequest(handlerContext, request); + if (responseToReturn != null) { + assertThat(response[0]).isNull(); + response[0] = responseToReturn; + } + + if (response[0] == null) { + return false; } - }).when(handlerContext).writeAndFlush(any(Object.class)); - RemotingCommand responseToReturn = sendMessageProcessor.processRequest(handlerContext, request); - if (responseToReturn != null) { - assertThat(response[0]).isNull(); - response[0] = responseToReturn; - } - assertThat(response[0].getCode()).isEqualTo(responseCode); - assertThat(response[0].getOpaque()).isEqualTo(request.getOpaque()); - } -} \ No newline at end of file + assertThat(response[0].getCode()).isEqualTo(responseCode); + assertThat(response[0].getOpaque()).isEqualTo(request.getOpaque()); + return true; + }); + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/schedule/ScheduleMessageServiceTest.java b/broker/src/test/java/org/apache/rocketmq/broker/schedule/ScheduleMessageServiceTest.java new file mode 100644 index 00000000000..b90fb2931d5 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/schedule/ScheduleMessageServiceTest.java @@ -0,0 +1,307 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.schedule; + +import java.io.File; +import java.lang.reflect.Field; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.failover.EscapeBridge; +import org.apache.rocketmq.broker.util.HookUtils; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.store.ConsumeQueueExt; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.GetMessageStatus; +import org.apache.rocketmq.store.MessageArrivingListener; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.stats.BrokerStatsManager; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import static org.apache.rocketmq.common.stats.Stats.BROKER_PUT_NUMS; +import static org.apache.rocketmq.common.stats.Stats.TOPIC_PUT_NUMS; +import static org.apache.rocketmq.common.stats.Stats.TOPIC_PUT_SIZE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; + +public class ScheduleMessageServiceTest { + + private BrokerController brokerController; + private ScheduleMessageService scheduleMessageService; + + /** + * t defaultMessageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h" + */ + String testMessageDelayLevel = "5s 8s"; + /** + * choose delay level + */ + int delayLevel = 3; + + private static final String STORE_PATH = System.getProperty("java.io.tmpdir") + File.separator + "schedule_test#" + UUID.randomUUID(); + private static final int COMMIT_LOG_FILE_SIZE = 1024; + private static final int CQ_FILE_SIZE = 10; + private static final int CQ_EXT_FILE_SIZE = 10 * (ConsumeQueueExt.CqExtUnit.MIN_EXT_UNIT_SIZE + 64); + + private static SocketAddress bornHost; + private static SocketAddress storeHost; + private DefaultMessageStore messageStore; + private MessageStoreConfig messageStoreConfig; + private BrokerConfig brokerConfig; + + static String sendMessage = " ------- schedule message test -------"; + static String topic = "schedule_topic_test"; + static String messageGroup = "delayGroupTest"; + private Random random = new Random(); + + static { + try { + bornHost = new InetSocketAddress(InetAddress.getLocalHost(), 8123); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + try { + storeHost = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + } + + @Before + public void setUp() throws Exception { + messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setMessageDelayLevel(testMessageDelayLevel); + messageStoreConfig.setMappedFileSizeCommitLog(COMMIT_LOG_FILE_SIZE); + messageStoreConfig.setMappedFileSizeConsumeQueue(CQ_FILE_SIZE); + messageStoreConfig.setMappedFileSizeConsumeQueueExt(CQ_EXT_FILE_SIZE); + messageStoreConfig.setMessageIndexEnable(false); + messageStoreConfig.setEnableConsumeQueueExt(true); + messageStoreConfig.setStorePathRootDir(STORE_PATH); + messageStoreConfig.setStorePathCommitLog(STORE_PATH + File.separator + "commitlog"); + // Let OS pick an available port + messageStoreConfig.setHaListenPort(0); + + brokerConfig = new BrokerConfig(); + BrokerStatsManager manager = new BrokerStatsManager(brokerConfig.getBrokerClusterName(), brokerConfig.isEnableDetailStat()); + messageStore = new DefaultMessageStore(messageStoreConfig, manager, new MyMessageArrivingListener(), new BrokerConfig(), new ConcurrentHashMap<>()); + + assertThat(messageStore.load()).isTrue(); + + messageStore.start(); + brokerController = Mockito.mock(BrokerController.class); + Mockito.when(brokerController.getMessageStore()).thenReturn(messageStore); + Mockito.when(brokerController.getMessageStoreConfig()).thenReturn(messageStoreConfig); + Mockito.when(brokerController.getBrokerConfig()).thenReturn(brokerConfig); + Mockito.when(brokerController.peekMasterBroker()).thenReturn(brokerController); + Mockito.when(brokerController.getBrokerStatsManager()).thenReturn(manager); + EscapeBridge escapeBridge = new EscapeBridge(brokerController); + Mockito.when(brokerController.getEscapeBridge()).thenReturn(escapeBridge); + scheduleMessageService = new ScheduleMessageService(brokerController); + scheduleMessageService.load(); + scheduleMessageService.start(); + Mockito.when(brokerController.getScheduleMessageService()).thenReturn(scheduleMessageService); + } + + @Test + public void testLoad() { + ConcurrentMap offsetTable = scheduleMessageService.getOffsetTable(); + //offsetTable.put(0, 1L); + offsetTable.put(1, 3L); + offsetTable.put(2, 5L); + scheduleMessageService.persist(); + + ScheduleMessageService controlInstance = new ScheduleMessageService(brokerController); + assertTrue(controlInstance.load()); + + ConcurrentMap loaded = controlInstance.getOffsetTable(); + for (long offset : loaded.values()) { + assertEquals(0, offset); + } + } + + @Test + public void testCorrectDelayOffset_whenInit() throws Exception { + + ConcurrentMap offsetTable = null; + + scheduleMessageService = new ScheduleMessageService(brokerController); + scheduleMessageService.parseDelayLevel(); + + ConcurrentMap offsetTable1 = new ConcurrentHashMap<>(); + for (int i = 1; i <= 2; i++) { + offsetTable1.put(i, random.nextLong()); + } + + Field field = scheduleMessageService.getClass().getDeclaredField("offsetTable"); + field.setAccessible(true); + field.set(scheduleMessageService, offsetTable1); + + String jsonStr = scheduleMessageService.encode(); + scheduleMessageService.decode(jsonStr); + + offsetTable = (ConcurrentMap) field.get(scheduleMessageService); + + for (Map.Entry entry : offsetTable.entrySet()) { + assertEquals(entry.getValue(), offsetTable1.get(entry.getKey())); + } + + boolean success = scheduleMessageService.correctDelayOffset(); + + System.out.printf("correctDelayOffset %s", success); + + offsetTable = (ConcurrentMap) field.get(scheduleMessageService); + + for (long offset : offsetTable.values()) { + assertEquals(0, offset); + } + } + + @Test + public void testDeliverDelayedMessageTimerTask() throws Exception { + assertThat(messageStore.getMessageStoreConfig().isEnableScheduleMessageStats()).isTrue(); + + assertThat(messageStore.getBrokerStatsManager().getStatsItem(TOPIC_PUT_NUMS, topic)).isNull(); + + MessageExtBrokerInner msg = buildMessage(); + int realQueueId = msg.getQueueId(); + // set delayLevel,and send delay message + msg.setDelayTimeLevel(delayLevel); + HookUtils.handleScheduleMessage(brokerController, msg); + PutMessageResult result = messageStore.putMessage(msg); + assertThat(result.isOk()).isTrue(); + + // consumer message + int delayQueueId = ScheduleMessageService.delayLevel2QueueId(delayLevel); + assertThat(delayQueueId).isEqualTo(delayLevel - 1); + + Long offset = result.getAppendMessageResult().getLogicsOffset(); + + // now, no message in queue,must wait > delayTime + GetMessageResult messageResult = getMessage(realQueueId, offset); + assertThat(messageResult.getStatus()).isEqualTo(GetMessageStatus.NO_MESSAGE_IN_QUEUE); + + // timer run maybe delay, then consumer message again + // and wait offsetTable + TimeUnit.SECONDS.sleep(15); + scheduleMessageService.buildRunningStats(new HashMap<>()); + + messageResult = getMessage(realQueueId, offset); + // now,found the message + assertThat(messageResult.getStatus()).isEqualTo(GetMessageStatus.FOUND); + + // get the stats change + assertThat(messageStore.getBrokerStatsManager().getStatsItem(BROKER_PUT_NUMS, brokerConfig.getBrokerClusterName()).getValue().sum()).isEqualTo(1); + assertThat(messageStore.getBrokerStatsManager().getStatsItem(TOPIC_PUT_NUMS, topic).getValue().sum()).isEqualTo(1L); + assertThat(messageStore.getBrokerStatsManager().getStatsItem(TOPIC_PUT_SIZE, topic).getValue().sum()).isEqualTo(messageResult.getBufferTotalSize()); + + // get the message body + ByteBuffer byteBuffer = ByteBuffer.allocate(messageResult.getBufferTotalSize()); + List byteBufferList = messageResult.getMessageBufferList(); + for (ByteBuffer bb : byteBufferList) { + byteBuffer.put(bb); + } + + // warp and decode the message + byteBuffer = ByteBuffer.wrap(byteBuffer.array()); + List msgList = MessageDecoder.decodes(byteBuffer); + String retryMsg = new String(msgList.get(0).getBody()); + assertThat(sendMessage).isEqualTo(retryMsg); + + // method will wait 10s,so I run it by myself + scheduleMessageService.persist(); + + // add mapFile release + messageResult.release(); + + } + + /** + * add some [error/no use] code test + */ + @Test + public void otherTest() { + // the method no use ,why need ? + int queueId = ScheduleMessageService.queueId2DelayLevel(delayLevel); + assertThat(queueId).isEqualTo(delayLevel + 1); + + // error delayLevelTest + Long time = scheduleMessageService.computeDeliverTimestamp(999, 0); + assertThat(time).isEqualTo(1000); + + // just decode + scheduleMessageService.decode(new DelayOffsetSerializeWrapper().toJson()); + } + + private GetMessageResult getMessage(int queueId, Long offset) { + return messageStore.getMessage(messageGroup, topic, + queueId, offset, 1, null); + + } + + @After + public void shutdown() throws InterruptedException { + scheduleMessageService.shutdown(); + messageStore.shutdown(); + messageStore.destroy(); + File file = new File(messageStoreConfig.getStorePathRootDir()); + UtilAll.deleteFile(file); + } + + public MessageExtBrokerInner buildMessage() { + + byte[] msgBody = sendMessage.getBytes(); + MessageExtBrokerInner msg = new MessageExtBrokerInner(); + msg.setTopic(topic); + msg.setTags("schedule_tag"); + msg.setKeys("schedule_key"); + msg.setBody(msgBody); + msg.setSysFlag(0); + msg.setBornTimestamp(System.currentTimeMillis()); + msg.setStoreHost(storeHost); + msg.setBornHost(bornHost); + return msg; + } + + private class MyMessageArrivingListener implements MessageArrivingListener { + @Override + public void arriving(String topic, int queueId, long logicOffset, long tagsCode, long msgStoreTime, + byte[] filterBitMap, Map properties) { + } + } +} \ No newline at end of file diff --git a/broker/src/test/java/org/apache/rocketmq/broker/subscription/ForbiddenTest.java b/broker/src/test/java/org/apache/rocketmq/broker/subscription/ForbiddenTest.java new file mode 100644 index 00000000000..bdaee3b3c9a --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/subscription/ForbiddenTest.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.subscription; + +import static org.junit.Assert.assertEquals; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.Test; + +public class ForbiddenTest { + @Test + public void testBrokerRestart() throws Exception { + SubscriptionGroupManager s = new SubscriptionGroupManager( + new BrokerController(new BrokerConfig(), new NettyServerConfig(), new NettyClientConfig(), new MessageStoreConfig())); + s.updateForbidden("g", "t", 0, true); + assertEquals(1, s.getForbidden("g", "t")); + assertEquals(true, s.getForbidden("g", "t", 0)); + + s.updateForbidden("g", "t", 1, true); + assertEquals(3, s.getForbidden("g", "t")); + assertEquals(true, s.getForbidden("g", "t", 1)); + + s.updateForbidden("g", "t", 2, true); + assertEquals(7, s.getForbidden("g", "t")); + assertEquals(true, s.getForbidden("g", "t", 2)); + + s.updateForbidden("g", "t", 1, false); + assertEquals(5, s.getForbidden("g", "t")); + assertEquals(false, s.getForbidden("g", "t", 1)); + + s.updateForbidden("g", "t", 1, false); + assertEquals(5, s.getForbidden("g", "t")); + assertEquals(false, s.getForbidden("g", "t", 1)); + + s.updateForbidden("g", "t", 0, false); + assertEquals(4, s.getForbidden("g", "t")); + assertEquals(false, s.getForbidden("g", "t", 0)); + + s.updateForbidden("g", "t", 2, false); + assertEquals(0, s.getForbidden("g", "t")); + assertEquals(false, s.getForbidden("g", "t", 2)); + } + +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/subscription/SubscriptionGroupManagerTest.java b/broker/src/test/java/org/apache/rocketmq/broker/subscription/SubscriptionGroupManagerTest.java new file mode 100644 index 00000000000..3c829437cf1 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/subscription/SubscriptionGroupManagerTest.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.subscription; + +import com.google.common.collect.ImmutableMap; +import java.util.Map; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.SubscriptionGroupAttributes; +import org.apache.rocketmq.common.attribute.BooleanAttribute; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class SubscriptionGroupManagerTest { + private String group = "group"; + @Mock + private BrokerController brokerControllerMock; + private SubscriptionGroupManager subscriptionGroupManager; + + @Before + public void before() { + SubscriptionGroupAttributes.ALL.put("test", new BooleanAttribute( + "test", + false, + false + )); + subscriptionGroupManager = spy(new SubscriptionGroupManager(brokerControllerMock)); + when(brokerControllerMock.getMessageStore()).thenReturn(null); + doNothing().when(subscriptionGroupManager).persist(); + } + + @After + public void destroy() { + if (MixAll.isMac()) { + return; + } + if (subscriptionGroupManager != null) { + subscriptionGroupManager.stop(); + } + } + + @Test + public void testUpdateAndCreateSubscriptionGroupInRocksdb() { + if (MixAll.isMac()) { + return; + } + when(brokerControllerMock.getMessageStoreConfig()).thenReturn(new MessageStoreConfig()); + subscriptionGroupManager = spy(new RocksDBSubscriptionGroupManager(brokerControllerMock)); + subscriptionGroupManager.load(); + group += System.currentTimeMillis(); + updateSubscriptionGroupConfig(); + } + + @Test + public void updateSubscriptionGroupConfig() { + SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); + subscriptionGroupConfig.setGroupName(group); + Map attr = ImmutableMap.of("+test", "true"); + subscriptionGroupConfig.setAttributes(attr); + subscriptionGroupManager.updateSubscriptionGroupConfig(subscriptionGroupConfig); + SubscriptionGroupConfig result = subscriptionGroupManager.getSubscriptionGroupTable().get(group); + assertThat(result).isNotNull(); + assertThat(result.getGroupName()).isEqualTo(group); + assertThat(result.getAttributes().get("test")).isEqualTo("true"); + + + SubscriptionGroupConfig subscriptionGroupConfig1 = new SubscriptionGroupConfig(); + subscriptionGroupConfig1.setGroupName(group); + Map attrRemove = ImmutableMap.of("-test", ""); + subscriptionGroupConfig1.setAttributes(attrRemove); + assertThatThrownBy(() -> subscriptionGroupManager.updateSubscriptionGroupConfig(subscriptionGroupConfig1)) + .isInstanceOf(RuntimeException.class).hasMessage("attempt to update an unchangeable attribute. key: test"); + } +} \ No newline at end of file diff --git a/broker/src/test/java/org/apache/rocketmq/broker/topic/RocksdbTopicConfigManagerTest.java b/broker/src/test/java/org/apache/rocketmq/broker/topic/RocksdbTopicConfigManagerTest.java new file mode 100644 index 00000000000..ed71a3313a8 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/topic/RocksdbTopicConfigManagerTest.java @@ -0,0 +1,375 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.topic; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicAttributes; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.attribute.Attribute; +import org.apache.rocketmq.common.attribute.BooleanAttribute; +import org.apache.rocketmq.common.attribute.CQType; +import org.apache.rocketmq.common.attribute.EnumAttribute; +import org.apache.rocketmq.common.attribute.LongRangeAttribute; +import org.apache.rocketmq.common.utils.QueueTypeUtils; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static com.google.common.collect.Sets.newHashSet; +import static java.util.Arrays.asList; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class RocksdbTopicConfigManagerTest { + private RocksDBTopicConfigManager topicConfigManager; + @Mock + private BrokerController brokerController; + + @Mock + private DefaultMessageStore defaultMessageStore; + + @Before + public void init() { + if (notToBeExecuted()) { + return; + } + BrokerConfig brokerConfig = new BrokerConfig(); + when(brokerController.getBrokerConfig()).thenReturn(brokerConfig); + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + when(brokerController.getMessageStoreConfig()).thenReturn(messageStoreConfig); + when(brokerController.getMessageStore()).thenReturn(defaultMessageStore); + when(defaultMessageStore.getStateMachineVersion()).thenReturn(0L); + topicConfigManager = new RocksDBTopicConfigManager(brokerController); + topicConfigManager.load(); + } + + @After + public void destroy() { + if (notToBeExecuted()) { + return; + } + if (topicConfigManager != null) { + topicConfigManager.stop(); + } + } + + @Test + public void testAddUnsupportedKeyOnCreating() { + if (notToBeExecuted()) { + return; + } + String unsupportedKey = "key4"; + String topicName = "testAddUnsupportedKeyOnCreating-" + System.currentTimeMillis(); + + supportAttributes(asList( + new EnumAttribute("enum.key", true, newHashSet("enum-1", "enum-2", "enum-3"), "enum-1"), + new BooleanAttribute("bool.key", false, false), + new LongRangeAttribute("long.range.key", true, 10, 20, 15) + )); + + Map attributes = new HashMap<>(); + attributes.put("+enum.key", "enum-2"); + attributes.put("+" + unsupportedKey, "value1"); + + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName(topicName); + topicConfig.setAttributes(attributes); + + RuntimeException runtimeException = Assert.assertThrows(RuntimeException.class, () -> topicConfigManager.updateTopicConfig(topicConfig)); + Assert.assertEquals("unsupported key: " + unsupportedKey, runtimeException.getMessage()); + } + + @Test + public void testAddWrongFormatKeyOnCreating() { + if (notToBeExecuted()) { + return; + } + String topicName = "testAddWrongFormatKeyOnCreating-" + System.currentTimeMillis(); + + supportAttributes(asList( + new EnumAttribute("enum.key", true, newHashSet("enum-1", "enum-2", "enum-3"), "enum-1"), + new BooleanAttribute("bool.key", false, false), + new LongRangeAttribute("long.range.key", true, 10, 20, 15) + )); + + Map attributes = new HashMap<>(); + attributes.put("++enum.key", "value1"); + + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName(topicName); + topicConfig.setAttributes(attributes); + + RuntimeException runtimeException = Assert.assertThrows(RuntimeException.class, () -> topicConfigManager.updateTopicConfig(topicConfig)); + Assert.assertEquals("kv string format wrong.", runtimeException.getMessage()); + } + + @Test + public void testDeleteKeyOnCreating() { + if (notToBeExecuted()) { + return; + } + String topicName = "testDeleteKeyOnCreating-" + System.currentTimeMillis(); + + String key = "enum.key"; + supportAttributes(asList( + new EnumAttribute("enum.key", true, newHashSet("enum-1", "enum-2", "enum-3"), "enum-1"), + new BooleanAttribute("bool.key", false, false), + new LongRangeAttribute("long.range.key", true, 10, 20, 15) + )); + + Map attributes = new HashMap<>(); + attributes.put("-" + key, ""); + + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName(topicName); + topicConfig.setAttributes(attributes); + + RuntimeException runtimeException = Assert.assertThrows(RuntimeException.class, () -> topicConfigManager.updateTopicConfig(topicConfig)); + Assert.assertEquals("only add attribute is supported while creating topic. key: " + key, runtimeException.getMessage()); + } + + @Test + public void testAddWrongValueOnCreating() { + if (notToBeExecuted()) { + return; + } + String topicName = "testAddWrongValueOnCreating-" + System.currentTimeMillis(); + + Map attributes = new HashMap<>(); + attributes.put("+" + TopicAttributes.QUEUE_TYPE_ATTRIBUTE.getName(), "wrong-value"); + + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName(topicName); + topicConfig.setAttributes(attributes); + + RuntimeException runtimeException = Assert.assertThrows(RuntimeException.class, () -> topicConfigManager.updateTopicConfig(topicConfig)); + Assert.assertEquals("value is not in set: [SimpleCQ, BatchCQ]", runtimeException.getMessage()); + } + + @Test + public void testNormalAddKeyOnCreating() { + if (notToBeExecuted()) { + return; + } + String topic = "testNormalAddKeyOnCreating-" + System.currentTimeMillis(); + + supportAttributes(asList( + new EnumAttribute("enum.key", true, newHashSet("enum-1", "enum-2", "enum-3"), "enum-1"), + new BooleanAttribute("bool.key", false, false), + new LongRangeAttribute("long.range.key", true, 10, 20, 15) + )); + + Map attributes = new HashMap<>(); + attributes.put("+enum.key", "enum-2"); + attributes.put("+long.range.key", "16"); + + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName(topic); + topicConfig.setAttributes(attributes); + topicConfigManager.updateTopicConfig(topicConfig); + + TopicConfig existingTopicConfig = topicConfigManager.getTopicConfigTable().get(topic); + Assert.assertEquals("enum-2", existingTopicConfig.getAttributes().get("enum.key")); + Assert.assertEquals("16", existingTopicConfig.getAttributes().get("long.range.key")); + // assert file + } + + @Test + public void testAddDuplicatedKeyOnUpdating() { + if (notToBeExecuted()) { + return; + } + String duplicatedKey = "long.range.key"; + String topicName = "testAddDuplicatedKeyOnUpdating-" + System.currentTimeMillis(); + + supportAttributes(asList( + new EnumAttribute("enum.key", true, newHashSet("enum-1", "enum-2", "enum-3"), "enum-1"), + new BooleanAttribute("bool.key", false, false), + new LongRangeAttribute("long.range.key", true, 10, 20, 15) + )); + + Map attributes = new HashMap<>(); + attributes.put("+enum.key", "enum-3"); + attributes.put("+bool.key", "true"); + attributes.put("+long.range.key", "12"); + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName(topicName); + topicConfig.setAttributes(attributes); + topicConfigManager.updateTopicConfig(topicConfig); + + + + attributes = new HashMap<>(); + attributes.put("+" + duplicatedKey, "11"); + attributes.put("-" + duplicatedKey, ""); + TopicConfig duplicateTopicConfig = new TopicConfig(); + duplicateTopicConfig.setTopicName(topicName); + duplicateTopicConfig.setAttributes(attributes); + + RuntimeException runtimeException = Assert.assertThrows(RuntimeException.class, () -> topicConfigManager.updateTopicConfig(duplicateTopicConfig)); + Assert.assertEquals("alter duplication key. key: " + duplicatedKey, runtimeException.getMessage()); + } + + @Test + public void testDeleteNonexistentKeyOnUpdating() { + if (notToBeExecuted()) { + return; + } + String key = "nonexisting.key"; + String topicName = "testDeleteNonexistentKeyOnUpdating-" + System.currentTimeMillis(); + + supportAttributes(asList( + new EnumAttribute("enum.key", true, newHashSet("enum-1", "enum-2", "enum-3"), "enum-1"), + new BooleanAttribute("bool.key", false, false), + new LongRangeAttribute("long.range.key", true, 10, 20, 15) + )); + + Map attributes = new HashMap<>(); + attributes.put("+enum.key", "enum-2"); + attributes.put("+bool.key", "true"); + + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName(topicName); + topicConfig.setAttributes(attributes); + + topicConfigManager.updateTopicConfig(topicConfig); + + attributes = new HashMap<>(); + attributes.clear(); + attributes.put("-" + key, ""); + topicConfig.setAttributes(attributes); + RuntimeException runtimeException = Assert.assertThrows(RuntimeException.class, () -> topicConfigManager.updateTopicConfig(topicConfig)); + Assert.assertEquals("attempt to delete a nonexistent key: " + key, runtimeException.getMessage()); + } + + @Test + public void testAlterTopicWithoutChangingAttributes() { + if (notToBeExecuted()) { + return; + } + String topic = "testAlterTopicWithoutChangingAttributes-" + System.currentTimeMillis(); + + supportAttributes(asList( + new EnumAttribute("enum.key", true, newHashSet("enum-1", "enum-2", "enum-3"), "enum-1"), + new BooleanAttribute("bool.key", false, false), + new LongRangeAttribute("long.range.key", true, 10, 20, 15) + )); + + Map attributes = new HashMap<>(); + attributes.put("+enum.key", "enum-2"); + attributes.put("+bool.key", "true"); + + TopicConfig topicConfigInit = new TopicConfig(); + topicConfigInit.setTopicName(topic); + topicConfigInit.setAttributes(attributes); + + topicConfigManager.updateTopicConfig(topicConfigInit); + Assert.assertEquals("enum-2", topicConfigManager.getTopicConfigTable().get(topic).getAttributes().get("enum.key")); + Assert.assertEquals("true", topicConfigManager.getTopicConfigTable().get(topic).getAttributes().get("bool.key")); + + TopicConfig topicConfigAlter = new TopicConfig(); + topicConfigAlter.setTopicName(topic); + topicConfigAlter.setReadQueueNums(10); + topicConfigAlter.setWriteQueueNums(10); + topicConfigManager.updateTopicConfig(topicConfigAlter); + Assert.assertEquals("enum-2", topicConfigManager.getTopicConfigTable().get(topic).getAttributes().get("enum.key")); + Assert.assertEquals("true", topicConfigManager.getTopicConfigTable().get(topic).getAttributes().get("bool.key")); + } + + @Test + public void testNormalUpdateUnchangeableKeyOnUpdating() { + if (notToBeExecuted()) { + return; + } + String topic = "testNormalUpdateUnchangeableKeyOnUpdating-" + System.currentTimeMillis(); + + supportAttributes(asList( + new EnumAttribute("enum.key", true, newHashSet("enum-1", "enum-2", "enum-3"), "enum-1"), + new BooleanAttribute("bool.key", true, false), + new LongRangeAttribute("long.range.key", false, 10, 20, 15) + )); + + Map attributes = new HashMap<>(); + attributes.put("+long.range.key", "14"); + + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName(topic); + topicConfig.setAttributes(attributes); + + topicConfigManager.updateTopicConfig(topicConfig); + + attributes.put("+long.range.key", "16"); + topicConfig.setAttributes(attributes); + RuntimeException runtimeException = Assert.assertThrows(RuntimeException.class, () -> topicConfigManager.updateTopicConfig(topicConfig)); + Assert.assertEquals("attempt to update an unchangeable attribute. key: long.range.key", runtimeException.getMessage()); + } + + @Test + public void testNormalQueryKeyOnGetting() { + if (notToBeExecuted()) { + return; + } + String topic = "testNormalQueryKeyOnGetting-" + System.currentTimeMillis(); + String unchangeable = "bool.key"; + + supportAttributes(asList( + new EnumAttribute("enum.key", true, newHashSet("enum-1", "enum-2", "enum-3"), "enum-1"), + new BooleanAttribute("bool.key", false, false), + new LongRangeAttribute("long.range.key", true, 10, 20, 15) + )); + + Map attributes = new HashMap<>(); + attributes.put("+" + unchangeable, "true"); + + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName(topic); + topicConfig.setAttributes(attributes); + + topicConfigManager.updateTopicConfig(topicConfig); + + TopicConfig topicConfigUpdated = topicConfigManager.getTopicConfigTable().get(topic); + Assert.assertEquals(CQType.SimpleCQ, QueueTypeUtils.getCQType(Optional.of(topicConfigUpdated))); + + Assert.assertEquals("true", topicConfigUpdated.getAttributes().get(unchangeable)); + } + + private void supportAttributes(List supportAttributes) { + Map supportedAttributes = new HashMap<>(); + + for (Attribute supportAttribute : supportAttributes) { + supportedAttributes.put(supportAttribute.getName(), supportAttribute); + } + + TopicAttributes.ALL.putAll(supportedAttributes); + } + + private boolean notToBeExecuted() { + return MixAll.isMac(); + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/topic/TopicConfigManagerTest.java b/broker/src/test/java/org/apache/rocketmq/broker/topic/TopicConfigManagerTest.java new file mode 100644 index 00000000000..6052a79d413 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/topic/TopicConfigManagerTest.java @@ -0,0 +1,321 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.topic; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.TopicAttributes; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.attribute.Attribute; +import org.apache.rocketmq.common.attribute.BooleanAttribute; +import org.apache.rocketmq.common.attribute.CQType; +import org.apache.rocketmq.common.attribute.EnumAttribute; +import org.apache.rocketmq.common.attribute.LongRangeAttribute; +import org.apache.rocketmq.common.utils.QueueTypeUtils; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static com.google.common.collect.Sets.newHashSet; +import static java.util.Arrays.asList; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class TopicConfigManagerTest { + private TopicConfigManager topicConfigManager; + @Mock + private BrokerController brokerController; + + @Mock + private DefaultMessageStore defaultMessageStore; + + @Before + public void init() { + BrokerConfig brokerConfig = new BrokerConfig(); + when(brokerController.getBrokerConfig()).thenReturn(brokerConfig); + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + when(brokerController.getMessageStoreConfig()).thenReturn(messageStoreConfig); + when(brokerController.getMessageStore()).thenReturn(defaultMessageStore); + when(defaultMessageStore.getStateMachineVersion()).thenReturn(0L); + topicConfigManager = new TopicConfigManager(brokerController); + } + + @Test + public void testAddUnsupportedKeyOnCreating() { + String unsupportedKey = "key4"; + + supportAttributes(asList( + new EnumAttribute("enum.key", true, newHashSet("enum-1", "enum-2", "enum-3"), "enum-1"), + new BooleanAttribute("bool.key", false, false), + new LongRangeAttribute("long.range.key", true, 10, 20, 15) + )); + + Map attributes = new HashMap<>(); + attributes.put("+enum.key", "enum-2"); + attributes.put("+" + unsupportedKey, "value1"); + + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName("new-topic"); + topicConfig.setAttributes(attributes); + + RuntimeException runtimeException = Assert.assertThrows(RuntimeException.class, () -> topicConfigManager.updateTopicConfig(topicConfig)); + Assert.assertEquals("unsupported key: " + unsupportedKey, runtimeException.getMessage()); + } + + @Test + public void testAddWrongFormatKeyOnCreating() { + supportAttributes(asList( + new EnumAttribute("enum.key", true, newHashSet("enum-1", "enum-2", "enum-3"), "enum-1"), + new BooleanAttribute("bool.key", false, false), + new LongRangeAttribute("long.range.key", true, 10, 20, 15) + )); + + Map attributes = new HashMap<>(); + attributes.put("++enum.key", "value1"); + + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName("new-topic"); + topicConfig.setAttributes(attributes); + + RuntimeException runtimeException = Assert.assertThrows(RuntimeException.class, () -> topicConfigManager.updateTopicConfig(topicConfig)); + Assert.assertEquals("kv string format wrong.", runtimeException.getMessage()); + } + + @Test + public void testDeleteKeyOnCreating() { + String key = "enum.key"; + supportAttributes(asList( + new EnumAttribute("enum.key", true, newHashSet("enum-1", "enum-2", "enum-3"), "enum-1"), + new BooleanAttribute("bool.key", false, false), + new LongRangeAttribute("long.range.key", true, 10, 20, 15) + )); + + Map attributes = new HashMap<>(); + attributes.put("-" + key, ""); + + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName("new-topic"); + topicConfig.setAttributes(attributes); + + RuntimeException runtimeException = Assert.assertThrows(RuntimeException.class, () -> topicConfigManager.updateTopicConfig(topicConfig)); + Assert.assertEquals("only add attribute is supported while creating topic. key: " + key, runtimeException.getMessage()); + } + + @Test + public void testAddWrongValueOnCreating() { + Map attributes = new HashMap<>(); + attributes.put("+" + TopicAttributes.QUEUE_TYPE_ATTRIBUTE.getName(), "wrong-value"); + + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName("new-topic"); + topicConfig.setAttributes(attributes); + + RuntimeException runtimeException = Assert.assertThrows(RuntimeException.class, () -> topicConfigManager.updateTopicConfig(topicConfig)); + Assert.assertEquals("value is not in set: [SimpleCQ, BatchCQ]", runtimeException.getMessage()); + } + + @Test + public void testNormalAddKeyOnCreating() { + String topic = "new-topic"; + + supportAttributes(asList( + new EnumAttribute("enum.key", true, newHashSet("enum-1", "enum-2", "enum-3"), "enum-1"), + new BooleanAttribute("bool.key", false, false), + new LongRangeAttribute("long.range.key", true, 10, 20, 15) + )); + + Map attributes = new HashMap<>(); + attributes.put("+enum.key", "enum-2"); + attributes.put("+long.range.key", "16"); + + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName(topic); + topicConfig.setAttributes(attributes); + topicConfigManager.updateTopicConfig(topicConfig); + + TopicConfig existingTopicConfig = topicConfigManager.getTopicConfigTable().get(topic); + Assert.assertEquals("enum-2", existingTopicConfig.getAttributes().get("enum.key")); + Assert.assertEquals("16", existingTopicConfig.getAttributes().get("long.range.key")); +// assert file + } + + @Test + public void testAddDuplicatedKeyOnUpdating() { + String duplicatedKey = "long.range.key"; + + supportAttributes(asList( + new EnumAttribute("enum.key", true, newHashSet("enum-1", "enum-2", "enum-3"), "enum-1"), + new BooleanAttribute("bool.key", false, false), + new LongRangeAttribute("long.range.key", true, 10, 20, 15) + )); + + createTopic(); + + Map attributes = new HashMap<>(); + attributes.put("+" + duplicatedKey, "11"); + attributes.put("-" + duplicatedKey, ""); + + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName("new-topic"); + topicConfig.setAttributes(attributes); + + RuntimeException runtimeException = Assert.assertThrows(RuntimeException.class, () -> topicConfigManager.updateTopicConfig(topicConfig)); + Assert.assertEquals("alter duplication key. key: " + duplicatedKey, runtimeException.getMessage()); + } + + private void createTopic() { + Map attributes = new HashMap<>(); + attributes.put("+enum.key", "enum-3"); + attributes.put("+bool.key", "true"); + attributes.put("+long.range.key", "12"); + + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName("new-topic"); + topicConfig.setAttributes(attributes); + + topicConfigManager.updateTopicConfig(topicConfig); + } + + @Test + public void testDeleteNonexistentKeyOnUpdating() { + String key = "nonexisting.key"; + + supportAttributes(asList( + new EnumAttribute("enum.key", true, newHashSet("enum-1", "enum-2", "enum-3"), "enum-1"), + new BooleanAttribute("bool.key", false, false), + new LongRangeAttribute("long.range.key", true, 10, 20, 15) + )); + + Map attributes = new HashMap<>(); + attributes.put("+enum.key", "enum-2"); + attributes.put("+bool.key", "true"); + + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName("new-topic"); + topicConfig.setAttributes(attributes); + + topicConfigManager.updateTopicConfig(topicConfig); + + attributes = new HashMap<>(); + attributes.clear(); + attributes.put("-" + key, ""); + topicConfig.setAttributes(attributes); + RuntimeException runtimeException = Assert.assertThrows(RuntimeException.class, () -> topicConfigManager.updateTopicConfig(topicConfig)); + Assert.assertEquals("attempt to delete a nonexistent key: " + key, runtimeException.getMessage()); + } + + @Test + public void testAlterTopicWithoutChangingAttributes() { + String topic = "new-topic"; + + supportAttributes(asList( + new EnumAttribute("enum.key", true, newHashSet("enum-1", "enum-2", "enum-3"), "enum-1"), + new BooleanAttribute("bool.key", false, false), + new LongRangeAttribute("long.range.key", true, 10, 20, 15) + )); + + Map attributes = new HashMap<>(); + attributes.put("+enum.key", "enum-2"); + attributes.put("+bool.key", "true"); + + TopicConfig topicConfigInit = new TopicConfig(); + topicConfigInit.setTopicName(topic); + topicConfigInit.setAttributes(attributes); + + topicConfigManager.updateTopicConfig(topicConfigInit); + Assert.assertEquals("enum-2", topicConfigManager.getTopicConfigTable().get(topic).getAttributes().get("enum.key")); + Assert.assertEquals("true", topicConfigManager.getTopicConfigTable().get(topic).getAttributes().get("bool.key")); + + TopicConfig topicConfigAlter = new TopicConfig(); + topicConfigAlter.setTopicName(topic); + topicConfigAlter.setReadQueueNums(10); + topicConfigAlter.setWriteQueueNums(10); + topicConfigManager.updateTopicConfig(topicConfigAlter); + Assert.assertEquals("enum-2", topicConfigManager.getTopicConfigTable().get(topic).getAttributes().get("enum.key")); + Assert.assertEquals("true", topicConfigManager.getTopicConfigTable().get(topic).getAttributes().get("bool.key")); + } + + @Test + public void testNormalUpdateUnchangeableKeyOnUpdating() { + String topic = "exist-topic"; + + supportAttributes(asList( + new EnumAttribute("enum.key", true, newHashSet("enum-1", "enum-2", "enum-3"), "enum-1"), + new BooleanAttribute("bool.key", true, false), + new LongRangeAttribute("long.range.key", false, 10, 20, 15) + )); + + Map attributes = new HashMap<>(); + attributes.put("+long.range.key", "14"); + + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName(topic); + topicConfig.setAttributes(attributes); + + topicConfigManager.updateTopicConfig(topicConfig); + + attributes.put("+long.range.key", "16"); + topicConfig.setAttributes(attributes); + RuntimeException runtimeException = Assert.assertThrows(RuntimeException.class, () -> topicConfigManager.updateTopicConfig(topicConfig)); + Assert.assertEquals("attempt to update an unchangeable attribute. key: long.range.key", runtimeException.getMessage()); + } + + @Test + public void testNormalQueryKeyOnGetting() { + String topic = "exist-topic"; + String unchangeable = "bool.key"; + + supportAttributes(asList( + new EnumAttribute("enum.key", true, newHashSet("enum-1", "enum-2", "enum-3"), "enum-1"), + new BooleanAttribute("bool.key", false, false), + new LongRangeAttribute("long.range.key", true, 10, 20, 15) + )); + + Map attributes = new HashMap<>(); + attributes.put("+" + unchangeable, "true"); + + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName(topic); + topicConfig.setAttributes(attributes); + + topicConfigManager.updateTopicConfig(topicConfig); + + TopicConfig topicConfigUpdated = topicConfigManager.getTopicConfigTable().get(topic); + Assert.assertEquals(CQType.SimpleCQ, QueueTypeUtils.getCQType(Optional.of(topicConfigUpdated))); + + Assert.assertEquals("true", topicConfigUpdated.getAttributes().get(unchangeable)); + } + + private void supportAttributes(List supportAttributes) { + Map supportedAttributes = new HashMap<>(); + + for (Attribute supportAttribute : supportAttributes) { + supportedAttributes.put(supportAttribute.getName(), supportAttribute); + } + + TopicAttributes.ALL.putAll(supportedAttributes); + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/topic/TopicQueueMappingManagerTest.java b/broker/src/test/java/org/apache/rocketmq/broker/topic/TopicQueueMappingManagerTest.java new file mode 100644 index 00000000000..b74e57ab936 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/topic/TopicQueueMappingManagerTest.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.topic; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingDetail; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingUtils; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicRemappingDetailWrapper; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class TopicQueueMappingManagerTest { + @Mock + private BrokerController brokerController; + private static final String BROKER1_NAME = "broker1"; + + @Before + public void before() { + BrokerConfig brokerConfig = new BrokerConfig(); + brokerConfig.setBrokerName(BROKER1_NAME); + when(brokerController.getBrokerConfig()).thenReturn(brokerConfig); + + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setStorePathRootDir(System.getProperty("java.io.tmpdir")); + messageStoreConfig.setDeleteWhen("01;02;03;04;05;06;07;08;09;10;11;12;13;14;15;16;17;18;19;20;21;22;23;00"); + when(brokerController.getMessageStoreConfig()).thenReturn(messageStoreConfig); + } + + + private void delete(TopicQueueMappingManager topicQueueMappingManager) throws Exception { + if (topicQueueMappingManager == null) { + return; + } + Files.deleteIfExists(Paths.get(topicQueueMappingManager.configFilePath())); + Files.deleteIfExists(Paths.get(topicQueueMappingManager.configFilePath() + ".bak")); + + + } + + @Test + public void testEncodeDecode() throws Exception { + Map mappingDetailMap = new HashMap<>(); + TopicQueueMappingManager topicQueueMappingManager = null; + Set brokers = new HashSet<>(); + brokers.add(BROKER1_NAME); + { + for (int i = 0; i < 10; i++) { + String topic = UUID.randomUUID().toString(); + int queueNum = 10; + TopicRemappingDetailWrapper topicRemappingDetailWrapper = TopicQueueMappingUtils.createTopicConfigMapping(topic, queueNum, brokers, new HashMap<>()); + Assert.assertEquals(1, topicRemappingDetailWrapper.getBrokerConfigMap().size()); + TopicQueueMappingDetail topicQueueMappingDetail = topicRemappingDetailWrapper.getBrokerConfigMap().values().iterator().next().getMappingDetail(); + Assert.assertEquals(queueNum, topicQueueMappingDetail.getHostedQueues().size()); + mappingDetailMap.put(topic, topicQueueMappingDetail); + } + } + + { + topicQueueMappingManager = new TopicQueueMappingManager(brokerController); + Assert.assertTrue(topicQueueMappingManager.load()); + Assert.assertEquals(0, topicQueueMappingManager.getTopicQueueMappingTable().size()); + for (TopicQueueMappingDetail mappingDetail : mappingDetailMap.values()) { + for (int i = 0; i < 10; i++) { + topicQueueMappingManager.updateTopicQueueMapping(mappingDetail, false, false, true); + } + } + topicQueueMappingManager.persist(); + } + + { + topicQueueMappingManager = new TopicQueueMappingManager(brokerController); + Assert.assertTrue(topicQueueMappingManager.load()); + Assert.assertEquals(mappingDetailMap.size(), topicQueueMappingManager.getTopicQueueMappingTable().size()); + for (TopicQueueMappingDetail topicQueueMappingDetail: topicQueueMappingManager.getTopicQueueMappingTable().values()) { + Assert.assertEquals(topicQueueMappingDetail, mappingDetailMap.get(topicQueueMappingDetail.getTopic())); + } + } + delete(topicQueueMappingManager); + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/transaction/queue/DefaultTransactionalMessageCheckListenerTest.java b/broker/src/test/java/org/apache/rocketmq/broker/transaction/queue/DefaultTransactionalMessageCheckListenerTest.java new file mode 100644 index 00000000000..986b15aa098 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/transaction/queue/DefaultTransactionalMessageCheckListenerTest.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.transaction.queue; + +import java.net.InetSocketAddress; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class DefaultTransactionalMessageCheckListenerTest { + + private DefaultTransactionalMessageCheckListener listener; + @Mock + private MessageStore messageStore; + + @Spy + private BrokerController brokerController = new BrokerController(new BrokerConfig(), + new NettyServerConfig(), + new NettyClientConfig(), new MessageStoreConfig()); + + @Before + public void init() throws Exception { + listener = new DefaultTransactionalMessageCheckListener(); + listener.setBrokerController(brokerController); + brokerController.setMessageStore(messageStore); + + } + + @After + public void destroy() { +// brokerController.shutdown(); + } + + @Test + public void testResolveHalfMsg() { + listener.resolveHalfMsg(createMessageExt()); + } + + @Test + public void testSendCheckMessage() throws Exception { + MessageExt messageExt = createMessageExt(); + listener.sendCheckMessage(messageExt); + } + + @Test + public void sendCheckMessage() { + listener.resolveDiscardMsg(createMessageExt()); + } + + private MessageExtBrokerInner createMessageExt() { + MessageExtBrokerInner inner = new MessageExtBrokerInner(); + MessageAccessor.putProperty(inner, MessageConst.PROPERTY_REAL_QUEUE_ID, "1"); + MessageAccessor.putProperty(inner, MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, "1234255"); + MessageAccessor.putProperty(inner, MessageConst.PROPERTY_REAL_TOPIC, "realTopic"); + inner.setTransactionId(inner.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX)); + inner.setBody("check".getBytes()); + inner.setMsgId("12344567890"); + inner.setQueueId(0); + return inner; + } + + @Test + public void testResolveDiscardMsg() { + MessageExt messageExt = new MessageExt(); + messageExt.setTopic(TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC); + messageExt.setQueueId(0); + messageExt.setBody("test resolve discard msg".getBytes()); + messageExt.setStoreHost(new InetSocketAddress("127.0.0.1", 10911)); + messageExt.setBornHost(new InetSocketAddress("127.0.0.1", 54270)); + MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_REAL_TOPIC, "test_topic"); + MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_PRODUCER_GROUP, "PID_TEST_DISCARD_MSG"); + MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true"); + MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_TRANSACTION_CHECK_TIMES, "15"); + MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_REAL_QUEUE_ID, "2"); + MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_TAGS, "test_discard_msg"); + MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, "AC14157E4F1C18B4AAC27EB1A0F30000"); + listener.resolveDiscardMsg(messageExt); + } + +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/transaction/queue/TransactionMetricsTest.java b/broker/src/test/java/org/apache/rocketmq/broker/transaction/queue/TransactionMetricsTest.java new file mode 100644 index 00000000000..690b4eabb57 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/transaction/queue/TransactionMetricsTest.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.transaction.queue; + +import org.apache.rocketmq.broker.transaction.TransactionMetrics; +import org.apache.rocketmq.broker.transaction.TransactionMetrics.Metric; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Collections; + + +@RunWith(MockitoJUnitRunner.class) +public class TransactionMetricsTest { + private TransactionMetrics transactionMetrics; + private String configPath; + + @Before + public void setUp() throws Exception { + configPath = "configPath"; + transactionMetrics = new TransactionMetrics(configPath); + } + + /** + * test addAndGet method + */ + @Test + public void testAddAndGet() { + String topic = "testAddAndGet"; + int value = 10; + long result = transactionMetrics.addAndGet(topic, value); + + assert result == value; + } + + @Test + public void testGetTopicPair() { + String topic = "getTopicPair"; + Metric result = transactionMetrics.getTopicPair(topic); + assert result != null; + } + + @Test + public void testGetTransactionCount() { + String topicExist = "topicExist"; + String topicNotExist = "topicNotExist"; + + transactionMetrics.addAndGet(topicExist, 10); + + assert transactionMetrics.getTransactionCount(topicExist) == 10; + assert transactionMetrics.getTransactionCount(topicNotExist) == 0; + } + + + /** + * test clean metrics + */ + @Test + public void testCleanMetrics() { + String topic = "testCleanMetrics"; + int value = 10; + assert transactionMetrics.addAndGet(topic, value) == value; + transactionMetrics.cleanMetrics(Collections.singleton(topic)); + assert transactionMetrics.getTransactionCount(topic) == 0; + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageBridgeTest.java b/broker/src/test/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageBridgeTest.java new file mode 100644 index 00000000000..e01182fcbbe --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageBridgeTest.java @@ -0,0 +1,217 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.transaction.queue; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.consumer.PullStatus; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.store.AppendMessageResult; +import org.apache.rocketmq.store.AppendMessageStatus; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.GetMessageStatus; +import org.apache.rocketmq.store.MessageFilter; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class TransactionalMessageBridgeTest { + + private TransactionalMessageBridge transactionBridge; + + @Spy + private BrokerController brokerController = new BrokerController(new BrokerConfig(), new NettyServerConfig(), + new NettyClientConfig(), new MessageStoreConfig()); + + @Mock + private MessageStore messageStore; + + @Before + public void init() { + brokerController.setMessageStore(messageStore); + transactionBridge = new TransactionalMessageBridge(brokerController, messageStore); + } + + @Test + public void testPutOpMessage() { + when(messageStore.putMessage(any(MessageExtBrokerInner.class))).thenReturn( + new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK))); + boolean isSuccess = transactionBridge.writeOp(0, createMessageBrokerInner()); + assertThat(isSuccess).isTrue(); + } + + @Test + public void testPutHalfMessage() { + when(messageStore.putMessage(any(MessageExtBrokerInner.class))) + .thenReturn(new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK))); + PutMessageResult result = transactionBridge.putHalfMessage(createMessageBrokerInner()); + assertThat(result.getPutMessageStatus()).isEqualTo(PutMessageStatus.PUT_OK); + } + + @Test + public void testAsyncPutHalfMessage() throws Exception { + when(messageStore.asyncPutMessage(any(MessageExtBrokerInner.class))) + .thenReturn(CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK)))); + CompletableFuture result = transactionBridge.asyncPutHalfMessage(createMessageBrokerInner()); + assertThat(result.get().getPutMessageStatus()).isEqualTo(PutMessageStatus.PUT_OK); + } + + @Test + public void testFetchMessageQueues() { + Set messageQueues = transactionBridge.fetchMessageQueues(TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC); + assertThat(messageQueues.size()).isEqualTo(1); + } + + @Test + public void testFetchConsumeOffset() { + MessageQueue mq = new MessageQueue(TransactionalMessageUtil.buildOpTopic(), this.brokerController.getBrokerConfig().getBrokerName(), + 0); + long offset = transactionBridge.fetchConsumeOffset(mq); + assertThat(offset).isGreaterThan(-1); + } + + @Test + public void updateConsumeOffset() { + MessageQueue mq = new MessageQueue(TransactionalMessageUtil.buildOpTopic(), this.brokerController.getBrokerConfig().getBrokerName(), + 0); + transactionBridge.updateConsumeOffset(mq, 0); + } + + @Test + public void testGetHalfMessage() { + when(messageStore.getMessage(anyString(), anyString(), anyInt(), anyLong(), anyInt(), ArgumentMatchers.nullable(MessageFilter.class))).thenReturn(createGetMessageResult(GetMessageStatus.NO_MESSAGE_IN_QUEUE)); + PullResult result = transactionBridge.getHalfMessage(0, 0, 1); + assertThat(result.getPullStatus()).isEqualTo(PullStatus.NO_NEW_MSG); + } + + @Test + public void testGetOpMessage() { + when(messageStore.getMessage(anyString(), anyString(), anyInt(), anyLong(), anyInt(), ArgumentMatchers.nullable(MessageFilter.class))).thenReturn(createGetMessageResult(GetMessageStatus.NO_MESSAGE_IN_QUEUE)); + PullResult result = transactionBridge.getOpMessage(0, 0, 1); + assertThat(result.getPullStatus()).isEqualTo(PullStatus.NO_NEW_MSG); + } + + @Test + public void testPutMessageReturnResult() { + when(messageStore.putMessage(any(MessageExtBrokerInner.class))) + .thenReturn(new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK))); + PutMessageResult result = transactionBridge.putMessageReturnResult(createMessageBrokerInner()); + assertThat(result.getPutMessageStatus()).isEqualTo(PutMessageStatus.PUT_OK); + } + + @Test + public void testPutMessage() { + when(messageStore.putMessage(any(MessageExtBrokerInner.class))) + .thenReturn(new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK))); + Boolean success = transactionBridge.putMessage(createMessageBrokerInner()); + assertThat(success).isEqualTo(true); + } + + @Test + public void testRenewImmunityHalfMessageInner() { + MessageExt messageExt = createMessageBrokerInner(); + final String offset = "123456789"; + MessageExtBrokerInner msgInner = transactionBridge.renewImmunityHalfMessageInner(messageExt); + MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_TRANSACTION_PREPARED_QUEUE_OFFSET,offset); + assertThat(msgInner).isNotNull(); + Map properties = msgInner.getProperties(); + assertThat(properties).isNotNull(); + String resOffset = properties.get(MessageConst.PROPERTY_TRANSACTION_PREPARED_QUEUE_OFFSET); + assertThat(resOffset).isEqualTo(offset); + } + + + @Test + public void testRenewHalfMessageInner() { + MessageExt messageExt = new MessageExt(); + long bornTimeStamp = messageExt.getBornTimestamp(); + MessageExt messageExtRes = transactionBridge.renewHalfMessageInner(messageExt); + assertThat(messageExtRes.getBornTimestamp()).isEqualTo(bornTimeStamp); + } + + @Test + public void testLookMessageByOffset() { + when(messageStore.lookMessageByOffset(anyLong())).thenReturn(new MessageExt()); + MessageExt messageExt = transactionBridge.lookMessageByOffset(123); + assertThat(messageExt).isNotNull(); + } + + @Test + public void testGetHalfMessageStatusFound() { + when(messageStore + .getMessage(anyString(), anyString(), anyInt(), anyLong(), anyInt(), ArgumentMatchers.nullable(MessageFilter.class))) + .thenReturn(createGetMessageResult(GetMessageStatus.FOUND)); + PullResult result = transactionBridge.getHalfMessage(0, 0, 1); + assertThat(result.getPullStatus()).isEqualTo(PullStatus.FOUND); + } + + @Test + public void testGetHalfMessageNull() { + when(messageStore + .getMessage(anyString(), anyString(), anyInt(), anyLong(), anyInt(), ArgumentMatchers.nullable(MessageFilter.class))) + .thenReturn(null); + PullResult result = transactionBridge.getHalfMessage(0, 0, 1); + assertThat(result).isNull(); + } + + private GetMessageResult createGetMessageResult(GetMessageStatus status) { + GetMessageResult getMessageResult = new GetMessageResult(); + getMessageResult.setStatus(status); + getMessageResult.setMinOffset(100); + getMessageResult.setMaxOffset(1024); + getMessageResult.setNextBeginOffset(516); + return getMessageResult; + } + + private MessageExtBrokerInner createMessageBrokerInner() { + MessageExtBrokerInner inner = new MessageExtBrokerInner(); + inner.setTransactionId("12342123444"); + inner.setBornTimestamp(System.currentTimeMillis()); + inner.setBody("prepare".getBytes()); + inner.setMsgId("123456-123"); + inner.setQueueId(0); + inner.setTopic("hello"); + return inner; + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageServiceImplTest.java b/broker/src/test/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageServiceImplTest.java new file mode 100644 index 00000000000..2a63774555e --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageServiceImplTest.java @@ -0,0 +1,265 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.transaction.queue; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.transaction.AbstractTransactionalMessageCheckListener; +import org.apache.rocketmq.broker.transaction.OperationResult; +import org.apache.rocketmq.broker.transaction.TransactionalMessageService; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.consumer.PullStatus; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.EndTransactionRequestHeader; +import org.apache.rocketmq.store.AppendMessageResult; +import org.apache.rocketmq.store.AppendMessageStatus; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class TransactionalMessageServiceImplTest { + + private TransactionalMessageService queueTransactionMsgService; + + @Mock + private TransactionalMessageBridge bridge; + + @Spy + private BrokerController brokerController = new BrokerController(new BrokerConfig(), new NettyServerConfig(), + new NettyClientConfig(), new MessageStoreConfig()); + + @Mock + private AbstractTransactionalMessageCheckListener listener; + + @Before + public void init() { + when(bridge.getBrokerController()).thenReturn(brokerController); + listener.setBrokerController(brokerController); + queueTransactionMsgService = new TransactionalMessageServiceImpl(bridge); + } + + @Test + public void testPrepareMessage() { + MessageExtBrokerInner inner = createMessageBrokerInner(); + when(bridge.putHalfMessage(any(MessageExtBrokerInner.class))) + .thenReturn(new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK))); + PutMessageResult result = queueTransactionMsgService.prepareMessage(inner); + assert result.isOk(); + } + + @Test + public void testCommitMessage() { + when(bridge.lookMessageByOffset(anyLong())).thenReturn(createMessageBrokerInner()); + OperationResult result = queueTransactionMsgService.commitMessage(createEndTransactionRequestHeader(MessageSysFlag.TRANSACTION_COMMIT_TYPE)); + assertThat(result.getResponseCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testRollbackMessage() { + when(bridge.lookMessageByOffset(anyLong())).thenReturn(createMessageBrokerInner()); + OperationResult result = queueTransactionMsgService.commitMessage(createEndTransactionRequestHeader(MessageSysFlag.TRANSACTION_ROLLBACK_TYPE)); + assertThat(result.getResponseCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testCheck_withDiscard() { + when(bridge.fetchMessageQueues(TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC)).thenReturn(createMessageQueueSet(TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC)); + when(bridge.getHalfMessage(0, 0, 1)).thenReturn(createDiscardPullResult(TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC, 5, "hellp", 1)); + when(bridge.getHalfMessage(0, 1, 1)).thenReturn(createPullResult(TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC, 6, "hellp", 0)); + when(bridge.getOpMessage(anyInt(), anyLong(), anyInt())).thenReturn(createOpPulResult(TopicValidator.RMQ_SYS_TRANS_OP_HALF_TOPIC, 1, "10", 1)); + when(bridge.getBrokerController()).thenReturn(this.brokerController); + long timeOut = this.brokerController.getBrokerConfig().getTransactionTimeOut(); + int checkMax = this.brokerController.getBrokerConfig().getTransactionCheckMax(); + final AtomicInteger checkMessage = new AtomicInteger(0); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) { + checkMessage.addAndGet(1); + return null; + } + }).when(listener).resolveDiscardMsg(any(MessageExt.class)); + queueTransactionMsgService.check(timeOut, checkMax, listener); + assertThat(checkMessage.get()).isEqualTo(1); + } + + @Test + public void testCheck_withCheck() { + when(bridge.fetchMessageQueues(TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC)).thenReturn(createMessageQueueSet(TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC)); + when(bridge.getHalfMessage(0, 0, 1)).thenReturn(createPullResult(TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC, 5, "hello", 1)); + when(bridge.getHalfMessage(0, 1, 1)).thenReturn(createPullResult(TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC, 6, "hellp", 0)); + when(bridge.getOpMessage(anyInt(), anyLong(), anyInt())).thenReturn(createPullResult(TopicValidator.RMQ_SYS_TRANS_OP_HALF_TOPIC, 1, "5", 0)); + when(bridge.getBrokerController()).thenReturn(this.brokerController); + when(bridge.renewHalfMessageInner(any(MessageExtBrokerInner.class))).thenReturn(createMessageBrokerInner()); + when(bridge.putMessageReturnResult(any(MessageExtBrokerInner.class))) + .thenReturn(new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK))); + long timeOut = this.brokerController.getBrokerConfig().getTransactionTimeOut(); + final int checkMax = this.brokerController.getBrokerConfig().getTransactionCheckMax(); + final AtomicInteger checkMessage = new AtomicInteger(0); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) { + checkMessage.addAndGet(1); + return checkMessage; + } + }).when(listener).resolveHalfMsg(any(MessageExt.class)); + queueTransactionMsgService.check(timeOut, checkMax, listener); + assertThat(checkMessage.get()).isEqualTo(1); + } + + @Test + public void testDeletePrepareMessage_queueFull() throws InterruptedException { + ((TransactionalMessageServiceImpl)queueTransactionMsgService).getDeleteContext().put(0, new MessageQueueOpContext(0, 1)); + boolean res = queueTransactionMsgService.deletePrepareMessage(createMessageBrokerInner()); + assertThat(res).isTrue(); + when(bridge.writeOp(any(Integer.class), any(Message.class))).thenReturn(false); + res = queueTransactionMsgService.deletePrepareMessage(createMessageBrokerInner()); + assertThat(res).isFalse(); + } + + @Test + public void testDeletePrepareMessage_maxSize() throws InterruptedException { + brokerController.getBrokerConfig().setTransactionOpMsgMaxSize(1); + brokerController.getBrokerConfig().setTransactionOpBatchInterval(3000); + queueTransactionMsgService.open(); + boolean res = queueTransactionMsgService.deletePrepareMessage(createMessageBrokerInner(1000, "test", "testHello")); + assertThat(res).isTrue(); + verify(bridge, timeout(50)).writeOp(any(Integer.class), any(Message.class)); + queueTransactionMsgService.close(); + } + + @Test + public void testOpen() { + boolean isOpen = queueTransactionMsgService.open(); + assertThat(isOpen).isTrue(); + } + + private PullResult createDiscardPullResult(String topic, long queueOffset, String body, int size) { + PullResult result = createPullResult(topic, queueOffset, body, size); + List msgs = result.getMsgFoundList(); + for (MessageExt msg : msgs) { + msg.putUserProperty(MessageConst.PROPERTY_TRANSACTION_CHECK_TIMES, "100000"); + } + return result; + } + + private PullResult createPullResult(String topic, long queueOffset, String body, int size) { + PullResult result = null; + if (0 == size) { + result = new PullResult(PullStatus.NO_NEW_MSG, 1, 0, 1, + null); + } else { + result = new PullResult(PullStatus.FOUND, 1, 0, 1, + getMessageList(queueOffset, topic, body, 1)); + return result; + } + return result; + } + + private PullResult createOpPulResult(String topic, long queueOffset, String body, int size) { + PullResult result = createPullResult(topic, queueOffset, body, size); + List msgs = result.getMsgFoundList(); + for (MessageExt msg : msgs) { + msg.setTags(TransactionalMessageUtil.REMOVE_TAG); + } + return result; + } + + private PullResult createImmunityPulResult(String topic, long queueOffset, String body, int size) { + PullResult result = createPullResult(topic, queueOffset, body, size); + List msgs = result.getMsgFoundList(); + for (MessageExt msg : msgs) { + msg.putUserProperty(MessageConst.PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS, "0"); + } + return result; + } + + private List getMessageList(long queueOffset, String topic, String body, int size) { + List msgs = new ArrayList<>(); + for (int i = 0; i < size; i++) { + MessageExt messageExt = createMessageBrokerInner(queueOffset, topic, body); + msgs.add(messageExt); + } + return msgs; + } + + private Set createMessageQueueSet(String topic) { + Set messageQueues = new HashSet<>(); + MessageQueue messageQueue = new MessageQueue(topic, "DefaultCluster", 0); + messageQueues.add(messageQueue); + return messageQueues; + } + + private EndTransactionRequestHeader createEndTransactionRequestHeader(int status) { + EndTransactionRequestHeader header = new EndTransactionRequestHeader(); + header.setCommitLogOffset(123456789L); + header.setCommitOrRollback(status); + header.setMsgId("12345678"); + header.setTransactionId("123"); + header.setProducerGroup("testTransactionGroup"); + header.setTranStateTableOffset(1234L); + return header; + } + + private MessageExtBrokerInner createMessageBrokerInner(long queueOffset, String topic, String body) { + MessageExtBrokerInner inner = new MessageExtBrokerInner(); + inner.setBornTimestamp(System.currentTimeMillis() - 80000); + inner.setTransactionId("123456123"); + inner.setTopic(topic); + inner.setQueueOffset(queueOffset); + inner.setBody(body.getBytes()); + inner.setMsgId("123456123"); + inner.setQueueId(0); + inner.setTopic("hello"); + return inner; + } + + private MessageExtBrokerInner createMessageBrokerInner() { + return createMessageBrokerInner(1, "testTopic", "hello world"); + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageUtilTest.java b/broker/src/test/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageUtilTest.java new file mode 100644 index 00000000000..722a306848e --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/transaction/queue/TransactionalMessageUtilTest.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.transaction.queue; + + +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.junit.Assert; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class TransactionalMessageUtilTest { + + @Test + public void testBuildTransactionalMessageFromHalfMessage() { + MessageExt halfMessage = new MessageExt(); + halfMessage.setTopic(TransactionalMessageUtil.buildHalfTopic()); + MessageAccessor.putProperty(halfMessage, MessageConst.PROPERTY_REAL_TOPIC, "real-topic"); + halfMessage.setMsgId("msgId"); + halfMessage.setTransactionId("tranId"); + MessageAccessor.putProperty(halfMessage, MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, "tranId"); + MessageAccessor.putProperty(halfMessage, MessageConst.PROPERTY_PRODUCER_GROUP, "trans-producer-grp"); + + MessageExtBrokerInner msgExtInner = TransactionalMessageUtil.buildTransactionalMessageFromHalfMessage(halfMessage); + + + assertEquals("real-topic", msgExtInner.getTopic()); + assertEquals("true", msgExtInner.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED)); + assertEquals(msgExtInner.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX), + halfMessage.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX)); + assertEquals(msgExtInner.getMsgId(), halfMessage.getMsgId()); + assertTrue(MessageSysFlag.check(msgExtInner.getSysFlag(), MessageSysFlag.TRANSACTION_PREPARED_TYPE)); + assertEquals(msgExtInner.getProperty(MessageConst.PROPERTY_PRODUCER_GROUP), halfMessage.getProperty(MessageConst.PROPERTY_PRODUCER_GROUP)); + } + + @Test + public void testGetImmunityTime() { + long transactionTimeout = 6 * 1000; + + String checkImmunityTimeStr = "1"; + long immunityTime = TransactionalMessageUtil.getImmunityTime(checkImmunityTimeStr, transactionTimeout); + Assert.assertEquals(6 * 1000, immunityTime); + + checkImmunityTimeStr = "5"; + immunityTime = TransactionalMessageUtil.getImmunityTime(checkImmunityTimeStr, transactionTimeout); + Assert.assertEquals(6 * 1000, immunityTime); + + checkImmunityTimeStr = "7"; + immunityTime = TransactionalMessageUtil.getImmunityTime(checkImmunityTimeStr, transactionTimeout); + Assert.assertEquals(7 * 1000, immunityTime); + + + checkImmunityTimeStr = null; + immunityTime = TransactionalMessageUtil.getImmunityTime(checkImmunityTimeStr, transactionTimeout); + Assert.assertEquals(6 * 1000, immunityTime); + + checkImmunityTimeStr = "-1"; + immunityTime = TransactionalMessageUtil.getImmunityTime(checkImmunityTimeStr, transactionTimeout); + Assert.assertEquals(6 * 1000, immunityTime); + + checkImmunityTimeStr = "60"; + immunityTime = TransactionalMessageUtil.getImmunityTime(checkImmunityTimeStr, transactionTimeout); + Assert.assertEquals(60 * 1000, immunityTime); + + checkImmunityTimeStr = "100"; + immunityTime = TransactionalMessageUtil.getImmunityTime(checkImmunityTimeStr, transactionTimeout); + Assert.assertEquals(100 * 1000, immunityTime); + + + checkImmunityTimeStr = "100.5"; + immunityTime = TransactionalMessageUtil.getImmunityTime(checkImmunityTimeStr, transactionTimeout); + Assert.assertEquals(6 * 1000, immunityTime); + } +} \ No newline at end of file diff --git a/broker/src/test/java/org/apache/rocketmq/broker/util/HookUtilsTest.java b/broker/src/test/java/org/apache/rocketmq/broker/util/HookUtilsTest.java new file mode 100644 index 00000000000..738690c691b --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/util/HookUtilsTest.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.util; + +import java.util.Objects; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.RunningFlags; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +public class HookUtilsTest { + + @Test + public void testCheckBeforePutMessage() { + BrokerController brokerController = Mockito.mock(BrokerController.class); + MessageStore messageStore = Mockito.mock(MessageStore.class); + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + RunningFlags runningFlags = Mockito.mock(RunningFlags.class); + + Mockito.when(brokerController.getMessageStore()).thenReturn(messageStore); + Mockito.when(brokerController.getMessageStore().isShutdown()).thenReturn(false); + Mockito.when(brokerController.getMessageStoreConfig()).thenReturn(messageStoreConfig); + Mockito.when(messageStore.getRunningFlags()).thenReturn(runningFlags); + Mockito.when(messageStore.getRunningFlags().isWriteable()).thenReturn(true); + + MessageExt messageExt = new MessageExt(); + messageExt.setTopic(RandomStringUtils.randomAlphabetic(Byte.MAX_VALUE).toUpperCase()); + messageExt.setBody(RandomStringUtils.randomAlphabetic(Byte.MAX_VALUE).toUpperCase().getBytes()); + Assert.assertNull(HookUtils.checkBeforePutMessage(brokerController, messageExt)); + + messageExt.setTopic(RandomStringUtils.randomAlphabetic(Byte.MAX_VALUE + 1).toUpperCase()); + Assert.assertEquals(PutMessageStatus.MESSAGE_ILLEGAL, Objects.requireNonNull( + HookUtils.checkBeforePutMessage(brokerController, messageExt)).getPutMessageStatus()); + + messageExt.setTopic(MixAll.RETRY_GROUP_TOPIC_PREFIX + + RandomStringUtils.randomAlphabetic(Byte.MAX_VALUE + 1).toUpperCase()); + Assert.assertNull(HookUtils.checkBeforePutMessage(brokerController, messageExt)); + + messageExt.setTopic(MixAll.RETRY_GROUP_TOPIC_PREFIX + + RandomStringUtils.randomAlphabetic(255 - MixAll.RETRY_GROUP_TOPIC_PREFIX.length()).toUpperCase()); + Assert.assertNull(HookUtils.checkBeforePutMessage(brokerController, messageExt)); + + messageExt.setTopic(MixAll.RETRY_GROUP_TOPIC_PREFIX + + RandomStringUtils.randomAlphabetic(256 - MixAll.RETRY_GROUP_TOPIC_PREFIX.length()).toUpperCase()); + Assert.assertEquals(PutMessageStatus.MESSAGE_ILLEGAL, Objects.requireNonNull( + HookUtils.checkBeforePutMessage(brokerController, messageExt)).getPutMessageStatus()); + } +} \ No newline at end of file diff --git a/broker/src/test/java/org/apache/rocketmq/broker/util/LogTransactionalMessageCheckListener.java b/broker/src/test/java/org/apache/rocketmq/broker/util/LogTransactionalMessageCheckListener.java new file mode 100644 index 00000000000..3a2098eabd1 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/util/LogTransactionalMessageCheckListener.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.util; + +import org.apache.rocketmq.broker.transaction.AbstractTransactionalMessageCheckListener; +import org.apache.rocketmq.common.message.MessageExt; + +public class LogTransactionalMessageCheckListener extends AbstractTransactionalMessageCheckListener { + + @Override + public void resolveDiscardMsg(MessageExt msgExt) { + + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/util/ServiceProviderTest.java b/broker/src/test/java/org/apache/rocketmq/broker/util/ServiceProviderTest.java new file mode 100644 index 00000000000..53fba00fa9e --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/util/ServiceProviderTest.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.broker.util; + +import org.apache.rocketmq.acl.AccessValidator; +import org.apache.rocketmq.broker.transaction.AbstractTransactionalMessageCheckListener; +import org.apache.rocketmq.broker.transaction.TransactionalMessageService; +import org.apache.rocketmq.common.utils.ServiceProvider; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +public class ServiceProviderTest { + + @Test + public void loadTransactionMsgServiceTest() { + TransactionalMessageService transactionService = ServiceProvider.loadClass(TransactionalMessageService.class); + assertThat(transactionService).isNotNull(); + } + + @Test + public void loadAbstractTransactionListenerTest() { + AbstractTransactionalMessageCheckListener listener = ServiceProvider.loadClass( + AbstractTransactionalMessageCheckListener.class); + assertThat(listener).isNotNull(); + } + + @Test + public void loadAccessValidatorTest() { + List accessValidators = ServiceProvider.load(AccessValidator.class); + assertThat(accessValidators).isNotNull(); + } +} diff --git a/broker/src/test/java/org/apache/rocketmq/broker/util/TransactionalMessageServiceImpl.java b/broker/src/test/java/org/apache/rocketmq/broker/util/TransactionalMessageServiceImpl.java new file mode 100644 index 00000000000..2de4c307e08 --- /dev/null +++ b/broker/src/test/java/org/apache/rocketmq/broker/util/TransactionalMessageServiceImpl.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.broker.util; + +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.broker.transaction.AbstractTransactionalMessageCheckListener; +import org.apache.rocketmq.broker.transaction.OperationResult; +import org.apache.rocketmq.broker.transaction.TransactionMetrics; +import org.apache.rocketmq.broker.transaction.TransactionalMessageService; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.header.EndTransactionRequestHeader; +import org.apache.rocketmq.store.PutMessageResult; + +public class TransactionalMessageServiceImpl implements TransactionalMessageService { + private static final Logger log = LoggerFactory.getLogger(LoggerName.TRANSACTION_LOGGER_NAME); + + @Override + public PutMessageResult prepareMessage(MessageExtBrokerInner messageInner) { + return null; + } + + @Override + public CompletableFuture asyncPrepareMessage(MessageExtBrokerInner messageInner) { + return null; + } + + @Override + public boolean deletePrepareMessage(MessageExt messageExt) { + return false; + } + + @Override + public OperationResult commitMessage(EndTransactionRequestHeader requestHeader) { + return null; + } + + @Override + public OperationResult rollbackMessage(EndTransactionRequestHeader requestHeader) { + return null; + } + + @Override + public void check(long transactionTimeout, int transactionCheckMax, AbstractTransactionalMessageCheckListener listener) { + log.warn("check check!"); + } + + @Override + public boolean open() { + return true; + } + + @Override + public void close() { + + } + + @Override + public TransactionMetrics getTransactionMetrics() { + return null; + } + + @Override + public void setTransactionMetrics(TransactionMetrics transactionMetrics) { + + } +} diff --git a/broker/src/test/resources/META-INF/service/org.apache.rocketmq.acl.AccessValidator b/broker/src/test/resources/META-INF/service/org.apache.rocketmq.acl.AccessValidator new file mode 100644 index 00000000000..1abc92e0162 --- /dev/null +++ b/broker/src/test/resources/META-INF/service/org.apache.rocketmq.acl.AccessValidator @@ -0,0 +1 @@ +org.apache.rocketmq.acl.plain.PlainAccessValidator \ No newline at end of file diff --git a/broker/src/test/resources/META-INF/service/org.apache.rocketmq.broker.transaction.AbstractTransactionalMessageCheckListener b/broker/src/test/resources/META-INF/service/org.apache.rocketmq.broker.transaction.AbstractTransactionalMessageCheckListener new file mode 100644 index 00000000000..455b266b9f1 --- /dev/null +++ b/broker/src/test/resources/META-INF/service/org.apache.rocketmq.broker.transaction.AbstractTransactionalMessageCheckListener @@ -0,0 +1 @@ +org.apache.rocketmq.broker.util.LogTransactionalMessageCheckListener \ No newline at end of file diff --git a/broker/src/test/resources/META-INF/service/org.apache.rocketmq.broker.transaction.TransactionalMessageService b/broker/src/test/resources/META-INF/service/org.apache.rocketmq.broker.transaction.TransactionalMessageService new file mode 100644 index 00000000000..b012e1451c7 --- /dev/null +++ b/broker/src/test/resources/META-INF/service/org.apache.rocketmq.broker.transaction.TransactionalMessageService @@ -0,0 +1 @@ +org.apache.rocketmq.broker.util.TransactionalMessageServiceImpl \ No newline at end of file diff --git a/broker/src/test/resources/logback-test.xml b/broker/src/test/resources/logback-test.xml deleted file mode 100644 index 1978b73ae07..00000000000 --- a/broker/src/test/resources/logback-test.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - %d{yyy-MM-dd HH\:mm\:ss,GMT+8} %p %t - %m%n - UTF-8 - - - - - - - - diff --git a/broker/src/test/resources/rmq.logback-test.xml b/broker/src/test/resources/rmq.logback-test.xml new file mode 100644 index 00000000000..8695d52d57c --- /dev/null +++ b/broker/src/test/resources/rmq.logback-test.xml @@ -0,0 +1,36 @@ + + + + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/BUILD.bazel b/client/BUILD.bazel new file mode 100644 index 00000000000..46e29452b95 --- /dev/null +++ b/client/BUILD.bazel @@ -0,0 +1,67 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +load("//bazel:GenTestRules.bzl", "GenTestRules") + +java_library( + name = "client", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//common", + "//remoting", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:commons_validator_commons_validator", + "@maven//:com_github_luben_zstd_jni", + "@maven//:org_lz4_lz4_java", + "@maven//:com_alibaba_fastjson", + "@maven//:io_netty_netty_all", + "@maven//:io_opentracing_opentracing_api", + "@maven//:commons_collections_commons_collections", + "@maven//:io_github_aliyunmq_rocketmq_slf4j_api", + "@maven//:io_github_aliyunmq_rocketmq_logback_classic", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "tests", + srcs = glob(["src/test/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + ":client", + "//remoting", + "//common", + "//:test_deps", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:io_netty_netty_all", + "@maven//:io_opentracing_opentracing_api", + "@maven//:io_opentracing_opentracing_mock", + "@maven//:org_awaitility_awaitility", + ], + resources = glob(["src/test/resources/certs/*.pem"]) + glob(["src/test/resources/certs/*.key"]) +) + +GenTestRules( + name = "GeneratedTestRules", + test_files = glob(["src/test/java/**/*Test.java"]), + deps = [ + ":tests", + ], + exclude_tests = [ + "src/test/java/org/apache/rocketmq/client/consumer/DefaultLitePullConsumerTest", + ], +) diff --git a/client/pom.xml b/client/pom.xml index a8034c2165d..29a62708dc0 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -19,7 +19,7 @@ org.apache.rocketmq rocketmq-all - 4.2.0 + 5.2.0 4.0.0 @@ -28,14 +28,13 @@ rocketmq-client ${project.version} - 1.6 - 1.6 + ${basedir}/.. ${project.groupId} - rocketmq-common + rocketmq-remoting io.netty @@ -43,23 +42,29 @@ - - org.slf4j - slf4j-api - org.apache.commons commons-lang3 - org.apache.logging.log4j - log4j-core - test + io.opentracing + opentracing-api + + + io.opentracing + opentracing-mock + + + com.google.guava + guava + + + io.github.aliyunmq + rocketmq-slf4j-api - org.apache.logging.log4j - log4j-slf4j-impl - test + io.github.aliyunmq + rocketmq-logback-classic diff --git a/client/src/main/java/org/apache/rocketmq/client/AccessChannel.java b/client/src/main/java/org/apache/rocketmq/client/AccessChannel.java new file mode 100644 index 00000000000..d6feb579cea --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/AccessChannel.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client; + +/** + * Used for set access channel, if need migrate the rocketmq service to cloud, it is We recommend set the value with + * "CLOUD". otherwise set with "LOCAL", especially used the message trace feature. + */ +public enum AccessChannel { + /** + * Means connect to private IDC cluster. + */ + LOCAL, + + /** + * Means connect to Cloud service. + */ + CLOUD, +} diff --git a/client/src/main/java/org/apache/rocketmq/client/ClientConfig.java b/client/src/main/java/org/apache/rocketmq/client/ClientConfig.java index a9eabfe6313..8a7beffc704 100644 --- a/client/src/main/java/org/apache/rocketmq/client/ClientConfig.java +++ b/client/src/main/java/org/apache/rocketmq/client/ClientConfig.java @@ -16,20 +16,41 @@ */ package org.apache.rocketmq.client; -import org.apache.rocketmq.common.MixAll; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.common.UtilAll; -import org.apache.rocketmq.remoting.common.RemotingUtil; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.utils.NameServerAddressUtils; +import org.apache.rocketmq.common.utils.NetworkUtil; import org.apache.rocketmq.remoting.netty.TlsSystemConfig; +import org.apache.rocketmq.remoting.protocol.LanguageCode; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; +import org.apache.rocketmq.remoting.protocol.RequestType; /** * Client Common configuration */ public class ClientConfig { public static final String SEND_MESSAGE_WITH_VIP_CHANNEL_PROPERTY = "com.rocketmq.sendMessageWithVIPChannel"; - private String namesrvAddr = System.getProperty(MixAll.NAMESRV_ADDR_PROPERTY, System.getenv(MixAll.NAMESRV_ADDR_ENV)); - private String clientIP = RemotingUtil.getLocalAddress(); + public static final String SOCKS_PROXY_CONFIG = "com.rocketmq.socks.proxy.config"; + public static final String DECODE_READ_BODY = "com.rocketmq.read.body"; + public static final String DECODE_DECOMPRESS_BODY = "com.rocketmq.decompress.body"; + public static final String SEND_LATENCY_ENABLE = "com.rocketmq.sendLatencyEnable"; + public static final String START_DETECTOR_ENABLE = "com.rocketmq.startDetectorEnable"; + public static final String HEART_BEAT_V2 = "com.rocketmq.heartbeat.v2"; + private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses(); + private String clientIP = NetworkUtil.getLocalAddress(); private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT"); private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + @Deprecated + protected String namespace; + private boolean namespaceInitialized = false; + protected String namespaceV2; + protected AccessChannel accessChannel = AccessChannel.LOCAL; + /** * Pulling topic information interval from the named server */ @@ -42,12 +63,41 @@ public class ClientConfig { * Offset persistent interval for consumer */ private int persistConsumerOffsetInterval = 1000 * 5; + private long pullTimeDelayMillsWhenException = 1000; private boolean unitMode = false; private String unitName; - private boolean vipChannelEnabled = Boolean.parseBoolean(System.getProperty(SEND_MESSAGE_WITH_VIP_CHANNEL_PROPERTY, "true")); + private boolean decodeReadBody = Boolean.parseBoolean(System.getProperty(DECODE_READ_BODY, "true")); + private boolean decodeDecompressBody = Boolean.parseBoolean(System.getProperty(DECODE_DECOMPRESS_BODY, "true")); + private boolean vipChannelEnabled = Boolean.parseBoolean(System.getProperty(SEND_MESSAGE_WITH_VIP_CHANNEL_PROPERTY, "false")); + private boolean useHeartbeatV2 = Boolean.parseBoolean(System.getProperty(HEART_BEAT_V2, "false")); private boolean useTLS = TlsSystemConfig.tlsEnable; + private String socksProxyConfig = System.getProperty(SOCKS_PROXY_CONFIG, "{}"); + + private int mqClientApiTimeout = 3 * 1000; + private int detectTimeout = 200; + private int detectInterval = 2 * 1000; + + private LanguageCode language = LanguageCode.JAVA; + + /** + * Enable stream request type will inject a RPCHook to add corresponding request type to remoting layer. + * And it will also generate a different client id to prevent unexpected reuses of MQClientInstance. + */ + protected boolean enableStreamRequestType = false; + + /** + * Enable the fault tolerance mechanism of the client sending process. + * DO NOT OPEN when ORDER messages are required. + * Turning on will interfere with the queue selection functionality, + * possibly conflicting with the order message. + */ + private boolean sendLatencyEnable = Boolean.parseBoolean(System.getProperty(SEND_LATENCY_ENABLE, "false")); + private boolean startDetectorEnable = Boolean.parseBoolean(System.getProperty(START_DETECTOR_ENABLE, "false")); + + private boolean enableHeartbeatChannelEventListener = true; + public String buildMQClientId() { StringBuilder sb = new StringBuilder(); sb.append(this.getClientIP()); @@ -59,6 +109,11 @@ public String buildMQClientId() { sb.append(this.unitName); } + if (enableStreamRequestType) { + sb.append("@"); + sb.append(RequestType.STREAM); + } + return sb.toString(); } @@ -80,8 +135,57 @@ public void setInstanceName(String instanceName) { public void changeInstanceNameToPID() { if (this.instanceName.equals("DEFAULT")) { - this.instanceName = String.valueOf(UtilAll.getPid()); + this.instanceName = UtilAll.getPid() + "#" + System.nanoTime(); + } + } + + @Deprecated + public String withNamespace(String resource) { + return NamespaceUtil.wrapNamespace(this.getNamespace(), resource); + } + + @Deprecated + public Set withNamespace(Set resourceSet) { + Set resourceWithNamespace = new HashSet<>(); + for (String resource : resourceSet) { + resourceWithNamespace.add(withNamespace(resource)); + } + return resourceWithNamespace; + } + + @Deprecated + public String withoutNamespace(String resource) { + return NamespaceUtil.withoutNamespace(resource, this.getNamespace()); + } + + @Deprecated + public Set withoutNamespace(Set resourceSet) { + Set resourceWithoutNamespace = new HashSet<>(); + for (String resource : resourceSet) { + resourceWithoutNamespace.add(withoutNamespace(resource)); + } + return resourceWithoutNamespace; + } + + @Deprecated + public MessageQueue queueWithNamespace(MessageQueue queue) { + if (StringUtils.isEmpty(this.getNamespace())) { + return queue; } + return new MessageQueue(withNamespace(queue.getTopic()), queue.getBrokerName(), queue.getQueueId()); + } + + @Deprecated + public Collection queuesWithNamespace(Collection queues) { + if (StringUtils.isEmpty(this.getNamespace())) { + return queues; + } + Iterator iter = queues.iterator(); + while (iter.hasNext()) { + MessageQueue queue = iter.next(); + queue.setTopic(withNamespace(queue.getTopic())); + } + return queues; } public void resetClientConfig(final ClientConfig cc) { @@ -92,10 +196,25 @@ public void resetClientConfig(final ClientConfig cc) { this.pollNameServerInterval = cc.pollNameServerInterval; this.heartbeatBrokerInterval = cc.heartbeatBrokerInterval; this.persistConsumerOffsetInterval = cc.persistConsumerOffsetInterval; + this.pullTimeDelayMillsWhenException = cc.pullTimeDelayMillsWhenException; this.unitMode = cc.unitMode; this.unitName = cc.unitName; this.vipChannelEnabled = cc.vipChannelEnabled; this.useTLS = cc.useTLS; + this.socksProxyConfig = cc.socksProxyConfig; + this.namespace = cc.namespace; + this.language = cc.language; + this.mqClientApiTimeout = cc.mqClientApiTimeout; + this.decodeReadBody = cc.decodeReadBody; + this.decodeDecompressBody = cc.decodeDecompressBody; + this.enableStreamRequestType = cc.enableStreamRequestType; + this.useHeartbeatV2 = cc.useHeartbeatV2; + this.startDetectorEnable = cc.startDetectorEnable; + this.sendLatencyEnable = cc.sendLatencyEnable; + this.enableHeartbeatChannelEventListener = cc.enableHeartbeatChannelEventListener; + this.detectInterval = cc.detectInterval; + this.detectTimeout = cc.detectTimeout; + this.namespaceV2 = cc.namespaceV2; } public ClientConfig cloneClientConfig() { @@ -107,19 +226,43 @@ public ClientConfig cloneClientConfig() { cc.pollNameServerInterval = pollNameServerInterval; cc.heartbeatBrokerInterval = heartbeatBrokerInterval; cc.persistConsumerOffsetInterval = persistConsumerOffsetInterval; + cc.pullTimeDelayMillsWhenException = pullTimeDelayMillsWhenException; cc.unitMode = unitMode; cc.unitName = unitName; cc.vipChannelEnabled = vipChannelEnabled; cc.useTLS = useTLS; + cc.socksProxyConfig = socksProxyConfig; + cc.namespace = namespace; + cc.language = language; + cc.mqClientApiTimeout = mqClientApiTimeout; + cc.decodeReadBody = decodeReadBody; + cc.decodeDecompressBody = decodeDecompressBody; + cc.enableStreamRequestType = enableStreamRequestType; + cc.useHeartbeatV2 = useHeartbeatV2; + cc.startDetectorEnable = startDetectorEnable; + cc.enableHeartbeatChannelEventListener = enableHeartbeatChannelEventListener; + cc.sendLatencyEnable = sendLatencyEnable; + cc.detectInterval = detectInterval; + cc.detectTimeout = detectTimeout; + cc.namespaceV2 = namespaceV2; return cc; } public String getNamesrvAddr() { + if (StringUtils.isNotEmpty(namesrvAddr) && NameServerAddressUtils.NAMESRV_ENDPOINT_PATTERN.matcher(namesrvAddr.trim()).matches()) { + return NameServerAddressUtils.getNameSrvAddrFromNamesrvEndpoint(namesrvAddr); + } return namesrvAddr; } + /** + * Domain name mode access way does not support the delimiter(;), and only one domain name can be set. + * + * @param namesrvAddr name server address + */ public void setNamesrvAddr(String namesrvAddr) { this.namesrvAddr = namesrvAddr; + this.namespaceInitialized = false; } public int getClientCallbackExecutorThreads() { @@ -154,6 +297,14 @@ public void setPersistConsumerOffsetInterval(int persistConsumerOffsetInterval) this.persistConsumerOffsetInterval = persistConsumerOffsetInterval; } + public long getPullTimeDelayMillsWhenException() { + return pullTimeDelayMillsWhenException; + } + + public void setPullTimeDelayMillsWhenException(long pullTimeDelayMillsWhenException) { + this.pullTimeDelayMillsWhenException = pullTimeDelayMillsWhenException; + } + public String getUnitName() { return unitName; } @@ -186,12 +337,174 @@ public void setUseTLS(boolean useTLS) { this.useTLS = useTLS; } + public String getSocksProxyConfig() { + return socksProxyConfig; + } + + public void setSocksProxyConfig(String socksProxyConfig) { + this.socksProxyConfig = socksProxyConfig; + } + + public LanguageCode getLanguage() { + return language; + } + + public void setLanguage(LanguageCode language) { + this.language = language; + } + + public boolean isDecodeReadBody() { + return decodeReadBody; + } + + public void setDecodeReadBody(boolean decodeReadBody) { + this.decodeReadBody = decodeReadBody; + } + + public boolean isDecodeDecompressBody() { + return decodeDecompressBody; + } + + public void setDecodeDecompressBody(boolean decodeDecompressBody) { + this.decodeDecompressBody = decodeDecompressBody; + } + + @Deprecated + public String getNamespace() { + if (namespaceInitialized) { + return namespace; + } + + if (StringUtils.isNotEmpty(namespace)) { + return namespace; + } + + if (StringUtils.isNotEmpty(this.namesrvAddr)) { + if (NameServerAddressUtils.validateInstanceEndpoint(namesrvAddr)) { + namespace = NameServerAddressUtils.parseInstanceIdFromEndpoint(namesrvAddr); + } + } + namespaceInitialized = true; + return namespace; + } + + @Deprecated + public void setNamespace(String namespace) { + this.namespace = namespace; + this.namespaceInitialized = true; + } + + public String getNamespaceV2() { + return namespaceV2; + } + + public void setNamespaceV2(String namespaceV2) { + this.namespaceV2 = namespaceV2; + } + + public AccessChannel getAccessChannel() { + return this.accessChannel; + } + + public void setAccessChannel(AccessChannel accessChannel) { + this.accessChannel = accessChannel; + } + + public int getMqClientApiTimeout() { + return mqClientApiTimeout; + } + + public void setMqClientApiTimeout(int mqClientApiTimeout) { + this.mqClientApiTimeout = mqClientApiTimeout; + } + + public boolean isEnableStreamRequestType() { + return enableStreamRequestType; + } + + public void setEnableStreamRequestType(boolean enableStreamRequestType) { + this.enableStreamRequestType = enableStreamRequestType; + } + + public boolean isSendLatencyEnable() { + return sendLatencyEnable; + } + + public void setSendLatencyEnable(boolean sendLatencyEnable) { + this.sendLatencyEnable = sendLatencyEnable; + } + + public boolean isStartDetectorEnable() { + return startDetectorEnable; + } + + public void setStartDetectorEnable(boolean startDetectorEnable) { + this.startDetectorEnable = startDetectorEnable; + } + + public boolean isEnableHeartbeatChannelEventListener() { + return enableHeartbeatChannelEventListener; + } + + public void setEnableHeartbeatChannelEventListener(boolean enableHeartbeatChannelEventListener) { + this.enableHeartbeatChannelEventListener = enableHeartbeatChannelEventListener; + } + + public int getDetectTimeout() { + return this.detectTimeout; + } + + public void setDetectTimeout(int detectTimeout) { + this.detectTimeout = detectTimeout; + } + + public int getDetectInterval() { + return this.detectInterval; + } + + public void setDetectInterval(int detectInterval) { + this.detectInterval = detectInterval; + } + + public boolean isUseHeartbeatV2() { + return useHeartbeatV2; + } + + public void setUseHeartbeatV2(boolean useHeartbeatV2) { + this.useHeartbeatV2 = useHeartbeatV2; + } + @Override public String toString() { - return "ClientConfig [namesrvAddr=" + namesrvAddr + ", clientIP=" + clientIP + ", instanceName=" + instanceName - + ", clientCallbackExecutorThreads=" + clientCallbackExecutorThreads + ", pollNameServerInterval=" + pollNameServerInterval - + ", heartbeatBrokerInterval=" + heartbeatBrokerInterval + ", persistConsumerOffsetInterval=" - + persistConsumerOffsetInterval + ", unitMode=" + unitMode + ", unitName=" + unitName + ", vipChannelEnabled=" - + vipChannelEnabled + ", useTLS=" + useTLS + "]"; + return "ClientConfig{" + + "namesrvAddr='" + namesrvAddr + '\'' + + ", clientIP='" + clientIP + '\'' + + ", instanceName='" + instanceName + '\'' + + ", clientCallbackExecutorThreads=" + clientCallbackExecutorThreads + + ", namespace='" + namespace + '\'' + + ", namespaceInitialized=" + namespaceInitialized + + ", namespaceV2='" + namespaceV2 + '\'' + + ", accessChannel=" + accessChannel + + ", pollNameServerInterval=" + pollNameServerInterval + + ", heartbeatBrokerInterval=" + heartbeatBrokerInterval + + ", persistConsumerOffsetInterval=" + persistConsumerOffsetInterval + + ", pullTimeDelayMillsWhenException=" + pullTimeDelayMillsWhenException + + ", unitMode=" + unitMode + + ", unitName='" + unitName + '\'' + + ", decodeReadBody=" + decodeReadBody + + ", decodeDecompressBody=" + decodeDecompressBody + + ", vipChannelEnabled=" + vipChannelEnabled + + ", useHeartbeatV2=" + useHeartbeatV2 + + ", useTLS=" + useTLS + + ", socksProxyConfig='" + socksProxyConfig + '\'' + + ", mqClientApiTimeout=" + mqClientApiTimeout + + ", detectTimeout=" + detectTimeout + + ", detectInterval=" + detectInterval + + ", language=" + language + + ", enableStreamRequestType=" + enableStreamRequestType + + ", sendLatencyEnable=" + sendLatencyEnable + + ", startDetectorEnable=" + startDetectorEnable + + ", enableHeartbeatChannelEventListener=" + enableHeartbeatChannelEventListener + + '}'; } } diff --git a/client/src/main/java/org/apache/rocketmq/client/MQAdmin.java b/client/src/main/java/org/apache/rocketmq/client/MQAdmin.java index 019414b9e5c..c2e936be43f 100644 --- a/client/src/main/java/org/apache/rocketmq/client/MQAdmin.java +++ b/client/src/main/java/org/apache/rocketmq/client/MQAdmin.java @@ -22,29 +22,31 @@ import org.apache.rocketmq.common.message.MessageQueue; import org.apache.rocketmq.remoting.exception.RemotingException; +import java.util.Map; + /** * Base interface for MQ management */ public interface MQAdmin { /** - * Creates an topic - * - * @param key accesskey + * Creates a topic + * @param key accessKey * @param newTopic topic name * @param queueNum topic's queue number + * @param attributes */ - void createTopic(final String key, final String newTopic, final int queueNum) + void createTopic(final String key, final String newTopic, final int queueNum, Map attributes) throws MQClientException; /** - * Creates an topic - * - * @param key accesskey + * Creates a topic + * @param key accessKey * @param newTopic topic name * @param queueNum topic's queue number * @param topicSysFlag topic system flag + * @param attributes */ - void createTopic(String key, String newTopic, int queueNum, int topicSysFlag) + void createTopic(String key, String newTopic, int queueNum, int topicSysFlag, Map attributes) throws MQClientException; /** @@ -82,7 +84,7 @@ void createTopic(String key, String newTopic, int queueNum, int topicSysFlag) long earliestMsgStoreTime(final MessageQueue mq) throws MQClientException; /** - * Query message according tto message id + * Query message according to message id * * @param offsetMsgId message id * @return message diff --git a/client/src/main/java/org/apache/rocketmq/client/MQHelper.java b/client/src/main/java/org/apache/rocketmq/client/MQHelper.java index 7f0cef31b4d..9da6f5b4e48 100644 --- a/client/src/main/java/org/apache/rocketmq/client/MQHelper.java +++ b/client/src/main/java/org/apache/rocketmq/client/MQHelper.java @@ -19,12 +19,15 @@ import java.util.Set; import java.util.TreeSet; import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; -import org.apache.rocketmq.client.log.ClientLogger; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; -import org.slf4j.Logger; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; public class MQHelper { + private static final Logger log = LoggerFactory.getLogger(MQHelper.class); + + @Deprecated public static void resetOffsetByTimestamp( final MessageModel messageModel, final String consumerGroup, @@ -36,11 +39,11 @@ public static void resetOffsetByTimestamp( /** * Reset consumer topic offset according to time * - * @param messageModel which model - * @param instanceName which instance + * @param messageModel which model + * @param instanceName which instance * @param consumerGroup consumer group - * @param topic topic - * @param timestamp time + * @param topic topic + * @param timestamp time */ public static void resetOffsetByTimestamp( final MessageModel messageModel, @@ -48,7 +51,6 @@ public static void resetOffsetByTimestamp( final String consumerGroup, final String topic, final long timestamp) throws Exception { - final Logger log = ClientLogger.getLog(); DefaultMQPullConsumer consumer = new DefaultMQPullConsumer(consumerGroup); consumer.setInstanceName(instanceName); @@ -59,7 +61,7 @@ public static void resetOffsetByTimestamp( try { mqs = consumer.fetchSubscribeMessageQueues(topic); if (mqs != null && !mqs.isEmpty()) { - TreeSet mqsNew = new TreeSet(mqs); + TreeSet mqsNew = new TreeSet<>(mqs); for (MessageQueue mq : mqsNew) { long offset = consumer.searchOffset(mq, timestamp); if (offset >= 0) { diff --git a/client/src/main/java/org/apache/rocketmq/client/MqClientAdmin.java b/client/src/main/java/org/apache/rocketmq/client/MqClientAdmin.java new file mode 100644 index 00000000000..4eb74c0ca9b --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/MqClientAdmin.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.admin.ConsumeStats; +import org.apache.rocketmq.remoting.protocol.admin.TopicStatsTable; +import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.body.GroupList; +import org.apache.rocketmq.remoting.protocol.body.QueueTimeSpan; +import org.apache.rocketmq.remoting.protocol.body.TopicList; +import org.apache.rocketmq.remoting.protocol.header.ConsumeMessageDirectlyResultRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.CreateTopicRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.DeleteSubscriptionGroupRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.DeleteTopicRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumeStatsRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerConnectionListRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerRunningInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetTopicStatsInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumeTimeSpanRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QuerySubscriptionByConsumerRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryTopicConsumeByWhoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryTopicsByConsumerRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ResetOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ViewMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.DeleteKVConfigRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.DeleteTopicFromNamesrvRequestHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; + +public interface MqClientAdmin { + CompletableFuture> queryMessage(String address, boolean uniqueKeyFlag, boolean decompressBody, + QueryMessageRequestHeader requestHeader, long timeoutMillis); + + CompletableFuture getTopicStatsInfo(String address, + GetTopicStatsInfoRequestHeader requestHeader, long timeoutMillis); + + CompletableFuture> queryConsumeTimeSpan(String address, + QueryConsumeTimeSpanRequestHeader requestHeader, long timeoutMillis); + + CompletableFuture updateOrCreateTopic(String address, CreateTopicRequestHeader requestHeader, + long timeoutMillis); + + CompletableFuture updateOrCreateSubscriptionGroup(String address, SubscriptionGroupConfig config, + long timeoutMillis); + + CompletableFuture deleteTopicInBroker(String address, DeleteTopicRequestHeader requestHeader, + long timeoutMillis); + + CompletableFuture deleteTopicInNameserver(String address, DeleteTopicFromNamesrvRequestHeader requestHeader, + long timeoutMillis); + + CompletableFuture deleteKvConfig(String address, DeleteKVConfigRequestHeader requestHeader, + long timeoutMillis); + + CompletableFuture deleteSubscriptionGroup(String address, DeleteSubscriptionGroupRequestHeader requestHeader, + long timeoutMillis); + + CompletableFuture> invokeBrokerToResetOffset(String address, + ResetOffsetRequestHeader requestHeader, long timeoutMillis); + + CompletableFuture viewMessage(String address, ViewMessageRequestHeader requestHeader, + long timeoutMillis); + + CompletableFuture getBrokerClusterInfo(String address, long timeoutMillis); + + CompletableFuture getConsumerConnectionList(String address, + GetConsumerConnectionListRequestHeader requestHeader, long timeoutMillis); + + CompletableFuture queryTopicsByConsumer(String address, + QueryTopicsByConsumerRequestHeader requestHeader, long timeoutMillis); + + CompletableFuture querySubscriptionByConsumer(String address, + QuerySubscriptionByConsumerRequestHeader requestHeader, long timeoutMillis); + + CompletableFuture getConsumeStats(String address, GetConsumeStatsRequestHeader requestHeader, + long timeoutMillis); + + CompletableFuture queryTopicConsumeByWho(String address, + QueryTopicConsumeByWhoRequestHeader requestHeader, long timeoutMillis); + + CompletableFuture getConsumerRunningInfo(String address, + GetConsumerRunningInfoRequestHeader requestHeader, long timeoutMillis); + + CompletableFuture consumeMessageDirectly(String address, + ConsumeMessageDirectlyResultRequestHeader requestHeader, long timeoutMillis); +} diff --git a/client/src/main/java/org/apache/rocketmq/client/Validators.java b/client/src/main/java/org/apache/rocketmq/client/Validators.java index 5567e49b558..77e4bbd2383 100644 --- a/client/src/main/java/org/apache/rocketmq/client/Validators.java +++ b/client/src/main/java/org/apache/rocketmq/client/Validators.java @@ -17,34 +17,27 @@ package org.apache.rocketmq.client; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import java.io.File; +import java.util.Properties; +import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.DefaultMQProducer; -import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.constant.PermName; import org.apache.rocketmq.common.message.Message; -import org.apache.rocketmq.common.protocol.ResponseCode; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.remoting.protocol.ResponseCode; + +import static org.apache.rocketmq.common.topic.TopicValidator.isTopicOrGroupIllegal; /** * Common Validator */ public class Validators { - public static final String VALID_PATTERN_STR = "^[%|a-zA-Z0-9_-]+$"; - public static final Pattern PATTERN = Pattern.compile(VALID_PATTERN_STR); public static final int CHARACTER_MAX_LENGTH = 255; - - /** - * @return The resulting {@code String} - */ - public static String getGroupWithRegularExpression(String origin, String patternStr) { - Pattern pattern = Pattern.compile(patternStr); - Matcher matcher = pattern.matcher(origin); - while (matcher.find()) { - return matcher.group(0); - } - return null; - } + public static final int TOPIC_MAX_LENGTH = 127; /** * Validate group @@ -53,37 +46,26 @@ public static void checkGroup(String group) throws MQClientException { if (UtilAll.isBlank(group)) { throw new MQClientException("the specified group is blank", null); } - if (!regularExpressionMatcher(group, PATTERN)) { - throw new MQClientException(String.format( - "the specified group[%s] contains illegal characters, allowing only %s", group, - VALID_PATTERN_STR), null); - } + if (group.length() > CHARACTER_MAX_LENGTH) { throw new MQClientException("the specified group is longer than group max length 255.", null); } - } - /** - * @return true if, and only if, the entire origin sequence matches this matcher's pattern - */ - public static boolean regularExpressionMatcher(String origin, Pattern pattern) { - if (pattern == null) { - return true; + + if (isTopicOrGroupIllegal(group)) { + throw new MQClientException(String.format( + "the specified group[%s] contains illegal characters, allowing only %s", group, + "^[%|a-zA-Z0-9_-]+$"), null); } - Matcher matcher = pattern.matcher(origin); - return matcher.matches(); } - /** - * Validate message - */ - public static void checkMessage(Message msg, DefaultMQProducer defaultMQProducer) - throws MQClientException { + public static void checkMessage(Message msg, DefaultMQProducer defaultMQProducer) throws MQClientException { if (null == msg) { throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, "the message is null"); } // topic Validators.checkTopic(msg.getTopic()); + Validators.isNotAllowedSendTopic(msg.getTopic()); // body if (null == msg.getBody()) { @@ -98,30 +80,58 @@ public static void checkMessage(Message msg, DefaultMQProducer defaultMQProducer throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, "the message body size over max value, MAX: " + defaultMQProducer.getMaxMessageSize()); } + + String lmqPath = msg.getUserProperty(MessageConst.PROPERTY_INNER_MULTI_DISPATCH); + if (StringUtils.contains(lmqPath, File.separator)) { + throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, + "INNER_MULTI_DISPATCH " + lmqPath + " can not contains " + File.separator + " character"); + } } - /** - * Validate topic - */ public static void checkTopic(String topic) throws MQClientException { if (UtilAll.isBlank(topic)) { throw new MQClientException("The specified topic is blank", null); } - if (!regularExpressionMatcher(topic, PATTERN)) { + if (topic.length() > TOPIC_MAX_LENGTH) { + throw new MQClientException( + String.format("The specified topic is longer than topic max length %d.", TOPIC_MAX_LENGTH), null); + } + + if (isTopicOrGroupIllegal(topic)) { throw new MQClientException(String.format( - "The specified topic[%s] contains illegal characters, allowing only %s", topic, - VALID_PATTERN_STR), null); + "The specified topic[%s] contains illegal characters, allowing only %s", topic, + "^[%|a-zA-Z0-9_-]+$"), null); } + } - if (topic.length() > CHARACTER_MAX_LENGTH) { - throw new MQClientException("The specified topic is longer than topic max length 255.", null); + public static void isSystemTopic(String topic) throws MQClientException { + if (TopicValidator.isSystemTopic(topic)) { + throw new MQClientException( + String.format("The topic[%s] is conflict with system topic.", topic), null); } + } - //whether the same with system reserved keyword - if (topic.equals(MixAll.DEFAULT_TOPIC)) { + public static void isNotAllowedSendTopic(String topic) throws MQClientException { + if (TopicValidator.isNotAllowedSendTopic(topic)) { throw new MQClientException( - String.format("The topic[%s] is conflict with default topic.", topic), null); + String.format("Sending message to topic[%s] is forbidden.", topic), null); + } + } + + public static void checkTopicConfig(final TopicConfig topicConfig) throws MQClientException { + if (!PermName.isValid(topicConfig.getPerm())) { + throw new MQClientException(ResponseCode.NO_PERMISSION, + String.format("topicPermission value: %s is invalid.", topicConfig.getPerm())); + } + } + + public static void checkBrokerConfig(final Properties brokerConfig) throws MQClientException { + // TODO: use MixAll.isPropertyValid() when jdk upgrade to 1.8 + if (brokerConfig.containsKey("brokerPermission") + && !PermName.isValid(brokerConfig.getProperty("brokerPermission"))) { + throw new MQClientException(ResponseCode.NO_PERMISSION, + String.format("brokerPermission value: %s is invalid.", brokerConfig.getProperty("brokerPermission"))); } } } diff --git a/client/src/main/java/org/apache/rocketmq/client/common/ClientErrorCode.java b/client/src/main/java/org/apache/rocketmq/client/common/ClientErrorCode.java index 62a95dfa5a1..bc03b14df24 100644 --- a/client/src/main/java/org/apache/rocketmq/client/common/ClientErrorCode.java +++ b/client/src/main/java/org/apache/rocketmq/client/common/ClientErrorCode.java @@ -23,4 +23,6 @@ public class ClientErrorCode { public static final int BROKER_NOT_EXIST_EXCEPTION = 10003; public static final int NO_NAME_SERVER_EXCEPTION = 10004; public static final int NOT_FOUND_TOPIC_EXCEPTION = 10005; + public static final int REQUEST_TIMEOUT_EXCEPTION = 10006; + public static final int CREATE_REPLY_MESSAGE_EXCEPTION = 10007; } \ No newline at end of file diff --git a/client/src/main/java/org/apache/rocketmq/client/common/NameserverAccessConfig.java b/client/src/main/java/org/apache/rocketmq/client/common/NameserverAccessConfig.java new file mode 100644 index 00000000000..2cdae48ee7c --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/common/NameserverAccessConfig.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client.common; + +public class NameserverAccessConfig { + private String namesrvAddr; + private String namesrvDomain; + private String namesrvDomainSubgroup; + + public NameserverAccessConfig(String namesrvAddr, String namesrvDomain, String namesrvDomainSubgroup) { + this.namesrvAddr = namesrvAddr; + this.namesrvDomain = namesrvDomain; + this.namesrvDomainSubgroup = namesrvDomainSubgroup; + } + + public String getNamesrvAddr() { + return namesrvAddr; + } + + public String getNamesrvDomain() { + return namesrvDomain; + } + + public String getNamesrvDomainSubgroup() { + return namesrvDomainSubgroup; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/common/ThreadLocalIndex.java b/client/src/main/java/org/apache/rocketmq/client/common/ThreadLocalIndex.java index ab223c3ecac..c15cdbfadba 100644 --- a/client/src/main/java/org/apache/rocketmq/client/common/ThreadLocalIndex.java +++ b/client/src/main/java/org/apache/rocketmq/client/common/ThreadLocalIndex.java @@ -20,24 +20,22 @@ import java.util.Random; public class ThreadLocalIndex { - private final ThreadLocal threadLocalIndex = new ThreadLocal(); + private final ThreadLocal threadLocalIndex = new ThreadLocal<>(); private final Random random = new Random(); + private final static int POSITIVE_MASK = 0x7FFFFFFF; - public int getAndIncrement() { + public int incrementAndGet() { Integer index = this.threadLocalIndex.get(); if (null == index) { - index = Math.abs(random.nextInt()); - if (index < 0) - index = 0; - this.threadLocalIndex.set(index); + index = random.nextInt(); } + this.threadLocalIndex.set(++index); + return index & POSITIVE_MASK; + } - index = Math.abs(index + 1); - if (index < 0) - index = 0; - + public void reset() { + int index = Math.abs(random.nextInt(Integer.MAX_VALUE)); this.threadLocalIndex.set(index); - return index; } @Override diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/AckCallback.java b/client/src/main/java/org/apache/rocketmq/client/consumer/AckCallback.java new file mode 100644 index 00000000000..99a261c9de1 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/AckCallback.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.consumer; + +public interface AckCallback { + void onSuccess(final AckResult ackResult); + + void onException(final Throwable e); +} diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/AckResult.java b/client/src/main/java/org/apache/rocketmq/client/consumer/AckResult.java new file mode 100644 index 00000000000..06cb59a293c --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/AckResult.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.consumer; + + +public class AckResult { + private AckStatus status; + private String extraInfo; + private long popTime; + + public void setPopTime(long popTime) { + this.popTime = popTime; + } + + public long getPopTime() { + return popTime; + } + + public AckStatus getStatus() { + return status; + } + + public void setStatus(AckStatus status) { + this.status = status; + } + + public void setExtraInfo(String extraInfo) { + this.extraInfo = extraInfo; + } + + public String getExtraInfo() { + return extraInfo; + } + + @Override + public String toString() { + return "AckResult [AckStatus=" + status + ",extraInfo=" + extraInfo + "]"; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/AckStatus.java b/client/src/main/java/org/apache/rocketmq/client/consumer/AckStatus.java new file mode 100644 index 00000000000..b144f8f454c --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/AckStatus.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.consumer; + +public enum AckStatus { + /** + * ack success + */ + OK, + /** + * msg not exist + */ + NO_EXIST, +} diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/DefaultLitePullConsumer.java b/client/src/main/java/org/apache/rocketmq/client/consumer/DefaultLitePullConsumer.java new file mode 100644 index 00000000000..c193c6a42e4 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/DefaultLitePullConsumer.java @@ -0,0 +1,624 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.consumer; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.client.consumer.rebalance.AllocateMessageQueueAveragely; +import org.apache.rocketmq.client.consumer.store.OffsetStore; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.impl.consumer.DefaultLitePullConsumerImpl; +import org.apache.rocketmq.client.trace.AsyncTraceDispatcher; +import org.apache.rocketmq.client.trace.TraceDispatcher; +import org.apache.rocketmq.client.trace.hook.ConsumeMessageTraceHookImpl; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +import static org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData.SUB_ALL; + +public class DefaultLitePullConsumer extends ClientConfig implements LitePullConsumer { + + private static final Logger log = LoggerFactory.getLogger(DefaultLitePullConsumer.class); + + private final DefaultLitePullConsumerImpl defaultLitePullConsumerImpl; + + /** + * Consumers belonging to the same consumer group share a group id. The consumers in a group then divides the topic + * as fairly amongst themselves as possible by establishing that each queue is only consumed by a single consumer + * from the group. If all consumers are from the same group, it functions as a traditional message queue. Each + * message would be consumed by one consumer of the group only. When multiple consumer groups exist, the flow of the + * data consumption model aligns with the traditional publish-subscribe model. The messages are broadcast to all + * consumer groups. + */ + private String consumerGroup; + + /** + * Long polling mode, the Consumer connection max suspend time, it is not recommended to modify + */ + private long brokerSuspendMaxTimeMillis = 1000 * 20; + + /** + * Long polling mode, the Consumer connection timeout(must greater than brokerSuspendMaxTimeMillis), it is not + * recommended to modify + */ + private long consumerTimeoutMillisWhenSuspend = 1000 * 30; + + /** + * The socket timeout in milliseconds + */ + private long consumerPullTimeoutMillis = 1000 * 10; + + /** + * Consumption pattern,default is clustering + */ + private MessageModel messageModel = MessageModel.CLUSTERING; + /** + * Message queue listener + */ + private MessageQueueListener messageQueueListener; + /** + * Offset Storage + */ + private OffsetStore offsetStore; + + /** + * Queue allocation algorithm + */ + private AllocateMessageQueueStrategy allocateMessageQueueStrategy = new AllocateMessageQueueAveragely(); + /** + * Whether the unit of subscription group + */ + private boolean unitMode = false; + + /** + * The flag for auto commit offset + */ + private boolean autoCommit = true; + + /** + * Pull thread number + */ + private int pullThreadNums = 20; + + /** + * Minimum commit offset interval time in milliseconds. + */ + private static final long MIN_AUTOCOMMIT_INTERVAL_MILLIS = 1000; + + /** + * Maximum commit offset interval time in milliseconds. + */ + private long autoCommitIntervalMillis = 5 * 1000; + + /** + * Maximum number of messages pulled each time. + */ + private int pullBatchSize = 10; + + /** + * Flow control threshold for consume request, each consumer will cache at most 10000 consume requests by default. + * Consider the {@code pullBatchSize}, the instantaneous value may exceed the limit + */ + private long pullThresholdForAll = 10000; + + /** + * Consume max span offset. + */ + private int consumeMaxSpan = 2000; + + /** + * Flow control threshold on queue level, each message queue will cache at most 1000 messages by default, Consider + * the {@code pullBatchSize}, the instantaneous value may exceed the limit + */ + private int pullThresholdForQueue = 1000; + + /** + * Limit the cached message size on queue level, each message queue will cache at most 100 MiB messages by default, + * Consider the {@code pullBatchSize}, the instantaneous value may exceed the limit + * + *

+ * The size of a message only measured by message body, so it's not accurate + */ + private int pullThresholdSizeForQueue = 100; + + /** + * The poll timeout in milliseconds + */ + private long pollTimeoutMillis = 1000 * 5; + + /** + * Interval time in in milliseconds for checking changes in topic metadata. + */ + private long topicMetadataCheckIntervalMillis = 30 * 1000; + + private ConsumeFromWhere consumeFromWhere = ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET; + + /** + * Backtracking consumption time with second precision. Time format is 20131223171201
Implying Seventeen twelve + * and 01 seconds on December 23, 2013 year
Default backtracking consumption time Half an hour ago. + */ + private String consumeTimestamp = UtilAll.timeMillisToHumanString3(System.currentTimeMillis() - (1000 * 60 * 30)); + + /** + * Interface of asynchronous transfer data + */ + private TraceDispatcher traceDispatcher = null; + + /** + * The flag for message trace + */ + private boolean enableMsgTrace = false; + + /** + * The name value of message trace topic.If you don't config,you can use the default trace topic name. + */ + private String customizedTraceTopic; + + /** + * Default constructor. + */ + public DefaultLitePullConsumer() { + this(MixAll.DEFAULT_CONSUMER_GROUP, null); + } + + /** + * Constructor specifying consumer group. + * + * @param consumerGroup Consumer group. + */ + public DefaultLitePullConsumer(final String consumerGroup) { + this(consumerGroup, null); + } + + /** + * Constructor specifying RPC hook. + * + * @param rpcHook RPC hook to execute before each remoting command. + */ + public DefaultLitePullConsumer(RPCHook rpcHook) { + this(MixAll.DEFAULT_CONSUMER_GROUP, rpcHook); + } + + /** + * Constructor specifying consumer group, RPC hook + * + * @param consumerGroup Consumer group. + * @param rpcHook RPC hook to execute before each remoting command. + */ + public DefaultLitePullConsumer(final String consumerGroup, RPCHook rpcHook) { + this.consumerGroup = consumerGroup; + this.enableStreamRequestType = true; + defaultLitePullConsumerImpl = new DefaultLitePullConsumerImpl(this, rpcHook); + } + + /** + * Constructor specifying namespace, consumer group and RPC hook. + * + * @param consumerGroup Consumer group. + * @param rpcHook RPC hook to execute before each remoting command. + */ + @Deprecated + public DefaultLitePullConsumer(final String namespace, final String consumerGroup, RPCHook rpcHook) { + this.namespace = namespace; + this.consumerGroup = consumerGroup; + this.enableStreamRequestType = true; + defaultLitePullConsumerImpl = new DefaultLitePullConsumerImpl(this, rpcHook); + } + + @Override + public void start() throws MQClientException { + setTraceDispatcher(); + setConsumerGroup(NamespaceUtil.wrapNamespace(this.getNamespace(), this.consumerGroup)); + this.defaultLitePullConsumerImpl.start(); + if (null != traceDispatcher) { + try { + traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel()); + } catch (MQClientException e) { + log.warn("trace dispatcher start failed ", e); + } + } + } + + @Override + public void shutdown() { + this.defaultLitePullConsumerImpl.shutdown(); + if (null != traceDispatcher) { + traceDispatcher.shutdown(); + } + } + + @Override + public boolean isRunning() { + return this.defaultLitePullConsumerImpl.isRunning(); + } + + @Override + public void subscribe(String topic) throws MQClientException { + this.subscribe(topic, SUB_ALL); + } + + @Override + public void subscribe(String topic, String subExpression) throws MQClientException { + this.defaultLitePullConsumerImpl.subscribe(withNamespace(topic), subExpression); + } + + @Override + public void subscribe(String topic, MessageSelector messageSelector) throws MQClientException { + this.defaultLitePullConsumerImpl.subscribe(withNamespace(topic), messageSelector); + } + + @Override + public void unsubscribe(String topic) { + this.defaultLitePullConsumerImpl.unsubscribe(withNamespace(topic)); + } + @Override + public void assign(Collection messageQueues) { + defaultLitePullConsumerImpl.assign(queuesWithNamespace(messageQueues)); + } + + @Override + public void setSubExpressionForAssign(final String topic, final String subExpresion) { + defaultLitePullConsumerImpl.setSubExpressionForAssign(withNamespace(topic), subExpresion); + } + + @Override + public List poll() { + return defaultLitePullConsumerImpl.poll(this.getPollTimeoutMillis()); + } + + @Override + public List poll(long timeout) { + return defaultLitePullConsumerImpl.poll(timeout); + } + + @Override + public void seek(MessageQueue messageQueue, long offset) throws MQClientException { + this.defaultLitePullConsumerImpl.seek(queueWithNamespace(messageQueue), offset); + } + + @Override + public void pause(Collection messageQueues) { + this.defaultLitePullConsumerImpl.pause(queuesWithNamespace(messageQueues)); + } + + @Override + public void resume(Collection messageQueues) { + this.defaultLitePullConsumerImpl.resume(queuesWithNamespace(messageQueues)); + } + + @Override + public Collection fetchMessageQueues(String topic) throws MQClientException { + return this.defaultLitePullConsumerImpl.fetchMessageQueues(withNamespace(topic)); + } + + @Override + public Long offsetForTimestamp(MessageQueue messageQueue, Long timestamp) throws MQClientException { + return this.defaultLitePullConsumerImpl.searchOffset(queueWithNamespace(messageQueue), timestamp); + } + + @Override + public void registerTopicMessageQueueChangeListener(String topic, + TopicMessageQueueChangeListener topicMessageQueueChangeListener) throws MQClientException { + this.defaultLitePullConsumerImpl.registerTopicMessageQueueChangeListener(withNamespace(topic), topicMessageQueueChangeListener); + } + + @Deprecated + @Override + public void commitSync() { + this.defaultLitePullConsumerImpl.commitAll(); + } + + @Deprecated + @Override + public void commitSync(Map offsetMap, boolean persist) { + this.defaultLitePullConsumerImpl.commit(offsetMap, persist); + } + + @Override + public void commit() { + this.defaultLitePullConsumerImpl.commitAll(); + } + + @Override public void commit(Map offsetMap, boolean persist) { + this.defaultLitePullConsumerImpl.commit(offsetMap, persist); + } + + /** + * Get the MessageQueue assigned in subscribe mode + * + * @return + * @throws MQClientException + */ + @Override + public Set assignment() throws MQClientException { + return this.defaultLitePullConsumerImpl.assignment(); + } + + /** + * Subscribe some topic with subExpression and messageQueueListener + * + * @param topic + * @param subExpression + * @param messageQueueListener + */ + @Override + public void subscribe(String topic, String subExpression, MessageQueueListener messageQueueListener) throws MQClientException { + this.defaultLitePullConsumerImpl.subscribe(withNamespace(topic), subExpression, messageQueueListener); + } + + + @Override + public void commit(final Set messageQueues, boolean persist) { + this.defaultLitePullConsumerImpl.commit(messageQueues, persist); + } + + @Override + public Long committed(MessageQueue messageQueue) throws MQClientException { + return this.defaultLitePullConsumerImpl.committed(queueWithNamespace(messageQueue)); + } + + @Override + public void updateNameServerAddress(String nameServerAddress) { + this.defaultLitePullConsumerImpl.updateNameServerAddr(nameServerAddress); + } + + @Override + public void seekToBegin(MessageQueue messageQueue) throws MQClientException { + this.defaultLitePullConsumerImpl.seekToBegin(queueWithNamespace(messageQueue)); + } + + @Override + public void seekToEnd(MessageQueue messageQueue) throws MQClientException { + this.defaultLitePullConsumerImpl.seekToEnd(queueWithNamespace(messageQueue)); + } + + @Override + public boolean isAutoCommit() { + return autoCommit; + } + + @Override + public void setAutoCommit(boolean autoCommit) { + this.autoCommit = autoCommit; + } + + public boolean isConnectBrokerByUser() { + return this.defaultLitePullConsumerImpl.getPullAPIWrapper().isConnectBrokerByUser(); + } + + public void setConnectBrokerByUser(boolean connectBrokerByUser) { + this.defaultLitePullConsumerImpl.getPullAPIWrapper().setConnectBrokerByUser(connectBrokerByUser); + } + + public long getDefaultBrokerId() { + return this.defaultLitePullConsumerImpl.getPullAPIWrapper().getDefaultBrokerId(); + } + + public void setDefaultBrokerId(long defaultBrokerId) { + this.defaultLitePullConsumerImpl.getPullAPIWrapper().setDefaultBrokerId(defaultBrokerId); + } + + public int getPullThreadNums() { + return pullThreadNums; + } + + public void setPullThreadNums(int pullThreadNums) { + this.pullThreadNums = pullThreadNums; + } + + public long getAutoCommitIntervalMillis() { + return autoCommitIntervalMillis; + } + + public void setAutoCommitIntervalMillis(long autoCommitIntervalMillis) { + if (autoCommitIntervalMillis >= MIN_AUTOCOMMIT_INTERVAL_MILLIS) { + this.autoCommitIntervalMillis = autoCommitIntervalMillis; + } + } + + public int getPullBatchSize() { + return pullBatchSize; + } + + public void setPullBatchSize(int pullBatchSize) { + this.pullBatchSize = pullBatchSize; + } + + public long getPullThresholdForAll() { + return pullThresholdForAll; + } + + public void setPullThresholdForAll(long pullThresholdForAll) { + this.pullThresholdForAll = pullThresholdForAll; + } + + public int getConsumeMaxSpan() { + return consumeMaxSpan; + } + + public void setConsumeMaxSpan(int consumeMaxSpan) { + this.consumeMaxSpan = consumeMaxSpan; + } + + public int getPullThresholdForQueue() { + return pullThresholdForQueue; + } + + public void setPullThresholdForQueue(int pullThresholdForQueue) { + this.pullThresholdForQueue = pullThresholdForQueue; + } + + public int getPullThresholdSizeForQueue() { + return pullThresholdSizeForQueue; + } + + public void setPullThresholdSizeForQueue(int pullThresholdSizeForQueue) { + this.pullThresholdSizeForQueue = pullThresholdSizeForQueue; + } + + public AllocateMessageQueueStrategy getAllocateMessageQueueStrategy() { + return allocateMessageQueueStrategy; + } + + public void setAllocateMessageQueueStrategy(AllocateMessageQueueStrategy allocateMessageQueueStrategy) { + this.allocateMessageQueueStrategy = allocateMessageQueueStrategy; + } + + public long getBrokerSuspendMaxTimeMillis() { + return brokerSuspendMaxTimeMillis; + } + + public long getPollTimeoutMillis() { + return pollTimeoutMillis; + } + + public void setPollTimeoutMillis(long pollTimeoutMillis) { + this.pollTimeoutMillis = pollTimeoutMillis; + } + + public OffsetStore getOffsetStore() { + return offsetStore; + } + + public void setOffsetStore(OffsetStore offsetStore) { + this.offsetStore = offsetStore; + } + + @Override + public boolean isUnitMode() { + return unitMode; + } + + @Override + public void setUnitMode(boolean isUnitMode) { + this.unitMode = isUnitMode; + } + + public MessageModel getMessageModel() { + return messageModel; + } + + public void setMessageModel(MessageModel messageModel) { + this.messageModel = messageModel; + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public MessageQueueListener getMessageQueueListener() { + return messageQueueListener; + } + + public void setMessageQueueListener(MessageQueueListener messageQueueListener) { + this.messageQueueListener = messageQueueListener; + } + + public long getConsumerPullTimeoutMillis() { + return consumerPullTimeoutMillis; + } + + public void setConsumerPullTimeoutMillis(long consumerPullTimeoutMillis) { + this.consumerPullTimeoutMillis = consumerPullTimeoutMillis; + } + + public long getConsumerTimeoutMillisWhenSuspend() { + return consumerTimeoutMillisWhenSuspend; + } + + public void setConsumerTimeoutMillisWhenSuspend(long consumerTimeoutMillisWhenSuspend) { + this.consumerTimeoutMillisWhenSuspend = consumerTimeoutMillisWhenSuspend; + } + + public long getTopicMetadataCheckIntervalMillis() { + return topicMetadataCheckIntervalMillis; + } + + public void setTopicMetadataCheckIntervalMillis(long topicMetadataCheckIntervalMillis) { + this.topicMetadataCheckIntervalMillis = topicMetadataCheckIntervalMillis; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public ConsumeFromWhere getConsumeFromWhere() { + return consumeFromWhere; + } + + public void setConsumeFromWhere(ConsumeFromWhere consumeFromWhere) { + if (consumeFromWhere != ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET + && consumeFromWhere != ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET + && consumeFromWhere != ConsumeFromWhere.CONSUME_FROM_TIMESTAMP) { + throw new RuntimeException("Invalid ConsumeFromWhere Value", null); + } + this.consumeFromWhere = consumeFromWhere; + } + + public String getConsumeTimestamp() { + return consumeTimestamp; + } + + public void setConsumeTimestamp(String consumeTimestamp) { + this.consumeTimestamp = consumeTimestamp; + } + + public TraceDispatcher getTraceDispatcher() { + return traceDispatcher; + } + + public void setCustomizedTraceTopic(String customizedTraceTopic) { + this.customizedTraceTopic = customizedTraceTopic; + } + + private void setTraceDispatcher() { + if (isEnableMsgTrace()) { + try { + AsyncTraceDispatcher traceDispatcher = new AsyncTraceDispatcher(consumerGroup, TraceDispatcher.Type.CONSUME, customizedTraceTopic, null); + traceDispatcher.getTraceProducer().setUseTLS(this.isUseTLS()); + this.traceDispatcher = traceDispatcher; + this.defaultLitePullConsumerImpl.registerConsumeMessageHook( + new ConsumeMessageTraceHookImpl(traceDispatcher)); + } catch (Throwable e) { + log.error("system mqtrace hook init failed ,maybe can't send msg trace data"); + } + } + } + + public String getCustomizedTraceTopic() { + return customizedTraceTopic; + } + + public boolean isEnableMsgTrace() { + return enableMsgTrace; + } + + public void setEnableMsgTrace(boolean enableMsgTrace) { + this.enableMsgTrace = enableMsgTrace; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/DefaultMQPullConsumer.java b/client/src/main/java/org/apache/rocketmq/client/consumer/DefaultMQPullConsumer.java index cd706703078..499f7731915 100644 --- a/client/src/main/java/org/apache/rocketmq/client/consumer/DefaultMQPullConsumer.java +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/DefaultMQPullConsumer.java @@ -17,6 +17,7 @@ package org.apache.rocketmq.client.consumer; import java.util.HashSet; +import java.util.Map; import java.util.Set; import org.apache.rocketmq.client.ClientConfig; import org.apache.rocketmq.client.QueryResult; @@ -29,29 +30,31 @@ import org.apache.rocketmq.common.message.MessageDecoder; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; import org.apache.rocketmq.remoting.RPCHook; import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; /** - * Default pulling consumer + * @deprecated Default pulling consumer. This class will be removed in 2022, and a better implementation {@link + * DefaultLitePullConsumer} is recommend to use in the scenario of actively pulling messages. */ +@Deprecated public class DefaultMQPullConsumer extends ClientConfig implements MQPullConsumer { + protected final transient DefaultMQPullConsumerImpl defaultMQPullConsumerImpl; /** - * Do the same thing for the same Group, the application must be set,and - * guarantee Globally unique + * Do the same thing for the same Group, the application must be set,and guarantee Globally unique */ private String consumerGroup; /** - * Long polling mode, the Consumer connection max suspend time, it is not - * recommended to modify + * Long polling mode, the Consumer connection max suspend time, it is not recommended to modify */ private long brokerSuspendMaxTimeMillis = 1000 * 20; /** - * Long polling mode, the Consumer connection timeout(must greater than - * brokerSuspendMaxTimeMillis), it is not recommended to modify + * Long polling mode, the Consumer connection timeout(must greater than brokerSuspendMaxTimeMillis), it is not + * recommended to modify */ private long consumerTimeoutMillisWhenSuspend = 1000 * 30; /** @@ -73,7 +76,7 @@ public class DefaultMQPullConsumer extends ClientConfig implements MQPullConsume /** * Topic set you want to register */ - private Set registerTopics = new HashSet(); + private Set registerTopics = new HashSet<>(); /** * Queue allocation algorithm */ @@ -89,11 +92,6 @@ public DefaultMQPullConsumer() { this(MixAll.DEFAULT_CONSUMER_GROUP, null); } - public DefaultMQPullConsumer(final String consumerGroup, RPCHook rpcHook) { - this.consumerGroup = consumerGroup; - defaultMQPullConsumerImpl = new DefaultMQPullConsumerImpl(this, rpcHook); - } - public DefaultMQPullConsumer(final String consumerGroup) { this(consumerGroup, null); } @@ -102,46 +100,99 @@ public DefaultMQPullConsumer(RPCHook rpcHook) { this(MixAll.DEFAULT_CONSUMER_GROUP, rpcHook); } + public DefaultMQPullConsumer(final String consumerGroup, RPCHook rpcHook) { + this.consumerGroup = consumerGroup; + this.enableStreamRequestType = true; + defaultMQPullConsumerImpl = new DefaultMQPullConsumerImpl(this, rpcHook); + } + + /** + * Constructor specifying namespace, consumer group and RPC hook. + * + * @param consumerGroup Consumer group. + * @param rpcHook RPC hook to execute before each remoting command. + */ + public DefaultMQPullConsumer(final String namespace, final String consumerGroup, RPCHook rpcHook) { + this.namespace = namespace; + this.consumerGroup = consumerGroup; + this.enableStreamRequestType = true; + defaultMQPullConsumerImpl = new DefaultMQPullConsumerImpl(this, rpcHook); + } + + /** + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. + */ + @Deprecated @Override - public void createTopic(String key, String newTopic, int queueNum) throws MQClientException { - createTopic(key, newTopic, queueNum, 0); + public void createTopic(String key, String newTopic, int queueNum, + Map attributes) throws MQClientException { + createTopic(key, withNamespace(newTopic), queueNum, 0, null); } + /** + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. + */ + @Deprecated @Override - public void createTopic(String key, String newTopic, int queueNum, int topicSysFlag) throws MQClientException { - this.defaultMQPullConsumerImpl.createTopic(key, newTopic, queueNum, topicSysFlag); + public void createTopic(String key, String newTopic, int queueNum, int topicSysFlag, + Map attributes) throws MQClientException { + this.defaultMQPullConsumerImpl.createTopic(key, withNamespace(newTopic), queueNum, topicSysFlag); } + /** + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. + */ + @Deprecated @Override public long searchOffset(MessageQueue mq, long timestamp) throws MQClientException { - return this.defaultMQPullConsumerImpl.searchOffset(mq, timestamp); + return this.defaultMQPullConsumerImpl.searchOffset(queueWithNamespace(mq), timestamp); } + /** + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. + */ + @Deprecated @Override public long maxOffset(MessageQueue mq) throws MQClientException { - return this.defaultMQPullConsumerImpl.maxOffset(mq); + return this.defaultMQPullConsumerImpl.maxOffset(queueWithNamespace(mq)); } + /** + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. + */ + @Deprecated @Override public long minOffset(MessageQueue mq) throws MQClientException { - return this.defaultMQPullConsumerImpl.minOffset(mq); + return this.defaultMQPullConsumerImpl.minOffset(queueWithNamespace(mq)); } + /** + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. + */ + @Deprecated @Override public long earliestMsgStoreTime(MessageQueue mq) throws MQClientException { - return this.defaultMQPullConsumerImpl.earliestMsgStoreTime(mq); + return this.defaultMQPullConsumerImpl.earliestMsgStoreTime(queueWithNamespace(mq)); } + /** + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. + */ + @Deprecated @Override public MessageExt viewMessage(String offsetMsgId) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { return this.defaultMQPullConsumerImpl.viewMessage(offsetMsgId); } + /** + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. + */ + @Deprecated @Override public QueryResult queryMessage(String topic, String key, int maxNum, long begin, long end) throws MQClientException, InterruptedException { - return this.defaultMQPullConsumerImpl.queryMessage(topic, key, maxNum, begin, end); + return this.defaultMQPullConsumerImpl.queryMessage(withNamespace(topic), key, maxNum, begin, end); } public AllocateMessageQueueStrategy getAllocateMessageQueueStrategy() { @@ -156,6 +207,10 @@ public long getBrokerSuspendMaxTimeMillis() { return brokerSuspendMaxTimeMillis; } + /** + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. + */ + @Deprecated public void setBrokerSuspendMaxTimeMillis(long brokerSuspendMaxTimeMillis) { this.brokerSuspendMaxTimeMillis = brokerSuspendMaxTimeMillis; } @@ -205,28 +260,41 @@ public Set getRegisterTopics() { } public void setRegisterTopics(Set registerTopics) { - this.registerTopics = registerTopics; + this.registerTopics = withNamespace(registerTopics); } + /** + * This method will be removed or it's visibility will be changed in a certain version after April 5, 2020, so + * please do not use this method. + */ + @Deprecated @Override public void sendMessageBack(MessageExt msg, int delayLevel) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + msg.setTopic(withNamespace(msg.getTopic())); this.defaultMQPullConsumerImpl.sendMessageBack(msg, delayLevel, null); } + /** + * This method will be removed or it's visibility will be changed in a certain version after April 5, 2020, so + * please do not use this method. + */ + @Deprecated @Override public void sendMessageBack(MessageExt msg, int delayLevel, String brokerName) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + msg.setTopic(withNamespace(msg.getTopic())); this.defaultMQPullConsumerImpl.sendMessageBack(msg, delayLevel, brokerName); } @Override public Set fetchSubscribeMessageQueues(String topic) throws MQClientException { - return this.defaultMQPullConsumerImpl.fetchSubscribeMessageQueues(topic); + return this.defaultMQPullConsumerImpl.fetchSubscribeMessageQueues(withNamespace(topic)); } @Override public void start() throws MQClientException { + this.setConsumerGroup(NamespaceUtil.wrapNamespace(this.getNamespace(), this.consumerGroup)); this.defaultMQPullConsumerImpl.start(); } @@ -238,7 +306,7 @@ public void shutdown() { @Override public void registerMessageQueueListener(String topic, MessageQueueListener listener) { synchronized (this.registerTopics) { - this.registerTopics.add(topic); + this.registerTopics.add(withNamespace(topic)); if (listener != null) { this.messageQueueListener = listener; } @@ -248,54 +316,88 @@ public void registerMessageQueueListener(String topic, MessageQueueListener list @Override public PullResult pull(MessageQueue mq, String subExpression, long offset, int maxNums) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { - return this.defaultMQPullConsumerImpl.pull(mq, subExpression, offset, maxNums); + return this.defaultMQPullConsumerImpl.pull(queueWithNamespace(mq), subExpression, offset, maxNums); } @Override public PullResult pull(MessageQueue mq, String subExpression, long offset, int maxNums, long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { - return this.defaultMQPullConsumerImpl.pull(mq, subExpression, offset, maxNums, timeout); + return this.defaultMQPullConsumerImpl.pull(queueWithNamespace(mq), subExpression, offset, maxNums, timeout); + } + + @Override + public PullResult pull(MessageQueue mq, MessageSelector messageSelector, long offset, int maxNums) + throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + return this.defaultMQPullConsumerImpl.pull(queueWithNamespace(mq), messageSelector, offset, maxNums); + } + + @Override + public PullResult pull(MessageQueue mq, MessageSelector messageSelector, long offset, int maxNums, long timeout) + throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + return this.defaultMQPullConsumerImpl.pull(queueWithNamespace(mq), messageSelector, offset, maxNums, timeout); } @Override public void pull(MessageQueue mq, String subExpression, long offset, int maxNums, PullCallback pullCallback) throws MQClientException, RemotingException, InterruptedException { - this.defaultMQPullConsumerImpl.pull(mq, subExpression, offset, maxNums, pullCallback); + this.defaultMQPullConsumerImpl.pull(queueWithNamespace(mq), subExpression, offset, maxNums, pullCallback); } @Override public void pull(MessageQueue mq, String subExpression, long offset, int maxNums, PullCallback pullCallback, long timeout) throws MQClientException, RemotingException, InterruptedException { - this.defaultMQPullConsumerImpl.pull(mq, subExpression, offset, maxNums, pullCallback, timeout); + this.defaultMQPullConsumerImpl.pull(queueWithNamespace(mq), subExpression, offset, maxNums, pullCallback, timeout); + } + + @Override + public void pull(MessageQueue mq, String subExpression, long offset, int maxNums, int maxSize, + PullCallback pullCallback, + long timeout) + throws MQClientException, RemotingException, InterruptedException { + this.defaultMQPullConsumerImpl.pull(mq, subExpression, offset, maxNums, maxSize, pullCallback, timeout); + } + + @Override + public void pull(MessageQueue mq, MessageSelector messageSelector, long offset, int maxNums, + PullCallback pullCallback) + throws MQClientException, RemotingException, InterruptedException { + this.defaultMQPullConsumerImpl.pull(queueWithNamespace(mq), messageSelector, offset, maxNums, pullCallback); + } + + @Override + public void pull(MessageQueue mq, MessageSelector messageSelector, long offset, int maxNums, + PullCallback pullCallback, long timeout) + throws MQClientException, RemotingException, InterruptedException { + this.defaultMQPullConsumerImpl.pull(queueWithNamespace(mq), messageSelector, offset, maxNums, pullCallback, timeout); } @Override public PullResult pullBlockIfNotFound(MessageQueue mq, String subExpression, long offset, int maxNums) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { - return this.defaultMQPullConsumerImpl.pullBlockIfNotFound(mq, subExpression, offset, maxNums); + return this.defaultMQPullConsumerImpl.pullBlockIfNotFound(queueWithNamespace(mq), subExpression, offset, maxNums); } @Override public void pullBlockIfNotFound(MessageQueue mq, String subExpression, long offset, int maxNums, PullCallback pullCallback) throws MQClientException, RemotingException, InterruptedException { - this.defaultMQPullConsumerImpl.pullBlockIfNotFound(mq, subExpression, offset, maxNums, pullCallback); + this.defaultMQPullConsumerImpl.pullBlockIfNotFound(queueWithNamespace(mq), subExpression, offset, maxNums, pullCallback); } @Override public void updateConsumeOffset(MessageQueue mq, long offset) throws MQClientException { - this.defaultMQPullConsumerImpl.updateConsumeOffset(mq, offset); + this.defaultMQPullConsumerImpl.updateConsumeOffset(queueWithNamespace(mq), offset); } @Override public long fetchConsumeOffset(MessageQueue mq, boolean fromStore) throws MQClientException { - return this.defaultMQPullConsumerImpl.fetchConsumeOffset(mq, fromStore); + return this.defaultMQPullConsumerImpl.fetchConsumeOffset(queueWithNamespace(mq), fromStore); } @Override public Set fetchMessageQueuesInBalance(String topic) throws MQClientException { - return this.defaultMQPullConsumerImpl.fetchMessageQueuesInBalance(topic); + return this.defaultMQPullConsumerImpl.fetchMessageQueuesInBalance(withNamespace(topic)); } @Override @@ -307,31 +409,46 @@ public MessageExt viewMessage(String topic, } catch (Exception e) { // Ignore } - return this.defaultMQPullConsumerImpl.queryMessageByUniqKey(topic, uniqKey); + return this.defaultMQPullConsumerImpl.queryMessageByUniqKey(withNamespace(topic), uniqKey); } @Override public void sendMessageBack(MessageExt msg, int delayLevel, String brokerName, String consumerGroup) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + msg.setTopic(withNamespace(msg.getTopic())); this.defaultMQPullConsumerImpl.sendMessageBack(msg, delayLevel, brokerName, consumerGroup); } + /** + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. + */ + @Deprecated public OffsetStore getOffsetStore() { return offsetStore; } + /** + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. + */ + @Deprecated public void setOffsetStore(OffsetStore offsetStore) { this.offsetStore = offsetStore; } + /** + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. + */ + @Deprecated public DefaultMQPullConsumerImpl getDefaultMQPullConsumerImpl() { return defaultMQPullConsumerImpl; } + @Override public boolean isUnitMode() { return unitMode; } + @Override public void setUnitMode(boolean isUnitMode) { this.unitMode = isUnitMode; } @@ -343,4 +460,8 @@ public int getMaxReconsumeTimes() { public void setMaxReconsumeTimes(final int maxReconsumeTimes) { this.maxReconsumeTimes = maxReconsumeTimes; } + + public void persist(MessageQueue mq) { + this.getOffsetStore().persist(queueWithNamespace(mq)); + } } diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/DefaultMQPushConsumer.java b/client/src/main/java/org/apache/rocketmq/client/consumer/DefaultMQPushConsumer.java index d51030a1592..224ea67d5fb 100644 --- a/client/src/main/java/org/apache/rocketmq/client/consumer/DefaultMQPushConsumer.java +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/DefaultMQPushConsumer.java @@ -18,6 +18,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import org.apache.rocketmq.client.ClientConfig; import org.apache.rocketmq.client.QueryResult; @@ -28,16 +29,23 @@ import org.apache.rocketmq.client.consumer.store.OffsetStore; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.hook.ConsumeMessageHook; import org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl; +import org.apache.rocketmq.client.trace.AsyncTraceDispatcher; +import org.apache.rocketmq.client.trace.TraceDispatcher; +import org.apache.rocketmq.client.trace.hook.ConsumeMessageTraceHookImpl; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.UtilAll; import org.apache.rocketmq.common.consumer.ConsumeFromWhere; import org.apache.rocketmq.common.message.MessageDecoder; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; import org.apache.rocketmq.remoting.RPCHook; import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; /** * In most scenarios, this is the mostly recommended class to consume messages. @@ -56,6 +64,8 @@ */ public class DefaultMQPushConsumer extends ClientConfig implements MQPushConsumer { + private final Logger log = LoggerFactory.getLogger(DefaultMQPushConsumer.class); + /** * Internal implementation. Most of the functions herein are delegated to it. */ @@ -66,7 +76,7 @@ public class DefaultMQPushConsumer extends ClientConfig implements MQPushConsume * load balance. It's required and needs to be globally unique. *

* - * See here for further discussion. + * See here for further discussion. */ private String consumerGroup; @@ -133,13 +143,18 @@ public class DefaultMQPushConsumer extends ClientConfig implements MQPushConsume /** * Subscription relationship */ - private Map subscription = new HashMap(); + private Map subscription = new HashMap<>(); /** * Message listener */ private MessageListener messageListener; + /** + * Listener to call if message queue assignment is changed. + */ + private MessageQueueListener messageQueueListener; + /** * Offset Storage */ @@ -153,7 +168,7 @@ public class DefaultMQPushConsumer extends ClientConfig implements MQPushConsume /** * Max consumer thread number */ - private int consumeThreadMax = 64; + private int consumeThreadMax = 20; /** * Threshold for dynamic adjustment of the number of thread pool @@ -171,20 +186,26 @@ public class DefaultMQPushConsumer extends ClientConfig implements MQPushConsume */ private int pullThresholdForQueue = 1000; + /** + * Flow control threshold on queue level, means max num of messages waiting to ack. + * in contrast with pull threshold, once a message is popped, it's considered the beginning of consumption. + */ + private int popThresholdForQueue = 96; + /** * Limit the cached message size on queue level, each message queue will cache at most 100 MiB messages by default, * Consider the {@code pullBatchSize}, the instantaneous value may exceed the limit * *

- * The size of a message only measured by message body, so it's not accurate + * The size(MB) of a message only measured by message body, so it's not accurate */ private int pullThresholdSizeForQueue = 100; /** * Flow control threshold on topic level, default value is -1(Unlimited) *

- * The value of {@code pullThresholdForQueue} will be overwrote and calculated based on - * {@code pullThresholdForTopic} if it is't unlimited + * The value of {@code pullThresholdForQueue} will be overwritten and calculated based on + * {@code pullThresholdForTopic} if it isn't unlimited *

* For example, if the value of pullThresholdForTopic is 1000 and 10 message queues are assigned to this consumer, * then pullThresholdForQueue will be set to 100 @@ -194,8 +215,8 @@ public class DefaultMQPushConsumer extends ClientConfig implements MQPushConsume /** * Limit the cached message size on topic level, default value is -1 MiB(Unlimited) *

- * The value of {@code pullThresholdSizeForQueue} will be overwrote and calculated based on - * {@code pullThresholdSizeForTopic} if it is't unlimited + * The value of {@code pullThresholdSizeForQueue} will be overwritten and calculated based on + * {@code pullThresholdSizeForTopic} if it isn't unlimited *

* For example, if the value of pullThresholdSizeForTopic is 1000 MiB and 10 message queues are * assigned to this consumer, then pullThresholdSizeForQueue will be set to 100 MiB @@ -217,6 +238,9 @@ public class DefaultMQPushConsumer extends ClientConfig implements MQPushConsume */ private int pullBatchSize = 32; + + private int pullBatchSizeInBytes = 256 * 1024; + /** * Whether update subscription relationship when every pull */ @@ -228,11 +252,11 @@ public class DefaultMQPushConsumer extends ClientConfig implements MQPushConsume private boolean unitMode = false; /** - * Max re-consume times. -1 means 16 times. - *

+ * Max re-consume times. + * In concurrently mode, -1 means 16; + * In orderly mode, -1 means Integer.MAX_VALUE. * - * If messages are re-consumed more than {@link #maxReconsumeTimes} before success, it's be directed to a deletion - * queue waiting. + * If messages are re-consumed more than {@link #maxReconsumeTimes} before success. */ private int maxReconsumeTimes = -1; @@ -246,6 +270,29 @@ public class DefaultMQPushConsumer extends ClientConfig implements MQPushConsume */ private long consumeTimeout = 15; + /** + * Maximum amount of invisible time in millisecond of a message, rang is [5000, 300000] + */ + private long popInvisibleTime = 60000; + + /** + * Batch pop size. range is [1, 32] + */ + private int popBatchNums = 32; + + /** + * Maximum time to await message consuming when shutdown consumer, 0 indicates no await. + */ + private long awaitTerminationMillisWhenShutdown = 0; + + /** + * Interface of asynchronous transfer data + */ + private TraceDispatcher traceDispatcher = null; + + // force to use client rebalance + private boolean clientRebalance = true; + /** * Default constructor. */ @@ -253,12 +300,55 @@ public DefaultMQPushConsumer() { this(MixAll.DEFAULT_CONSUMER_GROUP, null, new AllocateMessageQueueAveragely()); } + /** + * Constructor specifying consumer group. + * + * @param consumerGroup Consumer group. + */ + public DefaultMQPushConsumer(final String consumerGroup) { + this(consumerGroup, null, new AllocateMessageQueueAveragely()); + } + + + /** + * Constructor specifying RPC hook. + * + * @param rpcHook RPC hook to execute before each remoting command. + */ + public DefaultMQPushConsumer(RPCHook rpcHook) { + this(MixAll.DEFAULT_CONSUMER_GROUP, rpcHook, new AllocateMessageQueueAveragely()); + } + + /** + * Constructor specifying consumer group, RPC hook. + * + * @param consumerGroup Consumer group. + * @param rpcHook RPC hook to execute before each remoting command. + */ + public DefaultMQPushConsumer(final String consumerGroup, RPCHook rpcHook) { + this.consumerGroup = consumerGroup; + this.allocateMessageQueueStrategy = new AllocateMessageQueueAveragely(); + defaultMQPushConsumerImpl = new DefaultMQPushConsumerImpl(this, rpcHook); + } + + + /** + * Constructor specifying consumer group, enabled msg trace flag and customized trace topic name. + * + * @param consumerGroup Consumer group. + * @param enableMsgTrace Switch flag instance for message trace. + * @param customizedTraceTopic The name value of message trace topic.If you don't config,you can use the default trace topic name. + */ + public DefaultMQPushConsumer(final String consumerGroup, boolean enableMsgTrace, final String customizedTraceTopic) { + this(consumerGroup, null, new AllocateMessageQueueAveragely(), enableMsgTrace, customizedTraceTopic); + } + /** * Constructor specifying consumer group, RPC hook and message queue allocating algorithm. * * @param consumerGroup Consume queue. * @param rpcHook RPC hook to execute before each remoting command. - * @param allocateMessageQueueStrategy message queue allocating algorithm. + * @param allocateMessageQueueStrategy Message queue allocating algorithm. */ public DefaultMQPushConsumer(final String consumerGroup, RPCHook rpcHook, AllocateMessageQueueStrategy allocateMessageQueueStrategy) { @@ -268,65 +358,186 @@ public DefaultMQPushConsumer(final String consumerGroup, RPCHook rpcHook, } /** - * Constructor specifying RPC hook. + * Constructor specifying consumer group, RPC hook, message queue allocating algorithm, enabled msg trace flag and customized trace topic name. * + * @param consumerGroup Consume queue. * @param rpcHook RPC hook to execute before each remoting command. + * @param allocateMessageQueueStrategy message queue allocating algorithm. + * @param enableMsgTrace Switch flag instance for message trace. + * @param customizedTraceTopic The name value of message trace topic.If you don't config,you can use the default trace topic name. */ - public DefaultMQPushConsumer(RPCHook rpcHook) { - this(MixAll.DEFAULT_CONSUMER_GROUP, rpcHook, new AllocateMessageQueueAveragely()); + public DefaultMQPushConsumer(final String consumerGroup, RPCHook rpcHook, + AllocateMessageQueueStrategy allocateMessageQueueStrategy, boolean enableMsgTrace, final String customizedTraceTopic) { + this.consumerGroup = consumerGroup; + this.allocateMessageQueueStrategy = allocateMessageQueueStrategy; + defaultMQPushConsumerImpl = new DefaultMQPushConsumerImpl(this, rpcHook); + if (enableMsgTrace) { + try { + AsyncTraceDispatcher dispatcher = new AsyncTraceDispatcher(consumerGroup, TraceDispatcher.Type.CONSUME, customizedTraceTopic, rpcHook); + dispatcher.setHostConsumer(this.defaultMQPushConsumerImpl); + traceDispatcher = dispatcher; + this.defaultMQPushConsumerImpl.registerConsumeMessageHook(new ConsumeMessageTraceHookImpl(traceDispatcher)); + } catch (Throwable e) { + log.error("system mqtrace hook init failed ,maybe can't send msg trace data"); + } + } } /** - * Constructor specifying consumer group. + * Constructor specifying namespace and consumer group. * + * @param namespace Namespace for this MQ Producer instance. * @param consumerGroup Consumer group. */ - public DefaultMQPushConsumer(final String consumerGroup) { - this(consumerGroup, null, new AllocateMessageQueueAveragely()); + @Deprecated + public DefaultMQPushConsumer(final String namespace, final String consumerGroup) { + this(namespace, consumerGroup, null, new AllocateMessageQueueAveragely()); + } + + /** + * Constructor specifying namespace, consumer group and RPC hook . + * + * @param namespace Namespace for this MQ Producer instance. + * @param consumerGroup Consumer group. + * @param rpcHook RPC hook to execute before each remoting command. + */ + @Deprecated + public DefaultMQPushConsumer(final String namespace, final String consumerGroup, RPCHook rpcHook) { + this(namespace, consumerGroup, rpcHook, new AllocateMessageQueueAveragely()); + } + + /** + * Constructor specifying namespace, consumer group, RPC hook and message queue allocating algorithm. + * + * @param namespace Namespace for this MQ Producer instance. + * @param consumerGroup Consume queue. + * @param rpcHook RPC hook to execute before each remoting command. + * @param allocateMessageQueueStrategy Message queue allocating algorithm. + */ + @Deprecated + public DefaultMQPushConsumer(final String namespace, final String consumerGroup, RPCHook rpcHook, + AllocateMessageQueueStrategy allocateMessageQueueStrategy) { + this.consumerGroup = consumerGroup; + this.namespace = namespace; + this.allocateMessageQueueStrategy = allocateMessageQueueStrategy; + defaultMQPushConsumerImpl = new DefaultMQPushConsumerImpl(this, rpcHook); + } + + /** + * Constructor specifying namespace, consumer group, RPC hook, message queue allocating algorithm, enabled msg trace flag and customized trace topic name. + * + * @param namespace Namespace for this MQ Producer instance. + * @param consumerGroup Consume queue. + * @param rpcHook RPC hook to execute before each remoting command. + * @param allocateMessageQueueStrategy message queue allocating algorithm. + * @param enableMsgTrace Switch flag instance for message trace. + * @param customizedTraceTopic The name value of message trace topic.If you don't config,you can use the default trace topic name. + */ + @Deprecated + public DefaultMQPushConsumer(final String namespace, final String consumerGroup, RPCHook rpcHook, + AllocateMessageQueueStrategy allocateMessageQueueStrategy, boolean enableMsgTrace, final String customizedTraceTopic) { + this.consumerGroup = consumerGroup; + this.namespace = namespace; + this.allocateMessageQueueStrategy = allocateMessageQueueStrategy; + defaultMQPushConsumerImpl = new DefaultMQPushConsumerImpl(this, rpcHook); + if (enableMsgTrace) { + try { + AsyncTraceDispatcher dispatcher = new AsyncTraceDispatcher(consumerGroup, TraceDispatcher.Type.CONSUME, customizedTraceTopic, rpcHook); + dispatcher.setHostConsumer(this.defaultMQPushConsumerImpl); + traceDispatcher = dispatcher; + this.defaultMQPushConsumerImpl.registerConsumeMessageHook(new ConsumeMessageTraceHookImpl(traceDispatcher)); + } catch (Throwable e) { + log.error("system mqtrace hook init failed ,maybe can't send msg trace data"); + } + } } + /** + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. + */ + @Deprecated @Override - public void createTopic(String key, String newTopic, int queueNum) throws MQClientException { - createTopic(key, newTopic, queueNum, 0); + public void createTopic(String key, String newTopic, int queueNum, Map attributes) throws MQClientException { + createTopic(key, withNamespace(newTopic), queueNum, 0, null); } @Override - public void createTopic(String key, String newTopic, int queueNum, int topicSysFlag) throws MQClientException { - this.defaultMQPushConsumerImpl.createTopic(key, newTopic, queueNum, topicSysFlag); + public void setUseTLS(boolean useTLS) { + super.setUseTLS(useTLS); + if (traceDispatcher instanceof AsyncTraceDispatcher) { + ((AsyncTraceDispatcher) traceDispatcher).getTraceProducer().setUseTLS(useTLS); + } } + /** + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. + */ + @Deprecated + @Override + public void createTopic(String key, String newTopic, int queueNum, int topicSysFlag, Map attributes) throws MQClientException { + this.defaultMQPushConsumerImpl.createTopic(key, withNamespace(newTopic), queueNum, topicSysFlag); + } + + /** + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. + */ + @Deprecated @Override public long searchOffset(MessageQueue mq, long timestamp) throws MQClientException { - return this.defaultMQPushConsumerImpl.searchOffset(mq, timestamp); + return this.defaultMQPushConsumerImpl.searchOffset(queueWithNamespace(mq), timestamp); } + /** + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. + */ + @Deprecated @Override public long maxOffset(MessageQueue mq) throws MQClientException { - return this.defaultMQPushConsumerImpl.maxOffset(mq); + return this.defaultMQPushConsumerImpl.maxOffset(queueWithNamespace(mq)); } + /** + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. + */ + @Deprecated @Override public long minOffset(MessageQueue mq) throws MQClientException { - return this.defaultMQPushConsumerImpl.minOffset(mq); + return this.defaultMQPushConsumerImpl.minOffset(queueWithNamespace(mq)); } + /** + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. + */ + @Deprecated @Override public long earliestMsgStoreTime(MessageQueue mq) throws MQClientException { - return this.defaultMQPushConsumerImpl.earliestMsgStoreTime(mq); + return this.defaultMQPushConsumerImpl.earliestMsgStoreTime(queueWithNamespace(mq)); } + /** + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. + */ + @Deprecated @Override public MessageExt viewMessage( String offsetMsgId) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { return this.defaultMQPushConsumerImpl.viewMessage(offsetMsgId); } + /** + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. + */ + @Deprecated @Override public QueryResult queryMessage(String topic, String key, int maxNum, long begin, long end) throws MQClientException, InterruptedException { - return this.defaultMQPushConsumerImpl.queryMessage(topic, key, maxNum, begin, end); + return this.defaultMQPushConsumerImpl.queryMessage(withNamespace(topic), key, maxNum, begin, end); } + /** + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. + */ + @Deprecated @Override public MessageExt viewMessage(String topic, String msgId) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { @@ -336,7 +547,7 @@ public MessageExt viewMessage(String topic, } catch (Exception e) { // Ignore } - return this.defaultMQPushConsumerImpl.queryMessageByUniqKey(topic, msgId); + return this.defaultMQPushConsumerImpl.queryMessageByUniqKey(withNamespace(topic), msgId); } public AllocateMessageQueueStrategy getAllocateMessageQueueStrategy() { @@ -395,6 +606,10 @@ public void setConsumeThreadMin(int consumeThreadMin) { this.consumeThreadMin = consumeThreadMin; } + /** + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. + */ + @Deprecated public DefaultMQPushConsumerImpl getDefaultMQPushConsumerImpl() { return defaultMQPushConsumerImpl; } @@ -439,6 +654,14 @@ public void setPullThresholdForQueue(int pullThresholdForQueue) { this.pullThresholdForQueue = pullThresholdForQueue; } + public int getPopThresholdForQueue() { + return popThresholdForQueue; + } + + public void setPopThresholdForQueue(int popThresholdForQueue) { + this.popThresholdForQueue = popThresholdForQueue; + } + public int getPullThresholdForTopic() { return pullThresholdForTopic; } @@ -467,13 +690,24 @@ public Map getSubscription() { return subscription; } + /** + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. + */ + @Deprecated public void setSubscription(Map subscription) { - this.subscription = subscription; + Map subscriptionWithNamespace = new HashMap<>(subscription.size(), 1); + for (Entry topicEntry : subscription.entrySet()) { + subscriptionWithNamespace.put(withNamespace(topicEntry.getKey()), topicEntry.getValue()); + } + this.subscription = subscriptionWithNamespace; } /** * Send message back to broker which will be re-delivered in future. * + * This method will be removed or it's visibility will be changed in a certain version after April 5, 2020, so + * please do not use this method. + * * @param msg Message to send back. * @param delayLevel delay level. * @throws RemotingException if there is any network-tier error. @@ -481,16 +715,21 @@ public void setSubscription(Map subscription) { * @throws InterruptedException if the thread is interrupted. * @throws MQClientException if there is any client error. */ + @Deprecated @Override public void sendMessageBack(MessageExt msg, int delayLevel) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { - this.defaultMQPushConsumerImpl.sendMessageBack(msg, delayLevel, null); + msg.setTopic(withNamespace(msg.getTopic())); + this.defaultMQPushConsumerImpl.sendMessageBack(msg, delayLevel, (String) null); } /** * Send message back to the broker whose name is brokerName and the message will be re-delivered in * future. * + * This method will be removed or it's visibility will be changed in a certain version after April 5, 2020, so + * please do not use this method. + * * @param msg Message to send back. * @param delayLevel delay level. * @param brokerName broker name. @@ -499,15 +738,17 @@ public void sendMessageBack(MessageExt msg, int delayLevel) * @throws InterruptedException if the thread is interrupted. * @throws MQClientException if there is any client error. */ + @Deprecated @Override public void sendMessageBack(MessageExt msg, int delayLevel, String brokerName) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + msg.setTopic(withNamespace(msg.getTopic())); this.defaultMQPushConsumerImpl.sendMessageBack(msg, delayLevel, brokerName); } @Override public Set fetchSubscribeMessageQueues(String topic) throws MQClientException { - return this.defaultMQPushConsumerImpl.fetchSubscribeMessageQueues(topic); + return this.defaultMQPushConsumerImpl.fetchSubscribeMessageQueues(withNamespace(topic)); } /** @@ -517,7 +758,15 @@ public Set fetchSubscribeMessageQueues(String topic) throws MQClie */ @Override public void start() throws MQClientException { + setConsumerGroup(NamespaceUtil.wrapNamespace(this.getNamespace(), this.consumerGroup)); this.defaultMQPushConsumerImpl.start(); + if (null != traceDispatcher) { + try { + traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel()); + } catch (MQClientException e) { + log.warn("trace dispatcher start failed ", e); + } + } } /** @@ -525,7 +774,10 @@ public void start() throws MQClientException { */ @Override public void shutdown() { - this.defaultMQPushConsumerImpl.shutdown(); + this.defaultMQPushConsumerImpl.shutdown(awaitTerminationMillisWhenShutdown); + if (null != traceDispatcher) { + traceDispatcher.shutdown(); + } } @Override @@ -567,7 +819,7 @@ public void registerMessageListener(MessageListenerOrderly messageListener) { */ @Override public void subscribe(String topic, String subExpression) throws MQClientException { - this.defaultMQPushConsumerImpl.subscribe(topic, subExpression); + this.defaultMQPushConsumerImpl.subscribe(withNamespace(topic), subExpression); } /** @@ -579,7 +831,7 @@ public void subscribe(String topic, String subExpression) throws MQClientExcepti */ @Override public void subscribe(String topic, String fullClassName, String filterClassSource) throws MQClientException { - this.defaultMQPushConsumerImpl.subscribe(topic, fullClassName, filterClassSource); + this.defaultMQPushConsumerImpl.subscribe(withNamespace(topic), fullClassName, filterClassSource); } /** @@ -592,7 +844,7 @@ public void subscribe(String topic, String fullClassName, String filterClassSour */ @Override public void subscribe(final String topic, final MessageSelector messageSelector) throws MQClientException { - this.defaultMQPushConsumerImpl.subscribe(topic, messageSelector); + this.defaultMQPushConsumerImpl.subscribe(withNamespace(topic), messageSelector); } /** @@ -631,10 +883,30 @@ public void resume() { this.defaultMQPushConsumerImpl.resume(); } + public boolean isPause() { + return this.defaultMQPushConsumerImpl.isPause(); + } + + public boolean isConsumeOrderly() { + return this.defaultMQPushConsumerImpl.isConsumeOrderly(); + } + + public void registerConsumeMessageHook(final ConsumeMessageHook hook) { + this.defaultMQPushConsumerImpl.registerConsumeMessageHook(hook); + } + + /** + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. + */ + @Deprecated public OffsetStore getOffsetStore() { return offsetStore; } + /** + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. + */ + @Deprecated public void setOffsetStore(OffsetStore offsetStore) { this.offsetStore = offsetStore; } @@ -655,10 +927,12 @@ public void setPostSubscriptionWhenPull(boolean postSubscriptionWhenPull) { this.postSubscriptionWhenPull = postSubscriptionWhenPull; } + @Override public boolean isUnitMode() { return unitMode; } + @Override public void setUnitMode(boolean isUnitMode) { this.unitMode = isUnitMode; } @@ -694,4 +968,56 @@ public long getConsumeTimeout() { public void setConsumeTimeout(final long consumeTimeout) { this.consumeTimeout = consumeTimeout; } + + public long getPopInvisibleTime() { + return popInvisibleTime; + } + + public void setPopInvisibleTime(long popInvisibleTime) { + this.popInvisibleTime = popInvisibleTime; + } + + public long getAwaitTerminationMillisWhenShutdown() { + return awaitTerminationMillisWhenShutdown; + } + + public void setAwaitTerminationMillisWhenShutdown(long awaitTerminationMillisWhenShutdown) { + this.awaitTerminationMillisWhenShutdown = awaitTerminationMillisWhenShutdown; + } + + public int getPullBatchSizeInBytes() { + return pullBatchSizeInBytes; + } + + public void setPullBatchSizeInBytes(int pullBatchSizeInBytes) { + this.pullBatchSizeInBytes = pullBatchSizeInBytes; + } + + public TraceDispatcher getTraceDispatcher() { + return traceDispatcher; + } + + public int getPopBatchNums() { + return popBatchNums; + } + + public void setPopBatchNums(int popBatchNums) { + this.popBatchNums = popBatchNums; + } + + public boolean isClientRebalance() { + return clientRebalance; + } + + public void setClientRebalance(boolean clientRebalance) { + this.clientRebalance = clientRebalance; + } + + public MessageQueueListener getMessageQueueListener() { + return messageQueueListener; + } + + public void setMessageQueueListener(MessageQueueListener messageQueueListener) { + this.messageQueueListener = messageQueueListener; + } } diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/LitePullConsumer.java b/client/src/main/java/org/apache/rocketmq/client/consumer/LitePullConsumer.java new file mode 100644 index 00000000000..6c6a5970a60 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/LitePullConsumer.java @@ -0,0 +1,272 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.consumer; + +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public interface LitePullConsumer { + + /** + * Start the consumer + */ + void start() throws MQClientException; + + /** + * Shutdown the consumer + */ + void shutdown(); + + /** + * This consumer is still running + * + * @return true if consumer is still running + */ + boolean isRunning(); + + /** + * Subscribe some topic with all tags + * @throws MQClientException if there is any client error. + */ + void subscribe(final String topic) throws MQClientException; + + /** + * Subscribe some topic with subExpression + * + * @param subExpression subscription expression.it only support or operation such as "tag1 || tag2 || tag3"
if + * null or * expression,meaning subscribe all + * @throws MQClientException if there is any client error. + */ + void subscribe(final String topic, final String subExpression) throws MQClientException; + + /** + * Subscribe some topic with subExpression and messageQueueListener + * @param topic + * @param subExpression + * @param messageQueueListener + */ + void subscribe(final String topic, final String subExpression, final MessageQueueListener messageQueueListener) throws MQClientException; + + /** + * Subscribe some topic with selector. + * + * @param selector message selector({@link MessageSelector}), can be null. + * @throws MQClientException if there is any client error. + */ + void subscribe(final String topic, final MessageSelector selector) throws MQClientException; + + /** + * Unsubscribe consumption some topic + * + * @param topic Message topic that needs to be unsubscribe. + */ + void unsubscribe(final String topic); + + + /** + * subscribe mode, get assigned MessageQueue + * @return + * @throws MQClientException + */ + Set assignment() throws MQClientException; + + /** + * Manually assign a list of message queues to this consumer. This interface does not allow for incremental + * assignment and will replace the previous assignment (if there is one). + * + * @param messageQueues Message queues that needs to be assigned. + */ + void assign(Collection messageQueues); + + /** + * Set topic subExpression for assign mode. This interface does not allow be call after start(). Default value is * if not set. + * assignment and will replace the previous assignment (if there is one). + * + * @param subExpression subscription expression.it only support or operation such as "tag1 || tag2 || tag3"
if + * * null or * expression,meaning subscribe all + */ + void setSubExpressionForAssign(final String topic, final String subExpression); + + /** + * Fetch data for the topics or partitions specified using assign API + * + * @return list of message, can be null. + */ + List poll(); + + /** + * Fetch data for the topics or partitions specified using assign API + * + * @param timeout The amount time, in milliseconds, spent waiting in poll if data is not available. Must not be + * negative + * @return list of message, can be null. + */ + List poll(long timeout); + + /** + * Overrides the fetch offsets that the consumer will use on the next poll. If this API is invoked for the same + * message queue more than once, the latest offset will be used on the next poll(). Note that you may lose data if + * this API is arbitrarily used in the middle of consumption. + * + * @param messageQueue + * @param offset + */ + void seek(MessageQueue messageQueue, long offset) throws MQClientException; + + /** + * Suspend pulling from the requested message queues. + * + * Because of the implementation of pre-pull, fetch data in {@link #poll()} will not stop immediately until the + * messages of the requested message queues drain. + * + * Note that this method does not affect message queue subscription. In particular, it does not cause a group + * rebalance. + * + * @param messageQueues Message queues that needs to be paused. + */ + void pause(Collection messageQueues); + + /** + * Resume specified message queues which have been paused with {@link #pause(Collection)}. + * + * @param messageQueues Message queues that needs to be resumed. + */ + void resume(Collection messageQueues); + + /** + * Whether to enable auto-commit consume offset. + * + * @return true if enable auto-commit, false if disable auto-commit. + */ + boolean isAutoCommit(); + + /** + * Set whether to enable auto-commit consume offset. + * + * @param autoCommit Whether to enable auto-commit. + */ + void setAutoCommit(boolean autoCommit); + + /** + * Get metadata about the message queues for a given topic. + * + * @param topic The topic that need to get metadata. + * @return collection of message queues + * @throws MQClientException if there is any client error. + */ + Collection fetchMessageQueues(String topic) throws MQClientException; + + /** + * Look up the offsets for the given message queue by timestamp. The returned offset for each message queue is the + * earliest offset whose timestamp is greater than or equal to the given timestamp in the corresponding message + * queue. + * + * @param messageQueue Message queues that needs to get offset by timestamp. + * @param timestamp + * @return offset + * @throws MQClientException if there is any client error. + */ + Long offsetForTimestamp(MessageQueue messageQueue, Long timestamp) throws MQClientException; + + @Deprecated + /** + * The method is deprecated because its name is ambiguous, this method relies on the background thread commit consumerOffset rather than the synchronous commit offset. + * The method is expected to be removed after version 5.1.0. It is recommended to use the {@link #commit()} method. + * + * Manually commit consume offset saved by the system. + */ + void commitSync(); + + @Deprecated + /** + * The method is deprecated because its name is ambiguous, this method relies on the background thread commit consumerOffset rather than the synchronous commit offset. + * The method is expected to be removed after version 5.1.0. It is recommended to use the {@link #commit(java.util.Map, boolean)} method. + * + * @param offsetMap Offset specified by batch commit + */ + void commitSync(Map offsetMap, boolean persist); + + /** + * Manually commit consume offset saved by the system. This is a non-blocking method. + */ + void commit(); + + /** + * Offset specified by batch commit + * + * @param offsetMap Offset specified by batch commit + * @param persist Whether to persist to the broker + */ + void commit(Map offsetMap, boolean persist); + + /** + * Manually commit consume offset saved by the system. + * + * @param messageQueues Message queues that need to submit consumer offset + * @param persist hether to persist to the broker + */ + void commit(final Set messageQueues, boolean persist); + + /** + * Get the last committed offset for the given message queue. + * + * @param messageQueue + * @return offset, if offset equals -1 means no offset in broker. + * @throws MQClientException if there is any client error. + */ + Long committed(MessageQueue messageQueue) throws MQClientException; + + /** + * Register a callback for sensing topic metadata changes. + * + * @param topic The topic that need to monitor. + * @param topicMessageQueueChangeListener Callback when topic metadata changes, refer {@link + * TopicMessageQueueChangeListener} + * @throws MQClientException if there is any client error. + */ + void registerTopicMessageQueueChangeListener(String topic, + TopicMessageQueueChangeListener topicMessageQueueChangeListener) throws MQClientException; + + /** + * Update name server addresses. + */ + void updateNameServerAddress(String nameServerAddress); + + /** + * Overrides the fetch offsets with the begin offset that the consumer will use on the next poll. If this API is + * invoked for the same message queue more than once, the latest offset will be used on the next poll(). Note that + * you may lose data if this API is arbitrarily used in the middle of consumption. + * + * @param messageQueue + */ + void seekToBegin(MessageQueue messageQueue)throws MQClientException; + + /** + * Overrides the fetch offsets with the end offset that the consumer will use on the next poll. If this API is + * invoked for the same message queue more than once, the latest offset will be used on the next poll(). Note that + * you may lose data if this API is arbitrarily used in the middle of consumption. + * + * @param messageQueue + */ + void seekToEnd(MessageQueue messageQueue)throws MQClientException; +} diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/MQConsumer.java b/client/src/main/java/org/apache/rocketmq/client/consumer/MQConsumer.java index f4a8eda23a4..81e06ee4176 100644 --- a/client/src/main/java/org/apache/rocketmq/client/consumer/MQConsumer.java +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/MQConsumer.java @@ -29,20 +29,22 @@ */ public interface MQConsumer extends MQAdmin { /** - * If consuming failure,message will be send back to the brokers,and delay consuming some time + * If consuming of messages failed, they will be sent back to the brokers for another delivery attempt after + * interval specified in delay level. */ @Deprecated void sendMessageBack(final MessageExt msg, final int delayLevel) throws RemotingException, MQBrokerException, InterruptedException, MQClientException; /** - * If consuming failure,message will be send back to the broker,and delay consuming some time + * If consuming of messages failed, they will be sent back to the brokers for another delivery attempt after + * interval specified in delay level. */ void sendMessageBack(final MessageExt msg, final int delayLevel, final String brokerName) throws RemotingException, MQBrokerException, InterruptedException, MQClientException; /** - * Fetch message queues from consumer cache according to the topic + * Fetch message queues from consumer cache pertaining to the given topic. * * @param topic message topic * @return queue set diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/MQPullConsumer.java b/client/src/main/java/org/apache/rocketmq/client/consumer/MQPullConsumer.java index 33002c98371..868ee93ff8a 100644 --- a/client/src/main/java/org/apache/rocketmq/client/consumer/MQPullConsumer.java +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/MQPullConsumer.java @@ -66,6 +66,39 @@ PullResult pull(final MessageQueue mq, final String subExpression, final long of final int maxNums, final long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException; + /** + * Pulling the messages, not blocking + *

+ * support other message selection, such as {@link org.apache.rocketmq.common.filter.ExpressionType#SQL92} + *

+ * + * @param mq from which message queue + * @param selector message selector({@link MessageSelector}), can be null. + * @param offset from where to pull + * @param maxNums max pulling numbers + * @return The resulting {@code PullRequest} + */ + PullResult pull(final MessageQueue mq, final MessageSelector selector, final long offset, + final int maxNums) throws MQClientException, RemotingException, MQBrokerException, + InterruptedException; + + /** + * Pulling the messages in the specified timeout + *

+ * support other message selection, such as {@link org.apache.rocketmq.common.filter.ExpressionType#SQL92} + *

+ * + * @param mq from which message queue + * @param selector message selector({@link MessageSelector}), can be null. + * @param offset from where to pull + * @param maxNums max pulling numbers + * @param timeout Pulling the messages in the specified timeout + * @return The resulting {@code PullRequest} + */ + PullResult pull(final MessageQueue mq, final MessageSelector selector, final long offset, + final int maxNums, final long timeout) throws MQClientException, RemotingException, MQBrokerException, + InterruptedException; + /** * Pulling the messages in a async. way */ @@ -80,6 +113,27 @@ void pull(final MessageQueue mq, final String subExpression, final long offset, final PullCallback pullCallback, long timeout) throws MQClientException, RemotingException, InterruptedException; + /** + * Pulling the messages in a async. way + */ + void pull(final MessageQueue mq, final String subExpression, final long offset, final int maxNums, final int maxSize, + final PullCallback pullCallback, long timeout) throws MQClientException, RemotingException, + InterruptedException; + + /** + * Pulling the messages in a async. way. Support message selection + */ + void pull(final MessageQueue mq, final MessageSelector selector, final long offset, final int maxNums, + final PullCallback pullCallback) throws MQClientException, RemotingException, + InterruptedException; + + /** + * Pulling the messages in a async. way. Support message selection + */ + void pull(final MessageQueue mq, final MessageSelector selector, final long offset, final int maxNums, + final PullCallback pullCallback, long timeout) throws MQClientException, RemotingException, + InterruptedException; + /** * Pulling the messages,if no message arrival,blocking some time * @@ -122,4 +176,5 @@ void pullBlockIfNotFound(final MessageQueue mq, final String subExpression, fina */ void sendMessageBack(MessageExt msg, int delayLevel, String brokerName, String consumerGroup) throws RemotingException, MQBrokerException, InterruptedException, MQClientException; + } diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/MQPullConsumerScheduleService.java b/client/src/main/java/org/apache/rocketmq/client/consumer/MQPullConsumerScheduleService.java index e0b546d247e..798162cc581 100644 --- a/client/src/main/java/org/apache/rocketmq/client/consumer/MQPullConsumerScheduleService.java +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/MQPullConsumerScheduleService.java @@ -24,24 +24,28 @@ import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.apache.rocketmq.client.exception.MQClientException; -import org.apache.rocketmq.client.log.ClientLogger; import org.apache.rocketmq.common.ThreadFactoryImpl; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; -import org.slf4j.Logger; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; /** - * Schedule service for pull consumer + * Schedule service for pull consumer. + * This Consumer will be removed in 2022, and a better implementation {@link + * DefaultLitePullConsumer} is recommend to use in the scenario of actively pulling messages. */ public class MQPullConsumerScheduleService { - private final Logger log = ClientLogger.getLog(); + private final Logger log = LoggerFactory.getLogger(MQPullConsumerScheduleService.class); private final MessageQueueListener messageQueueListener = new MessageQueueListenerImpl(); private final ConcurrentMap taskTable = - new ConcurrentHashMap(); + new ConcurrentHashMap<>(); private DefaultMQPullConsumer defaultMQPullConsumer; private int pullThreadNums = 20; private ConcurrentMap callbackTable = - new ConcurrentHashMap(); + new ConcurrentHashMap<>(); private ScheduledThreadPoolExecutor scheduledThreadPoolExecutor; public MQPullConsumerScheduleService(final String consumerGroup) { @@ -49,6 +53,11 @@ public MQPullConsumerScheduleService(final String consumerGroup) { this.defaultMQPullConsumer.setMessageModel(MessageModel.CLUSTERING); } + public MQPullConsumerScheduleService(final String consumerGroup, final RPCHook rpcHook) { + this.defaultMQPullConsumer = new DefaultMQPullConsumer(consumerGroup, rpcHook); + this.defaultMQPullConsumer.setMessageModel(MessageModel.CLUSTERING); + } + public void putTask(String topic, Set mqNewSet) { Iterator> it = this.taskTable.entrySet().iterator(); while (it.hasNext()) { @@ -87,7 +96,7 @@ public void start() throws MQClientException { } public void registerPullTaskCallback(final String topic, final PullTaskCallback callback) { - this.callbackTable.put(topic, callback); + this.callbackTable.put(NamespaceUtil.wrapNamespace(this.defaultMQPullConsumer.getNamespace(), topic), callback); this.defaultMQPullConsumer.registerMessageQueueListener(topic, null); } @@ -151,7 +160,7 @@ public void messageQueueChanged(String topic, Set mqAll, Setsubscribe(final String topic, final MessageSelector messageSelector) + * is recommended. + * * Subscribe some topic * * @param fullClassName full class name,must extend org.apache.rocketmq.common.filter. MessageFilter * @param filterClassSource class source code,used UTF-8 file encoding,must be responsible for your code safety */ + @Deprecated void subscribe(final String topic, final String fullClassName, final String filterClassSource) throws MQClientException; diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/MessageQueueListener.java b/client/src/main/java/org/apache/rocketmq/client/consumer/MessageQueueListener.java index 63795a6eeb0..74510f4c3ea 100644 --- a/client/src/main/java/org/apache/rocketmq/client/consumer/MessageQueueListener.java +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/MessageQueueListener.java @@ -26,8 +26,7 @@ public interface MessageQueueListener { /** * @param topic message topic * @param mqAll all queues in this message topic - * @param mqDivided collection of queues,assigned to the current consumer + * @param mqAssigned collection of queues, assigned to the current consumer */ - void messageQueueChanged(final String topic, final Set mqAll, - final Set mqDivided); + void messageQueueChanged(final String topic, final Set mqAll, final Set mqAssigned); } diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/MessageSelector.java b/client/src/main/java/org/apache/rocketmq/client/consumer/MessageSelector.java index 03983413aa5..236968e26b6 100644 --- a/client/src/main/java/org/apache/rocketmq/client/consumer/MessageSelector.java +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/MessageSelector.java @@ -47,7 +47,7 @@ private MessageSelector(String type, String expression) { } /** - * Use SLQ92 to select message. + * Use SQL92 to select message. * * @param sql if null or empty, will be treated as select all message. */ diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/PopCallback.java b/client/src/main/java/org/apache/rocketmq/client/consumer/PopCallback.java new file mode 100644 index 00000000000..4932e7485ba --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/PopCallback.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.consumer; + +/** + * Async message pop interface + */ +public interface PopCallback { + void onSuccess(final PopResult popResult); + + void onException(final Throwable e); +} diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/PopResult.java b/client/src/main/java/org/apache/rocketmq/client/consumer/PopResult.java new file mode 100644 index 00000000000..6423e90e491 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/PopResult.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.consumer; + +import java.util.List; +import org.apache.rocketmq.common.message.MessageExt; + +public class PopResult { + private List msgFoundList; + private PopStatus popStatus; + private long popTime; + private long invisibleTime; + private long restNum; + + public PopResult(PopStatus popStatus, List msgFoundList) { + this.popStatus = popStatus; + this.msgFoundList = msgFoundList; + } + + public long getPopTime() { + return popTime; + } + + + public void setPopTime(long popTime) { + this.popTime = popTime; + } + + public long getRestNum() { + return restNum; + } + + public void setRestNum(long restNum) { + this.restNum = restNum; + } + + public long getInvisibleTime() { + return invisibleTime; + } + + + public void setInvisibleTime(long invisibleTime) { + this.invisibleTime = invisibleTime; + } + + + public void setPopStatus(PopStatus popStatus) { + this.popStatus = popStatus; + } + + public PopStatus getPopStatus() { + return popStatus; + } + + public List getMsgFoundList() { + return msgFoundList; + } + + public void setMsgFoundList(List msgFoundList) { + this.msgFoundList = msgFoundList; + } + + @Override + public String toString() { + return "PopResult [popStatus=" + popStatus + ",msgFoundList=" + + (msgFoundList == null ? 0 : msgFoundList.size()) + ",restNum=" + restNum + "]"; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/PopStatus.java b/client/src/main/java/org/apache/rocketmq/client/consumer/PopStatus.java new file mode 100644 index 00000000000..17dda9a2001 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/PopStatus.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.consumer; + +public enum PopStatus { + /** + * Founded + */ + FOUND, + /** + * No new message can be pull after polling time out + * delete after next realease + */ + NO_NEW_MSG, + /** + * polling pool is full, do not try again immediately. + */ + POLLING_FULL, + /** + * polling time out but no message find + */ + POLLING_NOT_FOUND +} diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/PullResult.java b/client/src/main/java/org/apache/rocketmq/client/consumer/PullResult.java index 30d995270c9..d8875421b1d 100644 --- a/client/src/main/java/org/apache/rocketmq/client/consumer/PullResult.java +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/PullResult.java @@ -26,6 +26,7 @@ public class PullResult { private final long maxOffset; private List msgFoundList; + public PullResult(PullStatus pullStatus, long nextBeginOffset, long minOffset, long maxOffset, List msgFoundList) { super(); diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/TopicMessageQueueChangeListener.java b/client/src/main/java/org/apache/rocketmq/client/consumer/TopicMessageQueueChangeListener.java new file mode 100644 index 00000000000..fa6fd134e23 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/TopicMessageQueueChangeListener.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.consumer; + +import java.util.Set; +import org.apache.rocketmq.common.message.MessageQueue; + +public interface TopicMessageQueueChangeListener { + /** + * This method will be invoked in the condition of queue numbers changed, These scenarios occur when the topic is + * expanded or shrunk. + * + * @param messageQueues + */ + void onChanged(String topic, Set messageQueues); +} diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/listener/MessageListenerOrderly.java b/client/src/main/java/org/apache/rocketmq/client/consumer/listener/MessageListenerOrderly.java index d148df5c674..419169d76ac 100644 --- a/client/src/main/java/org/apache/rocketmq/client/consumer/listener/MessageListenerOrderly.java +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/listener/MessageListenerOrderly.java @@ -20,8 +20,7 @@ import org.apache.rocketmq.common.message.MessageExt; /** - * A MessageListenerConcurrently object is used to receive asynchronously delivered messages orderly.one queue,one - * thread + * A MessageListenerOrderly object is used to receive messages orderly. One queue by one thread */ public interface MessageListenerOrderly extends MessageListener { /** diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AbstractAllocateMessageQueueStrategy.java b/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AbstractAllocateMessageQueueStrategy.java new file mode 100644 index 00000000000..50d3cbe4661 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AbstractAllocateMessageQueueStrategy.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client.consumer.rebalance; + +import java.util.List; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.consumer.AllocateMessageQueueStrategy; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public abstract class AbstractAllocateMessageQueueStrategy implements AllocateMessageQueueStrategy { + + private static final Logger log = LoggerFactory.getLogger(AbstractAllocateMessageQueueStrategy.class); + + public boolean check(String consumerGroup, String currentCID, List mqAll, + List cidAll) { + if (StringUtils.isEmpty(currentCID)) { + throw new IllegalArgumentException("currentCID is empty"); + } + if (CollectionUtils.isEmpty(mqAll)) { + throw new IllegalArgumentException("mqAll is null or mqAll empty"); + } + if (CollectionUtils.isEmpty(cidAll)) { + throw new IllegalArgumentException("cidAll is null or cidAll empty"); + } + + if (!cidAll.contains(currentCID)) { + log.info("[BUG] ConsumerGroup: {} The consumerId: {} not in cidAll: {}", + consumerGroup, + currentCID, + cidAll); + return false; + } + + return true; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMachineRoomNearby.java b/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMachineRoomNearby.java new file mode 100644 index 00000000000..08e95dc5880 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMachineRoomNearby.java @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.consumer.rebalance; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.consumer.AllocateMessageQueueStrategy; +import org.apache.rocketmq.common.message.MessageQueue; + +/** + * An allocate strategy proxy for based on machine room nearside priority. An actual allocate strategy can be + * specified. + * + * If any consumer is alive in a machine room, the message queue of the broker which is deployed in the same machine + * should only be allocated to those. Otherwise, those message queues can be shared along all consumers since there are + * no alive consumer to monopolize them. + */ +public class AllocateMachineRoomNearby extends AbstractAllocateMessageQueueStrategy { + + private final AllocateMessageQueueStrategy allocateMessageQueueStrategy;//actual allocate strategy + private final MachineRoomResolver machineRoomResolver; + + public AllocateMachineRoomNearby(AllocateMessageQueueStrategy allocateMessageQueueStrategy, + MachineRoomResolver machineRoomResolver) throws NullPointerException { + if (allocateMessageQueueStrategy == null) { + throw new NullPointerException("allocateMessageQueueStrategy is null"); + } + + if (machineRoomResolver == null) { + throw new NullPointerException("machineRoomResolver is null"); + } + + this.allocateMessageQueueStrategy = allocateMessageQueueStrategy; + this.machineRoomResolver = machineRoomResolver; + } + + @Override + public List allocate(String consumerGroup, String currentCID, List mqAll, + List cidAll) { + + List result = new ArrayList<>(); + if (!check(consumerGroup, currentCID, mqAll, cidAll)) { + return result; + } + + //group mq by machine room + Map> mr2Mq = new TreeMap<>(); + for (MessageQueue mq : mqAll) { + String brokerMachineRoom = machineRoomResolver.brokerDeployIn(mq); + if (StringUtils.isNoneEmpty(brokerMachineRoom)) { + if (mr2Mq.get(brokerMachineRoom) == null) { + mr2Mq.put(brokerMachineRoom, new ArrayList<>()); + } + mr2Mq.get(brokerMachineRoom).add(mq); + } else { + throw new IllegalArgumentException("Machine room is null for mq " + mq); + } + } + + //group consumer by machine room + Map> mr2c = new TreeMap<>(); + for (String cid : cidAll) { + String consumerMachineRoom = machineRoomResolver.consumerDeployIn(cid); + if (StringUtils.isNoneEmpty(consumerMachineRoom)) { + if (mr2c.get(consumerMachineRoom) == null) { + mr2c.put(consumerMachineRoom, new ArrayList<>()); + } + mr2c.get(consumerMachineRoom).add(cid); + } else { + throw new IllegalArgumentException("Machine room is null for consumer id " + cid); + } + } + + List allocateResults = new ArrayList<>(); + + //1.allocate the mq that deploy in the same machine room with the current consumer + String currentMachineRoom = machineRoomResolver.consumerDeployIn(currentCID); + List mqInThisMachineRoom = mr2Mq.remove(currentMachineRoom); + List consumerInThisMachineRoom = mr2c.get(currentMachineRoom); + if (mqInThisMachineRoom != null && !mqInThisMachineRoom.isEmpty()) { + allocateResults.addAll(allocateMessageQueueStrategy.allocate(consumerGroup, currentCID, mqInThisMachineRoom, consumerInThisMachineRoom)); + } + + //2.allocate the rest mq to each machine room if there are no consumer alive in that machine room + for (Entry> machineRoomEntry : mr2Mq.entrySet()) { + if (!mr2c.containsKey(machineRoomEntry.getKey())) { // no alive consumer in the corresponding machine room, so all consumers share these queues + allocateResults.addAll(allocateMessageQueueStrategy.allocate(consumerGroup, currentCID, machineRoomEntry.getValue(), cidAll)); + } + } + + return allocateResults; + } + + @Override + public String getName() { + return "MACHINE_ROOM_NEARBY" + "-" + allocateMessageQueueStrategy.getName(); + } + + /** + * A resolver object to determine which machine room do the message queues or clients are deployed in. + * + * AllocateMachineRoomNearby will use the results to group the message queues and clients by machine room. + * + * The result returned from the implemented method CANNOT be null. + */ + public interface MachineRoomResolver { + String brokerDeployIn(MessageQueue messageQueue); + + String consumerDeployIn(String clientID); + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueAveragely.java b/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueAveragely.java index 35edbe0400f..75e5d1c218b 100644 --- a/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueAveragely.java +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueAveragely.java @@ -18,36 +18,19 @@ import java.util.ArrayList; import java.util.List; -import org.apache.rocketmq.client.consumer.AllocateMessageQueueStrategy; -import org.apache.rocketmq.client.log.ClientLogger; import org.apache.rocketmq.common.message.MessageQueue; -import org.slf4j.Logger; /** * Average Hashing queue algorithm */ -public class AllocateMessageQueueAveragely implements AllocateMessageQueueStrategy { - private final Logger log = ClientLogger.getLog(); +public class AllocateMessageQueueAveragely extends AbstractAllocateMessageQueueStrategy { @Override public List allocate(String consumerGroup, String currentCID, List mqAll, List cidAll) { - if (currentCID == null || currentCID.length() < 1) { - throw new IllegalArgumentException("currentCID is empty"); - } - if (mqAll == null || mqAll.isEmpty()) { - throw new IllegalArgumentException("mqAll is null or mqAll empty"); - } - if (cidAll == null || cidAll.isEmpty()) { - throw new IllegalArgumentException("cidAll is null or cidAll empty"); - } - List result = new ArrayList(); - if (!cidAll.contains(currentCID)) { - log.info("[BUG] ConsumerGroup: {} The consumerId: {} not in cidAll: {}", - consumerGroup, - currentCID, - cidAll); + List result = new ArrayList<>(); + if (!check(consumerGroup, currentCID, mqAll, cidAll)) { return result; } diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueAveragelyByCircle.java b/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueAveragelyByCircle.java index d715ea1f9d0..cc618a81acf 100644 --- a/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueAveragelyByCircle.java +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueAveragelyByCircle.java @@ -18,36 +18,19 @@ import java.util.ArrayList; import java.util.List; -import org.apache.rocketmq.client.consumer.AllocateMessageQueueStrategy; -import org.apache.rocketmq.client.log.ClientLogger; import org.apache.rocketmq.common.message.MessageQueue; -import org.slf4j.Logger; /** * Cycle average Hashing queue algorithm */ -public class AllocateMessageQueueAveragelyByCircle implements AllocateMessageQueueStrategy { - private final Logger log = ClientLogger.getLog(); +public class AllocateMessageQueueAveragelyByCircle extends AbstractAllocateMessageQueueStrategy { @Override public List allocate(String consumerGroup, String currentCID, List mqAll, List cidAll) { - if (currentCID == null || currentCID.length() < 1) { - throw new IllegalArgumentException("currentCID is empty"); - } - if (mqAll == null || mqAll.isEmpty()) { - throw new IllegalArgumentException("mqAll is null or mqAll empty"); - } - if (cidAll == null || cidAll.isEmpty()) { - throw new IllegalArgumentException("cidAll is null or cidAll empty"); - } - List result = new ArrayList(); - if (!cidAll.contains(currentCID)) { - log.info("[BUG] ConsumerGroup: {} The consumerId: {} not in cidAll: {}", - consumerGroup, - currentCID, - cidAll); + List result = new ArrayList<>(); + if (!check(consumerGroup, currentCID, mqAll, cidAll)) { return result; } diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueByConfig.java b/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueByConfig.java index e548803d0d0..5866e95dd40 100644 --- a/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueByConfig.java +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueByConfig.java @@ -17,10 +17,9 @@ package org.apache.rocketmq.client.consumer.rebalance; import java.util.List; -import org.apache.rocketmq.client.consumer.AllocateMessageQueueStrategy; import org.apache.rocketmq.common.message.MessageQueue; -public class AllocateMessageQueueByConfig implements AllocateMessageQueueStrategy { +public class AllocateMessageQueueByConfig extends AbstractAllocateMessageQueueStrategy { private List messageQueueList; @Override diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueByMachineRoom.java b/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueByMachineRoom.java index 37568317cb0..4a2ba888a92 100644 --- a/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueByMachineRoom.java +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueByMachineRoom.java @@ -19,24 +19,27 @@ import java.util.ArrayList; import java.util.List; import java.util.Set; -import org.apache.rocketmq.client.consumer.AllocateMessageQueueStrategy; import org.apache.rocketmq.common.message.MessageQueue; /** * Computer room Hashing queue algorithm, such as Alipay logic room */ -public class AllocateMessageQueueByMachineRoom implements AllocateMessageQueueStrategy { +public class AllocateMessageQueueByMachineRoom extends AbstractAllocateMessageQueueStrategy { private Set consumeridcs; @Override public List allocate(String consumerGroup, String currentCID, List mqAll, List cidAll) { - List result = new ArrayList(); + + List result = new ArrayList<>(); + if (!check(consumerGroup, currentCID, mqAll, cidAll)) { + return result; + } int currentIndex = cidAll.indexOf(currentCID); if (currentIndex < 0) { return result; } - List premqAll = new ArrayList(); + List premqAll = new ArrayList<>(); for (MessageQueue mq : mqAll) { String[] temp = mq.getBrokerName().split("@"); if (temp.length == 2 && consumeridcs.contains(temp[0])) { @@ -49,7 +52,7 @@ public List allocate(String consumerGroup, String currentCID, List int startIndex = mod * currentIndex; int endIndex = startIndex + mod; for (int i = startIndex; i < endIndex; i++) { - result.add(mqAll.get(i)); + result.add(premqAll.get(i)); } if (rem > currentIndex) { result.add(premqAll.get(currentIndex + mod * cidAll.size())); diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueConsistentHash.java b/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueConsistentHash.java index b00326e6fef..eea19ed7fe6 100644 --- a/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueConsistentHash.java +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueConsistentHash.java @@ -19,19 +19,15 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; -import org.apache.rocketmq.client.consumer.AllocateMessageQueueStrategy; -import org.apache.rocketmq.client.log.ClientLogger; import org.apache.rocketmq.common.consistenthash.ConsistentHashRouter; import org.apache.rocketmq.common.consistenthash.HashFunction; import org.apache.rocketmq.common.consistenthash.Node; import org.apache.rocketmq.common.message.MessageQueue; -import org.slf4j.Logger; /** * Consistent Hashing queue algorithm */ -public class AllocateMessageQueueConsistentHash implements AllocateMessageQueueStrategy { - private final Logger log = ClientLogger.getLog(); +public class AllocateMessageQueueConsistentHash extends AbstractAllocateMessageQueueStrategy { private final int virtualNodeCnt; private final HashFunction customHashFunction; @@ -56,38 +52,24 @@ public AllocateMessageQueueConsistentHash(int virtualNodeCnt, HashFunction custo public List allocate(String consumerGroup, String currentCID, List mqAll, List cidAll) { - if (currentCID == null || currentCID.length() < 1) { - throw new IllegalArgumentException("currentCID is empty"); - } - if (mqAll == null || mqAll.isEmpty()) { - throw new IllegalArgumentException("mqAll is null or mqAll empty"); - } - if (cidAll == null || cidAll.isEmpty()) { - throw new IllegalArgumentException("cidAll is null or cidAll empty"); - } - - List result = new ArrayList(); - if (!cidAll.contains(currentCID)) { - log.info("[BUG] ConsumerGroup: {} The consumerId: {} not in cidAll: {}", - consumerGroup, - currentCID, - cidAll); + List result = new ArrayList<>(); + if (!check(consumerGroup, currentCID, mqAll, cidAll)) { return result; } - Collection cidNodes = new ArrayList(); + Collection cidNodes = new ArrayList<>(); for (String cid : cidAll) { cidNodes.add(new ClientNode(cid)); } final ConsistentHashRouter router; //for building hash ring if (customHashFunction != null) { - router = new ConsistentHashRouter(cidNodes, virtualNodeCnt, customHashFunction); + router = new ConsistentHashRouter<>(cidNodes, virtualNodeCnt, customHashFunction); } else { - router = new ConsistentHashRouter(cidNodes, virtualNodeCnt); + router = new ConsistentHashRouter<>(cidNodes, virtualNodeCnt); } - List results = new ArrayList(); + List results = new ArrayList<>(); for (MessageQueue mq : mqAll) { ClientNode clientNode = router.routeNode(mq.toString()); if (clientNode != null && currentCID.equals(clientNode.getKey())) { diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/store/ControllableOffset.java b/client/src/main/java/org/apache/rocketmq/client/consumer/store/ControllableOffset.java new file mode 100644 index 00000000000..9db4bd2e2af --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/store/ControllableOffset.java @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client.consumer.store; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * The ControllableOffset class encapsulates a thread-safe offset value that can be + * updated atomically. Additionally, this class allows for the offset to be "frozen," + * which prevents further updates after the freeze operation has been performed. + *

+ * Concurrency Scenarios: + * If {@code updateAndFreeze} is called before any {@code update} operations, it sets + * {@code allowToUpdate} to false and updates the offset to the target value specified. + * After this operation, further invocations of {@code update} will not affect the offset, + * as it is considered frozen. + *

+ * If {@code update} is in progress while {@code updateAndFreeze} is invoked concurrently, + * the final outcome depends on the sequence of operations: + * 1. If {@code update}'s atomic update operation completes before {@code updateAndFreeze}, + * the latter will overwrite the offset and set {@code allowToUpdate} to false, + * preventing any further updates. + * 2. If {@code updateAndFreeze} executes before the {@code update} finalizes its operation, + * the ongoing {@code update} will not proceed with its changes. The {@link AtomicLong#getAndUpdate} + * method used in both operations ensures atomicity and respects the final state imposed by + * {@code updateAndFreeze}, even if the {@code update} function has already begun. + *

+ * In essence, once the {@code updateAndFreeze} operation is executed, the offset value remains + * immutable to any subsequent {@code update} calls due to the immediate visibility of the + * {@code allowToUpdate} state change, courtesy of its volatile nature. + *

+ * The combination of an AtomicLong for the offset value and a volatile boolean flag for update + * control provides a reliable mechanism for managing offset values in concurrent environments. + */ +public class ControllableOffset { + // Holds the current offset value in an atomic way. + private final AtomicLong value; + // Controls whether updates to the offset are allowed. + private volatile boolean allowToUpdate; + + public ControllableOffset(long value) { + this.value = new AtomicLong(value); + this.allowToUpdate = true; + } + + /** + * Attempts to update the offset to the target value. If increaseOnly is true, + * the offset will not be decreased. The update operation is atomic and thread-safe. + * The operation will respect the current allowToUpdate state, and if the offset + * has been frozen by a previous call to {@link #updateAndFreeze(long)}, + * this method will not update the offset. + * + * @param target the new target offset value. + * @param increaseOnly if true, the offset will only be updated if the target value + * is greater than the current value. + */ + public void update(long target, boolean increaseOnly) { + if (allowToUpdate) { + value.getAndUpdate(val -> { + if (allowToUpdate) { + if (increaseOnly) { + return Math.max(target, val); + } else { + return target; + } + } else { + return val; + } + }); + } + } + + /** + * Overloaded method for updating the offset value unconditionally. + * + * @param target The new target value for the offset. + */ + public void update(long target) { + update(target, false); + } + + /** + * Freezes the offset at the target value provided. Once frozen, the offset + * cannot be updated by subsequent calls to {@link #update(long, boolean)}. + * This method will set allowToUpdate to false and then update the offset, + * ensuring the new value is the final state of the offset. + * + * @param target the new target offset value to freeze at. + */ + public void updateAndFreeze(long target) { + value.getAndUpdate(val -> { + allowToUpdate = false; + return target; + }); + } + + public long getOffset() { + return value.get(); + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/store/LocalFileOffsetStore.java b/client/src/main/java/org/apache/rocketmq/client/consumer/store/LocalFileOffsetStore.java index 22ec674288c..38b0a5be35b 100644 --- a/client/src/main/java/org/apache/rocketmq/client/consumer/store/LocalFileOffsetStore.java +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/store/LocalFileOffsetStore.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -27,13 +28,13 @@ import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.impl.factory.MQClientInstance; -import org.apache.rocketmq.client.log.ClientLogger; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.UtilAll; import org.apache.rocketmq.common.help.FAQUrl; import org.apache.rocketmq.common.message.MessageQueue; import org.apache.rocketmq.remoting.exception.RemotingException; -import org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; /** * Local storage implementation @@ -42,12 +43,12 @@ public class LocalFileOffsetStore implements OffsetStore { public final static String LOCAL_OFFSET_STORE_DIR = System.getProperty( "rocketmq.client.localOffsetStoreDir", System.getProperty("user.home") + File.separator + ".rocketmq_offsets"); - private final static Logger log = ClientLogger.getLog(); + private final static Logger log = LoggerFactory.getLogger(LocalFileOffsetStore.class); private final MQClientInstance mQClientFactory; private final String groupName; private final String storePath; - private ConcurrentMap offsetTable = - new ConcurrentHashMap(); + private ConcurrentMap offsetTable = + new ConcurrentHashMap<>(); public LocalFileOffsetStore(MQClientInstance mQClientFactory, String groupName) { this.mQClientFactory = mQClientFactory; @@ -62,14 +63,13 @@ public LocalFileOffsetStore(MQClientInstance mQClientFactory, String groupName) public void load() throws MQClientException { OffsetSerializeWrapper offsetSerializeWrapper = this.readLocalOffset(); if (offsetSerializeWrapper != null && offsetSerializeWrapper.getOffsetTable() != null) { - offsetTable.putAll(offsetSerializeWrapper.getOffsetTable()); - - for (MessageQueue mq : offsetSerializeWrapper.getOffsetTable().keySet()) { - AtomicLong offset = offsetSerializeWrapper.getOffsetTable().get(mq); + for (Entry mqEntry : offsetSerializeWrapper.getOffsetTable().entrySet()) { + AtomicLong offset = mqEntry.getValue(); + offsetTable.put(mqEntry.getKey(), new ControllableOffset(offset.get())); log.info("load consumer's offset, {} {} {}", - this.groupName, - mq, - offset.get()); + this.groupName, + mqEntry.getKey(), + offset.get()); } } } @@ -77,30 +77,38 @@ public void load() throws MQClientException { @Override public void updateOffset(MessageQueue mq, long offset, boolean increaseOnly) { if (mq != null) { - AtomicLong offsetOld = this.offsetTable.get(mq); + ControllableOffset offsetOld = this.offsetTable.get(mq); if (null == offsetOld) { - offsetOld = this.offsetTable.putIfAbsent(mq, new AtomicLong(offset)); + offsetOld = this.offsetTable.putIfAbsent(mq, new ControllableOffset(offset)); } if (null != offsetOld) { if (increaseOnly) { - MixAll.compareAndIncreaseOnly(offsetOld, offset); + offsetOld.update(offset, true); } else { - offsetOld.set(offset); + offsetOld.update(offset); } } } } + @Override + public void updateAndFreezeOffset(MessageQueue mq, long offset) { + if (mq != null) { + this.offsetTable.computeIfAbsent(mq, k -> new ControllableOffset(offset)) + .updateAndFreeze(offset); + } + } + @Override public long readOffset(final MessageQueue mq, final ReadOffsetType type) { if (mq != null) { switch (type) { case MEMORY_FIRST_THEN_STORE: case READ_FROM_MEMORY: { - AtomicLong offset = this.offsetTable.get(mq); + ControllableOffset offset = this.offsetTable.get(mq); if (offset != null) { - return offset.get(); + return offset.getOffset(); } else if (ReadOffsetType.READ_FROM_MEMORY == type) { return -1; } @@ -130,13 +138,23 @@ public long readOffset(final MessageQueue mq, final ReadOffsetType type) { @Override public void persistAll(Set mqs) { - if (null == mqs || mqs.isEmpty()) + if (null == mqs || mqs.isEmpty()) { return; + } + OffsetSerializeWrapper offsetSerializeWrapper = null; + try { + offsetSerializeWrapper = readLocalOffset(); + } catch (MQClientException e) { + log.error("readLocalOffset exception", e); + return; + } - OffsetSerializeWrapper offsetSerializeWrapper = new OffsetSerializeWrapper(); - for (Map.Entry entry : this.offsetTable.entrySet()) { + if (offsetSerializeWrapper == null) { + offsetSerializeWrapper = new OffsetSerializeWrapper(); + } + for (Map.Entry entry : this.offsetTable.entrySet()) { if (mqs.contains(entry.getKey())) { - AtomicLong offset = entry.getValue(); + AtomicLong offset = new AtomicLong(entry.getValue().getOffset()); offsetSerializeWrapper.getOffsetTable().put(entry.getKey(), offset); } } @@ -153,11 +171,40 @@ public void persistAll(Set mqs) { @Override public void persist(MessageQueue mq) { + if (mq == null) { + return; + } + ControllableOffset offset = this.offsetTable.get(mq); + if (offset != null) { + OffsetSerializeWrapper offsetSerializeWrapper = null; + try { + offsetSerializeWrapper = readLocalOffset(); + } catch (MQClientException e) { + log.error("readLocalOffset exception", e); + return; + } + if (offsetSerializeWrapper == null) { + offsetSerializeWrapper = new OffsetSerializeWrapper(); + } + offsetSerializeWrapper.getOffsetTable().put(mq, new AtomicLong(offset.getOffset())); + String jsonString = offsetSerializeWrapper.toJson(true); + if (jsonString != null) { + try { + MixAll.string2File(jsonString, this.storePath); + } catch (IOException e) { + log.error("persist consumer offset exception, " + this.storePath, e); + } + } + } } @Override public void removeOffset(MessageQueue mq) { - + if (mq != null) { + this.offsetTable.remove(mq); + log.info("remove unnecessary messageQueue offset. group={}, mq={}, offsetTableSize={}", this.groupName, mq, + offsetTable.size()); + } } @Override @@ -168,13 +215,13 @@ public void updateConsumeOffsetToBroker(final MessageQueue mq, final long offset @Override public Map cloneOffsetTable(String topic) { - Map cloneOffsetTable = new HashMap(); - for (Map.Entry entry : this.offsetTable.entrySet()) { + Map cloneOffsetTable = new HashMap<>(this.offsetTable.size(), 1); + for (Map.Entry entry : this.offsetTable.entrySet()) { MessageQueue mq = entry.getKey(); if (!UtilAll.isBlank(topic) && !topic.equals(mq.getTopic())) { continue; } - cloneOffsetTable.put(mq, entry.getValue().get()); + cloneOffsetTable.put(mq, entry.getValue().getOffset()); } return cloneOffsetTable; diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/store/OffsetSerializeWrapper.java b/client/src/main/java/org/apache/rocketmq/client/consumer/store/OffsetSerializeWrapper.java index 7dfd97af6af..85fe0d43981 100644 --- a/client/src/main/java/org/apache/rocketmq/client/consumer/store/OffsetSerializeWrapper.java +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/store/OffsetSerializeWrapper.java @@ -27,7 +27,7 @@ */ public class OffsetSerializeWrapper extends RemotingSerializable { private ConcurrentMap offsetTable = - new ConcurrentHashMap(); + new ConcurrentHashMap<>(); public ConcurrentMap getOffsetTable() { return offsetTable; diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/store/OffsetStore.java b/client/src/main/java/org/apache/rocketmq/client/consumer/store/OffsetStore.java index 9deed0e3dfe..ecceedee178 100644 --- a/client/src/main/java/org/apache/rocketmq/client/consumer/store/OffsetStore.java +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/store/OffsetStore.java @@ -37,6 +37,14 @@ public interface OffsetStore { */ void updateOffset(final MessageQueue mq, final long offset, final boolean increaseOnly); + /** + * Update and freeze the message queue to prevent concurrent update action + * + * @param mq target message queue + * @param offset expect update offset + */ + void updateAndFreezeOffset(final MessageQueue mq, final long offset); + /** * Get offset from local storage * diff --git a/client/src/main/java/org/apache/rocketmq/client/consumer/store/RemoteBrokerOffsetStore.java b/client/src/main/java/org/apache/rocketmq/client/consumer/store/RemoteBrokerOffsetStore.java index b82e992875e..1a2ffe5470f 100644 --- a/client/src/main/java/org/apache/rocketmq/client/consumer/store/RemoteBrokerOffsetStore.java +++ b/client/src/main/java/org/apache/rocketmq/client/consumer/store/RemoteBrokerOffsetStore.java @@ -22,29 +22,29 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.atomic.AtomicLong; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.exception.OffsetNotFoundException; import org.apache.rocketmq.client.impl.FindBrokerResult; import org.apache.rocketmq.client.impl.factory.MQClientInstance; -import org.apache.rocketmq.client.log.ClientLogger; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.UtilAll; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.header.QueryConsumerOffsetRequestHeader; -import org.apache.rocketmq.common.protocol.header.UpdateConsumerOffsetRequestHeader; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.remoting.exception.RemotingException; -import org.slf4j.Logger; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumerOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateConsumerOffsetRequestHeader; /** * Remote storage implementation */ public class RemoteBrokerOffsetStore implements OffsetStore { - private final static Logger log = ClientLogger.getLog(); + private final static Logger log = LoggerFactory.getLogger(RemoteBrokerOffsetStore.class); private final MQClientInstance mQClientFactory; private final String groupName; - private ConcurrentMap offsetTable = - new ConcurrentHashMap(); + private ConcurrentMap offsetTable = + new ConcurrentHashMap<>(); public RemoteBrokerOffsetStore(MQClientInstance mQClientFactory, String groupName) { this.mQClientFactory = mQClientFactory; @@ -58,30 +58,38 @@ public void load() { @Override public void updateOffset(MessageQueue mq, long offset, boolean increaseOnly) { if (mq != null) { - AtomicLong offsetOld = this.offsetTable.get(mq); + ControllableOffset offsetOld = this.offsetTable.get(mq); if (null == offsetOld) { - offsetOld = this.offsetTable.putIfAbsent(mq, new AtomicLong(offset)); + offsetOld = this.offsetTable.putIfAbsent(mq, new ControllableOffset(offset)); } if (null != offsetOld) { if (increaseOnly) { - MixAll.compareAndIncreaseOnly(offsetOld, offset); + offsetOld.update(offset, true); } else { - offsetOld.set(offset); + offsetOld.update(offset); } } } } + @Override + public void updateAndFreezeOffset(MessageQueue mq, long offset) { + if (mq != null) { + this.offsetTable.computeIfAbsent(mq, k -> new ControllableOffset(offset)) + .updateAndFreeze(offset); + } + } + @Override public long readOffset(final MessageQueue mq, final ReadOffsetType type) { if (mq != null) { switch (type) { case MEMORY_FIRST_THEN_STORE: case READ_FROM_MEMORY: { - AtomicLong offset = this.offsetTable.get(mq); + ControllableOffset offset = this.offsetTable.get(mq); if (offset != null) { - return offset.get(); + return offset.getOffset(); } else if (ReadOffsetType.READ_FROM_MEMORY == type) { return -1; } @@ -89,12 +97,11 @@ public long readOffset(final MessageQueue mq, final ReadOffsetType type) { case READ_FROM_STORE: { try { long brokerOffset = this.fetchConsumeOffsetFromBroker(mq); - AtomicLong offset = new AtomicLong(brokerOffset); - this.updateOffset(mq, offset.get(), false); + this.updateOffset(mq, brokerOffset, false); return brokerOffset; } // No offset in broker - catch (MQBrokerException e) { + catch (OffsetNotFoundException e) { return -1; } //Other exceptions @@ -108,7 +115,7 @@ public long readOffset(final MessageQueue mq, final ReadOffsetType type) { } } - return -1; + return -3; } @Override @@ -116,26 +123,25 @@ public void persistAll(Set mqs) { if (null == mqs || mqs.isEmpty()) return; - final HashSet unusedMQ = new HashSet(); - if (!mqs.isEmpty()) { - for (Map.Entry entry : this.offsetTable.entrySet()) { - MessageQueue mq = entry.getKey(); - AtomicLong offset = entry.getValue(); - if (offset != null) { - if (mqs.contains(mq)) { - try { - this.updateConsumeOffsetToBroker(mq, offset.get()); - log.info("[persistAll] Group: {} ClientId: {} updateConsumeOffsetToBroker {} {}", - this.groupName, - this.mQClientFactory.getClientId(), - mq, - offset.get()); - } catch (Exception e) { - log.error("updateConsumeOffsetToBroker exception, " + mq.toString(), e); - } - } else { - unusedMQ.add(mq); + final HashSet unusedMQ = new HashSet<>(); + + for (Map.Entry entry : this.offsetTable.entrySet()) { + MessageQueue mq = entry.getKey(); + ControllableOffset offset = entry.getValue(); + if (offset != null) { + if (mqs.contains(mq)) { + try { + this.updateConsumeOffsetToBroker(mq, offset.getOffset()); + log.info("[persistAll] Group: {} ClientId: {} updateConsumeOffsetToBroker {} {}", + this.groupName, + this.mQClientFactory.getClientId(), + mq, + offset.getOffset()); + } catch (Exception e) { + log.error("updateConsumeOffsetToBroker exception, " + mq.toString(), e); } + } else { + unusedMQ.add(mq); } } } @@ -150,15 +156,15 @@ public void persistAll(Set mqs) { @Override public void persist(MessageQueue mq) { - AtomicLong offset = this.offsetTable.get(mq); + ControllableOffset offset = this.offsetTable.get(mq); if (offset != null) { try { - this.updateConsumeOffsetToBroker(mq, offset.get()); + this.updateConsumeOffsetToBroker(mq, offset.getOffset()); log.info("[persist] Group: {} ClientId: {} updateConsumeOffsetToBroker {} {}", this.groupName, this.mQClientFactory.getClientId(), mq, - offset.get()); + offset.getOffset()); } catch (Exception e) { log.error("updateConsumeOffsetToBroker exception, " + mq.toString(), e); } @@ -175,20 +181,19 @@ public void removeOffset(MessageQueue mq) { @Override public Map cloneOffsetTable(String topic) { - Map cloneOffsetTable = new HashMap(); - for (Map.Entry entry : this.offsetTable.entrySet()) { + Map cloneOffsetTable = new HashMap<>(this.offsetTable.size(), 1); + for (Map.Entry entry : this.offsetTable.entrySet()) { MessageQueue mq = entry.getKey(); if (!UtilAll.isBlank(topic) && !topic.equals(mq.getTopic())) { continue; } - cloneOffsetTable.put(mq, entry.getValue().get()); + cloneOffsetTable.put(mq, entry.getValue().getOffset()); } return cloneOffsetTable; } /** - * Update the Consumer Offset in one way, once the Master is off, updated to Slave, - * here need to be optimized. + * Update the Consumer Offset in one way, once the Master is off, updated to Slave, here need to be optimized. */ private void updateConsumeOffsetToBroker(MessageQueue mq, long offset) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { @@ -196,17 +201,15 @@ private void updateConsumeOffsetToBroker(MessageQueue mq, long offset) throws Re } /** - * Update the Consumer Offset synchronously, once the Master is off, updated to Slave, - * here need to be optimized. + * Update the Consumer Offset synchronously, once the Master is off, updated to Slave, here need to be optimized. */ @Override public void updateConsumeOffsetToBroker(MessageQueue mq, long offset, boolean isOneway) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { - FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInAdmin(mq.getBrokerName()); + FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(this.mQClientFactory.getBrokerNameFromMessageQueue(mq), MixAll.MASTER_ID, false); if (null == findBrokerResult) { - this.mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic()); - findBrokerResult = this.mQClientFactory.findBrokerAddressInAdmin(mq.getBrokerName()); + findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(this.mQClientFactory.getBrokerNameFromMessageQueue(mq), MixAll.MASTER_ID, false); } if (findBrokerResult != null) { @@ -215,6 +218,7 @@ public void updateConsumeOffsetToBroker(MessageQueue mq, long offset, boolean is requestHeader.setConsumerGroup(this.groupName); requestHeader.setQueueId(mq.getQueueId()); requestHeader.setCommitOffset(offset); + requestHeader.setBrokerName(mq.getBrokerName()); if (isOneway) { this.mQClientFactory.getMQClientAPIImpl().updateConsumerOffsetOneway( @@ -230,11 +234,10 @@ public void updateConsumeOffsetToBroker(MessageQueue mq, long offset, boolean is private long fetchConsumeOffsetFromBroker(MessageQueue mq) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { - FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInAdmin(mq.getBrokerName()); + FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(this.mQClientFactory.getBrokerNameFromMessageQueue(mq), MixAll.MASTER_ID, true); if (null == findBrokerResult) { - this.mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic()); - findBrokerResult = this.mQClientFactory.findBrokerAddressInAdmin(mq.getBrokerName()); + findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(this.mQClientFactory.getBrokerNameFromMessageQueue(mq), MixAll.MASTER_ID, false); } if (findBrokerResult != null) { @@ -242,6 +245,7 @@ private long fetchConsumeOffsetFromBroker(MessageQueue mq) throws RemotingExcept requestHeader.setTopic(mq.getTopic()); requestHeader.setConsumerGroup(this.groupName); requestHeader.setQueueId(mq.getQueueId()); + requestHeader.setBrokerName(mq.getBrokerName()); return this.mQClientFactory.getMQClientAPIImpl().queryConsumerOffset( findBrokerResult.getBrokerAddr(), requestHeader, 1000 * 5); diff --git a/client/src/main/java/org/apache/rocketmq/client/exception/MQBrokerException.java b/client/src/main/java/org/apache/rocketmq/client/exception/MQBrokerException.java index e4f2c8df850..7870ff1931b 100644 --- a/client/src/main/java/org/apache/rocketmq/client/exception/MQBrokerException.java +++ b/client/src/main/java/org/apache/rocketmq/client/exception/MQBrokerException.java @@ -23,12 +23,28 @@ public class MQBrokerException extends Exception { private static final long serialVersionUID = 5975020272601250368L; private final int responseCode; private final String errorMessage; + private final String brokerAddr; + + MQBrokerException() { + this.responseCode = 0; + this.errorMessage = null; + this.brokerAddr = null; + } public MQBrokerException(int responseCode, String errorMessage) { super(FAQUrl.attachDefaultURL("CODE: " + UtilAll.responseCode2String(responseCode) + " DESC: " - + errorMessage)); + + errorMessage)); this.responseCode = responseCode; this.errorMessage = errorMessage; + this.brokerAddr = null; + } + + public MQBrokerException(int responseCode, String errorMessage, String brokerAddr) { + super(FAQUrl.attachDefaultURL("CODE: " + UtilAll.responseCode2String(responseCode) + " DESC: " + + errorMessage + (brokerAddr != null ? " BROKER: " + brokerAddr : ""))); + this.responseCode = responseCode; + this.errorMessage = errorMessage; + this.brokerAddr = brokerAddr; } public int getResponseCode() { @@ -38,4 +54,8 @@ public int getResponseCode() { public String getErrorMessage() { return errorMessage; } + + public String getBrokerAddr() { + return brokerAddr; + } } diff --git a/client/src/main/java/org/apache/rocketmq/client/exception/MQClientException.java b/client/src/main/java/org/apache/rocketmq/client/exception/MQClientException.java index f4534742d53..9bbcce21783 100644 --- a/client/src/main/java/org/apache/rocketmq/client/exception/MQClientException.java +++ b/client/src/main/java/org/apache/rocketmq/client/exception/MQClientException.java @@ -37,6 +37,13 @@ public MQClientException(int responseCode, String errorMessage) { this.errorMessage = errorMessage; } + public MQClientException(int responseCode, String errorMessage, Throwable cause) { + super(FAQUrl.attachDefaultURL("CODE: " + UtilAll.responseCode2String(responseCode) + " DESC: " + + errorMessage), cause); + this.responseCode = responseCode; + this.errorMessage = errorMessage; + } + public int getResponseCode() { return responseCode; } diff --git a/client/src/main/java/org/apache/rocketmq/client/exception/OffsetNotFoundException.java b/client/src/main/java/org/apache/rocketmq/client/exception/OffsetNotFoundException.java new file mode 100644 index 00000000000..e73bbbf1508 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/exception/OffsetNotFoundException.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.exception; + +public class OffsetNotFoundException extends MQBrokerException { + + public OffsetNotFoundException() { + } + + public OffsetNotFoundException(int responseCode, String errorMessage) { + super(responseCode, errorMessage); + } + + public OffsetNotFoundException(int responseCode, String errorMessage, String brokerAddr) { + super(responseCode, errorMessage, brokerAddr); + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/exception/RequestTimeoutException.java b/client/src/main/java/org/apache/rocketmq/client/exception/RequestTimeoutException.java new file mode 100644 index 00000000000..2d756ece617 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/exception/RequestTimeoutException.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client.exception; + +import org.apache.rocketmq.common.UtilAll; + +public class RequestTimeoutException extends Exception { + private static final long serialVersionUID = -5758410930844185841L; + private int responseCode; + private String errorMessage; + + public RequestTimeoutException(String errorMessage, Throwable cause) { + super(errorMessage, cause); + this.responseCode = -1; + this.errorMessage = errorMessage; + } + + public RequestTimeoutException(int responseCode, String errorMessage) { + super("CODE: " + UtilAll.responseCode2String(responseCode) + " DESC: " + + errorMessage); + this.responseCode = responseCode; + this.errorMessage = errorMessage; + } + + public int getResponseCode() { + return responseCode; + } + + public RequestTimeoutException setResponseCode(final int responseCode) { + this.responseCode = responseCode; + return this; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(final String errorMessage) { + this.errorMessage = errorMessage; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/hook/ConsumeMessageContext.java b/client/src/main/java/org/apache/rocketmq/client/hook/ConsumeMessageContext.java index 28381b064e7..94633cea8b1 100644 --- a/client/src/main/java/org/apache/rocketmq/client/hook/ConsumeMessageContext.java +++ b/client/src/main/java/org/apache/rocketmq/client/hook/ConsumeMessageContext.java @@ -18,6 +18,8 @@ import java.util.List; import java.util.Map; + +import org.apache.rocketmq.client.AccessChannel; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageQueue; @@ -29,6 +31,8 @@ public class ConsumeMessageContext { private String status; private Object mqTraceContext; private Map props; + private String namespace; + private AccessChannel accessChannel; public String getConsumerGroup() { return consumerGroup; @@ -85,4 +89,20 @@ public String getStatus() { public void setStatus(String status) { this.status = status; } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public AccessChannel getAccessChannel() { + return accessChannel; + } + + public void setAccessChannel(AccessChannel accessChannel) { + this.accessChannel = accessChannel; + } } diff --git a/client/src/main/java/org/apache/rocketmq/client/hook/EndTransactionContext.java b/client/src/main/java/org/apache/rocketmq/client/hook/EndTransactionContext.java new file mode 100644 index 00000000000..5271ade82bf --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/hook/EndTransactionContext.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.hook; + +import org.apache.rocketmq.client.producer.LocalTransactionState; +import org.apache.rocketmq.common.message.Message; + +public class EndTransactionContext { + private String producerGroup; + private Message message; + private String brokerAddr; + private String msgId; + private String transactionId; + private LocalTransactionState transactionState; + private boolean fromTransactionCheck; + + public String getProducerGroup() { + return producerGroup; + } + + public void setProducerGroup(String producerGroup) { + this.producerGroup = producerGroup; + } + + public Message getMessage() { + return message; + } + + public void setMessage(Message message) { + this.message = message; + } + + public String getBrokerAddr() { + return brokerAddr; + } + + public void setBrokerAddr(String brokerAddr) { + this.brokerAddr = brokerAddr; + } + + public String getMsgId() { + return msgId; + } + + public void setMsgId(String msgId) { + this.msgId = msgId; + } + + public String getTransactionId() { + return transactionId; + } + + public void setTransactionId(String transactionId) { + this.transactionId = transactionId; + } + + public LocalTransactionState getTransactionState() { + return transactionState; + } + + public void setTransactionState(LocalTransactionState transactionState) { + this.transactionState = transactionState; + } + + public boolean isFromTransactionCheck() { + return fromTransactionCheck; + } + + public void setFromTransactionCheck(boolean fromTransactionCheck) { + this.fromTransactionCheck = fromTransactionCheck; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/hook/EndTransactionHook.java b/client/src/main/java/org/apache/rocketmq/client/hook/EndTransactionHook.java new file mode 100644 index 00000000000..834cb273126 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/hook/EndTransactionHook.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.hook; + +public interface EndTransactionHook { + String hookName(); + + void endTransaction(final EndTransactionContext context); +} diff --git a/client/src/main/java/org/apache/rocketmq/client/hook/SendMessageContext.java b/client/src/main/java/org/apache/rocketmq/client/hook/SendMessageContext.java index 00723fdff8b..e970d81fc8e 100644 --- a/client/src/main/java/org/apache/rocketmq/client/hook/SendMessageContext.java +++ b/client/src/main/java/org/apache/rocketmq/client/hook/SendMessageContext.java @@ -37,6 +37,7 @@ public class SendMessageContext { private Map props; private DefaultMQProducerImpl producer; private MessageType msgType = MessageType.Normal_Msg; + private String namespace; public MessageType getMsgType() { return msgType; @@ -133,4 +134,12 @@ public String getBornHost() { public void setBornHost(String bornHost) { this.bornHost = bornHost; } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } } diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/ClientRemotingProcessor.java b/client/src/main/java/org/apache/rocketmq/client/impl/ClientRemotingProcessor.java index 69478cf3287..2f18c610c14 100644 --- a/client/src/main/java/org/apache/rocketmq/client/impl/ClientRemotingProcessor.java +++ b/client/src/main/java/org/apache/rocketmq/client/impl/ClientRemotingProcessor.java @@ -17,37 +17,48 @@ package org.apache.rocketmq.client.impl; import io.netty.channel.ChannelHandlerContext; +import java.io.IOException; import java.nio.ByteBuffer; import java.util.HashMap; import java.util.Map; +import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.client.impl.factory.MQClientInstance; import org.apache.rocketmq.client.impl.producer.MQProducerInner; -import org.apache.rocketmq.client.log.ClientLogger; +import org.apache.rocketmq.client.producer.RequestFutureHolder; +import org.apache.rocketmq.client.producer.RequestResponseFuture; import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.compression.Compressor; +import org.apache.rocketmq.common.compression.CompressorFactory; +import org.apache.rocketmq.common.message.MessageAccessor; import org.apache.rocketmq.common.message.MessageConst; import org.apache.rocketmq.common.message.MessageDecoder; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.RequestCode; -import org.apache.rocketmq.common.protocol.ResponseCode; -import org.apache.rocketmq.common.protocol.body.ConsumeMessageDirectlyResult; -import org.apache.rocketmq.common.protocol.body.ConsumerRunningInfo; -import org.apache.rocketmq.common.protocol.body.GetConsumerStatusBody; -import org.apache.rocketmq.common.protocol.body.ResetOffsetBody; -import org.apache.rocketmq.common.protocol.header.CheckTransactionStateRequestHeader; -import org.apache.rocketmq.common.protocol.header.ConsumeMessageDirectlyResultRequestHeader; -import org.apache.rocketmq.common.protocol.header.GetConsumerRunningInfoRequestHeader; -import org.apache.rocketmq.common.protocol.header.GetConsumerStatusRequestHeader; -import org.apache.rocketmq.common.protocol.header.NotifyConsumerIdsChangedRequestHeader; -import org.apache.rocketmq.common.protocol.header.ResetOffsetRequestHeader; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.common.utils.NetworkUtil; import org.apache.rocketmq.remoting.common.RemotingHelper; import org.apache.rocketmq.remoting.exception.RemotingCommandException; import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; import org.apache.rocketmq.remoting.protocol.RemotingCommand; -import org.slf4j.Logger; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.body.GetConsumerStatusBody; +import org.apache.rocketmq.remoting.protocol.body.ResetOffsetBody; +import org.apache.rocketmq.remoting.protocol.header.CheckTransactionStateRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ConsumeMessageDirectlyResultRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerRunningInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerStatusRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.NotifyConsumerIdsChangedRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ReplyMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ResetOffsetRequestHeader; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; public class ClientRemotingProcessor implements NettyRequestProcessor { - private final Logger log = ClientLogger.getLog(); + private final Logger logger = LoggerFactory.getLogger(ClientRemotingProcessor.class); private final MQClientInstance mqClientFactory; public ClientRemotingProcessor(final MQClientInstance mqClientFactory) { @@ -72,6 +83,9 @@ public RemotingCommand processRequest(ChannelHandlerContext ctx, case RequestCode.CONSUME_MESSAGE_DIRECTLY: return this.consumeMessageDirectly(ctx, request); + + case RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT: + return this.receiveReplyMessage(ctx, request); default: break; } @@ -90,6 +104,14 @@ public RemotingCommand checkTransactionState(ChannelHandlerContext ctx, final ByteBuffer byteBuffer = ByteBuffer.wrap(request.getBody()); final MessageExt messageExt = MessageDecoder.decode(byteBuffer); if (messageExt != null) { + if (StringUtils.isNotEmpty(this.mqClientFactory.getClientConfig().getNamespace())) { + messageExt.setTopic(NamespaceUtil + .withoutNamespace(messageExt.getTopic(), this.mqClientFactory.getClientConfig().getNamespace())); + } + String transactionId = messageExt.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX); + if (null != transactionId && !"".equals(transactionId)) { + messageExt.setTransactionId(transactionId); + } final String group = messageExt.getProperty(MessageConst.PROPERTY_PRODUCER_GROUP); if (group != null) { MQProducerInner producer = this.mqClientFactory.selectProducer(group); @@ -97,13 +119,13 @@ public RemotingCommand checkTransactionState(ChannelHandlerContext ctx, final String addr = RemotingHelper.parseChannelRemoteAddr(ctx.channel()); producer.checkTransactionState(addr, messageExt, requestHeader); } else { - log.debug("checkTransactionState, pick producer by group[{}] failed", group); + logger.debug("checkTransactionState, pick producer by group[{}] failed", group); } } else { - log.warn("checkTransactionState, pick producer group failed"); + logger.warn("checkTransactionState, pick producer group failed"); } } else { - log.warn("checkTransactionState, decode message failed"); + logger.warn("checkTransactionState, decode message failed"); } return null; @@ -114,12 +136,12 @@ public RemotingCommand notifyConsumerIdsChanged(ChannelHandlerContext ctx, try { final NotifyConsumerIdsChangedRequestHeader requestHeader = (NotifyConsumerIdsChangedRequestHeader) request.decodeCommandCustomHeader(NotifyConsumerIdsChangedRequestHeader.class); - log.info("receive broker's notification[{}], the consumer group: {} changed, rebalance immediately", + logger.info("receive broker's notification[{}], the consumer group: {} changed, rebalance immediately", RemotingHelper.parseChannelRemoteAddr(ctx.channel()), requestHeader.getConsumerGroup()); this.mqClientFactory.rebalanceImmediately(); } catch (Exception e) { - log.error("notifyConsumerIdsChanged exception", RemotingHelper.exceptionSimpleDesc(e)); + logger.error("notifyConsumerIdsChanged exception", UtilAll.exceptionSimpleDesc(e)); } return null; } @@ -128,10 +150,10 @@ public RemotingCommand resetOffset(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException { final ResetOffsetRequestHeader requestHeader = (ResetOffsetRequestHeader) request.decodeCommandCustomHeader(ResetOffsetRequestHeader.class); - log.info("invoke reset offset operation from broker. brokerAddr={}, topic={}, group={}, timestamp={}", + logger.info("invoke reset offset operation from broker. brokerAddr={}, topic={}, group={}, timestamp={}", RemotingHelper.parseChannelRemoteAddr(ctx.channel()), requestHeader.getTopic(), requestHeader.getGroup(), requestHeader.getTimestamp()); - Map offsetTable = new HashMap(); + Map offsetTable = new HashMap<>(); if (request.getBody() != null) { ResetOffsetBody body = ResetOffsetBody.decode(request.getBody(), ResetOffsetBody.class); offsetTable = body.getOffsetTable(); @@ -186,7 +208,7 @@ private RemotingCommand consumeMessageDirectly(ChannelHandlerContext ctx, (ConsumeMessageDirectlyResultRequestHeader) request .decodeCommandCustomHeader(ConsumeMessageDirectlyResultRequestHeader.class); - final MessageExt msg = MessageDecoder.decode(ByteBuffer.wrap(request.getBody())); + final MessageExt msg = MessageDecoder.clientDecode(ByteBuffer.wrap(request.getBody()), true); ConsumeMessageDirectlyResult result = this.mqClientFactory.consumeMessageDirectly(msg, requestHeader.getConsumerGroup(), requestHeader.getBrokerName()); @@ -201,4 +223,73 @@ private RemotingCommand consumeMessageDirectly(ChannelHandlerContext ctx, return response; } + + private RemotingCommand receiveReplyMessage(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + long receiveTime = System.currentTimeMillis(); + ReplyMessageRequestHeader requestHeader = (ReplyMessageRequestHeader) request.decodeCommandCustomHeader(ReplyMessageRequestHeader.class); + + try { + MessageExt msg = new MessageExt(); + msg.setTopic(requestHeader.getTopic()); + msg.setQueueId(requestHeader.getQueueId()); + msg.setStoreTimestamp(requestHeader.getStoreTimestamp()); + + if (requestHeader.getBornHost() != null) { + msg.setBornHost(NetworkUtil.string2SocketAddress(requestHeader.getBornHost())); + } + + if (requestHeader.getStoreHost() != null) { + msg.setStoreHost(NetworkUtil.string2SocketAddress(requestHeader.getStoreHost())); + } + + byte[] body = request.getBody(); + int sysFlag = requestHeader.getSysFlag(); + if ((sysFlag & MessageSysFlag.COMPRESSED_FLAG) == MessageSysFlag.COMPRESSED_FLAG) { + try { + Compressor compressor = CompressorFactory.getCompressor(MessageSysFlag.getCompressionType(sysFlag)); + body = compressor.decompress(body); + } catch (IOException e) { + logger.warn("err when uncompress constant", e); + } + } + msg.setBody(body); + msg.setFlag(requestHeader.getFlag()); + MessageAccessor.setProperties(msg, MessageDecoder.string2messageProperties(requestHeader.getProperties())); + MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REPLY_MESSAGE_ARRIVE_TIME, String.valueOf(receiveTime)); + msg.setBornTimestamp(requestHeader.getBornTimestamp()); + msg.setReconsumeTimes(requestHeader.getReconsumeTimes() == null ? 0 : requestHeader.getReconsumeTimes()); + logger.debug("receive reply message :{}", msg); + + processReplyMessage(msg); + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + } catch (Exception e) { + logger.warn("unknown err when receiveReplyMsg", e); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("process reply message fail"); + } + return response; + } + + private void processReplyMessage(MessageExt replyMsg) { + final String correlationId = replyMsg.getUserProperty(MessageConst.PROPERTY_CORRELATION_ID); + final RequestResponseFuture requestResponseFuture = RequestFutureHolder.getInstance().getRequestFutureTable().get(correlationId); + if (requestResponseFuture != null) { + requestResponseFuture.putResponseMessage(replyMsg); + + RequestFutureHolder.getInstance().getRequestFutureTable().remove(correlationId); + + if (requestResponseFuture.getRequestCallback() != null) { + requestResponseFuture.getRequestCallback().onSuccess(replyMsg); + } + } else { + String bornHost = replyMsg.getBornHostString(); + logger.warn(String.format("receive reply message, but not matched any request, CorrelationId: %s , reply from host: %s", + correlationId, bornHost)); + } + } } diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/MQAdminImpl.java b/client/src/main/java/org/apache/rocketmq/client/impl/MQAdminImpl.java index b582b81e00d..83835bd3d3e 100644 --- a/client/src/main/java/org/apache/rocketmq/client/impl/MQAdminImpl.java +++ b/client/src/main/java/org/apache/rocketmq/client/impl/MQAdminImpl.java @@ -17,46 +17,50 @@ package org.apache.rocketmq.client.impl; import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; - import org.apache.rocketmq.client.QueryResult; +import org.apache.rocketmq.client.Validators; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.impl.factory.MQClientInstance; import org.apache.rocketmq.client.impl.producer.TopicPublishInfo; -import org.apache.rocketmq.client.log.ClientLogger; +import org.apache.rocketmq.common.BoundaryType; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.TopicConfig; import org.apache.rocketmq.common.help.FAQUrl; -import org.apache.rocketmq.common.message.MessageClientIDSetter; import org.apache.rocketmq.common.message.MessageConst; import org.apache.rocketmq.common.message.MessageDecoder; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageId; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.ResponseCode; -import org.apache.rocketmq.common.protocol.header.QueryMessageRequestHeader; -import org.apache.rocketmq.common.protocol.header.QueryMessageResponseHeader; -import org.apache.rocketmq.common.protocol.route.BrokerData; -import org.apache.rocketmq.common.protocol.route.TopicRouteData; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.remoting.InvokeCallback; -import org.apache.rocketmq.remoting.common.RemotingUtil; import org.apache.rocketmq.remoting.exception.RemotingCommandException; import org.apache.rocketmq.remoting.exception.RemotingException; import org.apache.rocketmq.remoting.netty.ResponseFuture; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; import org.apache.rocketmq.remoting.protocol.RemotingCommand; -import org.slf4j.Logger; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.QueryMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryMessageResponseHeader; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; public class MQAdminImpl { - private final Logger log = ClientLogger.getLog(); + private static final Logger log = LoggerFactory.getLogger(MQAdminImpl.class); private final MQClientInstance mQClientFactory; private long timeoutMillis = 6000; @@ -73,11 +77,14 @@ public void setTimeoutMillis(long timeoutMillis) { } public void createTopic(String key, String newTopic, int queueNum) throws MQClientException { - createTopic(key, newTopic, queueNum, 0); + createTopic(key, newTopic, queueNum, 0, null); } - public void createTopic(String key, String newTopic, int queueNum, int topicSysFlag) throws MQClientException { + public void createTopic(String key, String newTopic, int queueNum, int topicSysFlag, + Map attributes) throws MQClientException { try { + Validators.checkTopic(newTopic); + Validators.isSystemTopic(newTopic); TopicRouteData topicRouteData = this.mQClientFactory.getMQClientAPIImpl().getTopicRouteInfoFromNameServer(key, timeoutMillis); List brokerDataList = topicRouteData.getBrokerDatas(); if (brokerDataList != null && !brokerDataList.isEmpty()) { @@ -95,6 +102,7 @@ public void createTopic(String key, String newTopic, int queueNum, int topicSysF topicConfig.setReadQueueNums(queueNum); topicConfig.setWriteQueueNums(queueNum); topicConfig.setTopicSysFlag(topicSysFlag); + topicConfig.setAttributes(attributes); boolean createOK = false; for (int i = 0; i < 5; i++) { @@ -136,7 +144,7 @@ public List fetchPublishMessageQueues(String topic) throws MQClien if (topicRouteData != null) { TopicPublishInfo topicPublishInfo = MQClientInstance.topicRouteData2TopicPublishInfo(topic, topicRouteData); if (topicPublishInfo != null && topicPublishInfo.ok()) { - return topicPublishInfo.getMessageQueueList(); + return parsePublishMessageQueues(topicPublishInfo.getMessageQueueList()); } } } catch (Exception e) { @@ -146,6 +154,16 @@ public List fetchPublishMessageQueues(String topic) throws MQClien throw new MQClientException("Unknow why, Can not find Message Queue for this topic, " + topic, null); } + public List parsePublishMessageQueues(List messageQueueList) { + List resultQueues = new ArrayList<>(); + for (MessageQueue queue : messageQueueList) { + String userTopic = NamespaceUtil.withoutNamespace(queue.getTopic(), this.mQClientFactory.getClientConfig().getNamespace()); + resultQueues.add(new MessageQueue(userTopic, queue.getBrokerName(), queue.getQueueId())); + } + + return resultQueues; + } + public Set fetchSubscribeMessageQueues(String topic) throws MQClientException { try { TopicRouteData topicRouteData = this.mQClientFactory.getMQClientAPIImpl().getTopicRouteInfoFromNameServer(topic, timeoutMillis); @@ -167,16 +185,21 @@ public Set fetchSubscribeMessageQueues(String topic) throws MQClie } public long searchOffset(MessageQueue mq, long timestamp) throws MQClientException { - String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName()); + // default return lower boundary offset when there are more than one offsets. + return searchOffset(mq, timestamp, BoundaryType.LOWER); + } + + public long searchOffset(MessageQueue mq, long timestamp, BoundaryType boundaryType) throws MQClientException { + String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(this.mQClientFactory.getBrokerNameFromMessageQueue(mq)); if (null == brokerAddr) { this.mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic()); - brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName()); + brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(this.mQClientFactory.getBrokerNameFromMessageQueue(mq)); } if (brokerAddr != null) { try { - return this.mQClientFactory.getMQClientAPIImpl().searchOffset(brokerAddr, mq.getTopic(), mq.getQueueId(), timestamp, - timeoutMillis); + return this.mQClientFactory.getMQClientAPIImpl().searchOffset(brokerAddr, mq, timestamp, + boundaryType, timeoutMillis); } catch (Exception e) { throw new MQClientException("Invoke Broker[" + brokerAddr + "] exception", e); } @@ -186,15 +209,15 @@ public long searchOffset(MessageQueue mq, long timestamp) throws MQClientExcepti } public long maxOffset(MessageQueue mq) throws MQClientException { - String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName()); + String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(this.mQClientFactory.getBrokerNameFromMessageQueue(mq)); if (null == brokerAddr) { this.mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic()); - brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName()); + brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(this.mQClientFactory.getBrokerNameFromMessageQueue(mq)); } if (brokerAddr != null) { try { - return this.mQClientFactory.getMQClientAPIImpl().getMaxOffset(brokerAddr, mq.getTopic(), mq.getQueueId(), timeoutMillis); + return this.mQClientFactory.getMQClientAPIImpl().getMaxOffset(brokerAddr, mq, timeoutMillis); } catch (Exception e) { throw new MQClientException("Invoke Broker[" + brokerAddr + "] exception", e); } @@ -204,15 +227,15 @@ public long maxOffset(MessageQueue mq) throws MQClientException { } public long minOffset(MessageQueue mq) throws MQClientException { - String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName()); + String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(this.mQClientFactory.getBrokerNameFromMessageQueue(mq)); if (null == brokerAddr) { this.mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic()); - brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName()); + brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(this.mQClientFactory.getBrokerNameFromMessageQueue(mq)); } if (brokerAddr != null) { try { - return this.mQClientFactory.getMQClientAPIImpl().getMinOffset(brokerAddr, mq.getTopic(), mq.getQueueId(), timeoutMillis); + return this.mQClientFactory.getMQClientAPIImpl().getMinOffset(brokerAddr, mq, timeoutMillis); } catch (Exception e) { throw new MQClientException("Invoke Broker[" + brokerAddr + "] exception", e); } @@ -222,16 +245,15 @@ public long minOffset(MessageQueue mq) throws MQClientException { } public long earliestMsgStoreTime(MessageQueue mq) throws MQClientException { - String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName()); + String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(this.mQClientFactory.getBrokerNameFromMessageQueue(mq)); if (null == brokerAddr) { this.mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic()); - brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName()); + brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(this.mQClientFactory.getBrokerNameFromMessageQueue(mq)); } if (brokerAddr != null) { try { - return this.mQClientFactory.getMQClientAPIImpl().getEarliestMsgStoretime(brokerAddr, mq.getTopic(), mq.getQueueId(), - timeoutMillis); + return this.mQClientFactory.getMQClientAPIImpl().getEarliestMsgStoretime(brokerAddr, mq, timeoutMillis); } catch (Exception e) { throw new MQClientException("Invoke Broker[" + brokerAddr + "] exception", e); } @@ -240,16 +262,15 @@ public long earliestMsgStoreTime(MessageQueue mq) throws MQClientException { throw new MQClientException("The broker[" + mq.getBrokerName() + "] not exist", null); } - public MessageExt viewMessage( - String msgId) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { - + public MessageExt viewMessage(String msgId) + throws RemotingException, MQBrokerException, InterruptedException, MQClientException { MessageId messageId = null; try { messageId = MessageDecoder.decodeMessageId(msgId); } catch (Exception e) { throw new MQClientException(ResponseCode.NO_MESSAGE, "query message by id finished, but no message."); } - return this.mQClientFactory.getMQClientAPIImpl().viewMessage(RemotingUtil.socketAddress2String(messageId.getAddress()), + return this.mQClientFactory.getMQClientAPIImpl().viewMessage(NetworkUtil.socketAddress2String(messageId.getAddress()), messageId.getOffset(), timeoutMillis); } @@ -259,11 +280,30 @@ public QueryResult queryMessage(String topic, String key, int maxNum, long begin return queryMessage(topic, key, maxNum, begin, end, false); } + public QueryResult queryMessageByUniqKey(String topic, String uniqKey, int maxNum, long begin, long end) + throws MQClientException, InterruptedException { + + return queryMessage(topic, uniqKey, maxNum, begin, end, true); + } + public MessageExt queryMessageByUniqKey(String topic, String uniqKey) throws InterruptedException, MQClientException { + return queryMessageByUniqKey(topic, uniqKey, System.currentTimeMillis() - 3L * 24 * 60L * 60L * 1000L, Long.MAX_VALUE); + } + + public MessageExt queryMessageByUniqKey(String clusterName, String topic, + String uniqKey) throws InterruptedException, MQClientException { + return queryMessageByUniqKey(clusterName, topic, uniqKey, System.currentTimeMillis() - 3L * 24 * 60L * 60L * 1000L, Long.MAX_VALUE); + } + + public MessageExt queryMessageByUniqKey(String topic, + String uniqKey, long begin, long end) throws InterruptedException, MQClientException { + return queryMessageByUniqKey(null, topic, uniqKey, begin, end); + } - QueryResult qr = this.queryMessage(topic, uniqKey, 32, - MessageClientIDSetter.getNearlyTimeFromID(uniqKey).getTime() - 1000, Long.MAX_VALUE, true); + public MessageExt queryMessageByUniqKey(String clusterName, String topic, + String uniqKey, long begin, long end) throws InterruptedException, MQClientException { + QueryResult qr = this.queryMessage(clusterName, topic, uniqKey, 32, begin, end, true); if (qr != null && qr.getMessageList() != null && qr.getMessageList().size() > 0) { return qr.getMessageList().get(0); } else { @@ -272,6 +312,12 @@ public MessageExt queryMessageByUniqKey(String topic, } protected QueryResult queryMessage(String topic, String key, int maxNum, long begin, long end, + boolean isUniqKey) throws MQClientException, + InterruptedException { + return queryMessage(null, topic, key, maxNum, begin, end, isUniqKey); + } + + protected QueryResult queryMessage(String clusterName, String topic, String key, int maxNum, long begin, long end, boolean isUniqKey) throws MQClientException, InterruptedException { TopicRouteData topicRouteData = this.mQClientFactory.getAnExistTopicRouteData(topic); @@ -281,8 +327,12 @@ protected QueryResult queryMessage(String topic, String key, int maxNum, long be } if (topicRouteData != null) { - List brokerAddrs = new LinkedList(); + List brokerAddrs = new LinkedList<>(); for (BrokerData brokerData : topicRouteData.getBrokerDatas()) { + if (clusterName != null && !clusterName.isEmpty() + && !clusterName.equals(brokerData.getCluster())) { + continue; + } String addr = brokerData.selectBrokerAddr(); if (addr != null) { brokerAddrs.add(addr); @@ -291,7 +341,7 @@ protected QueryResult queryMessage(String topic, String key, int maxNum, long be if (!brokerAddrs.isEmpty()) { final CountDownLatch countDownLatch = new CountDownLatch(brokerAddrs.size()); - final List queryResultList = new LinkedList(); + final List queryResultList = new LinkedList<>(); final ReadWriteLock lock = new ReentrantReadWriteLock(false); for (String addr : brokerAddrs) { @@ -307,44 +357,51 @@ protected QueryResult queryMessage(String topic, String key, int maxNum, long be new InvokeCallback() { @Override public void operationComplete(ResponseFuture responseFuture) { + + } + + @Override + public void operationSucceed(RemotingCommand response) { try { - RemotingCommand response = responseFuture.getResponseCommand(); - if (response != null) { - switch (response.getCode()) { - case ResponseCode.SUCCESS: { - QueryMessageResponseHeader responseHeader = null; - try { - responseHeader = - (QueryMessageResponseHeader) response - .decodeCommandCustomHeader(QueryMessageResponseHeader.class); - } catch (RemotingCommandException e) { - log.error("decodeCommandCustomHeader exception", e); - return; - } - - List wrappers = - MessageDecoder.decodes(ByteBuffer.wrap(response.getBody()), true); - - QueryResult qr = new QueryResult(responseHeader.getIndexLastUpdateTimestamp(), wrappers); - try { - lock.writeLock().lock(); - queryResultList.add(qr); - } finally { - lock.writeLock().unlock(); - } - break; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + QueryMessageResponseHeader responseHeader = null; + try { + responseHeader = + (QueryMessageResponseHeader) response + .decodeCommandCustomHeader(QueryMessageResponseHeader.class); + } catch (RemotingCommandException e) { + log.error("decodeCommandCustomHeader exception", e); + return; + } + + List wrappers = + MessageDecoder.decodes(ByteBuffer.wrap(response.getBody()), true); + + QueryResult qr = new QueryResult(responseHeader.getIndexLastUpdateTimestamp(), wrappers); + try { + lock.writeLock().lock(); + queryResultList.add(qr); + } finally { + lock.writeLock().unlock(); } - default: - log.warn("getResponseCommand failed, {} {}", response.getCode(), response.getRemark()); - break; + break; } - } else { - log.warn("getResponseCommand return null"); + default: + log.warn("getResponseCommand failed, {} {}", response.getCode(), response.getRemark()); + break; } + } finally { countDownLatch.countDown(); } } + + @Override + public void operationFail(Throwable throwable) { + log.error("queryMessage error, requestHeader={}", requestHeader); + countDownLatch.countDown(); + } }, isUniqKey); } catch (Exception e) { log.warn("queryMessage exception", e); @@ -358,7 +415,7 @@ public void operationComplete(ResponseFuture responseFuture) { } long indexLastUpdateTimestamp = 0; - List messageList = new LinkedList(); + List messageList = new LinkedList<>(); for (QueryResult qr : queryResultList) { if (qr.getIndexLastUpdateTimestamp() > indexLastUpdateTimestamp) { indexLastUpdateTimestamp = qr.getIndexLastUpdateTimestamp(); @@ -367,33 +424,21 @@ public void operationComplete(ResponseFuture responseFuture) { for (MessageExt msgExt : qr.getMessageList()) { if (isUniqKey) { if (msgExt.getMsgId().equals(key)) { - - if (messageList.size() > 0) { - - if (messageList.get(0).getStoreTimestamp() > msgExt.getStoreTimestamp()) { - - messageList.clear(); - messageList.add(msgExt); - } - - } else { - - messageList.add(msgExt); - } + messageList.add(msgExt); } else { log.warn("queryMessage by uniqKey, find message key not matched, maybe hash duplicate {}", msgExt.toString()); } } else { String keys = msgExt.getKeys(); + String msgTopic = msgExt.getTopic(); if (keys != null) { boolean matched = false; String[] keyArray = keys.split(MessageConst.KEY_SEPARATOR); - if (keyArray != null) { - for (String k : keyArray) { - if (key.equals(k)) { - matched = true; - break; - } + for (String k : keyArray) { + // both topic and key must be equal at the same time + if (Objects.equals(key, k) && Objects.equals(topic, msgTopic)) { + matched = true; + break; } } @@ -407,6 +452,13 @@ public void operationComplete(ResponseFuture responseFuture) { } } + //If namespace not null , reset Topic without namespace. + if (null != this.mQClientFactory.getClientConfig().getNamespace()) { + for (MessageExt messageExt : messageList) { + messageExt.setTopic(NamespaceUtil.withoutNamespace(messageExt.getTopic(), this.mQClientFactory.getClientConfig().getNamespace())); + } + } + if (!messageList.isEmpty()) { return new QueryResult(indexLastUpdateTimestamp, messageList); } else { diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/MQClientAPIImpl.java b/client/src/main/java/org/apache/rocketmq/client/impl/MQClientAPIImpl.java index c5abc36bf67..1b4b3878c67 100644 --- a/client/src/main/java/org/apache/rocketmq/client/impl/MQClientAPIImpl.java +++ b/client/src/main/java/org/apache/rocketmq/client/impl/MQClientAPIImpl.java @@ -16,9 +16,12 @@ */ package org.apache.rocketmq.client.impl; +import com.alibaba.fastjson.JSON; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; @@ -27,27 +30,39 @@ import java.util.Properties; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; +import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.client.Validators; +import org.apache.rocketmq.client.consumer.AckCallback; +import org.apache.rocketmq.client.consumer.AckResult; +import org.apache.rocketmq.client.consumer.AckStatus; +import org.apache.rocketmq.client.consumer.PopCallback; +import org.apache.rocketmq.client.consumer.PopResult; +import org.apache.rocketmq.client.consumer.PopStatus; import org.apache.rocketmq.client.consumer.PullCallback; import org.apache.rocketmq.client.consumer.PullResult; import org.apache.rocketmq.client.consumer.PullStatus; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.exception.OffsetNotFoundException; import org.apache.rocketmq.client.hook.SendMessageContext; import org.apache.rocketmq.client.impl.consumer.PullResultExt; import org.apache.rocketmq.client.impl.factory.MQClientInstance; import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl; import org.apache.rocketmq.client.impl.producer.TopicPublishInfo; -import org.apache.rocketmq.client.log.ClientLogger; import org.apache.rocketmq.client.producer.SendCallback; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.client.rpchook.NamespaceRpcHook; +import org.apache.rocketmq.common.BoundaryType; import org.apache.rocketmq.common.MQVersion; import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.PlainAccessConfig; import org.apache.rocketmq.common.TopicConfig; import org.apache.rocketmq.common.UtilAll; -import org.apache.rocketmq.common.admin.ConsumeStats; -import org.apache.rocketmq.common.admin.TopicStatsTable; +import org.apache.rocketmq.common.attribute.AttributeParser; +import org.apache.rocketmq.common.constant.FIleReadaheadMode; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageBatch; import org.apache.rocketmq.common.message.MessageClientIDSetter; @@ -55,89 +70,22 @@ import org.apache.rocketmq.common.message.MessageDecoder; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.message.MessageQueueAssignment; +import org.apache.rocketmq.common.message.MessageRequestMode; +import org.apache.rocketmq.common.namesrv.DefaultTopAddressing; +import org.apache.rocketmq.common.namesrv.NameServerUpdateCallback; import org.apache.rocketmq.common.namesrv.TopAddressing; -import org.apache.rocketmq.common.protocol.RequestCode; -import org.apache.rocketmq.common.protocol.ResponseCode; -import org.apache.rocketmq.common.protocol.body.BrokerStatsData; -import org.apache.rocketmq.common.protocol.body.CheckClientRequestBody; -import org.apache.rocketmq.common.protocol.body.ClusterInfo; -import org.apache.rocketmq.common.protocol.body.ConsumeMessageDirectlyResult; -import org.apache.rocketmq.common.protocol.body.ConsumeStatsList; -import org.apache.rocketmq.common.protocol.body.ConsumerConnection; -import org.apache.rocketmq.common.protocol.body.ConsumerRunningInfo; -import org.apache.rocketmq.common.protocol.body.GetConsumerStatusBody; -import org.apache.rocketmq.common.protocol.body.GroupList; -import org.apache.rocketmq.common.protocol.body.KVTable; -import org.apache.rocketmq.common.protocol.body.LockBatchRequestBody; -import org.apache.rocketmq.common.protocol.body.LockBatchResponseBody; -import org.apache.rocketmq.common.protocol.body.ProducerConnection; -import org.apache.rocketmq.common.protocol.body.QueryConsumeQueueResponseBody; -import org.apache.rocketmq.common.protocol.body.QueryConsumeTimeSpanBody; -import org.apache.rocketmq.common.protocol.body.QueryCorrectionOffsetBody; -import org.apache.rocketmq.common.protocol.body.QueueTimeSpan; -import org.apache.rocketmq.common.protocol.body.ResetOffsetBody; -import org.apache.rocketmq.common.protocol.body.SubscriptionGroupWrapper; -import org.apache.rocketmq.common.protocol.body.TopicConfigSerializeWrapper; -import org.apache.rocketmq.common.protocol.body.TopicList; -import org.apache.rocketmq.common.protocol.body.UnlockBatchRequestBody; -import org.apache.rocketmq.common.protocol.header.CloneGroupOffsetRequestHeader; -import org.apache.rocketmq.common.protocol.header.ConsumeMessageDirectlyResultRequestHeader; -import org.apache.rocketmq.common.protocol.header.ConsumerSendMsgBackRequestHeader; -import org.apache.rocketmq.common.protocol.header.CreateTopicRequestHeader; -import org.apache.rocketmq.common.protocol.header.DeleteSubscriptionGroupRequestHeader; -import org.apache.rocketmq.common.protocol.header.DeleteTopicRequestHeader; -import org.apache.rocketmq.common.protocol.header.EndTransactionRequestHeader; -import org.apache.rocketmq.common.protocol.header.GetConsumeStatsInBrokerHeader; -import org.apache.rocketmq.common.protocol.header.GetConsumeStatsRequestHeader; -import org.apache.rocketmq.common.protocol.header.GetConsumerConnectionListRequestHeader; -import org.apache.rocketmq.common.protocol.header.GetConsumerListByGroupRequestHeader; -import org.apache.rocketmq.common.protocol.header.GetConsumerListByGroupResponseBody; -import org.apache.rocketmq.common.protocol.header.GetConsumerRunningInfoRequestHeader; -import org.apache.rocketmq.common.protocol.header.GetConsumerStatusRequestHeader; -import org.apache.rocketmq.common.protocol.header.GetEarliestMsgStoretimeRequestHeader; -import org.apache.rocketmq.common.protocol.header.GetEarliestMsgStoretimeResponseHeader; -import org.apache.rocketmq.common.protocol.header.GetMaxOffsetRequestHeader; -import org.apache.rocketmq.common.protocol.header.GetMaxOffsetResponseHeader; -import org.apache.rocketmq.common.protocol.header.GetMinOffsetRequestHeader; -import org.apache.rocketmq.common.protocol.header.GetMinOffsetResponseHeader; -import org.apache.rocketmq.common.protocol.header.GetProducerConnectionListRequestHeader; -import org.apache.rocketmq.common.protocol.header.GetTopicStatsInfoRequestHeader; -import org.apache.rocketmq.common.protocol.header.GetTopicsByClusterRequestHeader; -import org.apache.rocketmq.common.protocol.header.PullMessageRequestHeader; -import org.apache.rocketmq.common.protocol.header.PullMessageResponseHeader; -import org.apache.rocketmq.common.protocol.header.QueryConsumeQueueRequestHeader; -import org.apache.rocketmq.common.protocol.header.QueryConsumeTimeSpanRequestHeader; -import org.apache.rocketmq.common.protocol.header.QueryConsumerOffsetRequestHeader; -import org.apache.rocketmq.common.protocol.header.QueryConsumerOffsetResponseHeader; -import org.apache.rocketmq.common.protocol.header.QueryCorrectionOffsetHeader; -import org.apache.rocketmq.common.protocol.header.QueryMessageRequestHeader; -import org.apache.rocketmq.common.protocol.header.QueryTopicConsumeByWhoRequestHeader; -import org.apache.rocketmq.common.protocol.header.ResetOffsetRequestHeader; -import org.apache.rocketmq.common.protocol.header.SearchOffsetRequestHeader; -import org.apache.rocketmq.common.protocol.header.SearchOffsetResponseHeader; -import org.apache.rocketmq.common.protocol.header.SendMessageRequestHeader; -import org.apache.rocketmq.common.protocol.header.SendMessageRequestHeaderV2; -import org.apache.rocketmq.common.protocol.header.SendMessageResponseHeader; -import org.apache.rocketmq.common.protocol.header.UnregisterClientRequestHeader; -import org.apache.rocketmq.common.protocol.header.UpdateConsumerOffsetRequestHeader; -import org.apache.rocketmq.common.protocol.header.ViewBrokerStatsDataRequestHeader; -import org.apache.rocketmq.common.protocol.header.ViewMessageRequestHeader; -import org.apache.rocketmq.common.protocol.header.filtersrv.RegisterMessageFilterClassRequestHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.DeleteKVConfigRequestHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.GetKVConfigRequestHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.GetKVConfigResponseHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.GetKVListByNamespaceRequestHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.GetRouteInfoRequestHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.PutKVConfigRequestHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.WipeWritePermOfBrokerRequestHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.WipeWritePermOfBrokerResponseHeader; -import org.apache.rocketmq.common.protocol.heartbeat.HeartbeatData; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; -import org.apache.rocketmq.common.protocol.route.TopicRouteData; -import org.apache.rocketmq.common.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.common.sysflag.PullSysFlag; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.ChannelEventListener; +import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.InvokeCallback; import org.apache.rocketmq.remoting.RPCHook; import org.apache.rocketmq.remoting.RemotingClient; +import org.apache.rocketmq.remoting.common.HeartbeatV2Result; +import org.apache.rocketmq.remoting.common.RemotingHelper; import org.apache.rocketmq.remoting.exception.RemotingCommandException; import org.apache.rocketmq.remoting.exception.RemotingConnectException; import org.apache.rocketmq.remoting.exception.RemotingException; @@ -147,15 +95,145 @@ import org.apache.rocketmq.remoting.netty.NettyClientConfig; import org.apache.rocketmq.remoting.netty.NettyRemotingClient; import org.apache.rocketmq.remoting.netty.ResponseFuture; +import org.apache.rocketmq.remoting.protocol.DataVersion; import org.apache.rocketmq.remoting.protocol.LanguageCode; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; import org.apache.rocketmq.remoting.protocol.RemotingCommand; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; -import org.slf4j.Logger; - -public class MQClientAPIImpl { - - private final static Logger log = ClientLogger.getLog(); - public static boolean sendSmartMsg = +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.admin.ConsumeStats; +import org.apache.rocketmq.remoting.protocol.admin.TopicStatsTable; +import org.apache.rocketmq.remoting.protocol.body.BatchAck; +import org.apache.rocketmq.remoting.protocol.body.BatchAckMessageRequestBody; +import org.apache.rocketmq.remoting.protocol.body.BrokerMemberGroup; +import org.apache.rocketmq.remoting.protocol.body.BrokerReplicasInfo; +import org.apache.rocketmq.remoting.protocol.body.BrokerStatsData; +import org.apache.rocketmq.remoting.protocol.body.CheckClientRequestBody; +import org.apache.rocketmq.remoting.protocol.body.ClusterAclVersionInfo; +import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumeStatsList; +import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.body.EpochEntryCache; +import org.apache.rocketmq.remoting.protocol.body.GetConsumerStatusBody; +import org.apache.rocketmq.remoting.protocol.body.GroupList; +import org.apache.rocketmq.remoting.protocol.body.HARuntimeInfo; +import org.apache.rocketmq.remoting.protocol.body.KVTable; +import org.apache.rocketmq.remoting.protocol.body.LockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.body.LockBatchResponseBody; +import org.apache.rocketmq.remoting.protocol.body.ProducerConnection; +import org.apache.rocketmq.remoting.protocol.body.ProducerTableInfo; +import org.apache.rocketmq.remoting.protocol.body.QueryAssignmentRequestBody; +import org.apache.rocketmq.remoting.protocol.body.QueryAssignmentResponseBody; +import org.apache.rocketmq.remoting.protocol.body.QueryConsumeQueueResponseBody; +import org.apache.rocketmq.remoting.protocol.body.QueryConsumeTimeSpanBody; +import org.apache.rocketmq.remoting.protocol.body.QueryCorrectionOffsetBody; +import org.apache.rocketmq.remoting.protocol.body.QuerySubscriptionResponseBody; +import org.apache.rocketmq.remoting.protocol.body.QueueTimeSpan; +import org.apache.rocketmq.remoting.protocol.body.ResetOffsetBody; +import org.apache.rocketmq.remoting.protocol.body.SetMessageRequestModeRequestBody; +import org.apache.rocketmq.remoting.protocol.body.SubscriptionGroupWrapper; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.body.TopicList; +import org.apache.rocketmq.remoting.protocol.body.UnlockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.header.AckMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.AddBrokerRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ChangeInvisibleTimeRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ChangeInvisibleTimeResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.CloneGroupOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ConsumeMessageDirectlyResultRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ConsumerSendMsgBackRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.CreateAccessConfigRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.CreateTopicRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.DeleteAccessConfigRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.DeleteSubscriptionGroupRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.DeleteTopicRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.EndTransactionRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ExtraInfoUtil; +import org.apache.rocketmq.remoting.protocol.header.GetAllProducerInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetBrokerAclConfigResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumeStatsInBrokerHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumeStatsRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerConnectionListRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerListByGroupRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerListByGroupResponseBody; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerRunningInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerStatusRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetEarliestMsgStoretimeRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetEarliestMsgStoretimeResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMaxOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMaxOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMinOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMinOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.GetProducerConnectionListRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetSubscriptionGroupConfigRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetTopicConfigRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetTopicStatsInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetTopicsByClusterRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.HeartbeatRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.LockBatchMqRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PopMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PopMessageResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PullMessageResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumeQueueRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumeTimeSpanRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumerOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumerOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryCorrectionOffsetHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QuerySubscriptionByConsumerRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryTopicConsumeByWhoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryTopicsByConsumerRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.RemoveBrokerRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ResetMasterFlushOffsetHeader; +import org.apache.rocketmq.remoting.protocol.header.ResetOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ResumeCheckHalfMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SearchOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SearchOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeaderV2; +import org.apache.rocketmq.remoting.protocol.header.SendMessageResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.UnlockBatchMqRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UnregisterClientRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateConsumerOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateGlobalWhiteAddrsConfigRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateGroupForbiddenRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ViewBrokerStatsDataRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ViewMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetMetaDataResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.admin.CleanControllerBrokerDataRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.AddWritePermOfBrokerRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.AddWritePermOfBrokerResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.DeleteKVConfigRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.DeleteTopicFromNamesrvRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.GetKVConfigRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.GetKVConfigResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.GetKVListByNamespaceRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.GetRouteInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.PutKVConfigRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.WipeWritePermOfBrokerRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.WipeWritePermOfBrokerResponseHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.HeartbeatData; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicConfigAndQueueMapping; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingDetail; +import org.apache.rocketmq.remoting.protocol.subscription.GroupForbidden; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.remoting.rpchook.DynamicalExtFieldRPCHook; +import org.apache.rocketmq.remoting.rpchook.StreamTypeRPCHook; + +import static org.apache.rocketmq.remoting.protocol.RemotingSysResponseCode.SUCCESS; + +public class MQClientAPIImpl implements NameServerUpdateCallback { + private final static Logger log = LoggerFactory.getLogger(MQClientAPIImpl.class); + private static boolean sendSmartMsg = Boolean.parseBoolean(System.getProperty("org.apache.rocketmq.client.sendSmartMsg", "true")); static { @@ -171,12 +249,25 @@ public class MQClientAPIImpl { public MQClientAPIImpl(final NettyClientConfig nettyClientConfig, final ClientRemotingProcessor clientRemotingProcessor, RPCHook rpcHook, final ClientConfig clientConfig) { + this(nettyClientConfig, clientRemotingProcessor, rpcHook, clientConfig, null); + } + + public MQClientAPIImpl(final NettyClientConfig nettyClientConfig, + final ClientRemotingProcessor clientRemotingProcessor, + RPCHook rpcHook, final ClientConfig clientConfig, final ChannelEventListener channelEventListener) { this.clientConfig = clientConfig; - topAddressing = new TopAddressing(MixAll.getWSAddr(), clientConfig.getUnitName()); - this.remotingClient = new NettyRemotingClient(nettyClientConfig, null); + topAddressing = new DefaultTopAddressing(MixAll.getWSAddr(), clientConfig.getUnitName()); + topAddressing.registerChangeCallBack(this); + this.remotingClient = new NettyRemotingClient(nettyClientConfig, channelEventListener); this.clientRemotingProcessor = clientRemotingProcessor; + this.remotingClient.registerRPCHook(new NamespaceRpcHook(clientConfig)); + // Inject stream rpc hook first to make reserve field signature + if (clientConfig.isEnableStreamRequestType()) { + this.remotingClient.registerRPCHook(new StreamTypeRPCHook()); + } this.remotingClient.registerRPCHook(rpcHook); + this.remotingClient.registerRPCHook(new DynamicalExtFieldRPCHook()); this.remotingClient.registerProcessor(RequestCode.CHECK_TRANSACTION_STATE, this.clientRemotingProcessor, null); this.remotingClient.registerProcessor(RequestCode.NOTIFY_CONSUMER_IDS_CHANGED, this.clientRemotingProcessor, null); @@ -188,6 +279,8 @@ public MQClientAPIImpl(final NettyClientConfig nettyClientConfig, this.remotingClient.registerProcessor(RequestCode.GET_CONSUMER_RUNNING_INFO, this.clientRemotingProcessor, null); this.remotingClient.registerProcessor(RequestCode.CONSUME_MESSAGE_DIRECTLY, this.clientRemotingProcessor, null); + + this.remotingClient.registerProcessor(RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT, this.clientRemotingProcessor, null); } public List getNameServerAddressList() { @@ -201,7 +294,7 @@ public RemotingClient getRemotingClient() { public String fetchNameServerAddr() { try { String addrs = this.topAddressing.fetchNSAddr(); - if (addrs != null) { + if (!UtilAll.isBlank(addrs)) { if (!addrs.equals(this.nameSrvAddr)) { log.info("name server address changed, old=" + this.nameSrvAddr + ", new=" + addrs); this.updateNameServerAddressList(addrs); @@ -215,14 +308,23 @@ public String fetchNameServerAddr() { return nameSrvAddr; } - public void updateNameServerAddressList(final String addrs) { - List lst = new ArrayList(); - String[] addrArray = addrs.split(";"); - for (String addr : addrArray) { - lst.add(addr); + @Override + public String onNameServerAddressChange(String namesrvAddress) { + if (namesrvAddress != null) { + if (!namesrvAddress.equals(this.nameSrvAddr)) { + log.info("name server address changed, old=" + this.nameSrvAddr + ", new=" + namesrvAddress); + this.updateNameServerAddressList(namesrvAddress); + this.nameSrvAddr = namesrvAddress; + return nameSrvAddr; + } } + return nameSrvAddr; + } - this.remotingClient.updateNameServerAddressList(lst); + public void updateNameServerAddressList(final String addrs) { + String[] addrArray = addrs.split(";"); + List list = Arrays.asList(addrArray); + this.remotingClient.updateNameServerAddressList(list); } public void start() { @@ -233,9 +335,36 @@ public void shutdown() { this.remotingClient.shutdown(); } + public Set queryAssignment(final String addr, final String topic, + final String consumerGroup, final String clientId, final String strategyName, + final MessageModel messageModel, final long timeoutMillis) + throws RemotingException, MQBrokerException, InterruptedException { + QueryAssignmentRequestBody requestBody = new QueryAssignmentRequestBody(); + requestBody.setTopic(topic); + requestBody.setConsumerGroup(consumerGroup); + requestBody.setClientId(clientId); + requestBody.setMessageModel(messageModel); + requestBody.setStrategyName(strategyName); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_ASSIGNMENT, null); + request.setBody(requestBody.encode()); + + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), + request, timeoutMillis); + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + QueryAssignmentResponseBody queryAssignmentResponseBody = QueryAssignmentResponseBody.decode(response.getBody(), QueryAssignmentResponseBody.class); + return queryAssignmentResponseBody.getMessageQueueAssignments(); + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + public void createSubscriptionGroup(final String addr, final SubscriptionGroupConfig config, - final long timeoutMillis) - throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + final long timeoutMillis) throws RemotingException, InterruptedException, MQClientException { RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_AND_CREATE_SUBSCRIPTIONGROUP, null); byte[] body = RemotingSerializable.encode(config); @@ -259,6 +388,8 @@ public void createSubscriptionGroup(final String addr, final SubscriptionGroupCo public void createTopic(final String addr, final String defaultTopic, final TopicConfig topicConfig, final long timeoutMillis) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + Validators.checkTopicConfig(topicConfig); + CreateTopicRequestHeader requestHeader = new CreateTopicRequestHeader(); requestHeader.setTopic(topicConfig.getTopicName()); requestHeader.setDefaultTopic(defaultTopic); @@ -268,6 +399,7 @@ public void createTopic(final String addr, final String defaultTopic, final Topi requestHeader.setTopicFilterType(topicConfig.getTopicFilterType().name()); requestHeader.setTopicSysFlag(topicConfig.getTopicSysFlag()); requestHeader.setOrder(topicConfig.isOrder()); + requestHeader.setAttributes(AttributeParser.parseToString(topicConfig.getAttributes())); RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_AND_CREATE_TOPIC, requestHeader); @@ -285,6 +417,112 @@ public void createTopic(final String addr, final String defaultTopic, final Topi throw new MQClientException(response.getCode(), response.getRemark()); } + public void createPlainAccessConfig(final String addr, final PlainAccessConfig plainAccessConfig, + final long timeoutMillis) + throws RemotingException, InterruptedException, MQClientException { + CreateAccessConfigRequestHeader requestHeader = new CreateAccessConfigRequestHeader(); + requestHeader.setAccessKey(plainAccessConfig.getAccessKey()); + requestHeader.setSecretKey(plainAccessConfig.getSecretKey()); + requestHeader.setAdmin(plainAccessConfig.isAdmin()); + requestHeader.setDefaultGroupPerm(plainAccessConfig.getDefaultGroupPerm()); + requestHeader.setDefaultTopicPerm(plainAccessConfig.getDefaultTopicPerm()); + requestHeader.setWhiteRemoteAddress(plainAccessConfig.getWhiteRemoteAddress()); + requestHeader.setTopicPerms(UtilAll.join(plainAccessConfig.getTopicPerms(), ",")); + requestHeader.setGroupPerms(UtilAll.join(plainAccessConfig.getGroupPerms(), ",")); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_AND_CREATE_ACL_CONFIG, requestHeader); + + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), + request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return; + } + default: + break; + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + + public void deleteAccessConfig(final String addr, final String accessKey, final long timeoutMillis) + throws RemotingException, InterruptedException, MQClientException { + DeleteAccessConfigRequestHeader requestHeader = new DeleteAccessConfigRequestHeader(); + requestHeader.setAccessKey(accessKey); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.DELETE_ACL_CONFIG, requestHeader); + + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), + request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return; + } + default: + break; + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + + public void updateGlobalWhiteAddrsConfig(final String addr, final String globalWhiteAddrs, String aclFileFullPath, + final long timeoutMillis) + throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + UpdateGlobalWhiteAddrsConfigRequestHeader requestHeader = new UpdateGlobalWhiteAddrsConfigRequestHeader(); + requestHeader.setGlobalWhiteAddrs(globalWhiteAddrs); + requestHeader.setAclFileFullPath(aclFileFullPath); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_GLOBAL_WHITE_ADDRS_CONFIG, requestHeader); + + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), + request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return; + } + default: + break; + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + + public ClusterAclVersionInfo getBrokerClusterAclInfo(final String addr, + final long timeoutMillis) throws RemotingCommandException, InterruptedException, RemotingTimeoutException, + RemotingSendRequestException, RemotingConnectException, MQBrokerException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_BROKER_CLUSTER_ACL_INFO, null); + + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + GetBrokerAclConfigResponseHeader responseHeader = + (GetBrokerAclConfigResponseHeader) response.decodeCommandCustomHeader(GetBrokerAclConfigResponseHeader.class); + + ClusterAclVersionInfo clusterAclVersionInfo = new ClusterAclVersionInfo(); + clusterAclVersionInfo.setClusterName(responseHeader.getClusterName()); + clusterAclVersionInfo.setBrokerName(responseHeader.getBrokerName()); + clusterAclVersionInfo.setBrokerAddr(responseHeader.getBrokerAddr()); + clusterAclVersionInfo.setAclConfigDataVersion(DataVersion.fromJson(responseHeader.getVersion(), DataVersion.class)); + HashMap dataVersionMap = JSON.parseObject(responseHeader.getAllAclFileVersion(), HashMap.class); + Map allAclConfigDataVersion = new HashMap<>(dataVersionMap.size(), 1); + for (Map.Entry entry : dataVersionMap.entrySet()) { + allAclConfigDataVersion.put(entry.getKey(), DataVersion.fromJson(JSON.toJSONString(entry.getValue()), DataVersion.class)); + } + clusterAclVersionInfo.setAllAclConfigDataVersion(allAclConfigDataVersion); + return clusterAclVersionInfo; + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); + + } + public SendResult sendMessage( final String addr, final String brokerName, @@ -312,14 +550,25 @@ public SendResult sendMessage( final SendMessageContext context, final DefaultMQProducerImpl producer ) throws RemotingException, MQBrokerException, InterruptedException { + long beginStartTime = System.currentTimeMillis(); RemotingCommand request = null; - if (sendSmartMsg || msg instanceof MessageBatch) { - SendMessageRequestHeaderV2 requestHeaderV2 = SendMessageRequestHeaderV2.createSendMessageRequestHeaderV2(requestHeader); - request = RemotingCommand.createRequestCommand(msg instanceof MessageBatch ? RequestCode.SEND_BATCH_MESSAGE : RequestCode.SEND_MESSAGE_V2, requestHeaderV2); + String msgType = msg.getProperty(MessageConst.PROPERTY_MESSAGE_TYPE); + boolean isReply = msgType != null && msgType.equals(MixAll.REPLY_MESSAGE_FLAG); + if (isReply) { + if (sendSmartMsg) { + SendMessageRequestHeaderV2 requestHeaderV2 = SendMessageRequestHeaderV2.createSendMessageRequestHeaderV2(requestHeader); + request = RemotingCommand.createRequestCommand(RequestCode.SEND_REPLY_MESSAGE_V2, requestHeaderV2); + } else { + request = RemotingCommand.createRequestCommand(RequestCode.SEND_REPLY_MESSAGE, requestHeader); + } } else { - request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, requestHeader); + if (sendSmartMsg || msg instanceof MessageBatch) { + SendMessageRequestHeaderV2 requestHeaderV2 = SendMessageRequestHeaderV2.createSendMessageRequestHeaderV2(requestHeader); + request = RemotingCommand.createRequestCommand(msg instanceof MessageBatch ? RequestCode.SEND_BATCH_MESSAGE : RequestCode.SEND_MESSAGE_V2, requestHeaderV2); + } else { + request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, requestHeader); + } } - request.setBody(msg.getBody()); switch (communicationMode) { @@ -328,11 +577,19 @@ public SendResult sendMessage( return null; case ASYNC: final AtomicInteger times = new AtomicInteger(); - this.sendMessageAsync(addr, brokerName, msg, timeoutMillis, request, sendCallback, topicPublishInfo, instance, + long costTimeAsync = System.currentTimeMillis() - beginStartTime; + if (timeoutMillis < costTimeAsync) { + throw new RemotingTooMuchRequestException("sendMessage call timeout"); + } + this.sendMessageAsync(addr, brokerName, msg, timeoutMillis - costTimeAsync, request, sendCallback, topicPublishInfo, instance, retryTimesWhenSendFailed, times, context, producer); return null; case SYNC: - return this.sendMessageSync(addr, brokerName, msg, timeoutMillis, request); + long costTimeSync = System.currentTimeMillis() - beginStartTime; + if (timeoutMillis < costTimeSync) { + throw new RemotingTooMuchRequestException("sendMessage call timeout"); + } + return this.sendMessageSync(addr, brokerName, msg, timeoutMillis - costTimeSync, request); default: assert false; break; @@ -350,7 +607,15 @@ private SendResult sendMessageSync( ) throws RemotingException, MQBrokerException, InterruptedException { RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis); assert response != null; - return this.processSendResponse(brokerName, msg, response); + return this.processSendResponse(brokerName, msg, response, addr); + } + + void execRpcHooksAfterRequest(ResponseFuture responseFuture) { + if (this.remotingClient instanceof NettyRemotingClient) { + NettyRemotingClient remotingClient = (NettyRemotingClient) this.remotingClient; + RemotingCommand response = responseFuture.getResponseCommand(); + remotingClient.doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(responseFuture.getChannel()), responseFuture.getRequestCommand(), response); + } } private void sendMessageAsync( @@ -366,29 +631,34 @@ private void sendMessageAsync( final AtomicInteger times, final SendMessageContext context, final DefaultMQProducerImpl producer - ) throws InterruptedException, RemotingException { - this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() { - @Override - public void operationComplete(ResponseFuture responseFuture) { - RemotingCommand response = responseFuture.getResponseCommand(); - if (null == sendCallback && response != null) { + ) { + final long beginStartTime = System.currentTimeMillis(); + try { + this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { - try { - SendResult sendResult = MQClientAPIImpl.this.processSendResponse(brokerName, msg, response); - if (context != null && sendResult != null) { - context.setSendResult(sendResult); - context.getProducer().executeSendMessageHookAfter(context); + } + + @Override + public void operationSucceed(RemotingCommand response) { + long cost = System.currentTimeMillis() - beginStartTime; + if (null == sendCallback) { + try { + SendResult sendResult = MQClientAPIImpl.this.processSendResponse(brokerName, msg, response, addr); + if (context != null && sendResult != null) { + context.setSendResult(sendResult); + context.getProducer().executeSendMessageHookAfter(context); + } + } catch (Throwable e) { } - } catch (Throwable e) { - } - producer.updateFaultItem(brokerName, System.currentTimeMillis() - responseFuture.getBeginTimestamp(), false); - return; - } + producer.updateFaultItem(brokerName, System.currentTimeMillis() - beginStartTime, false, true); + return; + } - if (response != null) { try { - SendResult sendResult = MQClientAPIImpl.this.processSendResponse(brokerName, msg, response); + SendResult sendResult = MQClientAPIImpl.this.processSendResponse(brokerName, msg, response, addr); assert sendResult != null; if (context != null) { context.setSendResult(sendResult); @@ -400,31 +670,39 @@ public void operationComplete(ResponseFuture responseFuture) { } catch (Throwable e) { } - producer.updateFaultItem(brokerName, System.currentTimeMillis() - responseFuture.getBeginTimestamp(), false); + producer.updateFaultItem(brokerName, System.currentTimeMillis() - beginStartTime, false, true); } catch (Exception e) { - producer.updateFaultItem(brokerName, System.currentTimeMillis() - responseFuture.getBeginTimestamp(), true); - onExceptionImpl(brokerName, msg, 0L, request, sendCallback, topicPublishInfo, instance, + producer.updateFaultItem(brokerName, System.currentTimeMillis() - beginStartTime, true, true); + onExceptionImpl(brokerName, msg, timeoutMillis - cost, request, sendCallback, topicPublishInfo, instance, retryTimesWhenSendFailed, times, e, context, false, producer); } - } else { - producer.updateFaultItem(brokerName, System.currentTimeMillis() - responseFuture.getBeginTimestamp(), true); - if (!responseFuture.isSendRequestOK()) { - MQClientException ex = new MQClientException("send request failed", responseFuture.getCause()); - onExceptionImpl(brokerName, msg, 0L, request, sendCallback, topicPublishInfo, instance, + } + + @Override + public void operationFail(Throwable throwable) { + producer.updateFaultItem(brokerName, System.currentTimeMillis() - beginStartTime, true, true); + long cost = System.currentTimeMillis() - beginStartTime; + if (throwable instanceof RemotingSendRequestException) { + MQClientException ex = new MQClientException("send request failed", throwable); + onExceptionImpl(brokerName, msg, timeoutMillis - cost, request, sendCallback, topicPublishInfo, instance, retryTimesWhenSendFailed, times, ex, context, true, producer); - } else if (responseFuture.isTimeout()) { - MQClientException ex = new MQClientException("wait response timeout " + responseFuture.getTimeoutMillis() + "ms", - responseFuture.getCause()); - onExceptionImpl(brokerName, msg, 0L, request, sendCallback, topicPublishInfo, instance, + } else if (throwable instanceof RemotingTimeoutException) { + MQClientException ex = new MQClientException("wait response timeout, cost=" + cost, throwable); + onExceptionImpl(brokerName, msg, timeoutMillis - cost, request, sendCallback, topicPublishInfo, instance, retryTimesWhenSendFailed, times, ex, context, true, producer); } else { - MQClientException ex = new MQClientException("unknow reseaon", responseFuture.getCause()); - onExceptionImpl(brokerName, msg, 0L, request, sendCallback, topicPublishInfo, instance, + MQClientException ex = new MQClientException("unknow reseaon", throwable); + onExceptionImpl(brokerName, msg, timeoutMillis - cost, request, sendCallback, topicPublishInfo, instance, retryTimesWhenSendFailed, times, ex, context, true, producer); } } - } - }); + }); + } catch (Exception ex) { + long cost = System.currentTimeMillis() - beginStartTime; + producer.updateFaultItem(brokerName, cost, true, false); + onExceptionImpl(brokerName, msg, timeoutMillis - cost, request, sendCallback, topicPublishInfo, instance, + retryTimesWhenSendFailed, times, ex, context, true, producer); + } } private void onExceptionImpl(final String brokerName, @@ -445,31 +723,15 @@ private void onExceptionImpl(final String brokerName, if (needRetry && tmp <= timesTotal) { String retryBrokerName = brokerName;//by default, it will send to the same broker if (topicPublishInfo != null) { //select one message queue accordingly, in order to determine which broker to send - MessageQueue mqChosen = producer.selectOneMessageQueue(topicPublishInfo, brokerName); - retryBrokerName = mqChosen.getBrokerName(); + MessageQueue mqChosen = producer.selectOneMessageQueue(topicPublishInfo, brokerName, false); + retryBrokerName = instance.getBrokerNameFromMessageQueue(mqChosen); } String addr = instance.findBrokerAddressInPublish(retryBrokerName); - log.info("async send msg by retry {} times. topic={}, brokerAddr={}, brokerName={}", tmp, msg.getTopic(), addr, - retryBrokerName); - try { - request.setOpaque(RemotingCommand.createNewRequestId()); - sendMessageAsync(addr, retryBrokerName, msg, timeoutMillis, request, sendCallback, topicPublishInfo, instance, - timesTotal, curTimes, context, producer); - } catch (InterruptedException e1) { - onExceptionImpl(retryBrokerName, msg, timeoutMillis, request, sendCallback, topicPublishInfo, instance, timesTotal, curTimes, e1, - context, false, producer); - } catch (RemotingConnectException e1) { - producer.updateFaultItem(brokerName, 3000, true); - onExceptionImpl(retryBrokerName, msg, timeoutMillis, request, sendCallback, topicPublishInfo, instance, timesTotal, curTimes, e1, - context, true, producer); - } catch (RemotingTooMuchRequestException e1) { - onExceptionImpl(retryBrokerName, msg, timeoutMillis, request, sendCallback, topicPublishInfo, instance, timesTotal, curTimes, e1, - context, false, producer); - } catch (RemotingException e1) { - producer.updateFaultItem(brokerName, 3000, true); - onExceptionImpl(retryBrokerName, msg, timeoutMillis, request, sendCallback, topicPublishInfo, instance, timesTotal, curTimes, e1, - context, true, producer); - } + log.warn("async send msg by retry {} times. topic={}, brokerAddr={}, brokerName={}", tmp, msg.getTopic(), addr, + retryBrokerName, e); + request.setOpaque(RemotingCommand.createNewRequestId()); + sendMessageAsync(addr, retryBrokerName, msg, timeoutMillis, request, sendCallback, topicPublishInfo, instance, + timesTotal, curTimes, context, producer); } else { if (context != null) { @@ -484,71 +746,67 @@ private void onExceptionImpl(final String brokerName, } } - private SendResult processSendResponse( + protected SendResult processSendResponse( final String brokerName, final Message msg, - final RemotingCommand response + final RemotingCommand response, + final String addr ) throws MQBrokerException, RemotingCommandException { + SendStatus sendStatus; switch (response.getCode()) { - case ResponseCode.FLUSH_DISK_TIMEOUT: - case ResponseCode.FLUSH_SLAVE_TIMEOUT: + case ResponseCode.FLUSH_DISK_TIMEOUT: { + sendStatus = SendStatus.FLUSH_DISK_TIMEOUT; + break; + } + case ResponseCode.FLUSH_SLAVE_TIMEOUT: { + sendStatus = SendStatus.FLUSH_SLAVE_TIMEOUT; + break; + } case ResponseCode.SLAVE_NOT_AVAILABLE: { + sendStatus = SendStatus.SLAVE_NOT_AVAILABLE; + break; } case ResponseCode.SUCCESS: { - SendStatus sendStatus = SendStatus.SEND_OK; - switch (response.getCode()) { - case ResponseCode.FLUSH_DISK_TIMEOUT: - sendStatus = SendStatus.FLUSH_DISK_TIMEOUT; - break; - case ResponseCode.FLUSH_SLAVE_TIMEOUT: - sendStatus = SendStatus.FLUSH_SLAVE_TIMEOUT; - break; - case ResponseCode.SLAVE_NOT_AVAILABLE: - sendStatus = SendStatus.SLAVE_NOT_AVAILABLE; - break; - case ResponseCode.SUCCESS: - sendStatus = SendStatus.SEND_OK; - break; - default: - assert false; - break; - } + sendStatus = SendStatus.SEND_OK; + break; + } + default: { + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); + } + } - SendMessageResponseHeader responseHeader = - (SendMessageResponseHeader) response.decodeCommandCustomHeader(SendMessageResponseHeader.class); + SendMessageResponseHeader responseHeader = + (SendMessageResponseHeader) response.decodeCommandCustomHeader(SendMessageResponseHeader.class); - MessageQueue messageQueue = new MessageQueue(msg.getTopic(), brokerName, responseHeader.getQueueId()); + //If namespace not null , reset Topic without namespace. + String topic = msg.getTopic(); + if (StringUtils.isNotEmpty(this.clientConfig.getNamespace())) { + topic = NamespaceUtil.withoutNamespace(topic, this.clientConfig.getNamespace()); + } - String uniqMsgId = MessageClientIDSetter.getUniqID(msg); - if (msg instanceof MessageBatch) { - StringBuilder sb = new StringBuilder(); - for (Message message : (MessageBatch) msg) { - sb.append(sb.length() == 0 ? "" : ",").append(MessageClientIDSetter.getUniqID(message)); - } - uniqMsgId = sb.toString(); - } - SendResult sendResult = new SendResult(sendStatus, - uniqMsgId, - responseHeader.getMsgId(), messageQueue, responseHeader.getQueueOffset()); - sendResult.setTransactionId(responseHeader.getTransactionId()); - String regionId = response.getExtFields().get(MessageConst.PROPERTY_MSG_REGION); - String traceOn = response.getExtFields().get(MessageConst.PROPERTY_TRACE_SWITCH); - if (regionId == null || regionId.isEmpty()) { - regionId = MixAll.DEFAULT_TRACE_REGION_ID; - } - if (traceOn != null && traceOn.equals("false")) { - sendResult.setTraceOn(false); - } else { - sendResult.setTraceOn(true); - } - sendResult.setRegionId(regionId); - return sendResult; + MessageQueue messageQueue = new MessageQueue(topic, brokerName, responseHeader.getQueueId()); + + String uniqMsgId = MessageClientIDSetter.getUniqID(msg); + if (msg instanceof MessageBatch && responseHeader.getBatchUniqId() == null) { + // This means it is not an inner batch + StringBuilder sb = new StringBuilder(); + for (Message message : (MessageBatch) msg) { + sb.append(sb.length() == 0 ? "" : ",").append(MessageClientIDSetter.getUniqID(message)); } - default: - break; + uniqMsgId = sb.toString(); } - - throw new MQBrokerException(response.getCode(), response.getRemark()); + SendResult sendResult = new SendResult(sendStatus, + uniqMsgId, + responseHeader.getMsgId(), messageQueue, responseHeader.getQueueOffset()); + sendResult.setTransactionId(responseHeader.getTransactionId()); + String regionId = response.getExtFields().get(MessageConst.PROPERTY_MSG_REGION); + if (regionId == null || regionId.isEmpty()) { + regionId = MixAll.DEFAULT_TRACE_REGION_ID; + } + sendResult.setRegionId(regionId); + String traceOn = response.getExtFields().get(MessageConst.PROPERTY_TRACE_SWITCH); + sendResult.setTraceOn(!Boolean.FALSE.toString().equals(traceOn)); + return sendResult; } public PullResult pullMessage( @@ -558,7 +816,12 @@ public PullResult pullMessage( final CommunicationMode communicationMode, final PullCallback pullCallback ) throws RemotingException, MQBrokerException, InterruptedException { - RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, requestHeader); + RemotingCommand request; + if (PullSysFlag.hasLitePullFlag(requestHeader.getSysFlag())) { + request = RemotingCommand.createRequestCommand(RequestCode.LITE_PULL_MESSAGE, requestHeader); + } else { + request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, requestHeader); + } switch (communicationMode) { case ONEWAY: @@ -577,6 +840,174 @@ public PullResult pullMessage( return null; } + public void popMessageAsync( + final String brokerName, final String addr, final PopMessageRequestHeader requestHeader, + final long timeoutMillis, final PopCallback popCallback + ) throws RemotingException, InterruptedException { + final RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.POP_MESSAGE, requestHeader); + this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { + + } + + @Override + public void operationSucceed(RemotingCommand response) { + try { + PopResult popResult = MQClientAPIImpl.this.processPopResponse(brokerName, response, requestHeader.getTopic(), requestHeader); + popCallback.onSuccess(popResult); + } catch (Exception e) { + popCallback.onException(e); + } + } + @Override + public void operationFail(Throwable throwable) { + popCallback.onException(throwable); + } + }); + } + + public void ackMessageAsync( + final String addr, + final long timeOut, + final AckCallback ackCallback, + final AckMessageRequestHeader requestHeader + ) throws RemotingException, MQBrokerException, InterruptedException { + ackMessageAsync(addr, timeOut, ackCallback, requestHeader, null); + } + + public void batchAckMessageAsync( + final String addr, + final long timeOut, + final AckCallback ackCallback, + final String topic, + final String consumerGroup, + final List extraInfoList + ) throws RemotingException, MQBrokerException, InterruptedException { + String brokerName = null; + Map batchAckMap = new HashMap<>(); + for (String extraInfo : extraInfoList) { + String[] extraInfoData = ExtraInfoUtil.split(extraInfo); + if (brokerName == null) { + brokerName = ExtraInfoUtil.getBrokerName(extraInfoData); + } + String mergeKey = ExtraInfoUtil.getRetry(extraInfoData) + "@" + + ExtraInfoUtil.getQueueId(extraInfoData) + "@" + + ExtraInfoUtil.getCkQueueOffset(extraInfoData) + "@" + + ExtraInfoUtil.getPopTime(extraInfoData); + BatchAck bAck = batchAckMap.computeIfAbsent(mergeKey, k -> { + BatchAck newBatchAck = new BatchAck(); + newBatchAck.setConsumerGroup(consumerGroup); + newBatchAck.setTopic(topic); + newBatchAck.setRetry(ExtraInfoUtil.getRetry(extraInfoData)); + newBatchAck.setStartOffset(ExtraInfoUtil.getCkQueueOffset(extraInfoData)); + newBatchAck.setQueueId(ExtraInfoUtil.getQueueId(extraInfoData)); + newBatchAck.setReviveQueueId(ExtraInfoUtil.getReviveQid(extraInfoData)); + newBatchAck.setPopTime(ExtraInfoUtil.getPopTime(extraInfoData)); + newBatchAck.setInvisibleTime(ExtraInfoUtil.getInvisibleTime(extraInfoData)); + newBatchAck.setBitSet(new BitSet()); + return newBatchAck; + }); + bAck.getBitSet().set((int) (ExtraInfoUtil.getQueueOffset(extraInfoData) - ExtraInfoUtil.getCkQueueOffset(extraInfoData))); + } + + BatchAckMessageRequestBody requestBody = new BatchAckMessageRequestBody(); + requestBody.setBrokerName(brokerName); + requestBody.setAcks(new ArrayList<>(batchAckMap.values())); + batchAckMessageAsync(addr, timeOut, ackCallback, requestBody); + } + + public void batchAckMessageAsync( + final String addr, + final long timeOut, + final AckCallback ackCallback, + final BatchAckMessageRequestBody requestBody + ) throws RemotingException, MQBrokerException, InterruptedException { + ackMessageAsync(addr, timeOut, ackCallback, null, requestBody); + } + + protected void ackMessageAsync( + final String addr, + final long timeOut, + final AckCallback ackCallback, + final AckMessageRequestHeader requestHeader, + final BatchAckMessageRequestBody requestBody + ) throws RemotingException, MQBrokerException, InterruptedException { + RemotingCommand request; + if (requestHeader != null) { + request = RemotingCommand.createRequestCommand(RequestCode.ACK_MESSAGE, requestHeader); + } else { + request = RemotingCommand.createRequestCommand(RequestCode.BATCH_ACK_MESSAGE, null); + if (requestBody != null) { + request.setBody(requestBody.encode()); + } + } + this.remotingClient.invokeAsync(addr, request, timeOut, new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { + + } + + @Override + public void operationSucceed(RemotingCommand response) { + AckResult ackResult = new AckResult(); + if (ResponseCode.SUCCESS == response.getCode()) { + ackResult.setStatus(AckStatus.OK); + } else { + ackResult.setStatus(AckStatus.NO_EXIST); + } + ackCallback.onSuccess(ackResult); + } + + @Override + public void operationFail(Throwable throwable) { + ackCallback.onException(throwable); + } + }); + } + + public void changeInvisibleTimeAsync(// + final String brokerName, + final String addr, // + final ChangeInvisibleTimeRequestHeader requestHeader,// + final long timeoutMillis, + final AckCallback ackCallback + ) throws RemotingException, MQBrokerException, InterruptedException { + final RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CHANGE_MESSAGE_INVISIBLETIME, requestHeader); + this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { + + } + + @Override + public void operationSucceed(RemotingCommand response) { + try { + ChangeInvisibleTimeResponseHeader responseHeader = (ChangeInvisibleTimeResponseHeader) response.decodeCommandCustomHeader(ChangeInvisibleTimeResponseHeader.class); + AckResult ackResult = new AckResult(); + if (ResponseCode.SUCCESS == response.getCode()) { + ackResult.setStatus(AckStatus.OK); + ackResult.setPopTime(responseHeader.getPopTime()); + ackResult.setExtraInfo(ExtraInfoUtil + .buildExtraInfo(requestHeader.getOffset(), responseHeader.getPopTime(), responseHeader.getInvisibleTime(), + responseHeader.getReviveQid(), requestHeader.getTopic(), brokerName, requestHeader.getQueueId()) + MessageConst.KEY_SEPARATOR + + requestHeader.getOffset()); + } else { + ackResult.setStatus(AckStatus.NO_EXIST); + } + ackCallback.onSuccess(ackResult); + } catch (Exception e) { + ackCallback.onException(e); + } + } + + @Override + public void operationFail(Throwable throwable) { + ackCallback.onException(throwable); + } + }); + } + private void pullMessageAsync( final String addr, final RemotingCommand request, @@ -586,26 +1017,23 @@ private void pullMessageAsync( this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() { @Override public void operationComplete(ResponseFuture responseFuture) { - RemotingCommand response = responseFuture.getResponseCommand(); - if (response != null) { - try { - PullResult pullResult = MQClientAPIImpl.this.processPullResponse(response); - assert pullResult != null; - pullCallback.onSuccess(pullResult); - } catch (Exception e) { - pullCallback.onException(e); - } - } else { - if (!responseFuture.isSendRequestOK()) { - pullCallback.onException(new MQClientException("send request failed to " + addr + ". Request: " + request, responseFuture.getCause())); - } else if (responseFuture.isTimeout()) { - pullCallback.onException(new MQClientException("wait response from " + addr + " timeout :" + responseFuture.getTimeoutMillis() + "ms" + ". Request: " + request, - responseFuture.getCause())); - } else { - pullCallback.onException(new MQClientException("unknown reason. addr: " + addr + ", timeoutMillis: " + timeoutMillis + ". Request: " + request, responseFuture.getCause())); - } + + } + + @Override + public void operationSucceed(RemotingCommand response) { + try { + PullResult pullResult = MQClientAPIImpl.this.processPullResponse(response, addr); + pullCallback.onSuccess(pullResult); + } catch (Exception e) { + pullCallback.onException(e); } } + + @Override + public void operationFail(Throwable throwable) { + pullCallback.onException(throwable); + } }); } @@ -616,11 +1044,12 @@ private PullResult pullMessageSync( ) throws RemotingException, InterruptedException, MQBrokerException { RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis); assert response != null; - return this.processPullResponse(response); + return this.processPullResponse(response, addr); } private PullResult processPullResponse( - final RemotingCommand response) throws MQBrokerException, RemotingCommandException { + final RemotingCommand response, + final String addr) throws MQBrokerException, RemotingCommandException { PullStatus pullStatus = PullStatus.NO_NEW_MSG; switch (response.getCode()) { case ResponseCode.SUCCESS: @@ -637,14 +1066,165 @@ private PullResult processPullResponse( break; default: - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); } PullMessageResponseHeader responseHeader = (PullMessageResponseHeader) response.decodeCommandCustomHeader(PullMessageResponseHeader.class); return new PullResultExt(pullStatus, responseHeader.getNextBeginOffset(), responseHeader.getMinOffset(), - responseHeader.getMaxOffset(), null, responseHeader.getSuggestWhichBrokerId(), response.getBody()); + responseHeader.getMaxOffset(), null, responseHeader.getSuggestWhichBrokerId(), response.getBody(), responseHeader.getOffsetDelta()); + } + + private PopResult processPopResponse(final String brokerName, final RemotingCommand response, String topic, + CommandCustomHeader requestHeader) throws MQBrokerException, RemotingCommandException { + PopStatus popStatus = PopStatus.NO_NEW_MSG; + List msgFoundList = null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: + popStatus = PopStatus.FOUND; + ByteBuffer byteBuffer = ByteBuffer.wrap(response.getBody()); + msgFoundList = MessageDecoder.decodesBatch( + byteBuffer, + clientConfig.isDecodeReadBody(), + clientConfig.isDecodeDecompressBody(), + true); + break; + case ResponseCode.POLLING_FULL: + popStatus = PopStatus.POLLING_FULL; + break; + case ResponseCode.POLLING_TIMEOUT: + popStatus = PopStatus.POLLING_NOT_FOUND; + break; + case ResponseCode.PULL_NOT_FOUND: + popStatus = PopStatus.POLLING_NOT_FOUND; + break; + default: + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + PopResult popResult = new PopResult(popStatus, msgFoundList); + PopMessageResponseHeader responseHeader = (PopMessageResponseHeader) response.decodeCommandCustomHeader(PopMessageResponseHeader.class); + popResult.setRestNum(responseHeader.getRestNum()); + if (popStatus != PopStatus.FOUND) { + return popResult; + } + // it is a pop command if pop time greater than 0, we should set the check point info to extraInfo field + Map startOffsetInfo = null; + Map> msgOffsetInfo = null; + Map orderCountInfo = null; + if (requestHeader instanceof PopMessageRequestHeader) { + popResult.setInvisibleTime(responseHeader.getInvisibleTime()); + popResult.setPopTime(responseHeader.getPopTime()); + startOffsetInfo = ExtraInfoUtil.parseStartOffsetInfo(responseHeader.getStartOffsetInfo()); + msgOffsetInfo = ExtraInfoUtil.parseMsgOffsetInfo(responseHeader.getMsgOffsetInfo()); + orderCountInfo = ExtraInfoUtil.parseOrderCountInfo(responseHeader.getOrderCountInfo()); + } + Map/*msg queueOffset*/> sortMap + = buildQueueOffsetSortedMap(topic, msgFoundList); + Map map = new HashMap<>(5); + for (MessageExt messageExt : msgFoundList) { + if (requestHeader instanceof PopMessageRequestHeader) { + if (startOffsetInfo == null) { + // we should set the check point info to extraInfo field , if the command is popMsg + // find pop ck offset + String key = messageExt.getTopic() + messageExt.getQueueId(); + if (!map.containsKey(messageExt.getTopic() + messageExt.getQueueId())) { + map.put(key, ExtraInfoUtil.buildExtraInfo(messageExt.getQueueOffset(), responseHeader.getPopTime(), responseHeader.getInvisibleTime(), responseHeader.getReviveQid(), + messageExt.getTopic(), brokerName, messageExt.getQueueId())); + + } + messageExt.getProperties().put(MessageConst.PROPERTY_POP_CK, map.get(key) + MessageConst.KEY_SEPARATOR + messageExt.getQueueOffset()); + } else { + if (messageExt.getProperty(MessageConst.PROPERTY_POP_CK) == null) { + final String queueIdKey; + final String queueOffsetKey; + final int index; + final Long msgQueueOffset; + if (MixAll.isLmq(topic) && messageExt.getReconsumeTimes() == 0 && StringUtils.isNotEmpty( + messageExt.getProperty(MessageConst.PROPERTY_INNER_MULTI_DISPATCH))) { + // process LMQ, LMQ topic has only 1 queue, which queue id is 0 + queueIdKey = ExtraInfoUtil.getStartOffsetInfoMapKey(topic, MixAll.LMQ_QUEUE_ID); + queueOffsetKey = ExtraInfoUtil.getQueueOffsetMapKey(topic, MixAll.LMQ_QUEUE_ID, Long.parseLong( + messageExt.getProperty(MessageConst.PROPERTY_INNER_MULTI_QUEUE_OFFSET))); + index = sortMap.get(queueIdKey).indexOf( + Long.parseLong(messageExt.getProperty(MessageConst.PROPERTY_INNER_MULTI_QUEUE_OFFSET))); + msgQueueOffset = msgOffsetInfo.get(queueIdKey).get(index); + if (msgQueueOffset != Long.parseLong( + messageExt.getProperty(MessageConst.PROPERTY_INNER_MULTI_QUEUE_OFFSET))) { + log.warn("Queue offset[%d] of msg is strange, not equal to the stored in msg, %s", + msgQueueOffset, messageExt); + } + messageExt.getProperties().put(MessageConst.PROPERTY_POP_CK, + ExtraInfoUtil.buildExtraInfo(startOffsetInfo.get(queueIdKey), responseHeader.getPopTime(), responseHeader.getInvisibleTime(), + responseHeader.getReviveQid(), topic, brokerName, 0, msgQueueOffset) + ); + } else { + queueIdKey = ExtraInfoUtil.getStartOffsetInfoMapKey(messageExt.getTopic(), messageExt.getQueueId()); + queueOffsetKey = ExtraInfoUtil.getQueueOffsetMapKey(messageExt.getTopic(), messageExt.getQueueId(), messageExt.getQueueOffset()); + index = sortMap.get(queueIdKey).indexOf(messageExt.getQueueOffset()); + msgQueueOffset = msgOffsetInfo.get(queueIdKey).get(index); + if (msgQueueOffset != messageExt.getQueueOffset()) { + log.warn("Queue offset[%d] of msg is strange, not equal to the stored in msg, %s", msgQueueOffset, messageExt); + } + messageExt.getProperties().put(MessageConst.PROPERTY_POP_CK, + ExtraInfoUtil.buildExtraInfo(startOffsetInfo.get(queueIdKey), responseHeader.getPopTime(), responseHeader.getInvisibleTime(), + responseHeader.getReviveQid(), messageExt.getTopic(), brokerName, messageExt.getQueueId(), msgQueueOffset) + ); + } + if (((PopMessageRequestHeader) requestHeader).isOrder() && orderCountInfo != null) { + Integer count = orderCountInfo.get(queueOffsetKey); + if (count == null) { + count = orderCountInfo.get(queueIdKey); + } + if (count != null && count > 0) { + messageExt.setReconsumeTimes(count); + } + } + } + } + messageExt.getProperties().computeIfAbsent( + MessageConst.PROPERTY_FIRST_POP_TIME, k -> String.valueOf(responseHeader.getPopTime())); + } + messageExt.setBrokerName(brokerName); + messageExt.setTopic(NamespaceUtil.withoutNamespace(topic, this.clientConfig.getNamespace())); + } + return popResult; + } + + /** + * Build queue offset sorted map + * + * @param topic pop consumer topic + * @param msgFoundList popped message list + * @return sorted map, key is topicMark@queueId, value is sorted msg queueOffset list + */ + private static Map> buildQueueOffsetSortedMap(String topic, List msgFoundList) { + Map/*msg queueOffset*/> sortMap = new HashMap<>(16); + for (MessageExt messageExt : msgFoundList) { + final String key; + if (MixAll.isLmq(topic) && messageExt.getReconsumeTimes() == 0 + && StringUtils.isNotEmpty(messageExt.getProperty(MessageConst.PROPERTY_INNER_MULTI_DISPATCH))) { + // process LMQ, LMQ topic has only 1 queue, which queue id is 0 + key = ExtraInfoUtil.getStartOffsetInfoMapKey( + messageExt.getProperty(MessageConst.PROPERTY_INNER_MULTI_DISPATCH), 0); + if (!sortMap.containsKey(key)) { + sortMap.put(key, new ArrayList<>(4)); + } + sortMap.get(key).add( + Long.parseLong(messageExt.getProperty(MessageConst.PROPERTY_INNER_MULTI_QUEUE_OFFSET))); + continue; + } + // Value of POP_CK is used to determine whether it is a pop retry, + // cause topic could be rewritten by broker. + key = ExtraInfoUtil.getStartOffsetInfoMapKey(messageExt.getTopic(), + messageExt.getProperty(MessageConst.PROPERTY_POP_CK), messageExt.getQueueId()); + if (!sortMap.containsKey(key)) { + sortMap.put(key, new ArrayList<>(4)); + } + sortMap.get(key).add(messageExt.getQueueOffset()); + } + return sortMap; } public MessageExt viewMessage(final String addr, final long phyoffset, final long timeoutMillis) @@ -660,15 +1240,20 @@ public MessageExt viewMessage(final String addr, final long phyoffset, final lon case ResponseCode.SUCCESS: { ByteBuffer byteBuffer = ByteBuffer.wrap(response.getBody()); MessageExt messageExt = MessageDecoder.clientDecode(byteBuffer, true); + //If namespace not null , reset Topic without namespace. + if (StringUtils.isNotEmpty(this.clientConfig.getNamespace())) { + messageExt.setTopic(NamespaceUtil.withoutNamespace(messageExt.getTopic(), this.clientConfig.getNamespace())); + } return messageExt; } default: break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); } + @Deprecated public long searchOffset(final String addr, final String topic, final int queueId, final long timestamp, final long timeoutMillis) throws RemotingException, MQBrokerException, InterruptedException { @@ -691,14 +1276,48 @@ public long searchOffset(final String addr, final String topic, final int queueI break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); } - public long getMaxOffset(final String addr, final String topic, final int queueId, final long timeoutMillis) + public long searchOffset(final String addr, final MessageQueue messageQueue, final long timestamp, + final long timeoutMillis) + throws RemotingException, MQBrokerException, InterruptedException { + // default return lower boundary offset when there are more than one offsets. + return searchOffset(addr, messageQueue, timestamp, BoundaryType.LOWER, timeoutMillis); + } + + public long searchOffset(final String addr, final MessageQueue messageQueue, final long timestamp, + final BoundaryType boundaryType, final long timeoutMillis) + throws RemotingException, MQBrokerException, InterruptedException { + SearchOffsetRequestHeader requestHeader = new SearchOffsetRequestHeader(); + requestHeader.setTopic(messageQueue.getTopic()); + requestHeader.setQueueId(messageQueue.getQueueId()); + requestHeader.setBrokerName(messageQueue.getBrokerName()); + requestHeader.setTimestamp(timestamp); + requestHeader.setBoundaryType(boundaryType); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.SEARCH_OFFSET_BY_TIMESTAMP, requestHeader); + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), + request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + SearchOffsetResponseHeader responseHeader = + (SearchOffsetResponseHeader) response.decodeCommandCustomHeader(SearchOffsetResponseHeader.class); + return responseHeader.getOffset(); + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); + } + + public long getMaxOffset(final String addr, final MessageQueue messageQueue, final long timeoutMillis) throws RemotingException, MQBrokerException, InterruptedException { GetMaxOffsetRequestHeader requestHeader = new GetMaxOffsetRequestHeader(); - requestHeader.setTopic(topic); - requestHeader.setQueueId(queueId); + requestHeader.setTopic(messageQueue.getTopic()); + requestHeader.setQueueId(messageQueue.getQueueId()); + requestHeader.setBrokerName(messageQueue.getBrokerName()); RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_MAX_OFFSET, requestHeader); RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), @@ -715,7 +1334,7 @@ public long getMaxOffset(final String addr, final String topic, final int queueI break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); } public List getConsumerIdListByGroup( @@ -742,14 +1361,15 @@ public List getConsumerIdListByGroup( break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); } - public long getMinOffset(final String addr, final String topic, final int queueId, final long timeoutMillis) + public long getMinOffset(final String addr, final MessageQueue messageQueue, final long timeoutMillis) throws RemotingException, MQBrokerException, InterruptedException { GetMinOffsetRequestHeader requestHeader = new GetMinOffsetRequestHeader(); - requestHeader.setTopic(topic); - requestHeader.setQueueId(queueId); + requestHeader.setTopic(messageQueue.getTopic()); + requestHeader.setQueueId(messageQueue.getQueueId()); + requestHeader.setBrokerName(messageQueue.getBrokerName()); RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_MIN_OFFSET, requestHeader); RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), @@ -766,15 +1386,15 @@ public long getMinOffset(final String addr, final String topic, final int queueI break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); } - public long getEarliestMsgStoretime(final String addr, final String topic, final int queueId, - final long timeoutMillis) + public long getEarliestMsgStoretime(final String addr, final MessageQueue mq, final long timeoutMillis) throws RemotingException, MQBrokerException, InterruptedException { GetEarliestMsgStoretimeRequestHeader requestHeader = new GetEarliestMsgStoretimeRequestHeader(); - requestHeader.setTopic(topic); - requestHeader.setQueueId(queueId); + requestHeader.setTopic(mq.getTopic()); + requestHeader.setQueueId(mq.getQueueId()); + requestHeader.setBrokerName(mq.getBrokerName()); RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_EARLIEST_MSG_STORETIME, requestHeader); RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), @@ -791,7 +1411,7 @@ public long getEarliestMsgStoretime(final String addr, final String topic, final break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); } public long queryConsumerOffset( @@ -808,14 +1428,16 @@ public long queryConsumerOffset( case ResponseCode.SUCCESS: { QueryConsumerOffsetResponseHeader responseHeader = (QueryConsumerOffsetResponseHeader) response.decodeCommandCustomHeader(QueryConsumerOffsetResponseHeader.class); - return responseHeader.getOffset(); } + case ResponseCode.QUERY_NOT_FOUND: { + throw new OffsetNotFoundException(response.getCode(), response.getRemark(), addr); + } default: break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); } public void updateConsumerOffset( @@ -836,7 +1458,7 @@ public void updateConsumerOffset( break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); } public void updateConsumerOffsetOneway( @@ -850,13 +1472,13 @@ public void updateConsumerOffsetOneway( this.remotingClient.invokeOneway(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), request, timeoutMillis); } - public int sendHearbeat( + public int sendHeartbeat( final String addr, final HeartbeatData heartbeatData, final long timeoutMillis ) throws RemotingException, MQBrokerException, InterruptedException { - RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.HEART_BEAT, null); - + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.HEART_BEAT, new HeartbeatRequestHeader()); + request.setLanguage(clientConfig.getLanguage()); request.setBody(heartbeatData.encode()); RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis); assert response != null; @@ -868,6 +1490,30 @@ public int sendHearbeat( break; } + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); + } + + public HeartbeatV2Result sendHeartbeatV2( + final String addr, + final HeartbeatData heartbeatData, + final long timeoutMillis + ) throws RemotingException, MQBrokerException, InterruptedException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.HEART_BEAT, new HeartbeatRequestHeader()); + request.setLanguage(clientConfig.getLanguage()); + request.setBody(heartbeatData.encode()); + RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + if (response.getExtFields() != null) { + return new HeartbeatV2Result(response.getVersion(), Boolean.parseBoolean(response.getExtFields().get(MixAll.IS_SUB_CHANGE)), Boolean.parseBoolean(response.getExtFields().get(MixAll.IS_SUPPORT_HEART_BEAT_V2))); + } + return new HeartbeatV2Result(response.getVersion(), false, false); + } + default: + break; + } + throw new MQBrokerException(response.getCode(), response.getRemark()); } @@ -894,7 +1540,7 @@ public void unregisterClient( break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); } public void endTransactionOneway( @@ -902,7 +1548,7 @@ public void endTransactionOneway( final EndTransactionRequestHeader requestHeader, final String remark, final long timeoutMillis - ) throws RemotingException, MQBrokerException, InterruptedException { + ) throws RemotingException, InterruptedException { RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.END_TRANSACTION, requestHeader); request.setRemark(remark); @@ -924,7 +1570,7 @@ public void queryMessage( public boolean registerClient(final String addr, final HeartbeatData heartbeat, final long timeoutMillis) throws RemotingException, InterruptedException { - RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.HEART_BEAT, null); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.HEART_BEAT, new HeartbeatRequestHeader()); request.setBody(heartbeat.encode()); RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis); @@ -933,6 +1579,7 @@ public boolean registerClient(final String addr, final HeartbeatData heartbeat, public void consumerSendMessageBack( final String addr, + final String brokerName, final MessageExt msg, final String consumerGroup, final int delayLevel, @@ -948,6 +1595,7 @@ public void consumerSendMessageBack( requestHeader.setDelayLevel(delayLevel); requestHeader.setOriginMsgId(msg.getMsgId()); requestHeader.setMaxReconsumeTimes(maxConsumeRetryTimes); + requestHeader.setBrokerName(brokerName); RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), request, timeoutMillis); @@ -960,14 +1608,14 @@ public void consumerSendMessageBack( break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); } public Set lockBatchMQ( final String addr, final LockBatchRequestBody requestBody, final long timeoutMillis) throws RemotingException, MQBrokerException, InterruptedException { - RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.LOCK_BATCH_MQ, null); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.LOCK_BATCH_MQ, new LockBatchMqRequestHeader()); request.setBody(requestBody.encode()); RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), @@ -982,7 +1630,7 @@ public Set lockBatchMQ( break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); } public void unlockBatchMQ( @@ -991,7 +1639,7 @@ public void unlockBatchMQ( final long timeoutMillis, final boolean oneway ) throws RemotingException, MQBrokerException, InterruptedException { - RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UNLOCK_BATCH_MQ, null); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UNLOCK_BATCH_MQ, new UnlockBatchMqRequestHeader()); request.setBody(requestBody.encode()); @@ -1008,7 +1656,7 @@ public void unlockBatchMQ( break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); } } @@ -1031,7 +1679,7 @@ public TopicStatsTable getTopicStatsInfo(final String addr, final String topic, break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); } public ConsumeStats getConsumeStats(final String addr, final String consumerGroup, final long timeoutMillis) @@ -1061,7 +1709,7 @@ public ConsumeStats getConsumeStats(final String addr, final String consumerGrou break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); } public ProducerConnection getProducerConnectionList(final String addr, final String producerGroup, @@ -1083,7 +1731,27 @@ public ProducerConnection getProducerConnectionList(final String addr, final Str break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); + } + + public ProducerTableInfo getAllProducerInfo(final String addr, final long timeoutMillis) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, + MQBrokerException { + GetAllProducerInfoRequestHeader requestHeader = new GetAllProducerInfoRequestHeader(); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ALL_PRODUCER_INFO, requestHeader); + + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), + request, timeoutMillis); + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return ProducerTableInfo.decode(response.getBody(), ProducerTableInfo.class); + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); } public ConsumerConnection getConsumerConnectionList(final String addr, final String consumerGroup, @@ -1105,7 +1773,7 @@ public ConsumerConnection getConsumerConnectionList(final String addr, final Str break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); } public KVTable getBrokerRuntimeInfo(final String addr, final long timeoutMillis) throws RemotingConnectException, @@ -1123,12 +1791,54 @@ public KVTable getBrokerRuntimeInfo(final String addr, final long timeoutMillis) break; } + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); + } + + public void addBroker(final String addr, final String brokerConfigPath, final long timeoutMillis) + throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQBrokerException { + AddBrokerRequestHeader requestHeader = new AddBrokerRequestHeader(); + requestHeader.setConfigPath(brokerConfigPath); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.ADD_BROKER, requestHeader); + RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis); + assert response != null; + + switch (response.getCode()) { + case ResponseCode.SUCCESS: + return; + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public void removeBroker(final String addr, String clusterName, String brokerName, long brokerId, + final long timeoutMillis) + throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQBrokerException { + RemoveBrokerRequestHeader requestHeader = new RemoveBrokerRequestHeader(); + requestHeader.setBrokerClusterName(clusterName); + requestHeader.setBrokerName(brokerName); + requestHeader.setBrokerId(brokerId); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.REMOVE_BROKER, requestHeader); + RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis); + assert response != null; + + switch (response.getCode()) { + case ResponseCode.SUCCESS: + return; + default: + break; + } + throw new MQBrokerException(response.getCode(), response.getRemark()); } public void updateBrokerConfig(final String addr, final Properties properties, final long timeoutMillis) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, - MQBrokerException, UnsupportedEncodingException { + MQBrokerException, MQClientException, UnsupportedEncodingException { + Validators.checkBrokerConfig(properties); RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_BROKER_CONFIG, null); @@ -1145,7 +1855,7 @@ public void updateBrokerConfig(final String addr, final Properties properties, f break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); } } @@ -1164,6 +1874,82 @@ public Properties getBrokerConfig(final String addr, final long timeoutMillis) break; } + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); + } + + public void updateColdDataFlowCtrGroupConfig(final String addr, final Properties properties, final long timeoutMillis) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException, UnsupportedEncodingException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_COLD_DATA_FLOW_CTR_CONFIG, null); + String str = MixAll.properties2String(properties); + if (str != null && str.length() > 0) { + request.setBody(str.getBytes(MixAll.DEFAULT_CHARSET)); + RemotingCommand response = this.remotingClient.invokeSync( + MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), request, timeoutMillis); + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return; + } + default: + break; + } + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + } + + public void removeColdDataFlowCtrGroupConfig(final String addr, final String consumerGroup, final long timeoutMillis) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException, UnsupportedEncodingException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.REMOVE_COLD_DATA_FLOW_CTR_CONFIG, null); + if (consumerGroup != null && consumerGroup.length() > 0) { + request.setBody(consumerGroup.getBytes(MixAll.DEFAULT_CHARSET)); + RemotingCommand response = this.remotingClient.invokeSync( + MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), request, timeoutMillis); + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return; + } + default: + break; + } + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + } + + public String getColdDataFlowCtrInfo(final String addr, final long timeoutMillis) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException, UnsupportedEncodingException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_COLD_DATA_FLOW_CTR_INFO, null); + RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + if (null != response.getBody() && response.getBody().length > 0) { + return new String(response.getBody(), MixAll.DEFAULT_CHARSET); + } + return null; + } + default: + break; + } + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public String setCommitLogReadAheadMode(final String addr, final String mode, final long timeoutMillis) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.SET_COMMITLOG_READ_MODE, null); + HashMap extFields = new HashMap<>(); + extFields.put(FIleReadaheadMode.READ_AHEAD_MODE, mode); + request.setExtFields(extFields); + RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + if (null != response.getRemark() && response.getRemark().length() > 0) { + return response.getRemark(); + } + return null; + } + default: + break; + } throw new MQBrokerException(response.getCode(), response.getRemark()); } @@ -1185,15 +1971,14 @@ public ClusterInfo getBrokerClusterInfo( throw new MQBrokerException(response.getCode(), response.getRemark()); } - public TopicRouteData getDefaultTopicRouteInfoFromNameServer(final String topic, final long timeoutMillis) + public TopicRouteData getDefaultTopicRouteInfoFromNameServer(final long timeoutMillis) throws RemotingException, MQClientException, InterruptedException { - return getTopicRouteInfoFromNameServer(topic, timeoutMillis, false); + return getTopicRouteInfoFromNameServer(TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC, timeoutMillis, false); } public TopicRouteData getTopicRouteInfoFromNameServer(final String topic, final long timeoutMillis) throws RemotingException, MQClientException, InterruptedException { - return getTopicRouteInfoFromNameServer(topic, timeoutMillis, true); } @@ -1201,14 +1986,13 @@ public TopicRouteData getTopicRouteInfoFromNameServer(final String topic, final boolean allowTopicNotExist) throws MQClientException, InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException { GetRouteInfoRequestHeader requestHeader = new GetRouteInfoRequestHeader(); requestHeader.setTopic(topic); - - RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ROUTEINTO_BY_TOPIC, requestHeader); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ROUTEINFO_BY_TOPIC, requestHeader); RemotingCommand response = this.remotingClient.invokeSync(null, request, timeoutMillis); assert response != null; switch (response.getCode()) { case ResponseCode.TOPIC_NOT_EXIST: { - if (allowTopicNotExist && !topic.equals(MixAll.DEFAULT_TOPIC)) { + if (allowTopicNotExist) { log.warn("get Topic [{}] RouteInfoFromNameServer is not exist value", topic); } @@ -1230,7 +2014,6 @@ public TopicRouteData getTopicRouteInfoFromNameServer(final String topic, final public TopicList getTopicListFromNameServer(final long timeoutMillis) throws RemotingException, MQClientException, InterruptedException { RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ALL_TOPIC_LIST_FROM_NAMESERVER, null); - RemotingCommand response = this.remotingClient.invokeSync(null, request, timeoutMillis); assert response != null; switch (response.getCode()) { @@ -1254,7 +2037,6 @@ public int wipeWritePermOfBroker(final String namesrvAddr, String brokerName, requestHeader.setBrokerName(brokerName); RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.WIPE_WRITE_PERM_OF_BROKER, requestHeader); - RemotingCommand response = this.remotingClient.invokeSync(namesrvAddr, request, timeoutMillis); assert response != null; switch (response.getCode()) { @@ -1270,12 +2052,33 @@ public int wipeWritePermOfBroker(final String namesrvAddr, String brokerName, throw new MQClientException(response.getCode(), response.getRemark()); } + public int addWritePermOfBroker(final String nameSrvAddr, String brokerName, final long timeoutMillis) + throws RemotingCommandException, + RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQClientException { + AddWritePermOfBrokerRequestHeader requestHeader = new AddWritePermOfBrokerRequestHeader(); + requestHeader.setBrokerName(brokerName); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.ADD_WRITE_PERM_OF_BROKER, requestHeader); + + RemotingCommand response = this.remotingClient.invokeSync(nameSrvAddr, request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + AddWritePermOfBrokerResponseHeader responseHeader = + (AddWritePermOfBrokerResponseHeader) response.decodeCommandCustomHeader(AddWritePermOfBrokerResponseHeader.class); + return responseHeader.getAddTopicCount(); + } + default: + break; + } + throw new MQClientException(response.getCode(), response.getRemark()); + } + public void deleteTopicInBroker(final String addr, final String topic, final long timeoutMillis) - throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + throws RemotingException, InterruptedException, MQClientException { DeleteTopicRequestHeader requestHeader = new DeleteTopicRequestHeader(); requestHeader.setTopic(topic); RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.DELETE_TOPIC_IN_BROKER, requestHeader); - RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), request, timeoutMillis); assert response != null; @@ -1291,11 +2094,10 @@ public void deleteTopicInBroker(final String addr, final String topic, final lon } public void deleteTopicInNameServer(final String addr, final String topic, final long timeoutMillis) - throws RemotingException, MQBrokerException, InterruptedException, MQClientException { - DeleteTopicRequestHeader requestHeader = new DeleteTopicRequestHeader(); + throws RemotingException, InterruptedException, MQClientException { + DeleteTopicFromNamesrvRequestHeader requestHeader = new DeleteTopicFromNamesrvRequestHeader(); requestHeader.setTopic(topic); RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.DELETE_TOPIC_IN_NAMESRV, requestHeader); - RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis); assert response != null; switch (response.getCode()) { @@ -1309,10 +2111,32 @@ public void deleteTopicInNameServer(final String addr, final String topic, final throw new MQClientException(response.getCode(), response.getRemark()); } - public void deleteSubscriptionGroup(final String addr, final String groupName, final long timeoutMillis) + public void deleteTopicInNameServer(final String addr, final String clusterName, final String topic, + final long timeoutMillis) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + DeleteTopicFromNamesrvRequestHeader requestHeader = new DeleteTopicFromNamesrvRequestHeader(); + requestHeader.setTopic(topic); + requestHeader.setClusterName(clusterName); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.DELETE_TOPIC_IN_NAMESRV, requestHeader); + RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return; + } + default: + break; + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + + public void deleteSubscriptionGroup(final String addr, final String groupName, final boolean removeOffset, + final long timeoutMillis) + throws RemotingException, InterruptedException, MQClientException { DeleteSubscriptionGroupRequestHeader requestHeader = new DeleteSubscriptionGroupRequestHeader(); requestHeader.setGroupName(groupName); + requestHeader.setCleanOffset(removeOffset); RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.DELETE_SUBSCRIPTIONGROUP, requestHeader); RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), @@ -1336,7 +2160,6 @@ public String getKVConfigValue(final String namespace, final String key, final l requestHeader.setKey(key); RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_KV_CONFIG, requestHeader); - RemotingCommand response = this.remotingClient.invokeSync(null, request, timeoutMillis); assert response != null; switch (response.getCode()) { @@ -1416,7 +2239,6 @@ public KVTable getKVListByNamespace(final String namespace, final long timeoutMi requestHeader.setNamespace(namespace); RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_KVLIST_BY_NAMESPACE, requestHeader); - RemotingCommand response = this.remotingClient.invokeSync(null, request, timeoutMillis); assert response != null; switch (response.getCode()) { @@ -1436,6 +2258,40 @@ public Map invokeBrokerToResetOffset(final String addr, fina return invokeBrokerToResetOffset(addr, topic, group, timestamp, isForce, timeoutMillis, false); } + public Map invokeBrokerToResetOffset(final String addr, final String topic, final String group, + final long timestamp, int queueId, Long offset, final long timeoutMillis) + throws RemotingException, MQClientException, InterruptedException { + + ResetOffsetRequestHeader requestHeader = new ResetOffsetRequestHeader(); + requestHeader.setTopic(topic); + requestHeader.setGroup(group); + requestHeader.setQueueId(queueId); + requestHeader.setTimestamp(timestamp); + requestHeader.setOffset(offset); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.INVOKE_BROKER_TO_RESET_OFFSET, + requestHeader); + + RemotingCommand response = remotingClient.invokeSync( + MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), request, timeoutMillis); + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + if (null != response.getBody()) { + return ResetOffsetBody.decode(response.getBody(), ResetOffsetBody.class).getOffsetTable(); + } + break; + } + case ResponseCode.TOPIC_NOT_EXIST: + case ResponseCode.SUBSCRIPTION_NOT_EXIST: + case ResponseCode.SYSTEM_ERROR: + log.warn("Invoke broker to reset offset error code={}, remark={}", + response.getCode(), response.getRemark()); + break; + default: + break; + } + throw new MQClientException(response.getCode(), response.getRemark()); + } + public Map invokeBrokerToResetOffset(final String addr, final String topic, final String group, final long timestamp, final boolean isForce, final long timeoutMillis, boolean isC) throws RemotingException, MQClientException, InterruptedException { @@ -1444,12 +2300,13 @@ public Map invokeBrokerToResetOffset(final String addr, fina requestHeader.setGroup(group); requestHeader.setTimestamp(timestamp); requestHeader.setForce(isForce); + // offset is -1 means offset is null + requestHeader.setOffset(-1L); RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.INVOKE_BROKER_TO_RESET_OFFSET, requestHeader); if (isC) { request.setLanguage(LanguageCode.CPP); } - RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), request, timeoutMillis); assert response != null; @@ -1477,7 +2334,6 @@ public Map> invokeBrokerToGetConsumerStatus(fina requestHeader.setClientAddr(clientAddr); RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.INVOKE_BROKER_TO_GET_CONSUMER_STATUS, requestHeader); - RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), request, timeoutMillis); assert response != null; @@ -1502,7 +2358,6 @@ public GroupList queryTopicConsumeByWho(final String addr, final String topic, f requestHeader.setTopic(topic); RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_TOPIC_CONSUME_BY_WHO, requestHeader); - RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), request, timeoutMillis); switch (response.getCode()) { @@ -1514,25 +2369,22 @@ public GroupList queryTopicConsumeByWho(final String addr, final String topic, f break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); } - public List queryConsumeTimeSpan(final String addr, final String topic, final String group, - final long timeoutMillis) + public TopicList queryTopicsByConsumer(final String addr, final String group, final long timeoutMillis) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException { - QueryConsumeTimeSpanRequestHeader requestHeader = new QueryConsumeTimeSpanRequestHeader(); - requestHeader.setTopic(topic); + QueryTopicsByConsumerRequestHeader requestHeader = new QueryTopicsByConsumerRequestHeader(); requestHeader.setGroup(group); - RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_CONSUME_TIME_SPAN, requestHeader); - + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_TOPICS_BY_CONSUMER, requestHeader); RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), request, timeoutMillis); switch (response.getCode()) { case ResponseCode.SUCCESS: { - QueryConsumeTimeSpanBody consumeTimeSpanBody = GroupList.decode(response.getBody(), QueryConsumeTimeSpanBody.class); - return consumeTimeSpanBody.getConsumeTimeSpanSet(); + TopicList topicList = TopicList.decode(response.getBody(), TopicList.class); + return topicList; } default: break; @@ -1541,61 +2393,78 @@ public List queryConsumeTimeSpan(final String addr, final String throw new MQBrokerException(response.getCode(), response.getRemark()); } - public TopicList getTopicsByCluster(final String cluster, final long timeoutMillis) - throws RemotingException, MQClientException, InterruptedException { - GetTopicsByClusterRequestHeader requestHeader = new GetTopicsByClusterRequestHeader(); - requestHeader.setCluster(cluster); - RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_TOPICS_BY_CLUSTER, requestHeader); + public SubscriptionData querySubscriptionByConsumer(final String addr, final String group, final String topic, + final long timeoutMillis) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, + MQBrokerException { + QuerySubscriptionByConsumerRequestHeader requestHeader = new QuerySubscriptionByConsumerRequestHeader(); + requestHeader.setGroup(group); + requestHeader.setTopic(topic); - RemotingCommand response = this.remotingClient.invokeSync(null, request, timeoutMillis); - assert response != null; + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_SUBSCRIPTION_BY_CONSUMER, requestHeader); + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), + request, timeoutMillis); switch (response.getCode()) { case ResponseCode.SUCCESS: { - byte[] body = response.getBody(); - if (body != null) { - TopicList topicList = TopicList.decode(body, TopicList.class); - return topicList; - } + QuerySubscriptionResponseBody subscriptionResponseBody = + QuerySubscriptionResponseBody.decode(response.getBody(), QuerySubscriptionResponseBody.class); + return subscriptionResponseBody.getSubscriptionData(); } default: break; } - throw new MQClientException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark()); } - public void registerMessageFilterClass(final String addr, - final String consumerGroup, - final String topic, - final String className, - final int classCRC, - final byte[] classBody, - final long timeoutMillis) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, - InterruptedException, MQBrokerException { - RegisterMessageFilterClassRequestHeader requestHeader = new RegisterMessageFilterClassRequestHeader(); - requestHeader.setConsumerGroup(consumerGroup); - requestHeader.setClassName(className); + public List queryConsumeTimeSpan(final String addr, final String topic, final String group, + final long timeoutMillis) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, + MQBrokerException { + QueryConsumeTimeSpanRequestHeader requestHeader = new QueryConsumeTimeSpanRequestHeader(); requestHeader.setTopic(topic); - requestHeader.setClassCRC(classCRC); + requestHeader.setGroup(group); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_CONSUME_TIME_SPAN, requestHeader); + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), + request, timeoutMillis); + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + QueryConsumeTimeSpanBody consumeTimeSpanBody = GroupList.decode(response.getBody(), QueryConsumeTimeSpanBody.class); + return consumeTimeSpanBody.getConsumeTimeSpanSet(); + } + default: + break; + } - RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.REGISTER_MESSAGE_FILTER_CLASS, requestHeader); - request.setBody(classBody); - RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis); + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); + } + + public TopicList getTopicsByCluster(final String cluster, final long timeoutMillis) + throws RemotingException, MQClientException, InterruptedException { + GetTopicsByClusterRequestHeader requestHeader = new GetTopicsByClusterRequestHeader(); + requestHeader.setCluster(cluster); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_TOPICS_BY_CLUSTER, requestHeader); + RemotingCommand response = this.remotingClient.invokeSync(null, request, timeoutMillis); + assert response != null; switch (response.getCode()) { case ResponseCode.SUCCESS: { - return; + byte[] body = response.getBody(); + if (body != null) { + TopicList topicList = TopicList.decode(body, TopicList.class); + return topicList; + } } default: break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQClientException(response.getCode(), response.getRemark()); } public TopicList getSystemTopicList( final long timeoutMillis) throws RemotingException, MQClientException, InterruptedException { RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_SYSTEM_TOPIC_LIST_FROM_NS, null); - RemotingCommand response = this.remotingClient.invokeSync(null, request, timeoutMillis); assert response != null; switch (response.getCode()) { @@ -1623,7 +2492,6 @@ public TopicList getSystemTopicList( public TopicList getSystemTopicListFromBroker(final String addr, final long timeoutMillis) throws RemotingException, MQClientException, InterruptedException { RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_SYSTEM_TOPIC_LIST_FROM_BROKER, null); - RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), request, timeoutMillis); assert response != null; @@ -1659,6 +2527,22 @@ public boolean cleanExpiredConsumeQueue(final String addr, throw new MQClientException(response.getCode(), response.getRemark()); } + public boolean deleteExpiredCommitLog(final String addr, long timeoutMillis) throws MQClientException, + RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.DELETE_EXPIRED_COMMITLOG, null); + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), + request, timeoutMillis); + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return true; + } + default: + break; + } + + throw new MQClientException(response.getCode(), response.getRemark()); + } + public boolean cleanUnusedTopicByAddr(final String addr, long timeoutMillis) throws MQClientException, RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException { @@ -1707,9 +2591,11 @@ public ConsumerRunningInfo getConsumerRunningInfo(final String addr, String cons public ConsumeMessageDirectlyResult consumeMessageDirectly(final String addr, String consumerGroup, String clientId, + String topic, String msgId, final long timeoutMillis) throws RemotingException, MQClientException, InterruptedException { ConsumeMessageDirectlyResultRequestHeader requestHeader = new ConsumeMessageDirectlyResultRequestHeader(); + requestHeader.setTopic(topic); requestHeader.setConsumerGroup(consumerGroup); requestHeader.setClientId(clientId); requestHeader.setMsgId(msgId); @@ -1751,7 +2637,6 @@ public Map queryCorrectionOffset(final String addr, final String requestHeader.setFilterGroups(sb.toString()); } RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_CORRECTION_OFFSET, requestHeader); - RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), request, timeoutMillis); assert response != null; @@ -1772,7 +2657,6 @@ public Map queryCorrectionOffset(final String addr, final String public TopicList getUnitTopicList(final boolean containRetry, final long timeoutMillis) throws RemotingException, MQClientException, InterruptedException { RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_UNIT_TOPIC_LIST, null); - RemotingCommand response = this.remotingClient.invokeSync(null, request, timeoutMillis); assert response != null; switch (response.getCode()) { @@ -1784,8 +2668,9 @@ public TopicList getUnitTopicList(final boolean containRetry, final long timeout Iterator it = topicList.getTopicList().iterator(); while (it.hasNext()) { String topic = it.next(); - if (topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) + if (topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { it.remove(); + } } } @@ -1802,7 +2687,6 @@ public TopicList getUnitTopicList(final boolean containRetry, final long timeout public TopicList getHasUnitSubTopicList(final boolean containRetry, final long timeoutMillis) throws RemotingException, MQClientException, InterruptedException { RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_HAS_UNIT_SUB_TOPIC_LIST, null); - RemotingCommand response = this.remotingClient.invokeSync(null, request, timeoutMillis); assert response != null; switch (response.getCode()) { @@ -1814,8 +2698,9 @@ public TopicList getHasUnitSubTopicList(final boolean containRetry, final long t Iterator it = topicList.getTopicList().iterator(); while (it.hasNext()) { String topic = it.next(); - if (topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) + if (topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { it.remove(); + } } } return topicList; @@ -1831,7 +2716,6 @@ public TopicList getHasUnitSubTopicList(final boolean containRetry, final long t public TopicList getHasUnitSubUnUnitTopicList(final boolean containRetry, final long timeoutMillis) throws RemotingException, MQClientException, InterruptedException { RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_HAS_UNIT_SUB_UNUNIT_TOPIC_LIST, null); - RemotingCommand response = this.remotingClient.invokeSync(null, request, timeoutMillis); assert response != null; switch (response.getCode()) { @@ -1843,8 +2727,9 @@ public TopicList getHasUnitSubUnUnitTopicList(final boolean containRetry, final Iterator it = topicList.getTopicList().iterator(); while (it.hasNext()) { String topic = it.next(); - if (topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) + if (topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { it.remove(); + } } } return topicList; @@ -1866,7 +2751,6 @@ public void cloneGroupOffset(final String addr, final String srcGroup, final Str requestHeader.setTopic(topic); requestHeader.setOffline(isOffline); RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CLONE_GROUP_OFFSET, requestHeader); - RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), request, timeoutMillis); assert response != null; @@ -1889,7 +2773,6 @@ public BrokerStatsData viewBrokerStatsData(String brokerAddr, String statsName, requestHeader.setStatsKey(statsKey); RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.VIEW_BROKER_STATS_DATA, requestHeader); - RemotingCommand response = this.remotingClient .invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), brokerAddr), request, timeoutMillis); assert response != null; @@ -1908,8 +2791,7 @@ public BrokerStatsData viewBrokerStatsData(String brokerAddr, String statsName, } public Set getClusterList(String topic, - long timeoutMillis) throws MQClientException, RemotingConnectException, - RemotingSendRequestException, RemotingTimeoutException, InterruptedException { + long timeoutMillis) { return Collections.EMPTY_SET; } @@ -1920,7 +2802,6 @@ public ConsumeStatsList fetchConsumeStatsInBroker(String brokerAddr, boolean isO requestHeader.setIsOrder(isOrder); RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_BROKER_CONSUME_STATS, requestHeader); - RemotingCommand response = this.remotingClient .invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), brokerAddr), request, timeoutMillis); assert response != null; @@ -1952,7 +2833,26 @@ public SubscriptionGroupWrapper getAllSubscriptionGroup(final String brokerAddr, default: break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), brokerAddr); + } + + public SubscriptionGroupConfig getSubscriptionGroupConfig(final String brokerAddr, String group, + long timeoutMillis) throws InterruptedException, + RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQBrokerException { + GetSubscriptionGroupConfigRequestHeader header = new GetSubscriptionGroupConfigRequestHeader(); + header.setGroup(group); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_SUBSCRIPTIONGROUP_CONFIG, header); + RemotingCommand response = this.remotingClient + .invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), brokerAddr), request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return RemotingSerializable.decode(response.getBody(), SubscriptionGroupConfig.class); + } + default: + break; + } + throw new MQBrokerException(response.getCode(), response.getRemark(), brokerAddr); } public TopicConfigSerializeWrapper getAllTopicConfig(final String addr, @@ -1971,12 +2871,11 @@ public TopicConfigSerializeWrapper getAllTopicConfig(final String addr, break; } - throw new MQBrokerException(response.getCode(), response.getRemark()); + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); } public void updateNameServerConfig(final Properties properties, final List nameServers, long timeoutMillis) - throws UnsupportedEncodingException, - MQBrokerException, InterruptedException, RemotingTimeoutException, RemotingSendRequestException, + throws UnsupportedEncodingException, InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQClientException { String str = MixAll.properties2String(properties); if (str == null || str.length() < 1) { @@ -2021,7 +2920,7 @@ public Map getNameServerConfig(final List nameServer RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_NAMESRV_CONFIG, null); - Map configMap = new HashMap(4); + Map configMap = new HashMap<>(4); for (String nameServer : invokeNameServers) { RemotingCommand response = this.remotingClient.invokeSync(nameServer, request, timeoutMillis); @@ -2050,7 +2949,6 @@ public QueryConsumeQueueResponseBody queryConsumeQueue(final String brokerAddr, requestHeader.setConsumerGroup(consumerGroup); RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_CONSUME_QUEUE, requestHeader); - RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), brokerAddr), request, timeoutMillis); assert response != null; @@ -2084,4 +2982,319 @@ public void checkClientInBroker(final String brokerAddr, final String consumerGr throw new MQClientException(response.getCode(), response.getRemark()); } } -} \ No newline at end of file + + public boolean resumeCheckHalfMessage(final String addr, String msgId, + final long timeoutMillis) throws RemotingException, InterruptedException { + ResumeCheckHalfMessageRequestHeader requestHeader = new ResumeCheckHalfMessageRequestHeader(); + requestHeader.setMsgId(msgId); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.RESUME_CHECK_HALF_MESSAGE, requestHeader); + + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), + request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return true; + } + default: + log.error("Failed to resume half message check logic. Remark={}", response.getRemark()); + return false; + } + } + + public void setMessageRequestMode(final String brokerAddr, final String topic, final String consumerGroup, + final MessageRequestMode mode, final int popShareQueueNum, final long timeoutMillis) + throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, + RemotingConnectException, MQClientException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.SET_MESSAGE_REQUEST_MODE, null); + + SetMessageRequestModeRequestBody requestBody = new SetMessageRequestModeRequestBody(); + requestBody.setTopic(topic); + requestBody.setConsumerGroup(consumerGroup); + requestBody.setMode(mode); + requestBody.setPopShareQueueNum(popShareQueueNum); + request.setBody(requestBody.encode()); + + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), brokerAddr), request, timeoutMillis); + assert response != null; + if (ResponseCode.SUCCESS != response.getCode()) { + throw new MQClientException(response.getCode(), response.getRemark()); + } + } + + public TopicConfigAndQueueMapping getTopicConfig(final String brokerAddr, String topic, + long timeoutMillis) throws InterruptedException, + RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQBrokerException { + GetTopicConfigRequestHeader header = new GetTopicConfigRequestHeader(); + header.setTopic(topic); + header.setLo(true); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_TOPIC_CONFIG, header); + RemotingCommand response = this.remotingClient + .invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), brokerAddr), request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return RemotingSerializable.decode(response.getBody(), TopicConfigAndQueueMapping.class); + } + //should check the exist + case ResponseCode.TOPIC_NOT_EXIST: { + //should return null? + break; + } + default: + break; + } + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public void createStaticTopic(final String addr, final String defaultTopic, final TopicConfig topicConfig, + final TopicQueueMappingDetail topicQueueMappingDetail, boolean force, + final long timeoutMillis) throws RemotingException, InterruptedException, MQBrokerException { + CreateTopicRequestHeader requestHeader = new CreateTopicRequestHeader(); + requestHeader.setTopic(topicConfig.getTopicName()); + requestHeader.setDefaultTopic(defaultTopic); + requestHeader.setReadQueueNums(topicConfig.getReadQueueNums()); + requestHeader.setWriteQueueNums(topicConfig.getWriteQueueNums()); + requestHeader.setPerm(topicConfig.getPerm()); + requestHeader.setTopicFilterType(topicConfig.getTopicFilterType().name()); + requestHeader.setTopicSysFlag(topicConfig.getTopicSysFlag()); + requestHeader.setOrder(topicConfig.isOrder()); + requestHeader.setForce(force); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_AND_CREATE_STATIC_TOPIC, requestHeader); + request.setBody(topicQueueMappingDetail.encode()); + + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), + request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return; + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + /** + * @param addr + * @param requestHeader + * @param timeoutMillis + * @throws InterruptedException + * @throws RemotingTimeoutException + * @throws RemotingSendRequestException + * @throws RemotingConnectException + * @throws MQBrokerException + */ + public GroupForbidden updateAndGetGroupForbidden(String addr, UpdateGroupForbiddenRequestHeader requestHeader, + long timeoutMillis) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_AND_GET_GROUP_FORBIDDEN, requestHeader); + + RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), + request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return RemotingSerializable.decode(response.getBody(), GroupForbidden.class); + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark(), addr); + } + + public void resetMasterFlushOffset(final String brokerAddr, final long masterFlushOffset) + throws InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException, MQBrokerException { + ResetMasterFlushOffsetHeader requestHeader = new ResetMasterFlushOffsetHeader(); + requestHeader.setMasterFlushOffset(masterFlushOffset); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.RESET_MASTER_FLUSH_OFFSET, requestHeader); + + RemotingCommand response = this.remotingClient.invokeSync(brokerAddr, request, 3000); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return; + } + default: + break; + } + throw new MQBrokerException(response.getCode(), response.getRemark(), brokerAddr); + } + + public HARuntimeInfo getBrokerHAStatus(final String brokerAddr, final long timeoutMillis) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, + InterruptedException, MQBrokerException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_BROKER_HA_STATUS, null); + RemotingCommand response = this.remotingClient.invokeSync(brokerAddr, request, timeoutMillis); + assert response != null; + + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return HARuntimeInfo.decode(response.getBody(), HARuntimeInfo.class); + } + default: + break; + } + + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public GetMetaDataResponseHeader getControllerMetaData( + final String controllerAddress) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, RemotingCommandException, MQBrokerException { + final RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CONTROLLER_GET_METADATA_INFO, null); + final RemotingCommand response = this.remotingClient.invokeSync(controllerAddress, request, 3000); + assert response != null; + if (response.getCode() == SUCCESS) { + return (GetMetaDataResponseHeader) response.decodeCommandCustomHeader(GetMetaDataResponseHeader.class); + } + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public BrokerReplicasInfo getInSyncStateData(final String controllerAddress, + final List brokers) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException, RemotingCommandException { + // Get controller leader address. + final GetMetaDataResponseHeader controllerMetaData = getControllerMetaData(controllerAddress); + assert controllerMetaData != null; + assert controllerMetaData.getControllerLeaderAddress() != null; + final String leaderAddress = controllerMetaData.getControllerLeaderAddress(); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CONTROLLER_GET_SYNC_STATE_DATA, null); + final byte[] body = RemotingSerializable.encode(brokers); + request.setBody(body); + RemotingCommand response = this.remotingClient.invokeSync(leaderAddress, request, 3000); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return RemotingSerializable.decode(response.getBody(), BrokerReplicasInfo.class); + } + default: + break; + } + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public EpochEntryCache getBrokerEpochCache( + String brokerAddr) throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, MQBrokerException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_BROKER_EPOCH_CACHE, null); + final RemotingCommand response = this.remotingClient.invokeSync(brokerAddr, request, 3000); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return RemotingSerializable.decode(response.getBody(), EpochEntryCache.class); + } + default: + break; + } + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public Map getControllerConfig(final List controllerServers, + final long timeoutMillis) throws InterruptedException, RemotingTimeoutException, + RemotingSendRequestException, RemotingConnectException, MQClientException, UnsupportedEncodingException { + List invokeControllerServers = (controllerServers == null || controllerServers.isEmpty()) ? + this.remotingClient.getNameServerAddressList() : controllerServers; + if (invokeControllerServers == null || invokeControllerServers.isEmpty()) { + return null; + } + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_CONTROLLER_CONFIG, null); + + Map configMap = new HashMap<>(4); + for (String controller : invokeControllerServers) { + RemotingCommand response = this.remotingClient.invokeSync(controller, request, timeoutMillis); + + assert response != null; + + if (ResponseCode.SUCCESS == response.getCode()) { + configMap.put(controller, MixAll.string2Properties(new String(response.getBody(), MixAll.DEFAULT_CHARSET))); + } else { + throw new MQClientException(response.getCode(), response.getRemark()); + } + } + return configMap; + } + + public void updateControllerConfig(final Properties properties, final List controllers, + final long timeoutMillis) throws InterruptedException, RemotingConnectException, UnsupportedEncodingException, + RemotingSendRequestException, RemotingTimeoutException, MQClientException { + String str = MixAll.properties2String(properties); + if (str.length() < 1 || controllers == null || controllers.isEmpty()) { + return; + } + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_CONTROLLER_CONFIG, null); + request.setBody(str.getBytes(MixAll.DEFAULT_CHARSET)); + + RemotingCommand errResponse = null; + for (String controller : controllers) { + RemotingCommand response = this.remotingClient.invokeSync(controller, request, timeoutMillis); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + break; + } + default: + errResponse = response; + } + } + + if (errResponse != null) { + throw new MQClientException(errResponse.getCode(), errResponse.getRemark()); + } + } + + public Pair electMaster(String controllerAddr, String clusterName, + String brokerName, + Long brokerId) throws MQBrokerException, RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException, RemotingCommandException { + + //get controller leader address + final GetMetaDataResponseHeader controllerMetaData = this.getControllerMetaData(controllerAddr); + assert controllerMetaData != null; + assert controllerMetaData.getControllerLeaderAddress() != null; + final String leaderAddress = controllerMetaData.getControllerLeaderAddress(); + ElectMasterRequestHeader electRequestHeader = ElectMasterRequestHeader.ofAdminTrigger(clusterName, brokerName, brokerId); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CONTROLLER_ELECT_MASTER, electRequestHeader); + final RemotingCommand response = this.remotingClient.invokeSync(leaderAddress, request, 3000); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + BrokerMemberGroup brokerMemberGroup = RemotingSerializable.decode(response.getBody(), BrokerMemberGroup.class); + ElectMasterResponseHeader responseHeader = (ElectMasterResponseHeader) response.decodeCommandCustomHeader(ElectMasterResponseHeader.class); + return new Pair<>(responseHeader, brokerMemberGroup); + } + default: + break; + } + throw new MQBrokerException(response.getCode(), response.getRemark()); + } + + public void cleanControllerBrokerData(String controllerAddr, String clusterName, + String brokerName, String brokerControllerIdsToClean, boolean isCleanLivingBroker) + throws RemotingException, InterruptedException, MQBrokerException { + + //get controller leader address + final GetMetaDataResponseHeader controllerMetaData = this.getControllerMetaData(controllerAddr); + assert controllerMetaData != null; + assert controllerMetaData.getControllerLeaderAddress() != null; + final String leaderAddress = controllerMetaData.getControllerLeaderAddress(); + + CleanControllerBrokerDataRequestHeader cleanHeader = new CleanControllerBrokerDataRequestHeader(clusterName, brokerName, brokerControllerIdsToClean, isCleanLivingBroker); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CLEAN_BROKER_DATA, cleanHeader); + + final RemotingCommand response = this.remotingClient.invokeSync(leaderAddress, request, 3000); + assert response != null; + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + return; + } + default: + break; + } + throw new MQBrokerException(response.getCode(), response.getRemark()); + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/MQClientManager.java b/client/src/main/java/org/apache/rocketmq/client/impl/MQClientManager.java index 25877d7386d..02eaa66e99a 100644 --- a/client/src/main/java/org/apache/rocketmq/client/impl/MQClientManager.java +++ b/client/src/main/java/org/apache/rocketmq/client/impl/MQClientManager.java @@ -21,16 +21,20 @@ import java.util.concurrent.atomic.AtomicInteger; import org.apache.rocketmq.client.ClientConfig; import org.apache.rocketmq.client.impl.factory.MQClientInstance; -import org.apache.rocketmq.client.log.ClientLogger; +import org.apache.rocketmq.client.producer.ProduceAccumulator; import org.apache.rocketmq.remoting.RPCHook; -import org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; public class MQClientManager { - private final static Logger log = ClientLogger.getLog(); + private final static Logger log = LoggerFactory.getLogger(MQClientManager.class); private static MQClientManager instance = new MQClientManager(); private AtomicInteger factoryIndexGenerator = new AtomicInteger(); private ConcurrentMap factoryTable = - new ConcurrentHashMap(); + new ConcurrentHashMap<>(); + private ConcurrentMap accumulatorTable = + new ConcurrentHashMap(); + private MQClientManager() { @@ -40,11 +44,10 @@ public static MQClientManager getInstance() { return instance; } - public MQClientInstance getAndCreateMQClientInstance(final ClientConfig clientConfig) { - return getAndCreateMQClientInstance(clientConfig, null); + public MQClientInstance getOrCreateMQClientInstance(final ClientConfig clientConfig) { + return getOrCreateMQClientInstance(clientConfig, null); } - - public MQClientInstance getAndCreateMQClientInstance(final ClientConfig clientConfig, RPCHook rpcHook) { + public MQClientInstance getOrCreateMQClientInstance(final ClientConfig clientConfig, RPCHook rpcHook) { String clientId = clientConfig.buildMQClientId(); MQClientInstance instance = this.factoryTable.get(clientId); if (null == instance) { @@ -62,6 +65,22 @@ public MQClientInstance getAndCreateMQClientInstance(final ClientConfig clientCo return instance; } + public ProduceAccumulator getOrCreateProduceAccumulator(final ClientConfig clientConfig) { + String clientId = clientConfig.buildMQClientId(); + ProduceAccumulator accumulator = this.accumulatorTable.get(clientId); + if (null == accumulator) { + accumulator = new ProduceAccumulator(clientId); + ProduceAccumulator prev = this.accumulatorTable.putIfAbsent(clientId, accumulator); + if (prev != null) { + accumulator = prev; + log.warn("Returned Previous ProduceAccumulator for clientId:[{}]", clientId); + } else { + log.info("Created new ProduceAccumulator for clientId:[{}]", clientId); + } + } + + return accumulator; + } public void removeClientFactory(final String clientId) { this.factoryTable.remove(clientId); diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/admin/MqClientAdminImpl.java b/client/src/main/java/org/apache/rocketmq/client/impl/admin/MqClientAdminImpl.java new file mode 100644 index 00000000000..34f066c7ddd --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/admin/MqClientAdminImpl.java @@ -0,0 +1,438 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client.impl.admin; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.MqClientAdmin; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.RemotingClient; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.admin.ConsumeStats; +import org.apache.rocketmq.remoting.protocol.admin.TopicStatsTable; +import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.body.GroupList; +import org.apache.rocketmq.remoting.protocol.body.QueryConsumeTimeSpanBody; +import org.apache.rocketmq.remoting.protocol.body.QuerySubscriptionResponseBody; +import org.apache.rocketmq.remoting.protocol.body.QueueTimeSpan; +import org.apache.rocketmq.remoting.protocol.body.ResetOffsetBody; +import org.apache.rocketmq.remoting.protocol.body.TopicList; +import org.apache.rocketmq.remoting.protocol.header.ConsumeMessageDirectlyResultRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.CreateTopicRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.DeleteSubscriptionGroupRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.DeleteTopicRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumeStatsRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerConnectionListRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerRunningInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetTopicStatsInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumeTimeSpanRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QuerySubscriptionByConsumerRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryTopicConsumeByWhoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryTopicsByConsumerRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ResetOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ViewMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.DeleteKVConfigRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.DeleteTopicFromNamesrvRequestHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; + +public class MqClientAdminImpl implements MqClientAdmin { + private final static Logger log = LoggerFactory.getLogger(MqClientAdminImpl.class); + private final RemotingClient remotingClient; + + public MqClientAdminImpl(RemotingClient remotingClient) { + this.remotingClient = remotingClient; + } + + @Override + public CompletableFuture> queryMessage(String address, boolean uniqueKeyFlag, boolean decompressBody, + QueryMessageRequestHeader requestHeader, long timeoutMillis) { + CompletableFuture> future = new CompletableFuture<>(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_MESSAGE, requestHeader); + request.addExtField(MixAll.UNIQUE_MSG_QUERY_FLAG, String.valueOf(uniqueKeyFlag)); + remotingClient.invoke(address, request, timeoutMillis).thenAccept(response -> { + if (response.getCode() == ResponseCode.SUCCESS) { + List wrappers = MessageDecoder.decodesBatch(ByteBuffer.wrap(response.getBody()), true, decompressBody, true); + future.complete(filterMessages(wrappers, requestHeader.getTopic(), requestHeader.getKey(), uniqueKeyFlag)); + } else if (response.getCode() == ResponseCode.QUERY_NOT_FOUND) { + List wrappers = new ArrayList<>(); + future.complete(wrappers); + } else { + log.warn("queryMessage getResponseCommand failed, {} {}, header={}", response.getCode(), response.getRemark(), requestHeader); + future.completeExceptionally(new MQClientException(response.getCode(), response.getRemark())); + } + }); + + return future; + } + + @Override + public CompletableFuture getTopicStatsInfo(String address, + GetTopicStatsInfoRequestHeader requestHeader, long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_TOPIC_STATS_INFO, requestHeader); + remotingClient.invoke(address, request, timeoutMillis).thenAccept(response -> { + if (response.getCode() == ResponseCode.SUCCESS) { + TopicStatsTable topicStatsTable = TopicStatsTable.decode(response.getBody(), TopicStatsTable.class); + future.complete(topicStatsTable); + } else { + log.warn("getTopicStatsInfo getResponseCommand failed, {} {}, header={}", response.getCode(), response.getRemark(), requestHeader); + future.completeExceptionally(new MQClientException(response.getCode(), response.getRemark())); + } + }); + return future; + } + + @Override + public CompletableFuture> queryConsumeTimeSpan(String address, + QueryConsumeTimeSpanRequestHeader requestHeader, long timeoutMillis) { + CompletableFuture> future = new CompletableFuture<>(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_CONSUME_TIME_SPAN, requestHeader); + remotingClient.invoke(address, request, timeoutMillis).thenAccept(response -> { + if (response.getCode() == ResponseCode.SUCCESS) { + QueryConsumeTimeSpanBody consumeTimeSpanBody = GroupList.decode(response.getBody(), QueryConsumeTimeSpanBody.class); + future.complete(consumeTimeSpanBody.getConsumeTimeSpanSet()); + } else { + log.warn("queryConsumerTimeSpan getResponseCommand failed, {} {}, header={}", response.getCode(), response.getRemark(), requestHeader); + future.completeExceptionally(new MQClientException(response.getCode(), response.getRemark())); + } + }); + return future; + } + + @Override + public CompletableFuture updateOrCreateTopic(String address, CreateTopicRequestHeader requestHeader, + long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_AND_CREATE_TOPIC, requestHeader); + remotingClient.invoke(address, request, timeoutMillis).thenAccept(response -> { + if (response.getCode() == ResponseCode.SUCCESS) { + future.complete(null); + } else { + log.warn("updateOrCreateTopic getResponseCommand failed, {} {}, header={}", response.getCode(), response.getRemark(), requestHeader); + future.completeExceptionally(new MQClientException(response.getCode(), response.getRemark())); + } + }); + return future; + } + + @Override + public CompletableFuture updateOrCreateSubscriptionGroup(String address, SubscriptionGroupConfig config, + long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_AND_CREATE_SUBSCRIPTIONGROUP, null); + byte[] body = RemotingSerializable.encode(config); + request.setBody(body); + remotingClient.invoke(address, request, timeoutMillis).thenAccept(response -> { + if (response.getCode() == ResponseCode.SUCCESS) { + future.complete(null); + } else { + log.warn("updateOrCreateSubscriptionGroup getResponseCommand failed, {} {}, header={}", response.getCode(), response.getRemark(), config); + future.completeExceptionally(new MQClientException(response.getCode(), response.getRemark())); + } + }); + return future; + } + + @Override + public CompletableFuture deleteTopicInBroker(String address, DeleteTopicRequestHeader requestHeader, + long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.DELETE_TOPIC_IN_BROKER, requestHeader); + remotingClient.invoke(address, request, timeoutMillis).thenAccept(response -> { + if (response.getCode() == ResponseCode.SUCCESS) { + future.complete(null); + } else { + log.warn("deleteTopicInBroker getResponseCommand failed, {} {}, header={}", response.getCode(), response.getRemark(), requestHeader); + future.completeExceptionally(new MQClientException(response.getCode(), response.getRemark())); + } + }); + return future; + } + + @Override + public CompletableFuture deleteTopicInNameserver(String address, DeleteTopicFromNamesrvRequestHeader requestHeader, + long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.DELETE_TOPIC_IN_NAMESRV, requestHeader); + remotingClient.invoke(address, request, timeoutMillis).thenAccept(response -> { + if (response.getCode() == ResponseCode.SUCCESS) { + future.complete(null); + } else { + log.warn("deleteTopicInNameserver getResponseCommand failed, {} {}, header={}", response.getCode(), response.getRemark(), requestHeader); + future.completeExceptionally(new MQClientException(response.getCode(), response.getRemark())); + } + }); + return future; + } + + @Override + public CompletableFuture deleteKvConfig(String address, DeleteKVConfigRequestHeader requestHeader, + long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.DELETE_KV_CONFIG, requestHeader); + remotingClient.invoke(address, request, timeoutMillis).thenAccept(response -> { + if (response.getCode() == ResponseCode.SUCCESS) { + future.complete(null); + } else { + log.warn("deleteKvConfig getResponseCommand failed, {} {}, header={}", response.getCode(), response.getRemark(), requestHeader); + future.completeExceptionally(new MQClientException(response.getCode(), response.getRemark())); + } + }); + return future; + } + + @Override + public CompletableFuture deleteSubscriptionGroup(String address, DeleteSubscriptionGroupRequestHeader requestHeader, + long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.DELETE_SUBSCRIPTIONGROUP, requestHeader); + remotingClient.invoke(address, request, timeoutMillis).thenAccept(response -> { + if (response.getCode() == ResponseCode.SUCCESS) { + future.complete(null); + } else { + log.warn("deleteSubscriptionGroup getResponseCommand failed, {} {}, header={}", response.getCode(), response.getRemark(), requestHeader); + future.completeExceptionally(new MQClientException(response.getCode(), response.getRemark())); + } + }); + return future; + } + + @Override + public CompletableFuture> invokeBrokerToResetOffset(String address, + ResetOffsetRequestHeader requestHeader, long timeoutMillis) { + CompletableFuture> future = new CompletableFuture<>(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.INVOKE_BROKER_TO_RESET_OFFSET, requestHeader); + remotingClient.invoke(address, request, timeoutMillis).thenAccept(response -> { + if (response.getCode() == ResponseCode.SUCCESS && null != response.getBody()) { + Map offsetTable = ResetOffsetBody.decode(response.getBody(), ResetOffsetBody.class).getOffsetTable(); + future.complete(offsetTable); + log.info("Invoke broker to reset offset success. address:{}, header:{}, offsetTable:{}", + address, requestHeader, offsetTable); + } else { + log.warn("invokeBrokerToResetOffset getResponseCommand failed, {} {}, header={}", response.getCode(), response.getRemark(), requestHeader); + future.completeExceptionally(new MQClientException(response.getCode(), response.getRemark())); + } + }); + return future; + } + + @Override + public CompletableFuture viewMessage(String address, ViewMessageRequestHeader requestHeader, + long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.VIEW_MESSAGE_BY_ID, requestHeader); + remotingClient.invoke(address, request, timeoutMillis).thenAccept(response -> { + if (response.getCode() == ResponseCode.SUCCESS) { + ByteBuffer byteBuffer = ByteBuffer.wrap(response.getBody()); + MessageExt messageExt = MessageDecoder.clientDecode(byteBuffer, true); + future.complete(messageExt); + } else { + log.warn("viewMessage getResponseCommand failed, {} {}, header={}", response.getCode(), response.getRemark(), requestHeader); + future.completeExceptionally(new MQClientException(response.getCode(), response.getRemark())); + } + }); + return future; + } + + @Override + public CompletableFuture getBrokerClusterInfo(String address, long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_BROKER_CLUSTER_INFO, null); + remotingClient.invoke(address, request, timeoutMillis).thenAccept(response -> { + if (response.getCode() == ResponseCode.SUCCESS) { + ClusterInfo clusterInfo = ClusterInfo.decode(response.getBody(), ClusterInfo.class); + future.complete(clusterInfo); + } else { + log.warn("getBrokerClusterInfo getResponseCommand failed, {} {}", response.getCode(), response.getRemark()); + future.completeExceptionally(new MQClientException(response.getCode(), response.getRemark())); + } + }); + return future; + } + + @Override + public CompletableFuture getConsumerConnectionList(String address, + GetConsumerConnectionListRequestHeader requestHeader, long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_CONSUMER_CONNECTION_LIST, requestHeader); + remotingClient.invoke(address, request, timeoutMillis).thenAccept(response -> { + if (response.getCode() == ResponseCode.SUCCESS) { + ConsumerConnection consumerConnection = ConsumerConnection.decode(response.getBody(), ConsumerConnection.class); + future.complete(consumerConnection); + } else { + log.warn("getConsumerConnectionList getResponseCommand failed, {} {}", response.getCode(), response.getRemark()); + future.completeExceptionally(new MQClientException(response.getCode(), response.getRemark())); + } + }); + return future; + } + + @Override + public CompletableFuture queryTopicsByConsumer(String address, + QueryTopicsByConsumerRequestHeader requestHeader, long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_TOPICS_BY_CONSUMER, requestHeader); + remotingClient.invoke(address, request, timeoutMillis).thenAccept(response -> { + if (response.getCode() == ResponseCode.SUCCESS) { + TopicList topicList = TopicList.decode(response.getBody(), TopicList.class); + future.complete(topicList); + } else { + log.warn("queryTopicsByConsumer getResponseCommand failed, {} {}", response.getCode(), response.getRemark()); + future.completeExceptionally(new MQClientException(response.getCode(), response.getRemark())); + } + }); + return future; + } + + @Override + public CompletableFuture querySubscriptionByConsumer(String address, + QuerySubscriptionByConsumerRequestHeader requestHeader, long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_SUBSCRIPTION_BY_CONSUMER, requestHeader); + remotingClient.invoke(address, request, timeoutMillis).thenAccept(response -> { + if (response.getCode() == ResponseCode.SUCCESS) { + QuerySubscriptionResponseBody subscriptionResponseBody = + QuerySubscriptionResponseBody.decode(response.getBody(), QuerySubscriptionResponseBody.class); + future.complete(subscriptionResponseBody.getSubscriptionData()); + } else { + log.warn("querySubscriptionByConsumer getResponseCommand failed, {} {}", response.getCode(), response.getRemark()); + future.completeExceptionally(new MQClientException(response.getCode(), response.getRemark())); + } + }); + return future; + } + + @Override + public CompletableFuture getConsumeStats(String address, GetConsumeStatsRequestHeader requestHeader, + long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_CONSUME_STATS, requestHeader); + remotingClient.invoke(address, request, timeoutMillis).thenAccept(response -> { + if (response.getCode() == ResponseCode.SUCCESS) { + ConsumeStats consumeStats = ConsumeStats.decode(response.getBody(), ConsumeStats.class); + future.complete(consumeStats); + } else { + log.warn("getConsumeStats getResponseCommand failed, {} {}", response.getCode(), response.getRemark()); + future.completeExceptionally(new MQClientException(response.getCode(), response.getRemark())); + } + }); + return future; + } + + @Override + public CompletableFuture queryTopicConsumeByWho(String address, + QueryTopicConsumeByWhoRequestHeader requestHeader, long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_TOPIC_CONSUME_BY_WHO, requestHeader); + remotingClient.invoke(address, request, timeoutMillis).thenAccept(response -> { + if (response.getCode() == ResponseCode.SUCCESS) { + GroupList groupList = GroupList.decode(response.getBody(), GroupList.class); + future.complete(groupList); + } else { + log.warn("queryTopicConsumeByWho getResponseCommand failed, {} {}", response.getCode(), response.getRemark()); + future.completeExceptionally(new MQClientException(response.getCode(), response.getRemark())); + } + }); + return future; + } + + @Override + public CompletableFuture getConsumerRunningInfo(String address, + GetConsumerRunningInfoRequestHeader requestHeader, long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_CONSUMER_RUNNING_INFO, requestHeader); + remotingClient.invoke(address, request, timeoutMillis).thenAccept(response -> { + if (response.getCode() == ResponseCode.SUCCESS) { + ConsumerRunningInfo info = ConsumerRunningInfo.decode(response.getBody(), ConsumerRunningInfo.class); + future.complete(info); + } else { + log.warn("getConsumerRunningInfo getResponseCommand failed, {} {}", response.getCode(), response.getRemark()); + future.completeExceptionally(new MQClientException(response.getCode(), response.getRemark())); + } + }); + return future; + } + + @Override + public CompletableFuture consumeMessageDirectly(String address, + ConsumeMessageDirectlyResultRequestHeader requestHeader, long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CONSUME_MESSAGE_DIRECTLY, requestHeader); + remotingClient.invoke(address, request, timeoutMillis).thenAccept(response -> { + if (response.getCode() == ResponseCode.SUCCESS) { + ConsumeMessageDirectlyResult info = ConsumeMessageDirectlyResult.decode(response.getBody(), ConsumeMessageDirectlyResult.class); + future.complete(info); + } else { + log.warn("consumeMessageDirectly getResponseCommand failed, {} {}", response.getCode(), response.getRemark()); + future.completeExceptionally(new MQClientException(response.getCode(), response.getRemark())); + } + }); + return future; + } + + private List filterMessages(List messageFoundList, String topic, String key, + boolean uniqueKeyFlag) { + List matchedMessages = new ArrayList<>(); + if (uniqueKeyFlag) { + matchedMessages.addAll(messageFoundList.stream() + .filter(msg -> topic.equals(msg.getTopic())) + .filter(msg -> key.equals(msg.getMsgId())) + .collect(Collectors.toList()) + ); + } else { + matchedMessages.addAll(messageFoundList.stream() + .filter(msg -> topic.equals(msg.getTopic())) + .filter(msg -> { + boolean matched = false; + if (StringUtils.isNotBlank(msg.getKeys())) { + String[] keyArray = msg.getKeys().split(MessageConst.KEY_SEPARATOR); + for (String s : keyArray) { + if (key.equals(s)) { + matched = true; + break; + } + } + } + + return matched; + }).collect(Collectors.toList())); + } + + return matchedMessages; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/AssignedMessageQueue.java b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/AssignedMessageQueue.java new file mode 100644 index 00000000000..a57cb53b4df --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/AssignedMessageQueue.java @@ -0,0 +1,244 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.impl.consumer; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.common.message.MessageQueue; + +public class AssignedMessageQueue { + + private final ConcurrentHashMap assignedMessageQueueState; + + private RebalanceImpl rebalanceImpl; + + public AssignedMessageQueue() { + assignedMessageQueueState = new ConcurrentHashMap<>(); + } + + public void setRebalanceImpl(RebalanceImpl rebalanceImpl) { + this.rebalanceImpl = rebalanceImpl; + } + + public boolean isPaused(MessageQueue messageQueue) { + MessageQueueState messageQueueState = assignedMessageQueueState.get(messageQueue); + if (messageQueueState != null) { + return messageQueueState.isPaused(); + } + return true; + } + + public void pause(Collection messageQueues) { + for (MessageQueue messageQueue : messageQueues) { + MessageQueueState messageQueueState = assignedMessageQueueState.get(messageQueue); + if (assignedMessageQueueState.get(messageQueue) != null) { + messageQueueState.setPaused(true); + } + } + } + + public void resume(Collection messageQueueCollection) { + for (MessageQueue messageQueue : messageQueueCollection) { + MessageQueueState messageQueueState = assignedMessageQueueState.get(messageQueue); + if (assignedMessageQueueState.get(messageQueue) != null) { + messageQueueState.setPaused(false); + } + } + } + + public ProcessQueue getProcessQueue(MessageQueue messageQueue) { + MessageQueueState messageQueueState = assignedMessageQueueState.get(messageQueue); + if (messageQueueState != null) { + return messageQueueState.getProcessQueue(); + } + return null; + } + + public long getPullOffset(MessageQueue messageQueue) { + MessageQueueState messageQueueState = assignedMessageQueueState.get(messageQueue); + if (messageQueueState != null) { + return messageQueueState.getPullOffset(); + } + return -1; + } + + public void updatePullOffset(MessageQueue messageQueue, long offset, ProcessQueue processQueue) { + MessageQueueState messageQueueState = assignedMessageQueueState.get(messageQueue); + if (messageQueueState != null) { + if (messageQueueState.getProcessQueue() != processQueue) { + return; + } + messageQueueState.setPullOffset(offset); + } + } + + public long getConsumerOffset(MessageQueue messageQueue) { + MessageQueueState messageQueueState = assignedMessageQueueState.get(messageQueue); + if (messageQueueState != null) { + return messageQueueState.getConsumeOffset(); + } + return -1; + } + + public void updateConsumeOffset(MessageQueue messageQueue, long offset) { + MessageQueueState messageQueueState = assignedMessageQueueState.get(messageQueue); + if (messageQueueState != null) { + messageQueueState.setConsumeOffset(offset); + } + } + + public void setSeekOffset(MessageQueue messageQueue, long offset) { + MessageQueueState messageQueueState = assignedMessageQueueState.get(messageQueue); + if (messageQueueState != null) { + messageQueueState.setSeekOffset(offset); + } + } + + public long getSeekOffset(MessageQueue messageQueue) { + MessageQueueState messageQueueState = assignedMessageQueueState.get(messageQueue); + if (messageQueueState != null) { + return messageQueueState.getSeekOffset(); + } + return -1; + } + + public void updateAssignedMessageQueue(String topic, Collection assigned) { + synchronized (this.assignedMessageQueueState) { + Iterator> it = this.assignedMessageQueueState.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry next = it.next(); + if (next.getKey().getTopic().equals(topic)) { + if (!assigned.contains(next.getKey())) { + next.getValue().getProcessQueue().setDropped(true); + it.remove(); + } + } + } + addAssignedMessageQueue(assigned); + } + } + + public void updateAssignedMessageQueue(Collection assigned) { + synchronized (this.assignedMessageQueueState) { + Iterator> it = this.assignedMessageQueueState.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry next = it.next(); + if (!assigned.contains(next.getKey())) { + next.getValue().getProcessQueue().setDropped(true); + it.remove(); + } + } + addAssignedMessageQueue(assigned); + } + } + + private void addAssignedMessageQueue(Collection assigned) { + for (MessageQueue messageQueue : assigned) { + if (!this.assignedMessageQueueState.containsKey(messageQueue)) { + MessageQueueState messageQueueState; + if (rebalanceImpl != null && rebalanceImpl.getProcessQueueTable().get(messageQueue) != null) { + messageQueueState = new MessageQueueState(messageQueue, rebalanceImpl.getProcessQueueTable().get(messageQueue)); + } else { + ProcessQueue processQueue = new ProcessQueue(); + messageQueueState = new MessageQueueState(messageQueue, processQueue); + } + this.assignedMessageQueueState.put(messageQueue, messageQueueState); + } + } + } + + public void removeAssignedMessageQueue(String topic) { + synchronized (this.assignedMessageQueueState) { + Iterator> it = this.assignedMessageQueueState.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry next = it.next(); + if (next.getKey().getTopic().equals(topic)) { + it.remove(); + } + } + } + } + + public Set getAssignedMessageQueues() { + return this.assignedMessageQueueState.keySet(); + } + + private class MessageQueueState { + private MessageQueue messageQueue; + private ProcessQueue processQueue; + private volatile boolean paused = false; + private volatile long pullOffset = -1; + private volatile long consumeOffset = -1; + private volatile long seekOffset = -1; + + private MessageQueueState(MessageQueue messageQueue, ProcessQueue processQueue) { + this.messageQueue = messageQueue; + this.processQueue = processQueue; + } + + public MessageQueue getMessageQueue() { + return messageQueue; + } + + public void setMessageQueue(MessageQueue messageQueue) { + this.messageQueue = messageQueue; + } + + public boolean isPaused() { + return paused; + } + + public void setPaused(boolean paused) { + this.paused = paused; + } + + public long getPullOffset() { + return pullOffset; + } + + public void setPullOffset(long pullOffset) { + this.pullOffset = pullOffset; + } + + public ProcessQueue getProcessQueue() { + return processQueue; + } + + public void setProcessQueue(ProcessQueue processQueue) { + this.processQueue = processQueue; + } + + public long getConsumeOffset() { + return consumeOffset; + } + + public void setConsumeOffset(long consumeOffset) { + this.consumeOffset = consumeOffset; + } + + public long getSeekOffset() { + return seekOffset; + } + + public void setSeekOffset(long seekOffset) { + this.seekOffset = seekOffset; + } + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageConcurrentlyService.java b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageConcurrentlyService.java index 87120178368..ea6c8072b57 100644 --- a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageConcurrentlyService.java +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageConcurrentlyService.java @@ -35,21 +35,21 @@ import org.apache.rocketmq.client.consumer.listener.ConsumeReturnType; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.client.hook.ConsumeMessageContext; -import org.apache.rocketmq.client.log.ClientLogger; import org.apache.rocketmq.client.stat.ConsumerStatsManager; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.UtilAll; import org.apache.rocketmq.common.message.MessageAccessor; -import org.apache.rocketmq.common.message.MessageConst; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.body.CMResult; -import org.apache.rocketmq.common.protocol.body.ConsumeMessageDirectlyResult; -import org.apache.rocketmq.remoting.common.RemotingHelper; -import org.slf4j.Logger; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.remoting.protocol.body.CMResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; public class ConsumeMessageConcurrentlyService implements ConsumeMessageService { - private static final Logger log = ClientLogger.getLog(); + private static final Logger log = LoggerFactory.getLogger(ConsumeMessageConcurrentlyService.class); private final DefaultMQPushConsumerImpl defaultMQPushConsumerImpl; private final DefaultMQPushConsumer defaultMQPushConsumer; private final MessageListenerConcurrently messageListener; @@ -67,18 +67,19 @@ public ConsumeMessageConcurrentlyService(DefaultMQPushConsumerImpl defaultMQPush this.defaultMQPushConsumer = this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer(); this.consumerGroup = this.defaultMQPushConsumer.getConsumerGroup(); - this.consumeRequestQueue = new LinkedBlockingQueue(); + this.consumeRequestQueue = new LinkedBlockingQueue<>(); + String consumerGroupTag = (consumerGroup.length() > 100 ? consumerGroup.substring(0, 100) : consumerGroup) + "_"; this.consumeExecutor = new ThreadPoolExecutor( this.defaultMQPushConsumer.getConsumeThreadMin(), this.defaultMQPushConsumer.getConsumeThreadMax(), 1000 * 60, TimeUnit.MILLISECONDS, this.consumeRequestQueue, - new ThreadFactoryImpl("ConsumeMessageThread_")); + new ThreadFactoryImpl("ConsumeMessageThread_" + consumerGroupTag)); - this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("ConsumeMessageScheduledThread_")); - this.cleanExpireMsgExecutors = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("CleanExpireMsgScheduledThread_")); + this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("ConsumeMessageScheduledThread_" + consumerGroupTag)); + this.cleanExpireMsgExecutors = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("CleanExpireMsgScheduledThread_" + consumerGroupTag)); } public void start() { @@ -86,15 +87,19 @@ public void start() { @Override public void run() { - cleanExpireMsg(); + try { + cleanExpireMsg(); + } catch (Throwable e) { + log.error("scheduleAtFixedRate cleanExpireMsg exception", e); + } } }, this.defaultMQPushConsumer.getConsumeTimeout(), this.defaultMQPushConsumer.getConsumeTimeout(), TimeUnit.MINUTES); } - public void shutdown() { + public void shutdown(long awaitTerminateMillis) { this.scheduledExecutorService.shutdown(); - this.consumeExecutor.shutdown(); + ThreadUtils.shutdownGracefully(this.consumeExecutor, awaitTerminateMillis, TimeUnit.MILLISECONDS); this.cleanExpireMsgExecutors.shutdown(); } @@ -109,32 +114,12 @@ public void updateCorePoolSize(int corePoolSize) { @Override public void incCorePoolSize() { - // long corePoolSize = this.consumeExecutor.getCorePoolSize(); - // if (corePoolSize < this.defaultMQPushConsumer.getConsumeThreadMax()) - // { - // this.consumeExecutor.setCorePoolSize(this.consumeExecutor.getCorePoolSize() - // + 1); - // } - // log.info("incCorePoolSize Concurrently from {} to {}, ConsumerGroup: - // {}", - // corePoolSize, - // this.consumeExecutor.getCorePoolSize(), - // this.consumerGroup); + } @Override public void decCorePoolSize() { - // long corePoolSize = this.consumeExecutor.getCorePoolSize(); - // if (corePoolSize > this.defaultMQPushConsumer.getConsumeThreadMin()) - // { - // this.consumeExecutor.setCorePoolSize(this.consumeExecutor.getCorePoolSize() - // - 1); - // } - // log.info("decCorePoolSize Concurrently from {} to {}, ConsumerGroup: - // {}", - // corePoolSize, - // this.consumeExecutor.getCorePoolSize(), - // this.consumerGroup); + } @Override @@ -148,7 +133,8 @@ public ConsumeMessageDirectlyResult consumeMessageDirectly(MessageExt msg, Strin result.setOrder(false); result.setAutoCommit(true); - List msgs = new ArrayList(); + msg.setBrokerName(brokerName); + List msgs = new ArrayList<>(); msgs.add(msg); MessageQueue mq = new MessageQueue(); mq.setBrokerName(brokerName); @@ -157,7 +143,7 @@ public ConsumeMessageDirectlyResult consumeMessageDirectly(MessageExt msg, Strin ConsumeConcurrentlyContext context = new ConsumeConcurrentlyContext(mq); - this.resetRetryTopic(msgs); + this.defaultMQPushConsumerImpl.resetRetryAndNamespace(msgs, this.consumerGroup); final long beginTime = System.currentTimeMillis(); @@ -181,10 +167,10 @@ public ConsumeMessageDirectlyResult consumeMessageDirectly(MessageExt msg, Strin } } catch (Throwable e) { result.setConsumeResult(CMResult.CR_THROW_EXCEPTION); - result.setRemark(RemotingHelper.exceptionSimpleDesc(e)); + result.setRemark(UtilAll.exceptionSimpleDesc(e)); log.warn(String.format("consumeMessageDirectly exception: %s Group: %s Msgs: %s MQ: %s", - RemotingHelper.exceptionSimpleDesc(e), + UtilAll.exceptionSimpleDesc(e), ConsumeMessageConcurrentlyService.this.consumerGroup, msgs, mq), e); @@ -213,7 +199,7 @@ public void submitConsumeRequest( } } else { for (int total = 0; total < msgs.size(); ) { - List msgThis = new ArrayList(consumeBatchSize); + List msgThis = new ArrayList<>(consumeBatchSize); for (int i = 0; i < consumeBatchSize; i++, total++) { if (total < msgs.size()) { msgThis.add(msgs.get(total)); @@ -236,14 +222,11 @@ public void submitConsumeRequest( } } - public void resetRetryTopic(final List msgs) { - final String groupTopic = MixAll.getRetryTopic(consumerGroup); - for (MessageExt msg : msgs) { - String retryTopic = msg.getProperty(MessageConst.PROPERTY_RETRY_TOPIC); - if (retryTopic != null && groupTopic.equals(msg.getTopic())) { - msg.setTopic(retryTopic); - } - } + @Override + public void submitPopConsumeRequest(final List msgs, + final PopProcessQueue processQueue, + final MessageQueue messageQueue) { + throw new UnsupportedOperationException(); } private void cleanExpireMsg() { @@ -293,9 +276,16 @@ public void processConsumeResult( } break; case CLUSTERING: - List msgBackFailed = new ArrayList(consumeRequest.getMsgs().size()); + List msgBackFailed = new ArrayList<>(consumeRequest.getMsgs().size()); for (int i = ackIndex + 1; i < consumeRequest.getMsgs().size(); i++) { MessageExt msg = consumeRequest.getMsgs().get(i); + // Maybe message is expired and cleaned, just ignore it. + if (!consumeRequest.getProcessQueue().containsMessage(msg)) { + log.info("Message is not found in its process queue; skip send-back-procedure, topic={}, " + + "brokerName={}, queueId={}, queueOffset={}", msg.getTopic(), msg.getBrokerName(), + msg.getQueueId(), msg.getQueueOffset()); + continue; + } boolean result = this.sendMessageBack(msg, context); if (!result) { msg.setReconsumeTimes(msg.getReconsumeTimes() + 1); @@ -326,11 +316,13 @@ public ConsumerStatsManager getConsumerStatsManager() { public boolean sendMessageBack(final MessageExt msg, final ConsumeConcurrentlyContext context) { int delayLevel = context.getDelayLevelWhenNextConsume(); + // Wrap topic with namespace before sending back message. + msg.setTopic(this.defaultMQPushConsumer.withNamespace(msg.getTopic())); try { - this.defaultMQPushConsumerImpl.sendMessageBack(msg, delayLevel, context.getMessageQueue().getBrokerName()); + this.defaultMQPushConsumerImpl.sendMessageBack(msg, delayLevel, this.defaultMQPushConsumer.queueWithNamespace(context.getMessageQueue())); return true; } catch (Exception e) { - log.error("sendMessageBack exception, group: " + this.consumerGroup + " msg: " + msg.toString(), e); + log.error("sendMessageBack exception, group: " + this.consumerGroup + " msg: " + msg, e); } return false; @@ -392,12 +384,15 @@ public void run() { MessageListenerConcurrently listener = ConsumeMessageConcurrentlyService.this.messageListener; ConsumeConcurrentlyContext context = new ConsumeConcurrentlyContext(messageQueue); ConsumeConcurrentlyStatus status = null; + defaultMQPushConsumerImpl.tryResetPopRetryTopic(msgs, consumerGroup); + defaultMQPushConsumerImpl.resetRetryAndNamespace(msgs, defaultMQPushConsumer.getConsumerGroup()); ConsumeMessageContext consumeMessageContext = null; if (ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.hasHook()) { consumeMessageContext = new ConsumeMessageContext(); + consumeMessageContext.setNamespace(defaultMQPushConsumer.getNamespace()); consumeMessageContext.setConsumerGroup(defaultMQPushConsumer.getConsumerGroup()); - consumeMessageContext.setProps(new HashMap()); + consumeMessageContext.setProps(new HashMap<>()); consumeMessageContext.setMq(messageQueue); consumeMessageContext.setMsgList(msgs); consumeMessageContext.setSuccess(false); @@ -408,7 +403,6 @@ public void run() { boolean hasException = false; ConsumeReturnType returnType = ConsumeReturnType.SUCCESS; try { - ConsumeMessageConcurrentlyService.this.resetRetryTopic(msgs); if (msgs != null && !msgs.isEmpty()) { for (MessageExt msg : msgs) { MessageAccessor.setConsumeStartTimeStamp(msg, String.valueOf(System.currentTimeMillis())); @@ -416,11 +410,11 @@ public void run() { } status = listener.consumeMessage(Collections.unmodifiableList(msgs), context); } catch (Throwable e) { - log.warn("consumeMessage exception: {} Group: {} Msgs: {} MQ: {}", - RemotingHelper.exceptionSimpleDesc(e), + log.warn(String.format("consumeMessage exception: %s Group: %s Msgs: %s MQ: %s", + UtilAll.exceptionSimpleDesc(e), ConsumeMessageConcurrentlyService.this.consumerGroup, msgs, - messageQueue); + messageQueue), e); hasException = true; } long consumeRT = System.currentTimeMillis() - beginTimestamp; @@ -453,6 +447,7 @@ public void run() { if (ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.hasHook()) { consumeMessageContext.setStatus(status.toString()); consumeMessageContext.setSuccess(ConsumeConcurrentlyStatus.CONSUME_SUCCESS == status); + consumeMessageContext.setAccessChannel(defaultMQPushConsumer.getAccessChannel()); ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.executeHookAfter(consumeMessageContext); } diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageOrderlyService.java b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageOrderlyService.java index 43199e5b28c..cab4fe5d69f 100644 --- a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageOrderlyService.java +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageOrderlyService.java @@ -26,13 +26,13 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; import org.apache.rocketmq.client.consumer.listener.ConsumeReturnType; import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; import org.apache.rocketmq.client.hook.ConsumeMessageContext; -import org.apache.rocketmq.client.log.ClientLogger; import org.apache.rocketmq.client.stat.ConsumerStatsManager; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.ThreadFactoryImpl; @@ -42,14 +42,16 @@ import org.apache.rocketmq.common.message.MessageConst; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.body.CMResult; -import org.apache.rocketmq.common.protocol.body.ConsumeMessageDirectlyResult; -import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; -import org.apache.rocketmq.remoting.common.RemotingHelper; -import org.slf4j.Logger; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; +import org.apache.rocketmq.remoting.protocol.body.CMResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; public class ConsumeMessageOrderlyService implements ConsumeMessageService { - private static final Logger log = ClientLogger.getLog(); + private static final Logger log = LoggerFactory.getLogger(ConsumeMessageOrderlyService.class); private final static long MAX_TIME_CONSUME_CONTINUOUSLY = Long.parseLong(System.getProperty("rocketmq.client.maxTimeConsumeContinuously", "60000")); private final DefaultMQPushConsumerImpl defaultMQPushConsumerImpl; @@ -69,17 +71,18 @@ public ConsumeMessageOrderlyService(DefaultMQPushConsumerImpl defaultMQPushConsu this.defaultMQPushConsumer = this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer(); this.consumerGroup = this.defaultMQPushConsumer.getConsumerGroup(); - this.consumeRequestQueue = new LinkedBlockingQueue(); + this.consumeRequestQueue = new LinkedBlockingQueue<>(); + String consumerGroupTag = (consumerGroup.length() > 100 ? consumerGroup.substring(0, 100) : consumerGroup) + "_"; this.consumeExecutor = new ThreadPoolExecutor( this.defaultMQPushConsumer.getConsumeThreadMin(), this.defaultMQPushConsumer.getConsumeThreadMax(), 1000 * 60, TimeUnit.MILLISECONDS, this.consumeRequestQueue, - new ThreadFactoryImpl("ConsumeMessageThread_")); + new ThreadFactoryImpl("ConsumeMessageThread_" + consumerGroupTag)); - this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("ConsumeMessageScheduledThread_")); + this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("ConsumeMessageScheduledThread_" + consumerGroupTag)); } public void start() { @@ -87,16 +90,20 @@ public void start() { this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { @Override public void run() { - ConsumeMessageOrderlyService.this.lockMQPeriodically(); + try { + ConsumeMessageOrderlyService.this.lockMQPeriodically(); + } catch (Throwable e) { + log.error("scheduleAtFixedRate lockMQPeriodically exception", e); + } } }, 1000 * 1, ProcessQueue.REBALANCE_LOCK_INTERVAL, TimeUnit.MILLISECONDS); } } - public void shutdown() { + public void shutdown(long awaitTerminateMillis) { this.stopped = true; this.scheduledExecutorService.shutdown(); - this.consumeExecutor.shutdown(); + ThreadUtils.shutdownGracefully(this.consumeExecutor, awaitTerminateMillis, TimeUnit.MILLISECONDS); if (MessageModel.CLUSTERING.equals(this.defaultMQPushConsumerImpl.messageModel())) { this.unlockAllMQ(); } @@ -133,7 +140,7 @@ public ConsumeMessageDirectlyResult consumeMessageDirectly(MessageExt msg, Strin ConsumeMessageDirectlyResult result = new ConsumeMessageDirectlyResult(); result.setOrder(true); - List msgs = new ArrayList(); + List msgs = new ArrayList<>(); msgs.add(msg); MessageQueue mq = new MessageQueue(); mq.setBrokerName(brokerName); @@ -142,6 +149,8 @@ public ConsumeMessageDirectlyResult consumeMessageDirectly(MessageExt msg, Strin ConsumeOrderlyContext context = new ConsumeOrderlyContext(mq); + this.defaultMQPushConsumerImpl.resetRetryAndNamespace(msgs, this.consumerGroup); + final long beginTime = System.currentTimeMillis(); log.info("consumeMessageDirectly receive new message: {}", msg); @@ -170,10 +179,10 @@ public ConsumeMessageDirectlyResult consumeMessageDirectly(MessageExt msg, Strin } } catch (Throwable e) { result.setConsumeResult(CMResult.CR_THROW_EXCEPTION); - result.setRemark(RemotingHelper.exceptionSimpleDesc(e)); + result.setRemark(UtilAll.exceptionSimpleDesc(e)); log.warn(String.format("consumeMessageDirectly exception: %s Group: %s Msgs: %s MQ: %s", - RemotingHelper.exceptionSimpleDesc(e), + UtilAll.exceptionSimpleDesc(e), ConsumeMessageOrderlyService.this.consumerGroup, msgs, mq), e); @@ -199,6 +208,13 @@ public void submitConsumeRequest( } } + @Override + public void submitPopConsumeRequest(final List msgs, + final PopProcessQueue processQueue, + final MessageQueue messageQueue) { + throw new UnsupportedOperationException(); + } + public synchronized void lockMQPeriodically() { if (!this.stopped) { this.defaultMQPushConsumerImpl.getRebalanceImpl().lockAll(); @@ -274,7 +290,7 @@ public boolean processConsumeResult( case SUSPEND_CURRENT_QUEUE_A_MOMENT: this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size()); if (checkReconsumeTimes(msgs)) { - consumeRequest.getProcessQueue().makeMessageToCosumeAgain(msgs); + consumeRequest.getProcessQueue().makeMessageToConsumeAgain(msgs); this.submitConsumeRequestLater( consumeRequest.getProcessQueue(), consumeRequest.getMessageQueue(), @@ -306,7 +322,7 @@ public boolean processConsumeResult( case SUSPEND_CURRENT_QUEUE_A_MOMENT: this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size()); if (checkReconsumeTimes(msgs)) { - consumeRequest.getProcessQueue().makeMessageToCosumeAgain(msgs); + consumeRequest.getProcessQueue().makeMessageToConsumeAgain(msgs); this.submitConsumeRequestLater( consumeRequest.getProcessQueue(), consumeRequest.getMessageQueue(), @@ -362,16 +378,17 @@ public boolean sendMessageBack(final MessageExt msg) { try { // max reconsume times exceeded then send to dead letter queue. Message newMsg = new Message(MixAll.getRetryTopic(this.defaultMQPushConsumer.getConsumerGroup()), msg.getBody()); + MessageAccessor.setProperties(newMsg, msg.getProperties()); String originMsgId = MessageAccessor.getOriginMessageId(msg); MessageAccessor.setOriginMessageId(newMsg, UtilAll.isBlank(originMsgId) ? msg.getMsgId() : originMsgId); newMsg.setFlag(msg.getFlag()); - MessageAccessor.setProperties(newMsg, msg.getProperties()); MessageAccessor.putProperty(newMsg, MessageConst.PROPERTY_RETRY_TOPIC, msg.getTopic()); - MessageAccessor.setReconsumeTime(newMsg, String.valueOf(msg.getReconsumeTimes())); + MessageAccessor.setReconsumeTime(newMsg, String.valueOf(msg.getReconsumeTimes() + 1)); MessageAccessor.setMaxReconsumeTimes(newMsg, String.valueOf(getMaxReconsumeTimes())); + MessageAccessor.clearProperty(newMsg, MessageConst.PROPERTY_TRANSACTION_PREPARED); newMsg.setDelayTimeLevel(3 + msg.getReconsumeTimes()); - this.defaultMQPushConsumer.getDefaultMQPushConsumerImpl().getmQClientFactory().getDefaultMQProducer().send(newMsg); + this.defaultMQPushConsumerImpl.getmQClientFactory().getDefaultMQProducer().send(newMsg); return true; } catch (Exception e) { log.error("sendMessageBack exception, group: " + this.consumerGroup + " msg: " + msg.toString(), e); @@ -380,6 +397,14 @@ public boolean sendMessageBack(final MessageExt msg) { return false; } + public void resetNamespace(final List msgs) { + for (MessageExt msg : msgs) { + if (StringUtils.isNotEmpty(this.defaultMQPushConsumer.getNamespace())) { + msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(), this.defaultMQPushConsumer.getNamespace())); + } + } + } + class ConsumeRequest implements Runnable { private final ProcessQueue processQueue; private final MessageQueue messageQueue; @@ -407,7 +432,7 @@ public void run() { final Object objLock = messageQueueLock.fetchLockObject(this.messageQueue); synchronized (objLock) { if (MessageModel.BROADCASTING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel()) - || (this.processQueue.isLocked() && !this.processQueue.isLockExpired())) { + || this.processQueue.isLocked() && !this.processQueue.isLockExpired()) { final long beginTime = System.currentTimeMillis(); for (boolean continueConsume = true; continueConsume; ) { if (this.processQueue.isDropped()) { @@ -438,7 +463,8 @@ public void run() { final int consumeBatchSize = ConsumeMessageOrderlyService.this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize(); - List msgs = this.processQueue.takeMessags(consumeBatchSize); + List msgs = this.processQueue.takeMessages(consumeBatchSize); + defaultMQPushConsumerImpl.resetRetryAndNamespace(msgs, defaultMQPushConsumer.getConsumerGroup()); if (!msgs.isEmpty()) { final ConsumeOrderlyContext context = new ConsumeOrderlyContext(this.messageQueue); @@ -449,11 +475,12 @@ public void run() { consumeMessageContext = new ConsumeMessageContext(); consumeMessageContext .setConsumerGroup(ConsumeMessageOrderlyService.this.defaultMQPushConsumer.getConsumerGroup()); + consumeMessageContext.setNamespace(defaultMQPushConsumer.getNamespace()); consumeMessageContext.setMq(messageQueue); consumeMessageContext.setMsgList(msgs); consumeMessageContext.setSuccess(false); // init the consume context type - consumeMessageContext.setProps(new HashMap()); + consumeMessageContext.setProps(new HashMap<>()); ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.executeHookBefore(consumeMessageContext); } @@ -461,7 +488,7 @@ public void run() { ConsumeReturnType returnType = ConsumeReturnType.SUCCESS; boolean hasException = false; try { - this.processQueue.getLockConsume().lock(); + this.processQueue.getConsumeLock().readLock().lock(); if (this.processQueue.isDropped()) { log.warn("consumeMessage, the message queue not be able to consume, because it's dropped. {}", this.messageQueue); @@ -470,14 +497,14 @@ public void run() { status = messageListener.consumeMessage(Collections.unmodifiableList(msgs), context); } catch (Throwable e) { - log.warn("consumeMessage exception: {} Group: {} Msgs: {} MQ: {}", - RemotingHelper.exceptionSimpleDesc(e), + log.warn(String.format("consumeMessage exception: %s Group: %s Msgs: %s MQ: %s", + UtilAll.exceptionSimpleDesc(e), ConsumeMessageOrderlyService.this.consumerGroup, msgs, - messageQueue); + messageQueue), e); hasException = true; } finally { - this.processQueue.getLockConsume().unlock(); + this.processQueue.getConsumeLock().readLock().unlock(); } if (null == status @@ -516,6 +543,7 @@ public void run() { consumeMessageContext.setStatus(status.toString()); consumeMessageContext .setSuccess(ConsumeOrderlyStatus.SUCCESS == status || ConsumeOrderlyStatus.COMMIT == status); + consumeMessageContext.setAccessChannel(defaultMQPushConsumer.getAccessChannel()); ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.executeHookAfter(consumeMessageContext); } diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopConcurrentlyService.java b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopConcurrentlyService.java new file mode 100644 index 00000000000..a61454f5955 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopConcurrentlyService.java @@ -0,0 +1,484 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.impl.consumer; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.client.consumer.AckCallback; +import org.apache.rocketmq.client.consumer.AckResult; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.ConsumeReturnType; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.hook.ConsumeMessageContext; +import org.apache.rocketmq.client.stat.ConsumerStatsManager; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.remoting.protocol.body.CMResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; +import org.apache.rocketmq.remoting.protocol.header.ExtraInfoUtil; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class ConsumeMessagePopConcurrentlyService implements ConsumeMessageService { + private static final Logger log = LoggerFactory.getLogger(ConsumeMessagePopConcurrentlyService.class); + private final DefaultMQPushConsumerImpl defaultMQPushConsumerImpl; + private final DefaultMQPushConsumer defaultMQPushConsumer; + private final MessageListenerConcurrently messageListener; + private final BlockingQueue consumeRequestQueue; + private final ThreadPoolExecutor consumeExecutor; + private final String consumerGroup; + + private final ScheduledExecutorService scheduledExecutorService; + + public ConsumeMessagePopConcurrentlyService(DefaultMQPushConsumerImpl defaultMQPushConsumerImpl, + MessageListenerConcurrently messageListener) { + this.defaultMQPushConsumerImpl = defaultMQPushConsumerImpl; + this.messageListener = messageListener; + + this.defaultMQPushConsumer = this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer(); + this.consumerGroup = this.defaultMQPushConsumer.getConsumerGroup(); + this.consumeRequestQueue = new LinkedBlockingQueue<>(); + + this.consumeExecutor = new ThreadPoolExecutor( + this.defaultMQPushConsumer.getConsumeThreadMin(), + this.defaultMQPushConsumer.getConsumeThreadMax(), + 1000 * 60, + TimeUnit.MILLISECONDS, + this.consumeRequestQueue, + new ThreadFactoryImpl("ConsumeMessageThread_")); + + this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("ConsumeMessageScheduledThread_")); + } + + public void start() { + } + + public void shutdown(long awaitTerminateMillis) { + this.scheduledExecutorService.shutdown(); + ThreadUtils.shutdownGracefully(this.consumeExecutor, awaitTerminateMillis, TimeUnit.MILLISECONDS); + } + + @Override + public void updateCorePoolSize(int corePoolSize) { + if (corePoolSize > 0 + && corePoolSize <= Short.MAX_VALUE + && corePoolSize < this.defaultMQPushConsumer.getConsumeThreadMax()) { + this.consumeExecutor.setCorePoolSize(corePoolSize); + } + } + + @Override + public void incCorePoolSize() { + } + + @Override + public void decCorePoolSize() { + } + + @Override + public int getCorePoolSize() { + return this.consumeExecutor.getCorePoolSize(); + } + + + @Override + public ConsumeMessageDirectlyResult consumeMessageDirectly(MessageExt msg, String brokerName) { + ConsumeMessageDirectlyResult result = new ConsumeMessageDirectlyResult(); + result.setOrder(false); + result.setAutoCommit(true); + + List msgs = new ArrayList<>(); + msgs.add(msg); + MessageQueue mq = new MessageQueue(); + mq.setBrokerName(brokerName); + mq.setTopic(msg.getTopic()); + mq.setQueueId(msg.getQueueId()); + + ConsumeConcurrentlyContext context = new ConsumeConcurrentlyContext(mq); + + this.defaultMQPushConsumerImpl.resetRetryAndNamespace(msgs, this.consumerGroup); + + final long beginTime = System.currentTimeMillis(); + + log.info("consumeMessageDirectly receive new message: {}", msg); + + try { + ConsumeConcurrentlyStatus status = this.messageListener.consumeMessage(msgs, context); + if (status != null) { + switch (status) { + case CONSUME_SUCCESS: + result.setConsumeResult(CMResult.CR_SUCCESS); + break; + case RECONSUME_LATER: + result.setConsumeResult(CMResult.CR_LATER); + break; + default: + break; + } + } else { + result.setConsumeResult(CMResult.CR_RETURN_NULL); + } + } catch (Throwable e) { + result.setConsumeResult(CMResult.CR_THROW_EXCEPTION); + result.setRemark(UtilAll.exceptionSimpleDesc(e)); + + log.warn(String.format("consumeMessageDirectly exception: %s Group: %s Msgs: %s MQ: %s", + UtilAll.exceptionSimpleDesc(e), + ConsumeMessagePopConcurrentlyService.this.consumerGroup, + msgs, + mq), e); + } + + result.setSpentTimeMills(System.currentTimeMillis() - beginTime); + + log.info("consumeMessageDirectly Result: {}", result); + + return result; + } + + @Override + public void submitConsumeRequest(List msgs, ProcessQueue processQueue, + MessageQueue messageQueue, boolean dispathToConsume) { + throw new UnsupportedOperationException(); + } + + @Override + public void submitPopConsumeRequest( + final List msgs, + final PopProcessQueue processQueue, + final MessageQueue messageQueue) { + final int consumeBatchSize = this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize(); + if (msgs.size() <= consumeBatchSize) { + ConsumeRequest consumeRequest = new ConsumeRequest(msgs, processQueue, messageQueue); + try { + this.consumeExecutor.submit(consumeRequest); + } catch (RejectedExecutionException e) { + this.submitConsumeRequestLater(consumeRequest); + } + } else { + for (int total = 0; total < msgs.size(); ) { + List msgThis = new ArrayList<>(consumeBatchSize); + for (int i = 0; i < consumeBatchSize; i++, total++) { + if (total < msgs.size()) { + msgThis.add(msgs.get(total)); + } else { + break; + } + } + + ConsumeRequest consumeRequest = new ConsumeRequest(msgThis, processQueue, messageQueue); + try { + this.consumeExecutor.submit(consumeRequest); + } catch (RejectedExecutionException e) { + for (; total < msgs.size(); total++) { + msgThis.add(msgs.get(total)); + } + + this.submitConsumeRequestLater(consumeRequest); + } + } + } + } + + public void processConsumeResult( + final ConsumeConcurrentlyStatus status, + final ConsumeConcurrentlyContext context, + final ConsumeRequest consumeRequest) { + + if (consumeRequest.getMsgs().isEmpty()) { + return; + } + + int ackIndex = context.getAckIndex(); + String topic = consumeRequest.getMessageQueue().getTopic(); + + switch (status) { + case CONSUME_SUCCESS: + if (ackIndex >= consumeRequest.getMsgs().size()) { + ackIndex = consumeRequest.getMsgs().size() - 1; + } + int ok = ackIndex + 1; + int failed = consumeRequest.getMsgs().size() - ok; + this.getConsumerStatsManager().incConsumeOKTPS(consumerGroup, topic, ok); + this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, topic, failed); + break; + case RECONSUME_LATER: + ackIndex = -1; + this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, topic, + consumeRequest.getMsgs().size()); + break; + default: + break; + } + + //ack if consume success + for (int i = 0; i <= ackIndex; i++) { + this.defaultMQPushConsumerImpl.ackAsync(consumeRequest.getMsgs().get(i), consumerGroup); + consumeRequest.getPopProcessQueue().ack(); + } + + //consume later if consume fail + for (int i = ackIndex + 1; i < consumeRequest.getMsgs().size(); i++) { + MessageExt msgExt = consumeRequest.getMsgs().get(i); + consumeRequest.getPopProcessQueue().ack(); + if (msgExt.getReconsumeTimes() >= this.defaultMQPushConsumerImpl.getMaxReconsumeTimes()) { + checkNeedAckOrDelay(msgExt); + continue; + } + + int delayLevel = context.getDelayLevelWhenNextConsume(); + changePopInvisibleTime(consumeRequest.getMsgs().get(i), consumerGroup, delayLevel); + } + } + + private void checkNeedAckOrDelay(MessageExt msgExt) { + int[] delayLevelTable = this.defaultMQPushConsumerImpl.getPopDelayLevel(); + + long msgDelaytime = System.currentTimeMillis() - msgExt.getBornTimestamp(); + if (msgDelaytime > delayLevelTable[delayLevelTable.length - 1] * 1000 * 2) { + log.warn("Consume too many times, ack message async. message {}", msgExt.toString()); + this.defaultMQPushConsumerImpl.ackAsync(msgExt, consumerGroup); + } else { + int delayLevel = delayLevelTable.length - 1; + for (; delayLevel >= 0; delayLevel--) { + if (msgDelaytime >= delayLevelTable[delayLevel] * 1000) { + delayLevel++; + break; + } + } + + changePopInvisibleTime(msgExt, consumerGroup, delayLevel); + log.warn("Consume too many times, but delay time {} not enough. changePopInvisibleTime to delayLevel {} . message key:{}", + msgDelaytime, delayLevel, msgExt.getKeys()); + } + } + + private void changePopInvisibleTime(final MessageExt msg, String consumerGroup, int delayLevel) { + if (0 == delayLevel) { + delayLevel = msg.getReconsumeTimes(); + } + + int[] delayLevelTable = this.defaultMQPushConsumerImpl.getPopDelayLevel(); + int delaySecond = delayLevel >= delayLevelTable.length ? delayLevelTable[delayLevelTable.length - 1] : delayLevelTable[delayLevel]; + String extraInfo = msg.getProperty(MessageConst.PROPERTY_POP_CK); + + try { + this.defaultMQPushConsumerImpl.changePopInvisibleTimeAsync(msg.getTopic(), consumerGroup, extraInfo, + delaySecond * 1000L, new AckCallback() { + @Override + public void onSuccess(AckResult ackResult) { + } + + + @Override + public void onException(Throwable e) { + log.error("changePopInvisibleTimeAsync fail. msg:{} error info: {}", msg.toString(), e.toString()); + } + }); + } catch (Throwable t) { + log.error("changePopInvisibleTimeAsync fail, group:{} msg:{} errorInfo:{}", consumerGroup, msg.toString(), t.toString()); + } + } + + public ConsumerStatsManager getConsumerStatsManager() { + return this.defaultMQPushConsumerImpl.getConsumerStatsManager(); + } + + private void submitConsumeRequestLater( + final List msgs, + final PopProcessQueue processQueue, + final MessageQueue messageQueue + ) { + + this.scheduledExecutorService.schedule(new Runnable() { + + @Override + public void run() { + ConsumeMessagePopConcurrentlyService.this.submitPopConsumeRequest(msgs, processQueue, messageQueue); + } + }, 5000, TimeUnit.MILLISECONDS); + } + + private void submitConsumeRequestLater(final ConsumeRequest consumeRequest + ) { + + this.scheduledExecutorService.schedule(new Runnable() { + + @Override + public void run() { + ConsumeMessagePopConcurrentlyService.this.consumeExecutor.submit(consumeRequest); + } + }, 5000, TimeUnit.MILLISECONDS); + } + + class ConsumeRequest implements Runnable { + private final List msgs; + private final PopProcessQueue processQueue; + private final MessageQueue messageQueue; + private long popTime = 0; + private long invisibleTime = 0; + + public ConsumeRequest(List msgs, PopProcessQueue processQueue, MessageQueue messageQueue) { + this.msgs = msgs; + this.processQueue = processQueue; + this.messageQueue = messageQueue; + + try { + String extraInfo = msgs.get(0).getProperty(MessageConst.PROPERTY_POP_CK); + String[] extraInfoStrs = ExtraInfoUtil.split(extraInfo); + popTime = ExtraInfoUtil.getPopTime(extraInfoStrs); + invisibleTime = ExtraInfoUtil.getInvisibleTime(extraInfoStrs); + } catch (Throwable t) { + log.error("parse extra info error. msg:" + msgs.get(0), t); + } + } + + public boolean isPopTimeout() { + if (msgs.size() == 0 || popTime <= 0 || invisibleTime <= 0) { + return true; + } + + long current = System.currentTimeMillis(); + return current - popTime >= invisibleTime; + } + + public List getMsgs() { + return msgs; + } + + public PopProcessQueue getPopProcessQueue() { + return processQueue; + } + + @Override + public void run() { + if (this.processQueue.isDropped()) { + log.info("the message queue not be able to consume, because it's dropped(pop). group={} {}", ConsumeMessagePopConcurrentlyService.this.consumerGroup, this.messageQueue); + return; + } + + if (isPopTimeout()) { + log.info("the pop message time out so abort consume. popTime={} invisibleTime={}, group={} {}", + popTime, invisibleTime, ConsumeMessagePopConcurrentlyService.this.consumerGroup, this.messageQueue); + processQueue.decFoundMsg(-msgs.size()); + return; + } + + MessageListenerConcurrently listener = ConsumeMessagePopConcurrentlyService.this.messageListener; + ConsumeConcurrentlyContext context = new ConsumeConcurrentlyContext(messageQueue); + ConsumeConcurrentlyStatus status = null; + defaultMQPushConsumerImpl.resetRetryAndNamespace(msgs, defaultMQPushConsumer.getConsumerGroup()); + + ConsumeMessageContext consumeMessageContext = null; + if (ConsumeMessagePopConcurrentlyService.this.defaultMQPushConsumerImpl.hasHook()) { + consumeMessageContext = new ConsumeMessageContext(); + consumeMessageContext.setNamespace(defaultMQPushConsumer.getNamespace()); + consumeMessageContext.setConsumerGroup(defaultMQPushConsumer.getConsumerGroup()); + consumeMessageContext.setProps(new HashMap<>()); + consumeMessageContext.setMq(messageQueue); + consumeMessageContext.setMsgList(msgs); + consumeMessageContext.setSuccess(false); + ConsumeMessagePopConcurrentlyService.this.defaultMQPushConsumerImpl.executeHookBefore(consumeMessageContext); + } + + long beginTimestamp = System.currentTimeMillis(); + boolean hasException = false; + ConsumeReturnType returnType = ConsumeReturnType.SUCCESS; + try { + if (msgs != null && !msgs.isEmpty()) { + for (MessageExt msg : msgs) { + MessageAccessor.setConsumeStartTimeStamp(msg, String.valueOf(System.currentTimeMillis())); + } + } + status = listener.consumeMessage(Collections.unmodifiableList(msgs), context); + } catch (Throwable e) { + log.warn("consumeMessage exception: {} Group: {} Msgs: {} MQ: {}", + UtilAll.exceptionSimpleDesc(e), + ConsumeMessagePopConcurrentlyService.this.consumerGroup, + msgs, + messageQueue); + hasException = true; + } + long consumeRT = System.currentTimeMillis() - beginTimestamp; + if (null == status) { + if (hasException) { + returnType = ConsumeReturnType.EXCEPTION; + } else { + returnType = ConsumeReturnType.RETURNNULL; + } + } else if (consumeRT >= invisibleTime * 1000) { + returnType = ConsumeReturnType.TIME_OUT; + } else if (ConsumeConcurrentlyStatus.RECONSUME_LATER == status) { + returnType = ConsumeReturnType.FAILED; + } else if (ConsumeConcurrentlyStatus.CONSUME_SUCCESS == status) { + returnType = ConsumeReturnType.SUCCESS; + } + + if (null == status) { + log.warn("consumeMessage return null, Group: {} Msgs: {} MQ: {}", + ConsumeMessagePopConcurrentlyService.this.consumerGroup, + msgs, + messageQueue); + status = ConsumeConcurrentlyStatus.RECONSUME_LATER; + } + + if (ConsumeMessagePopConcurrentlyService.this.defaultMQPushConsumerImpl.hasHook()) { + consumeMessageContext.getProps().put(MixAll.CONSUME_CONTEXT_TYPE, returnType.name()); + consumeMessageContext.setStatus(status.toString()); + consumeMessageContext.setSuccess(ConsumeConcurrentlyStatus.CONSUME_SUCCESS == status); + consumeMessageContext.setAccessChannel(defaultMQPushConsumer.getAccessChannel()); + ConsumeMessagePopConcurrentlyService.this.defaultMQPushConsumerImpl.executeHookAfter(consumeMessageContext); + } + + ConsumeMessagePopConcurrentlyService.this.getConsumerStatsManager() + .incConsumeRT(ConsumeMessagePopConcurrentlyService.this.consumerGroup, messageQueue.getTopic(), consumeRT); + + if (!processQueue.isDropped() && !isPopTimeout()) { + ConsumeMessagePopConcurrentlyService.this.processConsumeResult(status, context, this); + } else { + if (msgs != null) { + processQueue.decFoundMsg(-msgs.size()); + } + + log.warn("processQueue invalid. isDropped={}, isPopTimeout={}, messageQueue={}, msgs={}", + processQueue.isDropped(), isPopTimeout(), messageQueue, msgs); + } + } + + public MessageQueue getMessageQueue() { + return messageQueue; + } + + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopOrderlyService.java b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopOrderlyService.java new file mode 100644 index 00000000000..ae6adfea5df --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopOrderlyService.java @@ -0,0 +1,407 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.impl.consumer; + +import io.netty.util.internal.ConcurrentSet; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; +import org.apache.rocketmq.client.stat.ConsumerStatsManager; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; +import org.apache.rocketmq.remoting.protocol.body.CMResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class ConsumeMessagePopOrderlyService implements ConsumeMessageService { + private static final Logger log = LoggerFactory.getLogger(ConsumeMessagePopOrderlyService.class); + private final DefaultMQPushConsumerImpl defaultMQPushConsumerImpl; + private final DefaultMQPushConsumer defaultMQPushConsumer; + private final MessageListenerOrderly messageListener; + private final BlockingQueue consumeRequestQueue; + private final ConcurrentSet consumeRequestSet = new ConcurrentSet<>(); + private final ThreadPoolExecutor consumeExecutor; + private final String consumerGroup; + private final MessageQueueLock messageQueueLock = new MessageQueueLock(); + private final MessageQueueLock consumeRequestLock = new MessageQueueLock(); + private final ScheduledExecutorService scheduledExecutorService; + private volatile boolean stopped = false; + + public ConsumeMessagePopOrderlyService(DefaultMQPushConsumerImpl defaultMQPushConsumerImpl, + MessageListenerOrderly messageListener) { + this.defaultMQPushConsumerImpl = defaultMQPushConsumerImpl; + this.messageListener = messageListener; + + this.defaultMQPushConsumer = this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer(); + this.consumerGroup = this.defaultMQPushConsumer.getConsumerGroup(); + this.consumeRequestQueue = new LinkedBlockingQueue<>(); + + this.consumeExecutor = new ThreadPoolExecutor( + this.defaultMQPushConsumer.getConsumeThreadMin(), + this.defaultMQPushConsumer.getConsumeThreadMax(), + 1000 * 60, + TimeUnit.MILLISECONDS, + this.consumeRequestQueue, + new ThreadFactoryImpl("ConsumeMessageThread_")); + + this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("ConsumeMessageScheduledThread_")); + } + + @Override + public void start() { + if (MessageModel.CLUSTERING.equals(ConsumeMessagePopOrderlyService.this.defaultMQPushConsumerImpl.messageModel())) { + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + ConsumeMessagePopOrderlyService.this.lockMQPeriodically(); + } + }, 1000 * 1, ProcessQueue.REBALANCE_LOCK_INTERVAL, TimeUnit.MILLISECONDS); + } + } + + @Override + public void shutdown(long awaitTerminateMillis) { + this.stopped = true; + this.scheduledExecutorService.shutdown(); + ThreadUtils.shutdownGracefully(this.consumeExecutor, awaitTerminateMillis, TimeUnit.MILLISECONDS); + if (MessageModel.CLUSTERING.equals(this.defaultMQPushConsumerImpl.messageModel())) { + this.unlockAllMessageQueues(); + } + } + + public synchronized void unlockAllMessageQueues() { + this.defaultMQPushConsumerImpl.getRebalanceImpl().unlockAll(false); + } + + @Override + public void updateCorePoolSize(int corePoolSize) { + if (corePoolSize > 0 + && corePoolSize <= Short.MAX_VALUE + && corePoolSize < this.defaultMQPushConsumer.getConsumeThreadMax()) { + this.consumeExecutor.setCorePoolSize(corePoolSize); + } + } + + @Override + public void incCorePoolSize() { + } + + @Override + public void decCorePoolSize() { + } + + @Override + public int getCorePoolSize() { + return this.consumeExecutor.getCorePoolSize(); + } + + @Override + public ConsumeMessageDirectlyResult consumeMessageDirectly(MessageExt msg, String brokerName) { + ConsumeMessageDirectlyResult result = new ConsumeMessageDirectlyResult(); + result.setOrder(true); + + List msgs = new ArrayList<>(); + msgs.add(msg); + MessageQueue mq = new MessageQueue(); + mq.setBrokerName(brokerName); + mq.setTopic(msg.getTopic()); + mq.setQueueId(msg.getQueueId()); + + ConsumeOrderlyContext context = new ConsumeOrderlyContext(mq); + + this.defaultMQPushConsumerImpl.resetRetryAndNamespace(msgs, this.consumerGroup); + + final long beginTime = System.currentTimeMillis(); + + log.info("consumeMessageDirectly receive new message: {}", msg); + + try { + ConsumeOrderlyStatus status = this.messageListener.consumeMessage(msgs, context); + if (status != null) { + switch (status) { + case COMMIT: + result.setConsumeResult(CMResult.CR_COMMIT); + break; + case ROLLBACK: + result.setConsumeResult(CMResult.CR_ROLLBACK); + break; + case SUCCESS: + result.setConsumeResult(CMResult.CR_SUCCESS); + break; + case SUSPEND_CURRENT_QUEUE_A_MOMENT: + result.setConsumeResult(CMResult.CR_LATER); + break; + default: + break; + } + } else { + result.setConsumeResult(CMResult.CR_RETURN_NULL); + } + } catch (Throwable e) { + result.setConsumeResult(CMResult.CR_THROW_EXCEPTION); + result.setRemark(UtilAll.exceptionSimpleDesc(e)); + + log.warn(String.format("consumeMessageDirectly exception: %s Group: %s Msgs: %s MQ: %s", + UtilAll.exceptionSimpleDesc(e), + ConsumeMessagePopOrderlyService.this.consumerGroup, + msgs, + mq), e); + } + + result.setAutoCommit(context.isAutoCommit()); + result.setSpentTimeMills(System.currentTimeMillis() - beginTime); + + log.info("consumeMessageDirectly Result: {}", result); + + return result; + } + + @Override + public void submitConsumeRequest(List msgs, ProcessQueue processQueue, + MessageQueue messageQueue, boolean dispathToConsume) { + throw new UnsupportedOperationException(); + } + + @Override + public void submitPopConsumeRequest(final List msgs, + final PopProcessQueue processQueue, + final MessageQueue messageQueue) { + ConsumeRequest req = new ConsumeRequest(processQueue, messageQueue); + submitConsumeRequest(req, false); + } + + public synchronized void lockMQPeriodically() { + if (!this.stopped) { + this.defaultMQPushConsumerImpl.getRebalanceImpl().lockAll(); + } + } + + private void removeConsumeRequest(final ConsumeRequest consumeRequest) { + consumeRequestSet.remove(consumeRequest); + } + + private void submitConsumeRequest(final ConsumeRequest consumeRequest, boolean force) { + Object lock = consumeRequestLock.fetchLockObject(consumeRequest.getMessageQueue(), consumeRequest.shardingKeyIndex); + synchronized (lock) { + boolean isNewReq = consumeRequestSet.add(consumeRequest); + if (force || isNewReq) { + try { + consumeExecutor.submit(consumeRequest); + } catch (Exception e) { + log.error("error submit consume request: {}, mq: {}, shardingKeyIndex: {}", + e.toString(), consumeRequest.getMessageQueue(), consumeRequest.getShardingKeyIndex()); + } + } + } + } + + private void submitConsumeRequestLater(final ConsumeRequest consumeRequest, final long suspendTimeMillis) { + long timeMillis = suspendTimeMillis; + if (timeMillis == -1) { + timeMillis = this.defaultMQPushConsumer.getSuspendCurrentQueueTimeMillis(); + } + + if (timeMillis < 10) { + timeMillis = 10; + } else if (timeMillis > 30000) { + timeMillis = 30000; + } + + this.scheduledExecutorService.schedule(new Runnable() { + + @Override + public void run() { + submitConsumeRequest(consumeRequest, true); + } + }, timeMillis, TimeUnit.MILLISECONDS); + } + + public boolean processConsumeResult( + final List msgs, + final ConsumeOrderlyStatus status, + final ConsumeOrderlyContext context, + final ConsumeRequest consumeRequest + ) { + return true; + } + + public ConsumerStatsManager getConsumerStatsManager() { + return this.defaultMQPushConsumerImpl.getConsumerStatsManager(); + } + + private int getMaxReconsumeTimes() { + // default reconsume times: Integer.MAX_VALUE + if (this.defaultMQPushConsumer.getMaxReconsumeTimes() == -1) { + return Integer.MAX_VALUE; + } else { + return this.defaultMQPushConsumer.getMaxReconsumeTimes(); + } + } + + private boolean checkReconsumeTimes(List msgs) { + boolean suspend = false; + if (msgs != null && !msgs.isEmpty()) { + for (MessageExt msg : msgs) { + if (msg.getReconsumeTimes() >= getMaxReconsumeTimes()) { + MessageAccessor.setReconsumeTime(msg, String.valueOf(msg.getReconsumeTimes())); + if (!sendMessageBack(msg)) { + suspend = true; + msg.setReconsumeTimes(msg.getReconsumeTimes() + 1); + } + } else { + suspend = true; + msg.setReconsumeTimes(msg.getReconsumeTimes() + 1); + } + } + } + return suspend; + } + + public boolean sendMessageBack(final MessageExt msg) { + try { + // max reconsume times exceeded then send to dead letter queue. + Message newMsg = new Message(MixAll.getRetryTopic(this.defaultMQPushConsumer.getConsumerGroup()), msg.getBody()); + MessageAccessor.setProperties(newMsg, msg.getProperties()); + String originMsgId = MessageAccessor.getOriginMessageId(msg); + MessageAccessor.setOriginMessageId(newMsg, UtilAll.isBlank(originMsgId) ? msg.getMsgId() : originMsgId); + newMsg.setFlag(msg.getFlag()); + MessageAccessor.putProperty(newMsg, MessageConst.PROPERTY_RETRY_TOPIC, msg.getTopic()); + MessageAccessor.setReconsumeTime(newMsg, String.valueOf(msg.getReconsumeTimes())); + MessageAccessor.setMaxReconsumeTimes(newMsg, String.valueOf(getMaxReconsumeTimes())); + newMsg.setDelayTimeLevel(3 + msg.getReconsumeTimes()); + + this.defaultMQPushConsumerImpl.getmQClientFactory().getDefaultMQProducer().send(newMsg); + return true; + } catch (Exception e) { + log.error("sendMessageBack exception, group: " + this.consumerGroup + " msg: " + msg.toString(), e); + } + + return false; + } + + public void resetNamespace(final List msgs) { + for (MessageExt msg : msgs) { + if (StringUtils.isNotEmpty(this.defaultMQPushConsumer.getNamespace())) { + msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(), this.defaultMQPushConsumer.getNamespace())); + } + } + } + + class ConsumeRequest implements Runnable { + private final PopProcessQueue processQueue; + private final MessageQueue messageQueue; + private int shardingKeyIndex = 0; + + public ConsumeRequest(PopProcessQueue processQueue, MessageQueue messageQueue) { + this.processQueue = processQueue; + this.messageQueue = messageQueue; + this.shardingKeyIndex = 0; + } + + public ConsumeRequest(PopProcessQueue processQueue, MessageQueue messageQueue, int shardingKeyIndex) { + this.processQueue = processQueue; + this.messageQueue = messageQueue; + this.shardingKeyIndex = shardingKeyIndex; + } + + public PopProcessQueue getProcessQueue() { + return processQueue; + } + + public MessageQueue getMessageQueue() { + return messageQueue; + } + + public int getShardingKeyIndex() { + return shardingKeyIndex; + } + + @Override + public void run() { + if (this.processQueue.isDropped()) { + log.warn("run, message queue not be able to consume, because it's dropped. {}", this.messageQueue); + ConsumeMessagePopOrderlyService.this.removeConsumeRequest(this); + return; + } + + // lock on sharding key index + final Object objLock = messageQueueLock.fetchLockObject(this.messageQueue, shardingKeyIndex); + } + + @Override + public int hashCode() { + int hash = shardingKeyIndex; + if (processQueue != null) { + hash += processQueue.hashCode() * 31; + } + if (messageQueue != null) { + hash += messageQueue.hashCode() * 31; + } + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + ConsumeRequest other = (ConsumeRequest) obj; + if (shardingKeyIndex != other.shardingKeyIndex) { + return false; + } + + if (processQueue != other.processQueue) { + return false; + } + + if (messageQueue == other.messageQueue) { + return true; + } + if (messageQueue != null && messageQueue.equals(other.messageQueue)) { + return true; + } + return false; + } + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageService.java b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageService.java index 0f6f3bb38af..ee684730aed 100644 --- a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageService.java +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageService.java @@ -19,12 +19,12 @@ import java.util.List; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.body.ConsumeMessageDirectlyResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; public interface ConsumeMessageService { void start(); - void shutdown(); + void shutdown(long awaitTerminateMillis); void updateCorePoolSize(int corePoolSize); @@ -41,4 +41,9 @@ void submitConsumeRequest( final ProcessQueue processQueue, final MessageQueue messageQueue, final boolean dispathToConsume); + + void submitPopConsumeRequest( + final List msgs, + final PopProcessQueue processQueue, + final MessageQueue messageQueue); } diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/DefaultLitePullConsumerImpl.java b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/DefaultLitePullConsumerImpl.java new file mode 100644 index 00000000000..9350970a07e --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/DefaultLitePullConsumerImpl.java @@ -0,0 +1,1310 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.impl.consumer; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.Validators; +import org.apache.rocketmq.client.consumer.DefaultLitePullConsumer; +import org.apache.rocketmq.client.consumer.MessageQueueListener; +import org.apache.rocketmq.client.consumer.MessageSelector; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.consumer.TopicMessageQueueChangeListener; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.store.LocalFileOffsetStore; +import org.apache.rocketmq.client.consumer.store.OffsetStore; +import org.apache.rocketmq.client.consumer.store.ReadOffsetType; +import org.apache.rocketmq.client.consumer.store.RemoteBrokerOffsetStore; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.hook.ConsumeMessageContext; +import org.apache.rocketmq.client.hook.ConsumeMessageHook; +import org.apache.rocketmq.client.hook.FilterMessageHook; +import org.apache.rocketmq.client.impl.CommunicationMode; +import org.apache.rocketmq.client.impl.MQClientManager; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.ServiceState; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.filter.ExpressionType; +import org.apache.rocketmq.common.help.FAQUrl; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.sysflag.PullSysFlag; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.body.ProcessQueueInfo; +import org.apache.rocketmq.remoting.protocol.filter.FilterAPI; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class DefaultLitePullConsumerImpl implements MQConsumerInner { + + private static final Logger log = LoggerFactory.getLogger(DefaultLitePullConsumerImpl.class); + + private final long consumerStartTimestamp = System.currentTimeMillis(); + + private final RPCHook rpcHook; + + private final ArrayList filterMessageHookList = new ArrayList<>(); + + private volatile ServiceState serviceState = ServiceState.CREATE_JUST; + + protected MQClientInstance mQClientFactory; + + private PullAPIWrapper pullAPIWrapper; + + private OffsetStore offsetStore; + + private RebalanceImpl rebalanceImpl = new RebalanceLitePullImpl(this); + + private enum SubscriptionType { + NONE, SUBSCRIBE, ASSIGN + } + + private static final String NOT_RUNNING_EXCEPTION_MESSAGE = "The consumer not running, please start it first."; + + private static final String SUBSCRIPTION_CONFLICT_EXCEPTION_MESSAGE = "Subscribe and assign are mutually exclusive."; + /** + * the type of subscription + */ + private SubscriptionType subscriptionType = SubscriptionType.NONE; + /** + * Delay some time when exception occur + */ + private long pullTimeDelayMillsWhenException = 1000; + /** + * Flow control interval when message cache is full + */ + private static final long PULL_TIME_DELAY_MILLS_WHEN_CACHE_FLOW_CONTROL = 50; + /** + * Flow control interval when broker return flow control + */ + private static final long PULL_TIME_DELAY_MILLS_WHEN_BROKER_FLOW_CONTROL = 20; + /** + * Delay some time when suspend pull service + */ + private static final long PULL_TIME_DELAY_MILLS_WHEN_PAUSE = 1000; + + private static final long PULL_TIME_DELAY_MILLS_ON_EXCEPTION = 3 * 1000; + + private ConcurrentHashMap topicToSubExpression = new ConcurrentHashMap<>(); + + private DefaultLitePullConsumer defaultLitePullConsumer; + + private final ConcurrentMap taskTable = + new ConcurrentHashMap<>(); + + private AssignedMessageQueue assignedMessageQueue = new AssignedMessageQueue(); + + private final BlockingQueue consumeRequestCache = new LinkedBlockingQueue<>(); + + private ScheduledThreadPoolExecutor scheduledThreadPoolExecutor; + + private final ScheduledExecutorService scheduledExecutorService; + + private Map topicMessageQueueChangeListenerMap = new HashMap<>(); + + private Map> messageQueuesForTopic = new HashMap<>(); + + private long consumeRequestFlowControlTimes = 0L; + + private long queueFlowControlTimes = 0L; + + private long queueMaxSpanFlowControlTimes = 0L; + + private long nextAutoCommitDeadline = -1L; + + private final MessageQueueLock messageQueueLock = new MessageQueueLock(); + + private final ArrayList consumeMessageHookList = new ArrayList<>(); + + // only for test purpose, will be modified by reflection in unit test. + @SuppressWarnings("FieldMayBeFinal") + private static boolean doNotUpdateTopicSubscribeInfoWhenSubscriptionChanged = false; + + public DefaultLitePullConsumerImpl(final DefaultLitePullConsumer defaultLitePullConsumer, final RPCHook rpcHook) { + this.defaultLitePullConsumer = defaultLitePullConsumer; + this.rpcHook = rpcHook; + this.scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor( + this.defaultLitePullConsumer.getPullThreadNums(), + new ThreadFactoryImpl("PullMsgThread-" + this.defaultLitePullConsumer.getConsumerGroup()) + ); + this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("MonitorMessageQueueChangeThread")); + this.pullTimeDelayMillsWhenException = defaultLitePullConsumer.getPullTimeDelayMillsWhenException(); + } + + public void registerConsumeMessageHook(final ConsumeMessageHook hook) { + this.consumeMessageHookList.add(hook); + log.info("register consumeMessageHook Hook, {}", hook.hookName()); + } + + public void executeHookBefore(final ConsumeMessageContext context) { + if (!this.consumeMessageHookList.isEmpty()) { + for (ConsumeMessageHook hook : this.consumeMessageHookList) { + try { + hook.consumeMessageBefore(context); + } catch (Throwable e) { + log.error("consumeMessageHook {} executeHookBefore exception", hook.hookName(), e); + } + } + } + } + + public void executeHookAfter(final ConsumeMessageContext context) { + if (!this.consumeMessageHookList.isEmpty()) { + for (ConsumeMessageHook hook : this.consumeMessageHookList) { + try { + hook.consumeMessageAfter(context); + } catch (Throwable e) { + log.error("consumeMessageHook {} executeHookAfter exception", hook.hookName(), e); + } + } + } + } + + private void checkServiceState() { + if (this.serviceState != ServiceState.RUNNING) { + throw new IllegalStateException(NOT_RUNNING_EXCEPTION_MESSAGE); + } + } + + public void updateNameServerAddr(String newAddresses) { + this.mQClientFactory.getMQClientAPIImpl().updateNameServerAddressList(newAddresses); + } + + private synchronized void setSubscriptionType(SubscriptionType type) { + if (this.subscriptionType == SubscriptionType.NONE) { + this.subscriptionType = type; + } else if (this.subscriptionType != type) { + throw new IllegalStateException(SUBSCRIPTION_CONFLICT_EXCEPTION_MESSAGE); + } + } + + private void updateAssignedMessageQueue(String topic, Set assignedMessageQueue) { + this.assignedMessageQueue.updateAssignedMessageQueue(topic, assignedMessageQueue); + } + + private void updatePullTask(String topic, Set mqNewSet) { + Iterator> it = this.taskTable.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry next = it.next(); + if (next.getKey().getTopic().equals(topic)) { + if (!mqNewSet.contains(next.getKey())) { + next.getValue().setCancelled(true); + it.remove(); + } + } + } + startPullTask(mqNewSet); + } + + class MessageQueueListenerImpl implements MessageQueueListener { + @Override + public void messageQueueChanged(String topic, Set mqAll, Set mqDivided) { + updateAssignQueueAndStartPullTask(topic, mqAll, mqDivided); + } + } + + public void updateAssignQueueAndStartPullTask(String topic, Set mqAll, Set mqDivided) { + MessageModel messageModel = defaultLitePullConsumer.getMessageModel(); + switch (messageModel) { + case BROADCASTING: + updateAssignedMessageQueue(topic, mqAll); + updatePullTask(topic, mqAll); + break; + case CLUSTERING: + updateAssignedMessageQueue(topic, mqDivided); + updatePullTask(topic, mqDivided); + break; + default: + break; + } + } + + public synchronized void shutdown() { + switch (this.serviceState) { + case CREATE_JUST: + break; + case RUNNING: + persistConsumerOffset(); + this.mQClientFactory.unregisterConsumer(this.defaultLitePullConsumer.getConsumerGroup()); + scheduledThreadPoolExecutor.shutdown(); + scheduledExecutorService.shutdown(); + this.mQClientFactory.shutdown(); + this.serviceState = ServiceState.SHUTDOWN_ALREADY; + log.info("the consumer [{}] shutdown OK", this.defaultLitePullConsumer.getConsumerGroup()); + break; + default: + break; + } + } + + public synchronized boolean isRunning() { + return this.serviceState == ServiceState.RUNNING; + } + + public synchronized void start() throws MQClientException { + switch (this.serviceState) { + case CREATE_JUST: + this.serviceState = ServiceState.START_FAILED; + + this.checkConfig(); + + if (this.defaultLitePullConsumer.getMessageModel() == MessageModel.CLUSTERING) { + this.defaultLitePullConsumer.changeInstanceNameToPID(); + } + + initMQClientFactory(); + + initRebalanceImpl(); + + initPullAPIWrapper(); + + initOffsetStore(); + + mQClientFactory.start(); + + startScheduleTask(); + + this.serviceState = ServiceState.RUNNING; + + log.info("the consumer [{}] start OK", this.defaultLitePullConsumer.getConsumerGroup()); + + operateAfterRunning(); + + break; + case RUNNING: + case START_FAILED: + case SHUTDOWN_ALREADY: + throw new MQClientException("The PullConsumer service state not OK, maybe started once, " + + this.serviceState + + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK), + null); + default: + break; + } + } + + private void initMQClientFactory() throws MQClientException { + this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultLitePullConsumer, this.rpcHook); + boolean registerOK = mQClientFactory.registerConsumer(this.defaultLitePullConsumer.getConsumerGroup(), this); + if (!registerOK) { + this.serviceState = ServiceState.CREATE_JUST; + + throw new MQClientException("The consumer group[" + this.defaultLitePullConsumer.getConsumerGroup() + + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL), + null); + } + } + + private void initRebalanceImpl() { + this.rebalanceImpl.setConsumerGroup(this.defaultLitePullConsumer.getConsumerGroup()); + this.rebalanceImpl.setMessageModel(this.defaultLitePullConsumer.getMessageModel()); + this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultLitePullConsumer.getAllocateMessageQueueStrategy()); + this.rebalanceImpl.setmQClientFactory(this.mQClientFactory); + } + + private void initPullAPIWrapper() { + this.pullAPIWrapper = new PullAPIWrapper( + mQClientFactory, + this.defaultLitePullConsumer.getConsumerGroup(), isUnitMode()); + this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList); + } + + private void initOffsetStore() throws MQClientException { + if (this.defaultLitePullConsumer.getOffsetStore() != null) { + this.offsetStore = this.defaultLitePullConsumer.getOffsetStore(); + } else { + switch (this.defaultLitePullConsumer.getMessageModel()) { + case BROADCASTING: + this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultLitePullConsumer.getConsumerGroup()); + break; + case CLUSTERING: + this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultLitePullConsumer.getConsumerGroup()); + break; + default: + break; + } + this.defaultLitePullConsumer.setOffsetStore(this.offsetStore); + } + this.offsetStore.load(); + } + + private void startScheduleTask() { + scheduledExecutorService.scheduleAtFixedRate( + new Runnable() { + @Override + public void run() { + try { + fetchTopicMessageQueuesAndCompare(); + } catch (Exception e) { + log.error("ScheduledTask fetchMessageQueuesAndCompare exception", e); + } + } + }, 1000 * 10, this.getDefaultLitePullConsumer().getTopicMetadataCheckIntervalMillis(), TimeUnit.MILLISECONDS); + } + + private void operateAfterRunning() throws MQClientException { + // If subscribe function invoke before start function, then update topic subscribe info after initialization. + if (subscriptionType == SubscriptionType.SUBSCRIBE) { + updateTopicSubscribeInfoWhenSubscriptionChanged(); + } + // If assign function invoke before start function, then update pull task after initialization. + if (subscriptionType == SubscriptionType.ASSIGN) { + updateAssignPullTask(assignedMessageQueue.getAssignedMessageQueues()); + } + + for (String topic : topicMessageQueueChangeListenerMap.keySet()) { + Set messageQueues = fetchMessageQueues(topic); + messageQueuesForTopic.put(topic, messageQueues); + } + this.mQClientFactory.checkClientInBroker(); + } + + private void checkConfig() throws MQClientException { + // Check consumerGroup + Validators.checkGroup(this.defaultLitePullConsumer.getConsumerGroup()); + + // Check consumerGroup name is not equal default consumer group name. + if (this.defaultLitePullConsumer.getConsumerGroup().equals(MixAll.DEFAULT_CONSUMER_GROUP)) { + throw new MQClientException( + "consumerGroup can not equal " + + MixAll.DEFAULT_CONSUMER_GROUP + + ", please specify another one." + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } + + // Check messageModel is not null. + if (null == this.defaultLitePullConsumer.getMessageModel()) { + throw new MQClientException( + "messageModel is null" + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } + + // Check allocateMessageQueueStrategy is not null + if (null == this.defaultLitePullConsumer.getAllocateMessageQueueStrategy()) { + throw new MQClientException( + "allocateMessageQueueStrategy is null" + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } + + if (this.defaultLitePullConsumer.getConsumerTimeoutMillisWhenSuspend() < this.defaultLitePullConsumer.getBrokerSuspendMaxTimeMillis()) { + throw new MQClientException( + "Long polling mode, the consumer consumerTimeoutMillisWhenSuspend must greater than brokerSuspendMaxTimeMillis" + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } + } + + public PullAPIWrapper getPullAPIWrapper() { + return pullAPIWrapper; + } + + private void startPullTask(Collection mqSet) { + for (MessageQueue messageQueue : mqSet) { + if (!this.taskTable.containsKey(messageQueue)) { + PullTaskImpl pullTask = new PullTaskImpl(messageQueue); + this.taskTable.put(messageQueue, pullTask); + this.scheduledThreadPoolExecutor.schedule(pullTask, 0, TimeUnit.MILLISECONDS); + } + } + } + + private void updateAssignPullTask(Collection mqNewSet) { + Iterator> it = this.taskTable.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry next = it.next(); + if (!mqNewSet.contains(next.getKey())) { + next.getValue().setCancelled(true); + it.remove(); + } + } + + startPullTask(mqNewSet); + } + + private void updateTopicSubscribeInfoWhenSubscriptionChanged() { + if (doNotUpdateTopicSubscribeInfoWhenSubscriptionChanged) { + return; + } + Map subTable = rebalanceImpl.getSubscriptionInner(); + if (subTable != null) { + for (final Map.Entry entry : subTable.entrySet()) { + final String topic = entry.getKey(); + this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic); + } + } + } + + /** + * subscribe data by customizing messageQueueListener + * + * @param topic + * @param subExpression + * @param messageQueueListener + * @throws MQClientException + */ + public synchronized void subscribe(String topic, String subExpression, + MessageQueueListener messageQueueListener) throws MQClientException { + try { + if (StringUtils.isEmpty(topic)) { + throw new IllegalArgumentException("Topic can not be null or empty."); + } + setSubscriptionType(SubscriptionType.SUBSCRIBE); + SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(topic, subExpression); + this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData); + this.defaultLitePullConsumer.setMessageQueueListener(new MessageQueueListener() { + @Override + public void messageQueueChanged(String topic, Set mqAll, Set mqDivided) { + // First, update the assign queue + updateAssignQueueAndStartPullTask(topic, mqAll, mqDivided); + // run custom listener + messageQueueListener.messageQueueChanged(topic, mqAll, mqDivided); + } + }); + assignedMessageQueue.setRebalanceImpl(this.rebalanceImpl); + if (serviceState == ServiceState.RUNNING) { + this.mQClientFactory.sendHeartbeatToAllBrokerWithLock(); + updateTopicSubscribeInfoWhenSubscriptionChanged(); + } + } catch (Exception e) { + throw new MQClientException("subscribe exception", e); + } + } + + public synchronized void subscribe(String topic, String subExpression) throws MQClientException { + try { + if (topic == null || "".equals(topic)) { + throw new IllegalArgumentException("Topic can not be null or empty."); + } + setSubscriptionType(SubscriptionType.SUBSCRIBE); + SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(topic, subExpression); + this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData); + this.defaultLitePullConsumer.setMessageQueueListener(new MessageQueueListenerImpl()); + assignedMessageQueue.setRebalanceImpl(this.rebalanceImpl); + if (serviceState == ServiceState.RUNNING) { + this.mQClientFactory.sendHeartbeatToAllBrokerWithLock(); + updateTopicSubscribeInfoWhenSubscriptionChanged(); + } + } catch (Exception e) { + throw new MQClientException("subscribe exception", e); + } + } + + public synchronized void subscribe(String topic, MessageSelector messageSelector) throws MQClientException { + try { + if (topic == null || "".equals(topic)) { + throw new IllegalArgumentException("Topic can not be null or empty."); + } + setSubscriptionType(SubscriptionType.SUBSCRIBE); + if (messageSelector == null) { + subscribe(topic, SubscriptionData.SUB_ALL); + return; + } + SubscriptionData subscriptionData = FilterAPI.build(topic, + messageSelector.getExpression(), messageSelector.getExpressionType()); + this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData); + this.defaultLitePullConsumer.setMessageQueueListener(new MessageQueueListenerImpl()); + assignedMessageQueue.setRebalanceImpl(this.rebalanceImpl); + if (serviceState == ServiceState.RUNNING) { + this.mQClientFactory.sendHeartbeatToAllBrokerWithLock(); + updateTopicSubscribeInfoWhenSubscriptionChanged(); + } + } catch (Exception e) { + throw new MQClientException("subscribe exception", e); + } + } + + public synchronized void unsubscribe(final String topic) { + this.rebalanceImpl.getSubscriptionInner().remove(topic); + removePullTaskCallback(topic); + assignedMessageQueue.removeAssignedMessageQueue(topic); + } + + public synchronized void assign(Collection messageQueues) { + if (messageQueues == null || messageQueues.isEmpty()) { + throw new IllegalArgumentException("Message queues can not be null or empty."); + } + setSubscriptionType(SubscriptionType.ASSIGN); + assignedMessageQueue.updateAssignedMessageQueue(messageQueues); + if (serviceState == ServiceState.RUNNING) { + updateAssignPullTask(messageQueues); + } + } + + public synchronized void setSubExpressionForAssign(final String topic, final String subExpression) { + if (StringUtils.isBlank(subExpression)) { + throw new IllegalArgumentException("subExpression can not be null or empty."); + } + if (serviceState != ServiceState.CREATE_JUST) { + throw new IllegalStateException("setAssignTag only can be called before start."); + } + setSubscriptionType(SubscriptionType.ASSIGN); + topicToSubExpression.put(topic, subExpression); + } + + private void maybeAutoCommit() { + long now = System.currentTimeMillis(); + if (now >= nextAutoCommitDeadline) { + commitAll(); + nextAutoCommitDeadline = now + defaultLitePullConsumer.getAutoCommitIntervalMillis(); + } + } + + public synchronized List poll(long timeout) { + try { + checkServiceState(); + if (timeout < 0) { + throw new IllegalArgumentException("Timeout must not be negative"); + } + + if (defaultLitePullConsumer.isAutoCommit()) { + maybeAutoCommit(); + } + long endTime = System.currentTimeMillis() + timeout; + + ConsumeRequest consumeRequest = consumeRequestCache.poll(endTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + + if (endTime - System.currentTimeMillis() > 0) { + while (consumeRequest != null && consumeRequest.getProcessQueue().isDropped()) { + consumeRequest = consumeRequestCache.poll(endTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + if (endTime - System.currentTimeMillis() <= 0) { + break; + } + } + } + + if (consumeRequest != null && !consumeRequest.getProcessQueue().isDropped()) { + List messages = consumeRequest.getMessageExts(); + long offset = consumeRequest.getProcessQueue().removeMessage(messages); + assignedMessageQueue.updateConsumeOffset(consumeRequest.getMessageQueue(), offset); + //If namespace not null , reset Topic without namespace. + this.resetTopic(messages); + if (!this.consumeMessageHookList.isEmpty()) { + ConsumeMessageContext consumeMessageContext = new ConsumeMessageContext(); + consumeMessageContext.setNamespace(defaultLitePullConsumer.getNamespace()); + consumeMessageContext.setConsumerGroup(this.groupName()); + consumeMessageContext.setMq(consumeRequest.getMessageQueue()); + consumeMessageContext.setMsgList(messages); + consumeMessageContext.setSuccess(false); + this.executeHookBefore(consumeMessageContext); + consumeMessageContext.setStatus(ConsumeConcurrentlyStatus.CONSUME_SUCCESS.toString()); + consumeMessageContext.setSuccess(true); + consumeMessageContext.setAccessChannel(defaultLitePullConsumer.getAccessChannel()); + this.executeHookAfter(consumeMessageContext); + } + consumeRequest.getProcessQueue().setLastConsumeTimestamp(System.currentTimeMillis()); + return messages; + } + } catch (InterruptedException ignore) { + + } + + return Collections.emptyList(); + } + + public void pause(Collection messageQueues) { + assignedMessageQueue.pause(messageQueues); + } + + public void resume(Collection messageQueues) { + assignedMessageQueue.resume(messageQueues); + } + + public synchronized void seek(MessageQueue messageQueue, long offset) throws MQClientException { + if (!assignedMessageQueue.getAssignedMessageQueues().contains(messageQueue)) { + if (subscriptionType == SubscriptionType.SUBSCRIBE) { + throw new MQClientException("The message queue is not in assigned list, may be rebalancing, message queue: " + messageQueue, null); + } else { + throw new MQClientException("The message queue is not in assigned list, message queue: " + messageQueue, null); + } + } + long minOffset = minOffset(messageQueue); + long maxOffset = maxOffset(messageQueue); + if (offset < minOffset || offset > maxOffset) { + throw new MQClientException("Seek offset illegal, seek offset = " + offset + ", min offset = " + minOffset + ", max offset = " + maxOffset, null); + } + final Object objLock = messageQueueLock.fetchLockObject(messageQueue); + synchronized (objLock) { + clearMessageQueueInCache(messageQueue); + + PullTaskImpl oldPullTaskImpl = this.taskTable.get(messageQueue); + if (oldPullTaskImpl != null) { + oldPullTaskImpl.tryInterrupt(); + this.taskTable.remove(messageQueue); + } + assignedMessageQueue.setSeekOffset(messageQueue, offset); + if (!this.taskTable.containsKey(messageQueue)) { + PullTaskImpl pullTask = new PullTaskImpl(messageQueue); + this.taskTable.put(messageQueue, pullTask); + this.scheduledThreadPoolExecutor.schedule(pullTask, 0, TimeUnit.MILLISECONDS); + } + } + } + + public void seekToBegin(MessageQueue messageQueue) throws MQClientException { + long begin = minOffset(messageQueue); + this.seek(messageQueue, begin); + } + + public void seekToEnd(MessageQueue messageQueue) throws MQClientException { + long end = maxOffset(messageQueue); + this.seek(messageQueue, end); + } + + private long maxOffset(MessageQueue messageQueue) throws MQClientException { + checkServiceState(); + return this.mQClientFactory.getMQAdminImpl().maxOffset(messageQueue); + } + + private long minOffset(MessageQueue messageQueue) throws MQClientException { + checkServiceState(); + return this.mQClientFactory.getMQAdminImpl().minOffset(messageQueue); + } + + private void removePullTaskCallback(final String topic) { + removePullTask(topic); + } + + private void removePullTask(final String topic) { + Iterator> it = this.taskTable.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry next = it.next(); + if (next.getKey().getTopic().equals(topic)) { + next.getValue().setCancelled(true); + it.remove(); + } + } + } + + public synchronized void commitAll() { + for (MessageQueue messageQueue : assignedMessageQueue.getAssignedMessageQueues()) { + try { + commit(messageQueue); + } catch (Exception e) { + log.error("An error occurred when update consume offset Automatically."); + } + } + } + + /** + * Specify offset commit + * + * @param messageQueues + * @param persist + */ + public synchronized void commit(final Map messageQueues, boolean persist) { + if (messageQueues == null || messageQueues.size() == 0) { + log.warn("MessageQueues is empty, Ignore this commit "); + return; + } + for (Map.Entry messageQueueEntry : messageQueues.entrySet()) { + MessageQueue messageQueue = messageQueueEntry.getKey(); + long offset = messageQueueEntry.getValue(); + if (offset != -1) { + ProcessQueue processQueue = assignedMessageQueue.getProcessQueue(messageQueue); + if (processQueue != null && !processQueue.isDropped()) { + updateConsumeOffset(messageQueue, offset); + } + } else { + log.error("consumerOffset is -1 in messageQueue [" + messageQueue + "]."); + } + } + + if (persist) { + this.offsetStore.persistAll(messageQueues.keySet()); + } + } + + /** + * Get the queue assigned in subscribe mode + * + * @return + */ + public synchronized Set assignment() { + return assignedMessageQueue.getAssignedMessageQueues(); + } + + public synchronized void commit(final Set messageQueues, boolean persist) { + if (messageQueues == null || messageQueues.size() == 0) { + return; + } + + for (MessageQueue messageQueue : messageQueues) { + commit(messageQueue); + } + + if (persist) { + this.offsetStore.persistAll(messageQueues); + } + } + + private synchronized void commit(MessageQueue messageQueue) { + long consumerOffset = assignedMessageQueue.getConsumerOffset(messageQueue); + + if (consumerOffset != -1) { + ProcessQueue processQueue = assignedMessageQueue.getProcessQueue(messageQueue); + if (processQueue != null && !processQueue.isDropped()) { + updateConsumeOffset(messageQueue, consumerOffset); + } + } else { + log.error("consumerOffset is -1 in messageQueue [" + messageQueue + "]."); + } + } + + private void updatePullOffset(MessageQueue messageQueue, long nextPullOffset, ProcessQueue processQueue) { + if (assignedMessageQueue.getSeekOffset(messageQueue) == -1) { + assignedMessageQueue.updatePullOffset(messageQueue, nextPullOffset, processQueue); + } + } + + private void submitConsumeRequest(ConsumeRequest consumeRequest) { + try { + consumeRequestCache.put(consumeRequest); + } catch (InterruptedException e) { + log.error("Submit consumeRequest error", e); + } + } + + private long fetchConsumeOffset(MessageQueue messageQueue) throws MQClientException { + checkServiceState(); + long offset = this.rebalanceImpl.computePullFromWhereWithException(messageQueue); + return offset; + } + + public long committed(MessageQueue messageQueue) throws MQClientException { + checkServiceState(); + long offset = this.offsetStore.readOffset(messageQueue, ReadOffsetType.MEMORY_FIRST_THEN_STORE); + if (offset == -2) { + throw new MQClientException("Fetch consume offset from broker exception", null); + } + return offset; + } + + private void clearMessageQueueInCache(MessageQueue messageQueue) { + ProcessQueue processQueue = assignedMessageQueue.getProcessQueue(messageQueue); + if (processQueue != null) { + processQueue.clear(); + } + Iterator iter = consumeRequestCache.iterator(); + while (iter.hasNext()) { + if (iter.next().getMessageQueue().equals(messageQueue)) { + iter.remove(); + } + } + } + + private long nextPullOffset(MessageQueue messageQueue) throws MQClientException { + long offset = -1; + long seekOffset = assignedMessageQueue.getSeekOffset(messageQueue); + if (seekOffset != -1) { + offset = seekOffset; + assignedMessageQueue.updateConsumeOffset(messageQueue, offset); + assignedMessageQueue.setSeekOffset(messageQueue, -1); + } else { + offset = assignedMessageQueue.getPullOffset(messageQueue); + if (offset == -1) { + offset = fetchConsumeOffset(messageQueue); + } + } + return offset; + } + + public long searchOffset(MessageQueue mq, long timestamp) throws MQClientException { + checkServiceState(); + return this.mQClientFactory.getMQAdminImpl().searchOffset(mq, timestamp); + } + + public class PullTaskImpl implements Runnable { + private final MessageQueue messageQueue; + private volatile boolean cancelled = false; + private Thread currentThread; + + public PullTaskImpl(final MessageQueue messageQueue) { + this.messageQueue = messageQueue; + } + + public void tryInterrupt() { + setCancelled(true); + if (currentThread == null) { + return; + } + if (!currentThread.isInterrupted()) { + currentThread.interrupt(); + } + } + + @Override + public void run() { + + if (!this.isCancelled()) { + + this.currentThread = Thread.currentThread(); + + if (assignedMessageQueue.isPaused(messageQueue)) { + scheduledThreadPoolExecutor.schedule(this, PULL_TIME_DELAY_MILLS_WHEN_PAUSE, TimeUnit.MILLISECONDS); + log.debug("Message Queue: {} has been paused!", messageQueue); + return; + } + + ProcessQueue processQueue = assignedMessageQueue.getProcessQueue(messageQueue); + + if (null == processQueue || processQueue.isDropped()) { + log.info("The message queue not be able to poll, because it's dropped. group={}, messageQueue={}", defaultLitePullConsumer.getConsumerGroup(), this.messageQueue); + return; + } + + processQueue.setLastPullTimestamp(System.currentTimeMillis()); + + if ((long) consumeRequestCache.size() * defaultLitePullConsumer.getPullBatchSize() > defaultLitePullConsumer.getPullThresholdForAll()) { + scheduledThreadPoolExecutor.schedule(this, PULL_TIME_DELAY_MILLS_WHEN_CACHE_FLOW_CONTROL, TimeUnit.MILLISECONDS); + if ((consumeRequestFlowControlTimes++ % 1000) == 0) { + log.warn("The consume request count exceeds threshold {}, so do flow control, consume request count={}, flowControlTimes={}", consumeRequestCache.size(), consumeRequestFlowControlTimes); + } + return; + } + + long cachedMessageCount = processQueue.getMsgCount().get(); + long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024); + + if (cachedMessageCount > defaultLitePullConsumer.getPullThresholdForQueue()) { + scheduledThreadPoolExecutor.schedule(this, PULL_TIME_DELAY_MILLS_WHEN_CACHE_FLOW_CONTROL, TimeUnit.MILLISECONDS); + if ((queueFlowControlTimes++ % 1000) == 0) { + log.warn( + "The cached message count exceeds the threshold {}, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, flowControlTimes={}", + defaultLitePullConsumer.getPullThresholdForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, queueFlowControlTimes); + } + return; + } + + if (cachedMessageSizeInMiB > defaultLitePullConsumer.getPullThresholdSizeForQueue()) { + scheduledThreadPoolExecutor.schedule(this, PULL_TIME_DELAY_MILLS_WHEN_CACHE_FLOW_CONTROL, TimeUnit.MILLISECONDS); + if ((queueFlowControlTimes++ % 1000) == 0) { + log.warn( + "The cached message size exceeds the threshold {} MiB, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, flowControlTimes={}", + defaultLitePullConsumer.getPullThresholdSizeForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, queueFlowControlTimes); + } + return; + } + + if (processQueue.getMaxSpan() > defaultLitePullConsumer.getConsumeMaxSpan()) { + scheduledThreadPoolExecutor.schedule(this, PULL_TIME_DELAY_MILLS_WHEN_CACHE_FLOW_CONTROL, TimeUnit.MILLISECONDS); + if ((queueMaxSpanFlowControlTimes++ % 1000) == 0) { + log.warn( + "The queue's messages, span too long, so do flow control, minOffset={}, maxOffset={}, maxSpan={}, flowControlTimes={}", + processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), processQueue.getMaxSpan(), queueMaxSpanFlowControlTimes); + } + return; + } + + long offset = 0L; + try { + offset = nextPullOffset(messageQueue); + } catch (Exception e) { + log.error("Failed to get next pull offset", e); + scheduledThreadPoolExecutor.schedule(this, PULL_TIME_DELAY_MILLS_ON_EXCEPTION, TimeUnit.MILLISECONDS); + return; + } + + if (this.isCancelled() || processQueue.isDropped()) { + return; + } + long pullDelayTimeMills = 0; + try { + SubscriptionData subscriptionData; + String topic = this.messageQueue.getTopic(); + if (subscriptionType == SubscriptionType.SUBSCRIBE) { + subscriptionData = rebalanceImpl.getSubscriptionInner().get(topic); + } else { + String subExpression4Assign = topicToSubExpression.get(topic); + subExpression4Assign = subExpression4Assign == null ? SubscriptionData.SUB_ALL : subExpression4Assign; + subscriptionData = FilterAPI.buildSubscriptionData(topic, subExpression4Assign); + } + + PullResult pullResult = pull(messageQueue, subscriptionData, offset, defaultLitePullConsumer.getPullBatchSize()); + if (this.isCancelled() || processQueue.isDropped()) { + return; + } + switch (pullResult.getPullStatus()) { + case FOUND: + final Object objLock = messageQueueLock.fetchLockObject(messageQueue); + synchronized (objLock) { + if (pullResult.getMsgFoundList() != null && !pullResult.getMsgFoundList().isEmpty() && assignedMessageQueue.getSeekOffset(messageQueue) == -1) { + processQueue.putMessage(pullResult.getMsgFoundList()); + submitConsumeRequest(new ConsumeRequest(pullResult.getMsgFoundList(), messageQueue, processQueue)); + } + } + break; + case OFFSET_ILLEGAL: + log.warn("The pull request offset illegal, {}", pullResult.toString()); + break; + default: + break; + } + updatePullOffset(messageQueue, pullResult.getNextBeginOffset(), processQueue); + } catch (InterruptedException interruptedException) { + log.warn("Polling thread was interrupted.", interruptedException); + } catch (Throwable e) { + if (e instanceof MQBrokerException && ((MQBrokerException) e).getResponseCode() == ResponseCode.FLOW_CONTROL) { + pullDelayTimeMills = PULL_TIME_DELAY_MILLS_WHEN_BROKER_FLOW_CONTROL; + } else { + pullDelayTimeMills = pullTimeDelayMillsWhenException; + } + log.error("An error occurred in pull message process.", e); + } + + if (!this.isCancelled()) { + scheduledThreadPoolExecutor.schedule(this, pullDelayTimeMills, TimeUnit.MILLISECONDS); + } else { + log.warn("The Pull Task is cancelled after doPullTask, {}", messageQueue); + } + } + } + + public boolean isCancelled() { + return cancelled; + } + + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } + + public MessageQueue getMessageQueue() { + return messageQueue; + } + } + + private PullResult pull(MessageQueue mq, SubscriptionData subscriptionData, long offset, int maxNums) + throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + return pull(mq, subscriptionData, offset, maxNums, this.defaultLitePullConsumer.getConsumerPullTimeoutMillis()); + } + + private PullResult pull(MessageQueue mq, SubscriptionData subscriptionData, long offset, int maxNums, long timeout) + throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + return this.pullSyncImpl(mq, subscriptionData, offset, maxNums, true, timeout); + } + + private PullResult pullSyncImpl(MessageQueue mq, SubscriptionData subscriptionData, long offset, int maxNums, + boolean block, + long timeout) + throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + + if (null == mq) { + throw new MQClientException("mq is null", null); + } + + if (offset < 0) { + throw new MQClientException("offset < 0", null); + } + + if (maxNums <= 0) { + throw new MQClientException("maxNums <= 0", null); + } + + int sysFlag = PullSysFlag.buildSysFlag(false, block, true, false, true); + + long timeoutMillis = block ? this.defaultLitePullConsumer.getConsumerTimeoutMillisWhenSuspend() : timeout; + + boolean isTagType = ExpressionType.isTagType(subscriptionData.getExpressionType()); + PullResult pullResult = this.pullAPIWrapper.pullKernelImpl( + mq, + subscriptionData.getSubString(), + subscriptionData.getExpressionType(), + isTagType ? 0L : subscriptionData.getSubVersion(), + offset, + maxNums, + sysFlag, + 0, + this.defaultLitePullConsumer.getBrokerSuspendMaxTimeMillis(), + timeoutMillis, + CommunicationMode.SYNC, + null + ); + this.pullAPIWrapper.processPullResult(mq, pullResult, subscriptionData); + return pullResult; + } + + private void resetTopic(List msgList) { + if (null == msgList || msgList.size() == 0) { + return; + } + + //If namespace not null , reset Topic without namespace. + for (MessageExt messageExt : msgList) { + if (null != this.defaultLitePullConsumer.getNamespace()) { + messageExt.setTopic(NamespaceUtil.withoutNamespace(messageExt.getTopic(), this.defaultLitePullConsumer.getNamespace())); + } + } + + } + + public void updateConsumeOffset(MessageQueue mq, long offset) { + checkServiceState(); + this.offsetStore.updateOffset(mq, offset, false); + } + + @Override + public String groupName() { + return this.defaultLitePullConsumer.getConsumerGroup(); + } + + @Override + public MessageModel messageModel() { + return this.defaultLitePullConsumer.getMessageModel(); + } + + @Override + public ConsumeType consumeType() { + return ConsumeType.CONSUME_ACTIVELY; + } + + @Override + public ConsumeFromWhere consumeFromWhere() { + return ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET; + } + + @Override + public Set subscriptions() { + Set subSet = new HashSet<>(); + + subSet.addAll(this.rebalanceImpl.getSubscriptionInner().values()); + + return subSet; + } + + @Override + public void doRebalance() { + if (this.rebalanceImpl != null) { + this.rebalanceImpl.doRebalance(false); + } + } + + @Override + public boolean tryRebalance() { + if (this.rebalanceImpl != null) { + return this.rebalanceImpl.doRebalance(false); + } + return false; + } + + @Override + public void persistConsumerOffset() { + try { + checkServiceState(); + Set mqs = new HashSet<>(); + if (this.subscriptionType == SubscriptionType.SUBSCRIBE) { + Set allocateMq = this.rebalanceImpl.getProcessQueueTable().keySet(); + mqs.addAll(allocateMq); + } else if (this.subscriptionType == SubscriptionType.ASSIGN) { + Set assignedMessageQueue = this.assignedMessageQueue.getAssignedMessageQueues(); + mqs.addAll(assignedMessageQueue); + } + this.offsetStore.persistAll(mqs); + } catch (Exception e) { + log.error("Persist consumer offset error for group: {} ", this.defaultLitePullConsumer.getConsumerGroup(), e); + } + } + + @Override + public void updateTopicSubscribeInfo(String topic, Set info) { + Map subTable = this.rebalanceImpl.getSubscriptionInner(); + if (subTable != null) { + if (subTable.containsKey(topic)) { + this.rebalanceImpl.getTopicSubscribeInfoTable().put(topic, info); + } + } + } + + @Override + public boolean isSubscribeTopicNeedUpdate(String topic) { + Map subTable = this.rebalanceImpl.getSubscriptionInner(); + if (subTable != null) { + if (subTable.containsKey(topic)) { + return !this.rebalanceImpl.topicSubscribeInfoTable.containsKey(topic); + } + } + + return false; + } + + @Override + public boolean isUnitMode() { + return this.defaultLitePullConsumer.isUnitMode(); + } + + @Override + public ConsumerRunningInfo consumerRunningInfo() { + ConsumerRunningInfo info = new ConsumerRunningInfo(); + + Properties prop = MixAll.object2Properties(this.defaultLitePullConsumer); + prop.put(ConsumerRunningInfo.PROP_CONSUMER_START_TIMESTAMP, String.valueOf(this.consumerStartTimestamp)); + info.setProperties(prop); + + info.getSubscriptionSet().addAll(this.subscriptions()); + + for (MessageQueue mq : this.assignedMessageQueue.getAssignedMessageQueues()) { + ProcessQueue pq = this.assignedMessageQueue.getProcessQueue(mq); + ProcessQueueInfo pqInfo = new ProcessQueueInfo(); + pqInfo.setCommitOffset(this.offsetStore.readOffset(mq, ReadOffsetType.MEMORY_FIRST_THEN_STORE)); + pq.fillProcessQueueInfo(pqInfo); + info.getMqTable().put(mq, pqInfo); + } + + return info; + } + + private void updateConsumeOffsetToBroker(MessageQueue mq, long offset, boolean isOneway) throws RemotingException, + MQBrokerException, InterruptedException, MQClientException { + this.offsetStore.updateConsumeOffsetToBroker(mq, offset, isOneway); + } + + public OffsetStore getOffsetStore() { + return offsetStore; + } + + public DefaultLitePullConsumer getDefaultLitePullConsumer() { + return defaultLitePullConsumer; + } + + public Set fetchMessageQueues(String topic) throws MQClientException { + checkServiceState(); + Set result = this.mQClientFactory.getMQAdminImpl().fetchSubscribeMessageQueues(topic); + return parseMessageQueues(result); + } + + private synchronized void fetchTopicMessageQueuesAndCompare() throws MQClientException { + for (Map.Entry entry : topicMessageQueueChangeListenerMap.entrySet()) { + String topic = entry.getKey(); + TopicMessageQueueChangeListener topicMessageQueueChangeListener = entry.getValue(); + Set oldMessageQueues = messageQueuesForTopic.get(topic); + Set newMessageQueues = fetchMessageQueues(topic); + boolean isChanged = !isSetEqual(newMessageQueues, oldMessageQueues); + if (isChanged) { + messageQueuesForTopic.put(topic, newMessageQueues); + if (topicMessageQueueChangeListener != null) { + topicMessageQueueChangeListener.onChanged(topic, newMessageQueues); + } + } + } + } + + private boolean isSetEqual(Set set1, Set set2) { + if (set1 == null && set2 == null) { + return true; + } + + if (set1 == null || set2 == null || set1.size() != set2.size() || set1.size() == 0) { + return false; + } + + Iterator iter = set2.iterator(); + boolean isEqual = true; + while (iter.hasNext()) { + if (!set1.contains(iter.next())) { + isEqual = false; + } + } + return isEqual; + } + + public AssignedMessageQueue getAssignedMessageQueue() { + return assignedMessageQueue; + } + + public synchronized void registerTopicMessageQueueChangeListener(String topic, + TopicMessageQueueChangeListener listener) throws MQClientException { + if (topic == null || listener == null) { + throw new MQClientException("Topic or listener is null", null); + } + if (topicMessageQueueChangeListenerMap.containsKey(topic)) { + log.warn("Topic {} had been registered, new listener will overwrite the old one", topic); + } + topicMessageQueueChangeListenerMap.put(topic, listener); + if (this.serviceState == ServiceState.RUNNING) { + Set messageQueues = fetchMessageQueues(topic); + messageQueuesForTopic.put(topic, messageQueues); + } + } + + private Set parseMessageQueues(Set queueSet) { + Set resultQueues = new HashSet<>(); + for (MessageQueue messageQueue : queueSet) { + String userTopic = NamespaceUtil.withoutNamespace(messageQueue.getTopic(), + this.defaultLitePullConsumer.getNamespace()); + resultQueues.add(new MessageQueue(userTopic, messageQueue.getBrokerName(), messageQueue.getQueueId())); + } + return resultQueues; + } + + public class ConsumeRequest { + private final List messageExts; + private final MessageQueue messageQueue; + private final ProcessQueue processQueue; + + public ConsumeRequest(final List messageExts, final MessageQueue messageQueue, + final ProcessQueue processQueue) { + this.messageExts = messageExts; + this.messageQueue = messageQueue; + this.processQueue = processQueue; + } + + public List getMessageExts() { + return messageExts; + } + + public MessageQueue getMessageQueue() { + return messageQueue; + } + + public ProcessQueue getProcessQueue() { + return processQueue; + } + + } + + public void setPullTimeDelayMillsWhenException(long pullTimeDelayMillsWhenException) { + this.pullTimeDelayMillsWhenException = pullTimeDelayMillsWhenException; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/DefaultMQPullConsumerImpl.java b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/DefaultMQPullConsumerImpl.java index 6eca38184e3..f5d326071d2 100644 --- a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/DefaultMQPullConsumerImpl.java +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/DefaultMQPullConsumerImpl.java @@ -26,6 +26,7 @@ import org.apache.rocketmq.client.QueryResult; import org.apache.rocketmq.client.Validators; import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +import org.apache.rocketmq.client.consumer.MessageSelector; import org.apache.rocketmq.client.consumer.PullCallback; import org.apache.rocketmq.client.consumer.PullResult; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; @@ -41,37 +42,44 @@ import org.apache.rocketmq.client.impl.CommunicationMode; import org.apache.rocketmq.client.impl.MQClientManager; import org.apache.rocketmq.client.impl.factory.MQClientInstance; -import org.apache.rocketmq.client.log.ClientLogger; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.ServiceState; import org.apache.rocketmq.common.UtilAll; import org.apache.rocketmq.common.consumer.ConsumeFromWhere; -import org.apache.rocketmq.common.filter.FilterAPI; +import org.apache.rocketmq.common.filter.ExpressionType; import org.apache.rocketmq.common.help.FAQUrl; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageAccessor; import org.apache.rocketmq.common.message.MessageConst; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.body.ConsumerRunningInfo; -import org.apache.rocketmq.common.protocol.heartbeat.ConsumeType; -import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; import org.apache.rocketmq.common.sysflag.PullSysFlag; import org.apache.rocketmq.remoting.RPCHook; import org.apache.rocketmq.remoting.common.RemotingHelper; import org.apache.rocketmq.remoting.exception.RemotingException; -import org.slf4j.Logger; - +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.filter.FilterAPI; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +/** + * This class will be removed in 2022, and a better implementation {@link DefaultLitePullConsumerImpl} is recommend to use + * in the scenario of actively pulling messages. + */ +@Deprecated public class DefaultMQPullConsumerImpl implements MQConsumerInner { - private final Logger log = ClientLogger.getLog(); + private static final Logger log = LoggerFactory.getLogger(DefaultMQPullConsumerImpl.class); private final DefaultMQPullConsumer defaultMQPullConsumer; private final long consumerStartTimestamp = System.currentTimeMillis(); private final RPCHook rpcHook; - private final ArrayList consumeMessageHookList = new ArrayList(); - private final ArrayList filterMessageHookList = new ArrayList(); + private final ArrayList consumeMessageHookList = new ArrayList<>(); + private final ArrayList filterMessageHookList = new ArrayList<>(); private volatile ServiceState serviceState = ServiceState.CREATE_JUST; - private MQClientInstance mQClientFactory; + protected MQClientInstance mQClientFactory; private PullAPIWrapper pullAPIWrapper; private OffsetStore offsetStore; private RebalanceImpl rebalanceImpl = new RebalancePullImpl(this); @@ -91,13 +99,13 @@ public void createTopic(String key, String newTopic, int queueNum) throws MQClie } public void createTopic(String key, String newTopic, int queueNum, int topicSysFlag) throws MQClientException { - this.makeSureStateOK(); - this.mQClientFactory.getMQAdminImpl().createTopic(key, newTopic, queueNum, topicSysFlag); + this.isRunning(); + this.mQClientFactory.getMQAdminImpl().createTopic(key, newTopic, queueNum, topicSysFlag, null); } - private void makeSureStateOK() throws MQClientException { + private void isRunning() throws MQClientException { if (this.serviceState != ServiceState.RUNNING) { - throw new MQClientException("The consumer service state not OK, " + throw new MQClientException("The consumer is not in running status, " + this.serviceState + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK), null); @@ -105,49 +113,65 @@ private void makeSureStateOK() throws MQClientException { } public long fetchConsumeOffset(MessageQueue mq, boolean fromStore) throws MQClientException { - this.makeSureStateOK(); + this.isRunning(); return this.offsetStore.readOffset(mq, fromStore ? ReadOffsetType.READ_FROM_STORE : ReadOffsetType.MEMORY_FIRST_THEN_STORE); } public Set fetchMessageQueuesInBalance(String topic) throws MQClientException { - this.makeSureStateOK(); + this.isRunning(); if (null == topic) { throw new IllegalArgumentException("topic is null"); } ConcurrentMap mqTable = this.rebalanceImpl.getProcessQueueTable(); - Set mqResult = new HashSet(); + Set mqResult = new HashSet<>(); for (MessageQueue mq : mqTable.keySet()) { if (mq.getTopic().equals(topic)) { mqResult.add(mq); } } - return mqResult; + return parseSubscribeMessageQueues(mqResult); } public List fetchPublishMessageQueues(String topic) throws MQClientException { - this.makeSureStateOK(); + this.isRunning(); return this.mQClientFactory.getMQAdminImpl().fetchPublishMessageQueues(topic); } public Set fetchSubscribeMessageQueues(String topic) throws MQClientException { - this.makeSureStateOK(); - return this.mQClientFactory.getMQAdminImpl().fetchSubscribeMessageQueues(topic); + this.isRunning(); + // check if has info in memory, otherwise invoke api. + Set result = this.rebalanceImpl.getTopicSubscribeInfoTable().get(topic); + if (null == result) { + result = this.mQClientFactory.getMQAdminImpl().fetchSubscribeMessageQueues(topic); + } + + return parseSubscribeMessageQueues(result); + } + + public Set parseSubscribeMessageQueues(Set queueSet) { + Set resultQueues = new HashSet<>(); + for (MessageQueue messageQueue : queueSet) { + String userTopic = NamespaceUtil.withoutNamespace(messageQueue.getTopic(), + this.defaultMQPullConsumer.getNamespace()); + resultQueues.add(new MessageQueue(userTopic, messageQueue.getBrokerName(), messageQueue.getQueueId())); + } + return resultQueues; } public long earliestMsgStoreTime(MessageQueue mq) throws MQClientException { - this.makeSureStateOK(); + this.isRunning(); return this.mQClientFactory.getMQAdminImpl().earliestMsgStoreTime(mq); } public long maxOffset(MessageQueue mq) throws MQClientException { - this.makeSureStateOK(); + this.isRunning(); return this.mQClientFactory.getMQAdminImpl().maxOffset(mq); } public long minOffset(MessageQueue mq) throws MQClientException { - this.makeSureStateOK(); + this.isRunning(); return this.mQClientFactory.getMQAdminImpl().minOffset(mq); } @@ -158,17 +182,57 @@ public PullResult pull(MessageQueue mq, String subExpression, long offset, int m public PullResult pull(MessageQueue mq, String subExpression, long offset, int maxNums, long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { - return this.pullSyncImpl(mq, subExpression, offset, maxNums, false, timeout); + SubscriptionData subscriptionData = getSubscriptionData(mq, subExpression); + return this.pullSyncImpl(mq, subscriptionData, offset, maxNums, false, timeout); } - private PullResult pullSyncImpl(MessageQueue mq, String subExpression, long offset, int maxNums, boolean block, - long timeout) + public PullResult pull(MessageQueue mq, MessageSelector messageSelector, long offset, int maxNums) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { - this.makeSureStateOK(); + return pull(mq, messageSelector, offset, maxNums, this.defaultMQPullConsumer.getConsumerPullTimeoutMillis()); + } + + public PullResult pull(MessageQueue mq, MessageSelector messageSelector, long offset, int maxNums, long timeout) + throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + SubscriptionData subscriptionData = getSubscriptionData(mq, messageSelector); + return this.pullSyncImpl(mq, subscriptionData, offset, maxNums, false, timeout); + } + + private SubscriptionData getSubscriptionData(MessageQueue mq, String subExpression) + throws MQClientException { if (null == mq) { throw new MQClientException("mq is null", null); + } + try { + return FilterAPI.buildSubscriptionData(mq.getTopic(), subExpression); + } catch (Exception e) { + throw new MQClientException("parse subscription error", e); + } + } + + private SubscriptionData getSubscriptionData(MessageQueue mq, MessageSelector messageSelector) + throws MQClientException { + + if (null == mq) { + throw new MQClientException("mq is null", null); + } + + try { + return FilterAPI.build(mq.getTopic(), + messageSelector.getExpression(), messageSelector.getExpressionType()); + } catch (Exception e) { + throw new MQClientException("parse subscription error", e); + } + } + + private PullResult pullSyncImpl(MessageQueue mq, SubscriptionData subscriptionData, long offset, int maxNums, boolean block, + long timeout) + throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + this.isRunning(); + + if (null == mq) { + throw new MQClientException("mq is null", null); } if (offset < 0) { @@ -183,20 +247,14 @@ private PullResult pullSyncImpl(MessageQueue mq, String subExpression, long offs int sysFlag = PullSysFlag.buildSysFlag(false, block, true, false); - SubscriptionData subscriptionData; - try { - subscriptionData = FilterAPI.buildSubscriptionData(this.defaultMQPullConsumer.getConsumerGroup(), - mq.getTopic(), subExpression); - } catch (Exception e) { - throw new MQClientException("parse subscription error", e); - } - long timeoutMillis = block ? this.defaultMQPullConsumer.getConsumerTimeoutMillisWhenSuspend() : timeout; + boolean isTagType = ExpressionType.isTagType(subscriptionData.getExpressionType()); PullResult pullResult = this.pullAPIWrapper.pullKernelImpl( mq, subscriptionData.getSubString(), - 0L, + subscriptionData.getExpressionType(), + isTagType ? 0L : subscriptionData.getSubVersion(), offset, maxNums, sysFlag, @@ -207,9 +265,12 @@ private PullResult pullSyncImpl(MessageQueue mq, String subExpression, long offs null ); this.pullAPIWrapper.processPullResult(mq, pullResult, subscriptionData); + //If namespace is not null , reset Topic without namespace. + this.resetTopic(pullResult.getMsgFoundList()); if (!this.consumeMessageHookList.isEmpty()) { ConsumeMessageContext consumeMessageContext = null; consumeMessageContext = new ConsumeMessageContext(); + consumeMessageContext.setNamespace(defaultMQPullConsumer.getNamespace()); consumeMessageContext.setConsumerGroup(this.groupName()); consumeMessageContext.setMq(mq); consumeMessageContext.setMsgList(pullResult.getMsgFoundList()); @@ -217,16 +278,30 @@ private PullResult pullSyncImpl(MessageQueue mq, String subExpression, long offs this.executeHookBefore(consumeMessageContext); consumeMessageContext.setStatus(ConsumeConcurrentlyStatus.CONSUME_SUCCESS.toString()); consumeMessageContext.setSuccess(true); + consumeMessageContext.setAccessChannel(defaultMQPullConsumer.getAccessChannel()); this.executeHookAfter(consumeMessageContext); } return pullResult; } + public void resetTopic(List msgList) { + if (null == msgList || msgList.size() == 0) { + return; + } + + //If namespace not null , reset Topic without namespace. + for (MessageExt messageExt : msgList) { + if (null != this.getDefaultMQPullConsumer().getNamespace()) { + messageExt.setTopic(NamespaceUtil.withoutNamespace(messageExt.getTopic(), this.defaultMQPullConsumer.getNamespace())); + } + } + + } + public void subscriptionAutomatically(final String topic) { if (!this.rebalanceImpl.getSubscriptionInner().containsKey(topic)) { try { - SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(this.defaultMQPullConsumer.getConsumerGroup(), - topic, SubscriptionData.SUB_ALL); + SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(topic, SubscriptionData.SUB_ALL); this.rebalanceImpl.subscriptionInner.putIfAbsent(topic, subscriptionData); } catch (Exception ignore) { } @@ -281,7 +356,7 @@ public ConsumeFromWhere consumeFromWhere() { @Override public Set subscriptions() { - Set result = new HashSet(); + Set result = new HashSet<>(); Set topics = this.defaultMQPullConsumer.getRegisterTopics(); if (topics != null) { @@ -289,12 +364,14 @@ public Set subscriptions() { for (String t : topics) { SubscriptionData ms = null; try { - ms = FilterAPI.buildSubscriptionData(this.groupName(), t, SubscriptionData.SUB_ALL); + ms = FilterAPI.buildSubscriptionData(t, SubscriptionData.SUB_ALL); } catch (Exception e) { log.error("parse subscription error", e); } - ms.setSubVersion(0L); - result.add(ms); + if (ms != null) { + ms.setSubVersion(0L); + result.add(ms); + } } } } @@ -309,11 +386,19 @@ public void doRebalance() { } } + @Override + public boolean tryRebalance() { + if (this.rebalanceImpl != null) { + return this.rebalanceImpl.doRebalance(false); + } + return false; + } + @Override public void persistConsumerOffset() { try { - this.makeSureStateOK(); - Set mqs = new HashSet(); + this.isRunning(); + Set mqs = new HashSet<>(); Set allocateMq = this.rebalanceImpl.getProcessQueueTable().keySet(); mqs.addAll(allocateMq); this.offsetStore.persistAll(mqs); @@ -369,18 +454,41 @@ public void pull(MessageQueue mq, String subExpression, long offset, int maxNums public void pull(MessageQueue mq, String subExpression, long offset, int maxNums, PullCallback pullCallback, long timeout) throws MQClientException, RemotingException, InterruptedException { - this.pullAsyncImpl(mq, subExpression, offset, maxNums, pullCallback, false, timeout); + SubscriptionData subscriptionData = getSubscriptionData(mq, subExpression); + this.pullAsyncImpl(mq, subscriptionData, offset, maxNums, pullCallback, false, timeout); + } + + public void pull(MessageQueue mq, String subExpression, long offset, int maxNums, int maxSize, PullCallback pullCallback, + long timeout) + throws MQClientException, RemotingException, InterruptedException { + SubscriptionData subscriptionData = getSubscriptionData(mq, subExpression); + this.pullAsyncImpl(mq, subscriptionData, offset, maxNums, maxSize, pullCallback, false, timeout); + } + + public void pull(MessageQueue mq, MessageSelector messageSelector, long offset, int maxNums, + PullCallback pullCallback) + throws MQClientException, RemotingException, InterruptedException { + pull(mq, messageSelector, offset, maxNums, pullCallback, this.defaultMQPullConsumer.getConsumerPullTimeoutMillis()); + } + + public void pull(MessageQueue mq, MessageSelector messageSelector, long offset, int maxNums, + PullCallback pullCallback, + long timeout) + throws MQClientException, RemotingException, InterruptedException { + SubscriptionData subscriptionData = getSubscriptionData(mq, messageSelector); + this.pullAsyncImpl(mq, subscriptionData, offset, maxNums, pullCallback, false, timeout); } private void pullAsyncImpl( final MessageQueue mq, - final String subExpression, + final SubscriptionData subscriptionData, final long offset, final int maxNums, + final int maxSizeInBytes, final PullCallback pullCallback, final boolean block, final long timeout) throws MQClientException, RemotingException, InterruptedException { - this.makeSureStateOK(); + this.isRunning(); if (null == mq) { throw new MQClientException("mq is null", null); @@ -394,6 +502,11 @@ private void pullAsyncImpl( throw new MQClientException("maxNums <= 0", null); } + if (maxSizeInBytes <= 0) { + throw new MQClientException("maxSizeInBytes <= 0", null); + } + + if (null == pullCallback) { throw new MQClientException("pullCallback is null", null); } @@ -403,22 +516,17 @@ private void pullAsyncImpl( try { int sysFlag = PullSysFlag.buildSysFlag(false, block, true, false); - final SubscriptionData subscriptionData; - try { - subscriptionData = FilterAPI.buildSubscriptionData(this.defaultMQPullConsumer.getConsumerGroup(), - mq.getTopic(), subExpression); - } catch (Exception e) { - throw new MQClientException("parse subscription error", e); - } - long timeoutMillis = block ? this.defaultMQPullConsumer.getConsumerTimeoutMillisWhenSuspend() : timeout; + boolean isTagType = ExpressionType.isTagType(subscriptionData.getExpressionType()); this.pullAPIWrapper.pullKernelImpl( mq, subscriptionData.getSubString(), - 0L, + subscriptionData.getExpressionType(), + isTagType ? 0L : subscriptionData.getSubVersion(), offset, maxNums, + maxSizeInBytes, sysFlag, 0, this.defaultMQPullConsumer.getBrokerSuspendMaxTimeMillis(), @@ -428,8 +536,9 @@ private void pullAsyncImpl( @Override public void onSuccess(PullResult pullResult) { - pullCallback - .onSuccess(DefaultMQPullConsumerImpl.this.pullAPIWrapper.processPullResult(mq, pullResult, subscriptionData)); + PullResult userPullResult = DefaultMQPullConsumerImpl.this.pullAPIWrapper.processPullResult(mq, pullResult, subscriptionData); + resetTopic(userPullResult.getMsgFoundList()); + pullCallback.onSuccess(userPullResult); } @Override @@ -442,9 +551,30 @@ public void onException(Throwable e) { } } + private void pullAsyncImpl( + final MessageQueue mq, + final SubscriptionData subscriptionData, + final long offset, + final int maxNums, + final PullCallback pullCallback, + final boolean block, + final long timeout) throws MQClientException, RemotingException, InterruptedException { + pullAsyncImpl( + mq, + subscriptionData, + offset, + maxNums, + Integer.MAX_VALUE, + pullCallback, + block, + timeout + ); + } + public PullResult pullBlockIfNotFound(MessageQueue mq, String subExpression, long offset, int maxNums) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { - return this.pullSyncImpl(mq, subExpression, offset, maxNums, true, this.getDefaultMQPullConsumer().getConsumerPullTimeoutMillis()); + SubscriptionData subscriptionData = getSubscriptionData(mq, subExpression); + return this.pullSyncImpl(mq, subscriptionData, offset, maxNums, true, this.getDefaultMQPullConsumer().getConsumerPullTimeoutMillis()); } public DefaultMQPullConsumer getDefaultMQPullConsumer() { @@ -454,24 +584,25 @@ public DefaultMQPullConsumer getDefaultMQPullConsumer() { public void pullBlockIfNotFound(MessageQueue mq, String subExpression, long offset, int maxNums, PullCallback pullCallback) throws MQClientException, RemotingException, InterruptedException { - this.pullAsyncImpl(mq, subExpression, offset, maxNums, pullCallback, true, + SubscriptionData subscriptionData = getSubscriptionData(mq, subExpression); + this.pullAsyncImpl(mq, subscriptionData, offset, maxNums, pullCallback, true, this.getDefaultMQPullConsumer().getConsumerPullTimeoutMillis()); } public QueryResult queryMessage(String topic, String key, int maxNum, long begin, long end) throws MQClientException, InterruptedException { - this.makeSureStateOK(); + this.isRunning(); return this.mQClientFactory.getMQAdminImpl().queryMessage(topic, key, maxNum, begin, end); } public MessageExt queryMessageByUniqKey(String topic, String uniqKey) throws MQClientException, InterruptedException { - this.makeSureStateOK(); + this.isRunning(); return this.mQClientFactory.getMQAdminImpl().queryMessageByUniqKey(topic, uniqKey); } public long searchOffset(MessageQueue mq, long timestamp) throws MQClientException { - this.makeSureStateOK(); + this.isRunning(); return this.mQClientFactory.getMQAdminImpl().searchOffset(mq, timestamp); } @@ -485,18 +616,23 @@ public void updateConsumeOffsetToBroker(MessageQueue mq, long offset, boolean is this.offsetStore.updateConsumeOffsetToBroker(mq, offset, isOneway); } + @Deprecated public void sendMessageBack(MessageExt msg, int delayLevel, final String brokerName, String consumerGroup) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { try { - String brokerAddr = (null != brokerName) ? this.mQClientFactory.findBrokerAddressInPublish(brokerName) + String destBrokerName = brokerName; + if (destBrokerName != null && destBrokerName.startsWith(MixAll.LOGICAL_QUEUE_MOCK_BROKER_PREFIX)) { + destBrokerName = this.mQClientFactory.getBrokerNameFromMessageQueue(this.defaultMQPullConsumer.queueWithNamespace(new MessageQueue(msg.getTopic(), msg.getBrokerName(), msg.getQueueId()))); + } + String brokerAddr = (null != destBrokerName) ? this.mQClientFactory.findBrokerAddressInPublish(destBrokerName) : RemotingHelper.parseSocketAddressAddr(msg.getStoreHost()); if (UtilAll.isBlank(consumerGroup)) { consumerGroup = this.defaultMQPullConsumer.getConsumerGroup(); } - this.mQClientFactory.getMQClientAPIImpl().consumerSendMessageBack(brokerAddr, msg, consumerGroup, delayLevel, 3000, - this.defaultMQPullConsumer.getMaxReconsumeTimes()); + this.mQClientFactory.getMQClientAPIImpl().consumerSendMessageBack(brokerAddr, brokerName, msg, consumerGroup, + delayLevel, 3000, this.defaultMQPullConsumer.getMaxReconsumeTimes()); } catch (Exception e) { log.error("sendMessageBack Exception, " + this.defaultMQPullConsumer.getConsumerGroup(), e); @@ -510,6 +646,8 @@ public void sendMessageBack(MessageExt msg, int delayLevel, final String brokerN MessageAccessor.setMaxReconsumeTimes(newMsg, String.valueOf(this.defaultMQPullConsumer.getMaxReconsumeTimes())); newMsg.setDelayTimeLevel(3 + msg.getReconsumeTimes()); this.mQClientFactory.getDefaultMQProducer().send(newMsg); + } finally { + msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(), this.defaultMQPullConsumer.getNamespace())); } } @@ -544,7 +682,7 @@ public synchronized void start() throws MQClientException { this.defaultMQPullConsumer.changeInstanceNameToPID(); } - this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQPullConsumer, this.rpcHook); + this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPullConsumer, this.rpcHook); this.rebalanceImpl.setConsumerGroup(this.defaultMQPullConsumer.getConsumerGroup()); this.rebalanceImpl.setMessageModel(this.defaultMQPullConsumer.getMessageModel()); @@ -652,8 +790,7 @@ private void copySubscription() throws MQClientException { Set registerTopics = this.defaultMQPullConsumer.getRegisterTopics(); if (registerTopics != null) { for (final String topic : registerTopics) { - SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(this.defaultMQPullConsumer.getConsumerGroup(), - topic, SubscriptionData.SUB_ALL); + SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(topic, SubscriptionData.SUB_ALL); this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData); } } @@ -663,13 +800,13 @@ private void copySubscription() throws MQClientException { } public void updateConsumeOffset(MessageQueue mq, long offset) throws MQClientException { - this.makeSureStateOK(); + this.isRunning(); this.offsetStore.updateOffset(mq, offset, false); } public MessageExt viewMessage(String msgId) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { - this.makeSureStateOK(); + this.isRunning(); return this.mQClientFactory.getMQAdminImpl().viewMessage(msgId); } diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/DefaultMQPushConsumerImpl.java b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/DefaultMQPushConsumerImpl.java index f560376c6ad..6666c4335eb 100644 --- a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/DefaultMQPushConsumerImpl.java +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/DefaultMQPushConsumerImpl.java @@ -27,10 +27,19 @@ import java.util.Properties; import java.util.Set; import java.util.concurrent.ConcurrentMap; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.client.QueryResult; import org.apache.rocketmq.client.Validators; +import org.apache.rocketmq.client.consumer.AckCallback; +import org.apache.rocketmq.client.consumer.AckResult; +import org.apache.rocketmq.client.consumer.AckStatus; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.MessageQueueListener; import org.apache.rocketmq.client.consumer.MessageSelector; +import org.apache.rocketmq.client.consumer.PopCallback; +import org.apache.rocketmq.client.consumer.PopResult; +import org.apache.rocketmq.client.consumer.PopStatus; import org.apache.rocketmq.client.consumer.PullCallback; import org.apache.rocketmq.client.consumer.PullResult; import org.apache.rocketmq.client.consumer.listener.MessageListener; @@ -44,59 +53,72 @@ import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.hook.ConsumeMessageContext; import org.apache.rocketmq.client.hook.ConsumeMessageHook; +import org.apache.rocketmq.client.hook.FilterMessageContext; import org.apache.rocketmq.client.hook.FilterMessageHook; import org.apache.rocketmq.client.impl.CommunicationMode; +import org.apache.rocketmq.client.impl.FindBrokerResult; import org.apache.rocketmq.client.impl.MQClientManager; import org.apache.rocketmq.client.impl.factory.MQClientInstance; -import org.apache.rocketmq.client.log.ClientLogger; import org.apache.rocketmq.client.stat.ConsumerStatsManager; +import org.apache.rocketmq.common.KeyBuilder; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.ServiceState; import org.apache.rocketmq.common.UtilAll; import org.apache.rocketmq.common.consumer.ConsumeFromWhere; -import org.apache.rocketmq.common.filter.FilterAPI; import org.apache.rocketmq.common.help.FAQUrl; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageAccessor; import org.apache.rocketmq.common.message.MessageConst; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.body.ConsumeStatus; -import org.apache.rocketmq.common.protocol.body.ConsumerRunningInfo; -import org.apache.rocketmq.common.protocol.body.ProcessQueueInfo; -import org.apache.rocketmq.common.protocol.body.QueueTimeSpan; -import org.apache.rocketmq.common.protocol.heartbeat.ConsumeType; -import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; -import org.apache.rocketmq.common.protocol.route.BrokerData; -import org.apache.rocketmq.common.protocol.route.TopicRouteData; import org.apache.rocketmq.common.sysflag.PullSysFlag; import org.apache.rocketmq.remoting.RPCHook; import org.apache.rocketmq.remoting.common.RemotingHelper; import org.apache.rocketmq.remoting.exception.RemotingException; -import org.slf4j.Logger; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.ConsumeStatus; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.body.PopProcessQueueInfo; +import org.apache.rocketmq.remoting.protocol.body.ProcessQueueInfo; +import org.apache.rocketmq.remoting.protocol.body.QueueTimeSpan; +import org.apache.rocketmq.remoting.protocol.filter.FilterAPI; +import org.apache.rocketmq.remoting.protocol.header.AckMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ChangeInvisibleTimeRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ExtraInfoUtil; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; public class DefaultMQPushConsumerImpl implements MQConsumerInner { /** * Delay some time when exception occur */ - private static final long PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION = 3000; + private long pullTimeDelayMillsWhenException = 3000; /** - * Flow control interval + * Flow control interval when message cache is full */ - private static final long PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL = 50; + private static final long PULL_TIME_DELAY_MILLS_WHEN_CACHE_FLOW_CONTROL = 50; + /** + * Flow control interval when broker return flow control + */ + private static final long PULL_TIME_DELAY_MILLS_WHEN_BROKER_FLOW_CONTROL = 20; /** * Delay some time when suspend pull service */ private static final long PULL_TIME_DELAY_MILLS_WHEN_SUSPEND = 1000; private static final long BROKER_SUSPEND_MAX_TIME_MILLIS = 1000 * 15; private static final long CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND = 1000 * 30; - private final Logger log = ClientLogger.getLog(); + private static final Logger log = LoggerFactory.getLogger(DefaultMQPushConsumerImpl.class); private final DefaultMQPushConsumer defaultMQPushConsumer; private final RebalanceImpl rebalanceImpl = new RebalancePushImpl(this); - private final ArrayList filterMessageHookList = new ArrayList(); + private final ArrayList filterMessageHookList = new ArrayList<>(); private final long consumerStartTimestamp = System.currentTimeMillis(); - private final ArrayList consumeMessageHookList = new ArrayList(); + private final ArrayList consumeMessageHookList = new ArrayList<>(); private final RPCHook rpcHook; private volatile ServiceState serviceState = ServiceState.CREATE_JUST; private MQClientInstance mQClientFactory; @@ -106,12 +128,25 @@ public class DefaultMQPushConsumerImpl implements MQConsumerInner { private MessageListener messageListenerInner; private OffsetStore offsetStore; private ConsumeMessageService consumeMessageService; + private ConsumeMessageService consumeMessagePopService; private long queueFlowControlTimes = 0; private long queueMaxSpanFlowControlTimes = 0; + //10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h + private final int[] popDelayLevel = new int[] {10, 30, 60, 120, 180, 240, 300, 360, 420, 480, 540, 600, 1200, 1800, 3600, 7200}; + + private static final int MAX_POP_INVISIBLE_TIME = 300000; + private static final int MIN_POP_INVISIBLE_TIME = 5000; + private static final int ASYNC_TIMEOUT = 3000; + + // only for test purpose, will be modified by reflection in unit test. + @SuppressWarnings("FieldMayBeFinal") + private static boolean doNotUpdateTopicSubscribeInfoWhenSubscriptionChanged = false; + public DefaultMQPushConsumerImpl(DefaultMQPushConsumer defaultMQPushConsumer, RPCHook rpcHook) { this.defaultMQPushConsumer = defaultMQPushConsumer; this.rpcHook = rpcHook; + this.pullTimeDelayMillsWhenException = defaultMQPushConsumer.getPullTimeDelayMillsWhenException(); } public void registerFilterMessageHook(final FilterMessageHook hook) { @@ -134,6 +169,7 @@ public void executeHookBefore(final ConsumeMessageContext context) { try { hook.consumeMessageBefore(context); } catch (Throwable e) { + log.warn("consumeMessageHook {} executeHookBefore exception", hook.hookName(), e); } } } @@ -145,6 +181,7 @@ public void executeHookAfter(final ConsumeMessageContext context) { try { hook.consumeMessageAfter(context); } catch (Throwable e) { + log.warn("consumeMessageHook {} executeHookAfter exception", hook.hookName(), e); } } } @@ -155,7 +192,7 @@ public void createTopic(String key, String newTopic, int queueNum) throws MQClie } public void createTopic(String key, String newTopic, int queueNum, int topicSysFlag) throws MQClientException { - this.mQClientFactory.getMQAdminImpl().createTopic(key, newTopic, queueNum, topicSysFlag); + this.mQClientFactory.getMQAdminImpl().createTopic(key, newTopic, queueNum, topicSysFlag, null); } public Set fetchSubscribeMessageQueues(String topic) throws MQClientException { @@ -169,7 +206,17 @@ public Set fetchSubscribeMessageQueues(String topic) throws MQClie throw new MQClientException("The topic[" + topic + "] not exist", null); } - return result; + return parseSubscribeMessageQueues(result); + } + + public Set parseSubscribeMessageQueues(Set messageQueueList) { + Set resultQueues = new HashSet<>(); + for (MessageQueue queue : messageQueueList) { + String userTopic = NamespaceUtil.withoutNamespace(queue.getTopic(), this.defaultMQPushConsumer.getNamespace()); + resultQueues.add(new MessageQueue(userTopic, queue.getBrokerName(), queue.getQueueId())); + } + + return resultQueues; } public DefaultMQPushConsumer getDefaultMQPushConsumer() { @@ -209,7 +256,7 @@ public void pullMessage(final PullRequest pullRequest) { this.makeSureStateOK(); } catch (MQClientException e) { log.warn("pullMessage exception, consumer state not ok", e); - this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION); + this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException); return; } @@ -223,7 +270,7 @@ public void pullMessage(final PullRequest pullRequest) { long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024); if (cachedMessageCount > this.defaultMQPushConsumer.getPullThresholdForQueue()) { - this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL); + this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_CACHE_FLOW_CONTROL); if ((queueFlowControlTimes++ % 1000) == 0) { log.warn( "the cached message count exceeds the threshold {}, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}", @@ -233,7 +280,7 @@ public void pullMessage(final PullRequest pullRequest) { } if (cachedMessageSizeInMiB > this.defaultMQPushConsumer.getPullThresholdSizeForQueue()) { - this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL); + this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_CACHE_FLOW_CONTROL); if ((queueFlowControlTimes++ % 1000) == 0) { log.warn( "the cached message size exceeds the threshold {} MiB, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}", @@ -244,7 +291,7 @@ public void pullMessage(final PullRequest pullRequest) { if (!this.consumeOrderly) { if (processQueue.getMaxSpan() > this.defaultMQPushConsumer.getConsumeConcurrentlyMaxSpan()) { - this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL); + this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_CACHE_FLOW_CONTROL); if ((queueMaxSpanFlowControlTimes++ % 1000) == 0) { log.warn( "the queue's messages, span too long, so do flow control, minOffset={}, maxOffset={}, maxSpan={}, pullRequest={}, flowControlTimes={}", @@ -255,8 +302,18 @@ public void pullMessage(final PullRequest pullRequest) { } } else { if (processQueue.isLocked()) { - if (!pullRequest.isLockedFirst()) { - final long offset = this.rebalanceImpl.computePullFromWhere(pullRequest.getMessageQueue()); + if (!pullRequest.isPreviouslyLocked()) { + long offset = -1L; + try { + offset = this.rebalanceImpl.computePullFromWhereWithException(pullRequest.getMessageQueue()); + if (offset < 0) { + throw new MQClientException(ResponseCode.SYSTEM_ERROR, "Unexpected offset " + offset); + } + } catch (Exception e) { + this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException); + log.error("Failed to compute pull offset, pullResult: {}", pullRequest, e); + return; + } boolean brokerBusy = offset < pullRequest.getNextOffset(); log.info("the first time to pull message, so fix offset from broker. pullRequest: {} NewOffset: {} brokerBusy: {}", pullRequest, offset, brokerBusy); @@ -265,19 +322,20 @@ public void pullMessage(final PullRequest pullRequest) { pullRequest, offset); } - pullRequest.setLockedFirst(true); + pullRequest.setPreviouslyLocked(true); pullRequest.setNextOffset(offset); } } else { - this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION); + this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException); log.info("pull message later because not locked in broker, {}", pullRequest); return; } } - final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic()); + final MessageQueue messageQueue = pullRequest.getMessageQueue(); + final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(messageQueue.getTopic()); if (null == subscriptionData) { - this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION); + this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException); log.warn("find the consumer's subscription failed, {}", pullRequest); return; } @@ -308,12 +366,12 @@ public void onSuccess(PullResult pullResult) { DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(pullRequest.getConsumerGroup(), pullRequest.getMessageQueue().getTopic(), pullResult.getMsgFoundList().size()); - boolean dispathToConsume = processQueue.putMessage(pullResult.getMsgFoundList()); + boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList()); DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest( pullResult.getMsgFoundList(), processQueue, pullRequest.getMessageQueue(), - dispathToConsume); + dispatchToConsume); if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) { DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest, @@ -334,12 +392,6 @@ public void onSuccess(PullResult pullResult) { break; case NO_NEW_MSG: - pullRequest.setNextOffset(pullResult.getNextBeginOffset()); - - DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest); - - DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest); - break; case NO_MATCHED_MSG: pullRequest.setNextOffset(pullResult.getNextBeginOffset()); @@ -353,24 +405,26 @@ public void onSuccess(PullResult pullResult) { pullRequest.setNextOffset(pullResult.getNextBeginOffset()); pullRequest.getProcessQueue().setDropped(true); - DefaultMQPushConsumerImpl.this.executeTaskLater(new Runnable() { + DefaultMQPushConsumerImpl.this.executeTask(new Runnable() { @Override public void run() { try { - DefaultMQPushConsumerImpl.this.offsetStore.updateOffset(pullRequest.getMessageQueue(), - pullRequest.getNextOffset(), false); + DefaultMQPushConsumerImpl.this.offsetStore.updateAndFreezeOffset(pullRequest.getMessageQueue(), + pullRequest.getNextOffset()); DefaultMQPushConsumerImpl.this.offsetStore.persist(pullRequest.getMessageQueue()); + // removeProcessQueue will also remove offset to cancel the frozen status. DefaultMQPushConsumerImpl.this.rebalanceImpl.removeProcessQueue(pullRequest.getMessageQueue()); + DefaultMQPushConsumerImpl.this.rebalanceImpl.getmQClientFactory().rebalanceImmediately(); log.warn("fix the pull request offset, {}", pullRequest); } catch (Throwable e) { log.error("executeTaskLater Exception", e); } } - }, 10000); + }); break; default: break; @@ -381,10 +435,18 @@ public void run() { @Override public void onException(Throwable e) { if (!pullRequest.getMessageQueue().getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { - log.warn("execute the pull request exception", e); + if (e instanceof MQBrokerException && ((MQBrokerException) e).getResponseCode() == ResponseCode.SUBSCRIPTION_NOT_LATEST) { + log.warn("the subscription is not latest, group={}, messageQueue={}", groupName(), messageQueue); + } else { + log.warn("execute the pull request exception, group={}, messageQueue={}", groupName(), messageQueue, e); + } } - DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION); + if (e instanceof MQBrokerException && ((MQBrokerException) e).getResponseCode() == ResponseCode.FLOW_CONTROL) { + DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_BROKER_FLOW_CONTROL); + } else { + DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException); + } } }; @@ -422,6 +484,7 @@ public void onException(Throwable e) { subscriptionData.getSubVersion(), pullRequest.getNextOffset(), this.defaultMQPushConsumer.getPullBatchSize(), + this.defaultMQPushConsumer.getPullBatchSizeInBytes(), sysFlag, commitOffsetValue, BROKER_SUSPEND_MAX_TIME_MILLIS, @@ -431,10 +494,177 @@ public void onException(Throwable e) { ); } catch (Exception e) { log.error("pullKernelImpl exception", e); - this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION); + this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException); } } + void popMessage(final PopRequest popRequest) { + final PopProcessQueue processQueue = popRequest.getPopProcessQueue(); + if (processQueue.isDropped()) { + log.info("the pop request[{}] is dropped.", popRequest.toString()); + return; + } + + processQueue.setLastPopTimestamp(System.currentTimeMillis()); + + try { + this.makeSureStateOK(); + } catch (MQClientException e) { + log.warn("pullMessage exception, consumer state not ok", e); + this.executePopPullRequestLater(popRequest, pullTimeDelayMillsWhenException); + return; + } + + if (this.isPause()) { + log.warn("consumer was paused, execute pull request later. instanceName={}, group={}", this.defaultMQPushConsumer.getInstanceName(), this.defaultMQPushConsumer.getConsumerGroup()); + this.executePopPullRequestLater(popRequest, PULL_TIME_DELAY_MILLS_WHEN_SUSPEND); + return; + } + + if (processQueue.getWaiAckMsgCount() > this.defaultMQPushConsumer.getPopThresholdForQueue()) { + this.executePopPullRequestLater(popRequest, PULL_TIME_DELAY_MILLS_WHEN_CACHE_FLOW_CONTROL); + if ((queueFlowControlTimes++ % 1000) == 0) { + log.warn("the messages waiting to ack exceeds the threshold {}, so do flow control, popRequest={}, flowControlTimes={}, wait count={}", + this.defaultMQPushConsumer.getPopThresholdForQueue(), popRequest, queueFlowControlTimes, processQueue.getWaiAckMsgCount()); + } + return; + } + + //POPTODO think of pop mode orderly implementation later. + final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(popRequest.getMessageQueue().getTopic()); + if (null == subscriptionData) { + this.executePopPullRequestLater(popRequest, pullTimeDelayMillsWhenException); + log.warn("find the consumer's subscription failed, {}", popRequest); + return; + } + + final long beginTimestamp = System.currentTimeMillis(); + + PopCallback popCallback = new PopCallback() { + @Override + public void onSuccess(PopResult popResult) { + if (popResult == null) { + log.error("pop callback popResult is null"); + DefaultMQPushConsumerImpl.this.executePopPullRequestImmediately(popRequest); + return; + } + + processPopResult(popResult, subscriptionData); + + switch (popResult.getPopStatus()) { + case FOUND: + long pullRT = System.currentTimeMillis() - beginTimestamp; + DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullRT(popRequest.getConsumerGroup(), + popRequest.getMessageQueue().getTopic(), pullRT); + if (popResult.getMsgFoundList() == null || popResult.getMsgFoundList().isEmpty()) { + DefaultMQPushConsumerImpl.this.executePopPullRequestImmediately(popRequest); + } else { + DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(popRequest.getConsumerGroup(), + popRequest.getMessageQueue().getTopic(), popResult.getMsgFoundList().size()); + popRequest.getPopProcessQueue().incFoundMsg(popResult.getMsgFoundList().size()); + + DefaultMQPushConsumerImpl.this.consumeMessagePopService.submitPopConsumeRequest( + popResult.getMsgFoundList(), + processQueue, + popRequest.getMessageQueue()); + + if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) { + DefaultMQPushConsumerImpl.this.executePopPullRequestLater(popRequest, + DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval()); + } else { + DefaultMQPushConsumerImpl.this.executePopPullRequestImmediately(popRequest); + } + } + break; + case NO_NEW_MSG: + case POLLING_NOT_FOUND: + DefaultMQPushConsumerImpl.this.executePopPullRequestImmediately(popRequest); + break; + case POLLING_FULL: + DefaultMQPushConsumerImpl.this.executePopPullRequestLater(popRequest, pullTimeDelayMillsWhenException); + break; + default: + DefaultMQPushConsumerImpl.this.executePopPullRequestLater(popRequest, pullTimeDelayMillsWhenException); + break; + } + + } + + @Override + public void onException(Throwable e) { + if (!popRequest.getMessageQueue().getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + log.warn("execute the pull request exception: {}", e); + } + + if (e instanceof MQBrokerException && ((MQBrokerException) e).getResponseCode() == ResponseCode.FLOW_CONTROL) { + DefaultMQPushConsumerImpl.this.executePopPullRequestLater(popRequest, PULL_TIME_DELAY_MILLS_WHEN_BROKER_FLOW_CONTROL); + } else { + DefaultMQPushConsumerImpl.this.executePopPullRequestLater(popRequest, pullTimeDelayMillsWhenException); + } + } + }; + + + try { + + long invisibleTime = this.defaultMQPushConsumer.getPopInvisibleTime(); + if (invisibleTime < MIN_POP_INVISIBLE_TIME || invisibleTime > MAX_POP_INVISIBLE_TIME) { + invisibleTime = 60000; + } + this.pullAPIWrapper.popAsync(popRequest.getMessageQueue(), invisibleTime, this.defaultMQPushConsumer.getPopBatchNums(), + popRequest.getConsumerGroup(), BROKER_SUSPEND_MAX_TIME_MILLIS, popCallback, true, popRequest.getInitMode(), + false, subscriptionData.getExpressionType(), subscriptionData.getSubString()); + } catch (Exception e) { + log.error("popAsync exception", e); + this.executePopPullRequestLater(popRequest, pullTimeDelayMillsWhenException); + } + } + + private PopResult processPopResult(final PopResult popResult, final SubscriptionData subscriptionData) { + if (PopStatus.FOUND == popResult.getPopStatus()) { + List msgFoundList = popResult.getMsgFoundList(); + List msgListFilterAgain = msgFoundList; + if (!subscriptionData.getTagsSet().isEmpty() && !subscriptionData.isClassFilterMode() + && popResult.getMsgFoundList().size() > 0) { + msgListFilterAgain = new ArrayList<>(popResult.getMsgFoundList().size()); + for (MessageExt msg : popResult.getMsgFoundList()) { + if (msg.getTags() != null) { + if (subscriptionData.getTagsSet().contains(msg.getTags())) { + msgListFilterAgain.add(msg); + } + } + } + } + + if (!this.filterMessageHookList.isEmpty()) { + FilterMessageContext filterMessageContext = new FilterMessageContext(); + filterMessageContext.setUnitMode(this.defaultMQPushConsumer.isUnitMode()); + filterMessageContext.setMsgList(msgListFilterAgain); + if (!this.filterMessageHookList.isEmpty()) { + for (FilterMessageHook hook : this.filterMessageHookList) { + try { + hook.filterMessage(filterMessageContext); + } catch (Throwable e) { + log.error("execute hook error. hookName={}", hook.hookName()); + } + } + } + } + + if (msgFoundList.size() != msgListFilterAgain.size()) { + for (MessageExt msg : msgFoundList) { + if (!msgListFilterAgain.contains(msg)) { + ackAsync(msg, this.groupName()); + } + } + } + + popResult.setMsgFoundList(msgListFilterAgain); + } + + return popResult; + } + private void makeSureStateOK() throws MQClientException { if (this.serviceState != ServiceState.RUNNING) { throw new MQClientException("The consumer service state not OK, " @@ -444,7 +674,7 @@ private void makeSureStateOK() throws MQClientException { } } - private void executePullRequestLater(final PullRequest pullRequest, final long timeDelay) { + void executePullRequestLater(final PullRequest pullRequest, final long timeDelay) { this.mQClientFactory.getPullMessageService().executePullRequestLater(pullRequest, timeDelay); } @@ -464,6 +694,14 @@ public void executePullRequestImmediately(final PullRequest pullRequest) { this.mQClientFactory.getPullMessageService().executePullRequestImmediately(pullRequest); } + void executePopPullRequestLater(final PopRequest pullRequest, final long timeDelay) { + this.mQClientFactory.getPullMessageService().executePopPullRequestLater(pullRequest, timeDelay); + } + + void executePopPullRequestImmediately(final PopRequest pullRequest) { + this.mQClientFactory.getPullMessageService().executePopPullRequestImmediately(pullRequest); + } + private void correctTagsOffset(final PullRequest pullRequest) { if (0L == pullRequest.getProcessQueue().getMsgCount().get()) { this.offsetStore.updateOffset(pullRequest.getMessageQueue(), pullRequest.getNextOffset(), true); @@ -474,6 +712,10 @@ public void executeTaskLater(final Runnable r, final long timeDelay) { this.mQClientFactory.getPullMessageService().executeTaskLater(r, timeDelay); } + public void executeTask(final Runnable r) { + this.mQClientFactory.getPullMessageService().executeTask(r); + } + public QueryResult queryMessage(String topic, String key, int maxNum, long begin, long end) throws MQClientException, InterruptedException { return this.mQClientFactory.getMQAdminImpl().queryMessage(topic, key, maxNum, begin, end); @@ -494,33 +736,147 @@ public void resume() { log.info("resume this consumer, {}", this.defaultMQPushConsumer.getConsumerGroup()); } + @Deprecated public void sendMessageBack(MessageExt msg, int delayLevel, final String brokerName) + throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + sendMessageBack(msg, delayLevel, brokerName, null); + } + + public void sendMessageBack(MessageExt msg, int delayLevel, final MessageQueue mq) + throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + sendMessageBack(msg, delayLevel, null, mq); + } + + + private void sendMessageBack(MessageExt msg, int delayLevel, final String brokerName, final MessageQueue mq) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + boolean needRetry = true; try { - String brokerAddr = (null != brokerName) ? this.mQClientFactory.findBrokerAddressInPublish(brokerName) - : RemotingHelper.parseSocketAddressAddr(msg.getStoreHost()); - this.mQClientFactory.getMQClientAPIImpl().consumerSendMessageBack(brokerAddr, msg, - this.defaultMQPushConsumer.getConsumerGroup(), delayLevel, 5000, getMaxReconsumeTimes()); - } catch (Exception e) { - log.error("sendMessageBack Exception, " + this.defaultMQPushConsumer.getConsumerGroup(), e); + if (brokerName != null && brokerName.startsWith(MixAll.LOGICAL_QUEUE_MOCK_BROKER_PREFIX) + || mq != null && mq.getBrokerName().startsWith(MixAll.LOGICAL_QUEUE_MOCK_BROKER_PREFIX)) { + needRetry = false; + sendMessageBackAsNormalMessage(msg); + } else { + String brokerAddr = (null != brokerName) ? this.mQClientFactory.findBrokerAddressInPublish(brokerName) + : RemotingHelper.parseSocketAddressAddr(msg.getStoreHost()); + this.mQClientFactory.getMQClientAPIImpl().consumerSendMessageBack(brokerAddr, brokerName, msg, + this.defaultMQPushConsumer.getConsumerGroup(), delayLevel, 5000, getMaxReconsumeTimes()); + } + } catch (Throwable t) { + log.error("Failed to send message back, consumerGroup={}, brokerName={}, mq={}, message={}", + this.defaultMQPushConsumer.getConsumerGroup(), brokerName, mq, msg, t); + if (needRetry) { + sendMessageBackAsNormalMessage(msg); + } + } finally { + msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(), this.defaultMQPushConsumer.getNamespace())); + } + } + + private void sendMessageBackAsNormalMessage(MessageExt msg) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + Message newMsg = new Message(MixAll.getRetryTopic(this.defaultMQPushConsumer.getConsumerGroup()), msg.getBody()); + + String originMsgId = MessageAccessor.getOriginMessageId(msg); + MessageAccessor.setOriginMessageId(newMsg, UtilAll.isBlank(originMsgId) ? msg.getMsgId() : originMsgId); - Message newMsg = new Message(MixAll.getRetryTopic(this.defaultMQPushConsumer.getConsumerGroup()), msg.getBody()); + newMsg.setFlag(msg.getFlag()); + MessageAccessor.setProperties(newMsg, msg.getProperties()); + MessageAccessor.putProperty(newMsg, MessageConst.PROPERTY_RETRY_TOPIC, msg.getTopic()); + MessageAccessor.setReconsumeTime(newMsg, String.valueOf(msg.getReconsumeTimes() + 1)); + MessageAccessor.setMaxReconsumeTimes(newMsg, String.valueOf(getMaxReconsumeTimes())); + MessageAccessor.clearProperty(newMsg, MessageConst.PROPERTY_TRANSACTION_PREPARED); + newMsg.setDelayTimeLevel(3 + msg.getReconsumeTimes()); - String originMsgId = MessageAccessor.getOriginMessageId(msg); - MessageAccessor.setOriginMessageId(newMsg, UtilAll.isBlank(originMsgId) ? msg.getMsgId() : originMsgId); + this.mQClientFactory.getDefaultMQProducer().send(newMsg); + } + + void ackAsync(MessageExt message, String consumerGroup) { + final String extraInfo = message.getProperty(MessageConst.PROPERTY_POP_CK); + + try { + String[] extraInfoStrs = ExtraInfoUtil.split(extraInfo); + String brokerName = ExtraInfoUtil.getBrokerName(extraInfoStrs); + int queueId = ExtraInfoUtil.getQueueId(extraInfoStrs); + long queueOffset = ExtraInfoUtil.getQueueOffset(extraInfoStrs); + String topic = message.getTopic(); + + String desBrokerName = brokerName; + if (brokerName != null && brokerName.startsWith(MixAll.LOGICAL_QUEUE_MOCK_BROKER_PREFIX)) { + desBrokerName = this.mQClientFactory.getBrokerNameFromMessageQueue(this.defaultMQPushConsumer.queueWithNamespace(new MessageQueue(topic, brokerName, queueId))); + } - newMsg.setFlag(msg.getFlag()); - MessageAccessor.setProperties(newMsg, msg.getProperties()); - MessageAccessor.putProperty(newMsg, MessageConst.PROPERTY_RETRY_TOPIC, msg.getTopic()); - MessageAccessor.setReconsumeTime(newMsg, String.valueOf(msg.getReconsumeTimes() + 1)); - MessageAccessor.setMaxReconsumeTimes(newMsg, String.valueOf(getMaxReconsumeTimes())); - newMsg.setDelayTimeLevel(3 + msg.getReconsumeTimes()); - this.mQClientFactory.getDefaultMQProducer().send(newMsg); + FindBrokerResult + findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(desBrokerName, MixAll.MASTER_ID, true); + if (null == findBrokerResult) { + this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic); + findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(desBrokerName, MixAll.MASTER_ID, true); + } + + if (findBrokerResult == null) { + log.error("The broker[" + desBrokerName + "] not exist"); + return; + } + + AckMessageRequestHeader requestHeader = new AckMessageRequestHeader(); + requestHeader.setTopic(ExtraInfoUtil.getRealTopic(extraInfoStrs, topic, consumerGroup)); + requestHeader.setQueueId(queueId); + requestHeader.setOffset(queueOffset); + requestHeader.setConsumerGroup(consumerGroup); + requestHeader.setExtraInfo(extraInfo); + requestHeader.setBrokerName(brokerName); + this.mQClientFactory.getMQClientAPIImpl().ackMessageAsync(findBrokerResult.getBrokerAddr(), ASYNC_TIMEOUT, new AckCallback() { + @Override + public void onSuccess(AckResult ackResult) { + if (ackResult != null && !AckStatus.OK.equals(ackResult.getStatus())) { + log.warn("Ack message fail. ackResult: {}, extraInfo: {}", ackResult, extraInfo); + } + } + @Override + public void onException(Throwable e) { + log.warn("Ack message fail. extraInfo: {} error message: {}", extraInfo, e.toString()); + } + }, requestHeader); + + } catch (Throwable t) { + log.error("ack async error.", t); + } + } + + void changePopInvisibleTimeAsync(String topic, String consumerGroup, String extraInfo, long invisibleTime, AckCallback callback) + throws MQClientException, RemotingException, InterruptedException, MQBrokerException { + String[] extraInfoStrs = ExtraInfoUtil.split(extraInfo); + String brokerName = ExtraInfoUtil.getBrokerName(extraInfoStrs); + int queueId = ExtraInfoUtil.getQueueId(extraInfoStrs); + + String desBrokerName = brokerName; + if (brokerName != null && brokerName.startsWith(MixAll.LOGICAL_QUEUE_MOCK_BROKER_PREFIX)) { + desBrokerName = this.mQClientFactory.getBrokerNameFromMessageQueue(this.defaultMQPushConsumer.queueWithNamespace(new MessageQueue(topic, brokerName, queueId))); + } + + FindBrokerResult + findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(desBrokerName, MixAll.MASTER_ID, true); + if (null == findBrokerResult) { + this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic); + findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(desBrokerName, MixAll.MASTER_ID, true); } + if (findBrokerResult != null) { + ChangeInvisibleTimeRequestHeader requestHeader = new ChangeInvisibleTimeRequestHeader(); + requestHeader.setTopic(ExtraInfoUtil.getRealTopic(extraInfoStrs, topic, consumerGroup)); + requestHeader.setQueueId(queueId); + requestHeader.setOffset(ExtraInfoUtil.getQueueOffset(extraInfoStrs)); + requestHeader.setConsumerGroup(consumerGroup); + requestHeader.setExtraInfo(extraInfo); + requestHeader.setInvisibleTime(invisibleTime); + requestHeader.setBrokerName(brokerName); + //here the broker should be polished + this.mQClientFactory.getMQClientAPIImpl().changeInvisibleTimeAsync(brokerName, findBrokerResult.getBrokerAddr(), requestHeader, ASYNC_TIMEOUT, callback); + return; + } + throw new MQClientException("The broker[" + desBrokerName + "] not exist", null); } - private int getMaxReconsumeTimes() { + public int getMaxReconsumeTimes() { // default reconsume times: 16 if (this.defaultMQPushConsumer.getMaxReconsumeTimes() == -1) { return 16; @@ -529,12 +885,16 @@ private int getMaxReconsumeTimes() { } } - public synchronized void shutdown() { + public void shutdown() { + shutdown(0); + } + + public synchronized void shutdown(long awaitTerminateMillis) { switch (this.serviceState) { case CREATE_JUST: break; case RUNNING: - this.consumeMessageService.shutdown(); + this.consumeMessageService.shutdown(awaitTerminateMillis); this.persistConsumerOffset(); this.mQClientFactory.unregisterConsumer(this.defaultMQPushConsumer.getConsumerGroup()); this.mQClientFactory.shutdown(); @@ -564,16 +924,18 @@ public synchronized void start() throws MQClientException { this.defaultMQPushConsumer.changeInstanceNameToPID(); } - this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook); + this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook); this.rebalanceImpl.setConsumerGroup(this.defaultMQPushConsumer.getConsumerGroup()); this.rebalanceImpl.setMessageModel(this.defaultMQPushConsumer.getMessageModel()); this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPushConsumer.getAllocateMessageQueueStrategy()); this.rebalanceImpl.setmQClientFactory(this.mQClientFactory); - this.pullAPIWrapper = new PullAPIWrapper( - mQClientFactory, - this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode()); + if (this.pullAPIWrapper == null) { + this.pullAPIWrapper = new PullAPIWrapper( + mQClientFactory, + this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode()); + } this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList); if (this.defaultMQPushConsumer.getOffsetStore() != null) { @@ -597,18 +959,25 @@ public synchronized void start() throws MQClientException { this.consumeOrderly = true; this.consumeMessageService = new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner()); + //POPTODO reuse Executor ? + this.consumeMessagePopService = new ConsumeMessagePopOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner()); } else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) { this.consumeOrderly = false; this.consumeMessageService = new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner()); + //POPTODO reuse Executor ? + this.consumeMessagePopService = + new ConsumeMessagePopConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner()); } this.consumeMessageService.start(); + // POPTODO + this.consumeMessagePopService.start(); boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this); if (!registerOK) { this.serviceState = ServiceState.CREATE_JUST; - this.consumeMessageService.shutdown(); + this.consumeMessageService.shutdown(defaultMQPushConsumer.getAwaitTerminationMillisWhenShutdown()); throw new MQClientException("The consumer group[" + this.defaultMQPushConsumer.getConsumerGroup() + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL), null); @@ -631,8 +1000,9 @@ public synchronized void start() throws MQClientException { this.updateTopicSubscribeInfoWhenSubscriptionChanged(); this.mQClientFactory.checkClientInBroker(); - this.mQClientFactory.sendHeartbeatToAllBrokerWithLock(); - this.mQClientFactory.rebalanceImmediately(); + if (this.mQClientFactory.sendHeartbeatToAllBrokerWithLock()) { + this.mQClientFactory.rebalanceImmediately(); + } } private void checkConfig() throws MQClientException { @@ -803,6 +1173,23 @@ private void checkConfig() throws MQClientException { + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), null); } + + // popInvisibleTime + if (this.defaultMQPushConsumer.getPopInvisibleTime() < MIN_POP_INVISIBLE_TIME + || this.defaultMQPushConsumer.getPopInvisibleTime() > MAX_POP_INVISIBLE_TIME) { + throw new MQClientException( + "popInvisibleTime Out of range [" + MIN_POP_INVISIBLE_TIME + ", " + MAX_POP_INVISIBLE_TIME + "]" + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } + + // popBatchNums + if (this.defaultMQPushConsumer.getPopBatchNums() <= 0 || this.defaultMQPushConsumer.getPopBatchNums() > 32) { + throw new MQClientException( + "popBatchNums Out of range [1, 32]" + + FAQUrl.suggestTodo(FAQUrl.CLIENT_PARAMETER_CHECK_URL), + null); + } } private void copySubscription() throws MQClientException { @@ -812,8 +1199,7 @@ private void copySubscription() throws MQClientException { for (final Map.Entry entry : sub.entrySet()) { final String topic = entry.getKey(); final String subString = entry.getValue(); - SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(this.defaultMQPushConsumer.getConsumerGroup(), - topic, subString); + SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(topic, subString); this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData); } } @@ -827,8 +1213,7 @@ private void copySubscription() throws MQClientException { break; case CLUSTERING: final String retryTopic = MixAll.getRetryTopic(this.defaultMQPushConsumer.getConsumerGroup()); - SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(this.defaultMQPushConsumer.getConsumerGroup(), - retryTopic, SubscriptionData.SUB_ALL); + SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(retryTopic, SubscriptionData.SUB_ALL); this.rebalanceImpl.getSubscriptionInner().put(retryTopic, subscriptionData); break; default: @@ -844,6 +1229,9 @@ public MessageListener getMessageListenerInner() { } private void updateTopicSubscribeInfoWhenSubscriptionChanged() { + if (doNotUpdateTopicSubscribeInfoWhenSubscriptionChanged) { + return; + } Map subTable = this.getSubscriptionInner(); if (subTable != null) { for (final Map.Entry entry : subTable.entrySet()) { @@ -859,8 +1247,7 @@ public ConcurrentMap getSubscriptionInner() { public void subscribe(String topic, String subExpression) throws MQClientException { try { - SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(this.defaultMQPushConsumer.getConsumerGroup(), - topic, subExpression); + SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(topic, subExpression); this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData); if (this.mQClientFactory != null) { this.mQClientFactory.sendHeartbeatToAllBrokerWithLock(); @@ -872,8 +1259,7 @@ public void subscribe(String topic, String subExpression) throws MQClientExcepti public void subscribe(String topic, String fullClassName, String filterClassSource) throws MQClientException { try { - SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(this.defaultMQPushConsumer.getConsumerGroup(), - topic, "*"); + SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(topic, SubscriptionData.SUB_ALL); subscriptionData.setSubString(fullClassName); subscriptionData.setClassFilterMode(true); subscriptionData.setFilterClassSource(filterClassSource); @@ -940,12 +1326,11 @@ public void setConsumeOrderly(boolean consumeOrderly) { this.consumeOrderly = consumeOrderly; } - public void resetOffsetByTimeStamp(long timeStamp) - throws RemotingException, MQBrokerException, InterruptedException, MQClientException { + public void resetOffsetByTimeStamp(long timeStamp) throws MQClientException { for (String topic : rebalanceImpl.getSubscriptionInner().keySet()) { Set mqs = rebalanceImpl.getTopicSubscribeInfoTable().get(topic); - Map offsetTable = new HashMap(); - if (mqs != null) { + if (CollectionUtils.isNotEmpty(mqs)) { + Map offsetTable = new HashMap<>(mqs.size(), 1); for (MessageQueue mq : mqs) { long offset = searchOffset(mq, timeStamp); offsetTable.put(mq, offset); @@ -981,11 +1366,7 @@ public ConsumeFromWhere consumeFromWhere() { @Override public Set subscriptions() { - Set subSet = new HashSet(); - - subSet.addAll(this.rebalanceImpl.getSubscriptionInner().values()); - - return subSet; + return new HashSet<>(this.rebalanceImpl.getSubscriptionInner().values()); } @Override @@ -995,11 +1376,19 @@ public void doRebalance() { } } + @Override + public boolean tryRebalance() { + if (!this.pause) { + return this.rebalanceImpl.doRebalance(this.isConsumeOrderly()); + } + return false; + } + @Override public void persistConsumerOffset() { try { this.makeSureStateOK(); - Set mqs = new HashSet(); + Set mqs = new HashSet<>(); Set allocateMq = this.rebalanceImpl.getProcessQueueTable().keySet(); mqs.addAll(allocateMq); @@ -1063,6 +1452,17 @@ public ConsumerRunningInfo consumerRunningInfo() { info.getMqTable().put(mq, pqinfo); } + Iterator> popIt = this.rebalanceImpl.getPopProcessQueueTable().entrySet().iterator(); + while (popIt.hasNext()) { + Entry next = popIt.next(); + MessageQueue mq = next.getKey(); + PopProcessQueue pq = next.getValue(); + + PopProcessQueueInfo pqinfo = new PopProcessQueueInfo(); + pq.fillPopProcessQueueInfo(pqinfo); + info.getMqPopTable().put(mq, pqinfo); + } + for (SubscriptionData sd : subSet) { ConsumeStatus consumeStatus = this.mQClientFactory.getConsumerStatsManager().consumeStatus(this.groupName(), sd.getTopic()); info.getStatusTable().put(sd.getTopic(), consumeStatus); @@ -1121,7 +1521,7 @@ private long computeAccumulationTotal() { public List queryConsumeTimeSpan(final String topic) throws RemotingException, MQClientException, InterruptedException, MQBrokerException { - List queueTimeSpan = new ArrayList(); + List queueTimeSpan = new ArrayList<>(); TopicRouteData routeData = this.mQClientFactory.getMQClientAPIImpl().getTopicRouteInfoFromNameServer(topic, 3000); for (BrokerData brokerData : routeData.getBrokerDatas()) { String addr = brokerData.selectBrokerAddr(); @@ -1131,6 +1531,33 @@ public List queryConsumeTimeSpan(final String topic) return queueTimeSpan; } + public void tryResetPopRetryTopic(final List msgs, String consumerGroup) { + String popRetryPrefix = MixAll.RETRY_GROUP_TOPIC_PREFIX + consumerGroup + "_"; + for (MessageExt msg : msgs) { + if (msg.getTopic().startsWith(popRetryPrefix)) { + String normalTopic = KeyBuilder.parseNormalTopic(msg.getTopic(), consumerGroup); + if (normalTopic != null && !normalTopic.isEmpty()) { + msg.setTopic(normalTopic); + } + } + } + } + + + public void resetRetryAndNamespace(final List msgs, String consumerGroup) { + final String groupTopic = MixAll.getRetryTopic(consumerGroup); + for (MessageExt msg : msgs) { + String retryTopic = msg.getProperty(MessageConst.PROPERTY_RETRY_TOPIC); + if (retryTopic != null && groupTopic.equals(msg.getTopic())) { + msg.setTopic(retryTopic); + } + + if (StringUtils.isNotEmpty(this.defaultMQPushConsumer.getNamespace())) { + msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(), this.defaultMQPushConsumer.getNamespace())); + } + } + } + public ConsumeMessageService getConsumeMessageService() { return consumeMessageService; } @@ -1139,4 +1566,19 @@ public void setConsumeMessageService(ConsumeMessageService consumeMessageService this.consumeMessageService = consumeMessageService; } + + public void setPullTimeDelayMillsWhenException(long pullTimeDelayMillsWhenException) { + this.pullTimeDelayMillsWhenException = pullTimeDelayMillsWhenException; + } + + int[] getPopDelayLevel() { + return popDelayLevel; + } + + public MessageQueueListener getMessageQueueListener() { + if (null == defaultMQPushConsumer) { + return null; + } + return defaultMQPushConsumer.getMessageQueueListener(); + } } diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/MQConsumerInner.java b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/MQConsumerInner.java index c2e8a1dfc49..8fc1cc9059d 100644 --- a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/MQConsumerInner.java +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/MQConsumerInner.java @@ -19,10 +19,10 @@ import java.util.Set; import org.apache.rocketmq.common.consumer.ConsumeFromWhere; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.body.ConsumerRunningInfo; -import org.apache.rocketmq.common.protocol.heartbeat.ConsumeType; -import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; /** * Consumer inner interface @@ -40,6 +40,8 @@ public interface MQConsumerInner { void doRebalance(); + boolean tryRebalance(); + void persistConsumerOffset(); void updateTopicSubscribeInfo(final String topic, final Set info); diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/MessageQueueLock.java b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/MessageQueueLock.java index a02f1b6ef6e..0fc9c93b449 100644 --- a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/MessageQueueLock.java +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/MessageQueueLock.java @@ -24,19 +24,32 @@ * Message lock,strictly ensure the single queue only one thread at a time consuming */ public class MessageQueueLock { - private ConcurrentMap mqLockTable = - new ConcurrentHashMap(); + private ConcurrentMap> mqLockTable = + new ConcurrentHashMap<>(32); public Object fetchLockObject(final MessageQueue mq) { - Object objLock = this.mqLockTable.get(mq); - if (null == objLock) { - objLock = new Object(); - Object prevLock = this.mqLockTable.putIfAbsent(mq, objLock); + return fetchLockObject(mq, -1); + } + + public Object fetchLockObject(final MessageQueue mq, final int shardingKeyIndex) { + ConcurrentMap objMap = this.mqLockTable.get(mq); + if (null == objMap) { + objMap = new ConcurrentHashMap<>(32); + ConcurrentMap prevObjMap = this.mqLockTable.putIfAbsent(mq, objMap); + if (prevObjMap != null) { + objMap = prevObjMap; + } + } + + Object lock = objMap.get(shardingKeyIndex); + if (null == lock) { + lock = new Object(); + Object prevLock = objMap.putIfAbsent(shardingKeyIndex, lock); if (prevLock != null) { - objLock = prevLock; + lock = prevLock; } } - return objLock; + return lock; } } diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/MessageRequest.java b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/MessageRequest.java new file mode 100644 index 00000000000..a808538b019 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/MessageRequest.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.impl.consumer; + +import org.apache.rocketmq.common.message.MessageRequestMode; + +public interface MessageRequest { + MessageRequestMode getMessageRequestMode(); +} diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PopProcessQueue.java b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PopProcessQueue.java new file mode 100644 index 00000000000..50827545b69 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PopProcessQueue.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.impl.consumer; + +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.rocketmq.remoting.protocol.body.PopProcessQueueInfo; + +/** + * Queue consumption snapshot + */ +public class PopProcessQueue { + + private final static long PULL_MAX_IDLE_TIME = Long.parseLong(System.getProperty("rocketmq.client.pull.pullMaxIdleTime", "120000")); + + private long lastPopTimestamp = System.currentTimeMillis(); + private AtomicInteger waitAckCounter = new AtomicInteger(0); + private volatile boolean dropped = false; + + public long getLastPopTimestamp() { + return lastPopTimestamp; + } + + public void setLastPopTimestamp(long lastPopTimestamp) { + this.lastPopTimestamp = lastPopTimestamp; + } + + public void incFoundMsg(int count) { + this.waitAckCounter.getAndAdd(count); + } + + /** + * @return the value before decrement. + */ + public int ack() { + return this.waitAckCounter.getAndDecrement(); + } + + public void decFoundMsg(int count) { + this.waitAckCounter.addAndGet(count); + } + + public int getWaiAckMsgCount() { + return this.waitAckCounter.get(); + } + + public boolean isDropped() { + return dropped; + } + + public void setDropped(boolean dropped) { + this.dropped = dropped; + } + + public void fillPopProcessQueueInfo(final PopProcessQueueInfo info) { + info.setWaitAckCount(getWaiAckMsgCount()); + info.setDroped(isDropped()); + info.setLastPopTimestamp(getLastPopTimestamp()); + } + + public boolean isPullExpired() { + return (System.currentTimeMillis() - this.lastPopTimestamp) > PULL_MAX_IDLE_TIME; + } + + @Override + public String toString() { + return "PopProcessQueue[waitAckCounter:" + this.waitAckCounter.get() + + ", lastPopTimestamp:" + getLastPopTimestamp() + + ", drop:" + dropped + "]"; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PopRequest.java b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PopRequest.java new file mode 100644 index 00000000000..c47f2d020e7 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PopRequest.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.impl.consumer; + +import org.apache.rocketmq.common.constant.ConsumeInitMode; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.message.MessageRequestMode; + +public class PopRequest implements MessageRequest { + private String topic; + private String consumerGroup; + private MessageQueue messageQueue; + private PopProcessQueue popProcessQueue; + private boolean lockedFirst = false; + private int initMode = ConsumeInitMode.MAX; + + public boolean isLockedFirst() { + return lockedFirst; + } + + public void setLockedFirst(boolean lockedFirst) { + this.lockedFirst = lockedFirst; + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public MessageQueue getMessageQueue() { + return messageQueue; + } + + public void setMessageQueue(MessageQueue messageQueue) { + this.messageQueue = messageQueue; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public PopProcessQueue getPopProcessQueue() { + return popProcessQueue; + } + + public void setPopProcessQueue(PopProcessQueue popProcessQueue) { + this.popProcessQueue = popProcessQueue; + } + + public int getInitMode() { + return initMode; + } + + public void setInitMode(int initMode) { + this.initMode = initMode; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((topic == null) ? 0 : topic.hashCode()); + result = prime * result + ((consumerGroup == null) ? 0 : consumerGroup.hashCode()); + result = prime * result + ((messageQueue == null) ? 0 : messageQueue.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + + PopRequest other = (PopRequest) obj; + + if (topic == null) { + if (other.topic != null) + return false; + } else if (!topic.equals(other.topic)) { + return false; + } + + if (consumerGroup == null) { + if (other.consumerGroup != null) + return false; + } else if (!consumerGroup.equals(other.consumerGroup)) + return false; + + if (messageQueue == null) { + if (other.messageQueue != null) + return false; + } else if (!messageQueue.equals(other.messageQueue)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "PopRequest [topic=" + topic + ", consumerGroup=" + consumerGroup + ", messageQueue=" + messageQueue + "]"; + } + + @Override + public MessageRequestMode getMessageRequestMode() { + return MessageRequestMode.POP; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ProcessQueue.java b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ProcessQueue.java index 0cea1aea890..33e698b00c1 100644 --- a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ProcessQueue.java +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ProcessQueue.java @@ -22,17 +22,16 @@ import java.util.Map; import java.util.TreeMap; import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; -import org.apache.rocketmq.client.log.ClientLogger; import org.apache.rocketmq.common.message.MessageAccessor; import org.apache.rocketmq.common.message.MessageConst; import org.apache.rocketmq.common.message.MessageExt; -import org.apache.rocketmq.common.protocol.body.ProcessQueueInfo; -import org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.body.ProcessQueueInfo; /** * Queue consumption snapshot @@ -42,16 +41,16 @@ public class ProcessQueue { Long.parseLong(System.getProperty("rocketmq.client.rebalance.lockMaxLiveTime", "30000")); public final static long REBALANCE_LOCK_INTERVAL = Long.parseLong(System.getProperty("rocketmq.client.rebalance.lockInterval", "20000")); private final static long PULL_MAX_IDLE_TIME = Long.parseLong(System.getProperty("rocketmq.client.pull.pullMaxIdleTime", "120000")); - private final Logger log = ClientLogger.getLog(); - private final ReadWriteLock lockTreeMap = new ReentrantReadWriteLock(); - private final TreeMap msgTreeMap = new TreeMap(); + private final Logger log = LoggerFactory.getLogger(ProcessQueue.class); + private final ReadWriteLock treeMapLock = new ReentrantReadWriteLock(); + private final TreeMap msgTreeMap = new TreeMap<>(); private final AtomicLong msgCount = new AtomicLong(); private final AtomicLong msgSize = new AtomicLong(); - private final Lock lockConsume = new ReentrantLock(); + private final ReadWriteLock consumeLock = new ReentrantReadWriteLock(); /** * A subset of msgTreeMap, will only be used when orderly consume */ - private final TreeMap consumingMsgOrderlyTreeMap = new TreeMap(); + private final TreeMap consumingMsgOrderlyTreeMap = new TreeMap<>(); private final AtomicLong tryUnlockTimes = new AtomicLong(0); private volatile long queueOffsetMax = 0L; private volatile boolean dropped = false; @@ -74,35 +73,39 @@ public boolean isPullExpired() { * @param pushConsumer */ public void cleanExpiredMsg(DefaultMQPushConsumer pushConsumer) { - if (pushConsumer.getDefaultMQPushConsumerImpl().isConsumeOrderly()) { + if (pushConsumer.isConsumeOrderly()) { return; } - int loop = msgTreeMap.size() < 16 ? msgTreeMap.size() : 16; + int loop = Math.min(msgTreeMap.size(), 16); for (int i = 0; i < loop; i++) { MessageExt msg = null; try { - this.lockTreeMap.readLock().lockInterruptibly(); + this.treeMapLock.readLock().lockInterruptibly(); try { - if (!msgTreeMap.isEmpty() && System.currentTimeMillis() - Long.parseLong(MessageAccessor.getConsumeStartTimeStamp(msgTreeMap.firstEntry().getValue())) > pushConsumer.getConsumeTimeout() * 60 * 1000) { - msg = msgTreeMap.firstEntry().getValue(); - } else { - - break; + if (!msgTreeMap.isEmpty()) { + String consumeStartTimeStamp = MessageAccessor.getConsumeStartTimeStamp(msgTreeMap.firstEntry().getValue()); + if (StringUtils.isNotEmpty(consumeStartTimeStamp) && System.currentTimeMillis() - Long.parseLong(consumeStartTimeStamp) > pushConsumer.getConsumeTimeout() * 60 * 1000) { + msg = msgTreeMap.firstEntry().getValue(); + } } } finally { - this.lockTreeMap.readLock().unlock(); + this.treeMapLock.readLock().unlock(); } } catch (InterruptedException e) { log.error("getExpiredMsg exception", e); } + if (msg == null) { + break; + } + try { pushConsumer.sendMessageBack(msg, 3); log.info("send expire msg back. topic={}, msgId={}, storeHost={}, queueId={}, queueOffset={}", msg.getTopic(), msg.getMsgId(), msg.getStoreHost(), msg.getQueueId(), msg.getQueueOffset()); try { - this.lockTreeMap.writeLock().lockInterruptibly(); + this.treeMapLock.writeLock().lockInterruptibly(); try { if (!msgTreeMap.isEmpty() && msg.getQueueOffset() == msgTreeMap.firstKey()) { try { @@ -112,7 +115,7 @@ public void cleanExpiredMsg(DefaultMQPushConsumer pushConsumer) { } } } finally { - this.lockTreeMap.writeLock().unlock(); + this.treeMapLock.writeLock().unlock(); } } catch (InterruptedException e) { log.error("getExpiredMsg exception", e); @@ -126,7 +129,7 @@ public void cleanExpiredMsg(DefaultMQPushConsumer pushConsumer) { public boolean putMessage(final List msgs) { boolean dispatchToConsume = false; try { - this.lockTreeMap.writeLock().lockInterruptibly(); + this.treeMapLock.writeLock().lockInterruptibly(); try { int validMsgCnt = 0; for (MessageExt msg : msgs) { @@ -155,7 +158,7 @@ public boolean putMessage(final List msgs) { } } } finally { - this.lockTreeMap.writeLock().unlock(); + this.treeMapLock.writeLock().unlock(); } } catch (InterruptedException e) { log.error("putMessage exception", e); @@ -166,13 +169,13 @@ public boolean putMessage(final List msgs) { public long getMaxSpan() { try { - this.lockTreeMap.readLock().lockInterruptibly(); + this.treeMapLock.readLock().lockInterruptibly(); try { if (!this.msgTreeMap.isEmpty()) { return this.msgTreeMap.lastKey() - this.msgTreeMap.firstKey(); } } finally { - this.lockTreeMap.readLock().unlock(); + this.treeMapLock.readLock().unlock(); } } catch (InterruptedException e) { log.error("getMaxSpan exception", e); @@ -185,7 +188,7 @@ public long removeMessage(final List msgs) { long result = -1; final long now = System.currentTimeMillis(); try { - this.lockTreeMap.writeLock().lockInterruptibly(); + this.treeMapLock.writeLock().lockInterruptibly(); this.lastConsumeTimestamp = now; try { if (!msgTreeMap.isEmpty()) { @@ -195,17 +198,19 @@ public long removeMessage(final List msgs) { MessageExt prev = msgTreeMap.remove(msg.getQueueOffset()); if (prev != null) { removedCnt--; - msgSize.addAndGet(0 - msg.getBody().length); + msgSize.addAndGet(-msg.getBody().length); } } - msgCount.addAndGet(removedCnt); + if (msgCount.addAndGet(removedCnt) == 0) { + msgSize.set(0); + } if (!msgTreeMap.isEmpty()) { result = msgTreeMap.firstKey(); } } } finally { - this.lockTreeMap.writeLock().unlock(); + this.treeMapLock.writeLock().unlock(); } } catch (Throwable t) { log.error("removeMessage exception", t); @@ -244,12 +249,12 @@ public void setLocked(boolean locked) { public void rollback() { try { - this.lockTreeMap.writeLock().lockInterruptibly(); + this.treeMapLock.writeLock().lockInterruptibly(); try { this.msgTreeMap.putAll(this.consumingMsgOrderlyTreeMap); this.consumingMsgOrderlyTreeMap.clear(); } finally { - this.lockTreeMap.writeLock().unlock(); + this.treeMapLock.writeLock().unlock(); } } catch (InterruptedException e) { log.error("rollback exception", e); @@ -258,19 +263,22 @@ public void rollback() { public long commit() { try { - this.lockTreeMap.writeLock().lockInterruptibly(); + this.treeMapLock.writeLock().lockInterruptibly(); try { Long offset = this.consumingMsgOrderlyTreeMap.lastKey(); - msgCount.addAndGet(0 - this.consumingMsgOrderlyTreeMap.size()); - for (MessageExt msg : this.consumingMsgOrderlyTreeMap.values()) { - msgSize.addAndGet(0 - msg.getBody().length); + if (msgCount.addAndGet(-this.consumingMsgOrderlyTreeMap.size()) == 0) { + msgSize.set(0); + } else { + for (MessageExt msg : this.consumingMsgOrderlyTreeMap.values()) { + msgSize.addAndGet(-msg.getBody().length); + } } this.consumingMsgOrderlyTreeMap.clear(); if (offset != null) { return offset + 1; } } finally { - this.lockTreeMap.writeLock().unlock(); + this.treeMapLock.writeLock().unlock(); } } catch (InterruptedException e) { log.error("commit exception", e); @@ -279,27 +287,27 @@ public long commit() { return -1; } - public void makeMessageToCosumeAgain(List msgs) { + public void makeMessageToConsumeAgain(List msgs) { try { - this.lockTreeMap.writeLock().lockInterruptibly(); + this.treeMapLock.writeLock().lockInterruptibly(); try { for (MessageExt msg : msgs) { this.consumingMsgOrderlyTreeMap.remove(msg.getQueueOffset()); this.msgTreeMap.put(msg.getQueueOffset(), msg); } } finally { - this.lockTreeMap.writeLock().unlock(); + this.treeMapLock.writeLock().unlock(); } } catch (InterruptedException e) { log.error("makeMessageToCosumeAgain exception", e); } } - public List takeMessags(final int batchSize) { - List result = new ArrayList(batchSize); + public List takeMessages(final int batchSize) { + List result = new ArrayList<>(batchSize); final long now = System.currentTimeMillis(); try { - this.lockTreeMap.writeLock().lockInterruptibly(); + this.treeMapLock.writeLock().lockInterruptibly(); this.lastConsumeTimestamp = now; try { if (!this.msgTreeMap.isEmpty()) { @@ -318,7 +326,7 @@ public List takeMessags(final int batchSize) { consuming = false; } } finally { - this.lockTreeMap.writeLock().unlock(); + this.treeMapLock.writeLock().unlock(); } } catch (InterruptedException e) { log.error("take Messages exception", e); @@ -327,13 +335,34 @@ public List takeMessags(final int batchSize) { return result; } + /** + * Return the result that whether current message is exist in the process queue or not. + */ + public boolean containsMessage(MessageExt message) { + if (message == null) { + // should never reach here. + return false; + } + try { + this.treeMapLock.readLock().lockInterruptibly(); + try { + return this.msgTreeMap.containsKey(message.getQueueOffset()); + } finally { + this.treeMapLock.readLock().unlock(); + } + } catch (Throwable t) { + log.error("Failed to check message's existence in process queue, message={}", message, t); + } + return false; + } + public boolean hasTempMessage() { try { - this.lockTreeMap.readLock().lockInterruptibly(); + this.treeMapLock.readLock().lockInterruptibly(); try { return !this.msgTreeMap.isEmpty(); } finally { - this.lockTreeMap.readLock().unlock(); + this.treeMapLock.readLock().unlock(); } } catch (InterruptedException e) { } @@ -343,7 +372,7 @@ public boolean hasTempMessage() { public void clear() { try { - this.lockTreeMap.writeLock().lockInterruptibly(); + this.treeMapLock.writeLock().lockInterruptibly(); try { this.msgTreeMap.clear(); this.consumingMsgOrderlyTreeMap.clear(); @@ -351,7 +380,7 @@ public void clear() { this.msgSize.set(0); this.queueOffsetMax = 0L; } finally { - this.lockTreeMap.writeLock().unlock(); + this.treeMapLock.writeLock().unlock(); } } catch (InterruptedException e) { log.error("rollback exception", e); @@ -366,8 +395,8 @@ public void setLastLockTimestamp(long lastLockTimestamp) { this.lastLockTimestamp = lastLockTimestamp; } - public Lock getLockConsume() { - return lockConsume; + public ReadWriteLock getConsumeLock() { + return consumeLock; } public long getLastPullTimestamp() { @@ -396,14 +425,14 @@ public void incTryUnlockTimes() { public void fillProcessQueueInfo(final ProcessQueueInfo info) { try { - this.lockTreeMap.readLock().lockInterruptibly(); + this.treeMapLock.readLock().lockInterruptibly(); if (!this.msgTreeMap.isEmpty()) { info.setCachedMsgMinOffset(this.msgTreeMap.firstKey()); info.setCachedMsgMaxOffset(this.msgTreeMap.lastKey()); info.setCachedMsgCount(this.msgTreeMap.size()); - info.setCachedMsgSizeInMiB((int) (this.msgSize.get() / (1024 * 1024))); } + info.setCachedMsgSizeInMiB((int) (this.msgSize.get() / (1024 * 1024))); if (!this.consumingMsgOrderlyTreeMap.isEmpty()) { info.setTransactionMsgMinOffset(this.consumingMsgOrderlyTreeMap.firstKey()); @@ -420,7 +449,7 @@ public void fillProcessQueueInfo(final ProcessQueueInfo info) { info.setLastConsumeTimestamp(this.lastConsumeTimestamp); } catch (Exception e) { } finally { - this.lockTreeMap.readLock().unlock(); + this.treeMapLock.readLock().unlock(); } } @@ -431,4 +460,5 @@ public long getLastConsumeTimestamp() { public void setLastConsumeTimestamp(long lastConsumeTimestamp) { this.lastConsumeTimestamp = lastConsumeTimestamp; } + } diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PullAPIWrapper.java b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PullAPIWrapper.java index bbdf27d7a55..2bd0d9994e5 100644 --- a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PullAPIWrapper.java +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PullAPIWrapper.java @@ -23,6 +23,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicLong; +import org.apache.rocketmq.client.consumer.PopCallback; import org.apache.rocketmq.client.consumer.PullCallback; import org.apache.rocketmq.client.consumer.PullResult; import org.apache.rocketmq.client.consumer.PullStatus; @@ -33,7 +34,6 @@ import org.apache.rocketmq.client.impl.CommunicationMode; import org.apache.rocketmq.client.impl.FindBrokerResult; import org.apache.rocketmq.client.impl.factory.MQClientInstance; -import org.apache.rocketmq.client.log.ClientLogger; import org.apache.rocketmq.common.MQVersion; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.filter.ExpressionType; @@ -42,24 +42,27 @@ import org.apache.rocketmq.common.message.MessageDecoder; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.header.PullMessageRequestHeader; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; -import org.apache.rocketmq.common.protocol.route.TopicRouteData; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; import org.apache.rocketmq.common.sysflag.PullSysFlag; import org.apache.rocketmq.remoting.exception.RemotingException; -import org.slf4j.Logger; +import org.apache.rocketmq.remoting.protocol.header.PopMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; public class PullAPIWrapper { - private final Logger log = ClientLogger.getLog(); + private static final Logger log = LoggerFactory.getLogger(PullAPIWrapper.class); private final MQClientInstance mQClientFactory; private final String consumerGroup; private final boolean unitMode; private ConcurrentMap pullFromWhichNodeTable = - new ConcurrentHashMap(32); + new ConcurrentHashMap<>(32); private volatile boolean connectBrokerByUser = false; private volatile long defaultBrokerId = MixAll.MASTER_ID; - private Random random = new Random(System.currentTimeMillis()); - private ArrayList filterMessageHookList = new ArrayList(); + private Random random = new Random(System.nanoTime()); + private ArrayList filterMessageHookList = new ArrayList<>(); public PullAPIWrapper(MQClientInstance mQClientFactory, String consumerGroup, boolean unitMode) { this.mQClientFactory = mQClientFactory; @@ -74,11 +77,41 @@ public PullResult processPullResult(final MessageQueue mq, final PullResult pull this.updatePullFromWhichNode(mq, pullResultExt.getSuggestWhichBrokerId()); if (PullStatus.FOUND == pullResult.getPullStatus()) { ByteBuffer byteBuffer = ByteBuffer.wrap(pullResultExt.getMessageBinary()); - List msgList = MessageDecoder.decodes(byteBuffer); + List msgList = MessageDecoder.decodesBatch( + byteBuffer, + this.mQClientFactory.getClientConfig().isDecodeReadBody(), + this.mQClientFactory.getClientConfig().isDecodeDecompressBody(), + true + ); + + boolean needDecodeInnerMessage = false; + for (MessageExt messageExt: msgList) { + if (MessageSysFlag.check(messageExt.getSysFlag(), MessageSysFlag.INNER_BATCH_FLAG) + && MessageSysFlag.check(messageExt.getSysFlag(), MessageSysFlag.NEED_UNWRAP_FLAG)) { + needDecodeInnerMessage = true; + break; + } + } + if (needDecodeInnerMessage) { + List innerMsgList = new ArrayList<>(); + try { + for (MessageExt messageExt: msgList) { + if (MessageSysFlag.check(messageExt.getSysFlag(), MessageSysFlag.INNER_BATCH_FLAG) + && MessageSysFlag.check(messageExt.getSysFlag(), MessageSysFlag.NEED_UNWRAP_FLAG)) { + MessageDecoder.decodeMessage(messageExt, innerMsgList); + } else { + innerMsgList.add(messageExt); + } + } + msgList = innerMsgList; + } catch (Throwable t) { + log.error("Try to decode the inner batch failed for {}", pullResult.toString(), t); + } + } List msgListFilterAgain = msgList; if (!subscriptionData.getTagsSet().isEmpty() && !subscriptionData.isClassFilterMode()) { - msgListFilterAgain = new ArrayList(msgList.size()); + msgListFilterAgain = new ArrayList<>(msgList.size()); for (MessageExt msg : msgList) { if (msg.getTags() != null) { if (subscriptionData.getTagsSet().contains(msg.getTags())) { @@ -96,10 +129,19 @@ public PullResult processPullResult(final MessageQueue mq, final PullResult pull } for (MessageExt msg : msgListFilterAgain) { + String traFlag = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED); + if (Boolean.parseBoolean(traFlag)) { + msg.setTransactionId(msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX)); + } MessageAccessor.putProperty(msg, MessageConst.PROPERTY_MIN_OFFSET, Long.toString(pullResult.getMinOffset())); MessageAccessor.putProperty(msg, MessageConst.PROPERTY_MAX_OFFSET, Long.toString(pullResult.getMaxOffset())); + msg.setBrokerName(mq.getBrokerName()); + msg.setQueueId(mq.getQueueId()); + if (pullResultExt.getOffsetDelta() != null) { + msg.setQueueOffset(pullResultExt.getOffsetDelta() + msg.getQueueOffset()); + } } pullResultExt.setMsgFoundList(msgListFilterAgain); @@ -142,6 +184,7 @@ public PullResult pullKernelImpl( final long subVersion, final long offset, final int maxNums, + final int maxSizeInBytes, final int sysFlag, final long commitOffset, final long brokerSuspendMaxTimeMillis, @@ -150,15 +193,16 @@ public PullResult pullKernelImpl( final PullCallback pullCallback ) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { FindBrokerResult findBrokerResult = - this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(), + this.mQClientFactory.findBrokerAddressInSubscribe(this.mQClientFactory.getBrokerNameFromMessageQueue(mq), this.recalculatePullFromWhichNode(mq), false); if (null == findBrokerResult) { this.mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic()); findBrokerResult = - this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(), + this.mQClientFactory.findBrokerAddressInSubscribe(this.mQClientFactory.getBrokerNameFromMessageQueue(mq), this.recalculatePullFromWhichNode(mq), false); } + if (findBrokerResult != null) { { // check version @@ -185,11 +229,13 @@ public PullResult pullKernelImpl( requestHeader.setSuspendTimeoutMillis(brokerSuspendMaxTimeMillis); requestHeader.setSubscription(subExpression); requestHeader.setSubVersion(subVersion); + requestHeader.setMaxMsgBytes(maxSizeInBytes); requestHeader.setExpressionType(expressionType); + requestHeader.setBrokerName(mq.getBrokerName()); String brokerAddr = findBrokerResult.getBrokerAddr(); if (PullSysFlag.hasClassFilterFlag(sysFlagInner)) { - brokerAddr = computPullFromWhichFilterServer(mq.getTopic(), brokerAddr); + brokerAddr = computePullFromWhichFilterServer(mq.getTopic(), brokerAddr); } PullResult pullResult = this.mQClientFactory.getMQClientAPIImpl().pullMessage( @@ -206,30 +252,32 @@ public PullResult pullKernelImpl( } public PullResult pullKernelImpl( - final MessageQueue mq, + MessageQueue mq, final String subExpression, + final String expressionType, final long subVersion, - final long offset, + long offset, final int maxNums, final int sysFlag, - final long commitOffset, + long commitOffset, final long brokerSuspendMaxTimeMillis, final long timeoutMillis, final CommunicationMode communicationMode, - final PullCallback pullCallback + PullCallback pullCallback ) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { return pullKernelImpl( - mq, - subExpression, - ExpressionType.TAG, - subVersion, offset, - maxNums, - sysFlag, - commitOffset, - brokerSuspendMaxTimeMillis, - timeoutMillis, - communicationMode, - pullCallback + mq, + subExpression, + expressionType, + subVersion, offset, + maxNums, + Integer.MAX_VALUE, + sysFlag, + commitOffset, + brokerSuspendMaxTimeMillis, + timeoutMillis, + communicationMode, + pullCallback ); } @@ -246,7 +294,7 @@ public long recalculatePullFromWhichNode(final MessageQueue mq) { return MixAll.MASTER_ID; } - private String computPullFromWhichFilterServer(final String topic, final String brokerAddr) + private String computePullFromWhichFilterServer(final String topic, final String brokerAddr) throws MQClientException { ConcurrentMap topicRouteTable = this.mQClientFactory.getTopicRouteTable(); if (topicRouteTable != null) { @@ -292,4 +340,57 @@ public long getDefaultBrokerId() { public void setDefaultBrokerId(long defaultBrokerId) { this.defaultBrokerId = defaultBrokerId; } + + + /** + * + * @param mq + * @param invisibleTime + * @param maxNums + * @param consumerGroup + * @param timeout + * @param popCallback + * @param poll + * @param initMode + // * @param expressionType + // * @param expression + * @param order + * @throws MQClientException + * @throws RemotingException + * @throws InterruptedException + */ + public void popAsync(MessageQueue mq, long invisibleTime, int maxNums, String consumerGroup, + long timeout, PopCallback popCallback, boolean poll, int initMode, boolean order, String expressionType, String expression) + throws MQClientException, RemotingException, InterruptedException { + FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(), MixAll.MASTER_ID, true); + if (null == findBrokerResult) { + this.mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic()); + findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(), MixAll.MASTER_ID, true); + } + if (findBrokerResult != null) { + PopMessageRequestHeader requestHeader = new PopMessageRequestHeader(); + requestHeader.setConsumerGroup(consumerGroup); + requestHeader.setTopic(mq.getTopic()); + requestHeader.setQueueId(mq.getQueueId()); + requestHeader.setMaxMsgNums(maxNums); + requestHeader.setInvisibleTime(invisibleTime); + requestHeader.setInitMode(initMode); + requestHeader.setExpType(expressionType); + requestHeader.setExp(expression); + requestHeader.setOrder(order); + requestHeader.setBrokerName(mq.getBrokerName()); + //give 1000 ms for server response + if (poll) { + requestHeader.setPollTime(timeout); + requestHeader.setBornTime(System.currentTimeMillis()); + // timeout + 10s, fix the too earlier timeout of client when long polling. + timeout += 10 * 1000; + } + String brokerAddr = findBrokerResult.getBrokerAddr(); + this.mQClientFactory.getMQClientAPIImpl().popMessageAsync(mq.getBrokerName(), brokerAddr, requestHeader, timeout, popCallback); + return; + } + throw new MQClientException("The broker[" + mq.getBrokerName() + "] not exist", null); + } + } diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PullMessageService.java b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PullMessageService.java index ed4b8371820..ec6ede6bdea 100644 --- a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PullMessageService.java +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PullMessageService.java @@ -19,50 +19,83 @@ import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import org.apache.rocketmq.client.impl.factory.MQClientInstance; -import org.apache.rocketmq.client.log.ClientLogger; import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.message.MessageRequestMode; import org.apache.rocketmq.common.utils.ThreadUtils; -import org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; public class PullMessageService extends ServiceThread { - private final Logger log = ClientLogger.getLog(); - private final LinkedBlockingQueue pullRequestQueue = new LinkedBlockingQueue(); + private final Logger logger = LoggerFactory.getLogger(PullMessageService.class); + private final LinkedBlockingQueue messageRequestQueue = new LinkedBlockingQueue<>(); + private final MQClientInstance mQClientFactory; private final ScheduledExecutorService scheduledExecutorService = Executors - .newSingleThreadScheduledExecutor(new ThreadFactory() { - @Override - public Thread newThread(Runnable r) { - return new Thread(r, "PullMessageServiceScheduledThread"); - } - }); + .newSingleThreadScheduledExecutor(new ThreadFactoryImpl("PullMessageServiceScheduledThread")); public PullMessageService(MQClientInstance mQClientFactory) { this.mQClientFactory = mQClientFactory; } public void executePullRequestLater(final PullRequest pullRequest, final long timeDelay) { - this.scheduledExecutorService.schedule(new Runnable() { - - @Override - public void run() { - PullMessageService.this.executePullRequestImmediately(pullRequest); - } - }, timeDelay, TimeUnit.MILLISECONDS); + if (!isStopped()) { + this.scheduledExecutorService.schedule(new Runnable() { + @Override + public void run() { + PullMessageService.this.executePullRequestImmediately(pullRequest); + } + }, timeDelay, TimeUnit.MILLISECONDS); + } else { + logger.warn("PullMessageServiceScheduledThread has shutdown"); + } } public void executePullRequestImmediately(final PullRequest pullRequest) { try { - this.pullRequestQueue.put(pullRequest); + this.messageRequestQueue.put(pullRequest); } catch (InterruptedException e) { - log.error("executePullRequestImmediately pullRequestQueue.put", e); + logger.error("executePullRequestImmediately pullRequestQueue.put", e); + } + } + + public void executePopPullRequestLater(final PopRequest popRequest, final long timeDelay) { + if (!isStopped()) { + this.scheduledExecutorService.schedule(new Runnable() { + @Override + public void run() { + PullMessageService.this.executePopPullRequestImmediately(popRequest); + } + }, timeDelay, TimeUnit.MILLISECONDS); + } else { + logger.warn("PullMessageServiceScheduledThread has shutdown"); + } + } + + public void executePopPullRequestImmediately(final PopRequest popRequest) { + try { + this.messageRequestQueue.put(popRequest); + } catch (InterruptedException e) { + logger.error("executePullRequestImmediately pullRequestQueue.put", e); } } public void executeTaskLater(final Runnable r, final long timeDelay) { - this.scheduledExecutorService.schedule(r, timeDelay, TimeUnit.MILLISECONDS); + if (!isStopped()) { + this.scheduledExecutorService.schedule(r, timeDelay, TimeUnit.MILLISECONDS); + } else { + logger.warn("PullMessageServiceScheduledThread has shutdown"); + } + } + + public void executeTask(final Runnable r) { + if (!isStopped()) { + this.scheduledExecutorService.execute(r); + } else { + logger.warn("PullMessageServiceScheduledThread has shutdown"); + } } public ScheduledExecutorService getScheduledExecutorService() { @@ -75,27 +108,39 @@ private void pullMessage(final PullRequest pullRequest) { DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer; impl.pullMessage(pullRequest); } else { - log.warn("No matched consumer for the PullRequest {}, drop it", pullRequest); + logger.warn("No matched consumer for the PullRequest {}, drop it", pullRequest); + } + } + + private void popMessage(final PopRequest popRequest) { + final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(popRequest.getConsumerGroup()); + if (consumer != null) { + DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer; + impl.popMessage(popRequest); + } else { + logger.warn("No matched consumer for the PopRequest {}, drop it", popRequest); } } @Override public void run() { - log.info(this.getServiceName() + " service started"); + logger.info(this.getServiceName() + " service started"); while (!this.isStopped()) { try { - PullRequest pullRequest = this.pullRequestQueue.take(); - if (pullRequest != null) { - this.pullMessage(pullRequest); + MessageRequest messageRequest = this.messageRequestQueue.take(); + if (messageRequest.getMessageRequestMode() == MessageRequestMode.POP) { + this.popMessage((PopRequest) messageRequest); + } else { + this.pullMessage((PullRequest) messageRequest); } - } catch (InterruptedException e) { + } catch (InterruptedException ignored) { } catch (Exception e) { - log.error("Pull Message Service Run Method exception", e); + logger.error("Pull Message Service Run Method exception", e); } } - log.info(this.getServiceName() + " service end"); + logger.info(this.getServiceName() + " service end"); } @Override diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PullRequest.java b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PullRequest.java index 10aded07625..b90192b9925 100644 --- a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PullRequest.java +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PullRequest.java @@ -17,20 +17,21 @@ package org.apache.rocketmq.client.impl.consumer; import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.message.MessageRequestMode; -public class PullRequest { +public class PullRequest implements MessageRequest { private String consumerGroup; private MessageQueue messageQueue; private ProcessQueue processQueue; private long nextOffset; - private boolean lockedFirst = false; + private boolean previouslyLocked = false; - public boolean isLockedFirst() { - return lockedFirst; + public boolean isPreviouslyLocked() { + return previouslyLocked; } - public void setLockedFirst(boolean lockedFirst) { - this.lockedFirst = lockedFirst; + public void setPreviouslyLocked(boolean previouslyLocked) { + this.previouslyLocked = previouslyLocked; } public String getConsumerGroup() { @@ -101,4 +102,9 @@ public ProcessQueue getProcessQueue() { public void setProcessQueue(ProcessQueue processQueue) { this.processQueue = processQueue; } + + @Override + public MessageRequestMode getMessageRequestMode() { + return MessageRequestMode.PULL; + } } diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PullResultExt.java b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PullResultExt.java index c34a68f9ab3..45538eda8d3 100644 --- a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PullResultExt.java +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/PullResultExt.java @@ -25,11 +25,23 @@ public class PullResultExt extends PullResult { private final long suggestWhichBrokerId; private byte[] messageBinary; + private final Long offsetDelta; + public PullResultExt(PullStatus pullStatus, long nextBeginOffset, long minOffset, long maxOffset, List msgFoundList, final long suggestWhichBrokerId, final byte[] messageBinary) { + this(pullStatus, nextBeginOffset, minOffset, maxOffset, msgFoundList, suggestWhichBrokerId, messageBinary, 0L); + } + + public PullResultExt(PullStatus pullStatus, long nextBeginOffset, long minOffset, long maxOffset, + List msgFoundList, final long suggestWhichBrokerId, final byte[] messageBinary, final Long offsetDelta) { super(pullStatus, nextBeginOffset, minOffset, maxOffset, msgFoundList); this.suggestWhichBrokerId = suggestWhichBrokerId; this.messageBinary = messageBinary; + this.offsetDelta = offsetDelta; + } + + public Long getOffsetDelta() { + return offsetDelta; } public byte[] getMessageBinary() { diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/RebalanceImpl.java b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/RebalanceImpl.java index b555771c141..53addc5f50c 100644 --- a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/RebalanceImpl.java +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/RebalanceImpl.java @@ -28,32 +28,43 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.apache.rocketmq.client.consumer.AllocateMessageQueueStrategy; +import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.impl.FindBrokerResult; import org.apache.rocketmq.client.impl.factory.MQClientInstance; -import org.apache.rocketmq.client.log.ClientLogger; +import org.apache.rocketmq.common.KeyBuilder; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.body.LockBatchRequestBody; -import org.apache.rocketmq.common.protocol.body.UnlockBatchRequestBody; -import org.apache.rocketmq.common.protocol.heartbeat.ConsumeType; -import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; -import org.slf4j.Logger; - -/** - * Base class for rebalance algorithm - */ +import org.apache.rocketmq.common.message.MessageQueueAssignment; +import org.apache.rocketmq.common.message.MessageRequestMode; +import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; +import org.apache.rocketmq.remoting.protocol.body.LockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.body.UnlockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.filter.FilterAPI; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + public abstract class RebalanceImpl { - protected static final Logger log = ClientLogger.getLog(); - protected final ConcurrentMap processQueueTable = new ConcurrentHashMap(64); + protected static final Logger log = LoggerFactory.getLogger(RebalanceImpl.class); + + protected final ConcurrentMap processQueueTable = new ConcurrentHashMap<>(64); + protected final ConcurrentMap popProcessQueueTable = new ConcurrentHashMap<>(64); + protected final ConcurrentMap> topicSubscribeInfoTable = - new ConcurrentHashMap>(); + new ConcurrentHashMap<>(); protected final ConcurrentMap subscriptionInner = - new ConcurrentHashMap(); + new ConcurrentHashMap<>(); protected String consumerGroup; protected MessageModel messageModel; protected AllocateMessageQueueStrategy allocateMessageQueueStrategy; protected MQClientInstance mQClientFactory; + private static final int TIMEOUT_CHECK_TIMES = 3; + private static final int QUERY_ASSIGNMENT_TIMEOUT = 3000; + + private Map topicBrokerRebalance = new ConcurrentHashMap<>(); + private Map topicClientRebalance = new ConcurrentHashMap<>(); public RebalanceImpl(String consumerGroup, MessageModel messageModel, AllocateMessageQueueStrategy allocateMessageQueueStrategy, @@ -65,7 +76,7 @@ public RebalanceImpl(String consumerGroup, MessageModel messageModel, } public void unlock(final MessageQueue mq, final boolean oneway) { - FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(), MixAll.MASTER_ID, true); + FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(this.mQClientFactory.getBrokerNameFromMessageQueue(mq), MixAll.MASTER_ID, true); if (findBrokerResult != null) { UnlockBatchRequestBody requestBody = new UnlockBatchRequestBody(); requestBody.setConsumerGroup(this.consumerGroup); @@ -91,8 +102,9 @@ public void unlockAll(final boolean oneway) { final String brokerName = entry.getKey(); final Set mqs = entry.getValue(); - if (mqs.isEmpty()) + if (mqs.isEmpty()) { continue; + } FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(brokerName, MixAll.MASTER_ID, true); if (findBrokerResult != null) { @@ -119,11 +131,20 @@ public void unlockAll(final boolean oneway) { } private HashMap> buildProcessQueueTableByBrokerName() { - HashMap> result = new HashMap>(); - for (MessageQueue mq : this.processQueueTable.keySet()) { - Set mqs = result.get(mq.getBrokerName()); + HashMap> result = new HashMap<>(); + + for (Map.Entry entry : this.processQueueTable.entrySet()) { + MessageQueue mq = entry.getKey(); + ProcessQueue pq = entry.getValue(); + + if (pq.isDropped()) { + continue; + } + + String destBrokerName = this.mQClientFactory.getBrokerNameFromMessageQueue(mq); + Set mqs = result.get(destBrokerName); if (null == mqs) { - mqs = new HashSet(); + mqs = new HashSet<>(); result.put(mq.getBrokerName(), mqs); } @@ -134,7 +155,7 @@ public void unlockAll(final boolean oneway) { } public boolean lock(final MessageQueue mq) { - FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(), MixAll.MASTER_ID, true); + FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(this.mQClientFactory.getBrokerNameFromMessageQueue(mq), MixAll.MASTER_ID, true); if (findBrokerResult != null) { LockBatchRequestBody requestBody = new LockBatchRequestBody(); requestBody.setConsumerGroup(this.consumerGroup); @@ -153,10 +174,7 @@ public boolean lock(final MessageQueue mq) { } boolean lockOK = lockedMq.contains(mq); - log.info("the message queue lock {}, {} {}", - lockOK ? "OK" : "Failed", - this.consumerGroup, - mq); + log.info("message queue lock {}, {} {}", lockOK ? "OK" : "Failed", this.consumerGroup, mq); return lockOK; } catch (Exception e) { log.error("lockBatchMQ exception, " + mq, e); @@ -175,8 +193,9 @@ public void lockAll() { final String brokerName = entry.getKey(); final Set mqs = entry.getValue(); - if (mqs.isEmpty()) + if (mqs.isEmpty()) { continue; + } FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(brokerName, MixAll.MASTER_ID, true); if (findBrokerResult != null) { @@ -189,21 +208,16 @@ public void lockAll() { Set lockOKMQSet = this.mQClientFactory.getMQClientAPIImpl().lockBatchMQ(findBrokerResult.getBrokerAddr(), requestBody, 1000); - for (MessageQueue mq : lockOKMQSet) { + for (MessageQueue mq : mqs) { ProcessQueue processQueue = this.processQueueTable.get(mq); if (processQueue != null) { - if (!processQueue.isLocked()) { - log.info("the message queue locked OK, Group: {} {}", this.consumerGroup, mq); - } - - processQueue.setLocked(true); - processQueue.setLastLockTimestamp(System.currentTimeMillis()); - } - } - for (MessageQueue mq : mqs) { - if (!lockOKMQSet.contains(mq)) { - ProcessQueue processQueue = this.processQueueTable.get(mq); - if (processQueue != null) { + if (lockOKMQSet.contains(mq)) { + if (!processQueue.isLocked()) { + log.info("the message queue locked OK, Group: {} {}", this.consumerGroup, mq); + } + processQueue.setLocked(true); + processQueue.setLastLockTimestamp(System.currentTimeMillis()); + } else { processQueue.setLocked(false); log.warn("the message queue locked Failed, Group: {} {}", this.consumerGroup, mq); } @@ -216,29 +230,80 @@ public void lockAll() { } } - public void doRebalance(final boolean isOrder) { + public boolean clientRebalance(String topic) { + return true; + } + + public boolean doRebalance(final boolean isOrder) { + boolean balanced = true; Map subTable = this.getSubscriptionInner(); if (subTable != null) { for (final Map.Entry entry : subTable.entrySet()) { final String topic = entry.getKey(); try { - this.rebalanceByTopic(topic, isOrder); + if (!clientRebalance(topic) && tryQueryAssignment(topic)) { + boolean result = this.getRebalanceResultFromBroker(topic, isOrder); + if (!result) { + balanced = false; + } + } else { + boolean result = this.rebalanceByTopic(topic, isOrder); + if (!result) { + balanced = false; + } + } } catch (Throwable e) { if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { - log.warn("rebalanceByTopic Exception", e); + log.warn("rebalance Exception", e); + balanced = false; } } } } this.truncateMessageQueueNotMyTopic(); + + return balanced; + } + + private boolean tryQueryAssignment(String topic) { + if (topicClientRebalance.containsKey(topic)) { + return false; + } + + if (topicBrokerRebalance.containsKey(topic)) { + return true; + } + String strategyName = allocateMessageQueueStrategy != null ? allocateMessageQueueStrategy.getName() : null; + int retryTimes = 0; + while (retryTimes++ < TIMEOUT_CHECK_TIMES) { + try { + Set resultSet = mQClientFactory.queryAssignment(topic, consumerGroup, + strategyName, messageModel, QUERY_ASSIGNMENT_TIMEOUT / TIMEOUT_CHECK_TIMES * retryTimes); + topicBrokerRebalance.put(topic, topic); + return true; + } catch (Throwable t) { + if (!(t instanceof RemotingTimeoutException)) { + log.error("tryQueryAssignment error.", t); + topicClientRebalance.put(topic, topic); + return false; + } + } + } + if (retryTimes >= TIMEOUT_CHECK_TIMES) { + // if never success before and timeout exceed TIMEOUT_CHECK_TIMES, force client rebalance + topicClientRebalance.put(topic, topic); + return false; + } + return true; } public ConcurrentMap getSubscriptionInner() { return subscriptionInner; } - private void rebalanceByTopic(final String topic, final boolean isOrder) { + private boolean rebalanceByTopic(final String topic, final boolean isOrder) { + boolean balanced = true; switch (messageModel) { case BROADCASTING: { Set mqSet = this.topicSubscribeInfoTable.get(topic); @@ -246,13 +311,12 @@ private void rebalanceByTopic(final String topic, final boolean isOrder) { boolean changed = this.updateProcessQueueTableInRebalance(topic, mqSet, isOrder); if (changed) { this.messageQueueChanged(topic, mqSet, mqSet); - log.info("messageQueueChanged {} {} {} {}", - consumerGroup, - topic, - mqSet, - mqSet); + log.info("messageQueueChanged {} {} {} {}", consumerGroup, topic, mqSet, mqSet); } + + balanced = mqSet.equals(getWorkingMessageQueue(topic)); } else { + this.messageQueueChanged(topic, Collections.emptySet(), Collections.emptySet()); log.warn("doRebalance, {}, but the topic[{}] not exist.", consumerGroup, topic); } break; @@ -262,6 +326,7 @@ private void rebalanceByTopic(final String topic, final boolean isOrder) { List cidAll = this.mQClientFactory.findConsumerIdList(topic, consumerGroup); if (null == mqSet) { if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + this.messageQueueChanged(topic, Collections.emptySet(), Collections.emptySet()); log.warn("doRebalance, {}, but the topic[{}] not exist.", consumerGroup, topic); } } @@ -271,7 +336,7 @@ private void rebalanceByTopic(final String topic, final boolean isOrder) { } if (mqSet != null && cidAll != null) { - List mqAll = new ArrayList(); + List mqAll = new ArrayList<>(); mqAll.addAll(mqSet); Collections.sort(mqAll); @@ -287,12 +352,11 @@ private void rebalanceByTopic(final String topic, final boolean isOrder) { mqAll, cidAll); } catch (Throwable e) { - log.error("AllocateMessageQueueStrategy.allocate Exception. allocateMessageQueueStrategyName={}", strategy.getName(), - e); - return; + log.error("allocate message queue exception. strategy name: {}, ex: {}", strategy.getName(), e); + return false; } - Set allocateResultSet = new HashSet(); + Set allocateResultSet = new HashSet<>(); if (allocateResult != null) { allocateResultSet.addAll(allocateResult); } @@ -300,17 +364,76 @@ private void rebalanceByTopic(final String topic, final boolean isOrder) { boolean changed = this.updateProcessQueueTableInRebalance(topic, allocateResultSet, isOrder); if (changed) { log.info( - "rebalanced result changed. allocateMessageQueueStrategyName={}, group={}, topic={}, clientId={}, mqAllSize={}, cidAllSize={}, rebalanceResultSize={}, rebalanceResultSet={}", + "client rebalanced result changed. allocateMessageQueueStrategyName={}, group={}, topic={}, clientId={}, mqAllSize={}, cidAllSize={}, rebalanceResultSize={}, rebalanceResultSet={}", strategy.getName(), consumerGroup, topic, this.mQClientFactory.getClientId(), mqSet.size(), cidAll.size(), allocateResultSet.size(), allocateResultSet); this.messageQueueChanged(topic, mqSet, allocateResultSet); } + + balanced = allocateResultSet.equals(getWorkingMessageQueue(topic)); } break; } default: break; } + + return balanced; + } + + private boolean getRebalanceResultFromBroker(final String topic, final boolean isOrder) { + String strategyName = this.allocateMessageQueueStrategy.getName(); + Set messageQueueAssignments; + try { + messageQueueAssignments = this.mQClientFactory.queryAssignment(topic, consumerGroup, + strategyName, messageModel, QUERY_ASSIGNMENT_TIMEOUT); + } catch (Exception e) { + log.error("allocate message queue exception. strategy name: {}, ex: {}", strategyName, e); + return false; + } + + // null means invalid result, we should skip the update logic + if (messageQueueAssignments == null) { + return false; + } + Set mqSet = new HashSet<>(); + for (MessageQueueAssignment messageQueueAssignment : messageQueueAssignments) { + if (messageQueueAssignment.getMessageQueue() != null) { + mqSet.add(messageQueueAssignment.getMessageQueue()); + } + } + Set mqAll = null; + boolean changed = this.updateMessageQueueAssignment(topic, messageQueueAssignments, isOrder); + if (changed) { + log.info("broker rebalanced result changed. allocateMessageQueueStrategyName={}, group={}, topic={}, clientId={}, assignmentSet={}", + strategyName, consumerGroup, topic, this.mQClientFactory.getClientId(), messageQueueAssignments); + this.messageQueueChanged(topic, mqAll, mqSet); + } + + return mqSet.equals(getWorkingMessageQueue(topic)); + } + + private Set getWorkingMessageQueue(String topic) { + Set queueSet = new HashSet<>(); + for (Entry entry : this.processQueueTable.entrySet()) { + MessageQueue mq = entry.getKey(); + ProcessQueue pq = entry.getValue(); + + if (mq.getTopic().equals(topic) && !pq.isDropped()) { + queueSet.add(mq); + } + } + + for (Entry entry : this.popProcessQueueTable.entrySet()) { + MessageQueue mq = entry.getKey(); + PopProcessQueue pq = entry.getValue(); + + if (mq.getTopic().equals(topic) && !pq.isDropped()) { + queueSet.add(mq); + } + } + + return queueSet; } private void truncateMessageQueueNotMyTopic() { @@ -326,12 +449,39 @@ private void truncateMessageQueueNotMyTopic() { } } } + + for (MessageQueue mq : this.popProcessQueueTable.keySet()) { + if (!subTable.containsKey(mq.getTopic())) { + + PopProcessQueue pq = this.popProcessQueueTable.remove(mq); + if (pq != null) { + pq.setDropped(true); + log.info("doRebalance, {}, truncateMessageQueueNotMyTopic remove unnecessary pop mq, {}", consumerGroup, mq); + } + } + } + + Iterator> clientIter = topicClientRebalance.entrySet().iterator(); + while (clientIter.hasNext()) { + if (!subTable.containsKey(clientIter.next().getKey())) { + clientIter.remove(); + } + } + + Iterator> brokerIter = topicBrokerRebalance.entrySet().iterator(); + while (brokerIter.hasNext()) { + if (!subTable.containsKey(brokerIter.next().getKey())) { + brokerIter.remove(); + } + } } private boolean updateProcessQueueTableInRebalance(final String topic, final Set mqSet, final boolean isOrder) { boolean changed = false; + // drop process queues no longer belong me + HashMap removeQueueMap = new HashMap<>(this.processQueueTable.size()); Iterator> it = this.processQueueTable.entrySet().iterator(); while (it.hasNext()) { Entry next = it.next(); @@ -341,41 +491,42 @@ private boolean updateProcessQueueTableInRebalance(final String topic, final Set if (mq.getTopic().equals(topic)) { if (!mqSet.contains(mq)) { pq.setDropped(true); - if (this.removeUnnecessaryMessageQueue(mq, pq)) { - it.remove(); - changed = true; - log.info("doRebalance, {}, remove unnecessary mq, {}", consumerGroup, mq); - } - } else if (pq.isPullExpired()) { - switch (this.consumeType()) { - case CONSUME_ACTIVELY: - break; - case CONSUME_PASSIVELY: - pq.setDropped(true); - if (this.removeUnnecessaryMessageQueue(mq, pq)) { - it.remove(); - changed = true; - log.error("[BUG]doRebalance, {}, remove unnecessary mq, {}, because pull is pause, so try to fixed it", - consumerGroup, mq); - } - break; - default: - break; - } + removeQueueMap.put(mq, pq); + } else if (pq.isPullExpired() && this.consumeType() == ConsumeType.CONSUME_PASSIVELY) { + pq.setDropped(true); + removeQueueMap.put(mq, pq); + log.error("[BUG]doRebalance, {}, try remove unnecessary mq, {}, because pull is pause, so try to fixed it", + consumerGroup, mq); } } } - List pullRequestList = new ArrayList(); + // remove message queues no longer belong me + for (Entry entry : removeQueueMap.entrySet()) { + MessageQueue mq = entry.getKey(); + ProcessQueue pq = entry.getValue(); + + if (this.removeUnnecessaryMessageQueue(mq, pq)) { + this.processQueueTable.remove(mq); + changed = true; + log.info("doRebalance, {}, remove unnecessary mq, {}", consumerGroup, mq); + } + } + + // add new message queue + boolean allMQLocked = true; + List pullRequestList = new ArrayList<>(); for (MessageQueue mq : mqSet) { if (!this.processQueueTable.containsKey(mq)) { if (isOrder && !this.lock(mq)) { log.warn("doRebalance, {}, add a new mq failed, {}, because lock failed", consumerGroup, mq); + allMQLocked = false; continue; } this.removeDirtyOffset(mq); - ProcessQueue pq = new ProcessQueue(); + ProcessQueue pq = createProcessQueue(topic); + pq.setLocked(true); long nextOffset = this.computePullFromWhere(mq); if (nextOffset >= 0) { ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq); @@ -395,9 +546,201 @@ private boolean updateProcessQueueTableInRebalance(final String topic, final Set log.warn("doRebalance, {}, add new mq failed, {}", consumerGroup, mq); } } + + } + + if (!allMQLocked) { + mQClientFactory.rebalanceLater(500); + } + + this.dispatchPullRequest(pullRequestList, 500); + + return changed; + } + + private boolean updateMessageQueueAssignment(final String topic, final Set assignments, + final boolean isOrder) { + boolean changed = false; + + Map mq2PushAssignment = new HashMap<>(); + Map mq2PopAssignment = new HashMap<>(); + for (MessageQueueAssignment assignment : assignments) { + MessageQueue messageQueue = assignment.getMessageQueue(); + if (messageQueue == null) { + continue; + } + if (MessageRequestMode.POP == assignment.getMode()) { + mq2PopAssignment.put(messageQueue, assignment); + } else { + mq2PushAssignment.put(messageQueue, assignment); + } + } + + if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + if (mq2PopAssignment.isEmpty() && !mq2PushAssignment.isEmpty()) { + //pop switch to push + //subscribe pop retry topic + try { + final String retryTopic = KeyBuilder.buildPopRetryTopic(topic, getConsumerGroup()); + SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(retryTopic, SubscriptionData.SUB_ALL); + getSubscriptionInner().put(retryTopic, subscriptionData); + } catch (Exception ignored) { + } + + } else if (!mq2PopAssignment.isEmpty() && mq2PushAssignment.isEmpty()) { + //push switch to pop + //unsubscribe pop retry topic + try { + final String retryTopic = KeyBuilder.buildPopRetryTopic(topic, getConsumerGroup()); + getSubscriptionInner().remove(retryTopic); + } catch (Exception ignored) { + } + + } + } + + { + // drop process queues no longer belong me + HashMap removeQueueMap = new HashMap<>(this.processQueueTable.size()); + Iterator> it = this.processQueueTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + MessageQueue mq = next.getKey(); + ProcessQueue pq = next.getValue(); + + if (mq.getTopic().equals(topic)) { + if (!mq2PushAssignment.containsKey(mq)) { + pq.setDropped(true); + removeQueueMap.put(mq, pq); + } else if (pq.isPullExpired() && this.consumeType() == ConsumeType.CONSUME_PASSIVELY) { + pq.setDropped(true); + removeQueueMap.put(mq, pq); + log.error("[BUG]doRebalance, {}, try remove unnecessary mq, {}, because pull is pause, so try to fixed it", + consumerGroup, mq); + } + } + } + // remove message queues no longer belong me + for (Entry entry : removeQueueMap.entrySet()) { + MessageQueue mq = entry.getKey(); + ProcessQueue pq = entry.getValue(); + + if (this.removeUnnecessaryMessageQueue(mq, pq)) { + this.processQueueTable.remove(mq); + changed = true; + log.info("doRebalance, {}, remove unnecessary mq, {}", consumerGroup, mq); + } + } + } + + { + HashMap removeQueueMap = new HashMap<>(this.popProcessQueueTable.size()); + Iterator> it = this.popProcessQueueTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + MessageQueue mq = next.getKey(); + PopProcessQueue pq = next.getValue(); + + if (mq.getTopic().equals(topic)) { + if (!mq2PopAssignment.containsKey(mq)) { + //the queue is no longer your assignment + pq.setDropped(true); + removeQueueMap.put(mq, pq); + } else if (pq.isPullExpired() && this.consumeType() == ConsumeType.CONSUME_PASSIVELY) { + pq.setDropped(true); + removeQueueMap.put(mq, pq); + log.error("[BUG]doRebalance, {}, try remove unnecessary pop mq, {}, because pop is pause, so try to fixed it", + consumerGroup, mq); + } + } + } + // remove message queues no longer belong me + for (Entry entry : removeQueueMap.entrySet()) { + MessageQueue mq = entry.getKey(); + PopProcessQueue pq = entry.getValue(); + + if (this.removeUnnecessaryPopMessageQueue(mq, pq)) { + this.popProcessQueueTable.remove(mq); + changed = true; + log.info("doRebalance, {}, remove unnecessary pop mq, {}", consumerGroup, mq); + } + } } - this.dispatchPullRequest(pullRequestList); + { + // add new message queue + boolean allMQLocked = true; + List pullRequestList = new ArrayList<>(); + for (MessageQueue mq : mq2PushAssignment.keySet()) { + if (!this.processQueueTable.containsKey(mq)) { + if (isOrder && !this.lock(mq)) { + log.warn("doRebalance, {}, add a new mq failed, {}, because lock failed", consumerGroup, mq); + allMQLocked = false; + continue; + } + + this.removeDirtyOffset(mq); + ProcessQueue pq = createProcessQueue(); + pq.setLocked(true); + long nextOffset = -1L; + try { + nextOffset = this.computePullFromWhereWithException(mq); + } catch (Exception e) { + log.info("doRebalance, {}, compute offset failed, {}", consumerGroup, mq); + continue; + } + + if (nextOffset >= 0) { + ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq); + if (pre != null) { + log.info("doRebalance, {}, mq already exists, {}", consumerGroup, mq); + } else { + log.info("doRebalance, {}, add a new mq, {}", consumerGroup, mq); + PullRequest pullRequest = new PullRequest(); + pullRequest.setConsumerGroup(consumerGroup); + pullRequest.setNextOffset(nextOffset); + pullRequest.setMessageQueue(mq); + pullRequest.setProcessQueue(pq); + pullRequestList.add(pullRequest); + changed = true; + } + } else { + log.warn("doRebalance, {}, add new mq failed, {}", consumerGroup, mq); + } + } + } + + if (!allMQLocked) { + mQClientFactory.rebalanceLater(500); + } + this.dispatchPullRequest(pullRequestList, 500); + } + + { + // add new message queue + List popRequestList = new ArrayList<>(); + for (MessageQueue mq : mq2PopAssignment.keySet()) { + if (!this.popProcessQueueTable.containsKey(mq)) { + PopProcessQueue pq = createPopProcessQueue(); + PopProcessQueue pre = this.popProcessQueueTable.putIfAbsent(mq, pq); + if (pre != null) { + log.info("doRebalance, {}, mq pop already exists, {}", consumerGroup, mq); + } else { + log.info("doRebalance, {}, add a new pop mq, {}", consumerGroup, mq); + PopRequest popRequest = new PopRequest(); + popRequest.setTopic(topic); + popRequest.setConsumerGroup(consumerGroup); + popRequest.setMessageQueue(mq); + popRequest.setPopProcessQueue(pq); + popRequest.setInitMode(getConsumeInitMode()); + popRequestList.add(popRequest); + changed = true; + } + } + } + + this.dispatchPopPullRequest(popRequestList, 500); + } return changed; } @@ -407,13 +750,36 @@ public abstract void messageQueueChanged(final String topic, final Set pullRequestList); + public abstract long computePullFromWhereWithException(final MessageQueue mq) throws MQClientException; + + public abstract int getConsumeInitMode(); + + public abstract void dispatchPullRequest(final List pullRequestList, final long delay); + + public abstract void dispatchPopPullRequest(final List pullRequestList, final long delay); + + public abstract ProcessQueue createProcessQueue(); + + public abstract PopProcessQueue createPopProcessQueue(); + + public abstract ProcessQueue createProcessQueue(String topicName); public void removeProcessQueue(final MessageQueue mq) { ProcessQueue prev = this.processQueueTable.remove(mq); @@ -429,6 +795,10 @@ public ConcurrentMap getProcessQueueTable() { return processQueueTable; } + public ConcurrentMap getPopProcessQueueTable() { + return popProcessQueueTable; + } + public ConcurrentMap> getTopicSubscribeInfoTable() { return topicSubscribeInfoTable; } @@ -473,5 +843,13 @@ public void destroy() { } this.processQueueTable.clear(); + + Iterator> popIt = this.popProcessQueueTable.entrySet().iterator(); + while (popIt.hasNext()) { + Entry next = popIt.next(); + next.getValue().setDropped(true); + } + this.popProcessQueueTable.clear(); } + } diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/RebalanceLitePullImpl.java b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/RebalanceLitePullImpl.java new file mode 100644 index 00000000000..335d89b7877 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/RebalanceLitePullImpl.java @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.impl.consumer; + +import java.util.List; +import java.util.Set; +import org.apache.rocketmq.client.consumer.AllocateMessageQueueStrategy; +import org.apache.rocketmq.client.consumer.MessageQueueListener; +import org.apache.rocketmq.client.consumer.store.ReadOffsetType; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; + +public class RebalanceLitePullImpl extends RebalanceImpl { + + private final DefaultLitePullConsumerImpl litePullConsumerImpl; + + public RebalanceLitePullImpl(DefaultLitePullConsumerImpl litePullConsumerImpl) { + this(null, null, null, null, litePullConsumerImpl); + } + + public RebalanceLitePullImpl(String consumerGroup, MessageModel messageModel, + AllocateMessageQueueStrategy allocateMessageQueueStrategy, + MQClientInstance mQClientFactory, DefaultLitePullConsumerImpl litePullConsumerImpl) { + super(consumerGroup, messageModel, allocateMessageQueueStrategy, mQClientFactory); + this.litePullConsumerImpl = litePullConsumerImpl; + } + + @Override + public void messageQueueChanged(String topic, Set mqAll, Set mqDivided) { + MessageQueueListener messageQueueListener = this.litePullConsumerImpl.getDefaultLitePullConsumer().getMessageQueueListener(); + if (messageQueueListener != null) { + try { + messageQueueListener.messageQueueChanged(topic, mqAll, mqDivided); + } catch (Throwable e) { + log.error("messageQueueChanged exception", e); + } + } + } + + @Override + public boolean removeUnnecessaryMessageQueue(MessageQueue mq, ProcessQueue pq) { + this.litePullConsumerImpl.getOffsetStore().persist(mq); + this.litePullConsumerImpl.getOffsetStore().removeOffset(mq); + return true; + } + + @Override + public ConsumeType consumeType() { + return ConsumeType.CONSUME_ACTIVELY; + } + + @Override + public void removeDirtyOffset(final MessageQueue mq) { + this.litePullConsumerImpl.getOffsetStore().removeOffset(mq); + } + + @Deprecated + @Override + public long computePullFromWhere(MessageQueue mq) { + long result = -1L; + try { + result = computePullFromWhereWithException(mq); + } catch (MQClientException e) { + log.warn("Compute consume offset exception, mq={}", mq); + } + return result; + } + + @Override + public long computePullFromWhereWithException(MessageQueue mq) throws MQClientException { + ConsumeFromWhere consumeFromWhere = litePullConsumerImpl.getDefaultLitePullConsumer().getConsumeFromWhere(); + long result = -1; + switch (consumeFromWhere) { + case CONSUME_FROM_LAST_OFFSET: { + long lastOffset = litePullConsumerImpl.getOffsetStore().readOffset(mq, ReadOffsetType.MEMORY_FIRST_THEN_STORE); + if (lastOffset >= 0) { + result = lastOffset; + } else if (-1 == lastOffset) { + if (mq.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { // First start, no offset + result = 0L; + } else { + try { + result = this.mQClientFactory.getMQAdminImpl().maxOffset(mq); + } catch (MQClientException e) { + log.warn("Compute consume offset from last offset exception, mq={}, exception={}", mq, e); + throw e; + } + } + } else { + result = -1; + } + break; + } + case CONSUME_FROM_FIRST_OFFSET: { + long lastOffset = litePullConsumerImpl.getOffsetStore().readOffset(mq, ReadOffsetType.MEMORY_FIRST_THEN_STORE); + if (lastOffset >= 0) { + result = lastOffset; + } else if (-1 == lastOffset) { + result = 0L; + } else { + result = -1; + } + break; + } + case CONSUME_FROM_TIMESTAMP: { + long lastOffset = litePullConsumerImpl.getOffsetStore().readOffset(mq, ReadOffsetType.MEMORY_FIRST_THEN_STORE); + if (lastOffset >= 0) { + result = lastOffset; + } else if (-1 == lastOffset) { + if (mq.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + try { + result = this.mQClientFactory.getMQAdminImpl().maxOffset(mq); + } catch (MQClientException e) { + log.warn("Compute consume offset from last offset exception, mq={}, exception={}", mq, e); + throw e; + } + } else { + try { + long timestamp = UtilAll.parseDate(this.litePullConsumerImpl.getDefaultLitePullConsumer().getConsumeTimestamp(), + UtilAll.YYYYMMDDHHMMSS).getTime(); + result = this.mQClientFactory.getMQAdminImpl().searchOffset(mq, timestamp); + } catch (MQClientException e) { + log.warn("Compute consume offset from last offset exception, mq={}, exception={}", mq, e); + throw e; + } + } + } else { + result = -1; + } + break; + } + } + return result; + } + + @Override + public int getConsumeInitMode() { + throw new UnsupportedOperationException("no initMode for Pull"); + } + + @Override + public void dispatchPullRequest(final List pullRequestList, final long delay) { + } + + @Override + public void dispatchPopPullRequest(List pullRequestList, long delay) { + + } + + @Override + public ProcessQueue createProcessQueue() { + return new ProcessQueue(); + } + + @Override + public PopProcessQueue createPopProcessQueue() { + return null; + } + + public ProcessQueue createProcessQueue(String topicName) { + return createProcessQueue(); + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/RebalancePullImpl.java b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/RebalancePullImpl.java index 9dd408c1d14..1b5f9766174 100644 --- a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/RebalancePullImpl.java +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/RebalancePullImpl.java @@ -20,10 +20,11 @@ import java.util.Set; import org.apache.rocketmq.client.consumer.AllocateMessageQueueStrategy; import org.apache.rocketmq.client.consumer.MessageQueueListener; +import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.impl.factory.MQClientInstance; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.heartbeat.ConsumeType; -import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; public class RebalancePullImpl extends RebalanceImpl { private final DefaultMQPullConsumerImpl defaultMQPullConsumerImpl; @@ -68,12 +69,42 @@ public void removeDirtyOffset(final MessageQueue mq) { this.defaultMQPullConsumerImpl.getOffsetStore().removeOffset(mq); } + @Deprecated @Override public long computePullFromWhere(MessageQueue mq) { return 0; } @Override - public void dispatchPullRequest(List pullRequestList) { + public long computePullFromWhereWithException(MessageQueue mq) throws MQClientException { + return 0; + } + + @Override + public int getConsumeInitMode() { + throw new UnsupportedOperationException("no initMode for Pull"); + } + + @Override + public void dispatchPullRequest(final List pullRequestList, final long delay) { + } + + @Override + public void dispatchPopPullRequest(final List pullRequestList, final long delay) { } + + @Override + public ProcessQueue createProcessQueue() { + return new ProcessQueue(); + } + + @Override + public PopProcessQueue createPopProcessQueue() { + return null; + } + + public ProcessQueue createProcessQueue(String topicName) { + return createProcessQueue(); + } + } diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/RebalancePushImpl.java b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/RebalancePushImpl.java index e5166f35b59..f28890d306f 100644 --- a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/RebalancePushImpl.java +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/RebalancePushImpl.java @@ -20,22 +20,26 @@ import java.util.Set; import java.util.concurrent.TimeUnit; import org.apache.rocketmq.client.consumer.AllocateMessageQueueStrategy; +import org.apache.rocketmq.client.consumer.MessageQueueListener; import org.apache.rocketmq.client.consumer.store.OffsetStore; import org.apache.rocketmq.client.consumer.store.ReadOffsetType; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.impl.factory.MQClientInstance; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.constant.ConsumeInitMode; import org.apache.rocketmq.common.consumer.ConsumeFromWhere; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.heartbeat.ConsumeType; -import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; public class RebalancePushImpl extends RebalanceImpl { private final static long UNLOCK_DELAY_TIME_MILLS = Long.parseLong(System.getProperty("rocketmq.client.unlockDelayTimeMills", "20000")); private final DefaultMQPushConsumerImpl defaultMQPushConsumerImpl; + public RebalancePushImpl(DefaultMQPushConsumerImpl defaultMQPushConsumerImpl) { this(null, null, null, null, defaultMQPushConsumerImpl); } @@ -49,7 +53,7 @@ public RebalancePushImpl(String consumerGroup, MessageModel messageModel, @Override public void messageQueueChanged(String topic, Set mqAll, Set mqDivided) { - /** + /* * When rebalance result changed, should update subscription's version to notify broker. * Fix: inconsistency subscription may lead to consumer miss messages. */ @@ -78,52 +82,65 @@ public void messageQueueChanged(String topic, Set mqAll, Set pq.getLastLockTimestamp() + UNLOCK_DELAY_TIME_MILLS; + if (forceUnlock || pq.getConsumeLock().writeLock().tryLock(500, TimeUnit.MILLISECONDS)) { + try { + RebalancePushImpl.this.defaultMQPushConsumerImpl.getOffsetStore().persist(mq); + RebalancePushImpl.this.defaultMQPushConsumerImpl.getOffsetStore().removeOffset(mq); - if (pq.hasTempMessage()) { - log.info("[{}]unlockDelay, begin {} ", mq.hashCode(), mq); - this.defaultMQPushConsumerImpl.getmQClientFactory().getScheduledExecutorService().schedule(new Runnable() { - @Override - public void run() { - log.info("[{}]unlockDelay, execute at once {}", mq.hashCode(), mq); + pq.setLocked(false); RebalancePushImpl.this.unlock(mq, true); + return true; + } finally { + if (!forceUnlock) { + pq.getConsumeLock().writeLock().unlock(); + } } - }, UNLOCK_DELAY_TIME_MILLS, TimeUnit.MILLISECONDS); - } else { - this.unlock(mq, true); + } else { + pq.incTryUnlockTimes(); + } + } catch (Exception e) { + pq.incTryUnlockTimes(); } + + return false; + } + + @Override + public boolean clientRebalance(String topic) { + // POPTODO order pop consume not implement yet + return defaultMQPushConsumerImpl.getDefaultMQPushConsumer().isClientRebalance() || defaultMQPushConsumerImpl.isConsumeOrderly() || MessageModel.BROADCASTING.equals(messageModel); + } + + public boolean removeUnnecessaryPopMessageQueue(final MessageQueue mq, final PopProcessQueue pq) { return true; } @@ -137,8 +154,20 @@ public void removeDirtyOffset(final MessageQueue mq) { this.defaultMQPushConsumerImpl.getOffsetStore().removeOffset(mq); } + @Deprecated @Override public long computePullFromWhere(MessageQueue mq) { + long result = -1L; + try { + result = computePullFromWhereWithException(mq); + } catch (MQClientException e) { + log.warn("Compute consume offset exception, mq={}", mq); + } + return result; + } + + @Override + public long computePullFromWhereWithException(MessageQueue mq) throws MQClientException { long result = -1; final ConsumeFromWhere consumeFromWhere = this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer().getConsumeFromWhere(); final OffsetStore offsetStore = this.defaultMQPushConsumerImpl.getOffsetStore(); @@ -159,11 +188,13 @@ else if (-1 == lastOffset) { try { result = this.mQClientFactory.getMQAdminImpl().maxOffset(mq); } catch (MQClientException e) { - result = -1; + log.warn("Compute consume offset from last offset exception, mq={}, exception={}", mq, e); + throw e; } } } else { - result = -1; + throw new MQClientException(ResponseCode.QUERY_NOT_FOUND, "Failed to query consume offset from " + + "offset store"); } break; } @@ -172,9 +203,11 @@ else if (-1 == lastOffset) { if (lastOffset >= 0) { result = lastOffset; } else if (-1 == lastOffset) { + //the offset will be fixed by the OFFSET_ILLEGAL process result = 0L; } else { - result = -1; + throw new MQClientException(ResponseCode.QUERY_NOT_FOUND, "Failed to query offset from offset " + + "store"); } break; } @@ -187,7 +220,8 @@ else if (-1 == lastOffset) { try { result = this.mQClientFactory.getMQAdminImpl().maxOffset(mq); } catch (MQClientException e) { - result = -1; + log.warn("Compute consume offset from last offset exception, mq={}, exception={}", mq, e); + throw e; } } else { try { @@ -195,11 +229,13 @@ else if (-1 == lastOffset) { UtilAll.YYYYMMDDHHMMSS).getTime(); result = this.mQClientFactory.getMQAdminImpl().searchOffset(mq, timestamp); } catch (MQClientException e) { - result = -1; + log.warn("Compute consume offset from last offset exception, mq={}, exception={}", mq, e); + throw e; } } } else { - result = -1; + throw new MQClientException(ResponseCode.QUERY_NOT_FOUND, "Failed to query offset from offset " + + "store"); } break; } @@ -208,14 +244,57 @@ else if (-1 == lastOffset) { break; } + if (result < 0) { + throw new MQClientException(ResponseCode.SYSTEM_ERROR, "Found unexpected result " + result); + } + return result; } @Override - public void dispatchPullRequest(List pullRequestList) { + public int getConsumeInitMode() { + final ConsumeFromWhere consumeFromWhere = this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer().getConsumeFromWhere(); + if (ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET == consumeFromWhere) { + return ConsumeInitMode.MIN; + } else { + return ConsumeInitMode.MAX; + } + } + + @Override + public void dispatchPullRequest(final List pullRequestList, final long delay) { for (PullRequest pullRequest : pullRequestList) { - this.defaultMQPushConsumerImpl.executePullRequestImmediately(pullRequest); - log.info("doRebalance, {}, add a new pull request {}", consumerGroup, pullRequest); + if (delay <= 0) { + this.defaultMQPushConsumerImpl.executePullRequestImmediately(pullRequest); + } else { + this.defaultMQPushConsumerImpl.executePullRequestLater(pullRequest, delay); + } } } + + @Override + public void dispatchPopPullRequest(final List pullRequestList, final long delay) { + for (PopRequest pullRequest : pullRequestList) { + if (delay <= 0) { + this.defaultMQPushConsumerImpl.executePopPullRequestImmediately(pullRequest); + } else { + this.defaultMQPushConsumerImpl.executePopPullRequestLater(pullRequest, delay); + } + } + } + + @Override + public ProcessQueue createProcessQueue() { + return new ProcessQueue(); + } + + @Override + public ProcessQueue createProcessQueue(String topicName) { + return createProcessQueue(); + } + + @Override + public PopProcessQueue createPopProcessQueue() { + return new PopProcessQueue(); + } } diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/RebalanceService.java b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/RebalanceService.java index fab07cb36e0..8e586c85feb 100644 --- a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/RebalanceService.java +++ b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/RebalanceService.java @@ -17,16 +17,20 @@ package org.apache.rocketmq.client.impl.consumer; import org.apache.rocketmq.client.impl.factory.MQClientInstance; -import org.apache.rocketmq.client.log.ClientLogger; import org.apache.rocketmq.common.ServiceThread; -import org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; public class RebalanceService extends ServiceThread { private static long waitInterval = Long.parseLong(System.getProperty( "rocketmq.client.rebalance.waitInterval", "20000")); - private final Logger log = ClientLogger.getLog(); + private static long minInterval = + Long.parseLong(System.getProperty( + "rocketmq.client.rebalance.minInterval", "1000")); + private final Logger log = LoggerFactory.getLogger(RebalanceService.class); private final MQClientInstance mqClientFactory; + private long lastRebalanceTimestamp = System.currentTimeMillis(); public RebalanceService(MQClientInstance mqClientFactory) { this.mqClientFactory = mqClientFactory; @@ -36,9 +40,18 @@ public RebalanceService(MQClientInstance mqClientFactory) { public void run() { log.info(this.getServiceName() + " service started"); + long realWaitInterval = waitInterval; while (!this.isStopped()) { - this.waitForRunning(waitInterval); - this.mqClientFactory.doRebalance(); + this.waitForRunning(realWaitInterval); + + long interval = System.currentTimeMillis() - lastRebalanceTimestamp; + if (interval < minInterval) { + realWaitInterval = minInterval - interval; + } else { + boolean balanced = this.mqClientFactory.doRebalance(); + realWaitInterval = balanced ? waitInterval : minInterval; + lastRebalanceTimestamp = System.currentTimeMillis(); + } } log.info(this.getServiceName() + " service end"); diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/factory/MQClientInstance.java b/client/src/main/java/org/apache/rocketmq/client/impl/factory/MQClientInstance.java index 42b74369401..436782efd33 100644 --- a/client/src/main/java/org/apache/rocketmq/client/impl/factory/MQClientInstance.java +++ b/client/src/main/java/org/apache/rocketmq/client/impl/factory/MQClientInstance.java @@ -16,8 +16,7 @@ */ package org.apache.rocketmq.client.impl.factory; -import java.io.UnsupportedEncodingException; -import java.net.DatagramSocket; +import io.netty.channel.Channel; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -36,6 +35,8 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import com.alibaba.fastjson.JSON; +import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.client.ClientConfig; import org.apache.rocketmq.client.admin.MQAdminExtInner; import org.apache.rocketmq.client.exception.MQBrokerException; @@ -54,69 +55,92 @@ import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl; import org.apache.rocketmq.client.impl.producer.MQProducerInner; import org.apache.rocketmq.client.impl.producer.TopicPublishInfo; -import org.apache.rocketmq.client.log.ClientLogger; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.stat.ConsumerStatsManager; import org.apache.rocketmq.common.MQVersion; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.ServiceState; -import org.apache.rocketmq.common.UtilAll; import org.apache.rocketmq.common.constant.PermName; import org.apache.rocketmq.common.filter.ExpressionType; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.body.ConsumeMessageDirectlyResult; -import org.apache.rocketmq.common.protocol.body.ConsumerRunningInfo; -import org.apache.rocketmq.common.protocol.heartbeat.ConsumeType; -import org.apache.rocketmq.common.protocol.heartbeat.ConsumerData; -import org.apache.rocketmq.common.protocol.heartbeat.HeartbeatData; -import org.apache.rocketmq.common.protocol.heartbeat.ProducerData; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; -import org.apache.rocketmq.common.protocol.route.BrokerData; -import org.apache.rocketmq.common.protocol.route.QueueData; -import org.apache.rocketmq.common.protocol.route.TopicRouteData; +import org.apache.rocketmq.common.message.MessageQueueAssignment; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.remoting.ChannelEventListener; import org.apache.rocketmq.remoting.RPCHook; -import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.common.HeartbeatV2Result; import org.apache.rocketmq.remoting.exception.RemotingException; import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; import org.apache.rocketmq.remoting.protocol.RemotingCommand; -import org.slf4j.Logger; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumerData; +import org.apache.rocketmq.remoting.protocol.heartbeat.HeartbeatData; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.ProducerData; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +import static org.apache.rocketmq.remoting.rpc.ClientMetadata.topicRouteData2EndpointsForStaticTopic; public class MQClientInstance { private final static long LOCK_TIMEOUT_MILLIS = 3000; - private final Logger log = ClientLogger.getLog(); + private final static Logger log = LoggerFactory.getLogger(MQClientInstance.class); private final ClientConfig clientConfig; - private final int instanceIndex; private final String clientId; private final long bootTimestamp = System.currentTimeMillis(); - private final ConcurrentMap producerTable = new ConcurrentHashMap(); - private final ConcurrentMap consumerTable = new ConcurrentHashMap(); - private final ConcurrentMap adminExtTable = new ConcurrentHashMap(); + + /** + * The container of the producer in the current client. The key is the name of producerGroup. + */ + private final ConcurrentMap producerTable = new ConcurrentHashMap<>(); + + /** + * The container of the consumer in the current client. The key is the name of consumerGroup. + */ + private final ConcurrentMap consumerTable = new ConcurrentHashMap<>(); + + /** + * The container of the adminExt in the current client. The key is the name of adminExtGroup. + */ + private final ConcurrentMap adminExtTable = new ConcurrentHashMap<>(); private final NettyClientConfig nettyClientConfig; private final MQClientAPIImpl mQClientAPIImpl; private final MQAdminImpl mQAdminImpl; - private final ConcurrentMap topicRouteTable = new ConcurrentHashMap(); + private final ConcurrentMap topicRouteTable = new ConcurrentHashMap<>(); + private final ConcurrentMap> topicEndPointsTable = new ConcurrentHashMap<>(); private final Lock lockNamesrv = new ReentrantLock(); private final Lock lockHeartbeat = new ReentrantLock(); - private final ConcurrentMap> brokerAddrTable = - new ConcurrentHashMap>(); - private final ConcurrentMap> brokerVersionTable = - new ConcurrentHashMap>(); - private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { + + /** + * The container which stores the brokerClusterInfo. The key of the map is the brokerCluster name. + * And the value is the broker instance list that belongs to the broker cluster. + * For the sub map, the key is the id of single broker instance, and the value is the address. + */ + private final ConcurrentMap> brokerAddrTable = new ConcurrentHashMap<>(); + + private final ConcurrentMap> brokerVersionTable = new ConcurrentHashMap<>(); + private final Set brokerSupportV2HeartbeatSet = new HashSet(); + private final ConcurrentMap brokerAddrHeartbeatFingerprintTable = new ConcurrentHashMap(); + private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "MQClientFactoryScheduledThread")); + private final ScheduledExecutorService fetchRemoteConfigExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { @Override public Thread newThread(Runnable r) { - return new Thread(r, "MQClientFactoryScheduledThread"); + return new Thread(r, "MQClientFactoryFetchRemoteConfigScheduledThread"); } }); - private final ClientRemotingProcessor clientRemotingProcessor; private final PullMessageService pullMessageService; private final RebalanceService rebalanceService; private final DefaultMQProducer defaultMQProducer; private final ConsumerStatsManager consumerStatsManager; private final AtomicLong sendHeartbeatTimesTotal = new AtomicLong(0); private ServiceState serviceState = ServiceState.CREATE_JUST; - private DatagramSocket datagramSocket; - private Random random = new Random(); + private final Random random = new Random(); public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId) { this(clientConfig, instanceIndex, clientId, null); @@ -124,12 +148,52 @@ public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String cli public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) { this.clientConfig = clientConfig; - this.instanceIndex = instanceIndex; this.nettyClientConfig = new NettyClientConfig(); this.nettyClientConfig.setClientCallbackExecutorThreads(clientConfig.getClientCallbackExecutorThreads()); this.nettyClientConfig.setUseTLS(clientConfig.isUseTLS()); - this.clientRemotingProcessor = new ClientRemotingProcessor(this); - this.mQClientAPIImpl = new MQClientAPIImpl(this.nettyClientConfig, this.clientRemotingProcessor, rpcHook, clientConfig); + this.nettyClientConfig.setSocksProxyConfig(clientConfig.getSocksProxyConfig()); + ClientRemotingProcessor clientRemotingProcessor = new ClientRemotingProcessor(this); + ChannelEventListener channelEventListener; + if (clientConfig.isEnableHeartbeatChannelEventListener()) { + channelEventListener = new ChannelEventListener() { + private final ConcurrentMap> brokerAddrTable = MQClientInstance.this.brokerAddrTable; + @Override + public void onChannelConnect(String remoteAddr, Channel channel) { + } + + @Override + public void onChannelClose(String remoteAddr, Channel channel) { + } + + @Override + public void onChannelException(String remoteAddr, Channel channel) { + } + + @Override + public void onChannelIdle(String remoteAddr, Channel channel) { + } + + @Override + public void onChannelActive(String remoteAddr, Channel channel) { + for (Map.Entry> addressEntry : brokerAddrTable.entrySet()) { + for (Map.Entry entry : addressEntry.getValue().entrySet()) { + String addr = entry.getValue(); + if (addr.equals(remoteAddr)) { + long id = entry.getKey(); + String brokerName = addressEntry.getKey(); + if (sendHeartbeatToBroker(id, brokerName, addr)) { + rebalanceImmediately(); + } + break; + } + } + } + } + }; + } else { + channelEventListener = null; + } + this.mQClientAPIImpl = new MQClientAPIImpl(this.nettyClientConfig, clientRemotingProcessor, rpcHook, clientConfig, channelEventListener); if (this.clientConfig.getNamesrvAddr() != null) { this.mQClientAPIImpl.updateNameServerAddressList(this.clientConfig.getNamesrvAddr()); @@ -150,7 +214,7 @@ public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String cli this.consumerStatsManager = new ConsumerStatsManager(this.scheduledExecutorService); log.info("Created a new client Instance, InstanceIndex:{}, ClientID:{}, ClientConfig:{}, ClientVersion:{}, SerializerType:{}", - this.instanceIndex, + instanceIndex, this.clientId, this.clientConfig, MQVersion.getVersionDesc(MQVersion.CURRENT_VERSION), RemotingCommand.getSerializeTypeConfigInThisServer()); @@ -158,6 +222,7 @@ public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String cli public static TopicPublishInfo topicRouteData2TopicPublishInfo(final String topic, final TopicRouteData route) { TopicPublishInfo info = new TopicPublishInfo(); + // TO DO should check the usage of raw route, it is better to remove such field info.setTopicRouteData(route); if (route.getOrderTopicConf() != null && route.getOrderTopicConf().length() > 0) { String[] brokers = route.getOrderTopicConf().split(";"); @@ -171,6 +236,13 @@ public static TopicPublishInfo topicRouteData2TopicPublishInfo(final String topi } info.setOrderTopic(true); + } else if (route.getOrderTopicConf() == null + && route.getTopicQueueMappingByBroker() != null + && !route.getTopicQueueMappingByBroker().isEmpty()) { + info.setOrderTopic(false); + ConcurrentMap mqEndPoints = topicRouteData2EndpointsForStaticTopic(topic, route); + info.getMessageQueueList().addAll(mqEndPoints.keySet()); + info.getMessageQueueList().sort((mq1, mq2) -> MixAll.compareInteger(mq1.getQueueId(), mq2.getQueueId())); } else { List qds = route.getQueueDatas(); Collections.sort(qds); @@ -206,7 +278,12 @@ public static TopicPublishInfo topicRouteData2TopicPublishInfo(final String topi } public static Set topicRouteData2TopicSubscribeInfo(final String topic, final TopicRouteData route) { - Set mqList = new HashSet(); + Set mqList = new HashSet<>(); + if (route.getTopicQueueMappingByBroker() != null + && !route.getTopicQueueMappingByBroker().isEmpty()) { + ConcurrentMap mqEndPoints = topicRouteData2EndpointsForStaticTopic(topic, route); + return mqEndPoints.keySet(); + } List qds = route.getQueueDatas(); for (QueueData qd : qds) { if (PermName.isReadable(qd.getPerm())) { @@ -243,10 +320,6 @@ public void start() throws MQClientException { log.info("the client factory [{}] start OK", this.clientId); this.serviceState = ServiceState.RUNNING; break; - case RUNNING: - break; - case SHUTDOWN_ALREADY: - break; case START_FAILED: throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null); default: @@ -257,65 +330,45 @@ public void start() throws MQClientException { private void startScheduledTask() { if (null == this.clientConfig.getNamesrvAddr()) { - this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { - - @Override - public void run() { - try { - MQClientInstance.this.mQClientAPIImpl.fetchNameServerAddr(); - } catch (Exception e) { - log.error("ScheduledTask fetchNameServerAddr exception", e); - } + this.scheduledExecutorService.scheduleAtFixedRate(() -> { + try { + MQClientInstance.this.mQClientAPIImpl.fetchNameServerAddr(); + } catch (Exception e) { + log.error("ScheduledTask fetchNameServerAddr exception", e); } }, 1000 * 10, 1000 * 60 * 2, TimeUnit.MILLISECONDS); } - this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { - - @Override - public void run() { - try { - MQClientInstance.this.updateTopicRouteInfoFromNameServer(); - } catch (Exception e) { - log.error("ScheduledTask updateTopicRouteInfoFromNameServer exception", e); - } + this.scheduledExecutorService.scheduleAtFixedRate(() -> { + try { + MQClientInstance.this.updateTopicRouteInfoFromNameServer(); + } catch (Exception e) { + log.error("ScheduledTask updateTopicRouteInfoFromNameServer exception", e); } }, 10, this.clientConfig.getPollNameServerInterval(), TimeUnit.MILLISECONDS); - this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { - - @Override - public void run() { - try { - MQClientInstance.this.cleanOfflineBroker(); - MQClientInstance.this.sendHeartbeatToAllBrokerWithLock(); - } catch (Exception e) { - log.error("ScheduledTask sendHeartbeatToAllBroker exception", e); - } + this.scheduledExecutorService.scheduleAtFixedRate(() -> { + try { + MQClientInstance.this.cleanOfflineBroker(); + MQClientInstance.this.sendHeartbeatToAllBrokerWithLock(); + } catch (Exception e) { + log.error("ScheduledTask sendHeartbeatToAllBroker exception", e); } }, 1000, this.clientConfig.getHeartbeatBrokerInterval(), TimeUnit.MILLISECONDS); - this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { - - @Override - public void run() { - try { - MQClientInstance.this.persistAllConsumerOffset(); - } catch (Exception e) { - log.error("ScheduledTask persistAllConsumerOffset exception", e); - } + this.scheduledExecutorService.scheduleAtFixedRate(() -> { + try { + MQClientInstance.this.persistAllConsumerOffset(); + } catch (Exception e) { + log.error("ScheduledTask persistAllConsumerOffset exception", e); } }, 1000 * 10, this.clientConfig.getPersistConsumerOffsetInterval(), TimeUnit.MILLISECONDS); - this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { - - @Override - public void run() { - try { - MQClientInstance.this.adjustThreadPool(); - } catch (Exception e) { - log.error("ScheduledTask adjustThreadPool exception", e); - } + this.scheduledExecutorService.scheduleAtFixedRate(() -> { + try { + MQClientInstance.this.adjustThreadPool(); + } catch (Exception e) { + log.error("ScheduledTask adjustThreadPool exception", e); } }, 1, 1, TimeUnit.MINUTES); } @@ -325,13 +378,11 @@ public String getClientId() { } public void updateTopicRouteInfoFromNameServer() { - Set topicList = new HashSet(); + Set topicList = new HashSet<>(); // Consumer { - Iterator> it = this.consumerTable.entrySet().iterator(); - while (it.hasNext()) { - Entry entry = it.next(); + for (Entry entry : this.consumerTable.entrySet()) { MQConsumerInner impl = entry.getValue(); if (impl != null) { Set subList = impl.subscriptions(); @@ -346,9 +397,7 @@ public void updateTopicRouteInfoFromNameServer() { // Producer { - Iterator> it = this.producerTable.entrySet().iterator(); - while (it.hasNext()) { - Entry entry = it.next(); + for (Entry entry : this.producerTable.entrySet()) { MQProducerInner impl = entry.getValue(); if (impl != null) { Set lst = impl.getPublishTopicList(); @@ -362,6 +411,21 @@ public void updateTopicRouteInfoFromNameServer() { } } + public Map parseOffsetTableFromBroker(Map offsetTable, String namespace) { + HashMap newOffsetTable = new HashMap<>(offsetTable.size(), 1); + if (StringUtils.isNotEmpty(namespace)) { + for (Entry entry : offsetTable.entrySet()) { + MessageQueue queue = entry.getKey(); + queue.setTopic(NamespaceUtil.withoutNamespace(queue.getTopic(), namespace)); + newOffsetTable.put(queue, entry.getValue()); + } + } else { + newOffsetTable.putAll(offsetTable); + } + + return newOffsetTable; + } + /** * Remove offline broker */ @@ -369,7 +433,7 @@ private void cleanOfflineBroker() { try { if (this.lockNamesrv.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) try { - ConcurrentHashMap> updatedTable = new ConcurrentHashMap>(); + ConcurrentHashMap> updatedTable = new ConcurrentHashMap<>(this.brokerAddrTable.size(), 1); Iterator>> itBrokerTable = this.brokerAddrTable.entrySet().iterator(); while (itBrokerTable.hasNext()) { @@ -377,7 +441,7 @@ private void cleanOfflineBroker() { String brokerName = entry.getKey(); HashMap oneTable = entry.getValue(); - HashMap cloneAddrTable = new HashMap(); + HashMap cloneAddrTable = new HashMap<>(oneTable.size(), 1); cloneAddrTable.putAll(oneTable); Iterator> it = cloneAddrTable.entrySet().iterator(); @@ -410,10 +474,8 @@ private void cleanOfflineBroker() { } public void checkClientInBroker() throws MQClientException { - Iterator> it = this.consumerTable.entrySet().iterator(); - while (it.hasNext()) { - Entry entry = it.next(); + for (Entry entry : this.consumerTable.entrySet()) { Set subscriptionInner = entry.getValue().subscriptions(); if (subscriptionInner == null || subscriptionInner.isEmpty()) { return; @@ -424,13 +486,13 @@ public void checkClientInBroker() throws MQClientException { continue; } // may need to check one broker every cluster... - // assume that the configs of every broker in cluster are the the same. + // assume that the configs of every broker in cluster are the same. String addr = findBrokerAddrByTopic(subscriptionData.getTopic()); if (addr != null) { try { this.getMQClientAPIImpl().checkClientInBroker( - addr, entry.getKey(), this.clientId, subscriptionData, 3 * 1000 + addr, entry.getKey(), this.clientId, subscriptionData, clientConfig.getMqClientApiTimeout() ); } catch (Exception e) { if (e instanceof MQClientException) { @@ -447,34 +509,53 @@ public void checkClientInBroker() throws MQClientException { } } - public void sendHeartbeatToAllBrokerWithLock() { + public boolean sendHeartbeatToAllBrokerWithLockV2(boolean isRebalance) { if (this.lockHeartbeat.tryLock()) { try { - this.sendHeartbeatToAllBroker(); - this.uploadFilterClassSource(); + if (clientConfig.isUseHeartbeatV2()) { + return this.sendHeartbeatToAllBrokerV2(isRebalance); + } else { + return this.sendHeartbeatToAllBroker(); + } + } catch (final Exception e) { + log.error("sendHeartbeatToAllBrokerWithLockV2 exception", e); + } finally { + this.lockHeartbeat.unlock(); + } + } else { + log.warn("sendHeartbeatToAllBrokerWithLockV2 lock heartBeat, but failed."); + } + return false; + } + + public boolean sendHeartbeatToAllBrokerWithLock() { + if (this.lockHeartbeat.tryLock()) { + try { + if (clientConfig.isUseHeartbeatV2()) { + return this.sendHeartbeatToAllBrokerV2(false); + } else { + return this.sendHeartbeatToAllBroker(); + } } catch (final Exception e) { log.error("sendHeartbeatToAllBroker exception", e); } finally { this.lockHeartbeat.unlock(); } } else { - log.warn("lock heartBeat, but failed."); + log.warn("lock heartBeat, but failed. [{}]", this.clientId); } + return false; } private void persistAllConsumerOffset() { - Iterator> it = this.consumerTable.entrySet().iterator(); - while (it.hasNext()) { - Entry entry = it.next(); + for (Entry entry : this.consumerTable.entrySet()) { MQConsumerInner impl = entry.getValue(); impl.persistConsumerOffset(); } } public void adjustThreadPool() { - Iterator> it = this.consumerTable.entrySet().iterator(); - while (it.hasNext()) { - Entry entry = it.next(); + for (Entry entry : this.consumerTable.entrySet()) { MQConsumerInner impl = entry.getValue(); if (impl != null) { try { @@ -482,7 +563,7 @@ public void adjustThreadPool() { DefaultMQPushConsumerImpl dmq = (DefaultMQPushConsumerImpl) impl; dmq.adjustThreadPool(); } - } catch (Exception e) { + } catch (Exception ignored) { } } } @@ -493,9 +574,7 @@ public boolean updateTopicRouteInfoFromNameServer(final String topic) { } private boolean isBrokerAddrExistInTopicRouteTable(final String addr) { - Iterator> it = this.topicRouteTable.entrySet().iterator(); - while (it.hasNext()) { - Entry entry = it.next(); + for (Entry entry : this.topicRouteTable.entrySet()) { TopicRouteData topicRouteData = entry.getValue(); List bds = topicRouteData.getBrokerDatas(); for (BrokerData bd : bds) { @@ -510,79 +589,177 @@ private boolean isBrokerAddrExistInTopicRouteTable(final String addr) { return false; } - private void sendHeartbeatToAllBroker() { - final HeartbeatData heartbeatData = this.prepareHeartbeatData(); + public boolean sendHeartbeatToBroker(long id, String brokerName, String addr) { + if (this.lockHeartbeat.tryLock()) { + final HeartbeatData heartbeatDataWithSub = this.prepareHeartbeatData(false); + final boolean producerEmpty = heartbeatDataWithSub.getProducerDataSet().isEmpty(); + final boolean consumerEmpty = heartbeatDataWithSub.getConsumerDataSet().isEmpty(); + if (producerEmpty && consumerEmpty) { + log.warn("sendHeartbeatToBroker sending heartbeat, but no consumer and no producer. [{}]", this.clientId); + return false; + } + try { + if (clientConfig.isUseHeartbeatV2()) { + int currentHeartbeatFingerprint = heartbeatDataWithSub.computeHeartbeatFingerprint(); + heartbeatDataWithSub.setHeartbeatFingerprint(currentHeartbeatFingerprint); + HeartbeatData heartbeatDataWithoutSub = this.prepareHeartbeatData(true); + heartbeatDataWithoutSub.setHeartbeatFingerprint(currentHeartbeatFingerprint); + return this.sendHeartbeatToBrokerV2(id, brokerName, addr, heartbeatDataWithSub, heartbeatDataWithoutSub, currentHeartbeatFingerprint); + } else { + return this.sendHeartbeatToBroker(id, brokerName, addr, heartbeatDataWithSub); + } + } catch (final Exception e) { + log.error("sendHeartbeatToAllBroker exception", e); + } finally { + this.lockHeartbeat.unlock(); + } + } else { + log.warn("lock heartBeat, but failed. [{}]", this.clientId); + } + return false; + } + + private boolean sendHeartbeatToBroker(long id, String brokerName, String addr, HeartbeatData heartbeatData) { + try { + int version = this.mQClientAPIImpl.sendHeartbeat(addr, heartbeatData, clientConfig.getMqClientApiTimeout()); + if (!this.brokerVersionTable.containsKey(brokerName)) { + this.brokerVersionTable.put(brokerName, new HashMap<>(4)); + } + this.brokerVersionTable.get(brokerName).put(addr, version); + long times = this.sendHeartbeatTimesTotal.getAndIncrement(); + if (times % 20 == 0) { + log.info("send heart beat to broker[{} {} {}] success", brokerName, id, addr); + log.info(heartbeatData.toString()); + } + return true; + } catch (Exception e) { + if (this.isBrokerInNameServer(addr)) { + log.warn("send heart beat to broker[{} {} {}] failed", brokerName, id, addr, e); + } else { + log.warn("send heart beat to broker[{} {} {}] exception, because the broker not up, forget it", brokerName, + id, addr, e); + } + } + return false; + } + + private boolean sendHeartbeatToAllBroker() { + final HeartbeatData heartbeatData = this.prepareHeartbeatData(false); final boolean producerEmpty = heartbeatData.getProducerDataSet().isEmpty(); final boolean consumerEmpty = heartbeatData.getConsumerDataSet().isEmpty(); if (producerEmpty && consumerEmpty) { - log.warn("sending heartbeat, but no consumer and no producer"); - return; + log.warn("sending heartbeat, but no consumer and no producer. [{}]", this.clientId); + return false; } - if (!this.brokerAddrTable.isEmpty()) { - long times = this.sendHeartbeatTimesTotal.getAndIncrement(); - Iterator>> it = this.brokerAddrTable.entrySet().iterator(); - while (it.hasNext()) { - Entry> entry = it.next(); - String brokerName = entry.getKey(); - HashMap oneTable = entry.getValue(); - if (oneTable != null) { - for (Map.Entry entry1 : oneTable.entrySet()) { - Long id = entry1.getKey(); - String addr = entry1.getValue(); - if (addr != null) { - if (consumerEmpty) { - if (id != MixAll.MASTER_ID) - continue; - } + if (this.brokerAddrTable.isEmpty()) { + return false; + } + for (Entry> brokerClusterInfo : this.brokerAddrTable.entrySet()) { + String brokerName = brokerClusterInfo.getKey(); + HashMap oneTable = brokerClusterInfo.getValue(); + if (oneTable == null) { + continue; + } + for (Entry singleBrokerInstance : oneTable.entrySet()) { + Long id = singleBrokerInstance.getKey(); + String addr = singleBrokerInstance.getValue(); + if (addr == null) { + continue; + } + if (consumerEmpty && MixAll.MASTER_ID != id) { + continue; + } - try { - int version = this.mQClientAPIImpl.sendHearbeat(addr, heartbeatData, 3000); - if (!this.brokerVersionTable.containsKey(brokerName)) { - this.brokerVersionTable.put(brokerName, new HashMap(4)); - } - this.brokerVersionTable.get(brokerName).put(addr, version); - if (times % 20 == 0) { - log.info("send heart beat to broker[{} {} {}] success", brokerName, id, addr); - log.info(heartbeatData.toString()); - } - } catch (Exception e) { - if (this.isBrokerInNameServer(addr)) { - log.info("send heart beat to broker[{} {} {}] failed", brokerName, id, addr); - } else { - log.info("send heart beat to broker[{} {} {}] exception, because the broker not up, forget it", brokerName, - id, addr); - } - } - } + sendHeartbeatToBroker(id, brokerName, addr, heartbeatData); + } + } + return true; + } + + private boolean sendHeartbeatToBrokerV2(long id, String brokerName, String addr, HeartbeatData heartbeatDataWithSub, + HeartbeatData heartbeatDataWithoutSub, int currentHeartbeatFingerprint) { + try { + int version = 0; + boolean isBrokerSupportV2 = brokerSupportV2HeartbeatSet.contains(addr); + HeartbeatV2Result heartbeatV2Result = null; + if (isBrokerSupportV2 && null != brokerAddrHeartbeatFingerprintTable.get(addr) && brokerAddrHeartbeatFingerprintTable.get(addr) == currentHeartbeatFingerprint) { + heartbeatV2Result = this.mQClientAPIImpl.sendHeartbeatV2(addr, heartbeatDataWithoutSub, clientConfig.getMqClientApiTimeout()); + if (heartbeatV2Result.isSubChange()) { + brokerAddrHeartbeatFingerprintTable.remove(addr); + } + log.info("sendHeartbeatToAllBrokerV2 simple brokerName: {} subChange: {} brokerAddrHeartbeatFingerprintTable: {}", brokerName, heartbeatV2Result.isSubChange(), JSON.toJSONString(brokerAddrHeartbeatFingerprintTable)); + } else { + heartbeatV2Result = this.mQClientAPIImpl.sendHeartbeatV2(addr, heartbeatDataWithSub, clientConfig.getMqClientApiTimeout()); + if (heartbeatV2Result.isSupportV2()) { + brokerSupportV2HeartbeatSet.add(addr); + if (heartbeatV2Result.isSubChange()) { + brokerAddrHeartbeatFingerprintTable.remove(addr); + } else if (!brokerAddrHeartbeatFingerprintTable.containsKey(addr) || brokerAddrHeartbeatFingerprintTable.get(addr) != currentHeartbeatFingerprint) { + brokerAddrHeartbeatFingerprintTable.put(addr, currentHeartbeatFingerprint); } } + log.info("sendHeartbeatToAllBrokerV2 normal brokerName: {} subChange: {} brokerAddrHeartbeatFingerprintTable: {}", brokerName, heartbeatV2Result.isSubChange(), JSON.toJSONString(brokerAddrHeartbeatFingerprintTable)); + } + version = heartbeatV2Result.getVersion(); + if (!this.brokerVersionTable.containsKey(brokerName)) { + this.brokerVersionTable.put(brokerName, new HashMap<>(4)); + } + this.brokerVersionTable.get(brokerName).put(addr, version); + long times = this.sendHeartbeatTimesTotal.getAndIncrement(); + if (times % 20 == 0) { + log.info("send heart beat to broker[{} {} {}] success", brokerName, id, addr); + log.info(heartbeatDataWithSub.toString()); + } + return true; + } catch (Exception e) { + if (this.isBrokerInNameServer(addr)) { + log.warn("sendHeartbeatToAllBrokerV2 send heart beat to broker[{} {} {}] failed", brokerName, id, addr, e); + } else { + log.warn("sendHeartbeatToAllBrokerV2 send heart beat to broker[{} {} {}] exception, because the broker not up, forget it", brokerName, id, addr, e); } } + return false; } - private void uploadFilterClassSource() { - Iterator> it = this.consumerTable.entrySet().iterator(); - while (it.hasNext()) { - Entry next = it.next(); - MQConsumerInner consumer = next.getValue(); - if (ConsumeType.CONSUME_PASSIVELY == consumer.consumeType()) { - Set subscriptions = consumer.subscriptions(); - for (SubscriptionData sub : subscriptions) { - if (sub.isClassFilterMode() && sub.getFilterClassSource() != null) { - final String consumerGroup = consumer.groupName(); - final String className = sub.getSubString(); - final String topic = sub.getTopic(); - final String filterClassSource = sub.getFilterClassSource(); - try { - this.uploadFilterClassToAllFilterServer(consumerGroup, className, topic, filterClassSource); - } catch (Exception e) { - log.error("uploadFilterClassToAllFilterServer Exception", e); - } - } + private boolean sendHeartbeatToAllBrokerV2(boolean isRebalance) { + final HeartbeatData heartbeatDataWithSub = this.prepareHeartbeatData(false); + final boolean producerEmpty = heartbeatDataWithSub.getProducerDataSet().isEmpty(); + final boolean consumerEmpty = heartbeatDataWithSub.getConsumerDataSet().isEmpty(); + if (producerEmpty && consumerEmpty) { + log.warn("sendHeartbeatToAllBrokerV2 sending heartbeat, but no consumer and no producer. [{}]", this.clientId); + return false; + } + if (this.brokerAddrTable.isEmpty()) { + return false; + } + if (isRebalance) { + resetBrokerAddrHeartbeatFingerprintMap(); + } + int currentHeartbeatFingerprint = heartbeatDataWithSub.computeHeartbeatFingerprint(); + heartbeatDataWithSub.setHeartbeatFingerprint(currentHeartbeatFingerprint); + HeartbeatData heartbeatDataWithoutSub = this.prepareHeartbeatData(true); + heartbeatDataWithoutSub.setHeartbeatFingerprint(currentHeartbeatFingerprint); + + for (Entry> brokerClusterInfo : this.brokerAddrTable.entrySet()) { + String brokerName = brokerClusterInfo.getKey(); + HashMap oneTable = brokerClusterInfo.getValue(); + if (oneTable == null) { + continue; + } + for (Entry singleBrokerInstance : oneTable.entrySet()) { + Long id = singleBrokerInstance.getKey(); + String addr = singleBrokerInstance.getValue(); + if (addr == null) { + continue; + } + if (consumerEmpty && MixAll.MASTER_ID != id) { + continue; } + sendHeartbeatToBrokerV2(id, brokerName, addr, heartbeatDataWithSub, heartbeatDataWithoutSub, currentHeartbeatFingerprint); } } + return true; } public boolean updateTopicRouteInfoFromNameServer(final String topic, boolean isDefault, @@ -592,8 +769,7 @@ public boolean updateTopicRouteInfoFromNameServer(final String topic, boolean is try { TopicRouteData topicRouteData; if (isDefault && defaultMQProducer != null) { - topicRouteData = this.mQClientAPIImpl.getDefaultTopicRouteInfoFromNameServer(defaultMQProducer.getCreateTopicKey(), - 1000 * 3); + topicRouteData = this.mQClientAPIImpl.getDefaultTopicRouteInfoFromNameServer(clientConfig.getMqClientApiTimeout()); if (topicRouteData != null) { for (QueueData data : topicRouteData.getQueueDatas()) { int queueNums = Math.min(defaultMQProducer.getDefaultTopicQueueNums(), data.getReadQueueNums()); @@ -602,11 +778,11 @@ public boolean updateTopicRouteInfoFromNameServer(final String topic, boolean is } } } else { - topicRouteData = this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, 1000 * 3); + topicRouteData = this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, clientConfig.getMqClientApiTimeout()); } if (topicRouteData != null) { TopicRouteData old = this.topicRouteTable.get(topic); - boolean changed = topicRouteDataIsChange(old, topicRouteData); + boolean changed = topicRouteData.topicRouteDataChanged(old); if (!changed) { changed = this.isNeedUpdateTopicRouteInfo(topic); } else { @@ -614,19 +790,24 @@ public boolean updateTopicRouteInfoFromNameServer(final String topic, boolean is } if (changed) { - TopicRouteData cloneTopicRouteData = topicRouteData.cloneTopicRouteData(); for (BrokerData bd : topicRouteData.getBrokerDatas()) { this.brokerAddrTable.put(bd.getBrokerName(), bd.getBrokerAddrs()); } + // Update endpoint map + { + ConcurrentMap mqEndPoints = topicRouteData2EndpointsForStaticTopic(topic, topicRouteData); + if (!mqEndPoints.isEmpty()) { + topicEndPointsTable.put(topic, mqEndPoints); + } + } + // Update Pub info { TopicPublishInfo publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData); publishInfo.setHaveTopicRouterInfo(true); - Iterator> it = this.producerTable.entrySet().iterator(); - while (it.hasNext()) { - Entry entry = it.next(); + for (Entry entry : this.producerTable.entrySet()) { MQProducerInner impl = entry.getValue(); if (impl != null) { impl.updateTopicPublishInfo(topic, publishInfo); @@ -635,33 +816,35 @@ public boolean updateTopicRouteInfoFromNameServer(final String topic, boolean is } // Update sub info - { + if (!consumerTable.isEmpty()) { Set subscribeInfo = topicRouteData2TopicSubscribeInfo(topic, topicRouteData); - Iterator> it = this.consumerTable.entrySet().iterator(); - while (it.hasNext()) { - Entry entry = it.next(); + for (Entry entry : this.consumerTable.entrySet()) { MQConsumerInner impl = entry.getValue(); if (impl != null) { impl.updateTopicSubscribeInfo(topic, subscribeInfo); } } } + TopicRouteData cloneTopicRouteData = new TopicRouteData(topicRouteData); log.info("topicRouteTable.put. Topic = {}, TopicRouteData[{}]", topic, cloneTopicRouteData); this.topicRouteTable.put(topic, cloneTopicRouteData); return true; } } else { - log.warn("updateTopicRouteInfoFromNameServer, getTopicRouteInfoFromNameServer return null, Topic: {}", topic); + log.warn("updateTopicRouteInfoFromNameServer, getTopicRouteInfoFromNameServer return null, Topic: {}. [{}]", topic, this.clientId); } - } catch (Exception e) { - if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX) && !topic.equals(MixAll.DEFAULT_TOPIC)) { + } catch (MQClientException e) { + if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX) && !topic.equals(TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC)) { log.warn("updateTopicRouteInfoFromNameServer Exception", e); } + } catch (RemotingException e) { + log.error("updateTopicRouteInfoFromNameServer Exception", e); + throw new IllegalStateException(e); } finally { this.lockNamesrv.unlock(); } } else { - log.warn("updateTopicRouteInfoFromNameServer tryLock timeout {}ms", LOCK_TIMEOUT_MILLIS); + log.warn("updateTopicRouteInfoFromNameServer tryLock timeout {}ms. [{}]", LOCK_TIMEOUT_MILLIS, this.clientId); } } catch (InterruptedException e) { log.warn("updateTopicRouteInfoFromNameServer Exception", e); @@ -670,7 +853,7 @@ public boolean updateTopicRouteInfoFromNameServer(final String topic, boolean is return false; } - private HeartbeatData prepareHeartbeatData() { + private HeartbeatData prepareHeartbeatData(boolean isWithoutSub) { HeartbeatData heartbeatData = new HeartbeatData(); // clientID @@ -687,7 +870,9 @@ private HeartbeatData prepareHeartbeatData() { consumerData.setConsumeFromWhere(impl.consumeFromWhere()); consumerData.getSubscriptionDataSet().addAll(impl.subscriptions()); consumerData.setUnitMode(impl.isUnitMode()); - + if (!isWithoutSub) { + consumerData.getSubscriptionDataSet().addAll(impl.subscriptions()); + } heartbeatData.getConsumerDataSet().add(consumerData); } } @@ -702,14 +887,12 @@ private HeartbeatData prepareHeartbeatData() { heartbeatData.getProducerDataSet().add(producerData); } } - + heartbeatData.setWithoutSub(isWithoutSub); return heartbeatData; } private boolean isBrokerInNameServer(final String brokerAddr) { - Iterator> it = this.topicRouteTable.entrySet().iterator(); - while (it.hasNext()) { - Entry itNext = it.next(); + for (Entry itNext : this.topicRouteTable.entrySet()) { List brokerDatas = itNext.getValue().getBrokerDatas(); for (BrokerData bd : brokerDatas) { boolean contain = bd.getBrokerAddrs().containsValue(brokerAddr); @@ -721,80 +904,27 @@ private boolean isBrokerInNameServer(final String brokerAddr) { return false; } - private void uploadFilterClassToAllFilterServer(final String consumerGroup, final String fullClassName, - final String topic, - final String filterClassSource) throws UnsupportedEncodingException { - byte[] classBody = null; - int classCRC = 0; - try { - classBody = filterClassSource.getBytes(MixAll.DEFAULT_CHARSET); - classCRC = UtilAll.crc32(classBody); - } catch (Exception e1) { - log.warn("uploadFilterClassToAllFilterServer Exception, ClassName: {} {}", - fullClassName, - RemotingHelper.exceptionSimpleDesc(e1)); - } - - TopicRouteData topicRouteData = this.topicRouteTable.get(topic); - if (topicRouteData != null - && topicRouteData.getFilterServerTable() != null && !topicRouteData.getFilterServerTable().isEmpty()) { - Iterator>> it = topicRouteData.getFilterServerTable().entrySet().iterator(); - while (it.hasNext()) { - Entry> next = it.next(); - List value = next.getValue(); - for (final String fsAddr : value) { - try { - this.mQClientAPIImpl.registerMessageFilterClass(fsAddr, consumerGroup, topic, fullClassName, classCRC, classBody, - 5000); - - log.info("register message class filter to {} OK, ConsumerGroup: {} Topic: {} ClassName: {}", fsAddr, consumerGroup, - topic, fullClassName); - - } catch (Exception e) { - log.error("uploadFilterClassToAllFilterServer Exception", e); - } - } + private boolean isNeedUpdateTopicRouteInfo(final String topic) { + boolean result = false; + Iterator> producerIterator = this.producerTable.entrySet().iterator(); + while (producerIterator.hasNext() && !result) { + Entry entry = producerIterator.next(); + MQProducerInner impl = entry.getValue(); + if (impl != null) { + result = impl.isPublishTopicNeedUpdate(topic); } - } else { - log.warn("register message class filter failed, because no filter server, ConsumerGroup: {} Topic: {} ClassName: {}", - consumerGroup, topic, fullClassName); } - } - private boolean topicRouteDataIsChange(TopicRouteData olddata, TopicRouteData nowdata) { - if (olddata == null || nowdata == null) + if (result) { return true; - TopicRouteData old = olddata.cloneTopicRouteData(); - TopicRouteData now = nowdata.cloneTopicRouteData(); - Collections.sort(old.getQueueDatas()); - Collections.sort(old.getBrokerDatas()); - Collections.sort(now.getQueueDatas()); - Collections.sort(now.getBrokerDatas()); - return !old.equals(now); - - } - - private boolean isNeedUpdateTopicRouteInfo(final String topic) { - boolean result = false; - { - Iterator> it = this.producerTable.entrySet().iterator(); - while (it.hasNext() && !result) { - Entry entry = it.next(); - MQProducerInner impl = entry.getValue(); - if (impl != null) { - result = impl.isPublishTopicNeedUpdate(topic); - } - } } - { - Iterator> it = this.consumerTable.entrySet().iterator(); - while (it.hasNext() && !result) { - Entry entry = it.next(); - MQConsumerInner impl = entry.getValue(); - if (impl != null) { - result = impl.isSubscribeTopicNeedUpdate(topic); - } + Iterator> consumerIterator = this.consumerTable.entrySet().iterator(); + while (consumerIterator.hasNext() && !result) { + Entry entry = consumerIterator.next(); + MQConsumerInner impl = entry.getValue(); + if (impl != null) { + result = impl.isSubscribeTopicNeedUpdate(topic); } } @@ -816,8 +946,6 @@ public void shutdown() { synchronized (this) { switch (this.serviceState) { - case CREATE_JUST: - break; case RUNNING: this.defaultMQProducer.getDefaultMQProducerImpl().shutdown(false); @@ -827,22 +955,18 @@ public void shutdown() { this.mQClientAPIImpl.shutdown(); this.rebalanceService.shutdown(); - if (this.datagramSocket != null) { - this.datagramSocket.close(); - this.datagramSocket = null; - } MQClientManager.getInstance().removeClientFactory(this.clientId); log.info("the client factory [{}] shutdown OK", this.clientId); break; + case CREATE_JUST: case SHUTDOWN_ALREADY: - break; default: break; } } } - public boolean registerConsumer(final String group, final MQConsumerInner consumer) { + public synchronized boolean registerConsumer(final String group, final MQConsumerInner consumer) { if (null == group || null == consumer) { return false; } @@ -856,57 +980,38 @@ public boolean registerConsumer(final String group, final MQConsumerInner consum return true; } - public void unregisterConsumer(final String group) { + public synchronized void unregisterConsumer(final String group) { this.consumerTable.remove(group); - this.unregisterClientWithLock(null, group); - } - - private void unregisterClientWithLock(final String producerGroup, final String consumerGroup) { - try { - if (this.lockHeartbeat.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { - try { - this.unregisterClient(producerGroup, consumerGroup); - } catch (Exception e) { - log.error("unregisterClient exception", e); - } finally { - this.lockHeartbeat.unlock(); - } - } else { - log.warn("lock heartBeat, but failed."); - } - } catch (InterruptedException e) { - log.warn("unregisterClientWithLock exception", e); - } + this.unregisterClient(null, group); } private void unregisterClient(final String producerGroup, final String consumerGroup) { - Iterator>> it = this.brokerAddrTable.entrySet().iterator(); - while (it.hasNext()) { - Entry> entry = it.next(); - String brokerName = entry.getKey(); - HashMap oneTable = entry.getValue(); - - if (oneTable != null) { - for (Map.Entry entry1 : oneTable.entrySet()) { - String addr = entry1.getValue(); - if (addr != null) { - try { - this.mQClientAPIImpl.unregisterClient(addr, this.clientId, producerGroup, consumerGroup, 3000); - log.info("unregister client[Producer: {} Consumer: {}] from broker[{} {} {}] success", producerGroup, consumerGroup, brokerName, entry1.getKey(), addr); - } catch (RemotingException e) { - log.error("unregister client exception from broker: " + addr, e); - } catch (InterruptedException e) { - log.error("unregister client exception from broker: " + addr, e); - } catch (MQBrokerException e) { - log.error("unregister client exception from broker: " + addr, e); - } + for (Entry> brokerClusterInfo : this.brokerAddrTable.entrySet()) { + String brokerName = brokerClusterInfo.getKey(); + HashMap oneTable = brokerClusterInfo.getValue(); + + if (oneTable == null) { + continue; + } + for (Entry singleBrokerInstance : oneTable.entrySet()) { + String addr = singleBrokerInstance.getValue(); + if (addr != null) { + try { + this.mQClientAPIImpl.unregisterClient(addr, this.clientId, producerGroup, consumerGroup, clientConfig.getMqClientApiTimeout()); + log.info("unregister client[Producer: {} Consumer: {}] from broker[{} {} {}] success", producerGroup, consumerGroup, brokerName, singleBrokerInstance.getKey(), addr); + } catch (RemotingException e) { + log.warn("unregister client RemotingException from broker: {}, {}", addr, e.getMessage()); + } catch (InterruptedException e) { + log.warn("unregister client InterruptedException from broker: {}, {}", addr, e.getMessage()); + } catch (MQBrokerException e) { + log.warn("unregister client MQBrokerException from broker: {}, {}", addr, e.getMessage()); } } } } } - public boolean registerProducer(final String group, final DefaultMQProducerImpl producer) { + public synchronized boolean registerProducer(final String group, final DefaultMQProducerImpl producer) { if (null == group || null == producer) { return false; } @@ -920,9 +1025,9 @@ public boolean registerProducer(final String group, final DefaultMQProducerImpl return true; } - public void unregisterProducer(final String group) { + public synchronized void unregisterProducer(final String group) { this.producerTable.remove(group); - this.unregisterClientWithLock(group, null); + this.unregisterClient(group, null); } public boolean registerAdminExt(final String group, final MQAdminExtInner admin) { @@ -943,21 +1048,34 @@ public void unregisterAdminExt(final String group) { this.adminExtTable.remove(group); } + public void rebalanceLater(long delayMillis) { + if (delayMillis <= 0) { + this.rebalanceService.wakeup(); + } else { + this.scheduledExecutorService.schedule(MQClientInstance.this.rebalanceService::wakeup, delayMillis, TimeUnit.MILLISECONDS); + } + } + public void rebalanceImmediately() { this.rebalanceService.wakeup(); } - public void doRebalance() { + public boolean doRebalance() { + boolean balanced = true; for (Map.Entry entry : this.consumerTable.entrySet()) { MQConsumerInner impl = entry.getValue(); if (impl != null) { try { - impl.doRebalance(); + if (!impl.tryRebalance()) { + balanced = false; + } } catch (Throwable e) { log.error("doRebalance exception", e); } } } + + return balanced; } public MQProducerInner selectProducer(final String group) { @@ -968,7 +1086,17 @@ public MQConsumerInner selectConsumer(final String group) { return this.consumerTable.get(group); } + public String getBrokerNameFromMessageQueue(final MessageQueue mq) { + if (topicEndPointsTable.get(mq.getTopic()) != null && !topicEndPointsTable.get(mq.getTopic()).isEmpty()) { + return topicEndPointsTable.get(mq.getTopic()).get(mq); + } + return mq.getBrokerName(); + } + public FindBrokerResult findBrokerAddressInAdmin(final String brokerName) { + if (brokerName == null) { + return null; + } String brokerAddr = null; boolean slave = false; boolean found = false; @@ -980,11 +1108,7 @@ public FindBrokerResult findBrokerAddressInAdmin(final String brokerName) { brokerAddr = entry.getValue(); if (brokerAddr != null) { found = true; - if (MixAll.MASTER_ID == id) { - slave = false; - } else { - slave = true; - } + slave = MixAll.MASTER_ID != id; break; } @@ -999,6 +1123,9 @@ public FindBrokerResult findBrokerAddressInAdmin(final String brokerName) { } public String findBrokerAddressInPublish(final String brokerName) { + if (brokerName == null) { + return null; + } HashMap map = this.brokerAddrTable.get(brokerName); if (map != null && !map.isEmpty()) { return map.get(MixAll.MASTER_ID); @@ -1012,6 +1139,9 @@ public FindBrokerResult findBrokerAddressInSubscribe( final long brokerId, final boolean onlyThisBroker ) { + if (brokerName == null) { + return null; + } String brokerAddr = null; boolean slave = false; boolean found = false; @@ -1022,6 +1152,11 @@ public FindBrokerResult findBrokerAddressInSubscribe( slave = brokerId != MixAll.MASTER_ID; found = brokerAddr != null; + if (!found && slave) { + brokerAddr = map.get(brokerId + 1); + found = brokerAddr != null; + } + if (!found && !onlyThisBroker) { Entry entry = map.entrySet().iterator().next(); brokerAddr = entry.getValue(); @@ -1037,12 +1172,13 @@ public FindBrokerResult findBrokerAddressInSubscribe( return null; } - public int findBrokerVersion(String brokerName, String brokerAddr) { + private int findBrokerVersion(String brokerName, String brokerAddr) { if (this.brokerVersionTable.containsKey(brokerName)) { if (this.brokerVersionTable.get(brokerName).containsKey(brokerAddr)) { return this.brokerVersionTable.get(brokerName).get(brokerAddr); } } + //To do need to fresh the version return 0; } @@ -1055,7 +1191,7 @@ public List findConsumerIdList(final String topic, final String group) { if (null != brokerAddr) { try { - return this.mQClientAPIImpl.getConsumerIdListByGroup(brokerAddr, group, 3000); + return this.mQClientAPIImpl.getConsumerIdListByGroup(brokerAddr, group, clientConfig.getMqClientApiTimeout()); } catch (Exception e) { log.warn("getConsumerIdListByGroup exception, " + brokerAddr + " " + group, e); } @@ -1064,6 +1200,23 @@ public List findConsumerIdList(final String topic, final String group) { return null; } + public Set queryAssignment(final String topic, final String consumerGroup, + final String strategyName, final MessageModel messageModel, int timeout) + throws RemotingException, InterruptedException, MQBrokerException { + String brokerAddr = this.findBrokerAddrByTopic(topic); + if (null == brokerAddr) { + this.updateTopicRouteInfoFromNameServer(topic); + brokerAddr = this.findBrokerAddrByTopic(topic); + } + + if (null != brokerAddr) { + return this.mQClientAPIImpl.queryAssignment(brokerAddr, topic, consumerGroup, clientId, strategyName, + messageModel, timeout); + } + + return null; + } + public String findBrokerAddrByTopic(final String topic) { TopicRouteData topicRouteData = this.topicRouteTable.get(topic); if (topicRouteData != null) { @@ -1078,11 +1231,11 @@ public String findBrokerAddrByTopic(final String topic) { return null; } - public void resetOffset(String topic, String group, Map offsetTable) { + public synchronized void resetOffset(String topic, String group, Map offsetTable) { DefaultMQPushConsumerImpl consumer = null; try { MQConsumerInner impl = this.consumerTable.get(group); - if (impl != null && impl instanceof DefaultMQPushConsumerImpl) { + if (impl instanceof DefaultMQPushConsumerImpl) { consumer = (DefaultMQPushConsumerImpl) impl; } else { log.info("[reset-offset] consumer dose not exist. group={}", group); @@ -1102,7 +1255,7 @@ public void resetOffset(String topic, String group, Map offs try { TimeUnit.SECONDS.sleep(10); - } catch (InterruptedException e) { + } catch (InterruptedException ignored) { } Iterator iterator = processQueueTable.keySet().iterator(); @@ -1126,12 +1279,13 @@ public void resetOffset(String topic, String group, Map offs } } + @SuppressWarnings("unchecked") public Map getConsumerStatus(String topic, String group) { MQConsumerInner impl = this.consumerTable.get(group); - if (impl != null && impl instanceof DefaultMQPushConsumerImpl) { + if (impl instanceof DefaultMQPushConsumerImpl) { DefaultMQPushConsumerImpl consumer = (DefaultMQPushConsumerImpl) impl; return consumer.getOffsetStore().cloneOffsetTable(topic); - } else if (impl != null && impl instanceof DefaultMQPullConsumerImpl) { + } else if (impl instanceof DefaultMQPullConsumerImpl) { DefaultMQPullConsumerImpl consumer = (DefaultMQPullConsumerImpl) impl; return consumer.getOffsetStore().cloneOffsetTable(topic); } else { @@ -1175,11 +1329,10 @@ public ConsumeMessageDirectlyResult consumeMessageDirectly(final MessageExt msg, final String consumerGroup, final String brokerName) { MQConsumerInner mqConsumerInner = this.consumerTable.get(consumerGroup); - if (null != mqConsumerInner) { + if (mqConsumerInner instanceof DefaultMQPushConsumerImpl) { DefaultMQPushConsumerImpl consumer = (DefaultMQPushConsumerImpl) mqConsumerInner; - ConsumeMessageDirectlyResult result = consumer.getConsumeMessageService().consumeMessageDirectly(msg, brokerName); - return result; + return consumer.getConsumeMessageService().consumeMessageDirectly(msg, brokerName); } return null; @@ -1187,6 +1340,9 @@ public ConsumeMessageDirectlyResult consumeMessageDirectly(final MessageExt msg, public ConsumerRunningInfo consumerRunningInfo(final String consumerGroup) { MQConsumerInner mqConsumerInner = this.consumerTable.get(consumerGroup); + if (mqConsumerInner == null) { + return null; + } ConsumerRunningInfo consumerRunningInfo = mqConsumerInner.consumerRunningInfo(); @@ -1208,6 +1364,10 @@ public ConsumerRunningInfo consumerRunningInfo(final String consumerGroup) { return consumerRunningInfo; } + private void resetBrokerAddrHeartbeatFingerprintMap() { + brokerAddrHeartbeatFingerprintTable.clear(); + } + public ConsumerStatsManager getConsumerStatsManager() { return consumerStatsManager; } @@ -1215,4 +1375,17 @@ public ConsumerStatsManager getConsumerStatsManager() { public NettyClientConfig getNettyClientConfig() { return nettyClientConfig; } + + public ClientConfig getClientConfig() { + return clientConfig; + } + + public TopicRouteData queryTopicRouteData(String topic) { + TopicRouteData data = this.getAnExistTopicRouteData(topic); + if (data == null) { + this.updateTopicRouteInfoFromNameServer(topic); + data = this.getAnExistTopicRouteData(topic); + } + return data; + } } diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/mqclient/DoNothingClientRemotingProcessor.java b/client/src/main/java/org/apache/rocketmq/client/impl/mqclient/DoNothingClientRemotingProcessor.java new file mode 100644 index 00000000000..9d489f8adaf --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/mqclient/DoNothingClientRemotingProcessor.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.impl.mqclient; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.client.impl.ClientRemotingProcessor; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public class DoNothingClientRemotingProcessor extends ClientRemotingProcessor { + + public DoNothingClientRemotingProcessor(MQClientInstance mqClientFactory) { + super(mqClientFactory); + } + + @Override + public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) { + return null; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/mqclient/MQClientAPIExt.java b/client/src/main/java/org/apache/rocketmq/client/impl/mqclient/MQClientAPIExt.java new file mode 100644 index 00000000000..b97e00c577f --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/mqclient/MQClientAPIExt.java @@ -0,0 +1,618 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.impl.mqclient; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.client.consumer.AckCallback; +import org.apache.rocketmq.client.consumer.AckResult; +import org.apache.rocketmq.client.consumer.PopCallback; +import org.apache.rocketmq.client.consumer.PopResult; +import org.apache.rocketmq.client.consumer.PullCallback; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.consumer.PullStatus; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.OffsetNotFoundException; +import org.apache.rocketmq.client.impl.ClientRemotingProcessor; +import org.apache.rocketmq.client.impl.CommunicationMode; +import org.apache.rocketmq.client.impl.MQClientAPIImpl; +import org.apache.rocketmq.client.impl.admin.MqClientAdminImpl; +import org.apache.rocketmq.client.impl.consumer.PullResultExt; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageBatch; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.InvokeCallback; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.ResponseFuture; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.LockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.body.LockBatchResponseBody; +import org.apache.rocketmq.remoting.protocol.body.UnlockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.header.AckMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ChangeInvisibleTimeRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ConsumerSendMsgBackRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerListByGroupRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerListByGroupResponseBody; +import org.apache.rocketmq.remoting.protocol.header.GetMaxOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMaxOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMinOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMinOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.HeartbeatRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.LockBatchMqRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.NotificationRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.NotificationResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.PopMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumerOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumerOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.SearchOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SearchOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeaderV2; +import org.apache.rocketmq.remoting.protocol.header.UnlockBatchMqRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateConsumerOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.HeartbeatData; + +public class MQClientAPIExt extends MQClientAPIImpl { + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + + private final ClientConfig clientConfig; + + private final MqClientAdminImpl mqClientAdmin; + + public MQClientAPIExt( + ClientConfig clientConfig, + NettyClientConfig nettyClientConfig, + ClientRemotingProcessor clientRemotingProcessor, + RPCHook rpcHook + ) { + super(nettyClientConfig, clientRemotingProcessor, rpcHook, clientConfig); + this.clientConfig = clientConfig; + this.mqClientAdmin = new MqClientAdminImpl(getRemotingClient()); + } + + public boolean updateNameServerAddressList() { + if (this.clientConfig.getNamesrvAddr() != null) { + this.updateNameServerAddressList(this.clientConfig.getNamesrvAddr()); + log.info("user specified name server address: {}", this.clientConfig.getNamesrvAddr()); + return true; + } + return false; + } + + public CompletableFuture sendHeartbeatOneway( + String brokerAddr, + HeartbeatData heartbeatData, + long timeoutMillis + ) { + CompletableFuture future = new CompletableFuture<>(); + try { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.HEART_BEAT, new HeartbeatRequestHeader()); + request.setLanguage(clientConfig.getLanguage()); + request.setBody(heartbeatData.encode()); + this.getRemotingClient().invokeOneway(brokerAddr, request, timeoutMillis); + future.complete(null); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + public CompletableFuture sendHeartbeatAsync( + String brokerAddr, + HeartbeatData heartbeatData, + long timeoutMillis + ) { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.HEART_BEAT, new HeartbeatRequestHeader()); + request.setLanguage(clientConfig.getLanguage()); + request.setBody(heartbeatData.encode()); + + return this.getRemotingClient().invoke(brokerAddr, request, timeoutMillis).thenCompose(response -> { + CompletableFuture future0 = new CompletableFuture<>(); + if (ResponseCode.SUCCESS == response.getCode()) { + future0.complete(response.getVersion()); + } else { + future0.completeExceptionally(new MQBrokerException(response.getCode(), response.getRemark(), brokerAddr)); + } + return future0; + }); + } + + public CompletableFuture sendMessageAsync( + String brokerAddr, + String brokerName, + Message msg, + SendMessageRequestHeader requestHeader, + long timeoutMillis + ) { + SendMessageRequestHeaderV2 requestHeaderV2 = SendMessageRequestHeaderV2.createSendMessageRequestHeaderV2(requestHeader); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE_V2, requestHeaderV2); + request.setBody(msg.getBody()); + + return this.getRemotingClient().invoke(brokerAddr, request, timeoutMillis).thenCompose(response -> { + CompletableFuture future0 = new CompletableFuture<>(); + try { + future0.complete(this.processSendResponse(brokerName, msg, response, brokerAddr)); + } catch (Exception e) { + future0.completeExceptionally(e); + } + return future0; + }); + } + + public CompletableFuture sendMessageAsync( + String brokerAddr, + String brokerName, + List msgList, + SendMessageRequestHeader requestHeader, + long timeoutMillis + ) { + SendMessageRequestHeaderV2 requestHeaderV2 = SendMessageRequestHeaderV2.createSendMessageRequestHeaderV2(requestHeader); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.SEND_BATCH_MESSAGE, requestHeaderV2); + + CompletableFuture future = new CompletableFuture<>(); + try { + requestHeader.setBatch(true); + MessageBatch msgBatch = MessageBatch.generateFromList(msgList); + MessageClientIDSetter.setUniqID(msgBatch); + byte[] body = msgBatch.encode(); + msgBatch.setBody(body); + + request.setBody(body); + return this.getRemotingClient().invoke(brokerAddr, request, timeoutMillis).thenCompose(response -> { + CompletableFuture future0 = new CompletableFuture<>(); + try { + future0.complete(processSendResponse(brokerName, msgBatch, response, brokerAddr)); + } catch (Exception e) { + future0.completeExceptionally(e); + } + return future0; + }); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + public CompletableFuture sendMessageBackAsync( + String brokerAddr, + ConsumerSendMsgBackRequestHeader requestHeader, + long timeoutMillis + ) { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CONSUMER_SEND_MSG_BACK, requestHeader); + return this.getRemotingClient().invoke(brokerAddr, request, timeoutMillis); + } + + public CompletableFuture popMessageAsync( + String brokerAddr, + String brokerName, + PopMessageRequestHeader requestHeader, + long timeoutMillis + ) { + CompletableFuture future = new CompletableFuture<>(); + try { + this.popMessageAsync(brokerName, brokerAddr, requestHeader, timeoutMillis, new PopCallback() { + @Override + public void onSuccess(PopResult popResult) { + future.complete(popResult); + } + + @Override + public void onException(Throwable t) { + future.completeExceptionally(t); + } + }); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + public CompletableFuture ackMessageAsync( + String brokerAddr, + AckMessageRequestHeader requestHeader, + long timeoutMillis + ) { + CompletableFuture future = new CompletableFuture<>(); + try { + this.ackMessageAsync(brokerAddr, timeoutMillis, new AckCallback() { + @Override + public void onSuccess(AckResult ackResult) { + future.complete(ackResult); + } + + @Override + public void onException(Throwable t) { + future.completeExceptionally(t); + } + }, requestHeader); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + public CompletableFuture batchAckMessageAsync( + String brokerAddr, + String topic, + String consumerGroup, + List extraInfoList, + long timeoutMillis + ) { + CompletableFuture future = new CompletableFuture<>(); + try { + this.batchAckMessageAsync(brokerAddr, timeoutMillis, new AckCallback() { + @Override + public void onSuccess(AckResult ackResult) { + future.complete(ackResult); + } + + @Override + public void onException(Throwable t) { + future.completeExceptionally(t); + } + }, topic, consumerGroup, extraInfoList); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + public CompletableFuture changeInvisibleTimeAsync( + String brokerAddr, + String brokerName, + ChangeInvisibleTimeRequestHeader requestHeader, + long timeoutMillis + ) { + CompletableFuture future = new CompletableFuture<>(); + try { + this.changeInvisibleTimeAsync(brokerName, brokerAddr, requestHeader, timeoutMillis, + new AckCallback() { + @Override + public void onSuccess(AckResult ackResult) { + future.complete(ackResult); + } + + @Override + public void onException(Throwable t) { + future.completeExceptionally(t); + } + } + ); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + public CompletableFuture pullMessageAsync( + String brokerAddr, + PullMessageRequestHeader requestHeader, + long timeoutMillis + ) { + CompletableFuture future = new CompletableFuture<>(); + try { + this.pullMessage(brokerAddr, requestHeader, timeoutMillis, CommunicationMode.ASYNC, + new PullCallback() { + @Override + public void onSuccess(PullResult pullResult) { + if (pullResult instanceof PullResultExt) { + PullResultExt pullResultExt = (PullResultExt) pullResult; + if (PullStatus.FOUND.equals(pullResult.getPullStatus())) { + List messageExtList = MessageDecoder.decodesBatch( + ByteBuffer.wrap(pullResultExt.getMessageBinary()), + true, + false, + true + ); + pullResult.setMsgFoundList(messageExtList); + } + } + future.complete(pullResult); + } + + @Override + public void onException(Throwable t) { + future.completeExceptionally(t); + } + } + ); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + public CompletableFuture queryConsumerOffsetWithFuture( + String brokerAddr, + QueryConsumerOffsetRequestHeader requestHeader, + long timeoutMillis + ) { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.QUERY_CONSUMER_OFFSET, requestHeader); + return this.getRemotingClient().invoke(brokerAddr, request, timeoutMillis).thenCompose(response -> { + CompletableFuture future0 = new CompletableFuture<>(); + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + try { + QueryConsumerOffsetResponseHeader responseHeader = + (QueryConsumerOffsetResponseHeader) response.decodeCommandCustomHeader(QueryConsumerOffsetResponseHeader.class); + future0.complete(responseHeader.getOffset()); + } catch (RemotingCommandException e) { + future0.completeExceptionally(e); + } + break; + } + case ResponseCode.QUERY_NOT_FOUND: { + future0.completeExceptionally(new OffsetNotFoundException(response.getCode(), response.getRemark(), brokerAddr)); + break; + } + default: { + future0.completeExceptionally(new MQBrokerException(response.getCode(), response.getRemark())); + break; + } + } + return future0; + }); + } + + public CompletableFuture updateConsumerOffsetOneWay( + String brokerAddr, + UpdateConsumerOffsetRequestHeader header, + long timeoutMillis + ) { + CompletableFuture future = new CompletableFuture<>(); + try { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_CONSUMER_OFFSET, header); + this.getRemotingClient().invokeOneway(brokerAddr, request, timeoutMillis); + future.complete(null); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + public CompletableFuture> getConsumerListByGroupAsync( + String brokerAddr, + GetConsumerListByGroupRequestHeader requestHeader, + long timeoutMillis + ) { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_CONSUMER_LIST_BY_GROUP, requestHeader); + + CompletableFuture> future = new CompletableFuture<>(); + try { + this.getRemotingClient().invokeAsync(brokerAddr, request, timeoutMillis, new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { + + } + + @Override + public void operationSucceed(RemotingCommand response) { + switch (response.getCode()) { + case ResponseCode.SUCCESS: { + if (response.getBody() != null) { + GetConsumerListByGroupResponseBody body = + GetConsumerListByGroupResponseBody.decode(response.getBody(), GetConsumerListByGroupResponseBody.class); + future.complete(body.getConsumerIdList()); + return; + } + } + /* + @see org.apache.rocketmq.broker.processor.ConsumerManageProcessor#getConsumerListByGroup, + * broker will return {@link ResponseCode.SYSTEM_ERROR} if there is no consumer. + */ + case ResponseCode.SYSTEM_ERROR: { + future.complete(Collections.emptyList()); + return; + } + default: + break; + } + future.completeExceptionally(new MQBrokerException(response.getCode(), response.getRemark())); + } + + @Override + public void operationFail(Throwable throwable) { + future.completeExceptionally(throwable); + } + }); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + public CompletableFuture getMaxOffset(String brokerAddr, GetMaxOffsetRequestHeader requestHeader, + long timeoutMillis) { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_MAX_OFFSET, requestHeader); + + CompletableFuture future = new CompletableFuture<>(); + try { + this.getRemotingClient().invokeAsync(brokerAddr, request, timeoutMillis, new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { + + } + + @Override + public void operationSucceed(RemotingCommand response) { + if (ResponseCode.SUCCESS == response.getCode()) { + try { + GetMaxOffsetResponseHeader responseHeader = (GetMaxOffsetResponseHeader) response.decodeCommandCustomHeader(GetMaxOffsetResponseHeader.class); + future.complete(responseHeader.getOffset()); + } catch (Throwable t) { + future.completeExceptionally(t); + } + } + future.completeExceptionally(new MQBrokerException(response.getCode(), response.getRemark())); + } + + @Override + public void operationFail(Throwable throwable) { + future.completeExceptionally(throwable); + } + }); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + public CompletableFuture getMinOffset(String brokerAddr, GetMinOffsetRequestHeader requestHeader, + long timeoutMillis) { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_MIN_OFFSET, requestHeader); + + CompletableFuture future = new CompletableFuture<>(); + try { + this.getRemotingClient().invokeAsync(brokerAddr, request, timeoutMillis, new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { + + } + + @Override + public void operationSucceed(RemotingCommand response) { + if (ResponseCode.SUCCESS == response.getCode()) { + try { + GetMinOffsetResponseHeader responseHeader = (GetMinOffsetResponseHeader) response.decodeCommandCustomHeader(GetMinOffsetResponseHeader.class); + future.complete(responseHeader.getOffset()); + } catch (Throwable t) { + future.completeExceptionally(t); + } + } + future.completeExceptionally(new MQBrokerException(response.getCode(), response.getRemark())); + } + + @Override + public void operationFail(Throwable throwable) { + future.completeExceptionally(throwable); + } + }); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + public CompletableFuture searchOffset(String brokerAddr, SearchOffsetRequestHeader requestHeader, + long timeoutMillis) { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.SEARCH_OFFSET_BY_TIMESTAMP, requestHeader); + + return this.getRemotingClient().invoke(brokerAddr, request, timeoutMillis).thenCompose(response -> { + CompletableFuture future0 = new CompletableFuture<>(); + if (response.getCode() == ResponseCode.SUCCESS) { + try { + SearchOffsetResponseHeader responseHeader = (SearchOffsetResponseHeader) response.decodeCommandCustomHeader(SearchOffsetResponseHeader.class); + future0.complete(responseHeader.getOffset()); + } catch (Throwable t) { + future0.completeExceptionally(t); + } + } else { + future0.completeExceptionally(new MQBrokerException(response.getCode(), response.getRemark())); + } + return future0; + }); + } + + public CompletableFuture> lockBatchMQWithFuture(String brokerAddr, + LockBatchRequestBody requestBody, long timeoutMillis) { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.LOCK_BATCH_MQ, new LockBatchMqRequestHeader()); + request.setBody(requestBody.encode()); + return this.getRemotingClient().invoke(brokerAddr, request, timeoutMillis).thenCompose(response -> { + CompletableFuture> future0 = new CompletableFuture<>(); + if (response.getCode() == ResponseCode.SUCCESS) { + try { + LockBatchResponseBody responseBody = LockBatchResponseBody.decode(response.getBody(), LockBatchResponseBody.class); + Set messageQueues = responseBody.getLockOKMQSet(); + future0.complete(messageQueues); + } catch (Throwable t) { + future0.completeExceptionally(t); + } + } else { + future0.completeExceptionally(new MQBrokerException(response.getCode(), response.getRemark())); + } + return future0; + }); + } + + public CompletableFuture unlockBatchMQOneway(String brokerAddr, + UnlockBatchRequestBody requestBody, long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UNLOCK_BATCH_MQ, new UnlockBatchMqRequestHeader()); + request.setBody(requestBody.encode()); + try { + this.getRemotingClient().invokeOneway(brokerAddr, request, timeoutMillis); + future.complete(null); + } catch (Exception e) { + future.completeExceptionally(e); + } + return future; + } + + public CompletableFuture notification(String brokerAddr, NotificationRequestHeader requestHeader, + long timeoutMillis) { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.NOTIFICATION, requestHeader); + return this.getRemotingClient().invoke(brokerAddr, request, timeoutMillis).thenCompose(response -> { + CompletableFuture future0 = new CompletableFuture<>(); + if (response.getCode() == ResponseCode.SUCCESS) { + try { + NotificationResponseHeader responseHeader = (NotificationResponseHeader) response.decodeCommandCustomHeader(NotificationResponseHeader.class); + future0.complete(responseHeader.isHasMsg()); + } catch (Throwable t) { + future0.completeExceptionally(t); + } + } else { + future0.completeExceptionally(new MQBrokerException(response.getCode(), response.getRemark())); + } + return future0; + }); + } + + public CompletableFuture invoke(String brokerAddr, RemotingCommand request, long timeoutMillis) { + return getRemotingClient().invoke(brokerAddr, request, timeoutMillis); + } + + public CompletableFuture invokeOneway(String brokerAddr, RemotingCommand request, long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + try { + this.getRemotingClient().invokeOneway(brokerAddr, request, timeoutMillis); + future.complete(null); + } catch (Exception e) { + future.completeExceptionally(e); + } + return future; + } + + public MqClientAdminImpl getMqClientAdmin() { + return mqClientAdmin; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/mqclient/MQClientAPIFactory.java b/client/src/main/java/org/apache/rocketmq/client/impl/mqclient/MQClientAPIFactory.java new file mode 100644 index 00000000000..c68859b2889 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/impl/mqclient/MQClientAPIFactory.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.impl.mqclient; + +import com.google.common.base.Strings; +import java.time.Duration; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.client.common.NameserverAccessConfig; +import org.apache.rocketmq.client.impl.ClientRemotingProcessor; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.utils.StartAndShutdown; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; + +public class MQClientAPIFactory implements StartAndShutdown { + + private MQClientAPIExt[] clients; + private final String namePrefix; + private final int clientNum; + private final ClientRemotingProcessor clientRemotingProcessor; + private final RPCHook rpcHook; + private final ScheduledExecutorService scheduledExecutorService; + private final NameserverAccessConfig nameserverAccessConfig; + + public MQClientAPIFactory(NameserverAccessConfig nameserverAccessConfig, String namePrefix, int clientNum, + ClientRemotingProcessor clientRemotingProcessor, + RPCHook rpcHook, ScheduledExecutorService scheduledExecutorService) { + this.nameserverAccessConfig = nameserverAccessConfig; + this.namePrefix = namePrefix; + this.clientNum = clientNum; + this.clientRemotingProcessor = clientRemotingProcessor; + this.rpcHook = rpcHook; + this.scheduledExecutorService = scheduledExecutorService; + + this.init(); + } + + protected void init() { + System.setProperty(ClientConfig.SEND_MESSAGE_WITH_VIP_CHANNEL_PROPERTY, "false"); + if (StringUtils.isEmpty(nameserverAccessConfig.getNamesrvDomain())) { + if (Strings.isNullOrEmpty(nameserverAccessConfig.getNamesrvAddr())) { + throw new RuntimeException("The configuration item NamesrvAddr is not configured"); + } + System.setProperty(MixAll.NAMESRV_ADDR_PROPERTY, nameserverAccessConfig.getNamesrvAddr()); + } else { + System.setProperty("rocketmq.namesrv.domain", nameserverAccessConfig.getNamesrvDomain()); + System.setProperty("rocketmq.namesrv.domain.subgroup", nameserverAccessConfig.getNamesrvDomainSubgroup()); + } + } + + public MQClientAPIExt getClient() { + if (clients.length == 1) { + return this.clients[0]; + } + int index = ThreadLocalRandom.current().nextInt(this.clients.length); + return this.clients[index]; + } + + @Override + public void start() throws Exception { + this.clients = new MQClientAPIExt[this.clientNum]; + + for (int i = 0; i < this.clientNum; i++) { + clients[i] = createAndStart(this.namePrefix + "N_" + i); + } + } + + @Override + public void shutdown() throws Exception { + for (int i = 0; i < this.clientNum; i++) { + clients[i].shutdown(); + } + } + + protected MQClientAPIExt createAndStart(String instanceName) { + ClientConfig clientConfig = new ClientConfig(); + clientConfig.setInstanceName(instanceName); + clientConfig.setDecodeReadBody(true); + clientConfig.setDecodeDecompressBody(false); + + NettyClientConfig nettyClientConfig = new NettyClientConfig(); + nettyClientConfig.setDisableCallbackExecutor(true); + + MQClientAPIExt mqClientAPIExt = new MQClientAPIExt(clientConfig, nettyClientConfig, + clientRemotingProcessor, + rpcHook); + + if (!mqClientAPIExt.updateNameServerAddressList()) { + mqClientAPIExt.fetchNameServerAddr(); + this.scheduledExecutorService.scheduleAtFixedRate( + mqClientAPIExt::fetchNameServerAddr, + Duration.ofSeconds(10).toMillis(), + Duration.ofMinutes(2).toMillis(), + TimeUnit.MILLISECONDS + ); + } + mqClientAPIExt.start(); + return mqClientAPIExt; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/producer/DefaultMQProducerImpl.java b/client/src/main/java/org/apache/rocketmq/client/impl/producer/DefaultMQProducerImpl.java index 7c169796741..26e6297a8c7 100644 --- a/client/src/main/java/org/apache/rocketmq/client/impl/producer/DefaultMQProducerImpl.java +++ b/client/src/main/java/org/apache/rocketmq/client/impl/producer/DefaultMQProducerImpl.java @@ -29,74 +29,103 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.Semaphore; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import org.apache.rocketmq.common.message.Message; -import org.apache.rocketmq.common.message.MessageClientIDSetter; -import org.apache.rocketmq.common.message.MessageExt; -import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.message.MessageConst; -import org.apache.rocketmq.common.message.MessageDecoder; -import org.apache.rocketmq.common.message.MessageBatch; -import org.apache.rocketmq.common.message.MessageAccessor; -import org.apache.rocketmq.common.message.MessageType; -import org.apache.rocketmq.common.message.MessageId; + +import com.google.common.base.Optional; import org.apache.rocketmq.client.QueryResult; import org.apache.rocketmq.client.Validators; import org.apache.rocketmq.client.common.ClientErrorCode; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.exception.RequestTimeoutException; import org.apache.rocketmq.client.hook.CheckForbiddenContext; import org.apache.rocketmq.client.hook.CheckForbiddenHook; +import org.apache.rocketmq.client.hook.EndTransactionContext; +import org.apache.rocketmq.client.hook.EndTransactionHook; import org.apache.rocketmq.client.hook.SendMessageContext; import org.apache.rocketmq.client.hook.SendMessageHook; import org.apache.rocketmq.client.impl.CommunicationMode; import org.apache.rocketmq.client.impl.MQClientManager; import org.apache.rocketmq.client.impl.factory.MQClientInstance; import org.apache.rocketmq.client.latency.MQFaultStrategy; -import org.apache.rocketmq.client.log.ClientLogger; +import org.apache.rocketmq.client.latency.Resolver; +import org.apache.rocketmq.client.latency.ServiceDetector; import org.apache.rocketmq.client.producer.DefaultMQProducer; -import org.apache.rocketmq.client.producer.LocalTransactionExecuter; import org.apache.rocketmq.client.producer.LocalTransactionState; import org.apache.rocketmq.client.producer.MessageQueueSelector; +import org.apache.rocketmq.client.producer.RequestCallback; +import org.apache.rocketmq.client.producer.RequestFutureHolder; +import org.apache.rocketmq.client.producer.RequestResponseFuture; import org.apache.rocketmq.client.producer.SendCallback; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.client.producer.SendStatus; import org.apache.rocketmq.client.producer.TransactionCheckListener; +import org.apache.rocketmq.client.producer.TransactionListener; import org.apache.rocketmq.client.producer.TransactionMQProducer; import org.apache.rocketmq.client.producer.TransactionSendResult; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.ServiceState; +import org.apache.rocketmq.common.ThreadFactoryImpl; import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.compression.CompressionType; +import org.apache.rocketmq.common.compression.Compressor; +import org.apache.rocketmq.common.compression.CompressorFactory; import org.apache.rocketmq.common.help.FAQUrl; -import org.apache.rocketmq.common.protocol.ResponseCode; -import org.apache.rocketmq.common.protocol.header.CheckTransactionStateRequestHeader; -import org.apache.rocketmq.common.protocol.header.EndTransactionRequestHeader; -import org.apache.rocketmq.common.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageBatch; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageId; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.message.MessageType; import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.common.utils.CorrelationIdUtil; import org.apache.rocketmq.remoting.RPCHook; -import org.apache.rocketmq.remoting.common.RemotingHelper; import org.apache.rocketmq.remoting.exception.RemotingConnectException; import org.apache.rocketmq.remoting.exception.RemotingException; import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; -import org.slf4j.Logger; +import org.apache.rocketmq.remoting.exception.RemotingTooMuchRequestException; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; +import org.apache.rocketmq.remoting.protocol.header.CheckTransactionStateRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.EndTransactionRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; public class DefaultMQProducerImpl implements MQProducerInner { - private final Logger log = ClientLogger.getLog(); + + private final Logger log = LoggerFactory.getLogger(DefaultMQProducerImpl.class); private final Random random = new Random(); private final DefaultMQProducer defaultMQProducer; private final ConcurrentMap topicPublishInfoTable = - new ConcurrentHashMap(); - private final ArrayList sendMessageHookList = new ArrayList(); + new ConcurrentHashMap<>(); + private final ArrayList sendMessageHookList = new ArrayList<>(); + private final ArrayList endTransactionHookList = new ArrayList<>(); private final RPCHook rpcHook; + private final BlockingQueue asyncSenderThreadPoolQueue; + private final ExecutorService defaultAsyncSenderExecutor; protected BlockingQueue checkRequestQueue; protected ExecutorService checkExecutor; private ServiceState serviceState = ServiceState.CREATE_JUST; private MQClientInstance mQClientFactory; - private ArrayList checkForbiddenHookList = new ArrayList(); - private int zipCompressLevel = Integer.parseInt(System.getProperty(MixAll.MESSAGE_COMPRESS_LEVEL, "5")); + private ArrayList checkForbiddenHookList = new ArrayList<>(); + private MQFaultStrategy mqFaultStrategy; + private ExecutorService asyncSenderExecutor; + + // compression related + private int compressLevel = Integer.parseInt(System.getProperty(MixAll.MESSAGE_COMPRESS_LEVEL, "5")); + private CompressionType compressType = CompressionType.of(System.getProperty(MixAll.MESSAGE_COMPRESS_TYPE, "ZLIB")); + private final Compressor compressor = CompressorFactory.getCompressor(compressType); - private MQFaultStrategy mqFaultStrategy = new MQFaultStrategy(); + // backpressure related + private Semaphore semaphoreAsyncSendNum; + private Semaphore semaphoreAsyncSendSize; public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer) { this(defaultMQProducer, null); @@ -105,28 +134,93 @@ public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer) { public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer, RPCHook rpcHook) { this.defaultMQProducer = defaultMQProducer; this.rpcHook = rpcHook; - } + this.asyncSenderThreadPoolQueue = new LinkedBlockingQueue<>(50000); + this.defaultAsyncSenderExecutor = new ThreadPoolExecutor( + Runtime.getRuntime().availableProcessors(), + Runtime.getRuntime().availableProcessors(), + 1000 * 60, + TimeUnit.MILLISECONDS, + this.asyncSenderThreadPoolQueue, + new ThreadFactoryImpl("AsyncSenderExecutor_")); + if (defaultMQProducer.getBackPressureForAsyncSendNum() > 10) { + semaphoreAsyncSendNum = new Semaphore(Math.max(defaultMQProducer.getBackPressureForAsyncSendNum(), 10), true); + } else { + semaphoreAsyncSendNum = new Semaphore(10, true); + log.info("semaphoreAsyncSendNum can not be smaller than 10."); + } + + if (defaultMQProducer.getBackPressureForAsyncSendSize() > 1024 * 1024) { + semaphoreAsyncSendSize = new Semaphore(Math.max(defaultMQProducer.getBackPressureForAsyncSendSize(), 1024 * 1024), true); + } else { + semaphoreAsyncSendSize = new Semaphore(1024 * 1024, true); + log.info("semaphoreAsyncSendSize can not be smaller than 1M."); + } + + ServiceDetector serviceDetector = new ServiceDetector() { + @Override + public boolean detect(String endpoint, long timeoutMillis) { + Optional candidateTopic = pickTopic(); + if (!candidateTopic.isPresent()) { + return false; + } + try { + MessageQueue mq = new MessageQueue(candidateTopic.get(), null, 0); + mQClientFactory.getMQClientAPIImpl() + .getMaxOffset(endpoint, mq, timeoutMillis); + return true; + } catch (Exception e) { + return false; + } + } + }; + + this.mqFaultStrategy = new MQFaultStrategy(defaultMQProducer.cloneClientConfig(), new Resolver() { + @Override + public String resolve(String name) { + return DefaultMQProducerImpl.this.mQClientFactory.findBrokerAddressInPublish(name); + } + }, serviceDetector); + } + private Optional pickTopic() { + if (topicPublishInfoTable.isEmpty()) { + return Optional.absent(); + } + return Optional.of(topicPublishInfoTable.keySet().iterator().next()); + } public void registerCheckForbiddenHook(CheckForbiddenHook checkForbiddenHook) { this.checkForbiddenHookList.add(checkForbiddenHook); log.info("register a new checkForbiddenHook. hookName={}, allHookSize={}", checkForbiddenHook.hookName(), checkForbiddenHookList.size()); } + public void setSemaphoreAsyncSendNum(int num) { + semaphoreAsyncSendNum = new Semaphore(num, true); + } + + public void setSemaphoreAsyncSendSize(int size) { + semaphoreAsyncSendSize = new Semaphore(size, true); + } + public void initTransactionEnv() { TransactionMQProducer producer = (TransactionMQProducer) this.defaultMQProducer; - this.checkRequestQueue = new LinkedBlockingQueue(producer.getCheckRequestHoldMax()); - this.checkExecutor = new ThreadPoolExecutor( - producer.getCheckThreadPoolMinSize(), - producer.getCheckThreadPoolMaxSize(), - 1000 * 60, - TimeUnit.MILLISECONDS, - this.checkRequestQueue); + if (producer.getExecutorService() != null) { + this.checkExecutor = producer.getExecutorService(); + } else { + this.checkRequestQueue = new LinkedBlockingQueue<>(producer.getCheckRequestHoldMax()); + this.checkExecutor = new ThreadPoolExecutor( + producer.getCheckThreadPoolMinSize(), + producer.getCheckThreadPoolMaxSize(), + 1000 * 60, + TimeUnit.MILLISECONDS, + this.checkRequestQueue); + } } public void destroyTransactionEnv() { - this.checkExecutor.shutdown(); - this.checkRequestQueue.clear(); + if (this.checkExecutor != null) { + this.checkExecutor.shutdown(); + } } public void registerSendMessageHook(final SendMessageHook hook) { @@ -134,6 +228,11 @@ public void registerSendMessageHook(final SendMessageHook hook) { log.info("register sendMessage Hook, {}", hook.hookName()); } + public void registerEndTransactionHook(final EndTransactionHook hook) { + this.endTransactionHookList.add(hook); + log.info("register endTransaction Hook, {}", hook.hookName()); + } + public void start() throws MQClientException { this.start(true); } @@ -149,7 +248,7 @@ public void start(final boolean startFactory) throws MQClientException { this.defaultMQProducer.changeInstanceNameToPID(); } - this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQProducer, rpcHook); + this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook); boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this); if (!registerOK) { @@ -159,12 +258,14 @@ public void start(final boolean startFactory) throws MQClientException { null); } - this.topicPublishInfoTable.put(this.defaultMQProducer.getCreateTopicKey(), new TopicPublishInfo()); - if (startFactory) { mQClientFactory.start(); } + this.initTopicRoute(); + + this.mqFaultStrategy.startDetector(); + log.info("the producer [{}] start OK. sendMessageWithVIPChannel={}", this.defaultMQProducer.getProducerGroup(), this.defaultMQProducer.isSendMessageWithVIPChannel()); this.serviceState = ServiceState.RUNNING; @@ -181,15 +282,14 @@ public void start(final boolean startFactory) throws MQClientException { } this.mQClientFactory.sendHeartbeatToAllBrokerWithLock(); + + RequestFutureHolder.getInstance().startScheduledTask(this); + } private void checkConfig() throws MQClientException { Validators.checkGroup(this.defaultMQProducer.getProducerGroup()); - if (null == this.defaultMQProducer.getProducerGroup()) { - throw new MQClientException("producerGroup is null", null); - } - if (this.defaultMQProducer.getProducerGroup().equals(MixAll.DEFAULT_PRODUCER_GROUP)) { throw new MQClientException("producerGroup can not equal " + MixAll.DEFAULT_PRODUCER_GROUP + ", please specify another one.", null); @@ -206,10 +306,12 @@ public void shutdown(final boolean shutdownFactory) { break; case RUNNING: this.mQClientFactory.unregisterProducer(this.defaultMQProducer.getProducerGroup()); + this.defaultAsyncSenderExecutor.shutdown(); if (shutdownFactory) { this.mQClientFactory.shutdown(); } - + this.mqFaultStrategy.shutdown(); + RequestFutureHolder.getInstance().shutdown(this); log.info("the producer [{}] shutdown OK", this.defaultMQProducer.getProducerGroup()); this.serviceState = ServiceState.SHUTDOWN_ALREADY; break; @@ -222,12 +324,7 @@ public void shutdown(final boolean shutdownFactory) { @Override public Set getPublishTopicList() { - Set topicList = new HashSet(); - for (String key : this.topicPublishInfoTable.keySet()) { - topicList.add(key); - } - - return topicList; + return new HashSet<>(this.topicPublishInfoTable.keySet()); } @Override @@ -237,7 +334,11 @@ public boolean isPublishTopicNeedUpdate(String topic) { return null == prev || !prev.ok(); } + /** + * @deprecated This method will be removed in the version 5.0.0 and {@link DefaultMQProducerImpl#getCheckListener} is recommended. + */ @Override + @Deprecated public TransactionCheckListener checkListener() { if (this.defaultMQProducer instanceof TransactionMQProducer) { TransactionMQProducer producer = (TransactionMQProducer) defaultMQProducer; @@ -247,6 +348,15 @@ public TransactionCheckListener checkListener() { return null; } + @Override + public TransactionListener getCheckListener() { + if (this.defaultMQProducer instanceof TransactionMQProducer) { + TransactionMQProducer producer = (TransactionMQProducer) defaultMQProducer; + return producer.getTransactionListener(); + } + return null; + } + @Override public void checkTransactionState(final String addr, final MessageExt msg, final CheckTransactionStateRequestHeader header) { @@ -259,11 +369,17 @@ public void checkTransactionState(final String addr, final MessageExt msg, @Override public void run() { TransactionCheckListener transactionCheckListener = DefaultMQProducerImpl.this.checkListener(); - if (transactionCheckListener != null) { + TransactionListener transactionListener = getCheckListener(); + if (transactionCheckListener != null || transactionListener != null) { LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW; Throwable exception = null; try { - localTransactionState = transactionCheckListener.checkLocalTransactionState(message); + if (transactionCheckListener != null) { + localTransactionState = transactionCheckListener.checkLocalTransactionState(message); + } else { + log.debug("TransactionCheckListener is null, used new check API, producerGroup={}", group); + localTransactionState = transactionListener.checkLocalTransaction(message); + } } catch (Throwable e) { log.error("Broker call checkTransactionState, but checkLocalTransactionState exception", e); exception = e; @@ -274,7 +390,7 @@ public void run() { group, exception); } else { - log.warn("checkTransactionState, pick transactionCheckListener by group[{}] failed", group); + log.warn("CheckTransactionState, pick transactionCheckListener by group[{}] failed", group); } } @@ -287,6 +403,7 @@ private void processTransactionState( thisHeader.setProducerGroup(producerGroup); thisHeader.setTranStateTableOffset(checkRequestHeader.getTranStateTableOffset()); thisHeader.setFromTransactionCheck(true); + thisHeader.setBrokerName(checkRequestHeader.getBrokerName()); String uniqueKey = message.getProperties().get(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX); if (uniqueKey == null) { @@ -312,8 +429,9 @@ private void processTransactionState( String remark = null; if (exception != null) { - remark = "checkLocalTransactionState Exception: " + RemotingHelper.exceptionSimpleDesc(exception); + remark = "checkLocalTransactionState Exception: " + UtilAll.exceptionSimpleDesc(exception); } + doExecuteEndTransactionHook(msg, uniqueKey, brokerAddr, localTransactionState, true); try { DefaultMQProducerImpl.this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway(brokerAddr, thisHeader, remark, @@ -332,7 +450,7 @@ public void updateTopicPublishInfo(final String topic, final TopicPublishInfo in if (info != null && topic != null) { TopicPublishInfo prev = this.topicPublishInfoTable.put(topic, info); if (prev != null) { - log.info("updateTopicPublishInfo prev is not null, " + prev.toString()); + log.info("updateTopicPublishInfo prev is not null, " + prev); } } } @@ -349,8 +467,9 @@ public void createTopic(String key, String newTopic, int queueNum) throws MQClie public void createTopic(String key, String newTopic, int queueNum, int topicSysFlag) throws MQClientException { this.makeSureStateOK(); Validators.checkTopic(newTopic); + Validators.isSystemTopic(newTopic); - this.mQClientFactory.getMQAdminImpl().createTopic(key, newTopic, queueNum, topicSysFlag); + this.mQClientFactory.getMQAdminImpl().createTopic(key, newTopic, queueNum, topicSysFlag, null); } private void makeSureStateOK() throws MQClientException { @@ -414,21 +533,165 @@ public void send(Message msg, send(msg, sendCallback, this.defaultMQProducer.getSendMsgTimeout()); } - public void send(Message msg, SendCallback sendCallback, long timeout) + /** + * @param msg + * @param sendCallback + * @param timeout the sendCallback will be invoked at most time + * @throws RejectedExecutionException + * @deprecated It will be removed at 4.4.0 cause for exception handling and the wrong Semantics of timeout. A new one will be + * provided in next version + */ + @Deprecated + public void send(final Message msg, final SendCallback sendCallback, final long timeout) throws MQClientException, RemotingException, InterruptedException { + BackpressureSendCallBack newCallBack = new BackpressureSendCallBack(sendCallback); + + final long beginStartTime = System.currentTimeMillis(); + Runnable runnable = new Runnable() { + @Override + public void run() { + long costTime = System.currentTimeMillis() - beginStartTime; + if (timeout > costTime) { + try { + sendDefaultImpl(msg, CommunicationMode.ASYNC, newCallBack, timeout - costTime); + } catch (Exception e) { + newCallBack.onException(e); + } + } else { + newCallBack.onException( + new RemotingTooMuchRequestException("DEFAULT ASYNC send call timeout")); + } + } + }; + executeAsyncMessageSend(runnable, msg, newCallBack, timeout, beginStartTime); + } + + class BackpressureSendCallBack implements SendCallback { + public boolean isSemaphoreAsyncSizeAquired = false; + public boolean isSemaphoreAsyncNumAquired = false; + public int msgLen; + private final SendCallback sendCallback; + + public BackpressureSendCallBack(final SendCallback sendCallback) { + this.sendCallback = sendCallback; + } + + @Override + public void onSuccess(SendResult sendResult) { + if (isSemaphoreAsyncSizeAquired) { + semaphoreAsyncSendSize.release(msgLen); + } + if (isSemaphoreAsyncNumAquired) { + semaphoreAsyncSendNum.release(); + } + sendCallback.onSuccess(sendResult); + } + + @Override + public void onException(Throwable e) { + if (isSemaphoreAsyncSizeAquired) { + semaphoreAsyncSendSize.release(msgLen); + } + if (isSemaphoreAsyncNumAquired) { + semaphoreAsyncSendNum.release(); + } + sendCallback.onException(e); + } + } + + public void executeAsyncMessageSend(Runnable runnable, final Message msg, final BackpressureSendCallBack sendCallback, + final long timeout, final long beginStartTime) + throws MQClientException, InterruptedException { + ExecutorService executor = this.getAsyncSenderExecutor(); + boolean isEnableBackpressureForAsyncMode = this.getDefaultMQProducer().isEnableBackpressureForAsyncMode(); + boolean isSemaphoreAsyncNumAquired = false; + boolean isSemaphoreAsyncSizeAquired = false; + int msgLen = msg.getBody() == null ? 1 : msg.getBody().length; + try { - this.sendDefaultImpl(msg, CommunicationMode.ASYNC, sendCallback, timeout); - } catch (MQBrokerException e) { - throw new MQClientException("unknownn exception", e); + if (isEnableBackpressureForAsyncMode) { + long costTime = System.currentTimeMillis() - beginStartTime; + isSemaphoreAsyncNumAquired = timeout - costTime > 0 + && semaphoreAsyncSendNum.tryAcquire(timeout - costTime, TimeUnit.MILLISECONDS); + if (!isSemaphoreAsyncNumAquired) { + sendCallback.onException( + new RemotingTooMuchRequestException("send message tryAcquire semaphoreAsyncNum timeout")); + return; + } + costTime = System.currentTimeMillis() - beginStartTime; + isSemaphoreAsyncSizeAquired = timeout - costTime > 0 + && semaphoreAsyncSendSize.tryAcquire(msgLen, timeout - costTime, TimeUnit.MILLISECONDS); + if (!isSemaphoreAsyncSizeAquired) { + sendCallback.onException( + new RemotingTooMuchRequestException("send message tryAcquire semaphoreAsyncSize timeout")); + return; + } + } + sendCallback.isSemaphoreAsyncSizeAquired = isSemaphoreAsyncSizeAquired; + sendCallback.isSemaphoreAsyncNumAquired = isSemaphoreAsyncNumAquired; + sendCallback.msgLen = msgLen; + executor.submit(runnable); + } catch (RejectedExecutionException e) { + if (isEnableBackpressureForAsyncMode) { + runnable.run(); + } else { + throw new MQClientException("executor rejected ", e); + } } } - public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) { - return this.mqFaultStrategy.selectOneMessageQueue(tpInfo, lastBrokerName); + public MessageQueue invokeMessageQueueSelector(Message msg, MessageQueueSelector selector, Object arg, + final long timeout) throws MQClientException, RemotingTooMuchRequestException { + long beginStartTime = System.currentTimeMillis(); + this.makeSureStateOK(); + Validators.checkMessage(msg, this.defaultMQProducer); + + TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic()); + if (topicPublishInfo != null && topicPublishInfo.ok()) { + MessageQueue mq = null; + try { + List messageQueueList = + mQClientFactory.getMQAdminImpl().parsePublishMessageQueues(topicPublishInfo.getMessageQueueList()); + Message userMessage = MessageAccessor.cloneMessage(msg); + String userTopic = NamespaceUtil.withoutNamespace(userMessage.getTopic(), mQClientFactory.getClientConfig().getNamespace()); + userMessage.setTopic(userTopic); + + mq = mQClientFactory.getClientConfig().queueWithNamespace(selector.select(messageQueueList, userMessage, arg)); + } catch (Throwable e) { + throw new MQClientException("select message queue threw exception.", e); + } + + long costTime = System.currentTimeMillis() - beginStartTime; + if (timeout < costTime) { + throw new RemotingTooMuchRequestException("sendSelectImpl call timeout"); + } + if (mq != null) { + return mq; + } else { + throw new MQClientException("select message queue return null.", null); + } + } + + validateNameServerSetting(); + throw new MQClientException("No route info for this topic, " + msg.getTopic(), null); } - public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) { - this.mqFaultStrategy.updateFaultItem(brokerName, currentLatency, isolation); + public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName, final boolean resetIndex) { + return this.mqFaultStrategy.selectOneMessageQueue(tpInfo, lastBrokerName, resetIndex); + } + + public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation, + boolean reachable) { + this.mqFaultStrategy.updateFaultItem(brokerName, currentLatency, isolation, reachable); + } + + private void validateNameServerSetting() throws MQClientException { + List nsList = this.getMqClientFactory().getMQClientAPIImpl().getNameServerAddressList(); + if (null == nsList || nsList.isEmpty()) { + throw new MQClientException( + "No name server address, please set it." + FAQUrl.suggestTodo(FAQUrl.NAME_SERVER_ADDR_NOT_EXIST_URL), null).setResponseCode(ClientErrorCode.NO_NAME_SERVER_EXCEPTION); + } + } private SendResult sendDefaultImpl( @@ -439,30 +702,44 @@ private SendResult sendDefaultImpl( ) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { this.makeSureStateOK(); Validators.checkMessage(msg, this.defaultMQProducer); - final long invokeID = random.nextLong(); long beginTimestampFirst = System.currentTimeMillis(); long beginTimestampPrev = beginTimestampFirst; long endTimestamp = beginTimestampFirst; TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic()); if (topicPublishInfo != null && topicPublishInfo.ok()) { + boolean callTimeout = false; MessageQueue mq = null; Exception exception = null; SendResult sendResult = null; int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1; int times = 0; String[] brokersSent = new String[timesTotal]; + boolean resetIndex = false; for (; times < timesTotal; times++) { String lastBrokerName = null == mq ? null : mq.getBrokerName(); - MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName); + if (times > 0) { + resetIndex = true; + } + MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName, resetIndex); if (mqSelected != null) { mq = mqSelected; brokersSent[times] = mq.getBrokerName(); try { beginTimestampPrev = System.currentTimeMillis(); - sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout); + if (times > 0) { + //Reset topic with namespace during resend. + msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic())); + } + long costTime = beginTimestampPrev - beginTimestampFirst; + if (timeout < costTime) { + callTimeout = true; + break; + } + + sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime); endTimestamp = System.currentTimeMillis(); - this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false); + this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false, true); switch (communicationMode) { case ASYNC: return null; @@ -479,49 +756,52 @@ private SendResult sendDefaultImpl( default: break; } - } catch (RemotingException e) { + } catch (MQClientException e) { endTimestamp = System.currentTimeMillis(); - this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true); - log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e); + this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false, true); + log.warn("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq, e); log.warn(msg.toString()); exception = e; continue; - } catch (MQClientException e) { + } catch (RemotingException e) { endTimestamp = System.currentTimeMillis(); - this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true); - log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e); - log.warn(msg.toString()); + if (this.mqFaultStrategy.isStartDetectorEnable()) { + // Set this broker unreachable when detecting schedule task is running for RemotingException. + this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true, false); + } else { + // Otherwise, isolate this broker. + this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true, true); + } + log.warn("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq, e); + if (log.isDebugEnabled()) { + log.debug(msg.toString()); + } exception = e; continue; } catch (MQBrokerException e) { endTimestamp = System.currentTimeMillis(); - this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true); - log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e); - log.warn(msg.toString()); + this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true, false); + log.warn("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq, e); + if (log.isDebugEnabled()) { + log.debug(msg.toString()); + } exception = e; - switch (e.getResponseCode()) { - case ResponseCode.TOPIC_NOT_EXIST: - case ResponseCode.SERVICE_NOT_AVAILABLE: - case ResponseCode.SYSTEM_ERROR: - case ResponseCode.NO_PERMISSION: - case ResponseCode.NO_BUYER_ID: - case ResponseCode.NOT_IN_CURRENT_UNIT: - continue; - default: - if (sendResult != null) { - return sendResult; - } + if (this.defaultMQProducer.getRetryResponseCodes().contains(e.getResponseCode())) { + continue; + } else { + if (sendResult != null) { + return sendResult; + } - throw e; + throw e; } } catch (InterruptedException e) { endTimestamp = System.currentTimeMillis(); - this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false); - log.warn(String.format("sendKernelImpl exception, throw exception, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e); - log.warn(msg.toString()); - - log.warn("sendKernelImpl exception", e); - log.warn(msg.toString()); + this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false, true); + log.warn("sendKernelImpl exception, throw exception, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq, e); + if (log.isDebugEnabled()) { + log.debug(msg.toString()); + } throw e; } } else { @@ -532,7 +812,6 @@ private SendResult sendDefaultImpl( if (sendResult != null) { return sendResult; } - String info = String.format("Send [%d] times, still failed, cost [%d]ms, Topic: %s, BrokersSent: %s", times, System.currentTimeMillis() - beginTimestampFirst, @@ -542,6 +821,10 @@ private SendResult sendDefaultImpl( info += FAQUrl.suggestTodo(FAQUrl.SEND_MSG_FAILED); MQClientException mqClientException = new MQClientException(info, exception); + if (callTimeout) { + throw new RemotingTooMuchRequestException("sendDefaultImpl call timeout"); + } + if (exception instanceof MQBrokerException) { mqClientException.setResponseCode(((MQBrokerException) exception).getResponseCode()); } else if (exception instanceof RemotingConnectException) { @@ -555,13 +838,9 @@ private SendResult sendDefaultImpl( throw mqClientException; } - List nsList = this.getmQClientFactory().getMQClientAPIImpl().getNameServerAddressList(); - if (null == nsList || nsList.isEmpty()) { - throw new MQClientException( - "No name server address, please set it." + FAQUrl.suggestTodo(FAQUrl.NAME_SERVER_ADDR_NOT_EXIST_URL), null).setResponseCode(ClientErrorCode.NO_NAME_SERVER_EXCEPTION); - } + validateNameServerSetting(); - throw new MQClientException("No route info of this topic, " + msg.getTopic() + FAQUrl.suggestTodo(FAQUrl.NO_TOPIC_ROUTE_INFO), + throw new MQClientException("No route info of this topic: " + msg.getTopic() + FAQUrl.suggestTodo(FAQUrl.NO_TOPIC_ROUTE_INFO), null).setResponseCode(ClientErrorCode.NOT_FOUND_TOPIC_EXCEPTION); } @@ -588,10 +867,13 @@ private SendResult sendKernelImpl(final Message msg, final SendCallback sendCallback, final TopicPublishInfo topicPublishInfo, final long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { - String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName()); + long beginStartTime = System.currentTimeMillis(); + String brokerName = this.mQClientFactory.getBrokerNameFromMessageQueue(mq); + String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(brokerName); if (null == brokerAddr) { tryToFindTopicPublishInfo(mq.getTopic()); - brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName()); + brokerName = this.mQClientFactory.getBrokerNameFromMessageQueue(mq); + brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(brokerName); } SendMessageContext context = null; @@ -605,13 +887,22 @@ private SendResult sendKernelImpl(final Message msg, MessageClientIDSetter.setUniqID(msg); } + boolean topicWithNamespace = false; + if (null != this.mQClientFactory.getClientConfig().getNamespace()) { + msg.setInstanceId(this.mQClientFactory.getClientConfig().getNamespace()); + topicWithNamespace = true; + } + int sysFlag = 0; + boolean msgBodyCompressed = false; if (this.tryToCompressMessage(msg)) { sysFlag |= MessageSysFlag.COMPRESSED_FLAG; + sysFlag |= compressType.getCompressionFlag(); + msgBodyCompressed = true; } final String tranMsg = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED); - if (tranMsg != null && Boolean.parseBoolean(tranMsg)) { + if (Boolean.parseBoolean(tranMsg)) { sysFlag |= MessageSysFlag.TRANSACTION_PREPARED_TYPE; } @@ -636,6 +927,7 @@ private SendResult sendKernelImpl(final Message msg, context.setBrokerAddr(brokerAddr); context.setMessage(msg); context.setMq(mq); + context.setNamespace(this.defaultMQProducer.getNamespace()); String isTrans = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED); if (isTrans != null && isTrans.equals("true")) { context.setMsgType(MessageType.Trans_Msg_Half); @@ -660,6 +952,7 @@ private SendResult sendKernelImpl(final Message msg, requestHeader.setReconsumeTimes(0); requestHeader.setUnitMode(this.isUnitMode()); requestHeader.setBatch(msg instanceof MessageBatch); + requestHeader.setBrokerName(brokerName); if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { String reconsumeTimes = MessageAccessor.getReconsumeTime(msg); if (reconsumeTimes != null) { @@ -677,12 +970,35 @@ private SendResult sendKernelImpl(final Message msg, SendResult sendResult = null; switch (communicationMode) { case ASYNC: + Message tmpMessage = msg; + boolean messageCloned = false; + if (msgBodyCompressed) { + //If msg body was compressed, msgbody should be reset using prevBody. + //Clone new message using commpressed message body and recover origin massage. + //Fix bug:https://github.com/apache/rocketmq-externals/issues/66 + tmpMessage = MessageAccessor.cloneMessage(msg); + messageCloned = true; + msg.setBody(prevBody); + } + + if (topicWithNamespace) { + if (!messageCloned) { + tmpMessage = MessageAccessor.cloneMessage(msg); + messageCloned = true; + } + msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(), this.defaultMQProducer.getNamespace())); + } + + long costTimeAsync = System.currentTimeMillis() - beginStartTime; + if (timeout < costTimeAsync) { + throw new RemotingTooMuchRequestException("sendKernelImpl call timeout"); + } sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage( brokerAddr, - mq.getBrokerName(), - msg, + brokerName, + tmpMessage, requestHeader, - timeout, + timeout - costTimeAsync, communicationMode, sendCallback, topicPublishInfo, @@ -693,12 +1009,16 @@ private SendResult sendKernelImpl(final Message msg, break; case ONEWAY: case SYNC: + long costTimeSync = System.currentTimeMillis() - beginStartTime; + if (timeout < costTimeSync) { + throw new RemotingTooMuchRequestException("sendKernelImpl call timeout"); + } sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage( brokerAddr, - mq.getBrokerName(), + brokerName, msg, requestHeader, - timeout, + timeout - costTimeSync, communicationMode, context, this); @@ -714,19 +1034,7 @@ private SendResult sendKernelImpl(final Message msg, } return sendResult; - } catch (RemotingException e) { - if (this.hasSendMessageHook()) { - context.setException(e); - this.executeSendMessageHookAfter(context); - } - throw e; - } catch (MQBrokerException e) { - if (this.hasSendMessageHook()) { - context.setException(e); - this.executeSendMessageHookAfter(context); - } - throw e; - } catch (InterruptedException e) { + } catch (RemotingException | InterruptedException | MQBrokerException e) { if (this.hasSendMessageHook()) { context.setException(e); this.executeSendMessageHookAfter(context); @@ -734,33 +1042,41 @@ private SendResult sendKernelImpl(final Message msg, throw e; } finally { msg.setBody(prevBody); + msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(), this.defaultMQProducer.getNamespace())); } } - throw new MQClientException("The broker[" + mq.getBrokerName() + "] not exist", null); + throw new MQClientException("The broker[" + brokerName + "] not exist", null); + } + + public MQClientInstance getMqClientFactory() { + return mQClientFactory; } + @Deprecated public MQClientInstance getmQClientFactory() { return mQClientFactory; } private boolean tryToCompressMessage(final Message msg) { if (msg instanceof MessageBatch) { - //batch dose not support compressing right now + //batch does not support compressing right now return false; } byte[] body = msg.getBody(); if (body != null) { if (body.length >= this.defaultMQProducer.getCompressMsgBodyOverHowmuch()) { try { - byte[] data = UtilAll.compress(body, zipCompressLevel); + byte[] data = compressor.compress(body, compressLevel); if (data != null) { msg.setBody(data); return true; } } catch (IOException e) { log.error("tryToCompressMessage exception", e); - log.warn(msg.toString()); + if (log.isDebugEnabled()) { + log.debug(msg.toString()); + } } } } @@ -808,6 +1124,37 @@ public void executeSendMessageHookAfter(final SendMessageContext context) { } } + public boolean hasEndTransactionHook() { + return !this.endTransactionHookList.isEmpty(); + } + + public void executeEndTransactionHook(final EndTransactionContext context) { + if (!this.endTransactionHookList.isEmpty()) { + for (EndTransactionHook hook : this.endTransactionHookList) { + try { + hook.endTransaction(context); + } catch (Throwable e) { + log.warn("failed to executeEndTransactionHook", e); + } + } + } + } + + public void doExecuteEndTransactionHook(Message msg, String msgId, String brokerAddr, LocalTransactionState state, + boolean fromTransactionCheck) { + if (hasEndTransactionHook()) { + EndTransactionContext context = new EndTransactionContext(); + context.setProducerGroup(defaultMQProducer.getProducerGroup()); + context.setBrokerAddr(brokerAddr); + context.setMessage(msg); + context.setMsgId(msgId); + context.setTransactionId(msg.getTransactionId()); + context.setTransactionState(state); + context.setFromTransactionCheck(fromTransactionCheck); + executeEndTransactionHook(context); + } + } + /** * DEFAULT ONEWAY ------------------------------------------------------- */ @@ -829,6 +1176,7 @@ public SendResult send(Message msg, MessageQueue mq) public SendResult send(Message msg, MessageQueue mq, long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + long beginStartTime = System.currentTimeMillis(); this.makeSureStateOK(); Validators.checkMessage(msg, this.defaultMQProducer); @@ -836,6 +1184,11 @@ public SendResult send(Message msg, MessageQueue mq, long timeout) throw new MQClientException("message's topic not equal mq's topic", null); } + long costTime = System.currentTimeMillis() - beginStartTime; + if (timeout < costTime) { + throw new RemotingTooMuchRequestException("call timeout"); + } + return this.sendKernelImpl(msg, mq, CommunicationMode.SYNC, null, null, timeout); } @@ -847,20 +1200,51 @@ public void send(Message msg, MessageQueue mq, SendCallback sendCallback) send(msg, mq, sendCallback, this.defaultMQProducer.getSendMsgTimeout()); } - public void send(Message msg, MessageQueue mq, SendCallback sendCallback, long timeout) + /** + * @param msg + * @param mq + * @param sendCallback + * @param timeout the sendCallback will be invoked at most time + * @throws MQClientException + * @throws RemotingException + * @throws InterruptedException + * @deprecated It will be removed at 4.4.0 cause for exception handling and the wrong Semantics of timeout. A new one will be + * provided in next version + */ + @Deprecated + public void send(final Message msg, final MessageQueue mq, final SendCallback sendCallback, final long timeout) throws MQClientException, RemotingException, InterruptedException { - this.makeSureStateOK(); - Validators.checkMessage(msg, this.defaultMQProducer); + BackpressureSendCallBack newCallBack = new BackpressureSendCallBack(sendCallback); + final long beginStartTime = System.currentTimeMillis(); + Runnable runnable = new Runnable() { + @Override + public void run() { + try { + makeSureStateOK(); + Validators.checkMessage(msg, defaultMQProducer); - if (!msg.getTopic().equals(mq.getTopic())) { - throw new MQClientException("message's topic not equal mq's topic", null); - } + if (!msg.getTopic().equals(mq.getTopic())) { + throw new MQClientException("Topic of the message does not match its target message queue", null); + } + long costTime = System.currentTimeMillis() - beginStartTime; + if (timeout > costTime) { + try { + sendKernelImpl(msg, mq, CommunicationMode.ASYNC, newCallBack, null, + timeout - costTime); + } catch (MQBrokerException e) { + throw new MQClientException("unknown exception", e); + } + } else { + newCallBack.onException(new RemotingTooMuchRequestException("call timeout")); + } + } catch (Exception e) { + newCallBack.onException(e); + } + } - try { - this.sendKernelImpl(msg, mq, CommunicationMode.ASYNC, sendCallback, null, timeout); - } catch (MQBrokerException e) { - throw new MQClientException("unknown exception", e); - } + }; + + executeAsyncMessageSend(runnable, msg, newCallBack, timeout, beginStartTime); } /** @@ -898,6 +1282,7 @@ private SendResult sendSelectImpl( final CommunicationMode communicationMode, final SendCallback sendCallback, final long timeout ) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + long beginStartTime = System.currentTimeMillis(); this.makeSureStateOK(); Validators.checkMessage(msg, this.defaultMQProducer); @@ -905,18 +1290,29 @@ private SendResult sendSelectImpl( if (topicPublishInfo != null && topicPublishInfo.ok()) { MessageQueue mq = null; try { - mq = selector.select(topicPublishInfo.getMessageQueueList(), msg, arg); + List messageQueueList = + mQClientFactory.getMQAdminImpl().parsePublishMessageQueues(topicPublishInfo.getMessageQueueList()); + Message userMessage = MessageAccessor.cloneMessage(msg); + String userTopic = NamespaceUtil.withoutNamespace(userMessage.getTopic(), mQClientFactory.getClientConfig().getNamespace()); + userMessage.setTopic(userTopic); + + mq = mQClientFactory.getClientConfig().queueWithNamespace(selector.select(messageQueueList, userMessage, arg)); } catch (Throwable e) { - throw new MQClientException("select message queue throwed exception.", e); + throw new MQClientException("select message queue threw exception.", e); } + long costTime = System.currentTimeMillis() - beginStartTime; + if (timeout < costTime) { + throw new RemotingTooMuchRequestException("sendSelectImpl call timeout"); + } if (mq != null) { - return this.sendKernelImpl(msg, mq, communicationMode, sendCallback, null, timeout); + return this.sendKernelImpl(msg, mq, communicationMode, sendCallback, null, timeout - costTime); } else { throw new MQClientException("select message queue return null.", null); } } + validateNameServerSetting(); throw new MQClientException("No route info for this topic, " + msg.getTopic(), null); } @@ -928,13 +1324,47 @@ public void send(Message msg, MessageQueueSelector selector, Object arg, SendCal send(msg, selector, arg, sendCallback, this.defaultMQProducer.getSendMsgTimeout()); } - public void send(Message msg, MessageQueueSelector selector, Object arg, SendCallback sendCallback, long timeout) + /** + * It will be removed at 4.4.0 cause for exception handling and the wrong Semantics of timeout. A new one will be + * provided in next version + * + * @param msg + * @param selector + * @param arg + * @param sendCallback + * @param timeout the sendCallback will be invoked at most time + * @throws MQClientException + * @throws RemotingException + * @throws InterruptedException + */ + @Deprecated + public void send(final Message msg, final MessageQueueSelector selector, final Object arg, + final SendCallback sendCallback, final long timeout) throws MQClientException, RemotingException, InterruptedException { - try { - this.sendSelectImpl(msg, selector, arg, CommunicationMode.ASYNC, sendCallback, timeout); - } catch (MQBrokerException e) { - throw new MQClientException("unknownn exception", e); - } + BackpressureSendCallBack newCallBack = new BackpressureSendCallBack(sendCallback); + final long beginStartTime = System.currentTimeMillis(); + Runnable runnable = new Runnable() { + @Override + public void run() { + long costTime = System.currentTimeMillis() - beginStartTime; + if (timeout > costTime) { + try { + try { + sendSelectImpl(msg, selector, arg, CommunicationMode.ASYNC, newCallBack, + timeout - costTime); + } catch (MQBrokerException e) { + throw new MQClientException("unknown exception", e); + } + } catch (Exception e) { + newCallBack.onException(e); + } + } else { + newCallBack.onException(new RemotingTooMuchRequestException("call timeout")); + } + } + + }; + executeAsyncMessageSend(runnable, msg, newCallBack, timeout, beginStartTime); } /** @@ -950,11 +1380,18 @@ public void sendOneway(Message msg, MessageQueueSelector selector, Object arg) } public TransactionSendResult sendMessageInTransaction(final Message msg, - final LocalTransactionExecuter tranExecuter, final Object arg) + final TransactionListener localTransactionListener, final Object arg) throws MQClientException { - if (null == tranExecuter) { + TransactionListener transactionListener = getCheckListener(); + if (null == localTransactionListener && null == transactionListener) { throw new MQClientException("tranExecutor is null", null); } + + // ignore DelayTimeLevel parameter + if (msg.getDelayTimeLevel() != 0) { + MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_DELAY_TIME_LEVEL); + } + Validators.checkMessage(msg, this.defaultMQProducer); SendResult sendResult = null; @@ -974,18 +1411,27 @@ public TransactionSendResult sendMessageInTransaction(final Message msg, if (sendResult.getTransactionId() != null) { msg.putUserProperty("__transactionId__", sendResult.getTransactionId()); } - localTransactionState = tranExecuter.executeLocalTransactionBranch(msg, arg); + String transactionId = msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX); + if (null != transactionId && !"".equals(transactionId)) { + msg.setTransactionId(transactionId); + } + if (null != localTransactionListener) { + localTransactionState = localTransactionListener.executeLocalTransaction(msg, arg); + } else { + log.debug("Used new transaction API"); + localTransactionState = transactionListener.executeLocalTransaction(msg, arg); + } if (null == localTransactionState) { localTransactionState = LocalTransactionState.UNKNOW; } if (localTransactionState != LocalTransactionState.COMMIT_MESSAGE) { - log.info("executeLocalTransactionBranch return {}", localTransactionState); - log.info(msg.toString()); + log.info("executeLocalTransactionBranch return: {} messageTopic: {} transactionId: {} tag: {} key: {}", + localTransactionState, msg.getTopic(), msg.getTransactionId(), msg.getTags(), msg.getKeys()); } } catch (Throwable e) { - log.info("executeLocalTransactionBranch exception", e); - log.info(msg.toString()); + log.error("executeLocalTransactionBranch exception, messageTopic: {} transactionId: {} tag: {} key: {}", + msg.getTopic(), msg.getTransactionId(), msg.getTags(), msg.getKeys(), e); localException = e; } } @@ -1000,7 +1446,7 @@ public TransactionSendResult sendMessageInTransaction(final Message msg, } try { - this.endTransaction(sendResult, localTransactionState, localException); + this.endTransaction(msg, sendResult, localTransactionState, localException); } catch (Exception e) { log.warn("local transaction execute " + localTransactionState + ", but end broker transaction failed", e); } @@ -1024,6 +1470,7 @@ public SendResult send( } public void endTransaction( + final Message msg, final SendResult sendResult, final LocalTransactionState localTransactionState, final Throwable localException) throws RemotingException, MQBrokerException, InterruptedException, UnknownHostException { @@ -1034,10 +1481,12 @@ public void endTransaction( id = MessageDecoder.decodeMessageId(sendResult.getMsgId()); } String transactionId = sendResult.getTransactionId(); - final String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(sendResult.getMessageQueue().getBrokerName()); + final String destBrokerName = this.mQClientFactory.getBrokerNameFromMessageQueue(defaultMQProducer.queueWithNamespace(sendResult.getMessageQueue())); + final String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(destBrokerName); EndTransactionRequestHeader requestHeader = new EndTransactionRequestHeader(); requestHeader.setTransactionId(transactionId); requestHeader.setCommitLogOffset(id.getOffset()); + requestHeader.setBrokerName(destBrokerName); switch (localTransactionState) { case COMMIT_MESSAGE: requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_COMMIT_TYPE); @@ -1052,6 +1501,7 @@ public void endTransaction( break; } + doExecuteEndTransactionHook(msg, sendResult.getMsgId(), brokerAddr, localTransactionState, false); requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup()); requestHeader.setTranStateTableOffset(sendResult.getQueueOffset()); requestHeader.setMsgId(sendResult.getMsgId()); @@ -1064,21 +1514,265 @@ public void setCallbackExecutor(final ExecutorService callbackExecutor) { this.mQClientFactory.getMQClientAPIImpl().getRemotingClient().setCallbackExecutor(callbackExecutor); } + public ExecutorService getAsyncSenderExecutor() { + return null == asyncSenderExecutor ? defaultAsyncSenderExecutor : asyncSenderExecutor; + } + + public void setAsyncSenderExecutor(ExecutorService asyncSenderExecutor) { + this.asyncSenderExecutor = asyncSenderExecutor; + } + public SendResult send(Message msg, long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { return this.sendDefaultImpl(msg, CommunicationMode.SYNC, null, timeout); } + public Message request(final Message msg, + long timeout) throws RequestTimeoutException, MQClientException, RemotingException, MQBrokerException, InterruptedException { + long beginTimestamp = System.currentTimeMillis(); + prepareSendRequest(msg, timeout); + final String correlationId = msg.getProperty(MessageConst.PROPERTY_CORRELATION_ID); + + try { + final RequestResponseFuture requestResponseFuture = new RequestResponseFuture(correlationId, timeout, null); + RequestFutureHolder.getInstance().getRequestFutureTable().put(correlationId, requestResponseFuture); + + long cost = System.currentTimeMillis() - beginTimestamp; + this.sendDefaultImpl(msg, CommunicationMode.ASYNC, new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + requestResponseFuture.setSendRequestOk(true); + } + + @Override + public void onException(Throwable e) { + requestResponseFuture.setSendRequestOk(false); + requestResponseFuture.putResponseMessage(null); + requestResponseFuture.setCause(e); + } + }, timeout - cost); + + return waitResponse(msg, timeout, requestResponseFuture, cost); + } finally { + RequestFutureHolder.getInstance().getRequestFutureTable().remove(correlationId); + } + } + + public void request(Message msg, final RequestCallback requestCallback, long timeout) + throws RemotingException, InterruptedException, MQClientException, MQBrokerException { + long beginTimestamp = System.currentTimeMillis(); + prepareSendRequest(msg, timeout); + final String correlationId = msg.getProperty(MessageConst.PROPERTY_CORRELATION_ID); + + final RequestResponseFuture requestResponseFuture = new RequestResponseFuture(correlationId, timeout, requestCallback); + RequestFutureHolder.getInstance().getRequestFutureTable().put(correlationId, requestResponseFuture); + + long cost = System.currentTimeMillis() - beginTimestamp; + this.sendDefaultImpl(msg, CommunicationMode.ASYNC, new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + requestResponseFuture.setSendRequestOk(true); + requestResponseFuture.executeRequestCallback(); + } + + @Override + public void onException(Throwable e) { + requestResponseFuture.setCause(e); + requestFail(correlationId); + } + }, timeout - cost); + } + + public Message request(final Message msg, final MessageQueueSelector selector, final Object arg, + final long timeout) throws MQClientException, RemotingException, MQBrokerException, + InterruptedException, RequestTimeoutException { + long beginTimestamp = System.currentTimeMillis(); + prepareSendRequest(msg, timeout); + final String correlationId = msg.getProperty(MessageConst.PROPERTY_CORRELATION_ID); + + try { + final RequestResponseFuture requestResponseFuture = new RequestResponseFuture(correlationId, timeout, null); + RequestFutureHolder.getInstance().getRequestFutureTable().put(correlationId, requestResponseFuture); + + long cost = System.currentTimeMillis() - beginTimestamp; + this.sendSelectImpl(msg, selector, arg, CommunicationMode.ASYNC, new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + requestResponseFuture.setSendRequestOk(true); + } + + @Override + public void onException(Throwable e) { + requestResponseFuture.setSendRequestOk(false); + requestResponseFuture.putResponseMessage(null); + requestResponseFuture.setCause(e); + } + }, timeout - cost); + + return waitResponse(msg, timeout, requestResponseFuture, cost); + } finally { + RequestFutureHolder.getInstance().getRequestFutureTable().remove(correlationId); + } + } + + public void request(final Message msg, final MessageQueueSelector selector, final Object arg, + final RequestCallback requestCallback, final long timeout) + throws RemotingException, InterruptedException, MQClientException, MQBrokerException { + long beginTimestamp = System.currentTimeMillis(); + prepareSendRequest(msg, timeout); + final String correlationId = msg.getProperty(MessageConst.PROPERTY_CORRELATION_ID); + + final RequestResponseFuture requestResponseFuture = new RequestResponseFuture(correlationId, timeout, requestCallback); + RequestFutureHolder.getInstance().getRequestFutureTable().put(correlationId, requestResponseFuture); + + long cost = System.currentTimeMillis() - beginTimestamp; + this.sendSelectImpl(msg, selector, arg, CommunicationMode.ASYNC, new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + requestResponseFuture.setSendRequestOk(true); + } + + @Override + public void onException(Throwable e) { + requestResponseFuture.setCause(e); + requestFail(correlationId); + } + }, timeout - cost); + + } + + public Message request(final Message msg, final MessageQueue mq, final long timeout) + throws MQClientException, RemotingException, MQBrokerException, InterruptedException, RequestTimeoutException { + long beginTimestamp = System.currentTimeMillis(); + prepareSendRequest(msg, timeout); + final String correlationId = msg.getProperty(MessageConst.PROPERTY_CORRELATION_ID); + + try { + final RequestResponseFuture requestResponseFuture = new RequestResponseFuture(correlationId, timeout, null); + RequestFutureHolder.getInstance().getRequestFutureTable().put(correlationId, requestResponseFuture); + + long cost = System.currentTimeMillis() - beginTimestamp; + this.sendKernelImpl(msg, mq, CommunicationMode.ASYNC, new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + requestResponseFuture.setSendRequestOk(true); + } + + @Override + public void onException(Throwable e) { + requestResponseFuture.setSendRequestOk(false); + requestResponseFuture.putResponseMessage(null); + requestResponseFuture.setCause(e); + } + }, null, timeout - cost); + + return waitResponse(msg, timeout, requestResponseFuture, cost); + } finally { + RequestFutureHolder.getInstance().getRequestFutureTable().remove(correlationId); + } + } + + private Message waitResponse(Message msg, long timeout, RequestResponseFuture requestResponseFuture, + long cost) throws InterruptedException, RequestTimeoutException, MQClientException { + Message responseMessage = requestResponseFuture.waitResponseMessage(timeout - cost); + if (responseMessage == null) { + if (requestResponseFuture.isSendRequestOk()) { + throw new RequestTimeoutException(ClientErrorCode.REQUEST_TIMEOUT_EXCEPTION, + "send request message to <" + msg.getTopic() + "> OK, but wait reply message timeout, " + timeout + " ms."); + } else { + throw new MQClientException("send request message to <" + msg.getTopic() + "> fail", requestResponseFuture.getCause()); + } + } + return responseMessage; + } + + public void request(final Message msg, final MessageQueue mq, final RequestCallback requestCallback, long timeout) + throws RemotingException, InterruptedException, MQClientException, MQBrokerException { + long beginTimestamp = System.currentTimeMillis(); + prepareSendRequest(msg, timeout); + final String correlationId = msg.getProperty(MessageConst.PROPERTY_CORRELATION_ID); + + final RequestResponseFuture requestResponseFuture = new RequestResponseFuture(correlationId, timeout, requestCallback); + RequestFutureHolder.getInstance().getRequestFutureTable().put(correlationId, requestResponseFuture); + + long cost = System.currentTimeMillis() - beginTimestamp; + this.sendKernelImpl(msg, mq, CommunicationMode.ASYNC, new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + requestResponseFuture.setSendRequestOk(true); + } + + @Override + public void onException(Throwable e) { + requestResponseFuture.setCause(e); + requestFail(correlationId); + } + }, null, timeout - cost); + } + + private void requestFail(final String correlationId) { + RequestResponseFuture responseFuture = RequestFutureHolder.getInstance().getRequestFutureTable().remove(correlationId); + if (responseFuture != null) { + responseFuture.setSendRequestOk(false); + responseFuture.putResponseMessage(null); + try { + responseFuture.executeRequestCallback(); + } catch (Exception e) { + log.warn("execute requestCallback in requestFail, and callback throw", e); + } + } + } + + private void prepareSendRequest(final Message msg, long timeout) { + String correlationId = CorrelationIdUtil.createCorrelationId(); + String requestClientId = this.getMqClientFactory().getClientId(); + MessageAccessor.putProperty(msg, MessageConst.PROPERTY_CORRELATION_ID, correlationId); + MessageAccessor.putProperty(msg, MessageConst.PROPERTY_MESSAGE_REPLY_TO_CLIENT, requestClientId); + MessageAccessor.putProperty(msg, MessageConst.PROPERTY_MESSAGE_TTL, String.valueOf(timeout)); + + boolean hasRouteData = this.getMqClientFactory().getTopicRouteTable().containsKey(msg.getTopic()); + if (!hasRouteData) { + long beginTimestamp = System.currentTimeMillis(); + this.tryToFindTopicPublishInfo(msg.getTopic()); + this.getMqClientFactory().sendHeartbeatToAllBrokerWithLock(); + long cost = System.currentTimeMillis() - beginTimestamp; + if (cost > 500) { + log.warn("prepare send request for <{}> cost {} ms", msg.getTopic(), cost); + } + } + } + + private void initTopicRoute() { + List topics = this.defaultMQProducer.getTopics(); + if (topics != null && topics.size() > 0) { + topics.forEach(topic -> { + String newTopic = NamespaceUtil.wrapNamespace(this.defaultMQProducer.getNamespace(), topic); + TopicPublishInfo topicPublishInfo = tryToFindTopicPublishInfo(newTopic); + if (topicPublishInfo == null || !topicPublishInfo.ok()) { + log.warn("No route info of this topic: " + newTopic + FAQUrl.suggestTodo(FAQUrl.NO_TOPIC_ROUTE_INFO)); + } + }); + } + } + public ConcurrentMap getTopicPublishInfoTable() { return topicPublishInfoTable; } - public int getZipCompressLevel() { - return zipCompressLevel; + public int getCompressLevel() { + return compressLevel; + } + + public void setCompressLevel(int compressLevel) { + this.compressLevel = compressLevel; + } + + public CompressionType getCompressType() { + return compressType; } - public void setZipCompressLevel(int zipCompressLevel) { - this.zipCompressLevel = zipCompressLevel; + public void setCompressType(CompressionType compressType) { + this.compressType = compressType; } public ServiceState getServiceState() { @@ -1112,4 +1806,12 @@ public boolean isSendLatencyFaultEnable() { public void setSendLatencyFaultEnable(final boolean sendLatencyFaultEnable) { this.mqFaultStrategy.setSendLatencyFaultEnable(sendLatencyFaultEnable); } + + public DefaultMQProducer getDefaultMQProducer() { + return defaultMQProducer; + } + + public MQFaultStrategy getMqFaultStrategy() { + return mqFaultStrategy; + } } diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/producer/MQProducerInner.java b/client/src/main/java/org/apache/rocketmq/client/impl/producer/MQProducerInner.java index dfd485dd909..934a28073df 100644 --- a/client/src/main/java/org/apache/rocketmq/client/impl/producer/MQProducerInner.java +++ b/client/src/main/java/org/apache/rocketmq/client/impl/producer/MQProducerInner.java @@ -18,8 +18,9 @@ import java.util.Set; import org.apache.rocketmq.client.producer.TransactionCheckListener; +import org.apache.rocketmq.client.producer.TransactionListener; import org.apache.rocketmq.common.message.MessageExt; -import org.apache.rocketmq.common.protocol.header.CheckTransactionStateRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.CheckTransactionStateRequestHeader; public interface MQProducerInner { Set getPublishTopicList(); @@ -27,6 +28,7 @@ public interface MQProducerInner { boolean isPublishTopicNeedUpdate(final String topic); TransactionCheckListener checkListener(); + TransactionListener getCheckListener(); void checkTransactionState( final String addr, diff --git a/client/src/main/java/org/apache/rocketmq/client/impl/producer/TopicPublishInfo.java b/client/src/main/java/org/apache/rocketmq/client/impl/producer/TopicPublishInfo.java index deb02cff285..37b1f3252f7 100644 --- a/client/src/main/java/org/apache/rocketmq/client/impl/producer/TopicPublishInfo.java +++ b/client/src/main/java/org/apache/rocketmq/client/impl/producer/TopicPublishInfo.java @@ -18,18 +18,24 @@ import java.util.ArrayList; import java.util.List; + +import com.google.common.base.Preconditions; import org.apache.rocketmq.client.common.ThreadLocalIndex; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.route.QueueData; -import org.apache.rocketmq.common.protocol.route.TopicRouteData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; public class TopicPublishInfo { private boolean orderTopic = false; private boolean haveTopicRouterInfo = false; - private List messageQueueList = new ArrayList(); + private List messageQueueList = new ArrayList<>(); private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex(); private TopicRouteData topicRouteData; + public interface QueueFilter { + boolean filter(MessageQueue mq); + } + public boolean isOrderTopic() { return orderTopic; } @@ -66,15 +72,47 @@ public void setHaveTopicRouterInfo(boolean haveTopicRouterInfo) { this.haveTopicRouterInfo = haveTopicRouterInfo; } + public MessageQueue selectOneMessageQueue(QueueFilter ...filter) { + return selectOneMessageQueue(this.messageQueueList, this.sendWhichQueue, filter); + } + + private MessageQueue selectOneMessageQueue(List messageQueueList, ThreadLocalIndex sendQueue, QueueFilter ...filter) { + if (messageQueueList == null || messageQueueList.isEmpty()) { + return null; + } + + if (filter != null && filter.length != 0) { + for (int i = 0; i < messageQueueList.size(); i++) { + int index = Math.abs(sendQueue.incrementAndGet() % messageQueueList.size()); + MessageQueue mq = messageQueueList.get(index); + boolean filterResult = true; + for (QueueFilter f: filter) { + Preconditions.checkNotNull(f); + filterResult &= f.filter(mq); + } + if (filterResult) { + return mq; + } + } + + return null; + } + + int index = Math.abs(sendQueue.incrementAndGet() % messageQueueList.size()); + return messageQueueList.get(index); + } + + public void resetIndex() { + this.sendWhichQueue.reset(); + } + public MessageQueue selectOneMessageQueue(final String lastBrokerName) { if (lastBrokerName == null) { return selectOneMessageQueue(); } else { - int index = this.sendWhichQueue.getAndIncrement(); for (int i = 0; i < this.messageQueueList.size(); i++) { - int pos = Math.abs(index++) % this.messageQueueList.size(); - if (pos < 0) - pos = 0; + int index = this.sendWhichQueue.incrementAndGet(); + int pos = index % this.messageQueueList.size(); MessageQueue mq = this.messageQueueList.get(pos); if (!mq.getBrokerName().equals(lastBrokerName)) { return mq; @@ -85,14 +123,13 @@ public MessageQueue selectOneMessageQueue(final String lastBrokerName) { } public MessageQueue selectOneMessageQueue() { - int index = this.sendWhichQueue.getAndIncrement(); - int pos = Math.abs(index) % this.messageQueueList.size(); - if (pos < 0) - pos = 0; + int index = this.sendWhichQueue.incrementAndGet(); + int pos = index % this.messageQueueList.size(); + return this.messageQueueList.get(pos); } - public int getQueueIdByBroker(final String brokerName) { + public int getWriteQueueNumsByBroker(final String brokerName) { for (int i = 0; i < topicRouteData.getQueueDatas().size(); i++) { final QueueData queueData = this.topicRouteData.getQueueDatas().get(i); if (queueData.getBrokerName().equals(brokerName)) { diff --git a/client/src/main/java/org/apache/rocketmq/client/latency/LatencyFaultTolerance.java b/client/src/main/java/org/apache/rocketmq/client/latency/LatencyFaultTolerance.java index 09a8aa46189..17aaa266aae 100644 --- a/client/src/main/java/org/apache/rocketmq/client/latency/LatencyFaultTolerance.java +++ b/client/src/main/java/org/apache/rocketmq/client/latency/LatencyFaultTolerance.java @@ -18,11 +18,89 @@ package org.apache.rocketmq.client.latency; public interface LatencyFaultTolerance { - void updateFaultItem(final T name, final long currentLatency, final long notAvailableDuration); + /** + * Update brokers' states, to decide if they are good or not. + * + * @param name Broker's name. + * @param currentLatency Current message sending process's latency. + * @param notAvailableDuration Corresponding not available time, ms. The broker will be not available until it + * spends such time. + * @param reachable To decide if this broker is reachable or not. + */ + void updateFaultItem(final T name, final long currentLatency, final long notAvailableDuration, + final boolean reachable); + /** + * To check if this broker is available. + * + * @param name Broker's name. + * @return boolean variable, if this is true, then the broker is available. + */ boolean isAvailable(final T name); + /** + * To check if this broker is reachable. + * + * @param name Broker's name. + * @return boolean variable, if this is true, then the broker is reachable. + */ + boolean isReachable(final T name); + + /** + * Remove the broker in this fault item table. + * + * @param name broker's name. + */ void remove(final T name); + /** + * The worst situation, no broker can be available. Then choose random one. + * + * @return A random mq will be returned. + */ T pickOneAtLeast(); + + /** + * Start a new thread, to detect the broker's reachable tag. + */ + void startDetector(); + + /** + * Shutdown threads that started by LatencyFaultTolerance. + */ + void shutdown(); + + /** + * A function reserved, just detect by once, won't create a new thread. + */ + void detectByOneRound(); + + /** + * Use it to set the detect timeout bound. + * + * @param detectTimeout timeout bound + */ + void setDetectTimeout(final int detectTimeout); + + /** + * Use it to set the detector's detector interval for each broker (each broker will be detected once during this + * time) + * + * @param detectInterval each broker's detecting interval + */ + void setDetectInterval(final int detectInterval); + + /** + * Use it to set the detector work or not. + * + * @param startDetectorEnable set the detector's work status + */ + void setStartDetectorEnable(final boolean startDetectorEnable); + + /** + * Use it to judge if the detector enabled. + * + * @return is the detector should be started. + */ + boolean isStartDetectorEnable(); } diff --git a/client/src/main/java/org/apache/rocketmq/client/latency/LatencyFaultToleranceImpl.java b/client/src/main/java/org/apache/rocketmq/client/latency/LatencyFaultToleranceImpl.java index 72d43476f83..f629fe44a87 100644 --- a/client/src/main/java/org/apache/rocketmq/client/latency/LatencyFaultToleranceImpl.java +++ b/client/src/main/java/org/apache/rocketmq/client/latency/LatencyFaultToleranceImpl.java @@ -21,30 +21,101 @@ import java.util.Enumeration; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; import org.apache.rocketmq.client.common.ThreadLocalIndex; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; public class LatencyFaultToleranceImpl implements LatencyFaultTolerance { + private final static Logger log = LoggerFactory.getLogger(MQFaultStrategy.class); private final ConcurrentHashMap faultItemTable = new ConcurrentHashMap(16); - + private int detectTimeout = 200; + private int detectInterval = 2000; private final ThreadLocalIndex whichItemWorst = new ThreadLocalIndex(); + private volatile boolean startDetectorEnable = false; + private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "LatencyFaultToleranceScheduledThread"); + } + }); + + private final Resolver resolver; + + private final ServiceDetector serviceDetector; + + public LatencyFaultToleranceImpl(Resolver resolver, ServiceDetector serviceDetector) { + this.resolver = resolver; + this.serviceDetector = serviceDetector; + } + + public void detectByOneRound() { + for (Map.Entry item : this.faultItemTable.entrySet()) { + FaultItem brokerItem = item.getValue(); + if (System.currentTimeMillis() - brokerItem.checkStamp >= 0) { + brokerItem.checkStamp = System.currentTimeMillis() + this.detectInterval; + String brokerAddr = resolver.resolve(brokerItem.getName()); + if (brokerAddr == null) { + faultItemTable.remove(item.getKey()); + continue; + } + if (null == serviceDetector) { + continue; + } + boolean serviceOK = serviceDetector.detect(brokerAddr, detectTimeout); + if (serviceOK && !brokerItem.reachableFlag) { + log.info(brokerItem.name + " is reachable now, then it can be used."); + brokerItem.reachableFlag = true; + } + } + } + } + + public void startDetector() { + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + if (startDetectorEnable) { + detectByOneRound(); + } + } catch (Exception e) { + log.warn("Unexpected exception raised while detecting service reachability", e); + } + } + }, 3, 3, TimeUnit.SECONDS); + } + + public void shutdown() { + this.scheduledExecutorService.shutdown(); + } + @Override - public void updateFaultItem(final String name, final long currentLatency, final long notAvailableDuration) { + public void updateFaultItem(final String name, final long currentLatency, final long notAvailableDuration, + final boolean reachable) { FaultItem old = this.faultItemTable.get(name); if (null == old) { final FaultItem faultItem = new FaultItem(name); faultItem.setCurrentLatency(currentLatency); - faultItem.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration); - + faultItem.updateNotAvailableDuration(notAvailableDuration); + faultItem.setReachable(reachable); old = this.faultItemTable.putIfAbsent(name, faultItem); - if (old != null) { - old.setCurrentLatency(currentLatency); - old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration); - } - } else { + } + + if (null != old) { old.setCurrentLatency(currentLatency); - old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration); + old.updateNotAvailableDuration(notAvailableDuration); + old.setReachable(reachable); + } + + if (!reachable) { + log.info(name + " is unreachable, it will not be used until it's reachable"); } } @@ -57,11 +128,26 @@ public boolean isAvailable(final String name) { return true; } + public boolean isReachable(final String name) { + final FaultItem faultItem = this.faultItemTable.get(name); + if (faultItem != null) { + return faultItem.isReachable(); + } + return true; + } + @Override public void remove(final String name) { this.faultItemTable.remove(name); } + public boolean isStartDetectorEnable() { + return startDetectorEnable; + } + + public void setStartDetectorEnable(boolean startDetectorEnable) { + this.startDetectorEnable = startDetectorEnable; + } @Override public String pickOneAtLeast() { final Enumeration elements = this.faultItemTable.elements(); @@ -73,15 +159,10 @@ public String pickOneAtLeast() { if (!tmpList.isEmpty()) { Collections.shuffle(tmpList); - - Collections.sort(tmpList); - - final int half = tmpList.size() / 2; - if (half <= 0) { - return tmpList.get(0).getName(); - } else { - final int i = this.whichItemWorst.getAndIncrement() % half; - return tmpList.get(i).getName(); + for (FaultItem faultItem : tmpList) { + if (faultItem.reachableFlag) { + return faultItem.name; + } } } @@ -91,47 +172,77 @@ public String pickOneAtLeast() { @Override public String toString() { return "LatencyFaultToleranceImpl{" + - "faultItemTable=" + faultItemTable + - ", whichItemWorst=" + whichItemWorst + - '}'; + "faultItemTable=" + faultItemTable + + ", whichItemWorst=" + whichItemWorst + + '}'; } - class FaultItem implements Comparable { + public void setDetectTimeout(final int detectTimeout) { + this.detectTimeout = detectTimeout; + } + + public void setDetectInterval(final int detectInterval) { + this.detectInterval = detectInterval; + } + + public class FaultItem implements Comparable { private final String name; private volatile long currentLatency; private volatile long startTimestamp; + private volatile long checkStamp; + private volatile boolean reachableFlag; public FaultItem(final String name) { this.name = name; } + public void updateNotAvailableDuration(long notAvailableDuration) { + if (notAvailableDuration > 0 && System.currentTimeMillis() + notAvailableDuration > this.startTimestamp) { + this.startTimestamp = System.currentTimeMillis() + notAvailableDuration; + log.info(name + " will be isolated for " + notAvailableDuration + " ms."); + } + } + @Override public int compareTo(final FaultItem other) { if (this.isAvailable() != other.isAvailable()) { - if (this.isAvailable()) + if (this.isAvailable()) { return -1; + } - if (other.isAvailable()) + if (other.isAvailable()) { return 1; + } } - if (this.currentLatency < other.currentLatency) + if (this.currentLatency < other.currentLatency) { return -1; - else if (this.currentLatency > other.currentLatency) { + } else if (this.currentLatency > other.currentLatency) { return 1; } - if (this.startTimestamp < other.startTimestamp) + if (this.startTimestamp < other.startTimestamp) { return -1; - else if (this.startTimestamp > other.startTimestamp) { + } else if (this.startTimestamp > other.startTimestamp) { return 1; } - return 0; } + public void setReachable(boolean reachableFlag) { + this.reachableFlag = reachableFlag; + } + + public void setCheckStamp(long checkStamp) { + this.checkStamp = checkStamp; + } + public boolean isAvailable() { - return (System.currentTimeMillis() - startTimestamp) >= 0; + return System.currentTimeMillis() >= startTimestamp; + } + + public boolean isReachable() { + return reachableFlag; } @Override @@ -144,28 +255,32 @@ public int hashCode() { @Override public boolean equals(final Object o) { - if (this == o) + if (this == o) { return true; - if (!(o instanceof FaultItem)) + } + if (!(o instanceof FaultItem)) { return false; + } final FaultItem faultItem = (FaultItem) o; - if (getCurrentLatency() != faultItem.getCurrentLatency()) + if (getCurrentLatency() != faultItem.getCurrentLatency()) { return false; - if (getStartTimestamp() != faultItem.getStartTimestamp()) + } + if (getStartTimestamp() != faultItem.getStartTimestamp()) { return false; + } return getName() != null ? getName().equals(faultItem.getName()) : faultItem.getName() == null; - } @Override public String toString() { return "FaultItem{" + - "name='" + name + '\'' + - ", currentLatency=" + currentLatency + - ", startTimestamp=" + startTimestamp + - '}'; + "name='" + name + '\'' + + ", currentLatency=" + currentLatency + + ", startTimestamp=" + startTimestamp + + ", reachableFlag=" + reachableFlag + + '}'; } public String getName() { diff --git a/client/src/main/java/org/apache/rocketmq/client/latency/MQFaultStrategy.java b/client/src/main/java/org/apache/rocketmq/client/latency/MQFaultStrategy.java index 235aa20af8e..69fb533e5ad 100644 --- a/client/src/main/java/org/apache/rocketmq/client/latency/MQFaultStrategy.java +++ b/client/src/main/java/org/apache/rocketmq/client/latency/MQFaultStrategy.java @@ -17,24 +17,86 @@ package org.apache.rocketmq.client.latency; +import org.apache.rocketmq.client.ClientConfig; import org.apache.rocketmq.client.impl.producer.TopicPublishInfo; -import org.apache.rocketmq.client.log.ClientLogger; +import org.apache.rocketmq.client.impl.producer.TopicPublishInfo.QueueFilter; import org.apache.rocketmq.common.message.MessageQueue; -import org.slf4j.Logger; public class MQFaultStrategy { - private final static Logger log = ClientLogger.getLog(); - private final LatencyFaultTolerance latencyFaultTolerance = new LatencyFaultToleranceImpl(); + private LatencyFaultTolerance latencyFaultTolerance; + private volatile boolean sendLatencyFaultEnable; + private volatile boolean startDetectorEnable; + private long[] latencyMax = {50L, 100L, 550L, 1800L, 3000L, 5000L, 15000L}; + private long[] notAvailableDuration = {0L, 0L, 2000L, 5000L, 6000L, 10000L, 30000L}; - private boolean sendLatencyFaultEnable = false; + public static class BrokerFilter implements QueueFilter { + private String lastBrokerName; + + public void setLastBrokerName(String lastBrokerName) { + this.lastBrokerName = lastBrokerName; + } + + @Override public boolean filter(MessageQueue mq) { + if (lastBrokerName != null) { + return !mq.getBrokerName().equals(lastBrokerName); + } + return true; + } + } + + private ThreadLocal threadBrokerFilter = new ThreadLocal() { + @Override protected BrokerFilter initialValue() { + return new BrokerFilter(); + } + }; + + private QueueFilter reachableFilter = new QueueFilter() { + @Override public boolean filter(MessageQueue mq) { + return latencyFaultTolerance.isReachable(mq.getBrokerName()); + } + }; + + private QueueFilter availableFilter = new QueueFilter() { + @Override public boolean filter(MessageQueue mq) { + return latencyFaultTolerance.isAvailable(mq.getBrokerName()); + } + }; + + + public MQFaultStrategy(ClientConfig cc, Resolver fetcher, ServiceDetector serviceDetector) { + this.latencyFaultTolerance = new LatencyFaultToleranceImpl(fetcher, serviceDetector); + this.latencyFaultTolerance.setDetectInterval(cc.getDetectInterval()); + this.latencyFaultTolerance.setDetectTimeout(cc.getDetectTimeout()); + this.setStartDetectorEnable(cc.isStartDetectorEnable()); + this.setSendLatencyFaultEnable(cc.isSendLatencyEnable()); + } + + // For unit test. + public MQFaultStrategy(ClientConfig cc, LatencyFaultTolerance tolerance) { + this.setStartDetectorEnable(cc.isStartDetectorEnable()); + this.setSendLatencyFaultEnable(cc.isSendLatencyEnable()); + this.latencyFaultTolerance = tolerance; + this.latencyFaultTolerance.setDetectInterval(cc.getDetectInterval()); + this.latencyFaultTolerance.setDetectTimeout(cc.getDetectTimeout()); + } - private long[] latencyMax = {50L, 100L, 550L, 1000L, 2000L, 3000L, 15000L}; - private long[] notAvailableDuration = {0L, 0L, 30000L, 60000L, 120000L, 180000L, 600000L}; public long[] getNotAvailableDuration() { return notAvailableDuration; } + public QueueFilter getAvailableFilter() { + return availableFilter; + } + + public QueueFilter getReachableFilter() { + return reachableFilter; + } + + public ThreadLocal getThreadBrokerFilter() { + return threadBrokerFilter; + } + public void setNotAvailableDuration(final long[] notAvailableDuration) { this.notAvailableDuration = notAvailableDuration; } @@ -55,54 +117,63 @@ public void setSendLatencyFaultEnable(final boolean sendLatencyFaultEnable) { this.sendLatencyFaultEnable = sendLatencyFaultEnable; } - public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) { + public boolean isStartDetectorEnable() { + return startDetectorEnable; + } + + public void setStartDetectorEnable(boolean startDetectorEnable) { + this.startDetectorEnable = startDetectorEnable; + this.latencyFaultTolerance.setStartDetectorEnable(startDetectorEnable); + } + + public void startDetector() { + this.latencyFaultTolerance.startDetector(); + } + + public void shutdown() { + this.latencyFaultTolerance.shutdown(); + } + + public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName, final boolean resetIndex) { + BrokerFilter brokerFilter = threadBrokerFilter.get(); + brokerFilter.setLastBrokerName(lastBrokerName); if (this.sendLatencyFaultEnable) { - try { - int index = tpInfo.getSendWhichQueue().getAndIncrement(); - for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) { - int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size(); - if (pos < 0) - pos = 0; - MessageQueue mq = tpInfo.getMessageQueueList().get(pos); - if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) { - if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName)) - return mq; - } - } - - final String notBestBroker = latencyFaultTolerance.pickOneAtLeast(); - int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker); - if (writeQueueNums > 0) { - final MessageQueue mq = tpInfo.selectOneMessageQueue(); - if (notBestBroker != null) { - mq.setBrokerName(notBestBroker); - mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums); - } - return mq; - } else { - latencyFaultTolerance.remove(notBestBroker); - } - } catch (Exception e) { - log.error("Error occurred when selecting message queue", e); + if (resetIndex) { + tpInfo.resetIndex(); + } + MessageQueue mq = tpInfo.selectOneMessageQueue(availableFilter, brokerFilter); + if (mq != null) { + return mq; + } + + mq = tpInfo.selectOneMessageQueue(reachableFilter, brokerFilter); + if (mq != null) { + return mq; } return tpInfo.selectOneMessageQueue(); } - return tpInfo.selectOneMessageQueue(lastBrokerName); + MessageQueue mq = tpInfo.selectOneMessageQueue(brokerFilter); + if (mq != null) { + return mq; + } + return tpInfo.selectOneMessageQueue(); } - public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) { + public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation, + final boolean reachable) { if (this.sendLatencyFaultEnable) { - long duration = computeNotAvailableDuration(isolation ? 30000 : currentLatency); - this.latencyFaultTolerance.updateFaultItem(brokerName, currentLatency, duration); + long duration = computeNotAvailableDuration(isolation ? 10000 : currentLatency); + this.latencyFaultTolerance.updateFaultItem(brokerName, currentLatency, duration, reachable); } } private long computeNotAvailableDuration(final long currentLatency) { for (int i = latencyMax.length - 1; i >= 0; i--) { - if (currentLatency >= latencyMax[i]) + if (currentLatency >= latencyMax[i]) { return this.notAvailableDuration[i]; + } } return 0; diff --git a/client/src/main/java/org/apache/rocketmq/client/latency/Resolver.java b/client/src/main/java/org/apache/rocketmq/client/latency/Resolver.java new file mode 100644 index 00000000000..1c29ba33469 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/latency/Resolver.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.latency; + +public interface Resolver { + + String resolve(String name); +} diff --git a/client/src/main/java/org/apache/rocketmq/client/latency/ServiceDetector.java b/client/src/main/java/org/apache/rocketmq/client/latency/ServiceDetector.java new file mode 100644 index 00000000000..c6ffbad1cb6 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/latency/ServiceDetector.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.latency; + +/** + * Detect whether the remote service state is normal. + */ +public interface ServiceDetector { + + /** + * Check if the remote service is normal. + * @param endpoint Service endpoint to check against + * @return true if the service is back to normal; false otherwise. + */ + boolean detect(String endpoint, long timeoutMillis); +} diff --git a/client/src/main/java/org/apache/rocketmq/client/log/ClientLogger.java b/client/src/main/java/org/apache/rocketmq/client/log/ClientLogger.java deleted file mode 100644 index c3df9a63947..00000000000 --- a/client/src/main/java/org/apache/rocketmq/client/log/ClientLogger.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.client.log; - -import org.apache.rocketmq.common.constant.LoggerName; -import org.slf4j.ILoggerFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.lang.reflect.Method; -import java.net.URL; - -public class ClientLogger { - public static final String CLIENT_LOG_ROOT = "rocketmq.client.logRoot"; - public static final String CLIENT_LOG_MAXINDEX = "rocketmq.client.logFileMaxIndex"; - public static final String CLIENT_LOG_LEVEL = "rocketmq.client.logLevel"; - - private static Logger log; - - private static Logger createLogger(final String loggerName) { - String logConfigFilePath = System.getProperty("rocketmq.client.log.configFile", System.getenv("ROCKETMQ_CLIENT_LOG_CONFIGFILE")); - Boolean isloadconfig = - Boolean.parseBoolean(System.getProperty("rocketmq.client.log.loadconfig", "true")); - - final String log4JResourceFile = - System.getProperty("rocketmq.client.log4j.resource.fileName", "log4j_rocketmq_client.xml"); - - final String logbackResourceFile = - System.getProperty("rocketmq.client.logback.resource.fileName", "logback_rocketmq_client.xml"); - - final String log4J2ResourceFile = - System.getProperty("rocketmq.client.log4j2.resource.fileName", "log4j2_rocketmq_client.xml"); - - String clientLogRoot = System.getProperty(CLIENT_LOG_ROOT, System.getProperty("user.home") + "/logs/rocketmqlogs"); - System.setProperty("client.logRoot", clientLogRoot); - String clientLogLevel = System.getProperty(CLIENT_LOG_LEVEL, "INFO"); - System.setProperty("client.logLevel", clientLogLevel); - String clientLogMaxIndex = System.getProperty(CLIENT_LOG_MAXINDEX, "10"); - System.setProperty("client.logFileMaxIndex", clientLogMaxIndex); - - if (isloadconfig) { - try { - ILoggerFactory iLoggerFactory = LoggerFactory.getILoggerFactory(); - Class classType = iLoggerFactory.getClass(); - if (classType.getName().equals("org.slf4j.impl.Log4jLoggerFactory")) { - Class domconfigurator; - Object domconfiguratorobj; - domconfigurator = Class.forName("org.apache.log4j.xml.DOMConfigurator"); - domconfiguratorobj = domconfigurator.newInstance(); - if (null == logConfigFilePath) { - Method configure = domconfiguratorobj.getClass().getMethod("configure", URL.class); - URL url = ClientLogger.class.getClassLoader().getResource(log4JResourceFile); - configure.invoke(domconfiguratorobj, url); - } else { - Method configure = domconfiguratorobj.getClass().getMethod("configure", String.class); - configure.invoke(domconfiguratorobj, logConfigFilePath); - } - - } else if (classType.getName().equals("ch.qos.logback.classic.LoggerContext")) { - Class joranConfigurator; - Class context = Class.forName("ch.qos.logback.core.Context"); - Object joranConfiguratoroObj; - joranConfigurator = Class.forName("ch.qos.logback.classic.joran.JoranConfigurator"); - joranConfiguratoroObj = joranConfigurator.newInstance(); - Method setContext = joranConfiguratoroObj.getClass().getMethod("setContext", context); - setContext.invoke(joranConfiguratoroObj, iLoggerFactory); - if (null == logConfigFilePath) { - URL url = ClientLogger.class.getClassLoader().getResource(logbackResourceFile); - Method doConfigure = - joranConfiguratoroObj.getClass().getMethod("doConfigure", URL.class); - doConfigure.invoke(joranConfiguratoroObj, url); - } else { - Method doConfigure = - joranConfiguratoroObj.getClass().getMethod("doConfigure", String.class); - doConfigure.invoke(joranConfiguratoroObj, logConfigFilePath); - } - - } else if (classType.getName().equals("org.apache.logging.slf4j.Log4jLoggerFactory")) { - Class joranConfigurator = Class.forName("org.apache.logging.log4j.core.config.Configurator"); - Method initialize = joranConfigurator.getDeclaredMethod("initialize", String.class, String.class); - if (null == logConfigFilePath) { - initialize.invoke(joranConfigurator, "log4j2", log4J2ResourceFile); - } else { - initialize.invoke(joranConfigurator, "log4j2", logConfigFilePath); - } - } - } catch (Exception e) { - System.err.println(e); - } - } - return LoggerFactory.getLogger(LoggerName.CLIENT_LOGGER_NAME); - } - - public static Logger getLog() { - if (log == null) { - log = createLogger(LoggerName.CLIENT_LOGGER_NAME); - return log; - } else { - return log; - } - } - - public static void setLog(Logger log) { - ClientLogger.log = log; - } -} diff --git a/client/src/main/java/org/apache/rocketmq/client/producer/DefaultMQProducer.java b/client/src/main/java/org/apache/rocketmq/client/producer/DefaultMQProducer.java index a2f25dd0f8f..13be47c79da 100644 --- a/client/src/main/java/org/apache/rocketmq/client/producer/DefaultMQProducer.java +++ b/client/src/main/java/org/apache/rocketmq/client/producer/DefaultMQProducer.java @@ -16,43 +16,50 @@ */ package org.apache.rocketmq.client.producer; +import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.ExecutorService; import org.apache.rocketmq.client.ClientConfig; import org.apache.rocketmq.client.QueryResult; import org.apache.rocketmq.client.Validators; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.exception.RequestTimeoutException; +import org.apache.rocketmq.client.impl.MQClientManager; import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl; +import org.apache.rocketmq.client.trace.AsyncTraceDispatcher; +import org.apache.rocketmq.client.trace.TraceDispatcher; +import org.apache.rocketmq.client.trace.hook.EndTransactionTraceHookImpl; +import org.apache.rocketmq.client.trace.hook.SendMessageTraceHookImpl; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageBatch; import org.apache.rocketmq.common.message.MessageClientIDSetter; -import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageConst; import org.apache.rocketmq.common.message.MessageExt; -import org.apache.rocketmq.common.message.MessageId; import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.topic.TopicValidator; import org.apache.rocketmq.remoting.RPCHook; import org.apache.rocketmq.remoting.exception.RemotingException; -import org.apache.rocketmq.remoting.netty.NettyRemotingClient; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; /** - * This class is the entry point for applications intending to send messages. - *

- * + * This class is the entry point for applications intending to send messages.

+ *

* It's fine to tune fields which exposes getter/setter methods, but keep in mind, all of them should work well out of - * box for most scenarios. - *

- * - * This class aggregates various send methods to deliver messages to brokers. Each of them has pros and - * cons; you'd better understand strengths and weakness of them before actually coding. - *

- * + * box for most scenarios.

*

- * Thread Safety: After configuring and starting process, this class can be regarded as thread-safe - * and used among multiple threads context. - *

+ * This class aggregates various send methods to deliver messages to broker(s). Each of them has pros and + * cons; you'd better understand strengths and weakness of them before actually coding.

+ * + *

Thread Safety: After configuring and starting process, this class can be regarded as thread-safe + * and used among multiple threads context.

*/ public class DefaultMQProducer extends ClientConfig implements MQProducer { @@ -60,23 +67,35 @@ public class DefaultMQProducer extends ClientConfig implements MQProducer { * Wrapping internal implementations for virtually all methods presented in this class. */ protected final transient DefaultMQProducerImpl defaultMQProducerImpl; + private final Logger logger = LoggerFactory.getLogger(DefaultMQProducer.class); + private final Set retryResponseCodes = new CopyOnWriteArraySet<>(Arrays.asList( + ResponseCode.TOPIC_NOT_EXIST, + ResponseCode.SERVICE_NOT_AVAILABLE, + ResponseCode.SYSTEM_ERROR, + ResponseCode.NO_PERMISSION, + ResponseCode.NO_BUYER_ID, + ResponseCode.NOT_IN_CURRENT_UNIT + )); /** * Producer group conceptually aggregates all producer instances of exactly same role, which is particularly - * important when transactional messages are involved. - *

- * - * For non-transactional messages, it does not matter as long as it's unique per process. - *

- * - * See {@linktourl http://rocketmq.apache.org/docs/core-concept/} for more discussion. + * important when transactional messages are involved.

+ *

+ * For non-transactional messages, it does not matter as long as it's unique per process.

+ *

+ * See core concepts for more discussion. */ private String producerGroup; + /** + * Topics that need to be initialized for transaction producer + */ + private List topics; + /** * Just for testing or demo program */ - private String createTopicKey = MixAll.DEFAULT_TOPIC; + private String createTopicKey = TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC; /** * Number of queues to create per default topic. @@ -94,17 +113,15 @@ public class DefaultMQProducer extends ClientConfig implements MQProducer { private int compressMsgBodyOverHowmuch = 1024 * 4; /** - * Maximum number of retry to perform internally before claiming sending failure in synchronous mode. - *

- * + * Maximum number of retry to perform internally before claiming sending failure in synchronous mode.

+ *

* This may potentially cause message duplication which is up to application developers to resolve. */ private int retryTimesWhenSendFailed = 2; /** - * Maximum number of retry to perform internally before claiming sending failure in asynchronous mode. - *

- * + * Maximum number of retry to perform internally before claiming sending failure in asynchronous mode.

+ *

* This may potentially cause message duplication which is up to application developers to resolve. */ private int retryTimesWhenSendAsyncFailed = 2; @@ -115,61 +132,237 @@ public class DefaultMQProducer extends ClientConfig implements MQProducer { private boolean retryAnotherBrokerWhenNotStoreOK = false; /** - * Maximum allowed message size in bytes. + * Maximum allowed message body size in bytes. */ private int maxMessageSize = 1024 * 1024 * 4; // 4M + /** + * Interface of asynchronous transfer data + */ + private TraceDispatcher traceDispatcher = null; + + /** + * Switch flag instance for automatic batch message + */ + private boolean autoBatch = false; + /** + * Instance for batching message automatically + */ + private ProduceAccumulator produceAccumulator = null; + + /** + * Indicate whether to block message when asynchronous sending traffic is too heavy. + */ + private boolean enableBackpressureForAsyncMode = false; + + /** + * on BackpressureForAsyncMode, limit maximum number of on-going sending async messages + * default is 10000 + */ + private int backPressureForAsyncSendNum = 10000; + + /** + * on BackpressureForAsyncMode, limit maximum message size of on-going sending async messages + * default is 100M + */ + private int backPressureForAsyncSendSize = 100 * 1024 * 1024; + /** * Default constructor. */ public DefaultMQProducer() { - this(MixAll.DEFAULT_PRODUCER_GROUP, null); + this(MixAll.DEFAULT_PRODUCER_GROUP); + } + + /** + * Constructor specifying the RPC hook. + * + * @param rpcHook RPC hook to execute per each remoting command execution. + */ + public DefaultMQProducer(RPCHook rpcHook) { + this(MixAll.DEFAULT_PRODUCER_GROUP, rpcHook); + } + + /** + * Constructor specifying producer group. + * + * @param producerGroup Producer group, see the name-sake field. + */ + public DefaultMQProducer(final String producerGroup) { + this.producerGroup = producerGroup; + defaultMQProducerImpl = new DefaultMQProducerImpl(this, null); + produceAccumulator = MQClientManager.getInstance().getOrCreateProduceAccumulator(this); } /** * Constructor specifying both producer group and RPC hook. * * @param producerGroup Producer group, see the name-sake field. - * @param rpcHook RPC hook to execute per each remoting command execution. + * @param rpcHook RPC hook to execute per each remoting command execution. */ public DefaultMQProducer(final String producerGroup, RPCHook rpcHook) { this.producerGroup = producerGroup; defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook); + produceAccumulator = MQClientManager.getInstance().getOrCreateProduceAccumulator(this); + } + + /** + * Constructor specifying namespace, producer group, topics and RPC hook. + * + * @param producerGroup Producer group, see the name-sake field. + * @param rpcHook RPC hook to execute per each remoting command execution. + * @param topics Topic that needs to be initialized for routing + */ + public DefaultMQProducer(final String producerGroup, RPCHook rpcHook, + final List topics) { + this(producerGroup, rpcHook); + this.topics = topics; + } + + /** + * Constructor specifying producer group, enabled msgTrace flag and customized trace topic name. + * + * @param producerGroup Producer group, see the name-sake field. + * @param enableMsgTrace Switch flag instance for message trace. + * @param customizedTraceTopic The name value of message trace topic.If you don't config,you can use the default + * trace topic name. + */ + public DefaultMQProducer(final String producerGroup, boolean enableMsgTrace, final String customizedTraceTopic) { + this(producerGroup, null, enableMsgTrace, customizedTraceTopic); } /** * Constructor specifying producer group. * + * @param producerGroup Producer group, see the name-sake field. + * @param rpcHook RPC hook to execute per each remoting command execution. + * @param enableMsgTrace Switch flag instance for message trace. + * @param customizedTraceTopic The name value of message trace topic.If you don't config,you can use the default + * trace topic name. + */ + public DefaultMQProducer(final String producerGroup, RPCHook rpcHook, boolean enableMsgTrace, + final String customizedTraceTopic) { + this(producerGroup, rpcHook); + //if client open the message trace feature + if (enableMsgTrace) { + try { + AsyncTraceDispatcher dispatcher = new AsyncTraceDispatcher(producerGroup, TraceDispatcher.Type.PRODUCE, customizedTraceTopic, rpcHook); + dispatcher.setHostProducer(this.defaultMQProducerImpl); + traceDispatcher = dispatcher; + this.defaultMQProducerImpl.registerSendMessageHook( + new SendMessageTraceHookImpl(traceDispatcher)); + this.defaultMQProducerImpl.registerEndTransactionHook( + new EndTransactionTraceHookImpl(traceDispatcher)); + } catch (Throwable e) { + logger.error("system mqtrace hook init failed ,maybe can't send msg trace data"); + } + } + } + + /** + * Constructor specifying namespace, producer group, topics, RPC hook, enabled msgTrace flag and customized trace topic + * name. + * + * @param producerGroup Producer group, see the name-sake field. + * @param rpcHook RPC hook to execute per each remoting command execution. + * @param topics Topic that needs to be initialized for routing + * @param enableMsgTrace Switch flag instance for message trace. + * @param customizedTraceTopic The name value of message trace topic.If you don't config,you can use the default + * trace topic name. + */ + public DefaultMQProducer(final String producerGroup, RPCHook rpcHook, final List topics, + boolean enableMsgTrace, final String customizedTraceTopic) { + this(producerGroup, rpcHook, enableMsgTrace, customizedTraceTopic); + this.topics = topics; + } + + /** + * Constructor specifying producer group. + * + * @param namespace Namespace for this MQ Producer instance. * @param producerGroup Producer group, see the name-sake field. */ - public DefaultMQProducer(final String producerGroup) { - this(producerGroup, null); + @Deprecated + public DefaultMQProducer(final String namespace, final String producerGroup) { + this(namespace, producerGroup, null); } /** - * Constructor specifying the RPC hook. + * Constructor specifying namespace, producer group and RPC hook. * - * @param rpcHook RPC hook to execute per each remoting command execution. + * @param namespace Namespace for this MQ Producer instance. + * @param producerGroup Producer group, see the name-sake field. + * @param rpcHook RPC hook to execute per each remoting command execution. */ - public DefaultMQProducer(RPCHook rpcHook) { - this(MixAll.DEFAULT_PRODUCER_GROUP, rpcHook); + @Deprecated + public DefaultMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook) { + this.namespace = namespace; + this.producerGroup = producerGroup; + defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook); + produceAccumulator = MQClientManager.getInstance().getOrCreateProduceAccumulator(this); + } + + /** + * Constructor specifying namespace, producer group, RPC hook, enabled msgTrace flag and customized trace topic + * name. + * + * @param namespace Namespace for this MQ Producer instance. + * @param producerGroup Producer group, see the name-sake field. + * @param rpcHook RPC hook to execute per each remoting command execution. + * @param enableMsgTrace Switch flag instance for message trace. + * @param customizedTraceTopic The name value of message trace topic.If you don't config,you can use the default + * trace topic name. + */ + @Deprecated + public DefaultMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook, + boolean enableMsgTrace, final String customizedTraceTopic) { + this(namespace, producerGroup, rpcHook); + //if client open the message trace feature + if (enableMsgTrace) { + try { + AsyncTraceDispatcher dispatcher = new AsyncTraceDispatcher(producerGroup, TraceDispatcher.Type.PRODUCE, customizedTraceTopic, rpcHook); + dispatcher.setHostProducer(this.defaultMQProducerImpl); + traceDispatcher = dispatcher; + this.defaultMQProducerImpl.registerSendMessageHook( + new SendMessageTraceHookImpl(traceDispatcher)); + this.defaultMQProducerImpl.registerEndTransactionHook( + new EndTransactionTraceHookImpl(traceDispatcher)); + } catch (Throwable e) { + logger.error("system mqtrace hook init failed ,maybe can't send msg trace data"); + } + } + } + + @Override + public void setUseTLS(boolean useTLS) { + super.setUseTLS(useTLS); + if (traceDispatcher instanceof AsyncTraceDispatcher) { + ((AsyncTraceDispatcher) traceDispatcher).getTraceProducer().setUseTLS(useTLS); + } } /** - * Start this producer instance. - *

+ * Start this producer instance.

* - * - * Much internal initializing procedures are carried out to make this instance prepared, thus, it's a must to invoke - * this method before sending or querying messages. - * - *

+ * Much internal initializing procedures are carried out to make this instance prepared, thus, it's a must + * to invoke this method before sending or querying messages.

* * @throws MQClientException if there is any unexpected error. */ @Override public void start() throws MQClientException { + this.setProducerGroup(withNamespace(this.producerGroup)); this.defaultMQProducerImpl.start(); + if (this.produceAccumulator != null) { + this.produceAccumulator.start(); + } + if (null != traceDispatcher) { + try { + traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel()); + } catch (MQClientException e) { + logger.warn("trace dispatcher start failed ", e); + } + } } /** @@ -178,6 +371,12 @@ public void start() throws MQClientException { @Override public void shutdown() { this.defaultMQProducerImpl.shutdown(); + if (this.produceAccumulator != null) { + this.produceAccumulator.shutdown(); + } + if (null != traceDispatcher) { + traceDispatcher.shutdown(); + } } /** @@ -189,85 +388,118 @@ public void shutdown() { */ @Override public List fetchPublishMessageQueues(String topic) throws MQClientException { - return this.defaultMQProducerImpl.fetchPublishMessageQueues(topic); + return this.defaultMQProducerImpl.fetchPublishMessageQueues(withNamespace(topic)); + } + + private boolean canBatch(Message msg) { + // produceAccumulator is full + if (!produceAccumulator.tryAddMessage(msg)) { + return false; + } + // delay message do not support batch processing + if (msg.getDelayTimeLevel() > 0 || msg.getDelayTimeMs() > 0 || msg.getDelayTimeSec() > 0 || msg.getDeliverTimeMs() > 0) { + return false; + } + // retry message do not support batch processing + if (msg.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + return false; + } + // message which have been assigned to producer group do not support batch processing + if (msg.getProperties().containsKey(MessageConst.PROPERTY_PRODUCER_GROUP)) { + return false; + } + return true; } /** - * Send message in synchronous mode. This method returns only when the sending procedure totally completes. - *

+ * Send message in synchronous mode. This method returns only when the sending procedure totally completes.

* * Warn: this method has internal retry-mechanism, that is, internal implementation will retry - * {@link #retryTimesWhenSendFailed} times before claiming failure. As a result, multiple messages may potentially + * {@link #retryTimesWhenSendFailed} times before claiming failure. As a result, multiple messages may be potentially * delivered to broker(s). It's up to the application developers to resolve potential duplication issue. * * @param msg Message to send. * @return {@link SendResult} instance to inform senders details of the deliverable, say Message ID of the message, * {@link SendStatus} indicating broker storage/replication status, message queue sent to, etc. - * @throws MQClientException if there is any client error. - * @throws RemotingException if there is any network-tier error. - * @throws MQBrokerException if there is any error with broker. + * @throws MQClientException if there is any client error. + * @throws RemotingException if there is any network-tier error. + * @throws MQBrokerException if there is any error with broker. * @throws InterruptedException if the sending thread is interrupted. */ @Override public SendResult send( Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { - return this.defaultMQProducerImpl.send(msg); + msg.setTopic(withNamespace(msg.getTopic())); + if (this.getAutoBatch() && !(msg instanceof MessageBatch)) { + return sendByAccumulator(msg, null, null); + } else { + return sendDirect(msg, null, null); + } } /** * Same to {@link #send(Message)} with send timeout specified in addition. * - * @param msg Message to send. + * @param msg Message to send. * @param timeout send timeout. * @return {@link SendResult} instance to inform senders details of the deliverable, say Message ID of the message, * {@link SendStatus} indicating broker storage/replication status, message queue sent to, etc. - * @throws MQClientException if there is any client error. - * @throws RemotingException if there is any network-tier error. - * @throws MQBrokerException if there is any error with broker. + * @throws MQClientException if there is any client error. + * @throws RemotingException if there is any network-tier error. + * @throws MQBrokerException if there is any error with broker. * @throws InterruptedException if the sending thread is interrupted. */ @Override public SendResult send(Message msg, long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + msg.setTopic(withNamespace(msg.getTopic())); return this.defaultMQProducerImpl.send(msg, timeout); } /** - * Send message to broker asynchronously. - *

- * - * This method returns immediately. On sending completion, sendCallback will be executed. - *

+ * Send message to broker asynchronously.

+ *

+ * This method returns immediately. On sending completion, sendCallback will be executed.

+ *

+ * Similar to {@link #send(Message)}, internal implementation would potentially retry up to {@link + * #retryTimesWhenSendAsyncFailed} times before claiming sending failure, which may yield message duplication and + * application developers are the one to resolve this potential issue. * - * Similar to {@link #send(Message)}, internal implementation would potentially retry up to - * {@link #retryTimesWhenSendAsyncFailed} times before claiming sending failure, which may yield message duplication - * and application developers are the one to resolve this potential issue. - * - * @param msg Message to send. + * @param msg Message to send. * @param sendCallback Callback to execute on sending completed, either successful or unsuccessful. - * @throws MQClientException if there is any client error. - * @throws RemotingException if there is any network-tier error. + * @throws MQClientException if there is any client error. + * @throws RemotingException if there is any network-tier error. * @throws InterruptedException if the sending thread is interrupted. */ @Override public void send(Message msg, SendCallback sendCallback) throws MQClientException, RemotingException, InterruptedException { - this.defaultMQProducerImpl.send(msg, sendCallback); + msg.setTopic(withNamespace(msg.getTopic())); + try { + if (this.getAutoBatch() && !(msg instanceof MessageBatch)) { + sendByAccumulator(msg, null, sendCallback); + } else { + sendDirect(msg, null, sendCallback); + } + } catch (Throwable e) { + sendCallback.onException(e); + } } /** * Same to {@link #send(Message, SendCallback)} with send timeout specified in addition. * - * @param msg message to send. + * @param msg message to send. * @param sendCallback Callback to execute. - * @param timeout send timeout. - * @throws MQClientException if there is any client error. - * @throws RemotingException if there is any network-tier error. + * @param timeout send timeout. + * @throws MQClientException if there is any client error. + * @throws RemotingException if there is any network-tier error. * @throws InterruptedException if the sending thread is interrupted. */ @Override public void send(Message msg, SendCallback sendCallback, long timeout) throws MQClientException, RemotingException, InterruptedException { + msg.setTopic(withNamespace(msg.getTopic())); this.defaultMQProducerImpl.send(msg, sendCallback, timeout); } @@ -276,12 +508,13 @@ public void send(Message msg, SendCallback sendCallback, long timeout) * acknowledgement from broker before return. Obviously, it has maximums throughput yet potentials of message loss. * * @param msg Message to send. - * @throws MQClientException if there is any client error. - * @throws RemotingException if there is any network-tier error. + * @throws MQClientException if there is any client error. + * @throws RemotingException if there is any network-tier error. * @throws InterruptedException if the sending thread is interrupted. */ @Override public void sendOneway(Message msg) throws MQClientException, RemotingException, InterruptedException { + msg.setTopic(withNamespace(msg.getTopic())); this.defaultMQProducerImpl.sendOneway(msg); } @@ -289,279 +522,499 @@ public void sendOneway(Message msg) throws MQClientException, RemotingException, * Same to {@link #send(Message)} with target message queue specified in addition. * * @param msg Message to send. - * @param mq Target message queue. + * @param mq Target message queue. * @return {@link SendResult} instance to inform senders details of the deliverable, say Message ID of the message, * {@link SendStatus} indicating broker storage/replication status, message queue sent to, etc. - * @throws MQClientException if there is any client error. - * @throws RemotingException if there is any network-tier error. - * @throws MQBrokerException if there is any error with broker. + * @throws MQClientException if there is any client error. + * @throws RemotingException if there is any network-tier error. + * @throws MQBrokerException if there is any error with broker. * @throws InterruptedException if the sending thread is interrupted. */ @Override public SendResult send(Message msg, MessageQueue mq) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { - return this.defaultMQProducerImpl.send(msg, mq); + msg.setTopic(withNamespace(msg.getTopic())); + mq = queueWithNamespace(mq); + if (this.getAutoBatch() && !(msg instanceof MessageBatch)) { + return sendByAccumulator(msg, mq, null); + } else { + return sendDirect(msg, mq, null); + } } /** * Same to {@link #send(Message)} with target message queue and send timeout specified. * - * @param msg Message to send. - * @param mq Target message queue. + * @param msg Message to send. + * @param mq Target message queue. * @param timeout send timeout. * @return {@link SendResult} instance to inform senders details of the deliverable, say Message ID of the message, * {@link SendStatus} indicating broker storage/replication status, message queue sent to, etc. - * @throws MQClientException if there is any client error. - * @throws RemotingException if there is any network-tier error. - * @throws MQBrokerException if there is any error with broker. + * @throws MQClientException if there is any client error. + * @throws RemotingException if there is any network-tier error. + * @throws MQBrokerException if there is any error with broker. * @throws InterruptedException if the sending thread is interrupted. */ @Override public SendResult send(Message msg, MessageQueue mq, long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { - return this.defaultMQProducerImpl.send(msg, mq, timeout); + msg.setTopic(withNamespace(msg.getTopic())); + return this.defaultMQProducerImpl.send(msg, queueWithNamespace(mq), timeout); } /** * Same to {@link #send(Message, SendCallback)} with target message queue specified. * - * @param msg Message to send. - * @param mq Target message queue. + * @param msg Message to send. + * @param mq Target message queue. * @param sendCallback Callback to execute on sending completed, either successful or unsuccessful. - * @throws MQClientException if there is any client error. - * @throws RemotingException if there is any network-tier error. + * @throws MQClientException if there is any client error. + * @throws RemotingException if there is any network-tier error. * @throws InterruptedException if the sending thread is interrupted. */ @Override public void send(Message msg, MessageQueue mq, SendCallback sendCallback) throws MQClientException, RemotingException, InterruptedException { - this.defaultMQProducerImpl.send(msg, mq, sendCallback); + msg.setTopic(withNamespace(msg.getTopic())); + mq = queueWithNamespace(mq); + try { + if (this.getAutoBatch() && !(msg instanceof MessageBatch)) { + sendByAccumulator(msg, mq, sendCallback); + } else { + sendDirect(msg, mq, sendCallback); + } + } catch (MQBrokerException e) { + // ignore + } } /** * Same to {@link #send(Message, SendCallback)} with target message queue and send timeout specified. * - * @param msg Message to send. - * @param mq Target message queue. + * @param msg Message to send. + * @param mq Target message queue. * @param sendCallback Callback to execute on sending completed, either successful or unsuccessful. - * @param timeout Send timeout. - * @throws MQClientException if there is any client error. - * @throws RemotingException if there is any network-tier error. + * @param timeout Send timeout. + * @throws MQClientException if there is any client error. + * @throws RemotingException if there is any network-tier error. * @throws InterruptedException if the sending thread is interrupted. */ @Override public void send(Message msg, MessageQueue mq, SendCallback sendCallback, long timeout) throws MQClientException, RemotingException, InterruptedException { - this.defaultMQProducerImpl.send(msg, mq, sendCallback, timeout); + msg.setTopic(withNamespace(msg.getTopic())); + this.defaultMQProducerImpl.send(msg, queueWithNamespace(mq), sendCallback, timeout); } /** * Same to {@link #sendOneway(Message)} with target message queue specified. * * @param msg Message to send. - * @param mq Target message queue. - * @throws MQClientException if there is any client error. - * @throws RemotingException if there is any network-tier error. + * @param mq Target message queue. + * @throws MQClientException if there is any client error. + * @throws RemotingException if there is any network-tier error. * @throws InterruptedException if the sending thread is interrupted. */ @Override public void sendOneway(Message msg, MessageQueue mq) throws MQClientException, RemotingException, InterruptedException { - this.defaultMQProducerImpl.sendOneway(msg, mq); + msg.setTopic(withNamespace(msg.getTopic())); + this.defaultMQProducerImpl.sendOneway(msg, queueWithNamespace(mq)); } /** * Same to {@link #send(Message)} with message queue selector specified. * - * @param msg Message to send. + * @param msg Message to send. * @param selector Message queue selector, through which we get target message queue to deliver message to. - * @param arg Argument to work along with message queue selector. + * @param arg Argument to work along with message queue selector. * @return {@link SendResult} instance to inform senders details of the deliverable, say Message ID of the message, * {@link SendStatus} indicating broker storage/replication status, message queue sent to, etc. - * @throws MQClientException if there is any client error. - * @throws RemotingException if there is any network-tier error. - * @throws MQBrokerException if there is any error with broker. + * @throws MQClientException if there is any client error. + * @throws RemotingException if there is any network-tier error. + * @throws MQBrokerException if there is any error with broker. * @throws InterruptedException if the sending thread is interrupted. */ @Override public SendResult send(Message msg, MessageQueueSelector selector, Object arg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { - return this.defaultMQProducerImpl.send(msg, selector, arg); + msg.setTopic(withNamespace(msg.getTopic())); + MessageQueue mq = this.defaultMQProducerImpl.invokeMessageQueueSelector(msg, selector, arg, this.getSendMsgTimeout()); + mq = queueWithNamespace(mq); + if (this.getAutoBatch() && !(msg instanceof MessageBatch)) { + return sendByAccumulator(msg, mq, null); + } else { + return sendDirect(msg, mq, null); + } } /** * Same to {@link #send(Message, MessageQueueSelector, Object)} with send timeout specified. * - * @param msg Message to send. + * @param msg Message to send. * @param selector Message queue selector, through which we get target message queue to deliver message to. - * @param arg Argument to work along with message queue selector. - * @param timeout Send timeout. + * @param arg Argument to work along with message queue selector. + * @param timeout Send timeout. * @return {@link SendResult} instance to inform senders details of the deliverable, say Message ID of the message, * {@link SendStatus} indicating broker storage/replication status, message queue sent to, etc. - * @throws MQClientException if there is any client error. - * @throws RemotingException if there is any network-tier error. - * @throws MQBrokerException if there is any error with broker. + * @throws MQClientException if there is any client error. + * @throws RemotingException if there is any network-tier error. + * @throws MQBrokerException if there is any error with broker. * @throws InterruptedException if the sending thread is interrupted. */ @Override public SendResult send(Message msg, MessageQueueSelector selector, Object arg, long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + msg.setTopic(withNamespace(msg.getTopic())); return this.defaultMQProducerImpl.send(msg, selector, arg, timeout); } /** * Same to {@link #send(Message, SendCallback)} with message queue selector specified. * - * @param msg Message to send. - * @param selector Message selector through which to get target message queue. - * @param arg Argument used along with message queue selector. + * @param msg Message to send. + * @param selector Message selector through which to get target message queue. + * @param arg Argument used along with message queue selector. * @param sendCallback callback to execute on sending completion. - * @throws MQClientException if there is any client error. - * @throws RemotingException if there is any network-tier error. + * @throws MQClientException if there is any client error. + * @throws RemotingException if there is any network-tier error. * @throws InterruptedException if the sending thread is interrupted. */ @Override public void send(Message msg, MessageQueueSelector selector, Object arg, SendCallback sendCallback) throws MQClientException, RemotingException, InterruptedException { - this.defaultMQProducerImpl.send(msg, selector, arg, sendCallback); + msg.setTopic(withNamespace(msg.getTopic())); + try { + MessageQueue mq = this.defaultMQProducerImpl.invokeMessageQueueSelector(msg, selector, arg, this.getSendMsgTimeout()); + mq = queueWithNamespace(mq); + if (this.getAutoBatch() && !(msg instanceof MessageBatch)) { + sendByAccumulator(msg, mq, sendCallback); + } else { + sendDirect(msg, mq, sendCallback); + } + } catch (Throwable e) { + sendCallback.onException(e); + } } /** * Same to {@link #send(Message, MessageQueueSelector, Object, SendCallback)} with timeout specified. * - * @param msg Message to send. - * @param selector Message selector through which to get target message queue. - * @param arg Argument used along with message queue selector. + * @param msg Message to send. + * @param selector Message selector through which to get target message queue. + * @param arg Argument used along with message queue selector. * @param sendCallback callback to execute on sending completion. - * @param timeout Send timeout. - * @throws MQClientException if there is any client error. - * @throws RemotingException if there is any network-tier error. + * @param timeout Send timeout. + * @throws MQClientException if there is any client error. + * @throws RemotingException if there is any network-tier error. * @throws InterruptedException if the sending thread is interrupted. */ @Override public void send(Message msg, MessageQueueSelector selector, Object arg, SendCallback sendCallback, long timeout) throws MQClientException, RemotingException, InterruptedException { + msg.setTopic(withNamespace(msg.getTopic())); this.defaultMQProducerImpl.send(msg, selector, arg, sendCallback, timeout); } + public SendResult sendDirect(Message msg, MessageQueue mq, + SendCallback sendCallback) throws MQClientException, RemotingException, InterruptedException, MQBrokerException { + // send in sync mode + if (sendCallback == null) { + if (mq == null) { + return this.defaultMQProducerImpl.send(msg); + } else { + return this.defaultMQProducerImpl.send(msg, mq); + } + } else { + if (mq == null) { + this.defaultMQProducerImpl.send(msg, sendCallback); + } else { + this.defaultMQProducerImpl.send(msg, mq, sendCallback); + } + return null; + } + } + + public SendResult sendByAccumulator(Message msg, MessageQueue mq, + SendCallback sendCallback) throws MQClientException, RemotingException, InterruptedException, MQBrokerException { + // check whether it can batch + if (!canBatch(msg)) { + return sendDirect(msg, mq, sendCallback); + } else { + Validators.checkMessage(msg, this); + MessageClientIDSetter.setUniqID(msg); + if (sendCallback == null) { + return this.produceAccumulator.send(msg, mq, this); + } else { + this.produceAccumulator.send(msg, mq, sendCallback, this); + return null; + } + } + } + + /** + * Send request message in synchronous mode. This method returns only when the consumer consume the request message and reply a message.

+ * + * Warn: this method has internal retry-mechanism, that is, internal implementation will retry + * {@link #retryTimesWhenSendFailed} times before claiming failure. As a result, multiple messages may be potentially + * delivered to broker(s). It's up to the application developers to resolve potential duplication issue. + * + * @param msg request message to send + * @param timeout request timeout + * @return reply message + * @throws MQClientException if there is any client error. + * @throws RemotingException if there is any network-tier error. + * @throws MQBrokerException if there is any broker error. + * @throws InterruptedException if the thread is interrupted. + * @throws RequestTimeoutException if request timeout. + */ + @Override + public Message request(final Message msg, final long timeout) throws RequestTimeoutException, MQClientException, + RemotingException, MQBrokerException, InterruptedException { + msg.setTopic(withNamespace(msg.getTopic())); + return this.defaultMQProducerImpl.request(msg, timeout); + } + + /** + * Request asynchronously.

+ * This method returns immediately. On receiving reply message, requestCallback will be executed.

+ *

+ * Similar to {@link #request(Message, long)}, internal implementation would potentially retry up to {@link + * #retryTimesWhenSendAsyncFailed} times before claiming sending failure, which may yield message duplication and + * application developers are the one to resolve this potential issue. + * + * @param msg request message to send + * @param requestCallback callback to execute on request completion. + * @param timeout request timeout + * @throws MQClientException if there is any client error. + * @throws RemotingException if there is any network-tier error. + * @throws InterruptedException if the thread is interrupted. + * @throws MQBrokerException if there is any broker error. + */ + @Override + public void request(final Message msg, final RequestCallback requestCallback, final long timeout) + throws MQClientException, RemotingException, InterruptedException, MQBrokerException { + msg.setTopic(withNamespace(msg.getTopic())); + this.defaultMQProducerImpl.request(msg, requestCallback, timeout); + } + + /** + * Same to {@link #request(Message, long)} with message queue selector specified. + * + * @param msg request message to send + * @param selector message queue selector, through which we get target message queue to deliver message to. + * @param arg argument to work along with message queue selector. + * @param timeout timeout of request. + * @return reply message + * @throws MQClientException if there is any client error. + * @throws RemotingException if there is any network-tier error. + * @throws MQBrokerException if there is any broker error. + * @throws InterruptedException if the thread is interrupted. + * @throws RequestTimeoutException if request timeout. + */ + @Override + public Message request(final Message msg, final MessageQueueSelector selector, final Object arg, + final long timeout) throws MQClientException, RemotingException, MQBrokerException, + InterruptedException, RequestTimeoutException { + msg.setTopic(withNamespace(msg.getTopic())); + return this.defaultMQProducerImpl.request(msg, selector, arg, timeout); + } + + /** + * Same to {@link #request(Message, RequestCallback, long)} with target message selector specified. + * + * @param msg requst message to send + * @param selector message queue selector, through which we get target message queue to deliver message to. + * @param arg argument to work along with message queue selector. + * @param requestCallback callback to execute on request completion. + * @param timeout timeout of request. + * @throws MQClientException if there is any client error. + * @throws RemotingException if there is any network-tier error. + * @throws InterruptedException if the thread is interrupted. + * @throws MQBrokerException if there is any broker error. + */ + @Override + public void request(final Message msg, final MessageQueueSelector selector, final Object arg, + final RequestCallback requestCallback, final long timeout) throws MQClientException, RemotingException, + InterruptedException, MQBrokerException { + msg.setTopic(withNamespace(msg.getTopic())); + this.defaultMQProducerImpl.request(msg, selector, arg, requestCallback, timeout); + } + + /** + * Same to {@link #request(Message, long)} with target message queue specified in addition. + * + * @param msg request message to send + * @param mq target message queue. + * @param timeout request timeout + * @throws MQClientException if there is any client error. + * @throws RemotingException if there is any network-tier error. + * @throws MQBrokerException if there is any broker error. + * @throws InterruptedException if the thread is interrupted. + * @throws RequestTimeoutException if request timeout. + */ + @Override + public Message request(final Message msg, final MessageQueue mq, final long timeout) + throws MQClientException, RemotingException, MQBrokerException, InterruptedException, RequestTimeoutException { + msg.setTopic(withNamespace(msg.getTopic())); + return this.defaultMQProducerImpl.request(msg, mq, timeout); + } + + /** + * Same to {@link #request(Message, RequestCallback, long)} with target message queue specified. + * + * @param msg request message to send + * @param mq target message queue. + * @param requestCallback callback to execute on request completion. + * @param timeout timeout of request. + * @throws MQClientException if there is any client error. + * @throws RemotingException if there is any network-tier error. + * @throws InterruptedException if the thread is interrupted. + * @throws MQBrokerException if there is any broker error. + */ + @Override + public void request(final Message msg, final MessageQueue mq, final RequestCallback requestCallback, long timeout) + throws MQClientException, RemotingException, InterruptedException, MQBrokerException { + msg.setTopic(withNamespace(msg.getTopic())); + this.defaultMQProducerImpl.request(msg, mq, requestCallback, timeout); + } + /** * Same to {@link #sendOneway(Message)} with message queue selector specified. * - * @param msg Message to send. + * @param msg Message to send. * @param selector Message queue selector, through which to determine target message queue to deliver message - * @param arg Argument used along with message queue selector. - * @throws MQClientException if there is any client error. - * @throws RemotingException if there is any network-tier error. + * @param arg Argument used along with message queue selector. + * @throws MQClientException if there is any client error. + * @throws RemotingException if there is any network-tier error. * @throws InterruptedException if the sending thread is interrupted. */ @Override public void sendOneway(Message msg, MessageQueueSelector selector, Object arg) throws MQClientException, RemotingException, InterruptedException { + msg.setTopic(withNamespace(msg.getTopic())); this.defaultMQProducerImpl.sendOneway(msg, selector, arg); } /** - * This method is to send transactional messages. + * This method is used to send transactional messages. * * @param msg Transactional message to send. - * @param tranExecuter local transaction executor. * @param arg Argument used along with local transaction executor. * @return Transaction result. - * @throws MQClientException if there is any client error. + * @throws MQClientException */ @Override - public TransactionSendResult sendMessageInTransaction(Message msg, LocalTransactionExecuter tranExecuter, - final Object arg) - throws MQClientException { + public TransactionSendResult sendMessageInTransaction(Message msg, + Object arg) throws MQClientException { throw new RuntimeException("sendMessageInTransaction not implement, please use TransactionMQProducer class"); } /** - * Create a topic on broker. + * This method will be removed in a certain version after April 5, 2020, so please do not use this method. * - * @param key accesskey - * @param newTopic topic name - * @param queueNum topic's queue number + * @param key accessKey + * @param newTopic topic name + * @param queueNum topic's queue number + * @param attributes * @throws MQClientException if there is any client error. */ + @Deprecated @Override - public void createTopic(String key, String newTopic, int queueNum) throws MQClientException { - createTopic(key, newTopic, queueNum, 0); + public void createTopic(String key, String newTopic, int queueNum, + Map attributes) throws MQClientException { + createTopic(key, withNamespace(newTopic), queueNum, 0, null); } /** - * Create a topic on broker. + * Create a topic on broker. This method will be removed in a certain version after April 5, 2020, so please do not + * use this method. * - * @param key accesskey - * @param newTopic topic name - * @param queueNum topic's queue number + * @param key accessKey + * @param newTopic topic name + * @param queueNum topic's queue number * @param topicSysFlag topic system flag + * @param attributes * @throws MQClientException if there is any client error. */ + @Deprecated @Override - public void createTopic(String key, String newTopic, int queueNum, int topicSysFlag) throws MQClientException { - this.defaultMQProducerImpl.createTopic(key, newTopic, queueNum, topicSysFlag); + public void createTopic(String key, String newTopic, int queueNum, int topicSysFlag, + Map attributes) throws MQClientException { + this.defaultMQProducerImpl.createTopic(key, withNamespace(newTopic), queueNum, topicSysFlag); } /** * Search consume queue offset of the given time stamp. * - * @param mq Instance of MessageQueue + * @param mq Instance of MessageQueue * @param timestamp from when in milliseconds. * @return Consume queue offset. * @throws MQClientException if there is any client error. */ @Override public long searchOffset(MessageQueue mq, long timestamp) throws MQClientException { - return this.defaultMQProducerImpl.searchOffset(mq, timestamp); + return this.defaultMQProducerImpl.searchOffset(queueWithNamespace(mq), timestamp); } /** * Query maximum offset of the given message queue. + *

+ * This method will be removed in a certain version after April 5, 2020, so please do not use this method. * * @param mq Instance of MessageQueue * @return maximum offset of the given consume queue. * @throws MQClientException if there is any client error. */ + @Deprecated @Override public long maxOffset(MessageQueue mq) throws MQClientException { - return this.defaultMQProducerImpl.maxOffset(mq); + return this.defaultMQProducerImpl.maxOffset(queueWithNamespace(mq)); } /** * Query minimum offset of the given message queue. + *

+ * This method will be removed in a certain version after April 5, 2020, so please do not use this method. * * @param mq Instance of MessageQueue * @return minimum offset of the given message queue. * @throws MQClientException if there is any client error. */ + @Deprecated @Override public long minOffset(MessageQueue mq) throws MQClientException { - return this.defaultMQProducerImpl.minOffset(mq); + return this.defaultMQProducerImpl.minOffset(queueWithNamespace(mq)); } /** - * Query earliest message store time. + * Query the earliest message store time. + *

+ * This method will be removed in a certain version after April 5, 2020, so please do not use this method. * * @param mq Instance of MessageQueue * @return earliest message store time. * @throws MQClientException if there is any client error. */ + @Deprecated @Override public long earliestMsgStoreTime(MessageQueue mq) throws MQClientException { - return this.defaultMQProducerImpl.earliestMsgStoreTime(mq); + return this.defaultMQProducerImpl.earliestMsgStoreTime(queueWithNamespace(mq)); } /** * Query message of the given offset message ID. + *

+ * This method will be removed in a certain version after April 5, 2020, so please do not use this method. * * @param offsetMsgId message id * @return Message specified. - * @throws MQBrokerException if there is any broker error. - * @throws MQClientException if there is any client error. - * @throws RemotingException if there is any network-tier error. + * @throws MQBrokerException if there is any broker error. + * @throws MQClientException if there is any client error. + * @throws RemotingException if there is any network-tier error. * @throws InterruptedException if the sending thread is interrupted. */ + @Deprecated @Override public MessageExt viewMessage( String offsetMsgId) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { @@ -570,42 +1023,47 @@ public MessageExt viewMessage( /** * Query message by key. + *

+ * This method will be removed in a certain version after April 5, 2020, so please do not use this method. * - * @param topic message topic - * @param key message key index word + * @param topic message topic + * @param key message key index word * @param maxNum max message number - * @param begin from when - * @param end to when + * @param begin from when + * @param end to when * @return QueryResult instance contains matched messages. - * @throws MQClientException if there is any client error. + * @throws MQClientException if there is any client error. * @throws InterruptedException if the thread is interrupted. */ + @Deprecated @Override public QueryResult queryMessage(String topic, String key, int maxNum, long begin, long end) throws MQClientException, InterruptedException { - return this.defaultMQProducerImpl.queryMessage(topic, key, maxNum, begin, end); + return this.defaultMQProducerImpl.queryMessage(withNamespace(topic), key, maxNum, begin, end); } /** * Query message of the given message ID. + *

+ * This method will be removed in a certain version after April 5, 2020, so please do not use this method. * * @param topic Topic * @param msgId Message ID * @return Message specified. - * @throws MQBrokerException if there is any broker error. - * @throws MQClientException if there is any client error. - * @throws RemotingException if there is any network-tier error. + * @throws MQBrokerException if there is any broker error. + * @throws MQClientException if there is any client error. + * @throws RemotingException if there is any network-tier error. * @throws InterruptedException if the sending thread is interrupted. */ + @Deprecated @Override public MessageExt viewMessage(String topic, String msgId) throws RemotingException, MQBrokerException, InterruptedException, MQClientException { try { - MessageId oldMsgId = MessageDecoder.decodeMessageId(msgId); return this.viewMessage(msgId); - } catch (Exception e) { + } catch (Exception ignored) { } - return this.defaultMQProducerImpl.queryMessageByUniqKey(topic, msgId); + return this.defaultMQProducerImpl.queryMessageByUniqKey(withNamespace(topic), msgId); } @Override @@ -632,9 +1090,33 @@ public SendResult send(Collection msgs, MessageQueue messageQueue, return this.defaultMQProducerImpl.send(batch(msgs), messageQueue, timeout); } + @Override + public void send(Collection msgs, + SendCallback sendCallback) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + this.defaultMQProducerImpl.send(batch(msgs), sendCallback); + } + + @Override + public void send(Collection msgs, SendCallback sendCallback, + long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + this.defaultMQProducerImpl.send(batch(msgs), sendCallback, timeout); + } + + @Override + public void send(Collection msgs, MessageQueue mq, + SendCallback sendCallback) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + this.defaultMQProducerImpl.send(batch(msgs), queueWithNamespace(mq), sendCallback); + } + + @Override + public void send(Collection msgs, MessageQueue mq, + SendCallback sendCallback, + long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { + this.defaultMQProducerImpl.send(batch(msgs), queueWithNamespace(mq), sendCallback, timeout); + } + /** * Sets an Executor to be used for executing callback methods. - * If the Executor is not set, {@link NettyRemotingClient#publicExecutor} will be used. * * @param callbackExecutor the instance of Executor */ @@ -642,6 +1124,24 @@ public void setCallbackExecutor(final ExecutorService callbackExecutor) { this.defaultMQProducerImpl.setCallbackExecutor(callbackExecutor); } + /** + * Sets an Executor to be used for executing asynchronous send. + * + * @param asyncSenderExecutor the instance of Executor + */ + public void setAsyncSenderExecutor(final ExecutorService asyncSenderExecutor) { + this.defaultMQProducerImpl.setAsyncSenderExecutor(asyncSenderExecutor); + } + + /** + * Add response code for retrying. + * + * @param responseCode response code, {@link ResponseCode} + */ + public void addRetryResponseCode(int responseCode) { + this.retryResponseCodes.add(responseCode); + } + private MessageBatch batch(Collection msgs) throws MQClientException { MessageBatch msgBatch; try { @@ -649,14 +1149,73 @@ private MessageBatch batch(Collection msgs) throws MQClientException { for (Message message : msgBatch) { Validators.checkMessage(message, this); MessageClientIDSetter.setUniqID(message); + message.setTopic(withNamespace(message.getTopic())); } + MessageClientIDSetter.setUniqID(msgBatch); msgBatch.setBody(msgBatch.encode()); } catch (Exception e) { throw new MQClientException("Failed to initiate the MessageBatch", e); } + msgBatch.setTopic(withNamespace(msgBatch.getTopic())); return msgBatch; } + public int getBatchMaxDelayMs() { + if (this.produceAccumulator == null) { + return 0; + } + return produceAccumulator.getBatchMaxDelayMs(); + } + + public void batchMaxDelayMs(int holdMs) { + if (this.produceAccumulator == null) { + throw new UnsupportedOperationException("The currently constructed producer does not support autoBatch"); + } + this.produceAccumulator.batchMaxDelayMs(holdMs); + } + + public long getBatchMaxBytes() { + if (this.produceAccumulator == null) { + return 0; + } + return produceAccumulator.getBatchMaxBytes(); + } + + public void batchMaxBytes(long holdSize) { + if (this.produceAccumulator == null) { + throw new UnsupportedOperationException("The currently constructed producer does not support autoBatch"); + } + this.produceAccumulator.batchMaxBytes(holdSize); + } + + public long getTotalBatchMaxBytes() { + if (this.produceAccumulator == null) { + return 0; + } + return produceAccumulator.getTotalBatchMaxBytes(); + } + + public void totalBatchMaxBytes(long totalHoldSize) { + if (this.produceAccumulator == null) { + throw new UnsupportedOperationException("The currently constructed producer does not support autoBatch"); + } + this.produceAccumulator.totalBatchMaxBytes(totalHoldSize); + } + + public boolean getAutoBatch() { + if (this.produceAccumulator == null) { + return false; + } + return this.autoBatch; + } + + public void setAutoBatch(boolean autoBatch) { + if (this.produceAccumulator == null) { + throw new UnsupportedOperationException("The currently constructed producer does not support autoBatch"); + } + this.autoBatch = autoBatch; + } + public String getProducerGroup() { return producerGroup; } @@ -689,6 +1248,7 @@ public void setCompressMsgBodyOverHowmuch(int compressMsgBodyOverHowmuch) { this.compressMsgBodyOverHowmuch = compressMsgBodyOverHowmuch; } + @Deprecated public DefaultMQProducerImpl getDefaultMQProducerImpl() { return defaultMQProducerImpl; } @@ -764,4 +1324,52 @@ public int getRetryTimesWhenSendAsyncFailed() { public void setRetryTimesWhenSendAsyncFailed(final int retryTimesWhenSendAsyncFailed) { this.retryTimesWhenSendAsyncFailed = retryTimesWhenSendAsyncFailed; } + + public TraceDispatcher getTraceDispatcher() { + return traceDispatcher; + } + + public Set getRetryResponseCodes() { + return retryResponseCodes; + } + + public boolean isEnableBackpressureForAsyncMode() { + return enableBackpressureForAsyncMode; + } + + public void setEnableBackpressureForAsyncMode(boolean enableBackpressureForAsyncMode) { + this.enableBackpressureForAsyncMode = enableBackpressureForAsyncMode; + } + + public int getBackPressureForAsyncSendNum() { + return backPressureForAsyncSendNum; + } + + public void setBackPressureForAsyncSendNum(int backPressureForAsyncSendNum) { + this.backPressureForAsyncSendNum = backPressureForAsyncSendNum; + defaultMQProducerImpl.setSemaphoreAsyncSendNum(backPressureForAsyncSendNum); + } + + public int getBackPressureForAsyncSendSize() { + return backPressureForAsyncSendSize; + } + + public void setBackPressureForAsyncSendSize(int backPressureForAsyncSendSize) { + this.backPressureForAsyncSendSize = backPressureForAsyncSendSize; + defaultMQProducerImpl.setSemaphoreAsyncSendSize(backPressureForAsyncSendSize); + } + + public List getTopics() { + return topics; + } + + public void setTopics(List topics) { + this.topics = topics; + } + + @Override + public void setStartDetectorEnable(boolean startDetectorEnable) { + super.setStartDetectorEnable(startDetectorEnable); + this.defaultMQProducerImpl.getMqFaultStrategy().setStartDetectorEnable(startDetectorEnable); + } } diff --git a/client/src/main/java/org/apache/rocketmq/client/producer/LocalTransactionExecuter.java b/client/src/main/java/org/apache/rocketmq/client/producer/LocalTransactionExecuter.java deleted file mode 100644 index 80b5546925f..00000000000 --- a/client/src/main/java/org/apache/rocketmq/client/producer/LocalTransactionExecuter.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.client.producer; - -import org.apache.rocketmq.common.message.Message; - -public interface LocalTransactionExecuter { - LocalTransactionState executeLocalTransactionBranch(final Message msg, final Object arg); -} diff --git a/client/src/main/java/org/apache/rocketmq/client/producer/MQProducer.java b/client/src/main/java/org/apache/rocketmq/client/producer/MQProducer.java index 14caf6ffac9..8bd30e98d7b 100644 --- a/client/src/main/java/org/apache/rocketmq/client/producer/MQProducer.java +++ b/client/src/main/java/org/apache/rocketmq/client/producer/MQProducer.java @@ -21,6 +21,7 @@ import org.apache.rocketmq.client.MQAdmin; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.exception.RequestTimeoutException; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageQueue; import org.apache.rocketmq.remoting.exception.RemotingException; @@ -39,7 +40,7 @@ SendResult send(final Message msg, final long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException; void send(final Message msg, final SendCallback sendCallback) throws MQClientException, - RemotingException, InterruptedException; + RemotingException, InterruptedException, MQBrokerException; void send(final Message msg, final SendCallback sendCallback, final long timeout) throws MQClientException, RemotingException, InterruptedException; @@ -81,7 +82,7 @@ void sendOneway(final Message msg, final MessageQueueSelector selector, final Ob throws MQClientException, RemotingException, InterruptedException; TransactionSendResult sendMessageInTransaction(final Message msg, - final LocalTransactionExecuter tranExecuter, final Object arg) throws MQClientException; + final Object arg) throws MQClientException; //for batch SendResult send(final Collection msgs) throws MQClientException, RemotingException, MQBrokerException, @@ -95,4 +96,42 @@ SendResult send(final Collection msgs, final MessageQueue mq) throws MQ SendResult send(final Collection msgs, final MessageQueue mq, final long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException; + + void send(final Collection msgs, + final SendCallback sendCallback) throws MQClientException, RemotingException, MQBrokerException, + InterruptedException; + + void send(final Collection msgs, final SendCallback sendCallback, + final long timeout) throws MQClientException, RemotingException, + MQBrokerException, InterruptedException; + + void send(final Collection msgs, final MessageQueue mq, + final SendCallback sendCallback) throws MQClientException, RemotingException, + MQBrokerException, InterruptedException; + + void send(final Collection msgs, final MessageQueue mq, final SendCallback sendCallback, + final long timeout) throws MQClientException, + RemotingException, MQBrokerException, InterruptedException; + + //for rpc + Message request(final Message msg, final long timeout) throws RequestTimeoutException, MQClientException, + RemotingException, MQBrokerException, InterruptedException; + + void request(final Message msg, final RequestCallback requestCallback, final long timeout) + throws MQClientException, RemotingException, InterruptedException, MQBrokerException; + + Message request(final Message msg, final MessageQueueSelector selector, final Object arg, + final long timeout) throws RequestTimeoutException, MQClientException, RemotingException, MQBrokerException, + InterruptedException; + + void request(final Message msg, final MessageQueueSelector selector, final Object arg, + final RequestCallback requestCallback, + final long timeout) throws MQClientException, RemotingException, + InterruptedException, MQBrokerException; + + Message request(final Message msg, final MessageQueue mq, final long timeout) + throws RequestTimeoutException, MQClientException, RemotingException, MQBrokerException, InterruptedException; + + void request(final Message msg, final MessageQueue mq, final RequestCallback requestCallback, long timeout) + throws MQClientException, RemotingException, MQBrokerException, InterruptedException; } diff --git a/client/src/main/java/org/apache/rocketmq/client/producer/ProduceAccumulator.java b/client/src/main/java/org/apache/rocketmq/client/producer/ProduceAccumulator.java new file mode 100644 index 00000000000..46dfcf71d29 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/producer/ProduceAccumulator.java @@ -0,0 +1,510 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client.producer; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageBatch; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.exception.RemotingException; + +public class ProduceAccumulator { + // totalHoldSize normal value + private long totalHoldSize = 32 * 1024 * 1024; + // holdSize normal value + private long holdSize = 32 * 1024; + // holdMs normal value + private int holdMs = 10; + private final Logger log = LoggerFactory.getLogger(DefaultMQProducer.class); + private final GuardForSyncSendService guardThreadForSyncSend; + private final GuardForAsyncSendService guardThreadForAsyncSend; + private Map syncSendBatchs = new ConcurrentHashMap(); + private Map asyncSendBatchs = new ConcurrentHashMap(); + private AtomicLong currentlyHoldSize = new AtomicLong(0); + private final String instanceName; + + public ProduceAccumulator(String instanceName) { + this.instanceName = instanceName; + this.guardThreadForSyncSend = new GuardForSyncSendService(this.instanceName); + this.guardThreadForAsyncSend = new GuardForAsyncSendService(this.instanceName); + } + + private class GuardForSyncSendService extends ServiceThread { + private final String serviceName; + + public GuardForSyncSendService(String clientInstanceName) { + serviceName = String.format("Client_%s_GuardForSyncSend", clientInstanceName); + } + + @Override public String getServiceName() { + return serviceName; + } + + @Override public void run() { + log.info(this.getServiceName() + " service started"); + + while (!this.isStopped()) { + try { + this.doWork(); + } catch (Exception e) { + log.warn(this.getServiceName() + " service has exception. ", e); + } + } + + log.info(this.getServiceName() + " service end"); + } + + private void doWork() throws InterruptedException { + Collection values = syncSendBatchs.values(); + final int sleepTime = Math.max(1, holdMs / 2); + for (MessageAccumulation v : values) { + v.wakeup(); + synchronized (v) { + synchronized (v.closed) { + if (v.messagesSize.get() == 0) { + v.closed.set(true); + syncSendBatchs.remove(v.aggregateKey, v); + } else { + v.notify(); + } + } + } + } + Thread.sleep(sleepTime); + } + } + + private class GuardForAsyncSendService extends ServiceThread { + private final String serviceName; + + public GuardForAsyncSendService(String clientInstanceName) { + serviceName = String.format("Client_%s_GuardForAsyncSend", clientInstanceName); + } + + @Override public String getServiceName() { + return serviceName; + } + + @Override public void run() { + log.info(this.getServiceName() + " service started"); + + while (!this.isStopped()) { + try { + this.doWork(); + } catch (Exception e) { + log.warn(this.getServiceName() + " service has exception. ", e); + } + } + + log.info(this.getServiceName() + " service end"); + } + + private void doWork() throws Exception { + Collection values = asyncSendBatchs.values(); + final int sleepTime = Math.max(1, holdMs / 2); + for (MessageAccumulation v : values) { + if (v.readyToSend()) { + v.send(null); + } + synchronized (v.closed) { + if (v.messagesSize.get() == 0) { + v.closed.set(true); + asyncSendBatchs.remove(v.aggregateKey, v); + } + } + } + Thread.sleep(sleepTime); + } + } + + void start() { + guardThreadForSyncSend.start(); + guardThreadForAsyncSend.start(); + } + + void shutdown() { + guardThreadForSyncSend.shutdown(); + guardThreadForAsyncSend.shutdown(); + } + + int getBatchMaxDelayMs() { + return holdMs; + } + + void batchMaxDelayMs(int holdMs) { + if (holdMs <= 0 || holdMs > 30 * 1000) { + throw new IllegalArgumentException(String.format("batchMaxDelayMs expect between 1ms and 30s, but get %d!", holdMs)); + } + this.holdMs = holdMs; + } + + long getBatchMaxBytes() { + return holdSize; + } + + void batchMaxBytes(long holdSize) { + if (holdSize <= 0 || holdSize > 2 * 1024 * 1024) { + throw new IllegalArgumentException(String.format("batchMaxBytes expect between 1B and 2MB, but get %d!", holdSize)); + } + this.holdSize = holdSize; + } + + long getTotalBatchMaxBytes() { + return holdSize; + } + + void totalBatchMaxBytes(long totalHoldSize) { + if (totalHoldSize <= 0) { + throw new IllegalArgumentException(String.format("totalBatchMaxBytes must bigger then 0, but get %d!", totalHoldSize)); + } + this.totalHoldSize = totalHoldSize; + } + + private MessageAccumulation getOrCreateSyncSendBatch(AggregateKey aggregateKey, + DefaultMQProducer defaultMQProducer) { + MessageAccumulation batch = syncSendBatchs.get(aggregateKey); + if (batch != null) { + return batch; + } + batch = new MessageAccumulation(aggregateKey, defaultMQProducer); + MessageAccumulation previous = syncSendBatchs.putIfAbsent(aggregateKey, batch); + + return previous == null ? batch : previous; + } + + private MessageAccumulation getOrCreateAsyncSendBatch(AggregateKey aggregateKey, + DefaultMQProducer defaultMQProducer) { + MessageAccumulation batch = asyncSendBatchs.get(aggregateKey); + if (batch != null) { + return batch; + } + batch = new MessageAccumulation(aggregateKey, defaultMQProducer); + MessageAccumulation previous = asyncSendBatchs.putIfAbsent(aggregateKey, batch); + + return previous == null ? batch : previous; + } + + SendResult send(Message msg, + DefaultMQProducer defaultMQProducer) throws InterruptedException, MQBrokerException, RemotingException, MQClientException { + AggregateKey partitionKey = new AggregateKey(msg); + while (true) { + MessageAccumulation batch = getOrCreateSyncSendBatch(partitionKey, defaultMQProducer); + int index = batch.add(msg); + if (index == -1) { + syncSendBatchs.remove(partitionKey, batch); + } else { + return batch.sendResults[index]; + } + } + } + + SendResult send(Message msg, MessageQueue mq, + DefaultMQProducer defaultMQProducer) throws InterruptedException, MQBrokerException, RemotingException, MQClientException { + AggregateKey partitionKey = new AggregateKey(msg, mq); + while (true) { + MessageAccumulation batch = getOrCreateSyncSendBatch(partitionKey, defaultMQProducer); + int index = batch.add(msg); + if (index == -1) { + syncSendBatchs.remove(partitionKey, batch); + } else { + return batch.sendResults[index]; + } + } + } + + void send(Message msg, SendCallback sendCallback, + DefaultMQProducer defaultMQProducer) throws InterruptedException, RemotingException, MQClientException { + AggregateKey partitionKey = new AggregateKey(msg); + while (true) { + MessageAccumulation batch = getOrCreateAsyncSendBatch(partitionKey, defaultMQProducer); + if (!batch.add(msg, sendCallback)) { + asyncSendBatchs.remove(partitionKey, batch); + } else { + return; + } + } + } + + void send(Message msg, MessageQueue mq, + SendCallback sendCallback, + DefaultMQProducer defaultMQProducer) throws InterruptedException, RemotingException, MQClientException { + AggregateKey partitionKey = new AggregateKey(msg, mq); + while (true) { + MessageAccumulation batch = getOrCreateAsyncSendBatch(partitionKey, defaultMQProducer); + if (!batch.add(msg, sendCallback)) { + asyncSendBatchs.remove(partitionKey, batch); + } else { + return; + } + } + } + + boolean tryAddMessage(Message message) { + synchronized (currentlyHoldSize) { + if (currentlyHoldSize.get() < totalHoldSize) { + currentlyHoldSize.addAndGet(message.getBody().length); + return true; + } else { + return false; + } + } + } + + private class AggregateKey { + public String topic = null; + public MessageQueue mq = null; + public boolean waitStoreMsgOK = false; + public String tag = null; + + public AggregateKey(Message message) { + this(message.getTopic(), null, message.isWaitStoreMsgOK(), message.getTags()); + } + + public AggregateKey(Message message, MessageQueue mq) { + this(message.getTopic(), mq, message.isWaitStoreMsgOK(), message.getTags()); + } + + public AggregateKey(String topic, MessageQueue mq, boolean waitStoreMsgOK, String tag) { + this.topic = topic; + this.mq = mq; + this.waitStoreMsgOK = waitStoreMsgOK; + this.tag = tag; + } + + @Override public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + AggregateKey key = (AggregateKey) o; + return waitStoreMsgOK == key.waitStoreMsgOK && topic.equals(key.topic) && Objects.equals(mq, key.mq) && Objects.equals(tag, key.tag); + } + + @Override public int hashCode() { + return Objects.hash(topic, mq, waitStoreMsgOK, tag); + } + } + + private class MessageAccumulation { + private final DefaultMQProducer defaultMQProducer; + private LinkedList messages; + private LinkedList sendCallbacks; + private Set keys; + private AtomicBoolean closed; + private SendResult[] sendResults; + private AggregateKey aggregateKey; + private AtomicInteger messagesSize; + private int count; + private long createTime; + + public MessageAccumulation(AggregateKey aggregateKey, DefaultMQProducer defaultMQProducer) { + this.defaultMQProducer = defaultMQProducer; + this.messages = new LinkedList(); + this.sendCallbacks = new LinkedList(); + this.keys = new HashSet(); + this.closed = new AtomicBoolean(false); + this.messagesSize = new AtomicInteger(0); + this.aggregateKey = aggregateKey; + this.count = 0; + this.createTime = System.currentTimeMillis(); + } + + private boolean readyToSend() { + if (this.messagesSize.get() > holdSize + || System.currentTimeMillis() >= this.createTime + holdMs) { + return true; + } + return false; + } + + public int add( + Message msg) throws InterruptedException, MQBrokerException, RemotingException, MQClientException { + int ret = -1; + synchronized (this.closed) { + if (this.closed.get()) { + return ret; + } + ret = this.count++; + this.messages.add(msg); + messagesSize.addAndGet(msg.getBody().length); + String msgKeys = msg.getKeys(); + if (msgKeys != null) { + this.keys.addAll(Arrays.asList(msgKeys.split(MessageConst.KEY_SEPARATOR))); + } + } + synchronized (this) { + while (!this.closed.get()) { + if (readyToSend()) { + this.send(); + break; + } else { + this.wait(); + } + } + return ret; + } + } + + public boolean add(Message msg, + SendCallback sendCallback) throws InterruptedException, RemotingException, MQClientException { + synchronized (this.closed) { + if (this.closed.get()) { + return false; + } + this.count++; + this.messages.add(msg); + this.sendCallbacks.add(sendCallback); + messagesSize.getAndAdd(msg.getBody().length); + } + if (readyToSend()) { + this.send(sendCallback); + } + return true; + + } + + public synchronized void wakeup() { + if (this.closed.get()) { + return; + } + this.notify(); + } + + private MessageBatch batch() { + MessageBatch messageBatch = new MessageBatch(this.messages); + messageBatch.setTopic(this.aggregateKey.topic); + messageBatch.setWaitStoreMsgOK(this.aggregateKey.waitStoreMsgOK); + messageBatch.setKeys(this.keys); + messageBatch.setTags(this.aggregateKey.tag); + MessageClientIDSetter.setUniqID(messageBatch); + messageBatch.setBody(MessageDecoder.encodeMessages(this.messages)); + return messageBatch; + } + + private void splitSendResults(SendResult sendResult) { + if (sendResult == null) { + throw new IllegalArgumentException("sendResult is null"); + } + boolean isBatchConsumerQueue = !sendResult.getMsgId().contains(","); + this.sendResults = new SendResult[this.count]; + if (!isBatchConsumerQueue) { + String[] msgIds = sendResult.getMsgId().split(","); + String[] offsetMsgIds = sendResult.getOffsetMsgId().split(","); + if (offsetMsgIds.length != this.count || msgIds.length != this.count) { + throw new IllegalArgumentException("sendResult is illegal"); + } + for (int i = 0; i < this.count; i++) { + this.sendResults[i] = new SendResult(sendResult.getSendStatus(), msgIds[i], + sendResult.getMessageQueue(), sendResult.getQueueOffset() + i, + sendResult.getTransactionId(), offsetMsgIds[i], sendResult.getRegionId()); + } + } else { + for (int i = 0; i < this.count; i++) { + this.sendResults[i] = sendResult; + } + } + } + + private void send() throws InterruptedException, MQClientException, MQBrokerException, RemotingException { + synchronized (this.closed) { + if (this.closed.getAndSet(true)) { + return; + } + } + MessageBatch messageBatch = this.batch(); + SendResult sendResult = null; + try { + if (defaultMQProducer != null) { + sendResult = defaultMQProducer.sendDirect(messageBatch, aggregateKey.mq, null); + this.splitSendResults(sendResult); + } else { + throw new IllegalArgumentException("defaultMQProducer is null, can not send message"); + } + } finally { + currentlyHoldSize.addAndGet(-messagesSize.get()); + this.notifyAll(); + } + } + + private void send(SendCallback sendCallback) { + synchronized (this.closed) { + if (this.closed.getAndSet(true)) { + return; + } + } + MessageBatch messageBatch = this.batch(); + SendResult sendResult = null; + try { + if (defaultMQProducer != null) { + final int size = messagesSize.get(); + defaultMQProducer.sendDirect(messageBatch, aggregateKey.mq, new SendCallback() { + @Override public void onSuccess(SendResult sendResult) { + try { + splitSendResults(sendResult); + int i = 0; + Iterator it = sendCallbacks.iterator(); + while (it.hasNext()) { + SendCallback v = it.next(); + v.onSuccess(sendResults[i++]); + } + if (i != count) { + throw new IllegalArgumentException("sendResult is illegal"); + } + currentlyHoldSize.addAndGet(-size); + } catch (Exception e) { + onException(e); + } + } + + @Override public void onException(Throwable e) { + for (SendCallback v : sendCallbacks) { + v.onException(e); + } + currentlyHoldSize.addAndGet(-size); + } + }); + } else { + throw new IllegalArgumentException("defaultMQProducer is null, can not send message"); + } + } catch (Exception e) { + for (SendCallback v : sendCallbacks) { + v.onException(e); + } + } + } + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/producer/RequestCallback.java b/client/src/main/java/org/apache/rocketmq/client/producer/RequestCallback.java new file mode 100644 index 00000000000..3107ba57d6e --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/producer/RequestCallback.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client.producer; + +import org.apache.rocketmq.common.message.Message; + +public interface RequestCallback { + void onSuccess(final Message message); + + void onException(final Throwable e); +} diff --git a/client/src/main/java/org/apache/rocketmq/client/producer/RequestFutureHolder.java b/client/src/main/java/org/apache/rocketmq/client/producer/RequestFutureHolder.java new file mode 100644 index 00000000000..00f5bb6e6ea --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/producer/RequestFutureHolder.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client.producer; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.apache.rocketmq.client.common.ClientErrorCode; +import org.apache.rocketmq.client.exception.RequestTimeoutException; +import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class RequestFutureHolder { + private static final Logger log = LoggerFactory.getLogger(RequestFutureHolder.class); + private static final RequestFutureHolder INSTANCE = new RequestFutureHolder(); + private ConcurrentHashMap requestFutureTable = new ConcurrentHashMap<>(); + private final Set producerSet = new HashSet<>(); + private ScheduledExecutorService scheduledExecutorService = null; + + public ConcurrentHashMap getRequestFutureTable() { + return requestFutureTable; + } + + private void scanExpiredRequest() { + final List rfList = new LinkedList<>(); + Iterator> it = requestFutureTable.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry next = it.next(); + RequestResponseFuture rep = next.getValue(); + + if (rep.isTimeout()) { + it.remove(); + rfList.add(rep); + log.warn("remove timeout request, CorrelationId={}" + rep.getCorrelationId()); + } + } + + for (RequestResponseFuture rf : rfList) { + try { + Throwable cause = new RequestTimeoutException(ClientErrorCode.REQUEST_TIMEOUT_EXCEPTION, "request timeout, no reply message."); + rf.setCause(cause); + rf.executeRequestCallback(); + } catch (Throwable e) { + log.warn("scanResponseTable, operationComplete Exception", e); + } + } + } + + public synchronized void startScheduledTask(DefaultMQProducerImpl producer) { + this.producerSet.add(producer); + if (null == scheduledExecutorService) { + this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("RequestHouseKeepingService")); + + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + RequestFutureHolder.getInstance().scanExpiredRequest(); + } catch (Throwable e) { + log.error("scan RequestFutureTable exception", e); + } + } + }, 1000 * 3, 1000, TimeUnit.MILLISECONDS); + + } + } + + public synchronized void shutdown(DefaultMQProducerImpl producer) { + this.producerSet.remove(producer); + if (this.producerSet.size() <= 0 && null != this.scheduledExecutorService) { + ScheduledExecutorService executorService = this.scheduledExecutorService; + this.scheduledExecutorService = null; + executorService.shutdown(); + } + } + + private RequestFutureHolder() {} + + public static RequestFutureHolder getInstance() { + return INSTANCE; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/producer/RequestResponseFuture.java b/client/src/main/java/org/apache/rocketmq/client/producer/RequestResponseFuture.java new file mode 100644 index 00000000000..e66c08fdc53 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/producer/RequestResponseFuture.java @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client.producer; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.common.message.Message; + +public class RequestResponseFuture { + private final String correlationId; + private final RequestCallback requestCallback; + private final long beginTimestamp = System.currentTimeMillis(); + private final Message requestMsg = null; + private long timeoutMillis; + private CountDownLatch countDownLatch = new CountDownLatch(1); + private volatile Message responseMsg = null; + private volatile boolean sendRequestOk = true; + private volatile Throwable cause = null; + + public RequestResponseFuture(String correlationId, long timeoutMillis, RequestCallback requestCallback) { + this.correlationId = correlationId; + this.timeoutMillis = timeoutMillis; + this.requestCallback = requestCallback; + } + + public void executeRequestCallback() { + if (requestCallback != null) { + if (sendRequestOk && cause == null) { + requestCallback.onSuccess(responseMsg); + } else { + requestCallback.onException(cause); + } + } + } + + public boolean isTimeout() { + long diff = System.currentTimeMillis() - this.beginTimestamp; + return diff > this.timeoutMillis; + } + + public Message waitResponseMessage(final long timeout) throws InterruptedException { + this.countDownLatch.await(timeout, TimeUnit.MILLISECONDS); + return this.responseMsg; + } + + public void putResponseMessage(final Message responseMsg) { + this.responseMsg = responseMsg; + this.countDownLatch.countDown(); + } + + public String getCorrelationId() { + return correlationId; + } + + public long getTimeoutMillis() { + return timeoutMillis; + } + + public void setTimeoutMillis(long timeoutMillis) { + this.timeoutMillis = timeoutMillis; + } + + public RequestCallback getRequestCallback() { + return requestCallback; + } + + public long getBeginTimestamp() { + return beginTimestamp; + } + + public CountDownLatch getCountDownLatch() { + return countDownLatch; + } + + public void setCountDownLatch(CountDownLatch countDownLatch) { + this.countDownLatch = countDownLatch; + } + + public Message getResponseMsg() { + return responseMsg; + } + + public void setResponseMsg(Message responseMsg) { + this.responseMsg = responseMsg; + } + + public boolean isSendRequestOk() { + return sendRequestOk; + } + + public void setSendRequestOk(boolean sendRequestOk) { + this.sendRequestOk = sendRequestOk; + } + + public Message getRequestMsg() { + return requestMsg; + } + + public Throwable getCause() { + return cause; + } + + public void setCause(Throwable cause) { + this.cause = cause; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/producer/SendResult.java b/client/src/main/java/org/apache/rocketmq/client/producer/SendResult.java index 80948830fba..dd7ea1cdc5f 100644 --- a/client/src/main/java/org/apache/rocketmq/client/producer/SendResult.java +++ b/client/src/main/java/org/apache/rocketmq/client/producer/SendResult.java @@ -28,6 +28,7 @@ public class SendResult { private String offsetMsgId; private String regionId; private boolean traceOn = true; + private byte[] rawRespBody; public SendResult() { } @@ -130,4 +131,12 @@ public String toString() { return "SendResult [sendStatus=" + sendStatus + ", msgId=" + msgId + ", offsetMsgId=" + offsetMsgId + ", messageQueue=" + messageQueue + ", queueOffset=" + queueOffset + "]"; } + + public void setRawRespBody(byte[] body) { + this.rawRespBody = body; + } + + public byte[] getRawRespBody() { + return rawRespBody; + } } diff --git a/client/src/main/java/org/apache/rocketmq/client/producer/TransactionCheckListener.java b/client/src/main/java/org/apache/rocketmq/client/producer/TransactionCheckListener.java index 1cf5c4d90a5..5f595065755 100644 --- a/client/src/main/java/org/apache/rocketmq/client/producer/TransactionCheckListener.java +++ b/client/src/main/java/org/apache/rocketmq/client/producer/TransactionCheckListener.java @@ -18,6 +18,10 @@ import org.apache.rocketmq.common.message.MessageExt; +/** + * @deprecated This interface will be removed in the version 5.0.0, interface {@link TransactionListener} is recommended. + */ +@Deprecated public interface TransactionCheckListener { LocalTransactionState checkLocalTransactionState(final MessageExt msg); } diff --git a/client/src/main/java/org/apache/rocketmq/client/producer/TransactionListener.java b/client/src/main/java/org/apache/rocketmq/client/producer/TransactionListener.java new file mode 100644 index 00000000000..233af69bc1d --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/producer/TransactionListener.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.producer; + +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageExt; + +public interface TransactionListener { + /** + * When send transactional prepare(half) message succeed, this method will be invoked to execute local transaction. + * + * @param msg Half(prepare) message + * @param arg Custom business parameter + * @return Transaction state + */ + LocalTransactionState executeLocalTransaction(final Message msg, final Object arg); + + /** + * When no response to prepare(half) message. broker will send check message to check the transaction status, and this + * method will be invoked to get local transaction status. + * + * @param msg Check message + * @return Transaction state + */ + LocalTransactionState checkLocalTransaction(final MessageExt msg); +} \ No newline at end of file diff --git a/client/src/main/java/org/apache/rocketmq/client/producer/TransactionMQProducer.java b/client/src/main/java/org/apache/rocketmq/client/producer/TransactionMQProducer.java index 7b87e8f720b..5c7b437809a 100644 --- a/client/src/main/java/org/apache/rocketmq/client/producer/TransactionMQProducer.java +++ b/client/src/main/java/org/apache/rocketmq/client/producer/TransactionMQProducer.java @@ -16,9 +16,12 @@ */ package org.apache.rocketmq.client.producer; +import java.util.List; +import java.util.concurrent.ExecutorService; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; public class TransactionMQProducer extends DefaultMQProducer { private TransactionCheckListener transactionCheckListener; @@ -26,6 +29,10 @@ public class TransactionMQProducer extends DefaultMQProducer { private int checkThreadPoolMaxSize = 1; private int checkRequestHoldMax = 2000; + private ExecutorService executorService; + + private TransactionListener transactionListener; + public TransactionMQProducer() { } @@ -33,8 +40,30 @@ public TransactionMQProducer(final String producerGroup) { super(producerGroup); } + public TransactionMQProducer(final String producerGroup, final List topics) { + super(producerGroup, null, topics); + } + public TransactionMQProducer(final String producerGroup, RPCHook rpcHook) { - super(producerGroup, rpcHook); + super(producerGroup, rpcHook, null); + } + + public TransactionMQProducer(final String producerGroup, RPCHook rpcHook, final List topics) { + super(producerGroup, rpcHook, topics); + } + + public TransactionMQProducer(final String producerGroup, RPCHook rpcHook, boolean enableMsgTrace, final String customizedTraceTopic) { + super(producerGroup, rpcHook, enableMsgTrace, customizedTraceTopic); + } + + @Deprecated + public TransactionMQProducer(final String namespace, final String producerGroup) { + super(namespace, producerGroup); + } + + @Deprecated + public TransactionMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook, boolean enableMsgTrace, final String customizedTraceTopic) { + super(namespace, producerGroup, rpcHook, enableMsgTrace, customizedTraceTopic); } @Override @@ -51,18 +80,23 @@ public void shutdown() { @Override public TransactionSendResult sendMessageInTransaction(final Message msg, - final LocalTransactionExecuter tranExecuter, final Object arg) throws MQClientException { - if (null == this.transactionCheckListener) { - throw new MQClientException("localTransactionBranchCheckListener is null", null); + final Object arg) throws MQClientException { + if (null == this.transactionListener) { + throw new MQClientException("TransactionListener is null", null); } - return this.defaultMQProducerImpl.sendMessageInTransaction(msg, tranExecuter, arg); + msg.setTopic(NamespaceUtil.wrapNamespace(this.getNamespace(), msg.getTopic())); + return this.defaultMQProducerImpl.sendMessageInTransaction(msg, null, arg); } public TransactionCheckListener getTransactionCheckListener() { return transactionCheckListener; } + /** + * This method will be removed in the version 5.0.0 and set a custom thread pool is recommended. + */ + @Deprecated public void setTransactionCheckListener(TransactionCheckListener transactionCheckListener) { this.transactionCheckListener = transactionCheckListener; } @@ -71,6 +105,10 @@ public int getCheckThreadPoolMinSize() { return checkThreadPoolMinSize; } + /** + * This method will be removed in the version 5.0.0 and set a custom thread pool is recommended. + */ + @Deprecated public void setCheckThreadPoolMinSize(int checkThreadPoolMinSize) { this.checkThreadPoolMinSize = checkThreadPoolMinSize; } @@ -79,6 +117,10 @@ public int getCheckThreadPoolMaxSize() { return checkThreadPoolMaxSize; } + /** + * This method will be removed in the version 5.0.0 and set a custom thread pool is recommended. + */ + @Deprecated public void setCheckThreadPoolMaxSize(int checkThreadPoolMaxSize) { this.checkThreadPoolMaxSize = checkThreadPoolMaxSize; } @@ -87,7 +129,27 @@ public int getCheckRequestHoldMax() { return checkRequestHoldMax; } + /** + * This method will be removed in the version 5.0.0 and set a custom thread pool is recommended. + */ + @Deprecated public void setCheckRequestHoldMax(int checkRequestHoldMax) { this.checkRequestHoldMax = checkRequestHoldMax; } + + public ExecutorService getExecutorService() { + return executorService; + } + + public void setExecutorService(ExecutorService executorService) { + this.executorService = executorService; + } + + public TransactionListener getTransactionListener() { + return transactionListener; + } + + public void setTransactionListener(TransactionListener transactionListener) { + this.transactionListener = transactionListener; + } } diff --git a/client/src/main/java/org/apache/rocketmq/client/producer/selector/SelectMessageQueueByHash.java b/client/src/main/java/org/apache/rocketmq/client/producer/selector/SelectMessageQueueByHash.java index 11e2822b225..ba8ea8b58a3 100644 --- a/client/src/main/java/org/apache/rocketmq/client/producer/selector/SelectMessageQueueByHash.java +++ b/client/src/main/java/org/apache/rocketmq/client/producer/selector/SelectMessageQueueByHash.java @@ -25,12 +25,10 @@ public class SelectMessageQueueByHash implements MessageQueueSelector { @Override public MessageQueue select(List mqs, Message msg, Object arg) { - int value = arg.hashCode(); + int value = arg.hashCode() % mqs.size(); if (value < 0) { value = Math.abs(value); } - - value = value % mqs.size(); return mqs.get(value); } } diff --git a/client/src/main/java/org/apache/rocketmq/client/rpchook/NamespaceRpcHook.java b/client/src/main/java/org/apache/rocketmq/client/rpchook/NamespaceRpcHook.java new file mode 100644 index 00000000000..0178b2ca91b --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/rpchook/NamespaceRpcHook.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client.rpchook; + +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public class NamespaceRpcHook implements RPCHook { + private final ClientConfig clientConfig; + + public NamespaceRpcHook(ClientConfig clientConfig) { + this.clientConfig = clientConfig; + } + + @Override + public void doBeforeRequest(String remoteAddr, RemotingCommand request) { + if (StringUtils.isNotEmpty(clientConfig.getNamespaceV2())) { + request.addExtField(MixAll.RPC_REQUEST_HEADER_NAMESPACED_FIELD, "true"); + request.addExtField(MixAll.RPC_REQUEST_HEADER_NAMESPACE_FIELD, clientConfig.getNamespaceV2()); + } + } + + @Override + public void doAfterResponse(String remoteAddr, RemotingCommand request, + RemotingCommand response) { + + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/stat/ConsumerStatsManager.java b/client/src/main/java/org/apache/rocketmq/client/stat/ConsumerStatsManager.java index 350e1dd7015..a9f506e7600 100644 --- a/client/src/main/java/org/apache/rocketmq/client/stat/ConsumerStatsManager.java +++ b/client/src/main/java/org/apache/rocketmq/client/stat/ConsumerStatsManager.java @@ -18,15 +18,14 @@ package org.apache.rocketmq.client.stat; import java.util.concurrent.ScheduledExecutorService; -import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.common.protocol.body.ConsumeStatus; import org.apache.rocketmq.common.stats.StatsItemSet; import org.apache.rocketmq.common.stats.StatsSnapshot; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.body.ConsumeStatus; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; public class ConsumerStatsManager { - private static final Logger log = LoggerFactory.getLogger(LoggerName.CLIENT_LOGGER_NAME); + private static final Logger log = LoggerFactory.getLogger(ConsumerStatsManager.class); private static final String TOPIC_AND_GROUP_CONSUME_OK_TPS = "CONSUME_OK_TPS"; private static final String TOPIC_AND_GROUP_CONSUME_FAILED_TPS = "CONSUME_FAILED_TPS"; @@ -62,7 +61,7 @@ public void shutdown() { } public void incPullRT(final String group, final String topic, final long rt) { - this.topicAndGroupPullRT.addValue(topic + "@" + group, (int) rt, 1); + this.topicAndGroupPullRT.addRTValue(topic + "@" + group, (int) rt, 1); } public void incPullTPS(final String group, final String topic, final long msgs) { @@ -70,7 +69,7 @@ public void incPullTPS(final String group, final String topic, final long msgs) } public void incConsumeRT(final String group, final String topic, final long rt) { - this.topicAndGroupConsumeRT.addValue(topic + "@" + group, (int) rt, 1); + this.topicAndGroupConsumeRT.addRTValue(topic + "@" + group, (int) rt, 1); } public void incConsumeOKTPS(final String group, final String topic, final long msgs) { diff --git a/client/src/main/java/org/apache/rocketmq/client/trace/AsyncTraceDispatcher.java b/client/src/main/java/org/apache/rocketmq/client/trace/AsyncTraceDispatcher.java new file mode 100644 index 00000000000..ea423b71766 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/trace/AsyncTraceDispatcher.java @@ -0,0 +1,458 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.trace; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.rocketmq.client.AccessChannel; +import org.apache.rocketmq.client.common.ThreadLocalIndex; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl; +import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl; +import org.apache.rocketmq.client.impl.producer.TopicPublishInfo; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.MessageQueueSelector; +import org.apache.rocketmq.client.producer.SendCallback; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +import static org.apache.rocketmq.client.trace.TraceConstants.TRACE_INSTANCE_NAME; + +public class AsyncTraceDispatcher implements TraceDispatcher { + private final static Logger log = LoggerFactory.getLogger(AsyncTraceDispatcher.class); + private final static AtomicInteger COUNTER = new AtomicInteger(); + private final static short MAX_MSG_KEY_SIZE = Short.MAX_VALUE - 10000; + private final int queueSize; + private final int batchSize; + private final int maxMsgSize; + private final long pollingTimeMil; + private final long waitTimeThresholdMil; + private final DefaultMQProducer traceProducer; + private final ThreadPoolExecutor traceExecutor; + // The last discard number of log + private AtomicLong discardCount; + private Thread worker; + private final ArrayBlockingQueue traceContextQueue; + private final HashMap taskQueueByTopic; + private ArrayBlockingQueue appenderQueue; + private volatile Thread shutDownHook; + private volatile boolean stopped = false; + private DefaultMQProducerImpl hostProducer; + private DefaultMQPushConsumerImpl hostConsumer; + private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex(); + private String dispatcherId = UUID.randomUUID().toString(); + private volatile String traceTopicName; + private AtomicBoolean isStarted = new AtomicBoolean(false); + private volatile AccessChannel accessChannel = AccessChannel.LOCAL; + private String group; + private Type type; + + public AsyncTraceDispatcher(String group, Type type, String traceTopicName, RPCHook rpcHook) { + // queueSize is greater than or equal to the n power of 2 of value + this.queueSize = 2048; + this.batchSize = 100; + this.maxMsgSize = 128000; + this.pollingTimeMil = 100; + this.waitTimeThresholdMil = 500; + this.discardCount = new AtomicLong(0L); + this.traceContextQueue = new ArrayBlockingQueue<>(1024); + this.taskQueueByTopic = new HashMap(); + this.group = group; + this.type = type; + + this.appenderQueue = new ArrayBlockingQueue<>(queueSize); + if (!UtilAll.isBlank(traceTopicName)) { + this.traceTopicName = traceTopicName; + } else { + this.traceTopicName = TopicValidator.RMQ_SYS_TRACE_TOPIC; + } + this.traceExecutor = new ThreadPoolExecutor(// + 10, // + 20, // + 1000 * 60, // + TimeUnit.MILLISECONDS, // + this.appenderQueue, // + new ThreadFactoryImpl("MQTraceSendThread_")); + traceProducer = getAndCreateTraceProducer(rpcHook); + } + + public AccessChannel getAccessChannel() { + return accessChannel; + } + + public void setAccessChannel(AccessChannel accessChannel) { + this.accessChannel = accessChannel; + } + + public String getTraceTopicName() { + return traceTopicName; + } + + public void setTraceTopicName(String traceTopicName) { + this.traceTopicName = traceTopicName; + } + + public DefaultMQProducer getTraceProducer() { + return traceProducer; + } + + public DefaultMQProducerImpl getHostProducer() { + return hostProducer; + } + + public void setHostProducer(DefaultMQProducerImpl hostProducer) { + this.hostProducer = hostProducer; + } + + public DefaultMQPushConsumerImpl getHostConsumer() { + return hostConsumer; + } + + public void setHostConsumer(DefaultMQPushConsumerImpl hostConsumer) { + this.hostConsumer = hostConsumer; + } + + public void start(String nameSrvAddr, AccessChannel accessChannel) throws MQClientException { + if (isStarted.compareAndSet(false, true)) { + traceProducer.setNamesrvAddr(nameSrvAddr); + traceProducer.setInstanceName(TRACE_INSTANCE_NAME + "_" + nameSrvAddr); + traceProducer.start(); + } + this.accessChannel = accessChannel; + this.worker = new Thread(new AsyncRunnable(), "MQ-AsyncTraceDispatcher-Thread-" + dispatcherId); + this.worker.setDaemon(true); + this.worker.start(); + this.registerShutDownHook(); + } + + private DefaultMQProducer getAndCreateTraceProducer(RPCHook rpcHook) { + DefaultMQProducer traceProducerInstance = this.traceProducer; + if (traceProducerInstance == null) { + traceProducerInstance = new DefaultMQProducer(rpcHook); + traceProducerInstance.setProducerGroup(genGroupNameForTrace()); + traceProducerInstance.setSendMsgTimeout(5000); + traceProducerInstance.setVipChannelEnabled(false); + // The max size of message is 128K + traceProducerInstance.setMaxMessageSize(maxMsgSize); + } + return traceProducerInstance; + } + + private String genGroupNameForTrace() { + return TraceConstants.GROUP_NAME_PREFIX + "-" + this.group + "-" + this.type + "-" + COUNTER.incrementAndGet(); + } + + @Override + public boolean append(final Object ctx) { + boolean result = traceContextQueue.offer((TraceContext) ctx); + if (!result) { + log.info("buffer full" + discardCount.incrementAndGet() + " ,context is " + ctx); + } + return result; + } + + @Override + public void flush() { + // The maximum waiting time for refresh,avoid being written all the time, resulting in failure to return. + long end = System.currentTimeMillis() + 500; + while (System.currentTimeMillis() <= end) { + synchronized (taskQueueByTopic) { + for (TraceDataSegment taskInfo : taskQueueByTopic.values()) { + taskInfo.sendAllData(); + } + } + synchronized (traceContextQueue) { + if (traceContextQueue.size() == 0 && appenderQueue.size() == 0) { + break; + } + } + try { + Thread.sleep(1); + } catch (InterruptedException e) { + break; + } + } + log.info("------end trace send " + traceContextQueue.size() + " " + appenderQueue.size()); + } + + @Override + public void shutdown() { + this.stopped = true; + flush(); + this.traceExecutor.shutdown(); + if (isStarted.get()) { + traceProducer.shutdown(); + } + this.removeShutdownHook(); + } + + public void registerShutDownHook() { + if (shutDownHook == null) { + shutDownHook = new Thread(new Runnable() { + private volatile boolean hasShutdown = false; + + @Override + public void run() { + synchronized (this) { + if (!this.hasShutdown) { + flush(); + } + } + } + }, "ShutdownHookMQTrace"); + Runtime.getRuntime().addShutdownHook(shutDownHook); + } + } + + public void removeShutdownHook() { + if (shutDownHook != null) { + try { + Runtime.getRuntime().removeShutdownHook(shutDownHook); + } catch (IllegalStateException e) { + // ignore - VM is already shutting down + } + } + } + + class AsyncRunnable implements Runnable { + private boolean stopped; + + @Override + public void run() { + while (!stopped) { + synchronized (traceContextQueue) { + long endTime = System.currentTimeMillis() + pollingTimeMil; + while (System.currentTimeMillis() < endTime) { + try { + TraceContext traceContext = traceContextQueue.poll( + endTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS + ); + + if (traceContext != null && !traceContext.getTraceBeans().isEmpty()) { + // get the topic which the trace message will send to + String traceTopicName = this.getTraceTopicName(traceContext.getRegionId()); + + // get the traceDataSegment which will save this trace message, create if null + TraceDataSegment traceDataSegment = taskQueueByTopic.get(traceTopicName); + if (traceDataSegment == null) { + traceDataSegment = new TraceDataSegment(traceTopicName, traceContext.getRegionId()); + taskQueueByTopic.put(traceTopicName, traceDataSegment); + } + + // encode traceContext and save it into traceDataSegment + // NOTE if data size in traceDataSegment more than maxMsgSize, + // a AsyncDataSendTask will be created and submitted + TraceTransferBean traceTransferBean = TraceDataEncoder.encoderFromContextBean(traceContext); + traceDataSegment.addTraceTransferBean(traceTransferBean); + } + } catch (InterruptedException ignore) { + log.debug("traceContextQueue#poll exception"); + } + } + + // NOTE send the data in traceDataSegment which the first TraceTransferBean + // is longer than waitTimeThreshold + sendDataByTimeThreshold(); + + if (AsyncTraceDispatcher.this.stopped) { + this.stopped = true; + } + } + } + + } + + private void sendDataByTimeThreshold() { + long now = System.currentTimeMillis(); + for (TraceDataSegment taskInfo : taskQueueByTopic.values()) { + if (now - taskInfo.firstBeanAddTime >= waitTimeThresholdMil) { + taskInfo.sendAllData(); + } + } + } + + private String getTraceTopicName(String regionId) { + AccessChannel accessChannel = AsyncTraceDispatcher.this.getAccessChannel(); + if (AccessChannel.CLOUD == accessChannel) { + return TraceConstants.TRACE_TOPIC_PREFIX + regionId; + } + + return AsyncTraceDispatcher.this.getTraceTopicName(); + } + } + + class TraceDataSegment { + private long firstBeanAddTime; + private int currentMsgSize; + private int currentMsgKeySize; + private final String traceTopicName; + private final String regionId; + private final List traceTransferBeanList = new ArrayList(); + + TraceDataSegment(String traceTopicName, String regionId) { + this.traceTopicName = traceTopicName; + this.regionId = regionId; + } + + public void addTraceTransferBean(TraceTransferBean traceTransferBean) { + initFirstBeanAddTime(); + this.traceTransferBeanList.add(traceTransferBean); + this.currentMsgSize += traceTransferBean.getTransData().length(); + + this.currentMsgKeySize = traceTransferBean.getTransKey().stream() + .reduce(currentMsgKeySize, (acc, x) -> acc + x.length(), Integer::sum); + if (currentMsgSize >= traceProducer.getMaxMessageSize() - 10 * 1000 || currentMsgKeySize >= MAX_MSG_KEY_SIZE) { + List dataToSend = new ArrayList(traceTransferBeanList); + AsyncDataSendTask asyncDataSendTask = new AsyncDataSendTask(traceTopicName, regionId, dataToSend); + traceExecutor.submit(asyncDataSendTask); + this.clear(); + } + } + + public void sendAllData() { + if (this.traceTransferBeanList.isEmpty()) { + return; + } + List dataToSend = new ArrayList(traceTransferBeanList); + AsyncDataSendTask asyncDataSendTask = new AsyncDataSendTask(traceTopicName, regionId, dataToSend); + traceExecutor.submit(asyncDataSendTask); + + this.clear(); + } + + private void initFirstBeanAddTime() { + if (firstBeanAddTime == 0) { + firstBeanAddTime = System.currentTimeMillis(); + } + } + + private void clear() { + this.firstBeanAddTime = 0; + this.currentMsgSize = 0; + this.currentMsgKeySize = 0; + this.traceTransferBeanList.clear(); + } + } + + class AsyncDataSendTask implements Runnable { + private final String traceTopicName; + private final String regionId; + private final List traceTransferBeanList; + + public AsyncDataSendTask(String traceTopicName, String regionId, List traceTransferBeanList) { + this.traceTopicName = traceTopicName; + this.regionId = regionId; + this.traceTransferBeanList = traceTransferBeanList; + } + + @Override + public void run() { + StringBuilder buffer = new StringBuilder(1024); + Set keySet = new HashSet<>(); + for (TraceTransferBean bean : traceTransferBeanList) { + keySet.addAll(bean.getTransKey()); + buffer.append(bean.getTransData()); + } + sendTraceDataByMQ(keySet, buffer.toString(), traceTopicName); + } + + /** + * Send message trace data + * + * @param keySet the keyset in this batch(including msgId in original message not offsetMsgId) + * @param data the message trace data in this batch + * @param traceTopic the topic which message trace data will send to + */ + private void sendTraceDataByMQ(Set keySet, final String data, String traceTopic) { + final Message message = new Message(traceTopic, data.getBytes(StandardCharsets.UTF_8)); + // Keyset of message trace includes msgId of or original message + message.setKeys(keySet); + try { + Set traceBrokerSet = tryGetMessageQueueBrokerSet(traceProducer.getDefaultMQProducerImpl(), traceTopic); + SendCallback callback = new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + + } + + @Override + public void onException(Throwable e) { + log.error("send trace data failed, the traceData is {}", data, e); + } + }; + if (traceBrokerSet.isEmpty()) { + // No cross set + traceProducer.send(message, callback, 5000); + } else { + traceProducer.send(message, new MessageQueueSelector() { + @Override + public MessageQueue select(List mqs, Message msg, Object arg) { + Set brokerSet = (Set) arg; + List filterMqs = new ArrayList<>(); + for (MessageQueue queue : mqs) { + if (brokerSet.contains(queue.getBrokerName())) { + filterMqs.add(queue); + } + } + int index = sendWhichQueue.incrementAndGet(); + int pos = index % filterMqs.size(); + return filterMqs.get(pos); + } + }, traceBrokerSet, callback); + } + + } catch (Exception e) { + log.error("send trace data failed, the traceData is {}", data, e); + } + } + + private Set tryGetMessageQueueBrokerSet(DefaultMQProducerImpl producer, String topic) { + Set brokerSet = new HashSet<>(); + TopicPublishInfo topicPublishInfo = producer.getTopicPublishInfoTable().get(topic); + if (null == topicPublishInfo || !topicPublishInfo.ok()) { + producer.getTopicPublishInfoTable().putIfAbsent(topic, new TopicPublishInfo()); + producer.getMqClientFactory().updateTopicRouteInfoFromNameServer(topic); + topicPublishInfo = producer.getTopicPublishInfoTable().get(topic); + } + if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) { + for (MessageQueue queue : topicPublishInfo.getMessageQueueList()) { + brokerSet.add(queue.getBrokerName()); + } + } + return brokerSet; + } + } + +} diff --git a/client/src/main/java/org/apache/rocketmq/client/trace/TraceBean.java b/client/src/main/java/org/apache/rocketmq/client/trace/TraceBean.java new file mode 100644 index 00000000000..70c147e1eb3 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/trace/TraceBean.java @@ -0,0 +1,171 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.trace; + +import org.apache.rocketmq.client.producer.LocalTransactionState; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageType; + +public class TraceBean { + private static final String LOCAL_ADDRESS = UtilAll.ipToIPv4Str(UtilAll.getIP()); + private String topic = ""; + private String msgId = ""; + private String offsetMsgId = ""; + private String tags = ""; + private String keys = ""; + private String storeHost = LOCAL_ADDRESS; + private String clientHost = LOCAL_ADDRESS; + private long storeTime; + private int retryTimes; + private int bodyLength; + private MessageType msgType; + private LocalTransactionState transactionState; + private String transactionId; + private boolean fromTransactionCheck; + + public MessageType getMsgType() { + return msgType; + } + + + public void setMsgType(final MessageType msgType) { + this.msgType = msgType; + } + + + public String getOffsetMsgId() { + return offsetMsgId; + } + + + public void setOffsetMsgId(final String offsetMsgId) { + this.offsetMsgId = offsetMsgId; + } + + public String getTopic() { + return topic; + } + + + public void setTopic(String topic) { + this.topic = topic; + } + + + public String getMsgId() { + return msgId; + } + + + public void setMsgId(String msgId) { + this.msgId = msgId; + } + + + public String getTags() { + return tags; + } + + + public void setTags(String tags) { + this.tags = tags; + } + + + public String getKeys() { + return keys; + } + + + public void setKeys(String keys) { + this.keys = keys; + } + + + public String getStoreHost() { + return storeHost; + } + + + public void setStoreHost(String storeHost) { + this.storeHost = storeHost; + } + + + public String getClientHost() { + return clientHost; + } + + + public void setClientHost(String clientHost) { + this.clientHost = clientHost; + } + + + public long getStoreTime() { + return storeTime; + } + + + public void setStoreTime(long storeTime) { + this.storeTime = storeTime; + } + + + public int getRetryTimes() { + return retryTimes; + } + + + public void setRetryTimes(int retryTimes) { + this.retryTimes = retryTimes; + } + + + public int getBodyLength() { + return bodyLength; + } + + + public void setBodyLength(int bodyLength) { + this.bodyLength = bodyLength; + } + + public LocalTransactionState getTransactionState() { + return transactionState; + } + + public void setTransactionState(LocalTransactionState transactionState) { + this.transactionState = transactionState; + } + + public String getTransactionId() { + return transactionId; + } + + public void setTransactionId(String transactionId) { + this.transactionId = transactionId; + } + + public boolean isFromTransactionCheck() { + return fromTransactionCheck; + } + + public void setFromTransactionCheck(boolean fromTransactionCheck) { + this.fromTransactionCheck = fromTransactionCheck; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/trace/TraceConstants.java b/client/src/main/java/org/apache/rocketmq/client/trace/TraceConstants.java new file mode 100644 index 00000000000..1ad4b610515 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/trace/TraceConstants.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.trace; + +import org.apache.rocketmq.common.topic.TopicValidator; + +public class TraceConstants { + + public static final String GROUP_NAME_PREFIX = "_INNER_TRACE_PRODUCER"; + public static final char CONTENT_SPLITOR = (char) 1; + public static final char FIELD_SPLITOR = (char) 2; + public static final String TRACE_INSTANCE_NAME = "PID_CLIENT_INNER_TRACE_PRODUCER"; + public static final String TRACE_TOPIC_PREFIX = TopicValidator.SYSTEM_TOPIC_PREFIX + "TRACE_DATA_"; + public static final String TO_PREFIX = "To_"; + public static final String FROM_PREFIX = "From_"; + public static final String END_TRANSACTION = "EndTransaction"; + public static final String ROCKETMQ_SERVICE = "rocketmq"; + public static final String ROCKETMQ_SUCCESS = "rocketmq.success"; + public static final String ROCKETMQ_TAGS = "rocketmq.tags"; + public static final String ROCKETMQ_KEYS = "rocketmq.keys"; + public static final String ROCKETMQ_SOTRE_HOST = "rocketmq.store_host"; + public static final String ROCKETMQ_BODY_LENGTH = "rocketmq.body_length"; + public static final String ROCKETMQ_MSG_ID = "rocketmq.mgs_id"; + public static final String ROCKETMQ_MSG_TYPE = "rocketmq.mgs_type"; + public static final String ROCKETMQ_REGION_ID = "rocketmq.region_id"; + public static final String ROCKETMQ_TRANSACTION_ID = "rocketmq.transaction_id"; + public static final String ROCKETMQ_TRANSACTION_STATE = "rocketmq.transaction_state"; + public static final String ROCKETMQ_IS_FROM_TRANSACTION_CHECK = "rocketmq.is_from_transaction_check"; + public static final String ROCKETMQ_RETRY_TIMERS = "rocketmq.retry_times"; +} diff --git a/client/src/main/java/org/apache/rocketmq/client/trace/TraceContext.java b/client/src/main/java/org/apache/rocketmq/client/trace/TraceContext.java new file mode 100644 index 00000000000..a1f632e024c --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/trace/TraceContext.java @@ -0,0 +1,147 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.trace; + +import org.apache.rocketmq.client.AccessChannel; +import org.apache.rocketmq.common.message.MessageClientIDSetter; + +import java.util.List; + +/** + * The context of Trace + */ +public class TraceContext implements Comparable { + + private TraceType traceType; + private long timeStamp = System.currentTimeMillis(); + private String regionId = ""; + private String regionName = ""; + private String groupName = ""; + private int costTime = 0; + private boolean isSuccess = true; + private String requestId = MessageClientIDSetter.createUniqID(); + private int contextCode = 0; + private AccessChannel accessChannel; + private List traceBeans; + + public int getContextCode() { + return contextCode; + } + + public void setContextCode(final int contextCode) { + this.contextCode = contextCode; + } + + public List getTraceBeans() { + return traceBeans; + } + + public void setTraceBeans(List traceBeans) { + this.traceBeans = traceBeans; + } + + public String getRegionId() { + return regionId; + } + + public void setRegionId(String regionId) { + this.regionId = regionId; + } + + public TraceType getTraceType() { + return traceType; + } + + public void setTraceType(TraceType traceType) { + this.traceType = traceType; + } + + public long getTimeStamp() { + return timeStamp; + } + + public void setTimeStamp(long timeStamp) { + this.timeStamp = timeStamp; + } + + public String getGroupName() { + return groupName; + } + + public void setGroupName(String groupName) { + this.groupName = groupName; + } + + public int getCostTime() { + return costTime; + } + + public void setCostTime(int costTime) { + this.costTime = costTime; + } + + public boolean isSuccess() { + return isSuccess; + } + + public void setSuccess(boolean success) { + isSuccess = success; + } + + public String getRequestId() { + return requestId; + } + + public void setRequestId(String requestId) { + this.requestId = requestId; + } + + public String getRegionName() { + return regionName; + } + + public void setRegionName(String regionName) { + this.regionName = regionName; + } + + public AccessChannel getAccessChannel() { + return accessChannel; + } + + public void setAccessChannel(AccessChannel accessChannel) { + this.accessChannel = accessChannel; + } + + @Override + public int compareTo(TraceContext o) { + return Long.compare(this.timeStamp, o.getTimeStamp()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(1024); + sb.append("TraceContext{").append(traceType).append("_").append(groupName).append("_") + .append(regionId).append("_").append(isSuccess).append("_"); + if (traceBeans != null && traceBeans.size() > 0) { + for (TraceBean bean : traceBeans) { + sb.append(bean.getMsgId()).append("_").append(bean.getTopic()).append("_"); + } + } + sb.append('}'); + return sb.toString(); + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/trace/TraceDataEncoder.java b/client/src/main/java/org/apache/rocketmq/client/trace/TraceDataEncoder.java new file mode 100644 index 00000000000..0fdd95243a5 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/trace/TraceDataEncoder.java @@ -0,0 +1,232 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.trace; + +import org.apache.rocketmq.client.AccessChannel; +import org.apache.rocketmq.client.producer.LocalTransactionState; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageType; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Encode/decode for Trace Data + */ +public class TraceDataEncoder { + + /** + * Resolving traceContext list From trace data String + * + * @param traceData + * @return + */ + public static List decoderFromTraceDataString(String traceData) { + List resList = new ArrayList<>(); + if (traceData == null || traceData.length() <= 0) { + return resList; + } + String[] contextList = traceData.split(String.valueOf(TraceConstants.FIELD_SPLITOR)); + for (String context : contextList) { + String[] line = context.split(String.valueOf(TraceConstants.CONTENT_SPLITOR)); + if (line[0].equals(TraceType.Pub.name())) { + TraceContext pubContext = new TraceContext(); + pubContext.setTraceType(TraceType.Pub); + pubContext.setTimeStamp(Long.parseLong(line[1])); + pubContext.setRegionId(line[2]); + pubContext.setGroupName(line[3]); + TraceBean bean = new TraceBean(); + bean.setTopic(line[4]); + bean.setMsgId(line[5]); + bean.setTags(line[6]); + bean.setKeys(line[7]); + bean.setStoreHost(line[8]); + bean.setBodyLength(Integer.parseInt(line[9])); + pubContext.setCostTime(Integer.parseInt(line[10])); + bean.setMsgType(MessageType.values()[Integer.parseInt(line[11])]); + + if (line.length == 13) { + pubContext.setSuccess(Boolean.parseBoolean(line[12])); + } else if (line.length == 14) { + bean.setOffsetMsgId(line[12]); + pubContext.setSuccess(Boolean.parseBoolean(line[13])); + } + + // compatible with the old version + if (line.length >= 15) { + bean.setOffsetMsgId(line[12]); + pubContext.setSuccess(Boolean.parseBoolean(line[13])); + bean.setClientHost(line[14]); + } + + pubContext.setTraceBeans(new ArrayList<>(1)); + pubContext.getTraceBeans().add(bean); + resList.add(pubContext); + } else if (line[0].equals(TraceType.SubBefore.name())) { + TraceContext subBeforeContext = new TraceContext(); + subBeforeContext.setTraceType(TraceType.SubBefore); + subBeforeContext.setTimeStamp(Long.parseLong(line[1])); + subBeforeContext.setRegionId(line[2]); + subBeforeContext.setGroupName(line[3]); + subBeforeContext.setRequestId(line[4]); + TraceBean bean = new TraceBean(); + bean.setMsgId(line[5]); + bean.setRetryTimes(Integer.parseInt(line[6])); + bean.setKeys(line[7]); + subBeforeContext.setTraceBeans(new ArrayList<>(1)); + subBeforeContext.getTraceBeans().add(bean); + resList.add(subBeforeContext); + } else if (line[0].equals(TraceType.SubAfter.name())) { + TraceContext subAfterContext = new TraceContext(); + subAfterContext.setTraceType(TraceType.SubAfter); + subAfterContext.setRequestId(line[1]); + TraceBean bean = new TraceBean(); + bean.setMsgId(line[2]); + bean.setKeys(line[5]); + subAfterContext.setTraceBeans(new ArrayList<>(1)); + subAfterContext.getTraceBeans().add(bean); + subAfterContext.setCostTime(Integer.parseInt(line[3])); + subAfterContext.setSuccess(Boolean.parseBoolean(line[4])); + if (line.length >= 7) { + // add the context type + subAfterContext.setContextCode(Integer.parseInt(line[6])); + } + // compatible with the old version + if (line.length >= 9) { + subAfterContext.setTimeStamp(Long.parseLong(line[7])); + subAfterContext.setGroupName(line[8]); + } + resList.add(subAfterContext); + } else if (line[0].equals(TraceType.EndTransaction.name())) { + TraceContext endTransactionContext = new TraceContext(); + endTransactionContext.setTraceType(TraceType.EndTransaction); + endTransactionContext.setTimeStamp(Long.parseLong(line[1])); + endTransactionContext.setRegionId(line[2]); + endTransactionContext.setGroupName(line[3]); + TraceBean bean = new TraceBean(); + bean.setTopic(line[4]); + bean.setMsgId(line[5]); + bean.setTags(line[6]); + bean.setKeys(line[7]); + bean.setStoreHost(line[8]); + bean.setMsgType(MessageType.values()[Integer.parseInt(line[9])]); + bean.setTransactionId(line[10]); + bean.setTransactionState(LocalTransactionState.valueOf(line[11])); + bean.setFromTransactionCheck(Boolean.parseBoolean(line[12])); + + endTransactionContext.setTraceBeans(new ArrayList<>(1)); + endTransactionContext.getTraceBeans().add(bean); + resList.add(endTransactionContext); + } + } + return resList; + } + + /** + * Encoding the trace context into data strings and keyset sets + * + * @param ctx + * @return + */ + public static TraceTransferBean encoderFromContextBean(TraceContext ctx) { + if (ctx == null) { + return null; + } + //build message trace of the transferring entity content bean + TraceTransferBean transferBean = new TraceTransferBean(); + StringBuilder sb = new StringBuilder(256); + switch (ctx.getTraceType()) { + case Pub: { + TraceBean bean = ctx.getTraceBeans().get(0); + //append the content of context and traceBean to transferBean's TransData + sb.append(ctx.getTraceType()).append(TraceConstants.CONTENT_SPLITOR)// + .append(ctx.getTimeStamp()).append(TraceConstants.CONTENT_SPLITOR)// + .append(ctx.getRegionId()).append(TraceConstants.CONTENT_SPLITOR)// + .append(ctx.getGroupName()).append(TraceConstants.CONTENT_SPLITOR)// + .append(bean.getTopic()).append(TraceConstants.CONTENT_SPLITOR)// + .append(bean.getMsgId()).append(TraceConstants.CONTENT_SPLITOR)// + .append(bean.getTags()).append(TraceConstants.CONTENT_SPLITOR)// + .append(bean.getKeys()).append(TraceConstants.CONTENT_SPLITOR)// + .append(bean.getStoreHost()).append(TraceConstants.CONTENT_SPLITOR)// + .append(bean.getBodyLength()).append(TraceConstants.CONTENT_SPLITOR)// + .append(ctx.getCostTime()).append(TraceConstants.CONTENT_SPLITOR)// + .append(bean.getMsgType().ordinal()).append(TraceConstants.CONTENT_SPLITOR)// + .append(bean.getOffsetMsgId()).append(TraceConstants.CONTENT_SPLITOR)// + .append(ctx.isSuccess()).append(TraceConstants.FIELD_SPLITOR);// + } + break; + case SubBefore: { + for (TraceBean bean : ctx.getTraceBeans()) { + sb.append(ctx.getTraceType()).append(TraceConstants.CONTENT_SPLITOR)// + .append(ctx.getTimeStamp()).append(TraceConstants.CONTENT_SPLITOR)// + .append(ctx.getRegionId()).append(TraceConstants.CONTENT_SPLITOR)// + .append(ctx.getGroupName()).append(TraceConstants.CONTENT_SPLITOR)// + .append(ctx.getRequestId()).append(TraceConstants.CONTENT_SPLITOR)// + .append(bean.getMsgId()).append(TraceConstants.CONTENT_SPLITOR)// + .append(bean.getRetryTimes()).append(TraceConstants.CONTENT_SPLITOR)// + .append(bean.getKeys()).append(TraceConstants.FIELD_SPLITOR);// + } + } + break; + case SubAfter: { + for (TraceBean bean : ctx.getTraceBeans()) { + sb.append(ctx.getTraceType()).append(TraceConstants.CONTENT_SPLITOR)// + .append(ctx.getRequestId()).append(TraceConstants.CONTENT_SPLITOR)// + .append(bean.getMsgId()).append(TraceConstants.CONTENT_SPLITOR)// + .append(ctx.getCostTime()).append(TraceConstants.CONTENT_SPLITOR)// + .append(ctx.isSuccess()).append(TraceConstants.CONTENT_SPLITOR)// + .append(bean.getKeys()).append(TraceConstants.CONTENT_SPLITOR)// + .append(ctx.getContextCode()).append(TraceConstants.CONTENT_SPLITOR); + if (!ctx.getAccessChannel().equals(AccessChannel.CLOUD)) { + sb.append(ctx.getTimeStamp()).append(TraceConstants.CONTENT_SPLITOR) + .append(ctx.getGroupName()).append(TraceConstants.FIELD_SPLITOR); + } + } + } + break; + case EndTransaction: { + TraceBean bean = ctx.getTraceBeans().get(0); + sb.append(ctx.getTraceType()).append(TraceConstants.CONTENT_SPLITOR)// + .append(ctx.getTimeStamp()).append(TraceConstants.CONTENT_SPLITOR)// + .append(ctx.getRegionId()).append(TraceConstants.CONTENT_SPLITOR)// + .append(ctx.getGroupName()).append(TraceConstants.CONTENT_SPLITOR)// + .append(bean.getTopic()).append(TraceConstants.CONTENT_SPLITOR)// + .append(bean.getMsgId()).append(TraceConstants.CONTENT_SPLITOR)// + .append(bean.getTags()).append(TraceConstants.CONTENT_SPLITOR)// + .append(bean.getKeys()).append(TraceConstants.CONTENT_SPLITOR)// + .append(bean.getStoreHost()).append(TraceConstants.CONTENT_SPLITOR)// + .append(bean.getMsgType().ordinal()).append(TraceConstants.CONTENT_SPLITOR)// + .append(bean.getTransactionId()).append(TraceConstants.CONTENT_SPLITOR)// + .append(bean.getTransactionState().name()).append(TraceConstants.CONTENT_SPLITOR)// + .append(bean.isFromTransactionCheck()).append(TraceConstants.FIELD_SPLITOR); + } + break; + default: + } + transferBean.setTransData(sb.toString()); + for (TraceBean bean : ctx.getTraceBeans()) { + + transferBean.getTransKey().add(bean.getMsgId()); + if (bean.getKeys() != null && bean.getKeys().length() > 0) { + String[] keys = bean.getKeys().split(MessageConst.KEY_SEPARATOR); + transferBean.getTransKey().addAll(Arrays.asList(keys)); + } + } + return transferBean; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/trace/TraceDispatcher.java b/client/src/main/java/org/apache/rocketmq/client/trace/TraceDispatcher.java new file mode 100644 index 00000000000..ac4ef3ba882 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/trace/TraceDispatcher.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.trace; + +import org.apache.rocketmq.client.AccessChannel; +import org.apache.rocketmq.client.exception.MQClientException; +import java.io.IOException; + +/** + * Interface of asynchronous transfer data + */ +public interface TraceDispatcher { + enum Type { + PRODUCE, + CONSUME + } + /** + * Initialize asynchronous transfer data module + */ + void start(String nameSrvAddr, AccessChannel accessChannel) throws MQClientException; + + /** + * Append the transferring data + * @param ctx data information + * @return + */ + boolean append(Object ctx); + + /** + * Write flush action + * + * @throws IOException + */ + void flush() throws IOException; + + /** + * Close the trace Hook + */ + void shutdown(); +} diff --git a/client/src/main/java/org/apache/rocketmq/client/trace/TraceDispatcherType.java b/client/src/main/java/org/apache/rocketmq/client/trace/TraceDispatcherType.java new file mode 100644 index 00000000000..f09c9b8db4b --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/trace/TraceDispatcherType.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.trace; + +public enum TraceDispatcherType { + PRODUCER, + CONSUMER +} diff --git a/client/src/main/java/org/apache/rocketmq/client/trace/TraceTransferBean.java b/client/src/main/java/org/apache/rocketmq/client/trace/TraceTransferBean.java new file mode 100644 index 00000000000..482a782e517 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/trace/TraceTransferBean.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.trace; + +import java.util.HashSet; +import java.util.Set; + +/** + * Trace transferring bean + */ +public class TraceTransferBean { + private String transData; + private Set transKey = new HashSet<>(); + + public String getTransData() { + return transData; + } + + public void setTransData(String transData) { + this.transData = transData; + } + + public Set getTransKey() { + return transKey; + } + + public void setTransKey(Set transKey) { + this.transKey = transKey; + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/trace/TraceType.java b/client/src/main/java/org/apache/rocketmq/client/trace/TraceType.java new file mode 100644 index 00000000000..8870ddcbdb3 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/trace/TraceType.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.trace; + +public enum TraceType { + Pub, + SubBefore, + SubAfter, + EndTransaction, +} diff --git a/client/src/main/java/org/apache/rocketmq/client/trace/TraceView.java b/client/src/main/java/org/apache/rocketmq/client/trace/TraceView.java new file mode 100644 index 00000000000..01ce56699d8 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/trace/TraceView.java @@ -0,0 +1,180 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client.trace; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import org.apache.rocketmq.common.message.MessageExt; + +public class TraceView { + + private String msgId; + private String tags; + private String keys; + private String storeHost; + private String clientHost; + private int costTime; + private String msgType; + private String offSetMsgId; + private long timeStamp; + private long bornTime; + private String topic; + private String groupName; + private String status; + + public static List decodeFromTraceTransData(String key, MessageExt messageExt) { + List messageTraceViewList = new ArrayList<>(); + String messageBody = new String(messageExt.getBody(), StandardCharsets.UTF_8); + if (messageBody == null || messageBody.length() <= 0) { + return messageTraceViewList; + } + + List traceContextList = TraceDataEncoder.decoderFromTraceDataString(messageBody); + + for (TraceContext context : traceContextList) { + TraceView messageTraceView = new TraceView(); + TraceBean traceBean = context.getTraceBeans().get(0); + if (!traceBean.getMsgId().equals(key)) { + continue; + } + messageTraceView.setCostTime(context.getCostTime()); + messageTraceView.setGroupName(context.getGroupName()); + if (context.isSuccess()) { + messageTraceView.setStatus("success"); + } else { + messageTraceView.setStatus("failed"); + } + messageTraceView.setKeys(traceBean.getKeys()); + messageTraceView.setMsgId(traceBean.getMsgId()); + messageTraceView.setTags(traceBean.getTags()); + messageTraceView.setTopic(traceBean.getTopic()); + messageTraceView.setMsgType(context.getTraceType().name()); + messageTraceView.setOffSetMsgId(traceBean.getOffsetMsgId()); + messageTraceView.setTimeStamp(context.getTimeStamp()); + messageTraceView.setStoreHost(traceBean.getStoreHost()); + messageTraceView.setClientHost(messageExt.getBornHostString()); + messageTraceViewList.add(messageTraceView); + } + return messageTraceViewList; + } + + public String getMsgId() { + return msgId; + } + + public void setMsgId(String msgId) { + this.msgId = msgId; + } + + public String getTags() { + return tags; + } + + public void setTags(String tags) { + this.tags = tags; + } + + public String getKeys() { + return keys; + } + + public void setKeys(String keys) { + this.keys = keys; + } + + public String getStoreHost() { + return storeHost; + } + + public void setStoreHost(String storeHost) { + this.storeHost = storeHost; + } + + public String getClientHost() { + return clientHost; + } + + public void setClientHost(String clientHost) { + this.clientHost = clientHost; + } + + public int getCostTime() { + return costTime; + } + + public void setCostTime(int costTime) { + this.costTime = costTime; + } + + public String getMsgType() { + return msgType; + } + + public void setMsgType(String msgType) { + this.msgType = msgType; + } + + public String getOffSetMsgId() { + return offSetMsgId; + } + + public void setOffSetMsgId(String offSetMsgId) { + this.offSetMsgId = offSetMsgId; + } + + public long getTimeStamp() { + return timeStamp; + } + + public void setTimeStamp(long timeStamp) { + this.timeStamp = timeStamp; + } + + public long getBornTime() { + return bornTime; + } + + public void setBornTime(long bornTime) { + this.bornTime = bornTime; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getGroupName() { + return groupName; + } + + public void setGroupName(String groupName) { + this.groupName = groupName; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } +} \ No newline at end of file diff --git a/client/src/main/java/org/apache/rocketmq/client/trace/hook/ConsumeMessageOpenTracingHookImpl.java b/client/src/main/java/org/apache/rocketmq/client/trace/hook/ConsumeMessageOpenTracingHookImpl.java new file mode 100644 index 00000000000..b983df30613 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/trace/hook/ConsumeMessageOpenTracingHookImpl.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.trace.hook; + +import io.opentracing.Span; +import io.opentracing.SpanContext; +import io.opentracing.Tracer; +import io.opentracing.propagation.Format; +import io.opentracing.propagation.TextMapAdapter; +import io.opentracing.tag.Tags; +import java.util.ArrayList; +import java.util.List; +import org.apache.rocketmq.client.hook.ConsumeMessageContext; +import org.apache.rocketmq.client.hook.ConsumeMessageHook; +import org.apache.rocketmq.client.trace.TraceConstants; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; + + +public class ConsumeMessageOpenTracingHookImpl implements ConsumeMessageHook { + + private Tracer tracer; + + public ConsumeMessageOpenTracingHookImpl(Tracer tracer) { + this.tracer = tracer; + } + + @Override + public String hookName() { + return "ConsumeMessageOpenTracingHook"; + } + + @Override + public void consumeMessageBefore(ConsumeMessageContext context) { + if (context == null || context.getMsgList() == null || context.getMsgList().isEmpty()) { + return; + } + List spanList = new ArrayList<>(); + for (MessageExt msg : context.getMsgList()) { + if (msg == null) { + continue; + } + Tracer.SpanBuilder spanBuilder = tracer + .buildSpan(TraceConstants.FROM_PREFIX + msg.getTopic()) + .withTag(Tags.SPAN_KIND, Tags.SPAN_KIND_CONSUMER); + SpanContext spanContext = tracer.extract(Format.Builtin.TEXT_MAP, new TextMapAdapter(msg.getProperties())); + if (spanContext != null) { + spanBuilder.asChildOf(spanContext); + } + Span span = spanBuilder.start(); + + span.setTag(Tags.PEER_SERVICE, TraceConstants.ROCKETMQ_SERVICE); + span.setTag(Tags.MESSAGE_BUS_DESTINATION, NamespaceUtil.withoutNamespace(msg.getTopic())); + span.setTag(TraceConstants.ROCKETMQ_MSG_ID, msg.getMsgId()); + span.setTag(TraceConstants.ROCKETMQ_TAGS, msg.getTags()); + span.setTag(TraceConstants.ROCKETMQ_KEYS, msg.getKeys()); + span.setTag(TraceConstants.ROCKETMQ_BODY_LENGTH, msg.getStoreSize()); + span.setTag(TraceConstants.ROCKETMQ_RETRY_TIMERS, msg.getReconsumeTimes()); + span.setTag(TraceConstants.ROCKETMQ_REGION_ID, msg.getProperty(MessageConst.PROPERTY_MSG_REGION)); + spanList.add(span); + } + context.setMqTraceContext(spanList); + } + + @Override + public void consumeMessageAfter(ConsumeMessageContext context) { + if (context == null || context.getMsgList() == null || context.getMsgList().isEmpty()) { + return; + } + List spanList = (List) context.getMqTraceContext(); + if (spanList == null) { + return; + } + for (Span span : spanList) { + span.setTag(TraceConstants.ROCKETMQ_SUCCESS, context.isSuccess()); + span.finish(); + } + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/trace/hook/ConsumeMessageTraceHookImpl.java b/client/src/main/java/org/apache/rocketmq/client/trace/hook/ConsumeMessageTraceHookImpl.java new file mode 100644 index 00000000000..f23a4ff0aa1 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/trace/hook/ConsumeMessageTraceHookImpl.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.trace.hook; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.apache.rocketmq.client.consumer.listener.ConsumeReturnType; +import org.apache.rocketmq.client.hook.ConsumeMessageContext; +import org.apache.rocketmq.client.hook.ConsumeMessageHook; +import org.apache.rocketmq.client.trace.TraceBean; +import org.apache.rocketmq.client.trace.TraceContext; +import org.apache.rocketmq.client.trace.TraceDispatcher; +import org.apache.rocketmq.client.trace.TraceType; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; + +public class ConsumeMessageTraceHookImpl implements ConsumeMessageHook { + + private TraceDispatcher localDispatcher; + + public ConsumeMessageTraceHookImpl(TraceDispatcher localDispatcher) { + this.localDispatcher = localDispatcher; + } + + @Override + public String hookName() { + return "ConsumeMessageTraceHook"; + } + + @Override + public void consumeMessageBefore(ConsumeMessageContext context) { + if (context == null || context.getMsgList() == null || context.getMsgList().isEmpty()) { + return; + } + TraceContext traceContext = new TraceContext(); + context.setMqTraceContext(traceContext); + traceContext.setTraceType(TraceType.SubBefore);// + traceContext.setGroupName(NamespaceUtil.withoutNamespace(context.getConsumerGroup()));// + List beans = new ArrayList<>(); + for (MessageExt msg : context.getMsgList()) { + if (msg == null) { + continue; + } + String regionId = msg.getProperty(MessageConst.PROPERTY_MSG_REGION); + String traceOn = msg.getProperty(MessageConst.PROPERTY_TRACE_SWITCH); + + if (traceOn != null && traceOn.equals("false")) { + // If trace switch is false ,skip it + continue; + } + TraceBean traceBean = new TraceBean(); + traceBean.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic()));// + traceBean.setMsgId(msg.getMsgId());// + traceBean.setTags(msg.getTags());// + traceBean.setKeys(msg.getKeys());// + traceBean.setStoreTime(msg.getStoreTimestamp());// + traceBean.setBodyLength(msg.getStoreSize());// + traceBean.setRetryTimes(msg.getReconsumeTimes());// + traceContext.setRegionId(regionId);// + beans.add(traceBean); + } + if (beans.size() > 0) { + traceContext.setTraceBeans(beans); + traceContext.setTimeStamp(System.currentTimeMillis()); + localDispatcher.append(traceContext); + } + } + + @Override + public void consumeMessageAfter(ConsumeMessageContext context) { + if (context == null || context.getMsgList() == null || context.getMsgList().isEmpty()) { + return; + } + TraceContext subBeforeContext = (TraceContext) context.getMqTraceContext(); + + if (subBeforeContext.getTraceBeans() == null || subBeforeContext.getTraceBeans().size() < 1) { + // If subBefore bean is null ,skip it + return; + } + TraceContext subAfterContext = new TraceContext(); + subAfterContext.setTraceType(TraceType.SubAfter);// + subAfterContext.setRegionId(subBeforeContext.getRegionId());// + subAfterContext.setGroupName(NamespaceUtil.withoutNamespace(subBeforeContext.getGroupName()));// + subAfterContext.setRequestId(subBeforeContext.getRequestId());// + subAfterContext.setAccessChannel(context.getAccessChannel()); + subAfterContext.setSuccess(context.isSuccess());// + + // Calculate the cost time for processing messages + int costTime = (int) ((System.currentTimeMillis() - subBeforeContext.getTimeStamp()) / context.getMsgList().size()); + subAfterContext.setCostTime(costTime);// + subAfterContext.setTraceBeans(subBeforeContext.getTraceBeans()); + Map props = context.getProps(); + if (props != null) { + String contextType = props.get(MixAll.CONSUME_CONTEXT_TYPE); + if (contextType != null) { + subAfterContext.setContextCode(ConsumeReturnType.valueOf(contextType).ordinal()); + } + } + localDispatcher.append(subAfterContext); + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/trace/hook/EndTransactionOpenTracingHookImpl.java b/client/src/main/java/org/apache/rocketmq/client/trace/hook/EndTransactionOpenTracingHookImpl.java new file mode 100644 index 00000000000..62d310f1961 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/trace/hook/EndTransactionOpenTracingHookImpl.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.trace.hook; + +import io.opentracing.Span; +import io.opentracing.SpanContext; +import io.opentracing.Tracer; +import io.opentracing.propagation.Format; +import io.opentracing.propagation.TextMapAdapter; +import io.opentracing.tag.Tags; +import org.apache.rocketmq.client.hook.EndTransactionContext; +import org.apache.rocketmq.client.hook.EndTransactionHook; +import org.apache.rocketmq.client.trace.TraceConstants; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageType; + +public class EndTransactionOpenTracingHookImpl implements EndTransactionHook { + + private Tracer tracer; + + public EndTransactionOpenTracingHookImpl(Tracer tracer) { + this.tracer = tracer; + } + + @Override + public String hookName() { + return "EndTransactionOpenTracingHook"; + } + + @Override + public void endTransaction(EndTransactionContext context) { + if (context == null) { + return; + } + Message msg = context.getMessage(); + Tracer.SpanBuilder spanBuilder = tracer + .buildSpan(TraceConstants.END_TRANSACTION) + .withTag(Tags.SPAN_KIND, Tags.SPAN_KIND_PRODUCER); + SpanContext spanContext = tracer.extract(Format.Builtin.TEXT_MAP, new TextMapAdapter(msg.getProperties())); + if (spanContext != null) { + spanBuilder.asChildOf(spanContext); + } + + Span span = spanBuilder.start(); + span.setTag(Tags.PEER_SERVICE, TraceConstants.ROCKETMQ_SERVICE); + span.setTag(Tags.MESSAGE_BUS_DESTINATION, msg.getTopic()); + span.setTag(TraceConstants.ROCKETMQ_TAGS, msg.getTags()); + span.setTag(TraceConstants.ROCKETMQ_KEYS, msg.getKeys()); + span.setTag(TraceConstants.ROCKETMQ_SOTRE_HOST, context.getBrokerAddr()); + span.setTag(TraceConstants.ROCKETMQ_MSG_ID, context.getMsgId()); + span.setTag(TraceConstants.ROCKETMQ_MSG_TYPE, MessageType.Trans_msg_Commit.name()); + span.setTag(TraceConstants.ROCKETMQ_TRANSACTION_ID, context.getTransactionId()); + span.setTag(TraceConstants.ROCKETMQ_TRANSACTION_STATE, context.getTransactionState().name()); + span.setTag(TraceConstants.ROCKETMQ_IS_FROM_TRANSACTION_CHECK, context.isFromTransactionCheck()); + span.finish(); + } + +} diff --git a/client/src/main/java/org/apache/rocketmq/client/trace/hook/EndTransactionTraceHookImpl.java b/client/src/main/java/org/apache/rocketmq/client/trace/hook/EndTransactionTraceHookImpl.java new file mode 100644 index 00000000000..d69388e0418 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/trace/hook/EndTransactionTraceHookImpl.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.trace.hook; + +import java.util.ArrayList; +import org.apache.rocketmq.client.hook.EndTransactionContext; +import org.apache.rocketmq.client.hook.EndTransactionHook; +import org.apache.rocketmq.client.trace.AsyncTraceDispatcher; +import org.apache.rocketmq.client.trace.TraceBean; +import org.apache.rocketmq.client.trace.TraceContext; +import org.apache.rocketmq.client.trace.TraceDispatcher; +import org.apache.rocketmq.client.trace.TraceType; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageType; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; + +public class EndTransactionTraceHookImpl implements EndTransactionHook { + + private TraceDispatcher localDispatcher; + + public EndTransactionTraceHookImpl(TraceDispatcher localDispatcher) { + this.localDispatcher = localDispatcher; + } + + @Override + public String hookName() { + return "EndTransactionTraceHook"; + } + + @Override + public void endTransaction(EndTransactionContext context) { + //if it is message trace data,then it doesn't recorded + if (context == null || context.getMessage().getTopic().startsWith(((AsyncTraceDispatcher) localDispatcher).getTraceTopicName())) { + return; + } + Message msg = context.getMessage(); + //build the context content of TuxeTraceContext + TraceContext tuxeContext = new TraceContext(); + tuxeContext.setTraceBeans(new ArrayList<>(1)); + tuxeContext.setTraceType(TraceType.EndTransaction); + tuxeContext.setGroupName(NamespaceUtil.withoutNamespace(context.getProducerGroup())); + //build the data bean object of message trace + TraceBean traceBean = new TraceBean(); + traceBean.setTopic(NamespaceUtil.withoutNamespace(context.getMessage().getTopic())); + traceBean.setTags(context.getMessage().getTags()); + traceBean.setKeys(context.getMessage().getKeys()); + traceBean.setStoreHost(context.getBrokerAddr()); + traceBean.setMsgType(MessageType.Trans_msg_Commit); + traceBean.setClientHost(((AsyncTraceDispatcher)localDispatcher).getHostProducer().getMqClientFactory().getClientId()); + traceBean.setMsgId(context.getMsgId()); + traceBean.setTransactionState(context.getTransactionState()); + traceBean.setTransactionId(context.getTransactionId()); + traceBean.setFromTransactionCheck(context.isFromTransactionCheck()); + String regionId = msg.getProperty(MessageConst.PROPERTY_MSG_REGION); + if (regionId == null || regionId.isEmpty()) { + regionId = MixAll.DEFAULT_TRACE_REGION_ID; + } + tuxeContext.setRegionId(regionId); + tuxeContext.getTraceBeans().add(traceBean); + tuxeContext.setTimeStamp(System.currentTimeMillis()); + localDispatcher.append(tuxeContext); + } + +} diff --git a/client/src/main/java/org/apache/rocketmq/client/trace/hook/SendMessageOpenTracingHookImpl.java b/client/src/main/java/org/apache/rocketmq/client/trace/hook/SendMessageOpenTracingHookImpl.java new file mode 100644 index 00000000000..60c18a22a77 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/trace/hook/SendMessageOpenTracingHookImpl.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.trace.hook; + +import io.opentracing.Span; +import io.opentracing.SpanContext; +import io.opentracing.Tracer; +import io.opentracing.propagation.Format; +import io.opentracing.propagation.TextMapAdapter; +import io.opentracing.tag.Tags; +import org.apache.rocketmq.client.hook.SendMessageContext; +import org.apache.rocketmq.client.hook.SendMessageHook; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.client.trace.TraceConstants; +import org.apache.rocketmq.common.message.Message; + +public class SendMessageOpenTracingHookImpl implements SendMessageHook { + + private Tracer tracer; + + public SendMessageOpenTracingHookImpl(Tracer tracer) { + this.tracer = tracer; + } + + @Override + public String hookName() { + return "SendMessageOpenTracingHook"; + } + + @Override + public void sendMessageBefore(SendMessageContext context) { + if (context == null) { + return; + } + Message msg = context.getMessage(); + Tracer.SpanBuilder spanBuilder = tracer + .buildSpan(TraceConstants.TO_PREFIX + msg.getTopic()) + .withTag(Tags.SPAN_KIND, Tags.SPAN_KIND_PRODUCER); + SpanContext spanContext = tracer.extract(Format.Builtin.TEXT_MAP, new TextMapAdapter(msg.getProperties())); + if (spanContext != null) { + spanBuilder.asChildOf(spanContext); + } + Span span = spanBuilder.start(); + tracer.inject(span.context(), Format.Builtin.TEXT_MAP, new TextMapAdapter(msg.getProperties())); + span.setTag(Tags.PEER_SERVICE, TraceConstants.ROCKETMQ_SERVICE); + span.setTag(Tags.MESSAGE_BUS_DESTINATION, msg.getTopic()); + span.setTag(TraceConstants.ROCKETMQ_TAGS, msg.getTags()); + span.setTag(TraceConstants.ROCKETMQ_KEYS, msg.getKeys()); + span.setTag(TraceConstants.ROCKETMQ_SOTRE_HOST, context.getBrokerAddr()); + span.setTag(TraceConstants.ROCKETMQ_MSG_TYPE, context.getMsgType().name()); + span.setTag(TraceConstants.ROCKETMQ_BODY_LENGTH, msg.getBody().length); + context.setMqTraceContext(span); + } + + @Override + public void sendMessageAfter(SendMessageContext context) { + if (context == null || context.getMqTraceContext() == null) { + return; + } + if (context.getSendResult() == null) { + return; + } + + if (context.getSendResult().getRegionId() == null) { + return; + } + + Span span = (Span) context.getMqTraceContext(); + span.setTag(TraceConstants.ROCKETMQ_SUCCESS, context.getSendResult().getSendStatus().equals(SendStatus.SEND_OK)); + span.setTag(TraceConstants.ROCKETMQ_MSG_ID, context.getSendResult().getMsgId()); + span.setTag(TraceConstants.ROCKETMQ_REGION_ID, context.getSendResult().getRegionId()); + span.finish(); + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/trace/hook/SendMessageTraceHookImpl.java b/client/src/main/java/org/apache/rocketmq/client/trace/hook/SendMessageTraceHookImpl.java new file mode 100644 index 00000000000..dba04b593f2 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/trace/hook/SendMessageTraceHookImpl.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.trace.hook; + +import java.util.ArrayList; +import org.apache.rocketmq.client.hook.SendMessageContext; +import org.apache.rocketmq.client.hook.SendMessageHook; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.client.trace.AsyncTraceDispatcher; +import org.apache.rocketmq.client.trace.TraceBean; +import org.apache.rocketmq.client.trace.TraceContext; +import org.apache.rocketmq.client.trace.TraceDispatcher; +import org.apache.rocketmq.client.trace.TraceType; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; + +public class SendMessageTraceHookImpl implements SendMessageHook { + + private TraceDispatcher localDispatcher; + + public SendMessageTraceHookImpl(TraceDispatcher localDispatcher) { + this.localDispatcher = localDispatcher; + } + + @Override + public String hookName() { + return "SendMessageTraceHook"; + } + + @Override + public void sendMessageBefore(SendMessageContext context) { + //if it is message trace data,then it doesn't recorded + if (context == null || context.getMessage().getTopic().startsWith(((AsyncTraceDispatcher) localDispatcher).getTraceTopicName())) { + return; + } + //build the context content of TraceContext + TraceContext traceContext = new TraceContext(); + traceContext.setTraceBeans(new ArrayList<>(1)); + context.setMqTraceContext(traceContext); + traceContext.setTraceType(TraceType.Pub); + traceContext.setGroupName(NamespaceUtil.withoutNamespace(context.getProducerGroup())); + //build the data bean object of message trace + TraceBean traceBean = new TraceBean(); + traceBean.setTopic(NamespaceUtil.withoutNamespace(context.getMessage().getTopic())); + traceBean.setTags(context.getMessage().getTags()); + traceBean.setKeys(context.getMessage().getKeys()); + traceBean.setStoreHost(context.getBrokerAddr()); + traceBean.setBodyLength(context.getMessage().getBody().length); + traceBean.setMsgType(context.getMsgType()); + traceContext.getTraceBeans().add(traceBean); + } + + @Override + public void sendMessageAfter(SendMessageContext context) { + //if it is message trace data,then it doesn't recorded + if (context == null || context.getMessage().getTopic().startsWith(((AsyncTraceDispatcher) localDispatcher).getTraceTopicName()) + || context.getMqTraceContext() == null) { + return; + } + if (context.getSendResult() == null) { + return; + } + + if (context.getSendResult().getRegionId() == null + || !context.getSendResult().isTraceOn()) { + // if switch is false,skip it + return; + } + + TraceContext traceContext = (TraceContext) context.getMqTraceContext(); + TraceBean traceBean = traceContext.getTraceBeans().get(0); + int costTime = (int) ((System.currentTimeMillis() - traceContext.getTimeStamp()) / traceContext.getTraceBeans().size()); + traceContext.setCostTime(costTime); + if (context.getSendResult().getSendStatus().equals(SendStatus.SEND_OK)) { + traceContext.setSuccess(true); + } else { + traceContext.setSuccess(false); + } + traceContext.setRegionId(context.getSendResult().getRegionId()); + traceBean.setMsgId(context.getSendResult().getMsgId()); + traceBean.setOffsetMsgId(context.getSendResult().getOffsetMsgId()); + traceBean.setStoreTime(traceContext.getTimeStamp() + costTime / 2); + localDispatcher.append(traceContext); + } +} diff --git a/client/src/main/java/org/apache/rocketmq/client/utils/MessageUtil.java b/client/src/main/java/org/apache/rocketmq/client/utils/MessageUtil.java new file mode 100644 index 00000000000..416ba44da32 --- /dev/null +++ b/client/src/main/java/org/apache/rocketmq/client/utils/MessageUtil.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client.utils; + +import org.apache.rocketmq.client.common.ClientErrorCode; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageConst; + +public class MessageUtil { + public static Message createReplyMessage(final Message requestMessage, final byte[] body) throws MQClientException { + if (requestMessage != null) { + Message replyMessage = new Message(); + String cluster = requestMessage.getProperty(MessageConst.PROPERTY_CLUSTER); + String replyTo = requestMessage.getProperty(MessageConst.PROPERTY_MESSAGE_REPLY_TO_CLIENT); + String correlationId = requestMessage.getProperty(MessageConst.PROPERTY_CORRELATION_ID); + String ttl = requestMessage.getProperty(MessageConst.PROPERTY_MESSAGE_TTL); + replyMessage.setBody(body); + if (cluster != null) { + String replyTopic = MixAll.getReplyTopic(cluster); + replyMessage.setTopic(replyTopic); + MessageAccessor.putProperty(replyMessage, MessageConst.PROPERTY_MESSAGE_TYPE, MixAll.REPLY_MESSAGE_FLAG); + MessageAccessor.putProperty(replyMessage, MessageConst.PROPERTY_CORRELATION_ID, correlationId); + MessageAccessor.putProperty(replyMessage, MessageConst.PROPERTY_MESSAGE_REPLY_TO_CLIENT, replyTo); + MessageAccessor.putProperty(replyMessage, MessageConst.PROPERTY_MESSAGE_TTL, ttl); + + return replyMessage; + } else { + throw new MQClientException(ClientErrorCode.CREATE_REPLY_MESSAGE_EXCEPTION, "create reply message fail, requestMessage error, property[" + MessageConst.PROPERTY_CLUSTER + "] is null."); + } + } + throw new MQClientException(ClientErrorCode.CREATE_REPLY_MESSAGE_EXCEPTION, "create reply message fail, requestMessage cannot be null."); + } + + public static String getReplyToClient(final Message msg) { + return msg.getProperty(MessageConst.PROPERTY_MESSAGE_REPLY_TO_CLIENT); + } +} diff --git a/client/src/main/resources/log4j2_rocketmq_client.xml b/client/src/main/resources/log4j2_rocketmq_client.xml deleted file mode 100644 index 651553e0b09..00000000000 --- a/client/src/main/resources/log4j2_rocketmq_client.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/client/src/main/resources/log4j_rocketmq_client.xml b/client/src/main/resources/log4j_rocketmq_client.xml deleted file mode 100644 index cc86b71736b..00000000000 --- a/client/src/main/resources/log4j_rocketmq_client.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/main/resources/logback_rocketmq_client.xml b/client/src/main/resources/logback_rocketmq_client.xml deleted file mode 100644 index c045b42072e..00000000000 --- a/client/src/main/resources/logback_rocketmq_client.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - ${client.logRoot}/rocketmq_client.log - true - - ${client.logRoot}/otherdays/rocketmq_client.%i.log - - 1 - ${client.logFileMaxIndex} - - - 100MB - - - %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n - UTF-8 - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/main/resources/rmq.client.logback.xml b/client/src/main/resources/rmq.client.logback.xml new file mode 100644 index 00000000000..8e57d8d33ec --- /dev/null +++ b/client/src/main/resources/rmq.client.logback.xml @@ -0,0 +1,55 @@ + + + + + + + + %yellow(%d{yyy-MM-dd HH:mm:ss.SSS,GMT+8}) %highlight(%-5p) %boldWhite([%pid]) %magenta([%t]) %boldGreen([%logger{12}#%M:%L]) - %m%n + + UTF-8 + + + + true + + ${rocketmq.log.root:-${user.home}${file.separator}logs${file.separator}rocketmqlogs}${file.separator}rocketmq_client.log + + + + ${rocketmq.log.root:-${user.home}${file.separator}logs${file.separator}rocketmqlogs}${file.separator}other_days${file.separator}rocketmq_client-%i.log.gz + + 1 + ${rocketmq.log.file.maxIndex:-10} + + + 64MB + + + %d{yyy-MM-dd HH:mm:ss.SSS,GMT+8} %-5p [%pid] [%t] [%logger{12}#%M:%L] - %m%n + UTF-8 + + + + + + + + + + + + diff --git a/client/src/test/java/org/apache/rocketmq/client/ValidatorsTest.java b/client/src/test/java/org/apache/rocketmq/client/ValidatorsTest.java index 2db648d8626..b00c5d1450a 100644 --- a/client/src/test/java/org/apache/rocketmq/client/ValidatorsTest.java +++ b/client/src/test/java/org/apache/rocketmq/client/ValidatorsTest.java @@ -17,16 +17,31 @@ package org.apache.rocketmq.client; +import java.util.Properties; import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.client.exception.MQClientException; -import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.constant.PermName; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.remoting.protocol.ResponseCode; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Fail.failBecauseExceptionWasNotThrown; +import static org.junit.Assert.fail; public class ValidatorsTest { + @Test + public void testGroupNameBlank() { + try { + Validators.checkGroup(null); + fail("excepted MQClientException for group name is blank"); + } catch (MQClientException e) { + assertThat(e.getErrorMessage()).isEqualTo("the specified group is blank"); + } + } + @Test public void testCheckTopic_Success() throws MQClientException { Validators.checkTopic("Hello"); @@ -43,18 +58,7 @@ public void testCheckTopic_HasIllegalCharacters() { Validators.checkTopic(illegalTopic); failBecauseExceptionWasNotThrown(MQClientException.class); } catch (MQClientException e) { - assertThat(e).hasMessageStartingWith(String.format("The specified topic[%s] contains illegal characters, allowing only %s", illegalTopic, Validators.VALID_PATTERN_STR)); - } - } - - @Test - public void testCheckTopic_UseDefaultTopic() { - String defaultTopic = MixAll.DEFAULT_TOPIC; - try { - Validators.checkTopic(defaultTopic); - failBecauseExceptionWasNotThrown(MQClientException.class); - } catch (MQClientException e) { - assertThat(e).hasMessageStartingWith(String.format("The topic[%s] is conflict with default topic.", defaultTopic)); + assertThat(e).hasMessageStartingWith(String.format("The specified topic[%s] contains illegal characters, allowing only %s", illegalTopic, "^[%|a-zA-Z0-9_-]+$")); } } @@ -71,13 +75,98 @@ public void testCheckTopic_BlankTopic() { @Test public void testCheckTopic_TooLongTopic() { - String tooLongTopic = StringUtils.rightPad("TooLongTopic", Validators.CHARACTER_MAX_LENGTH + 1, "_"); - assertThat(tooLongTopic.length()).isGreaterThan(Validators.CHARACTER_MAX_LENGTH); + String tooLongTopic = StringUtils.rightPad("TooLongTopic", Validators.TOPIC_MAX_LENGTH + 1, "_"); + assertThat(tooLongTopic.length()).isGreaterThan(Validators.TOPIC_MAX_LENGTH); try { Validators.checkTopic(tooLongTopic); failBecauseExceptionWasNotThrown(MQClientException.class); } catch (MQClientException e) { - assertThat(e).hasMessageStartingWith("The specified topic is longer than topic max length 255."); + assertThat(e).hasMessageStartingWith("The specified topic is longer than topic max length"); + } + } + + @Test + public void testIsSystemTopic() { + for (String topic : TopicValidator.getSystemTopicSet()) { + try { + Validators.isSystemTopic(topic); + fail("excepted MQClientException for system topic"); + } catch (MQClientException e) { + assertThat(e.getResponseCode()).isEqualTo(-1); + assertThat(e.getErrorMessage()).isEqualTo(String.format("The topic[%s] is conflict with system topic.", topic)); + } + } + } + + @Test + public void testIsNotAllowedSendTopic() { + for (String topic : TopicValidator.getNotAllowedSendTopicSet()) { + try { + Validators.isNotAllowedSendTopic(topic); + fail("excepted MQClientException for blacklist topic"); + } catch (MQClientException e) { + assertThat(e.getResponseCode()).isEqualTo(-1); + assertThat(e.getErrorMessage()).isEqualTo(String.format("Sending message to topic[%s] is forbidden.", topic)); + } + } + } + + @Test + public void testTopicConfigValid() throws MQClientException { + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setPerm(PermName.PERM_INHERIT | PermName.PERM_WRITE | PermName.PERM_READ); + Validators.checkTopicConfig(topicConfig); + + topicConfig.setPerm(PermName.PERM_WRITE | PermName.PERM_READ); + Validators.checkTopicConfig(topicConfig); + + topicConfig.setPerm(PermName.PERM_READ); + Validators.checkTopicConfig(topicConfig); + + try { + topicConfig.setPerm(PermName.PERM_PRIORITY); + Validators.checkTopicConfig(topicConfig); + } catch (MQClientException e) { + assertThat(e.getResponseCode()).isEqualTo(ResponseCode.NO_PERMISSION); + assertThat(e.getErrorMessage()).isEqualTo(String.format("topicPermission value: %s is invalid.", topicConfig.getPerm())); + } + + try { + topicConfig.setPerm(PermName.PERM_PRIORITY | PermName.PERM_WRITE); + Validators.checkTopicConfig(topicConfig); + } catch (MQClientException e) { + assertThat(e.getResponseCode()).isEqualTo(ResponseCode.NO_PERMISSION); + assertThat(e.getErrorMessage()).isEqualTo(String.format("topicPermission value: %s is invalid.", topicConfig.getPerm())); + } + } + + @Test + public void testBrokerConfigValid() throws MQClientException { + Properties brokerConfig = new Properties(); + brokerConfig.setProperty("brokerPermission", + String.valueOf(PermName.PERM_INHERIT | PermName.PERM_WRITE | PermName.PERM_READ)); + Validators.checkBrokerConfig(brokerConfig); + + brokerConfig.setProperty("brokerPermission", String.valueOf(PermName.PERM_WRITE | PermName.PERM_READ)); + Validators.checkBrokerConfig(brokerConfig); + + brokerConfig.setProperty("brokerPermission", String.valueOf(PermName.PERM_READ)); + Validators.checkBrokerConfig(brokerConfig); + + try { + brokerConfig.setProperty("brokerPermission", String.valueOf(PermName.PERM_PRIORITY)); + Validators.checkBrokerConfig(brokerConfig); + } catch (MQClientException e) { + assertThat(e.getResponseCode()).isEqualTo(ResponseCode.NO_PERMISSION); + assertThat(e.getErrorMessage()).isEqualTo(String.format("brokerPermission value: %s is invalid.", brokerConfig.getProperty("brokerPermission"))); + } + + try { + brokerConfig.setProperty("brokerPermission", String.valueOf(PermName.PERM_PRIORITY | PermName.PERM_INHERIT)); + Validators.checkBrokerConfig(brokerConfig); + } catch (MQClientException e) { + assertThat(e.getResponseCode()).isEqualTo(ResponseCode.NO_PERMISSION); + assertThat(e.getErrorMessage()).isEqualTo(String.format("brokerPermission value: %s is invalid.", brokerConfig.getProperty("brokerPermission"))); } } } diff --git a/client/src/test/java/org/apache/rocketmq/client/common/ThreadLocalIndexTest.java b/client/src/test/java/org/apache/rocketmq/client/common/ThreadLocalIndexTest.java index 1be93ce0f3a..87a71df92b4 100644 --- a/client/src/test/java/org/apache/rocketmq/client/common/ThreadLocalIndexTest.java +++ b/client/src/test/java/org/apache/rocketmq/client/common/ThreadLocalIndexTest.java @@ -16,17 +16,46 @@ */ package org.apache.rocketmq.client.common; +import java.lang.reflect.Field; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; public class ThreadLocalIndexTest { @Test - public void testGetAndIncrement() throws Exception { + public void testIncrementAndGet() throws Exception { ThreadLocalIndex localIndex = new ThreadLocalIndex(); - int initialVal = localIndex.getAndIncrement(); + int initialVal = localIndex.incrementAndGet(); - assertThat(localIndex.getAndIncrement()).isEqualTo(initialVal + 1); + assertThat(localIndex.incrementAndGet()).isEqualTo(initialVal + 1); + } + + @Test + public void testIncrementAndGet2() throws Exception { + ThreadLocalIndex localIndex = new ThreadLocalIndex(); + int initialVal = localIndex.incrementAndGet(); + assertThat(initialVal >= 0).isTrue(); + } + + @Test + public void testIncrementAndGet3() throws Exception { + ThreadLocalIndex localIndex = new ThreadLocalIndex(); + Field threadLocalIndexField = ThreadLocalIndex.class.getDeclaredField("threadLocalIndex"); + ThreadLocal mockThreadLocal = new ThreadLocal<>(); + mockThreadLocal.set(Integer.MAX_VALUE); + + threadLocalIndexField.setAccessible(true); + threadLocalIndexField.set(localIndex, mockThreadLocal); + + int initialVal = localIndex.incrementAndGet(); + assertThat(initialVal >= 0).isTrue(); + } + + @Test + public void testResultOfResetIsGreaterThanOrEqualToZero() { + ThreadLocalIndex localIndex = new ThreadLocalIndex(); + localIndex.reset(); + assertThat(localIndex.incrementAndGet() > 0).isTrue(); } } \ No newline at end of file diff --git a/client/src/test/java/org/apache/rocketmq/client/consumer/DefaultLitePullConsumerTest.java b/client/src/test/java/org/apache/rocketmq/client/consumer/DefaultLitePullConsumerTest.java new file mode 100644 index 00000000000..24e39f56689 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/consumer/DefaultLitePullConsumerTest.java @@ -0,0 +1,901 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client.consumer; + +import java.io.ByteArrayOutputStream; +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.client.consumer.store.OffsetStore; +import org.apache.rocketmq.client.consumer.store.ReadOffsetType; +import org.apache.rocketmq.client.consumer.store.RemoteBrokerOffsetStore; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.impl.CommunicationMode; +import org.apache.rocketmq.client.impl.FindBrokerResult; +import org.apache.rocketmq.client.impl.MQAdminImpl; +import org.apache.rocketmq.client.impl.MQClientAPIImpl; +import org.apache.rocketmq.client.impl.MQClientManager; +import org.apache.rocketmq.client.impl.consumer.AssignedMessageQueue; +import org.apache.rocketmq.client.impl.consumer.DefaultLitePullConsumerImpl; +import org.apache.rocketmq.client.impl.consumer.PullAPIWrapper; +import org.apache.rocketmq.client.impl.consumer.PullResultExt; +import org.apache.rocketmq.client.impl.consumer.RebalanceImpl; +import org.apache.rocketmq.client.impl.consumer.RebalanceService; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.MessageClientExt; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Fail.failBecauseExceptionWasNotThrown; +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class DefaultLitePullConsumerTest { + @Spy + private MQClientInstance mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(new ClientConfig()); + + private MQClientInstance mqClientInstance; + + @Mock + private MQClientAPIImpl mQClientAPIImpl; + @Mock + private MQAdminImpl mQAdminImpl; + @Mock + private AssignedMessageQueue assignedMQ; + + private RebalanceImpl rebalanceImpl; + private OffsetStore offsetStore; + private DefaultLitePullConsumerImpl litePullConsumerImpl; + private String consumerGroup = "LitePullConsumerGroup"; + private String topic = "LitePullConsumerTest"; + private String brokerName = "BrokerA"; + private boolean flag = false; + + @BeforeClass + public static void setEnv() { + System.setProperty("rocketmq.client.logRoot", System.getProperty("java.io.tmpdir")); + } + + @Before + public void init() throws Exception { + ConcurrentMap factoryTable = (ConcurrentMap) FieldUtils.readDeclaredField(MQClientManager.getInstance(), "factoryTable", true); + factoryTable.forEach((s, instance) -> instance.shutdown()); + factoryTable.clear(); + + Field field = MQClientInstance.class.getDeclaredField("rebalanceService"); + field.setAccessible(true); + RebalanceService rebalanceService = (RebalanceService) field.get(mQClientFactory); + field = RebalanceService.class.getDeclaredField("waitInterval"); + field.setAccessible(true); + field.set(rebalanceService, 100); + + field = DefaultLitePullConsumerImpl.class.getDeclaredField("doNotUpdateTopicSubscribeInfoWhenSubscriptionChanged"); + field.setAccessible(true); + field.set(null, true); + } + + @After + public void destroy() { + if (mqClientInstance != null) { + mqClientInstance.unregisterConsumer(litePullConsumerImpl.groupName()); + mqClientInstance.shutdown(); + } + } + + @Test + public void testAssign_PollMessageSuccess() throws Exception { + DefaultLitePullConsumer litePullConsumer = createStartLitePullConsumer(); + try { + MessageQueue messageQueue = createMessageQueue(); + litePullConsumer.assign(Collections.singletonList(messageQueue)); + List result = litePullConsumer.poll(); + assertThat(result.get(0).getTopic()).isEqualTo(topic); + assertThat(result.get(0).getBody()).isEqualTo(new byte[] {'a'}); + } finally { + litePullConsumer.shutdown(); + } + } + + @Test + public void testSubscribeWithListener_PollMessageSuccess() throws Exception { + DefaultLitePullConsumer litePullConsumer = createSubscribeLitePullConsumerWithListener(); + try { + Set messageQueueSet = new HashSet<>(); + messageQueueSet.add(createMessageQueue()); + litePullConsumerImpl.updateTopicSubscribeInfo(topic, messageQueueSet); + litePullConsumer.setPollTimeoutMillis(20 * 1000); + List result = litePullConsumer.poll(); + assertThat(result.get(0).getTopic()).isEqualTo(topic); + assertThat(result.get(0).getBody()).isEqualTo(new byte[] {'a'}); + + Set assignment = litePullConsumer.assignment(); + assertThat(assignment.stream().findFirst().get()).isEqualTo(messageQueueSet.stream().findFirst().get()); + } finally { + litePullConsumer.shutdown(); + } + } + + @Test + public void testAssign_PollMessageWithTagSuccess() throws Exception { + DefaultLitePullConsumer litePullConsumer = createStartLitePullConsumerWithTag(); + try { + MessageQueue messageQueue = createMessageQueue(); + litePullConsumer.assign(Collections.singletonList(messageQueue)); + List result = litePullConsumer.poll(); + assertThat(result.get(0).getTopic()).isEqualTo(topic); + assertThat(result.get(0).getTags()).isEqualTo("tagA"); + assertThat(result.get(0).getBody()).isEqualTo(new byte[] {'a'}); + } finally { + litePullConsumer.shutdown(); + } + } + + @Test + public void testConsumerCommitSyncWithMQOffset() throws Exception { + DefaultLitePullConsumer litePullConsumer = createNotStartLitePullConsumer(); + try { + RemoteBrokerOffsetStore store = new RemoteBrokerOffsetStore(mQClientFactory, consumerGroup); + litePullConsumer.setOffsetStore(store); + litePullConsumer.start(); + initDefaultLitePullConsumer(litePullConsumer); + + //replace with real offsetStore. + Field offsetStore = litePullConsumerImpl.getClass().getDeclaredField("offsetStore"); + offsetStore.setAccessible(true); + offsetStore.set(litePullConsumerImpl, store); + + MessageQueue messageQueue = createMessageQueue(); + HashSet set = new HashSet<>(); + set.add(messageQueue); + + //mock assign and reset offset + litePullConsumer.assign(set); + litePullConsumer.seek(messageQueue, 0); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> assertThat(litePullConsumer.committed(messageQueue)).isEqualTo(0)); + //commit offset 1 + Map commitOffset = new HashMap<>(); + commitOffset.put(messageQueue, 1L); + litePullConsumer.commit(commitOffset, true); + + assertThat(litePullConsumer.committed(messageQueue)).isEqualTo(1); + } finally { + litePullConsumer.shutdown(); + } + } + + @Test + public void testSubscribe_PollMessageSuccess() throws Exception { + DefaultLitePullConsumer litePullConsumer = createSubscribeLitePullConsumer(); + try { + Set messageQueueSet = new HashSet<>(); + messageQueueSet.add(createMessageQueue()); + litePullConsumerImpl.updateTopicSubscribeInfo(topic, messageQueueSet); + litePullConsumer.setPollTimeoutMillis(20 * 1000); + List result = litePullConsumer.poll(); + assertThat(result.get(0).getTopic()).isEqualTo(topic); + assertThat(result.get(0).getBody()).isEqualTo(new byte[] {'a'}); + } finally { + litePullConsumer.shutdown(); + } + } + + @Test + public void testSubscribe_BroadcastPollMessageSuccess() throws Exception { + DefaultLitePullConsumer litePullConsumer = createBroadcastLitePullConsumer(); + try { + Set messageQueueSet = new HashSet<>(); + messageQueueSet.add(createMessageQueue()); + litePullConsumerImpl.updateTopicSubscribeInfo(topic, messageQueueSet); + litePullConsumer.setPollTimeoutMillis(20 * 1000); + List result = litePullConsumer.poll(); + assertThat(result.get(0).getTopic()).isEqualTo(topic); + assertThat(result.get(0).getBody()).isEqualTo(new byte[] {'a'}); + } finally { + litePullConsumer.shutdown(); + } + } + + @Test + public void testSubscriptionType_AssignAndSubscribeExclusive() throws Exception { + DefaultLitePullConsumer litePullConsumer = createStartLitePullConsumer(); + try { + litePullConsumer.subscribe(topic, "*"); + litePullConsumer.assign(Collections.singletonList(createMessageQueue())); + failBecauseExceptionWasNotThrown(IllegalStateException.class); + } catch (IllegalStateException e) { + assertThat(e).hasMessageContaining("Subscribe and assign are mutually exclusive."); + } finally { + litePullConsumer.shutdown(); + } + } + + @Test + public void testFetchMessageQueues_FetchMessageQueuesBeforeStart() throws Exception { + DefaultLitePullConsumer litePullConsumer = createNotStartLitePullConsumer(); + try { + litePullConsumer.fetchMessageQueues(topic); + failBecauseExceptionWasNotThrown(IllegalStateException.class); + } catch (IllegalStateException e) { + assertThat(e).hasMessageContaining("The consumer not running, please start it first."); + } finally { + litePullConsumer.shutdown(); + } + } + + @Test + public void testSeek_SeekOffsetSuccess() throws Exception { + DefaultLitePullConsumer litePullConsumer = createStartLitePullConsumer(); + when(mQAdminImpl.minOffset(any(MessageQueue.class))).thenReturn(0L); + when(mQAdminImpl.maxOffset(any(MessageQueue.class))).thenReturn(500L); + MessageQueue messageQueue = createMessageQueue(); + List messageQueues = Collections.singletonList(messageQueue); + litePullConsumer.assign(messageQueues); + litePullConsumer.pause(messageQueues); + litePullConsumer.pause(Collections.singletonList(messageQueue)); + long offset = litePullConsumer.committed(messageQueue); + litePullConsumer.seek(messageQueue, offset); + Field field = DefaultLitePullConsumerImpl.class.getDeclaredField("assignedMessageQueue"); + field.setAccessible(true); + AssignedMessageQueue assignedMessageQueue = (AssignedMessageQueue) field.get(litePullConsumerImpl); + assertEquals(assignedMessageQueue.getSeekOffset(messageQueue), offset); + litePullConsumer.shutdown(); + } + + @Test + public void testSeek_SeekToBegin() throws Exception { + DefaultLitePullConsumer litePullConsumer = createStartLitePullConsumer(); + when(mQAdminImpl.minOffset(any(MessageQueue.class))).thenReturn(0L); + when(mQAdminImpl.maxOffset(any(MessageQueue.class))).thenReturn(500L); + MessageQueue messageQueue = createMessageQueue(); + List messageQueues = Collections.singletonList(messageQueue); + litePullConsumer.assign(messageQueues); + litePullConsumer.pause(messageQueues); + litePullConsumer.pause(Collections.singletonList(messageQueue)); + litePullConsumer.seekToBegin(messageQueue); + Field field = DefaultLitePullConsumerImpl.class.getDeclaredField("assignedMessageQueue"); + field.setAccessible(true); + AssignedMessageQueue assignedMessageQueue = (AssignedMessageQueue) field.get(litePullConsumerImpl); + assertEquals(assignedMessageQueue.getSeekOffset(messageQueue), 0L); + litePullConsumer.shutdown(); + } + + @Test + public void testSeek_SeekToEnd() throws Exception { + DefaultLitePullConsumer litePullConsumer = createStartLitePullConsumer(); + when(mQAdminImpl.minOffset(any(MessageQueue.class))).thenReturn(0L); + when(mQAdminImpl.maxOffset(any(MessageQueue.class))).thenReturn(500L); + MessageQueue messageQueue = createMessageQueue(); + List messageQueues = Collections.singletonList(messageQueue); + litePullConsumer.assign(messageQueues); + litePullConsumer.pause(messageQueues); + litePullConsumer.pause(Collections.singletonList(messageQueue)); + litePullConsumer.seekToEnd(messageQueue); + Field field = DefaultLitePullConsumerImpl.class.getDeclaredField("assignedMessageQueue"); + field.setAccessible(true); + AssignedMessageQueue assignedMessageQueue = (AssignedMessageQueue) field.get(litePullConsumerImpl); + assertEquals(assignedMessageQueue.getSeekOffset(messageQueue), 500L); + litePullConsumer.shutdown(); + } + + @Test + public void testSeek_SeekOffsetIllegal() throws Exception { + DefaultLitePullConsumer litePullConsumer = createStartLitePullConsumer(); + when(mQAdminImpl.minOffset(any(MessageQueue.class))).thenReturn(0L); + when(mQAdminImpl.maxOffset(any(MessageQueue.class))).thenReturn(100L); + MessageQueue messageQueue = createMessageQueue(); + List messageQueues = Collections.singletonList(messageQueue); + litePullConsumer.assign(messageQueues); + litePullConsumer.pause(messageQueues); + litePullConsumer.pause(Collections.singletonList(messageQueue)); + try { + litePullConsumer.seek(messageQueue, -1); + failBecauseExceptionWasNotThrown(MQClientException.class); + } catch (MQClientException e) { + assertThat(e).hasMessageContaining("min offset = 0"); + } + + try { + litePullConsumer.seek(messageQueue, 1000); + failBecauseExceptionWasNotThrown(MQClientException.class); + } catch (MQClientException e) { + assertThat(e).hasMessageContaining("max offset = 100"); + } + litePullConsumer.shutdown(); + } + + @Test + public void testSeek_MessageQueueNotInAssignList() throws Exception { + DefaultLitePullConsumer litePullConsumer = createStartLitePullConsumer(); + try { + litePullConsumer.seek(createMessageQueue(), 0); + failBecauseExceptionWasNotThrown(MQClientException.class); + } catch (MQClientException e) { + assertThat(e).hasMessageContaining("The message queue is not in assigned list"); + } finally { + litePullConsumer.shutdown(); + } + + litePullConsumer = createSubscribeLitePullConsumer(); + try { + litePullConsumer.seek(createMessageQueue(), 0); + failBecauseExceptionWasNotThrown(MQClientException.class); + } catch (MQClientException e) { + assertThat(e).hasMessageContaining("The message queue is not in assigned list, may be rebalancing"); + } finally { + litePullConsumer.shutdown(); + } + } + + @Test + public void testOffsetForTimestamp_FailedAndSuccess() throws Exception { + MessageQueue messageQueue = createMessageQueue(); + DefaultLitePullConsumer litePullConsumer = createNotStartLitePullConsumer(); + try { + litePullConsumer.offsetForTimestamp(messageQueue, 123456L); + failBecauseExceptionWasNotThrown(IllegalStateException.class); + } catch (IllegalStateException e) { + assertThat(e).hasMessageContaining("The consumer not running, please start it first."); + } finally { + litePullConsumer.shutdown(); + } + doReturn(123L).when(mQAdminImpl).searchOffset(any(MessageQueue.class), anyLong()); + litePullConsumer = createStartLitePullConsumer(); + try { + long offset = litePullConsumer.offsetForTimestamp(messageQueue, 123456L); + assertThat(offset).isEqualTo(123L); + } finally { + litePullConsumer.shutdown(); + } + } + + @Test + public void testPauseAndResume_Success() throws Exception { + DefaultLitePullConsumer litePullConsumer = createNotStartLitePullConsumer(); + try { + MessageQueue messageQueue = createMessageQueue(); + litePullConsumer.assign(Collections.singletonList(messageQueue)); + litePullConsumer.pause(Collections.singletonList(messageQueue)); + litePullConsumer.start(); + initDefaultLitePullConsumer(litePullConsumer); + List result = litePullConsumer.poll(); + assertThat(result.isEmpty()).isTrue(); + litePullConsumer.resume(Collections.singletonList(messageQueue)); + result = litePullConsumer.poll(); + assertThat(result.get(0).getTopic()).isEqualTo(topic); + assertThat(result.get(0).getBody()).isEqualTo(new byte[] {'a'}); + } finally { + litePullConsumer.shutdown(); + } + } + + @Test + public void testPullTaskImpl_ProcessQueueNull() throws Exception { + DefaultLitePullConsumer litePullConsumer = createNotStartLitePullConsumer(); + try { + MessageQueue messageQueue = createMessageQueue(); + litePullConsumer.assign(Collections.singletonList(messageQueue)); + Field field = DefaultLitePullConsumer.class.getDeclaredField("defaultLitePullConsumerImpl"); + field.setAccessible(true); + // set ProcessQueue dropped = true + DefaultLitePullConsumerImpl localLitePullConsumerImpl = (DefaultLitePullConsumerImpl) field.get(litePullConsumer); + field = DefaultLitePullConsumerImpl.class.getDeclaredField("assignedMessageQueue"); + field.setAccessible(true); + when(assignedMQ.isPaused(any(MessageQueue.class))).thenReturn(false); + when(assignedMQ.getProcessQueue(any(MessageQueue.class))).thenReturn(null); + litePullConsumer.start(); + field.set(localLitePullConsumerImpl, assignedMQ); + + List result = litePullConsumer.poll(100); + assertThat(result.isEmpty()).isTrue(); + } finally { + litePullConsumer.shutdown(); + } + } + + @Test + public void testPullTaskImpl_ProcessQueueDropped() throws Exception { + DefaultLitePullConsumer litePullConsumer = createNotStartLitePullConsumer(); + try { + MessageQueue messageQueue = createMessageQueue(); + litePullConsumer.assign(Collections.singletonList(messageQueue)); + Field field = DefaultLitePullConsumer.class.getDeclaredField("defaultLitePullConsumerImpl"); + field.setAccessible(true); + // set ProcessQueue dropped = true + DefaultLitePullConsumerImpl localLitePullConsumerImpl = (DefaultLitePullConsumerImpl) field.get(litePullConsumer); + field = DefaultLitePullConsumerImpl.class.getDeclaredField("assignedMessageQueue"); + field.setAccessible(true); + AssignedMessageQueue assignedMessageQueue = (AssignedMessageQueue) field.get(localLitePullConsumerImpl); + assignedMessageQueue.getProcessQueue(messageQueue).setDropped(true); + litePullConsumer.start(); + + List result = litePullConsumer.poll(100); + assertThat(result.isEmpty()).isTrue(); + } finally { + litePullConsumer.shutdown(); + } + } + + @Test + public void testRegisterTopicMessageQueueChangeListener_Success() throws Exception { + flag = false; + DefaultLitePullConsumer litePullConsumer = createStartLitePullConsumer(); + try { + doReturn(Collections.emptySet()).when(mQAdminImpl).fetchSubscribeMessageQueues(anyString()); + litePullConsumer.setTopicMetadataCheckIntervalMillis(10); + litePullConsumer.registerTopicMessageQueueChangeListener(topic, new TopicMessageQueueChangeListener() { + @Override + public void onChanged(String topic, Set messageQueues) { + flag = true; + } + }); + Set set = new HashSet<>(); + set.add(createMessageQueue()); + doReturn(set).when(mQAdminImpl).fetchSubscribeMessageQueues(anyString()); + Thread.sleep(11 * 1000); + assertThat(flag).isTrue(); + } finally { + litePullConsumer.shutdown(); + } + } + + @Test + public void testFlowControl_Success() throws Exception { + DefaultLitePullConsumer litePullConsumer = createStartLitePullConsumer(); + try { + MessageQueue messageQueue = createMessageQueue(); + litePullConsumer.setPullThresholdForAll(-1); + litePullConsumer.assign(Collections.singletonList(messageQueue)); + litePullConsumer.setPollTimeoutMillis(500); + List result = litePullConsumer.poll(); + assertThat(result).isEmpty(); + } finally { + litePullConsumer.shutdown(); + } + + litePullConsumer = createStartLitePullConsumer(); + try { + MessageQueue messageQueue = createMessageQueue(); + litePullConsumer.setPullThresholdForQueue(-1); + litePullConsumer.assign(Collections.singletonList(messageQueue)); + litePullConsumer.setPollTimeoutMillis(500); + List result = litePullConsumer.poll(); + assertThat(result).isEmpty(); + } finally { + litePullConsumer.shutdown(); + } + + litePullConsumer = createStartLitePullConsumer(); + try { + MessageQueue messageQueue = createMessageQueue(); + litePullConsumer.setPullThresholdSizeForQueue(-1); + litePullConsumer.assign(Collections.singletonList(messageQueue)); + litePullConsumer.setPollTimeoutMillis(500); + List result = litePullConsumer.poll(); + assertThat(result).isEmpty(); + } finally { + litePullConsumer.shutdown(); + } + + litePullConsumer = createStartLitePullConsumer(); + try { + MessageQueue messageQueue = createMessageQueue(); + litePullConsumer.setConsumeMaxSpan(-1); + litePullConsumer.assign(Collections.singletonList(messageQueue)); + litePullConsumer.setPollTimeoutMillis(500); + List result = litePullConsumer.poll(); + assertThat(result).isEmpty(); + } finally { + litePullConsumer.shutdown(); + } + } + + @Test + public void testCheckConfig_Exception() { + DefaultLitePullConsumer litePullConsumer = new DefaultLitePullConsumer(MixAll.DEFAULT_CONSUMER_GROUP); + try { + litePullConsumer.start(); + failBecauseExceptionWasNotThrown(MQClientException.class); + } catch (MQClientException e) { + assertThat(e).hasMessageContaining("consumerGroup can not equal"); + } finally { + litePullConsumer.shutdown(); + } + + litePullConsumer = new DefaultLitePullConsumer(consumerGroup + System.currentTimeMillis()); + litePullConsumer.setMessageModel(null); + try { + litePullConsumer.start(); + failBecauseExceptionWasNotThrown(MQClientException.class); + } catch (MQClientException e) { + assertThat(e).hasMessageContaining("messageModel is null"); + } finally { + litePullConsumer.shutdown(); + } + + litePullConsumer = new DefaultLitePullConsumer(consumerGroup + System.currentTimeMillis()); + litePullConsumer.setAllocateMessageQueueStrategy(null); + try { + litePullConsumer.start(); + failBecauseExceptionWasNotThrown(MQClientException.class); + } catch (MQClientException e) { + assertThat(e).hasMessageContaining("allocateMessageQueueStrategy is null"); + } finally { + litePullConsumer.shutdown(); + } + + litePullConsumer = new DefaultLitePullConsumer(consumerGroup + System.currentTimeMillis()); + litePullConsumer.setConsumerTimeoutMillisWhenSuspend(1); + try { + litePullConsumer.start(); + failBecauseExceptionWasNotThrown(MQClientException.class); + } catch (MQClientException e) { + assertThat(e).hasMessageContaining("Long polling mode, the consumer consumerTimeoutMillisWhenSuspend must greater than brokerSuspendMaxTimeMillis"); + } finally { + litePullConsumer.shutdown(); + } + + } + + @Test + public void testComputePullFromWhereReturnedNotFound() throws Exception { + DefaultLitePullConsumer defaultLitePullConsumer = createStartLitePullConsumer(); + try { + defaultLitePullConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + MessageQueue messageQueue = createMessageQueue(); + when(offsetStore.readOffset(any(MessageQueue.class), any(ReadOffsetType.class))).thenReturn(-1L); + long offset = rebalanceImpl.computePullFromWhere(messageQueue); + assertThat(offset).isEqualTo(0); + } finally { + defaultLitePullConsumer.shutdown(); + } + } + + @Test + public void testComputePullFromWhereReturned() throws Exception { + DefaultLitePullConsumer defaultLitePullConsumer = createStartLitePullConsumer(); + defaultLitePullConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + MessageQueue messageQueue = createMessageQueue(); + when(offsetStore.readOffset(any(MessageQueue.class), any(ReadOffsetType.class))).thenReturn(100L); + long offset = rebalanceImpl.computePullFromWhere(messageQueue); + assertThat(offset).isEqualTo(100); + defaultLitePullConsumer.shutdown(); + } + + @Test + public void testComputePullFromLast() throws Exception { + DefaultLitePullConsumer defaultLitePullConsumer = createStartLitePullConsumer(); + defaultLitePullConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); + MessageQueue messageQueue = createMessageQueue(); + when(offsetStore.readOffset(any(MessageQueue.class), any(ReadOffsetType.class))).thenReturn(-1L); + when(mQClientFactory.getMQAdminImpl().maxOffset(any(MessageQueue.class))).thenReturn(100L); + long offset = rebalanceImpl.computePullFromWhere(messageQueue); + assertThat(offset).isEqualTo(100); + defaultLitePullConsumer.shutdown(); + } + + @Test + public void testComputePullByTimeStamp() throws Exception { + DefaultLitePullConsumer defaultLitePullConsumer = createStartLitePullConsumer(); + try { + defaultLitePullConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_TIMESTAMP); + defaultLitePullConsumer.setConsumeTimestamp("20191024171201"); + MessageQueue messageQueue = createMessageQueue(); + when(offsetStore.readOffset(any(MessageQueue.class), any(ReadOffsetType.class))).thenReturn(-1L); + when(mQClientFactory.getMQAdminImpl().searchOffset(any(MessageQueue.class), anyLong())).thenReturn(100L); + long offset = rebalanceImpl.computePullFromWhere(messageQueue); + assertThat(offset).isEqualTo(100); + } finally { + defaultLitePullConsumer.shutdown(); + } + } + + @Test + public void testConsumerAfterShutdown() throws Exception { + DefaultLitePullConsumer defaultLitePullConsumer = createSubscribeLitePullConsumer(); + + new AsyncConsumer().executeAsync(defaultLitePullConsumer); + + Thread.sleep(100); + defaultLitePullConsumer.shutdown(); + assertThat(defaultLitePullConsumer.isRunning()).isFalse(); + } + + @Test + public void testConsumerCommitWithMQ() throws Exception { + DefaultLitePullConsumer litePullConsumer = createNotStartLitePullConsumer(); + try { + RemoteBrokerOffsetStore store = new RemoteBrokerOffsetStore(mQClientFactory, consumerGroup); + litePullConsumer.setOffsetStore(store); + litePullConsumer.start(); + initDefaultLitePullConsumer(litePullConsumer); + + //replace with real offsetStore. + Field offsetStore = litePullConsumerImpl.getClass().getDeclaredField("offsetStore"); + offsetStore.setAccessible(true); + offsetStore.set(litePullConsumerImpl, store); + + MessageQueue messageQueue = createMessageQueue(); + HashSet set = new HashSet<>(); + set.add(messageQueue); + + //mock assign and reset offset + litePullConsumer.assign(set); + litePullConsumer.seek(messageQueue, 0); + + //commit + litePullConsumer.commit(set, true); + + assertThat(litePullConsumer.committed(messageQueue)).isEqualTo(0); + } finally { + litePullConsumer.shutdown(); + } + } + + static class AsyncConsumer { + public void executeAsync(final DefaultLitePullConsumer consumer) { + new Thread(() -> { + while (consumer.isRunning()) { + consumer.poll(2 * 1000); + } + }).start(); + } + } + + private void initDefaultLitePullConsumer(DefaultLitePullConsumer litePullConsumer) throws Exception { + Field field = DefaultLitePullConsumer.class.getDeclaredField("defaultLitePullConsumerImpl"); + field.setAccessible(true); + litePullConsumerImpl = (DefaultLitePullConsumerImpl) field.get(litePullConsumer); + field = DefaultLitePullConsumerImpl.class.getDeclaredField("mQClientFactory"); + field.setAccessible(true); + mqClientInstance = (MQClientInstance) field.get(litePullConsumerImpl); + field.set(litePullConsumerImpl, mQClientFactory); + + PullAPIWrapper pullAPIWrapper = litePullConsumerImpl.getPullAPIWrapper(); + field = PullAPIWrapper.class.getDeclaredField("mQClientFactory"); + field.setAccessible(true); + field.set(pullAPIWrapper, mQClientFactory); + + field = MQClientInstance.class.getDeclaredField("mQClientAPIImpl"); + field.setAccessible(true); + field.set(mQClientFactory, mQClientAPIImpl); + + field = MQClientInstance.class.getDeclaredField("mQAdminImpl"); + field.setAccessible(true); + field.set(mQClientFactory, mQAdminImpl); + + field = DefaultLitePullConsumerImpl.class.getDeclaredField("rebalanceImpl"); + field.setAccessible(true); + rebalanceImpl = (RebalanceImpl) field.get(litePullConsumerImpl); + field = RebalanceImpl.class.getDeclaredField("mQClientFactory"); + field.setAccessible(true); + field.set(rebalanceImpl, mQClientFactory); + + offsetStore = spy(litePullConsumerImpl.getOffsetStore()); + field = DefaultLitePullConsumerImpl.class.getDeclaredField("offsetStore"); + field.setAccessible(true); + field.set(litePullConsumerImpl, offsetStore); + + when(mQClientFactory.getMQClientAPIImpl().pullMessage(anyString(), any(PullMessageRequestHeader.class), + anyLong(), any(CommunicationMode.class), nullable(PullCallback.class))) + .thenAnswer(new Answer() { + @Override + public PullResult answer(InvocationOnMock mock) throws Throwable { + PullMessageRequestHeader requestHeader = mock.getArgument(1); + MessageClientExt messageClientExt = new MessageClientExt(); + messageClientExt.setTopic(topic); + messageClientExt.setQueueId(0); + messageClientExt.setMsgId("123"); + messageClientExt.setBody(new byte[] {'a'}); + messageClientExt.setOffsetMsgId("234"); + messageClientExt.setBornHost(new InetSocketAddress(8080)); + messageClientExt.setStoreHost(new InetSocketAddress(8080)); + PullResult pullResult = createPullResult(requestHeader, PullStatus.FOUND, Collections.singletonList(messageClientExt)); + return pullResult; + } + }); + + when(mQClientFactory.findBrokerAddressInSubscribe(anyString(), anyLong(), anyBoolean())).thenReturn(new FindBrokerResult("127.0.0.1:10911", false)); + + doReturn(Collections.singletonList(mQClientFactory.getClientId())).when(mQClientFactory).findConsumerIdList(anyString(), anyString()); + + doReturn(123L).when(offsetStore).readOffset(any(MessageQueue.class), any(ReadOffsetType.class)); + } + + private void initDefaultLitePullConsumerWithTag(DefaultLitePullConsumer litePullConsumer) throws Exception { + + Field field = DefaultLitePullConsumer.class.getDeclaredField("defaultLitePullConsumerImpl"); + field.setAccessible(true); + litePullConsumerImpl = (DefaultLitePullConsumerImpl) field.get(litePullConsumer); + field = DefaultLitePullConsumerImpl.class.getDeclaredField("mQClientFactory"); + field.setAccessible(true); + field.set(litePullConsumerImpl, mQClientFactory); + + PullAPIWrapper pullAPIWrapper = litePullConsumerImpl.getPullAPIWrapper(); + field = PullAPIWrapper.class.getDeclaredField("mQClientFactory"); + field.setAccessible(true); + field.set(pullAPIWrapper, mQClientFactory); + + field = MQClientInstance.class.getDeclaredField("mQClientAPIImpl"); + field.setAccessible(true); + field.set(mQClientFactory, mQClientAPIImpl); + + field = MQClientInstance.class.getDeclaredField("mQAdminImpl"); + field.setAccessible(true); + field.set(mQClientFactory, mQAdminImpl); + + field = DefaultLitePullConsumerImpl.class.getDeclaredField("rebalanceImpl"); + field.setAccessible(true); + rebalanceImpl = (RebalanceImpl) field.get(litePullConsumerImpl); + field = RebalanceImpl.class.getDeclaredField("mQClientFactory"); + field.setAccessible(true); + field.set(rebalanceImpl, mQClientFactory); + + offsetStore = spy(litePullConsumerImpl.getOffsetStore()); + field = DefaultLitePullConsumerImpl.class.getDeclaredField("offsetStore"); + field.setAccessible(true); + field.set(litePullConsumerImpl, offsetStore); + + when(mQClientFactory.getMQClientAPIImpl().pullMessage(anyString(), any(PullMessageRequestHeader.class), + anyLong(), any(CommunicationMode.class), nullable(PullCallback.class))) + .thenAnswer(new Answer() { + @Override + public PullResult answer(InvocationOnMock mock) throws Throwable { + PullMessageRequestHeader requestHeader = mock.getArgument(1); + MessageClientExt messageClientExt = new MessageClientExt(); + messageClientExt.setTopic(topic); + messageClientExt.setTags("tagA"); + messageClientExt.setQueueId(0); + messageClientExt.setMsgId("123"); + messageClientExt.setBody(new byte[] {'a'}); + messageClientExt.setOffsetMsgId("234"); + messageClientExt.setBornHost(new InetSocketAddress(8080)); + messageClientExt.setStoreHost(new InetSocketAddress(8080)); + PullResult pullResult = createPullResult(requestHeader, PullStatus.FOUND, Collections.singletonList(messageClientExt)); + return pullResult; + } + }); + + when(mQClientFactory.findBrokerAddressInSubscribe(anyString(), anyLong(), anyBoolean())).thenReturn(new FindBrokerResult("127.0.0.1:10911", false)); + + doReturn(123L).when(offsetStore).readOffset(any(MessageQueue.class), any(ReadOffsetType.class)); + } + + private DefaultLitePullConsumer createSubscribeLitePullConsumer() throws Exception { + DefaultLitePullConsumer litePullConsumer = new DefaultLitePullConsumer(consumerGroup + System.currentTimeMillis()); + litePullConsumer.setNamesrvAddr("127.0.0.1:9876"); + litePullConsumer.subscribe(topic, "*"); + suppressUpdateTopicRouteInfoFromNameServer(litePullConsumer); + litePullConsumer.start(); + initDefaultLitePullConsumer(litePullConsumer); + return litePullConsumer; + } + + private DefaultLitePullConsumer createSubscribeLitePullConsumerWithListener() throws Exception { + DefaultLitePullConsumer litePullConsumer = new DefaultLitePullConsumer(consumerGroup + System.currentTimeMillis()); + litePullConsumer.setNamesrvAddr("127.0.0.1:9876"); + litePullConsumer.subscribe(topic, "*", new MessageQueueListener() { + @Override + public void messageQueueChanged(String topic, Set mqAll, Set mqDivided) { + assertThat(mqAll.stream().findFirst().get().getTopic()).isEqualTo(mqDivided.stream().findFirst().get().getTopic()); + assertThat(mqAll.stream().findFirst().get().getBrokerName()).isEqualTo(mqDivided.stream().findFirst().get().getBrokerName()); + assertThat(mqAll.stream().findFirst().get().getQueueId()).isEqualTo(mqDivided.stream().findFirst().get().getQueueId()); + } + }); + suppressUpdateTopicRouteInfoFromNameServer(litePullConsumer); + litePullConsumer.start(); + initDefaultLitePullConsumer(litePullConsumer); + return litePullConsumer; + } + + private DefaultLitePullConsumer createStartLitePullConsumer() throws Exception { + DefaultLitePullConsumer litePullConsumer = new DefaultLitePullConsumer(consumerGroup + System.currentTimeMillis()); + litePullConsumer.setNamesrvAddr("127.0.0.1:9876"); + suppressUpdateTopicRouteInfoFromNameServer(litePullConsumer); + litePullConsumer.start(); + initDefaultLitePullConsumer(litePullConsumer); + return litePullConsumer; + } + + private DefaultLitePullConsumer createStartLitePullConsumerWithTag() throws Exception { + DefaultLitePullConsumer litePullConsumer = new DefaultLitePullConsumer(consumerGroup + System.currentTimeMillis()); + litePullConsumer.setNamesrvAddr("127.0.0.1:9876"); + suppressUpdateTopicRouteInfoFromNameServer(litePullConsumer); + litePullConsumer.setSubExpressionForAssign(topic, "tagA"); + litePullConsumer.start(); + initDefaultLitePullConsumerWithTag(litePullConsumer); + return litePullConsumer; + } + + private DefaultLitePullConsumer createNotStartLitePullConsumer() { + DefaultLitePullConsumer litePullConsumer = new DefaultLitePullConsumer(consumerGroup + System.currentTimeMillis()); + return litePullConsumer; + } + + private DefaultLitePullConsumer createBroadcastLitePullConsumer() throws Exception { + DefaultLitePullConsumer litePullConsumer = new DefaultLitePullConsumer(consumerGroup + System.currentTimeMillis()); + litePullConsumer.setNamesrvAddr("127.0.0.1:9876"); + litePullConsumer.setMessageModel(MessageModel.BROADCASTING); + litePullConsumer.subscribe(topic, "*"); + suppressUpdateTopicRouteInfoFromNameServer(litePullConsumer); + litePullConsumer.start(); + initDefaultLitePullConsumer(litePullConsumer); + return litePullConsumer; + } + + private MessageQueue createMessageQueue() { + MessageQueue messageQueue = new MessageQueue(); + messageQueue.setBrokerName(brokerName); + messageQueue.setQueueId(0); + messageQueue.setTopic(topic); + return messageQueue; + } + + private PullResultExt createPullResult(PullMessageRequestHeader requestHeader, PullStatus pullStatus, + List messageExtList) throws Exception { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + for (MessageExt messageExt : messageExtList) { + outputStream.write(MessageDecoder.encode(messageExt, false)); + } + return new PullResultExt(pullStatus, requestHeader.getQueueOffset() + messageExtList.size(), 123, 2048, messageExtList, 0, outputStream.toByteArray()); + } + + private void suppressUpdateTopicRouteInfoFromNameServer( + DefaultLitePullConsumer litePullConsumer) throws IllegalAccessException { + if (litePullConsumer.getMessageModel() == MessageModel.CLUSTERING) { + litePullConsumer.changeInstanceNameToPID(); + } + + ConcurrentMap factoryTable = (ConcurrentMap) FieldUtils.readDeclaredField(MQClientManager.getInstance(), "factoryTable", true); + factoryTable.put(litePullConsumer.buildMQClientId(), mQClientFactory); + doReturn(false).when(mQClientFactory).updateTopicRouteInfoFromNameServer(anyString()); + } +} diff --git a/client/src/test/java/org/apache/rocketmq/client/consumer/DefaultMQPullConsumerTest.java b/client/src/test/java/org/apache/rocketmq/client/consumer/DefaultMQPullConsumerTest.java index 569055dea25..31788ac998c 100644 --- a/client/src/test/java/org/apache/rocketmq/client/consumer/DefaultMQPullConsumerTest.java +++ b/client/src/test/java/org/apache/rocketmq/client/consumer/DefaultMQPullConsumerTest.java @@ -30,7 +30,7 @@ import org.apache.rocketmq.client.impl.factory.MQClientInstance; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -54,7 +54,7 @@ @RunWith(MockitoJUnitRunner.class) public class DefaultMQPullConsumerTest { @Spy - private MQClientInstance mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(new ClientConfig()); + private MQClientInstance mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(new ClientConfig()); @Mock private MQClientAPIImpl mQClientAPIImpl; private DefaultMQPullConsumer pullConsumer; @@ -106,7 +106,7 @@ public Object answer(InvocationOnMock mock) throws Throwable { assertThat(pullResult.getNextBeginOffset()).isEqualTo(1024 + 1); assertThat(pullResult.getMinOffset()).isEqualTo(123); assertThat(pullResult.getMaxOffset()).isEqualTo(2048); - assertThat(pullResult.getMsgFoundList()).isEqualTo(new ArrayList()); + assertThat(pullResult.getMsgFoundList()).isEqualTo(new ArrayList<>()); } @Test @@ -115,7 +115,7 @@ public void testPullMessage_NotFound() throws Exception { @Override public Object answer(InvocationOnMock mock) throws Throwable { PullMessageRequestHeader requestHeader = mock.getArgument(1); - return createPullResult(requestHeader, PullStatus.NO_NEW_MSG, new ArrayList()); + return createPullResult(requestHeader, PullStatus.NO_NEW_MSG, new ArrayList<>()); } }).when(mQClientAPIImpl).pullMessage(anyString(), any(PullMessageRequestHeader.class), anyLong(), any(CommunicationMode.class), nullable(PullCallback.class)); @@ -147,7 +147,7 @@ public void onSuccess(PullResult pullResult) { assertThat(pullResult.getNextBeginOffset()).isEqualTo(1024 + 1); assertThat(pullResult.getMinOffset()).isEqualTo(123); assertThat(pullResult.getMaxOffset()).isEqualTo(2048); - assertThat(pullResult.getMsgFoundList()).isEqualTo(new ArrayList()); + assertThat(pullResult.getMsgFoundList()).isEqualTo(new ArrayList<>()); } @Override @@ -161,4 +161,4 @@ private PullResultExt createPullResult(PullMessageRequestHeader requestHeader, P List messageExtList) throws Exception { return new PullResultExt(pullStatus, requestHeader.getQueueOffset() + messageExtList.size(), 123, 2048, messageExtList, 0, new byte[] {}); } -} \ No newline at end of file +} diff --git a/client/src/test/java/org/apache/rocketmq/client/consumer/DefaultMQPushConsumerTest.java b/client/src/test/java/org/apache/rocketmq/client/consumer/DefaultMQPushConsumerTest.java index b21edc9197c..3943b922899 100644 --- a/client/src/test/java/org/apache/rocketmq/client/consumer/DefaultMQPushConsumerTest.java +++ b/client/src/test/java/org/apache/rocketmq/client/consumer/DefaultMQPushConsumerTest.java @@ -22,9 +22,15 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.commons.lang3.reflect.FieldUtils; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext; @@ -36,6 +42,7 @@ import org.apache.rocketmq.client.impl.CommunicationMode; import org.apache.rocketmq.client.impl.FindBrokerResult; import org.apache.rocketmq.client.impl.MQClientAPIImpl; +import org.apache.rocketmq.client.impl.MQClientManager; import org.apache.rocketmq.client.impl.consumer.ConsumeMessageConcurrentlyService; import org.apache.rocketmq.client.impl.consumer.ConsumeMessageOrderlyService; import org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl; @@ -44,15 +51,17 @@ import org.apache.rocketmq.client.impl.consumer.PullMessageService; import org.apache.rocketmq.client.impl.consumer.PullRequest; import org.apache.rocketmq.client.impl.consumer.PullResultExt; -import org.apache.rocketmq.client.impl.consumer.RebalancePushImpl; +import org.apache.rocketmq.client.impl.consumer.RebalanceImpl; import org.apache.rocketmq.client.impl.factory.MQClientInstance; import org.apache.rocketmq.common.message.MessageClientExt; import org.apache.rocketmq.common.message.MessageDecoder; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.RPCHook; import org.apache.rocketmq.remoting.exception.RemotingException; -import org.junit.After; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -66,6 +75,7 @@ import static org.assertj.core.api.Fail.failBecauseExceptionWasNotThrown; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.nullable; @@ -73,24 +83,55 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; -@RunWith(MockitoJUnitRunner.class) +@RunWith(MockitoJUnitRunner.Silent.class) public class DefaultMQPushConsumerTest { private String consumerGroup; private String topic = "FooBar"; private String brokerName = "BrokerA"; private MQClientInstance mQClientFactory; + private final byte[] msgBody = Long.toString(System.currentTimeMillis()).getBytes(); + @Mock private MQClientAPIImpl mQClientAPIImpl; private PullAPIWrapper pullAPIWrapper; - private RebalancePushImpl rebalancePushImpl; - private DefaultMQPushConsumer pushConsumer; + private RebalanceImpl rebalanceImpl; + private static DefaultMQPushConsumer pushConsumer; + private AtomicLong queueOffset = new AtomicLong(1024); @Before public void init() throws Exception { + ConcurrentMap factoryTable = (ConcurrentMap) FieldUtils.readDeclaredField(MQClientManager.getInstance(), "factoryTable", true); + for (Map.Entry entry : factoryTable.entrySet()) { + entry.getValue().shutdown(); + } + factoryTable.clear(); + + when(mQClientAPIImpl.pullMessage(anyString(), any(PullMessageRequestHeader.class), + anyLong(), any(CommunicationMode.class), nullable(PullCallback.class))) + .thenAnswer(new Answer() { + @Override + public PullResult answer(InvocationOnMock mock) throws Throwable { + PullMessageRequestHeader requestHeader = mock.getArgument(1); + MessageClientExt messageClientExt = new MessageClientExt(); + messageClientExt.setTopic(topic); + messageClientExt.setQueueId(0); + messageClientExt.setMsgId("123"); + messageClientExt.setBody(new byte[] {'a'}); + messageClientExt.setOffsetMsgId("234"); + messageClientExt.setBornHost(new InetSocketAddress(8080)); + messageClientExt.setStoreHost(new InetSocketAddress(8080)); + PullResult pullResult = createPullResult(requestHeader, PullStatus.FOUND, Collections.singletonList(messageClientExt)); + ((PullCallback) mock.getArgument(4)).onSuccess(pullResult); + return pullResult; + } + }); + + consumerGroup = "FooBarGroup" + System.currentTimeMillis(); pushConsumer = new DefaultMQPushConsumer(consumerGroup); pushConsumer.setNamesrvAddr("127.0.0.1:9876"); pushConsumer.setPullInterval(60 * 1000); + pushConsumer.setClientRebalance(false); pushConsumer.registerMessageListener(new MessageListenerConcurrently() { @Override @@ -101,41 +142,48 @@ public ConsumeConcurrentlyStatus consumeMessage(List msgs, }); DefaultMQPushConsumerImpl pushConsumerImpl = pushConsumer.getDefaultMQPushConsumerImpl(); - rebalancePushImpl = spy(new RebalancePushImpl(pushConsumer.getDefaultMQPushConsumerImpl())); + + // suppress updateTopicRouteInfoFromNameServer + pushConsumer.changeInstanceNameToPID(); + mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(pushConsumer, (RPCHook) FieldUtils.readDeclaredField(pushConsumerImpl, "rpcHook", true)); + FieldUtils.writeDeclaredField(mQClientFactory, "mQClientAPIImpl", mQClientAPIImpl, true); + mQClientFactory = spy(mQClientFactory); + factoryTable.put(pushConsumer.buildMQClientId(), mQClientFactory); + doReturn(false).when(mQClientFactory).updateTopicRouteInfoFromNameServer(anyString()); + doReturn(null).when(mQClientFactory).queryAssignment(anyString(), anyString(), anyString(), any(MessageModel.class), anyInt()); + doReturn(new FindBrokerResult("127.0.0.1:10911", false)).when(mQClientFactory).findBrokerAddressInSubscribe(anyString(), anyLong(), anyBoolean()); + + rebalanceImpl = spy(pushConsumerImpl.getRebalanceImpl()); + doReturn(123L).when(rebalanceImpl).computePullFromWhereWithException(any(MessageQueue.class)); Field field = DefaultMQPushConsumerImpl.class.getDeclaredField("rebalanceImpl"); field.setAccessible(true); - field.set(pushConsumerImpl, rebalancePushImpl); - pushConsumer.subscribe(topic, "*"); - pushConsumer.start(); + field.set(pushConsumerImpl, rebalanceImpl); - mQClientFactory = spy(pushConsumerImpl.getmQClientFactory()); - field = DefaultMQPushConsumerImpl.class.getDeclaredField("mQClientFactory"); + field = DefaultMQPushConsumerImpl.class.getDeclaredField("doNotUpdateTopicSubscribeInfoWhenSubscriptionChanged"); field.setAccessible(true); - field.set(pushConsumerImpl, mQClientFactory); + field.set(null, true); - field = MQClientInstance.class.getDeclaredField("mQClientAPIImpl"); - field.setAccessible(true); - field.set(mQClientFactory, mQClientAPIImpl); + Set messageQueueSet = new HashSet<>(); + messageQueueSet.add(createPullRequest().getMessageQueue()); + pushConsumerImpl.updateTopicSubscribeInfo(topic, messageQueueSet); - pullAPIWrapper = spy(new PullAPIWrapper(mQClientFactory, consumerGroup, false)); - field = DefaultMQPushConsumerImpl.class.getDeclaredField("pullAPIWrapper"); - field.setAccessible(true); - field.set(pushConsumerImpl, pullAPIWrapper); + pushConsumerImpl.setmQClientFactory(mQClientFactory); - pushConsumer.getDefaultMQPushConsumerImpl().getRebalanceImpl().setmQClientFactory(mQClientFactory); - mQClientFactory.registerConsumer(consumerGroup, pushConsumerImpl); + pullAPIWrapper = spy(new PullAPIWrapper(mQClientFactory, consumerGroup, false)); + FieldUtils.writeDeclaredField(pushConsumerImpl, "pullAPIWrapper", pullAPIWrapper, true); - when(mQClientFactory.getMQClientAPIImpl().pullMessage(anyString(), any(PullMessageRequestHeader.class), + when(mQClientAPIImpl.pullMessage(anyString(), any(PullMessageRequestHeader.class), anyLong(), any(CommunicationMode.class), nullable(PullCallback.class))) - .thenAnswer(new Answer() { + .thenAnswer(new Answer() { @Override - public Object answer(InvocationOnMock mock) throws Throwable { + public PullResult answer(InvocationOnMock mock) throws Throwable { PullMessageRequestHeader requestHeader = mock.getArgument(1); MessageClientExt messageClientExt = new MessageClientExt(); messageClientExt.setTopic(topic); messageClientExt.setQueueId(0); - messageClientExt.setMsgId("123"); - messageClientExt.setBody(new byte[] {'a'}); + messageClientExt.setQueueOffset(queueOffset.getAndIncrement()); + messageClientExt.setMsgId("1024"); + messageClientExt.setBody(msgBody); messageClientExt.setOffsetMsgId("234"); messageClientExt.setBornHost(new InetSocketAddress(8080)); messageClientExt.setStoreHost(new InetSocketAddress(8080)); @@ -145,16 +193,14 @@ public Object answer(InvocationOnMock mock) throws Throwable { } }); - doReturn(new FindBrokerResult("127.0.0.1:10911", false)).when(mQClientFactory).findBrokerAddressInSubscribe(anyString(), anyLong(), anyBoolean()); - doReturn(Collections.singletonList(mQClientFactory.getClientId())).when(mQClientFactory).findConsumerIdList(anyString(), anyString()); - Set messageQueueSet = new HashSet(); - messageQueueSet.add(createPullRequest().getMessageQueue()); - pushConsumer.getDefaultMQPushConsumerImpl().updateTopicSubscribeInfo(topic, messageQueueSet); - doReturn(123L).when(rebalancePushImpl).computePullFromWhere(any(MessageQueue.class)); + pushConsumer.subscribe(topic, "*"); + pushConsumer.start(); + + mQClientFactory.registerConsumer(consumerGroup, pushConsumerImpl); } - @After - public void terminate() { + @AfterClass + public static void terminate() { pushConsumer.shutdown(); } @@ -166,12 +212,12 @@ public void testStart_OffsetShouldNotNUllAfterStart() { @Test public void testPullMessage_Success() throws InterruptedException, RemotingException, MQBrokerException { final CountDownLatch countDownLatch = new CountDownLatch(1); - final MessageExt[] messageExts = new MessageExt[1]; + final AtomicReference messageAtomic = new AtomicReference<>(); pushConsumer.getDefaultMQPushConsumerImpl().setConsumeMessageService(new ConsumeMessageConcurrentlyService(pushConsumer.getDefaultMQPushConsumerImpl(), new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { - messageExts[0] = msgs.get(0); + messageAtomic.set(msgs.get(0)); countDownLatch.countDown(); return null; } @@ -179,20 +225,22 @@ public ConsumeConcurrentlyStatus consumeMessage(List msgs, PullMessageService pullMessageService = mQClientFactory.getPullMessageService(); pullMessageService.executePullRequestImmediately(createPullRequest()); - countDownLatch.await(); - assertThat(messageExts[0].getTopic()).isEqualTo(topic); - assertThat(messageExts[0].getBody()).isEqualTo(new byte[] {'a'}); + countDownLatch.await(10, TimeUnit.SECONDS); + MessageExt msg = messageAtomic.get(); + assertThat(msg).isNotNull(); + assertThat(msg.getTopic()).isEqualTo(topic); + assertThat(msg.getBody()).isEqualTo(msgBody); } - @Test + @Test(timeout = 20000) public void testPullMessage_SuccessWithOrderlyService() throws Exception { final CountDownLatch countDownLatch = new CountDownLatch(1); - final MessageExt[] messageExts = new MessageExt[1]; + final AtomicReference messageAtomic = new AtomicReference<>(); MessageListenerOrderly listenerOrderly = new MessageListenerOrderly() { @Override public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) { - messageExts[0] = msgs.get(0); + messageAtomic.set(msgs.get(0)); countDownLatch.countDown(); return null; } @@ -204,9 +252,11 @@ public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderly PullMessageService pullMessageService = mQClientFactory.getPullMessageService(); pullMessageService.executePullRequestLater(createPullRequest(), 100); - countDownLatch.await(10, TimeUnit.SECONDS); - assertThat(messageExts[0].getTopic()).isEqualTo(topic); - assertThat(messageExts[0].getBody()).isEqualTo(new byte[] {'a'}); + countDownLatch.await(); + MessageExt msg = messageAtomic.get(); + assertThat(msg).isNotNull(); + assertThat(msg.getTopic()).isEqualTo(topic); + assertThat(msg.getBody()).isEqualTo(msgBody); } @Test @@ -250,6 +300,35 @@ public void testCheckConfig() { } } + @Test(timeout = 20000) + public void testGracefulShutdown() throws InterruptedException, RemotingException, MQBrokerException, MQClientException { + final CountDownLatch countDownLatch = new CountDownLatch(1); + pushConsumer.setAwaitTerminationMillisWhenShutdown(2000); + final AtomicBoolean messageConsumedFlag = new AtomicBoolean(false); + pushConsumer.getDefaultMQPushConsumerImpl().setConsumeMessageService(new ConsumeMessageConcurrentlyService(pushConsumer.getDefaultMQPushConsumerImpl(), new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, + ConsumeConcurrentlyContext context) { + assertThat(msgs.get(0).getBody()).isEqualTo(msgBody); + countDownLatch.countDown(); + try { + Thread.sleep(1000); + messageConsumedFlag.set(true); + } catch (InterruptedException e) { + } + + return null; + } + })); + + PullMessageService pullMessageService = mQClientFactory.getPullMessageService(); + pullMessageService.executePullRequestImmediately(createPullRequest()); + assertThat(countDownLatch.await(30, TimeUnit.SECONDS)).isTrue(); + + pushConsumer.shutdown(); + assertThat(messageConsumedFlag.get()).isTrue(); + } + private DefaultMQPushConsumer createPushConsumer() { DefaultMQPushConsumer pushConsumer = new DefaultMQPushConsumer(consumerGroup); pushConsumer.registerMessageListener(new MessageListenerConcurrently() { @@ -265,7 +344,7 @@ public ConsumeConcurrentlyStatus consumeMessage(List msgs, private PullRequest createPullRequest() { PullRequest pullRequest = new PullRequest(); pullRequest.setConsumerGroup(consumerGroup); - pullRequest.setNextOffset(1024); + pullRequest.setNextOffset(queueOffset.get()); MessageQueue messageQueue = new MessageQueue(); messageQueue.setBrokerName(brokerName); @@ -288,4 +367,25 @@ private PullResultExt createPullResult(PullMessageRequestHeader requestHeader, P } return new PullResultExt(pullStatus, requestHeader.getQueueOffset() + messageExtList.size(), 123, 2048, messageExtList, 0, outputStream.toByteArray()); } -} \ No newline at end of file + + @Test + public void testPullMessage_ExceptionOccursWhenComputePullFromWhere() throws MQClientException { + final CountDownLatch countDownLatch = new CountDownLatch(1); + final MessageExt[] messageExts = new MessageExt[1]; + pushConsumer.getDefaultMQPushConsumerImpl().setConsumeMessageService( + new ConsumeMessageConcurrentlyService(pushConsumer.getDefaultMQPushConsumerImpl(), + new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, + ConsumeConcurrentlyContext context) { + messageExts[0] = msgs.get(0); + return null; + } + })); + + pushConsumer.getDefaultMQPushConsumerImpl().setConsumeOrderly(true); + PullMessageService pullMessageService = mQClientFactory.getPullMessageService(); + pullMessageService.executePullRequestImmediately(createPullRequest()); + assertThat(messageExts[0]).isNull(); + } +} diff --git a/client/src/test/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMachineRoomNearByTest.java b/client/src/test/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMachineRoomNearByTest.java new file mode 100644 index 00000000000..6650f84a5dc --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMachineRoomNearByTest.java @@ -0,0 +1,211 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.consumer.rebalance; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import org.apache.rocketmq.client.consumer.AllocateMessageQueueStrategy; +import org.apache.rocketmq.common.message.MessageQueue; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + + +public class AllocateMachineRoomNearByTest { + + private static final String CID_PREFIX = "CID-"; + + private final String topic = "topic_test"; + private final AllocateMachineRoomNearby.MachineRoomResolver machineRoomResolver = new AllocateMachineRoomNearby.MachineRoomResolver() { + @Override + public String brokerDeployIn(MessageQueue messageQueue) { + return messageQueue.getBrokerName().split("-")[0]; + } + + @Override + public String consumerDeployIn(String clientID) { + return clientID.split("-")[0]; + } + }; + private final AllocateMessageQueueStrategy allocateMessageQueueStrategy = new AllocateMachineRoomNearby(new AllocateMessageQueueAveragely(), machineRoomResolver); + + + @Before + public void init() { + } + + + @Test + public void test1() { + testWhenIDCSizeEquals(5,20,10); + testWhenIDCSizeEquals(5,20,20); + testWhenIDCSizeEquals(5,20,30); + testWhenIDCSizeEquals(5,20,0); + } + + @Test + public void test2() { + testWhenConsumerIDCIsMore(5,1,10, 10, false); + testWhenConsumerIDCIsMore(5,1,10, 5, false); + testWhenConsumerIDCIsMore(5,1,10, 20, false); + testWhenConsumerIDCIsMore(5,1,10, 0, false); + } + + @Test + public void test3() { + testWhenConsumerIDCIsLess(5,2,10, 10, false); + testWhenConsumerIDCIsLess(5,2,10, 5, false); + testWhenConsumerIDCIsLess(5,2,10, 20, false); + testWhenConsumerIDCIsLess(5,2,10, 0, false); + } + + + @Test + public void testRun10RandomCase() { + for (int i = 0; i < 10; i++) { + int consumerSize = new Random().nextInt(200) + 1;//1-200 + int queueSize = new Random().nextInt(100) + 1;//1-100 + int brokerIDCSize = new Random().nextInt(10) + 1;//1-10 + int consumerIDCSize = new Random().nextInt(10) + 1;//1-10 + + if (brokerIDCSize == consumerIDCSize) { + testWhenIDCSizeEquals(brokerIDCSize,queueSize,consumerSize); + } + else if (brokerIDCSize > consumerIDCSize) { + testWhenConsumerIDCIsLess(brokerIDCSize,brokerIDCSize - consumerIDCSize, queueSize, consumerSize, false); + } else { + testWhenConsumerIDCIsMore(brokerIDCSize, consumerIDCSize - brokerIDCSize, queueSize, consumerSize, false); + } + } + } + + + + + public void testWhenIDCSizeEquals(int idcSize, int queueSize, int consumerSize) { + List cidAll = prepareConsumer(idcSize, consumerSize); + List mqAll = prepareMQ(idcSize, queueSize); + List resAll = new ArrayList<>(); + for (String currentID : cidAll) { + List res = allocateMessageQueueStrategy.allocate("Test-C-G",currentID,mqAll,cidAll); + for (MessageQueue mq : res) { + Assert.assertTrue(machineRoomResolver.brokerDeployIn(mq).equals(machineRoomResolver.consumerDeployIn(currentID))); + } + resAll.addAll(res); + } + Assert.assertTrue(hasAllocateAllQ(cidAll,mqAll,resAll)); + } + + public void testWhenConsumerIDCIsMore(int brokerIDCSize, int consumerMore, int queueSize, int consumerSize, boolean print) { + Set brokerIDCWithConsumer = new TreeSet<>(); + List cidAll = prepareConsumer(brokerIDCSize + consumerMore, consumerSize); + List mqAll = prepareMQ(brokerIDCSize, queueSize); + for (MessageQueue mq : mqAll) { + brokerIDCWithConsumer.add(machineRoomResolver.brokerDeployIn(mq)); + } + + List resAll = new ArrayList<>(); + for (String currentID : cidAll) { + List res = allocateMessageQueueStrategy.allocate("Test-C-G",currentID,mqAll,cidAll); + for (MessageQueue mq : res) { + if (brokerIDCWithConsumer.contains(machineRoomResolver.brokerDeployIn(mq))) { //healthy idc, so only consumer in this idc should be allocated + Assert.assertTrue(machineRoomResolver.brokerDeployIn(mq).equals(machineRoomResolver.consumerDeployIn(currentID))); + } + } + resAll.addAll(res); + } + + Assert.assertTrue(hasAllocateAllQ(cidAll,mqAll,resAll)); + } + + public void testWhenConsumerIDCIsLess(int brokerIDCSize, int consumerIDCLess, int queueSize, int consumerSize, boolean print) { + Set healthyIDC = new TreeSet<>(); + List cidAll = prepareConsumer(brokerIDCSize - consumerIDCLess, consumerSize); + List mqAll = prepareMQ(brokerIDCSize, queueSize); + for (String cid : cidAll) { + healthyIDC.add(machineRoomResolver.consumerDeployIn(cid)); + } + + List resAll = new ArrayList<>(); + Map> idc2Res = new TreeMap<>(); + for (String currentID : cidAll) { + String currentIDC = machineRoomResolver.consumerDeployIn(currentID); + List res = allocateMessageQueueStrategy.allocate("Test-C-G",currentID,mqAll,cidAll); + if (!idc2Res.containsKey(currentIDC)) { + idc2Res.put(currentIDC, new ArrayList<>()); + } + idc2Res.get(currentIDC).addAll(res); + resAll.addAll(res); + } + + for (String consumerIDC : healthyIDC) { + List resInOneIDC = idc2Res.get(consumerIDC); + List mqInThisIDC = createMessageQueueList(consumerIDC,queueSize); + Assert.assertTrue(resInOneIDC.containsAll(mqInThisIDC)); + } + + Assert.assertTrue(hasAllocateAllQ(cidAll,mqAll,resAll)); + } + + + private boolean hasAllocateAllQ(List cidAll,List mqAll, List allocatedResAll) { + if (cidAll.isEmpty()) { + return allocatedResAll.isEmpty(); + } + return mqAll.containsAll(allocatedResAll) && allocatedResAll.containsAll(mqAll) && mqAll.size() == allocatedResAll.size(); + } + + + private List createConsumerIdList(String machineRoom, int size) { + List consumerIdList = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + consumerIdList.add(machineRoom + "-" + CID_PREFIX + String.valueOf(i)); + } + return consumerIdList; + } + + private List createMessageQueueList(String machineRoom, int size) { + List messageQueueList = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + MessageQueue mq = new MessageQueue(topic, machineRoom + "-brokerName", i); + messageQueueList.add(mq); + } + return messageQueueList; + } + + private List prepareMQ(int brokerIDCSize, int queueSize) { + List mqAll = new ArrayList<>(); + for (int i = 1; i <= brokerIDCSize; i++) { + mqAll.addAll(createMessageQueueList("IDC" + i, queueSize)); + } + + return mqAll; + } + + private List prepareConsumer(int idcSize, int consumerSize) { + List cidAll = new ArrayList<>(); + for (int i = 1; i <= idcSize; i++) { + cidAll.addAll(createConsumerIdList("IDC" + i, consumerSize)); + } + return cidAll; + } +} diff --git a/client/src/test/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueAveragelyByCircleTest.java b/client/src/test/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueAveragelyByCircleTest.java new file mode 100644 index 00000000000..ee314bc684f --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueAveragelyByCircleTest.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.consumer.rebalance; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import junit.framework.TestCase; +import org.apache.rocketmq.common.message.MessageQueue; +import org.junit.Assert; + +public class AllocateMessageQueueAveragelyByCircleTest extends TestCase { + + public void testAllocateMessageQueueAveragelyByCircle() { + List consumerIdList = createConsumerIdList(4); + List messageQueueList = createMessageQueueList(10); + // the consumerId not in cidAll + List allocateQueues = new AllocateMessageQueueAveragelyByCircle().allocate("", "CID_PREFIX", messageQueueList, consumerIdList); + Assert.assertEquals(0, allocateQueues.size()); + + Map consumerAllocateQueue = new HashMap<>(consumerIdList.size()); + for (String consumerId : consumerIdList) { + List queues = new AllocateMessageQueueAveragelyByCircle().allocate("", consumerId, messageQueueList, consumerIdList); + int[] queueIds = new int[queues.size()]; + for (int i = 0; i < queues.size(); i++) { + queueIds[i] = queues.get(i).getQueueId(); + } + consumerAllocateQueue.put(consumerId, queueIds); + } + Assert.assertArrayEquals(new int[] {0, 4, 8}, consumerAllocateQueue.get("CID_PREFIX0")); + Assert.assertArrayEquals(new int[] {1, 5, 9}, consumerAllocateQueue.get("CID_PREFIX1")); + Assert.assertArrayEquals(new int[] {2, 6}, consumerAllocateQueue.get("CID_PREFIX2")); + Assert.assertArrayEquals(new int[] {3, 7}, consumerAllocateQueue.get("CID_PREFIX3")); + } + + private List createConsumerIdList(int size) { + List consumerIdList = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + consumerIdList.add("CID_PREFIX" + i); + } + return consumerIdList; + } + + private List createMessageQueueList(int size) { + List messageQueueList = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + MessageQueue mq = new MessageQueue("topic", "brokerName", i); + messageQueueList.add(mq); + } + return messageQueueList; + } +} + diff --git a/client/src/test/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueAveragelyTest.java b/client/src/test/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueAveragelyTest.java new file mode 100644 index 00000000000..498d9e240b4 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueAveragelyTest.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.consumer.rebalance; + +import junit.framework.TestCase; +import org.apache.rocketmq.common.message.MessageQueue; +import org.junit.Assert; + +import java.util.ArrayList; +import java.util.List; + +public class AllocateMessageQueueAveragelyTest extends TestCase { + + public void testAllocateMessageQueueAveragely() { + List consumerIdList = createConsumerIdList(4); + List messageQueueList = createMessageQueueList(10); + int[] results = new int[consumerIdList.size()]; + for (int i = 0; i < consumerIdList.size(); i++) { + List result = new AllocateMessageQueueAveragely().allocate("", consumerIdList.get(i), messageQueueList, consumerIdList); + results[i] = result.size(); + } + Assert.assertArrayEquals(new int[]{3, 3, 2, 2}, results); + } + + private List createConsumerIdList(int size) { + List consumerIdList = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + consumerIdList.add("CID_PREFIX" + i); + } + return consumerIdList; + } + + private List createMessageQueueList(int size) { + List messageQueueList = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + MessageQueue mq = new MessageQueue("topic", "brokerName", i); + messageQueueList.add(mq); + } + return messageQueueList; + } + + +} diff --git a/client/src/test/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueByConfigTest.java b/client/src/test/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueByConfigTest.java new file mode 100644 index 00000000000..baae104e21a --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueByConfigTest.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.consumer.rebalance; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import junit.framework.TestCase; +import org.apache.rocketmq.common.message.MessageQueue; +import org.junit.Assert; + +public class AllocateMessageQueueByConfigTest extends TestCase { + + public void testAllocateMessageQueueByConfig() { + List consumerIdList = createConsumerIdList(2); + List messageQueueList = createMessageQueueList(4); + AllocateMessageQueueByConfig allocateStrategy = new AllocateMessageQueueByConfig(); + allocateStrategy.setMessageQueueList(messageQueueList); + + Map consumerAllocateQueue = new HashMap<>(consumerIdList.size()); + for (String consumerId : consumerIdList) { + List queues = allocateStrategy.allocate("", consumerId, messageQueueList, consumerIdList); + int[] queueIds = new int[queues.size()]; + for (int i = 0; i < queues.size(); i++) { + queueIds[i] = queues.get(i).getQueueId(); + } + consumerAllocateQueue.put(consumerId, queueIds); + } + Assert.assertArrayEquals(new int[] {0, 1, 2, 3}, consumerAllocateQueue.get("CID_PREFIX0")); + Assert.assertArrayEquals(new int[] {0, 1, 2, 3}, consumerAllocateQueue.get("CID_PREFIX1")); + } + + private List createConsumerIdList(int size) { + List consumerIdList = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + consumerIdList.add("CID_PREFIX" + i); + } + return consumerIdList; + } + + private List createMessageQueueList(int size) { + List messageQueueList = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + MessageQueue mq = new MessageQueue("topic", "brokerName", i); + messageQueueList.add(mq); + } + return messageQueueList; + } +} + diff --git a/client/src/test/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueByMachineRoomTest.java b/client/src/test/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueByMachineRoomTest.java new file mode 100644 index 00000000000..62a77759727 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueByMachineRoomTest.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.consumer.rebalance; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import junit.framework.TestCase; +import org.apache.rocketmq.common.message.MessageQueue; +import org.junit.Assert; + +public class AllocateMessageQueueByMachineRoomTest extends TestCase { + + public void testAllocateMessageQueueByMachineRoom() { + List consumerIdList = createConsumerIdList(2); + List messageQueueList = createMessageQueueList(10); + Set consumeridcs = new HashSet<>(); + consumeridcs.add("room1"); + AllocateMessageQueueByMachineRoom allocateStrategy = new AllocateMessageQueueByMachineRoom(); + allocateStrategy.setConsumeridcs(consumeridcs); + + // mqAll is null or mqAll empty + try { + allocateStrategy.allocate("", consumerIdList.get(0), new ArrayList<>(), consumerIdList); + } catch (Exception e) { + assert e instanceof IllegalArgumentException; + Assert.assertEquals("mqAll is null or mqAll empty", e.getMessage()); + } + + Map consumerAllocateQueue = new HashMap<>(consumerIdList.size()); + for (String consumerId : consumerIdList) { + List queues = allocateStrategy.allocate("", consumerId, messageQueueList, consumerIdList); + int[] queueIds = new int[queues.size()]; + for (int i = 0; i < queues.size(); i++) { + queueIds[i] = queues.get(i).getQueueId(); + } + consumerAllocateQueue.put(consumerId, queueIds); + } + Assert.assertArrayEquals(new int[] {0, 1, 4}, consumerAllocateQueue.get("CID_PREFIX0")); + Assert.assertArrayEquals(new int[] {2, 3}, consumerAllocateQueue.get("CID_PREFIX1")); + } + + private List createConsumerIdList(int size) { + List consumerIdList = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + consumerIdList.add("CID_PREFIX" + i); + } + return consumerIdList; + } + + private List createMessageQueueList(int size) { + List messageQueueList = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + MessageQueue mq; + if (i < size / 2) { + mq = new MessageQueue("topic", "room1@broker-a", i); + } else { + mq = new MessageQueue("topic", "room2@broker-b", i); + } + messageQueueList.add(mq); + } + return messageQueueList; + } +} + diff --git a/client/src/test/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueConsitentHashTest.java b/client/src/test/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueConsitentHashTest.java index 3a581e13100..261d6d65a68 100644 --- a/client/src/test/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueConsitentHashTest.java +++ b/client/src/test/java/org/apache/rocketmq/client/consumer/rebalance/AllocateMessageQueueConsitentHashTest.java @@ -23,7 +23,6 @@ import java.util.Random; import java.util.TreeMap; import org.apache.rocketmq.client.consumer.AllocateMessageQueueStrategy; -import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageQueue; import org.junit.Assert; import org.junit.Before; @@ -39,23 +38,12 @@ public void init() { topic = "topic_test"; } - public void printMessageQueue(List messageQueueList, String name) { - if (messageQueueList == null || messageQueueList.size() < 1) - return; - System.out.println(name + ".......................................start"); - for (MessageQueue messageQueue : messageQueueList) { - System.out.println(messageQueue); - } - System.out.println(name + ".......................................end"); - } - @Test public void testCurrentCIDNotExists() { String currentCID = String.valueOf(Integer.MAX_VALUE); List consumerIdList = createConsumerIdList(2); List messageQueueList = createMessageQueueList(6); List result = new AllocateMessageQueueConsistentHash().allocate("", currentCID, messageQueueList, consumerIdList); - printMessageQueue(result, "testCurrentCIDNotExists"); Assert.assertEquals(result.size(), 0); } @@ -107,38 +95,34 @@ public void testAllocate(int queueSize, int consumerSize) { AllocateMessageQueueStrategy allocateMessageQueueConsistentHash = new AllocateMessageQueueConsistentHash(3); List mqAll = createMessageQueueList(queueSize); - //System.out.println("mqAll:" + mqAll.toString()); List cidAll = createConsumerIdList(consumerSize); - List allocatedResAll = new ArrayList(); + List allocatedResAll = new ArrayList<>(); - Map allocateToAllOrigin = new TreeMap(); + Map allocateToAllOrigin = new TreeMap<>(); //test allocate all { - List cidBegin = new ArrayList(cidAll); + List cidBegin = new ArrayList<>(cidAll); - //System.out.println("cidAll:" + cidBegin.toString()); for (String cid : cidBegin) { List rs = allocateMessageQueueConsistentHash.allocate("testConsumerGroup", cid, mqAll, cidBegin); for (MessageQueue mq : rs) { allocateToAllOrigin.put(mq, cid); } allocatedResAll.addAll(rs); - //System.out.println("rs[" + cid + "]:" + rs.toString()); } Assert.assertTrue( verifyAllocateAll(cidBegin, mqAll, allocatedResAll)); } - Map allocateToAllAfterRemoveOne = new TreeMap(); - List cidAfterRemoveOne = new ArrayList(cidAll); + Map allocateToAllAfterRemoveOne = new TreeMap<>(); + List cidAfterRemoveOne = new ArrayList<>(cidAll); //test allocate remove one cid { String removeCID = cidAfterRemoveOne.remove(0); - //System.out.println("removing one cid "+removeCID); - List mqShouldOnlyChanged = new ArrayList(); + List mqShouldOnlyChanged = new ArrayList<>(); Iterator> it = allocateToAllOrigin.entrySet().iterator(); while (it.hasNext()) { Map.Entry entry = it.next(); @@ -147,15 +131,13 @@ public void testAllocate(int queueSize, int consumerSize) { } } - //System.out.println("cidAll:" + cidAfterRemoveOne.toString()); - List allocatedResAllAfterRemove = new ArrayList(); + List allocatedResAllAfterRemove = new ArrayList<>(); for (String cid : cidAfterRemoveOne) { List rs = allocateMessageQueueConsistentHash.allocate("testConsumerGroup", cid, mqAll, cidAfterRemoveOne); allocatedResAllAfterRemove.addAll(rs); for (MessageQueue mq : rs) { allocateToAllAfterRemoveOne.put(mq, cid); } - //System.out.println("rs[" + cid + "]:" + "[" + rs.size() + "]" + rs.toString()); } Assert.assertTrue("queueSize" + queueSize + "consumerSize:" + consumerSize + "\nmqAll:" + mqAll + "\nallocatedResAllAfterRemove" + allocatedResAllAfterRemove, @@ -163,16 +145,14 @@ public void testAllocate(int queueSize, int consumerSize) { verifyAfterRemove(allocateToAllOrigin, allocateToAllAfterRemoveOne, removeCID); } - List cidAfterAdd = new ArrayList(cidAfterRemoveOne); + List cidAfterAdd = new ArrayList<>(cidAfterRemoveOne); //test allocate add one more cid { String newCid = CID_PREFIX + "NEW"; - //System.out.println("add one more cid "+newCid); cidAfterAdd.add(newCid); - List mqShouldOnlyChanged = new ArrayList(); - //System.out.println("cidAll:" + cidAfterAdd.toString()); - List allocatedResAllAfterAdd = new ArrayList(); - Map allocateToAll3 = new TreeMap(); + List mqShouldOnlyChanged = new ArrayList<>(); + List allocatedResAllAfterAdd = new ArrayList<>(); + Map allocateToAll3 = new TreeMap<>(); for (String cid : cidAfterAdd) { List rs = allocateMessageQueueConsistentHash.allocate("testConsumerGroup", cid, mqAll, cidAfterAdd); allocatedResAllAfterAdd.addAll(rs); @@ -182,7 +162,6 @@ public void testAllocate(int queueSize, int consumerSize) { mqShouldOnlyChanged.add(mq); } } - //System.out.println("rs[" + cid + "]:" + "[" + rs.size() + "]" + rs.toString()); } Assert.assertTrue( @@ -205,7 +184,7 @@ private void verifyAfterRemove(Map allocateToBefore, Map allocateBefore, Map createConsumerIdList(int size) { - List consumerIdList = new ArrayList(size); + List consumerIdList = new ArrayList<>(size); for (int i = 0; i < size; i++) { consumerIdList.add(CID_PREFIX + String.valueOf(i)); } @@ -233,7 +212,7 @@ private List createConsumerIdList(int size) { } private List createMessageQueueList(int size) { - List messageQueueList = new ArrayList(size); + List messageQueueList = new ArrayList<>(size); for (int i = 0; i < size; i++) { MessageQueue mq = new MessageQueue(topic, "brokerName", i); messageQueueList.add(mq); diff --git a/client/src/test/java/org/apache/rocketmq/client/consumer/store/LocalFileOffsetStoreTest.java b/client/src/test/java/org/apache/rocketmq/client/consumer/store/LocalFileOffsetStoreTest.java index a705b30fc3b..2f88523bc1e 100644 --- a/client/src/test/java/org/apache/rocketmq/client/consumer/store/LocalFileOffsetStoreTest.java +++ b/client/src/test/java/org/apache/rocketmq/client/consumer/store/LocalFileOffsetStoreTest.java @@ -17,6 +17,7 @@ package org.apache.rocketmq.client.consumer.store; import java.io.File; +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Map; @@ -71,7 +72,7 @@ public void testReadOffset_FromStore() throws Exception { offsetStore.updateOffset(messageQueue, 1024, false); assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_STORE)).isEqualTo(-1); - offsetStore.persistAll(new HashSet(Collections.singletonList(messageQueue))); + offsetStore.persistAll(new HashSet<>(Collections.singletonList(messageQueue))); assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_STORE)).isEqualTo(1024); } @@ -85,4 +86,48 @@ public void testCloneOffset() throws Exception { assertThat(cloneOffsetTable.size()).isEqualTo(1); assertThat(cloneOffsetTable.get(messageQueue)).isEqualTo(1024); } -} \ No newline at end of file + + @Test + public void testPersist() throws Exception { + OffsetStore offsetStore = new LocalFileOffsetStore(mQClientFactory, group); + + MessageQueue messageQueue0 = new MessageQueue(topic, brokerName, 0); + offsetStore.updateOffset(messageQueue0, 1024, false); + offsetStore.persist(messageQueue0); + assertThat(offsetStore.readOffset(messageQueue0, ReadOffsetType.READ_FROM_STORE)).isEqualTo(1024); + + MessageQueue messageQueue1 = new MessageQueue(topic, brokerName, 1); + assertThat(offsetStore.readOffset(messageQueue1, ReadOffsetType.READ_FROM_STORE)).isEqualTo(-1); + } + + @Test + public void testPersistAll() throws Exception { + OffsetStore offsetStore = new LocalFileOffsetStore(mQClientFactory, group); + + MessageQueue messageQueue0 = new MessageQueue(topic, brokerName, 0); + offsetStore.updateOffset(messageQueue0, 1024, false); + offsetStore.persistAll(new HashSet(Collections.singletonList(messageQueue0))); + assertThat(offsetStore.readOffset(messageQueue0, ReadOffsetType.READ_FROM_STORE)).isEqualTo(1024); + + MessageQueue messageQueue1 = new MessageQueue(topic, brokerName, 1); + MessageQueue messageQueue2 = new MessageQueue(topic, brokerName, 2); + offsetStore.updateOffset(messageQueue1, 1025, false); + offsetStore.updateOffset(messageQueue2, 1026, false); + offsetStore.persistAll(new HashSet(Arrays.asList(messageQueue1, messageQueue2))); + + assertThat(offsetStore.readOffset(messageQueue0, ReadOffsetType.READ_FROM_STORE)).isEqualTo(1024); + assertThat(offsetStore.readOffset(messageQueue1, ReadOffsetType.READ_FROM_STORE)).isEqualTo(1025); + assertThat(offsetStore.readOffset(messageQueue2, ReadOffsetType.READ_FROM_STORE)).isEqualTo(1026); + } + + @Test + public void testRemoveOffset() throws Exception { + OffsetStore offsetStore = new LocalFileOffsetStore(mQClientFactory, group); + MessageQueue messageQueue = new MessageQueue(topic, brokerName, 0); + offsetStore.updateOffset(messageQueue, 1024, false); + assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_MEMORY)).isEqualTo(1024); + + offsetStore.removeOffset(messageQueue); + assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_MEMORY)).isEqualTo(-1); + } +} diff --git a/client/src/test/java/org/apache/rocketmq/client/consumer/store/RemoteBrokerOffsetStoreTest.java b/client/src/test/java/org/apache/rocketmq/client/consumer/store/RemoteBrokerOffsetStoreTest.java index 64d64f29a94..ba6911e3e85 100644 --- a/client/src/test/java/org/apache/rocketmq/client/consumer/store/RemoteBrokerOffsetStoreTest.java +++ b/client/src/test/java/org/apache/rocketmq/client/consumer/store/RemoteBrokerOffsetStoreTest.java @@ -20,13 +20,16 @@ import java.util.HashSet; import org.apache.rocketmq.client.ClientConfig; import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.OffsetNotFoundException; import org.apache.rocketmq.client.impl.FindBrokerResult; import org.apache.rocketmq.client.impl.MQClientAPIImpl; import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.header.QueryConsumerOffsetRequestHeader; -import org.apache.rocketmq.common.protocol.header.UpdateConsumerOffsetRequestHeader; import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumerOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateConsumerOffsetRequestHeader; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -58,8 +61,9 @@ public void init() { System.setProperty("rocketmq.client.localOffsetStoreDir", System.getProperty("java.io.tmpdir") + ".rocketmq_offsets"); String clientId = new ClientConfig().buildMQClientId() + "#TestNamespace" + System.currentTimeMillis(); when(mQClientFactory.getClientId()).thenReturn(clientId); - when(mQClientFactory.findBrokerAddressInAdmin(brokerName)).thenReturn(new FindBrokerResult("127.0.0.1", false)); + when(mQClientFactory.findBrokerAddressInSubscribe(brokerName, MixAll.MASTER_ID, false)).thenReturn(new FindBrokerResult("127.0.0.1", false)); when(mQClientFactory.getMQClientAPIImpl()).thenReturn(mqClientAPI); + when(mQClientFactory.getBrokerNameFromMessageQueue(any())).thenReturn(brokerName); } @Test @@ -77,6 +81,38 @@ public void testUpdateOffset() throws Exception { assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_MEMORY)).isEqualTo(1023); } + @Test + public void testUpdateAndFreezeOffset() throws Exception { + OffsetStore offsetStore = new RemoteBrokerOffsetStore(mQClientFactory, group); + MessageQueue messageQueue = new MessageQueue(topic, brokerName, 1); + + offsetStore.updateAndFreezeOffset(messageQueue, 1024); + assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_MEMORY)).isEqualTo(1024); + + offsetStore.updateOffset(messageQueue, 1023, false); + assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_MEMORY)).isEqualTo(1024); + + offsetStore.updateOffset(messageQueue, 1022, true); + assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_MEMORY)).isEqualTo(1024); + } + + @Test + public void testUpdateAndFreezeOffsetWithRemove() throws Exception { + OffsetStore offsetStore = new RemoteBrokerOffsetStore(mQClientFactory, group); + MessageQueue messageQueue = new MessageQueue(topic, brokerName, 1); + + offsetStore.updateAndFreezeOffset(messageQueue, 1024); + assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_MEMORY)).isEqualTo(1024); + + offsetStore.updateOffset(messageQueue, 1023, false); + assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_MEMORY)).isEqualTo(1024); + + offsetStore.removeOffset(messageQueue); + assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_MEMORY)).isEqualTo(-1); + offsetStore.updateOffset(messageQueue, 1023, false); + assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_MEMORY)).isEqualTo(1023); + } + @Test public void testReadOffset_WithException() throws Exception { OffsetStore offsetStore = new RemoteBrokerOffsetStore(mQClientFactory, group); @@ -84,10 +120,15 @@ public void testReadOffset_WithException() throws Exception { offsetStore.updateOffset(messageQueue, 1024, false); - doThrow(new MQBrokerException(-1, "")) + doThrow(new OffsetNotFoundException(ResponseCode.QUERY_NOT_FOUND, "", null)) .when(mqClientAPI).queryConsumerOffset(anyString(), any(QueryConsumerOffsetRequestHeader.class), anyLong()); assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_STORE)).isEqualTo(-1); + + doThrow(new MQBrokerException(-1, "", null)) + .when(mqClientAPI).queryConsumerOffset(anyString(), any(QueryConsumerOffsetRequestHeader.class), anyLong()); + assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_STORE)).isEqualTo(-2); + doThrow(new RemotingException("", null)) .when(mqClientAPI).queryConsumerOffset(anyString(), any(QueryConsumerOffsetRequestHeader.class), anyLong()); assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_STORE)).isEqualTo(-2); @@ -120,7 +161,7 @@ public Object answer(InvocationOnMock mock) throws Throwable { assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_STORE)).isEqualTo(1023); offsetStore.updateOffset(messageQueue, 1025, false); - offsetStore.persistAll(new HashSet(Collections.singletonList(messageQueue))); + offsetStore.persistAll(new HashSet<>(Collections.singletonList(messageQueue))); assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_STORE)).isEqualTo(1025); } @@ -135,4 +176,4 @@ public void testRemoveOffset() throws Exception { offsetStore.removeOffset(messageQueue); assertThat(offsetStore.readOffset(messageQueue, ReadOffsetType.READ_FROM_MEMORY)).isEqualTo(-1); } -} \ No newline at end of file +} diff --git a/client/src/test/java/org/apache/rocketmq/client/impl/MQClientAPIImplTest.java b/client/src/test/java/org/apache/rocketmq/client/impl/MQClientAPIImplTest.java index bf019618d9c..9f1a71c92f6 100644 --- a/client/src/test/java/org/apache/rocketmq/client/impl/MQClientAPIImplTest.java +++ b/client/src/test/java/org/apache/rocketmq/client/impl/MQClientAPIImplTest.java @@ -17,19 +17,36 @@ package org.apache.rocketmq.client.impl; import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CountDownLatch; import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.client.consumer.AckCallback; +import org.apache.rocketmq.client.consumer.AckResult; +import org.apache.rocketmq.client.consumer.AckStatus; +import org.apache.rocketmq.client.consumer.PopCallback; +import org.apache.rocketmq.client.consumer.PopResult; +import org.apache.rocketmq.client.consumer.PopStatus; import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.hook.SendMessageContext; import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.SendCallback; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.PlainAccessConfig; +import org.apache.rocketmq.common.TopicConfig; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageConst; -import org.apache.rocketmq.common.protocol.ResponseCode; -import org.apache.rocketmq.common.protocol.header.SendMessageRequestHeader; -import org.apache.rocketmq.common.protocol.header.SendMessageResponseHeader; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.message.MessageQueueAssignment; +import org.apache.rocketmq.common.message.MessageRequestMode; import org.apache.rocketmq.remoting.InvokeCallback; import org.apache.rocketmq.remoting.RemotingClient; import org.apache.rocketmq.remoting.exception.RemotingException; @@ -37,9 +54,35 @@ import org.apache.rocketmq.remoting.netty.NettyClientConfig; import org.apache.rocketmq.remoting.netty.ResponseFuture; import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.QueryAssignmentResponseBody; +import org.apache.rocketmq.remoting.protocol.header.AckMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ChangeInvisibleTimeRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ChangeInvisibleTimeResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.ExtraInfoUtil; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerListByGroupResponseBody; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerListByGroupResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.GetEarliestMsgStoretimeResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMaxOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMinOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.PopMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PopMessageResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumerOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumerOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.SearchOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateConsumerOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateConsumerOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.AddWritePermOfBrokerResponseHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.assertj.core.api.Assertions; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Matchers; import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.MockitoJUnitRunner; @@ -64,9 +107,11 @@ public class MQClientAPIImplTest { private String brokerAddr = "127.0.0.1"; private String brokerName = "DefaultBroker"; + private String clusterName = "DefaultCluster"; private static String group = "FooBarGroup"; private static String topic = "FooBar"; private Message msg = new Message("FooBar", new byte[] {}); + private static String clientId = "127.0.0.2@UnitTest"; @Before public void init() throws Exception { @@ -110,7 +155,7 @@ public void testSendMessageSync_Success() throws InterruptedException, RemotingE @Override public Object answer(InvocationOnMock mock) throws Throwable { RemotingCommand request = mock.getArgument(1); - return createSuccessResponse(request); + return createSendMessageSuccessResponse(request); } }).when(remotingClient).invokeSync(anyString(), any(RemotingCommand.class), anyLong()); @@ -162,9 +207,9 @@ public void testSendMessageAsync_Success() throws RemotingException, Interrupted public Object answer(InvocationOnMock mock) throws Throwable { InvokeCallback callback = mock.getArgument(3); RemotingCommand request = mock.getArgument(1); - ResponseFuture responseFuture = new ResponseFuture(request.getOpaque(), 3 * 1000, null, null); - responseFuture.setResponseCommand(createSuccessResponse(request)); - callback.operationComplete(responseFuture); + ResponseFuture responseFuture = new ResponseFuture(null, request.getOpaque(), 3 * 1000, null, null); + responseFuture.setResponseCommand(createSendMessageSuccessResponse(request)); + callback.operationSucceed(responseFuture.getResponseCommand()); return null; } }).when(remotingClient).invokeAsync(anyString(), any(RemotingCommand.class), anyLong(), any(InvokeCallback.class)); @@ -191,26 +236,656 @@ public void onException(Throwable e) { public void testSendMessageAsync_WithException() throws RemotingException, InterruptedException, MQBrokerException { doThrow(new RemotingTimeoutException("Remoting Exception in Test")).when(remotingClient) .invokeAsync(anyString(), any(RemotingCommand.class), anyLong(), any(InvokeCallback.class)); - try { - mqClientAPI.sendMessage(brokerAddr, brokerName, msg, new SendMessageRequestHeader(), - 3 * 1000, CommunicationMode.ASYNC, new SendMessageContext(), defaultMQProducerImpl); - failBecauseExceptionWasNotThrown(RemotingException.class); - } catch (RemotingException e) { - assertThat(e).hasMessage("Remoting Exception in Test"); - } + SendMessageContext sendMessageContext = new SendMessageContext(); + sendMessageContext.setProducer(new DefaultMQProducerImpl(new DefaultMQProducer())); + mqClientAPI.sendMessage(brokerAddr, brokerName, msg, new SendMessageRequestHeader(), 3 * 1000, CommunicationMode.ASYNC, + new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + } + @Override + public void onException(Throwable e) { + assertThat(e).hasMessage("Remoting Exception in Test"); + } + }, null, null, 0, sendMessageContext, defaultMQProducerImpl); doThrow(new InterruptedException("Interrupted Exception in Test")).when(remotingClient) .invokeAsync(anyString(), any(RemotingCommand.class), anyLong(), any(InvokeCallback.class)); + mqClientAPI.sendMessage(brokerAddr, brokerName, msg, new SendMessageRequestHeader(), 3 * 1000, CommunicationMode.ASYNC, + new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + } + @Override + public void onException(Throwable e) { + assertThat(e).hasMessage("Interrupted Exception in Test"); + } + }, null, null, 0, sendMessageContext, defaultMQProducerImpl); + } + + @Test + public void testCreatePlainAccessConfig_Success() throws InterruptedException, RemotingException, MQBrokerException { + + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock mock) throws Throwable { + RemotingCommand request = mock.getArgument(1); + return createSuccessResponse4UpdateAclConfig(request); + } + }).when(remotingClient).invokeSync(anyString(), any(RemotingCommand.class), anyLong()); + + PlainAccessConfig config = createUpdateAclConfig(); + try { - mqClientAPI.sendMessage(brokerAddr, brokerName, msg, new SendMessageRequestHeader(), - 3 * 1000, CommunicationMode.ASYNC, new SendMessageContext(), defaultMQProducerImpl); - failBecauseExceptionWasNotThrown(InterruptedException.class); - } catch (InterruptedException e) { - assertThat(e).hasMessage("Interrupted Exception in Test"); + mqClientAPI.createPlainAccessConfig(brokerAddr, config, 3 * 1000); + } catch (MQClientException ex) { + + } + } + + @Test + public void testCreatePlainAccessConfig_Exception() throws InterruptedException, RemotingException, MQBrokerException { + + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock mock) throws Throwable { + RemotingCommand request = mock.getArgument(1); + return createErrorResponse4UpdateAclConfig(request); + } + }).when(remotingClient).invokeSync(anyString(), any(RemotingCommand.class), anyLong()); + + PlainAccessConfig config = createUpdateAclConfig(); + try { + mqClientAPI.createPlainAccessConfig(brokerAddr, config, 3 * 1000); + } catch (MQClientException ex) { + assertThat(ex.getResponseCode()).isEqualTo(209); + assertThat(ex.getErrorMessage()).isEqualTo("corresponding to accessConfig has been updated failed"); } } - private RemotingCommand createSuccessResponse(RemotingCommand request) { + @Test + public void testDeleteAccessConfig_Success() throws InterruptedException, RemotingException, MQBrokerException { + + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock mock) throws Throwable { + RemotingCommand request = mock.getArgument(1); + return createSuccessResponse4DeleteAclConfig(request); + } + }).when(remotingClient).invokeSync(anyString(), any(RemotingCommand.class), anyLong()); + + String accessKey = "1234567"; + try { + mqClientAPI.deleteAccessConfig(brokerAddr, accessKey, 3 * 1000); + } catch (MQClientException ex) { + + } + } + + @Test + public void testDeleteAccessConfig_Exception() throws InterruptedException, RemotingException, MQBrokerException { + + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock mock) throws Throwable { + RemotingCommand request = mock.getArgument(1); + return createErrorResponse4DeleteAclConfig(request); + } + }).when(remotingClient).invokeSync(anyString(), any(RemotingCommand.class), anyLong()); + + try { + mqClientAPI.deleteAccessConfig(brokerAddr, "11111", 3 * 1000); + } catch (MQClientException ex) { + assertThat(ex.getResponseCode()).isEqualTo(210); + assertThat(ex.getErrorMessage()).isEqualTo("corresponding to accessConfig has been deleted failed"); + } + } + + @Test + public void testResumeCheckHalfMessage_WithException() throws RemotingException, InterruptedException, MQBrokerException, MQClientException { + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock mock) throws Throwable { + RemotingCommand request = mock.getArgument(1); + RemotingCommand response = RemotingCommand.createResponseCommand(SendMessageResponseHeader.class); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setOpaque(request.getOpaque()); + response.setRemark("Put message back to RMQ_SYS_TRANS_HALF_TOPIC failed."); + return response; + } + }).when(remotingClient).invokeSync(anyString(), any(RemotingCommand.class), anyLong()); + + boolean result = mqClientAPI.resumeCheckHalfMessage(brokerAddr, "test", 3000); + assertThat(result).isEqualTo(false); + } + + @Test + public void testResumeCheckHalfMessage_Success() throws InterruptedException, RemotingException, MQBrokerException, MQClientException { + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock mock) throws Throwable { + RemotingCommand request = mock.getArgument(1); + return createResumeSuccessResponse(request); + } + }).when(remotingClient).invokeSync(anyString(), any(RemotingCommand.class), anyLong()); + + boolean result = mqClientAPI.resumeCheckHalfMessage(brokerAddr, "test", 3000); + + assertThat(result).isEqualTo(true); + } + + @Test + public void testSendMessageTypeofReply() throws Exception { + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock mock) throws Throwable { + InvokeCallback callback = mock.getArgument(3); + RemotingCommand request = mock.getArgument(1); + ResponseFuture responseFuture = new ResponseFuture(null, request.getOpaque(), 3 * 1000, null, null); + responseFuture.setResponseCommand(createSendMessageSuccessResponse(request)); + callback.operationSucceed(responseFuture.getResponseCommand()); + return null; + } + }).when(remotingClient).invokeAsync(Matchers.anyString(), Matchers.any(RemotingCommand.class), Matchers.anyLong(), Matchers.any(InvokeCallback.class)); + SendMessageContext sendMessageContext = new SendMessageContext(); + sendMessageContext.setProducer(new DefaultMQProducerImpl(new DefaultMQProducer())); + msg.getProperties().put("MSG_TYPE", "reply"); + mqClientAPI.sendMessage(brokerAddr, brokerName, msg, new SendMessageRequestHeader(), 3 * 1000, CommunicationMode.ASYNC, + new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + assertThat(sendResult.getSendStatus()).isEqualTo(SendStatus.SEND_OK); + assertThat(sendResult.getOffsetMsgId()).isEqualTo("123"); + assertThat(sendResult.getQueueOffset()).isEqualTo(123L); + assertThat(sendResult.getMessageQueue().getQueueId()).isEqualTo(1); + } + + @Override + public void onException(Throwable e) { + } + }, null, null, 0, sendMessageContext, defaultMQProducerImpl); + } + + @Test + public void testQueryAssignment_Success() throws Exception { + doAnswer(new Answer() { + @Override + public RemotingCommand answer(InvocationOnMock mock) { + RemotingCommand request = mock.getArgument(1); + + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + response.setOpaque(request.getOpaque()); + QueryAssignmentResponseBody b = new QueryAssignmentResponseBody(); + b.setMessageQueueAssignments(Collections.singleton(new MessageQueueAssignment())); + response.setBody(b.encode()); + return response; + } + }).when(remotingClient).invokeSync(anyString(), any(RemotingCommand.class), anyLong()); + Set assignments = mqClientAPI.queryAssignment(brokerAddr, topic, group, clientId, null, MessageModel.CLUSTERING, 10 * 1000); + assertThat(assignments).size().isEqualTo(1); + } + + @Test + public void testPopMessageAsync_Success() throws Exception { + final long popTime = System.currentTimeMillis(); + final int invisibleTime = 10 * 1000; + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock mock) throws Throwable { + InvokeCallback callback = mock.getArgument(3); + RemotingCommand request = mock.getArgument(1); + ResponseFuture responseFuture = new ResponseFuture(null, request.getOpaque(), 3 * 1000, null, null); + RemotingCommand response = RemotingCommand.createResponseCommand(PopMessageResponseHeader.class); + response.setCode(ResponseCode.SUCCESS); + response.setOpaque(request.getOpaque()); + + PopMessageResponseHeader responseHeader = (PopMessageResponseHeader) response.readCustomHeader(); + responseHeader.setInvisibleTime(invisibleTime); + responseHeader.setPopTime(popTime); + responseHeader.setReviveQid(0); + responseHeader.setRestNum(1); + StringBuilder startOffsetInfo = new StringBuilder(64); + ExtraInfoUtil.buildStartOffsetInfo(startOffsetInfo, topic, 0, 0L); + responseHeader.setStartOffsetInfo(startOffsetInfo.toString()); + StringBuilder msgOffsetInfo = new StringBuilder(64); + ExtraInfoUtil.buildMsgOffsetInfo(msgOffsetInfo, topic, 0, Collections.singletonList(0L)); + responseHeader.setMsgOffsetInfo(msgOffsetInfo.toString()); + response.setRemark("FOUND"); + response.makeCustomHeaderToNet(); + + MessageExt message = new MessageExt(); + message.setQueueId(0); + message.setFlag(12); + message.setQueueOffset(0L); + message.setCommitLogOffset(100L); + message.setSysFlag(0); + message.setBornTimestamp(System.currentTimeMillis()); + message.setBornHost(new InetSocketAddress("127.0.0.1", 10)); + message.setStoreTimestamp(System.currentTimeMillis()); + message.setStoreHost(new InetSocketAddress("127.0.0.1", 11)); + message.setBody("body".getBytes()); + message.setTopic(topic); + message.putUserProperty("key", "value"); + response.setBody(MessageDecoder.encode(message, false)); + responseFuture.setResponseCommand(response); + callback.operationSucceed(responseFuture.getResponseCommand()); + return null; + } + }).when(remotingClient).invokeAsync(anyString(), any(RemotingCommand.class), anyLong(), any(InvokeCallback.class)); + final CountDownLatch done = new CountDownLatch(1); + mqClientAPI.popMessageAsync(brokerName, brokerAddr, new PopMessageRequestHeader(), 10 * 1000, new PopCallback() { + @Override + public void onSuccess(PopResult popResult) { + assertThat(popResult.getPopStatus()).isEqualTo(PopStatus.FOUND); + assertThat(popResult.getRestNum()).isEqualTo(1); + assertThat(popResult.getInvisibleTime()).isEqualTo(invisibleTime); + assertThat(popResult.getPopTime()).isEqualTo(popTime); + assertThat(popResult.getMsgFoundList()).size().isEqualTo(1); + done.countDown(); + } + + @Override + public void onException(Throwable e) { + Assertions.fail("want no exception but got one", e); + done.countDown(); + } + }); + done.await(); + } + + @Test + public void testPopLmqMessage_async() throws Exception { + final long popTime = System.currentTimeMillis(); + final int invisibleTime = 10 * 1000; + final String lmqTopic = MixAll.LMQ_PREFIX + "lmq1"; + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock mock) throws Throwable { + InvokeCallback callback = mock.getArgument(3); + RemotingCommand request = mock.getArgument(1); + ResponseFuture responseFuture = new ResponseFuture(null, request.getOpaque(), 3 * 1000, null, null); + RemotingCommand response = RemotingCommand.createResponseCommand(PopMessageResponseHeader.class); + response.setCode(ResponseCode.SUCCESS); + response.setOpaque(request.getOpaque()); + + PopMessageResponseHeader responseHeader = (PopMessageResponseHeader) response.readCustomHeader(); + responseHeader.setInvisibleTime(invisibleTime); + responseHeader.setPopTime(popTime); + responseHeader.setReviveQid(0); + responseHeader.setRestNum(1); + StringBuilder startOffsetInfo = new StringBuilder(64); + ExtraInfoUtil.buildStartOffsetInfo(startOffsetInfo, topic, 0, 0L); + responseHeader.setStartOffsetInfo(startOffsetInfo.toString()); + StringBuilder msgOffsetInfo = new StringBuilder(64); + ExtraInfoUtil.buildMsgOffsetInfo(msgOffsetInfo, topic, 0, Collections.singletonList(0L)); + responseHeader.setMsgOffsetInfo(msgOffsetInfo.toString()); + response.setRemark("FOUND"); + response.makeCustomHeaderToNet(); + + MessageExt message = new MessageExt(); + message.setQueueId(3); + message.setFlag(0); + message.setQueueOffset(5L); + message.setCommitLogOffset(11111L); + message.setSysFlag(0); + message.setBornTimestamp(System.currentTimeMillis()); + message.setBornHost(new InetSocketAddress("127.0.0.1", 10)); + message.setStoreTimestamp(System.currentTimeMillis()); + message.setStoreHost(new InetSocketAddress("127.0.0.1", 11)); + message.setBody("body".getBytes()); + message.setTopic(topic); + message.putUserProperty("key", "value"); + message.putUserProperty(MessageConst.PROPERTY_INNER_MULTI_DISPATCH, lmqTopic); + message.getProperties().put(MessageConst.PROPERTY_INNER_MULTI_QUEUE_OFFSET, String.valueOf(0)); + response.setBody(MessageDecoder.encode(message, false)); + responseFuture.setResponseCommand(response); + callback.operationSucceed(responseFuture.getResponseCommand()); + return null; + } + }).when(remotingClient).invokeAsync(anyString(), any(RemotingCommand.class), anyLong(), any(InvokeCallback.class)); + final CountDownLatch done = new CountDownLatch(1); + final PopMessageRequestHeader requestHeader = new PopMessageRequestHeader(); + requestHeader.setTopic(lmqTopic); + mqClientAPI.popMessageAsync(brokerName, brokerAddr, requestHeader, 10 * 1000, new PopCallback() { + @Override + public void onSuccess(PopResult popResult) { + assertThat(popResult.getPopStatus()).isEqualTo(PopStatus.FOUND); + assertThat(popResult.getRestNum()).isEqualTo(1); + assertThat(popResult.getInvisibleTime()).isEqualTo(invisibleTime); + assertThat(popResult.getPopTime()).isEqualTo(popTime); + assertThat(popResult.getMsgFoundList()).size().isEqualTo(1); + assertThat(popResult.getMsgFoundList().get(0).getTopic()).isEqualTo(lmqTopic); + assertThat(popResult.getMsgFoundList().get(0).getProperty(MessageConst.PROPERTY_INNER_MULTI_DISPATCH)) + .isEqualTo(lmqTopic); + done.countDown(); + } + + @Override + public void onException(Throwable e) { + Assertions.fail("want no exception but got one", e); + done.countDown(); + } + }); + done.await(); + } + + @Test + public void testAckMessageAsync_Success() throws Exception { + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock mock) throws Throwable { + InvokeCallback callback = mock.getArgument(3); + RemotingCommand request = mock.getArgument(1); + ResponseFuture responseFuture = new ResponseFuture(null, request.getOpaque(), 3 * 1000, null, null); + RemotingCommand response = RemotingCommand.createResponseCommand(ResponseCode.SUCCESS, null); + response.setOpaque(request.getOpaque()); + response.setCode(ResponseCode.SUCCESS); + responseFuture.setResponseCommand(response); + callback.operationSucceed(responseFuture.getResponseCommand()); + return null; + } + }).when(remotingClient).invokeAsync(anyString(), any(RemotingCommand.class), anyLong(), any(InvokeCallback.class)); + + final CountDownLatch done = new CountDownLatch(1); + mqClientAPI.ackMessageAsync(brokerAddr, 10 * 1000, new AckCallback() { + @Override + public void onSuccess(AckResult ackResult) { + assertThat(ackResult.getStatus()).isEqualTo(AckStatus.OK); + done.countDown(); + } + + @Override + public void onException(Throwable e) { + Assertions.fail("want no exception but got one", e); + done.countDown(); + } + }, new AckMessageRequestHeader()); + done.await(); + } + + @Test + public void testChangeInvisibleTimeAsync_Success() throws Exception { + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock mock) throws Throwable { + InvokeCallback callback = mock.getArgument(3); + RemotingCommand request = mock.getArgument(1); + ResponseFuture responseFuture = new ResponseFuture(null, request.getOpaque(), 3 * 1000, null, null); + RemotingCommand response = RemotingCommand.createResponseCommand(ChangeInvisibleTimeResponseHeader.class); + response.setOpaque(request.getOpaque()); + response.setCode(ResponseCode.SUCCESS); + ChangeInvisibleTimeResponseHeader responseHeader = (ChangeInvisibleTimeResponseHeader) response.readCustomHeader(); + responseHeader.setPopTime(System.currentTimeMillis()); + responseHeader.setInvisibleTime(10 * 1000L); + responseFuture.setResponseCommand(response); + callback.operationSucceed(responseFuture.getResponseCommand()); + return null; + } + }).when(remotingClient).invokeAsync(anyString(), any(RemotingCommand.class), anyLong(), any(InvokeCallback.class)); + + final CountDownLatch done = new CountDownLatch(1); + ChangeInvisibleTimeRequestHeader requestHeader = new ChangeInvisibleTimeRequestHeader(); + requestHeader.setTopic(topic); + requestHeader.setQueueId(0); + requestHeader.setOffset(0L); + requestHeader.setInvisibleTime(10 * 1000L); + mqClientAPI.changeInvisibleTimeAsync(brokerName, brokerAddr, requestHeader, 10 * 1000, new AckCallback() { + @Override + public void onSuccess(AckResult ackResult) { + assertThat(ackResult.getStatus()).isEqualTo(AckStatus.OK); + done.countDown(); + } + + @Override + public void onException(Throwable e) { + Assertions.fail("want no exception but got one", e); + done.countDown(); + } + }); + done.await(); + } + + @Test + public void testSetMessageRequestMode_Success() throws Exception { + doAnswer(new Answer() { + @Override + public RemotingCommand answer(InvocationOnMock mock) { + RemotingCommand request = mock.getArgument(1); + + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + response.setOpaque(request.getOpaque()); + return response; + } + }).when(remotingClient).invokeSync(anyString(), any(RemotingCommand.class), anyLong()); + + mqClientAPI.setMessageRequestMode(brokerAddr, topic, group, MessageRequestMode.POP, 8, 10 * 1000L); + } + + @Test + public void testCreateSubscriptionGroup_Success() throws Exception { + doAnswer(new Answer() { + @Override + public RemotingCommand answer(InvocationOnMock mock) { + RemotingCommand request = mock.getArgument(1); + + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + response.setOpaque(request.getOpaque()); + return response; + } + }).when(remotingClient).invokeSync(anyString(), any(RemotingCommand.class), anyLong()); + + mqClientAPI.createSubscriptionGroup(brokerAddr, new SubscriptionGroupConfig(), 10000); + } + + @Test + public void testCreateTopic_Success() throws Exception { + doAnswer(new Answer() { + @Override + public RemotingCommand answer(InvocationOnMock mock) { + RemotingCommand request = mock.getArgument(1); + + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + response.setOpaque(request.getOpaque()); + return response; + } + }).when(remotingClient).invokeSync(anyString(), any(RemotingCommand.class), anyLong()); + + mqClientAPI.createTopic(brokerAddr, topic, new TopicConfig(), 10000); + } + + @Test + public void testViewMessage() throws Exception { + doAnswer(new Answer() { + @Override + public RemotingCommand answer(InvocationOnMock mock) throws Exception { + RemotingCommand request = mock.getArgument(1); + + RemotingCommand response = RemotingCommand.createResponseCommand(null); + MessageExt message = new MessageExt(); + message.setQueueId(0); + message.setFlag(12); + message.setQueueOffset(0L); + message.setCommitLogOffset(100L); + message.setSysFlag(0); + message.setBornTimestamp(System.currentTimeMillis()); + message.setBornHost(new InetSocketAddress("127.0.0.1", 10)); + message.setStoreTimestamp(System.currentTimeMillis()); + message.setStoreHost(new InetSocketAddress("127.0.0.1", 11)); + message.setBody("body".getBytes()); + message.setTopic(topic); + message.putUserProperty("key", "value"); + response.setBody(MessageDecoder.encode(message, false)); + response.makeCustomHeaderToNet(); + response.setCode(ResponseCode.SUCCESS); + response.setOpaque(request.getOpaque()); + return response; + } + }).when(remotingClient).invokeSync(anyString(), any(RemotingCommand.class), anyLong()); + + MessageExt messageExt = mqClientAPI.viewMessage(brokerAddr, 100L, 10000); + assertThat(messageExt.getTopic()).isEqualTo(topic); + } + + @Test + public void testSearchOffset() throws Exception { + doAnswer(new Answer() { + @Override + public RemotingCommand answer(InvocationOnMock mock) { + RemotingCommand request = mock.getArgument(1); + + final RemotingCommand response = RemotingCommand.createResponseCommand(SearchOffsetResponseHeader.class); + final SearchOffsetResponseHeader responseHeader = (SearchOffsetResponseHeader) response.readCustomHeader(); + responseHeader.setOffset(100L); + response.makeCustomHeaderToNet(); + response.setCode(ResponseCode.SUCCESS); + response.setOpaque(request.getOpaque()); + return response; + } + }).when(remotingClient).invokeSync(anyString(), any(RemotingCommand.class), anyLong()); + + long offset = mqClientAPI.searchOffset(brokerAddr, topic, 0, System.currentTimeMillis() - 1000, 10000); + assertThat(offset).isEqualTo(100L); + } + + @Test + public void testGetMaxOffset() throws Exception { + doAnswer(new Answer() { + @Override + public RemotingCommand answer(InvocationOnMock mock) { + RemotingCommand request = mock.getArgument(1); + + final RemotingCommand response = RemotingCommand.createResponseCommand(GetMaxOffsetResponseHeader.class); + final GetMaxOffsetResponseHeader responseHeader = (GetMaxOffsetResponseHeader) response.readCustomHeader(); + responseHeader.setOffset(100L); + response.makeCustomHeaderToNet(); + response.setCode(ResponseCode.SUCCESS); + response.setOpaque(request.getOpaque()); + return response; + } + }).when(remotingClient).invokeSync(anyString(), any(RemotingCommand.class), anyLong()); + + long offset = mqClientAPI.getMaxOffset(brokerAddr, new MessageQueue(topic, brokerName, 0), 10000); + assertThat(offset).isEqualTo(100L); + } + + @Test + public void testGetMinOffset() throws Exception { + doAnswer(new Answer() { + @Override + public RemotingCommand answer(InvocationOnMock mock) { + RemotingCommand request = mock.getArgument(1); + + final RemotingCommand response = RemotingCommand.createResponseCommand(GetMinOffsetResponseHeader.class); + final GetMinOffsetResponseHeader responseHeader = (GetMinOffsetResponseHeader) response.readCustomHeader(); + responseHeader.setOffset(100L); + response.makeCustomHeaderToNet(); + response.setCode(ResponseCode.SUCCESS); + response.setOpaque(request.getOpaque()); + return response; + } + }).when(remotingClient).invokeSync(anyString(), any(RemotingCommand.class), anyLong()); + + long offset = mqClientAPI.getMinOffset(brokerAddr, new MessageQueue(topic, brokerName, 0), 10000); + assertThat(offset).isEqualTo(100L); + } + + @Test + public void testGetEarliestMsgStoretime() throws Exception { + doAnswer(new Answer() { + @Override + public RemotingCommand answer(InvocationOnMock mock) { + RemotingCommand request = mock.getArgument(1); + + final RemotingCommand response = RemotingCommand.createResponseCommand(GetEarliestMsgStoretimeResponseHeader.class); + final GetEarliestMsgStoretimeResponseHeader responseHeader = (GetEarliestMsgStoretimeResponseHeader) response.readCustomHeader(); + responseHeader.setTimestamp(100L); + response.makeCustomHeaderToNet(); + response.setCode(ResponseCode.SUCCESS); + response.setOpaque(request.getOpaque()); + return response; + } + }).when(remotingClient).invokeSync(anyString(), any(RemotingCommand.class), anyLong()); + + long t = mqClientAPI.getEarliestMsgStoretime(brokerAddr, new MessageQueue(topic, brokerName, 0), 10000); + assertThat(t).isEqualTo(100L); + } + + @Test + public void testQueryConsumerOffset() throws Exception { + doAnswer(new Answer() { + @Override + public RemotingCommand answer(InvocationOnMock mock) { + RemotingCommand request = mock.getArgument(1); + + final RemotingCommand response = + RemotingCommand.createResponseCommand(QueryConsumerOffsetResponseHeader.class); + final QueryConsumerOffsetResponseHeader responseHeader = + (QueryConsumerOffsetResponseHeader) response.readCustomHeader(); + responseHeader.setOffset(100L); + response.makeCustomHeaderToNet(); + response.setCode(ResponseCode.SUCCESS); + response.setOpaque(request.getOpaque()); + return response; + } + }).when(remotingClient).invokeSync(anyString(), any(RemotingCommand.class), anyLong()); + + long t = mqClientAPI.queryConsumerOffset(brokerAddr, new QueryConsumerOffsetRequestHeader(), 1000); + assertThat(t).isEqualTo(100L); + } + + @Test + public void testUpdateConsumerOffset() throws Exception { + doAnswer(new Answer() { + @Override + public RemotingCommand answer(InvocationOnMock mock) { + RemotingCommand request = mock.getArgument(1); + + final RemotingCommand response = + RemotingCommand.createResponseCommand(UpdateConsumerOffsetResponseHeader.class); + response.makeCustomHeaderToNet(); + response.setCode(ResponseCode.SUCCESS); + response.setOpaque(request.getOpaque()); + return response; + } + }).when(remotingClient).invokeSync(anyString(), any(RemotingCommand.class), anyLong()); + + mqClientAPI.updateConsumerOffset(brokerAddr, new UpdateConsumerOffsetRequestHeader(), 1000); + } + + @Test + public void testGetConsumerIdListByGroup() throws Exception { + doAnswer(new Answer() { + @Override + public RemotingCommand answer(InvocationOnMock mock) { + RemotingCommand request = mock.getArgument(1); + + final RemotingCommand response = + RemotingCommand.createResponseCommand(GetConsumerListByGroupResponseHeader.class); + GetConsumerListByGroupResponseBody body = new GetConsumerListByGroupResponseBody(); + body.setConsumerIdList(Collections.singletonList("consumer1")); + response.setBody(body.encode()); + response.makeCustomHeaderToNet(); + response.setCode(ResponseCode.SUCCESS); + response.setOpaque(request.getOpaque()); + return response; + } + }).when(remotingClient).invokeSync(anyString(), any(RemotingCommand.class), anyLong()); + List consumerIdList = mqClientAPI.getConsumerIdListByGroup(brokerAddr, group, 10000); + assertThat(consumerIdList).size().isGreaterThan(0); + } + + private RemotingCommand createResumeSuccessResponse(RemotingCommand request) { + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + response.setOpaque(request.getOpaque()); + return response; + } + + private RemotingCommand createSendMessageSuccessResponse(RemotingCommand request) { RemotingCommand response = RemotingCommand.createResponseCommand(SendMessageResponseHeader.class); response.setCode(ResponseCode.SUCCESS); response.setOpaque(request.getOpaque()); @@ -228,6 +903,58 @@ private RemotingCommand createSuccessResponse(RemotingCommand request) { return response; } + private RemotingCommand createSuccessResponse4UpdateAclConfig(RemotingCommand request) { + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + response.setOpaque(request.getOpaque()); + response.markResponseType(); + response.setRemark(null); + + return response; + } + + private RemotingCommand createSuccessResponse4DeleteAclConfig(RemotingCommand request) { + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + response.setOpaque(request.getOpaque()); + response.markResponseType(); + response.setRemark(null); + + return response; + } + + private RemotingCommand createErrorResponse4UpdateAclConfig(RemotingCommand request) { + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.UPDATE_AND_CREATE_ACL_CONFIG_FAILED); + response.setOpaque(request.getOpaque()); + response.markResponseType(); + response.setRemark("corresponding to accessConfig has been updated failed"); + + return response; + } + + private RemotingCommand createErrorResponse4DeleteAclConfig(RemotingCommand request) { + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.DELETE_ACL_CONFIG_FAILED); + response.setOpaque(request.getOpaque()); + response.markResponseType(); + response.setRemark("corresponding to accessConfig has been deleted failed"); + + return response; + } + + private PlainAccessConfig createUpdateAclConfig() { + + PlainAccessConfig config = new PlainAccessConfig(); + config.setAccessKey("Rocketmq111"); + config.setSecretKey("123456789"); + config.setAdmin(true); + config.setWhiteRemoteAddress("127.0.0.1"); + config.setDefaultTopicPerm("DENY"); + config.setDefaultGroupPerm("SUB"); + return config; + } + private SendMessageRequestHeader createSendMessageRequestHeader() { SendMessageRequestHeader requestHeader = new SendMessageRequestHeader(); requestHeader.setBornTimestamp(System.currentTimeMillis()); @@ -237,4 +964,27 @@ private SendMessageRequestHeader createSendMessageRequestHeader() { requestHeader.setMaxReconsumeTimes(10); return requestHeader; } -} \ No newline at end of file + + @Test + public void testAddWritePermOfBroker() throws Exception { + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocationOnMock) throws Throwable { + RemotingCommand request = invocationOnMock.getArgument(1); + if (request.getCode() != RequestCode.ADD_WRITE_PERM_OF_BROKER) { + return null; + } + + RemotingCommand response = RemotingCommand.createResponseCommand(AddWritePermOfBrokerResponseHeader.class); + AddWritePermOfBrokerResponseHeader responseHeader = (AddWritePermOfBrokerResponseHeader) response.readCustomHeader(); + response.setCode(ResponseCode.SUCCESS); + responseHeader.setAddTopicCount(7); + response.addExtField("addTopicCount", String.valueOf(responseHeader.getAddTopicCount())); + return response; + } + }).when(remotingClient).invokeSync(anyString(), any(RemotingCommand.class), anyLong()); + + int topicCnt = mqClientAPI.addWritePermOfBroker("127.0.0.1", "default-broker", 1000); + assertThat(topicCnt).isEqualTo(7); + } +} diff --git a/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageConcurrentlyServiceTest.java b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageConcurrentlyServiceTest.java new file mode 100644 index 00000000000..749201e3c22 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageConcurrentlyServiceTest.java @@ -0,0 +1,265 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.impl.consumer; + +import java.io.ByteArrayOutputStream; +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.PullCallback; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.consumer.PullStatus; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.impl.CommunicationMode; +import org.apache.rocketmq.client.impl.FindBrokerResult; +import org.apache.rocketmq.client.impl.MQClientAPIImpl; +import org.apache.rocketmq.client.impl.MQClientManager; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.client.stat.ConsumerStatsManager; +import org.apache.rocketmq.common.message.MessageClientExt; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.stats.StatsItem; +import org.apache.rocketmq.common.stats.StatsItemSet; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.body.ConsumeStatus; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ConsumeMessageConcurrentlyServiceTest { + private String consumerGroup; + private String topic = "FooBar"; + private String brokerName = "BrokerA"; + private MQClientInstance mQClientFactory; + + @Mock + private MQClientAPIImpl mQClientAPIImpl; + private PullAPIWrapper pullAPIWrapper; + private RebalancePushImpl rebalancePushImpl; + private DefaultMQPushConsumer pushConsumer; + + @Before + public void init() throws Exception { + ConcurrentMap factoryTable = (ConcurrentMap) FieldUtils.readDeclaredField(MQClientManager.getInstance(), "factoryTable", true); + Collection instances = factoryTable.values(); + for (MQClientInstance instance : instances) { + instance.shutdown(); + } + factoryTable.clear(); + + consumerGroup = "FooBarGroup" + System.currentTimeMillis(); + pushConsumer = new DefaultMQPushConsumer(consumerGroup); + pushConsumer.setNamesrvAddr("127.0.0.1:9876"); + pushConsumer.setPullInterval(60 * 1000); + + pushConsumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, + ConsumeConcurrentlyContext context) { + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + + DefaultMQPushConsumerImpl pushConsumerImpl = pushConsumer.getDefaultMQPushConsumerImpl(); + rebalancePushImpl = spy(new RebalancePushImpl(pushConsumer.getDefaultMQPushConsumerImpl())); + Field field = DefaultMQPushConsumerImpl.class.getDeclaredField("rebalanceImpl"); + field.setAccessible(true); + field.set(pushConsumerImpl, rebalancePushImpl); + pushConsumer.subscribe(topic, "*"); + + // suppress updateTopicRouteInfoFromNameServer + pushConsumer.changeInstanceNameToPID(); + mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(pushConsumer, (RPCHook) FieldUtils.readDeclaredField(pushConsumerImpl, "rpcHook", true)); + mQClientFactory = spy(mQClientFactory); + field = DefaultMQPushConsumerImpl.class.getDeclaredField("mQClientFactory"); + field.setAccessible(true); + field.set(pushConsumerImpl, mQClientFactory); + factoryTable.put(pushConsumer.buildMQClientId(), mQClientFactory); + + field = MQClientInstance.class.getDeclaredField("mQClientAPIImpl"); + field.setAccessible(true); + field.set(mQClientFactory, mQClientAPIImpl); + + pullAPIWrapper = spy(new PullAPIWrapper(mQClientFactory, consumerGroup, false)); + field = DefaultMQPushConsumerImpl.class.getDeclaredField("pullAPIWrapper"); + field.setAccessible(true); + field.set(pushConsumerImpl, pullAPIWrapper); + + pushConsumer.getDefaultMQPushConsumerImpl().getRebalanceImpl().setmQClientFactory(mQClientFactory); + + when(mQClientFactory.getMQClientAPIImpl().pullMessage(anyString(), any(PullMessageRequestHeader.class), + anyLong(), any(CommunicationMode.class), nullable(PullCallback.class))) + .thenAnswer(new Answer() { + @Override + public PullResult answer(InvocationOnMock mock) throws Throwable { + PullMessageRequestHeader requestHeader = mock.getArgument(1); + MessageClientExt messageClientExt = new MessageClientExt(); + messageClientExt.setTopic(topic); + messageClientExt.setQueueId(0); + messageClientExt.setMsgId("123"); + messageClientExt.setBody(new byte[] {'a'}); + messageClientExt.setOffsetMsgId("234"); + messageClientExt.setBornHost(new InetSocketAddress(8080)); + messageClientExt.setStoreHost(new InetSocketAddress(8080)); + PullResult pullResult = createPullResult(requestHeader, PullStatus.FOUND, Collections.singletonList(messageClientExt)); + ((PullCallback) mock.getArgument(4)).onSuccess(pullResult); + return pullResult; + } + }); + + doReturn(new FindBrokerResult("127.0.0.1:10912", false)).when(mQClientFactory).findBrokerAddressInSubscribe(anyString(), anyLong(), anyBoolean()); + doReturn(false).when(mQClientFactory).updateTopicRouteInfoFromNameServer(anyString()); + Set messageQueueSet = new HashSet<>(); + messageQueueSet.add(createPullRequest().getMessageQueue()); + pushConsumer.getDefaultMQPushConsumerImpl().updateTopicSubscribeInfo(topic, messageQueueSet); + pushConsumer.start(); + } + + @Test + public void testPullMessage_ConsumeSuccess() throws InterruptedException, RemotingException, MQBrokerException, NoSuchFieldException,Exception { + final CountDownLatch countDownLatch = new CountDownLatch(1); + final AtomicReference messageAtomic = new AtomicReference<>(); + + ConsumeMessageConcurrentlyService normalServie = new ConsumeMessageConcurrentlyService(pushConsumer.getDefaultMQPushConsumerImpl(), new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, + ConsumeConcurrentlyContext context) { + messageAtomic.set(msgs.get(0)); + countDownLatch.countDown(); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + pushConsumer.getDefaultMQPushConsumerImpl().setConsumeMessageService(normalServie); + + PullMessageService pullMessageService = mQClientFactory.getPullMessageService(); + pullMessageService.executePullRequestImmediately(createPullRequest()); + countDownLatch.await(); + + Thread.sleep(1000); + + ConsumeStatus stats = normalServie.getConsumerStatsManager().consumeStatus(pushConsumer.getDefaultMQPushConsumerImpl().groupName(),topic); + + ConsumerStatsManager mgr = normalServie.getConsumerStatsManager(); + + Field statItmeSetField = mgr.getClass().getDeclaredField("topicAndGroupConsumeOKTPS"); + statItmeSetField.setAccessible(true); + + StatsItemSet itemSet = (StatsItemSet)statItmeSetField.get(mgr); + StatsItem item = itemSet.getAndCreateStatsItem(topic + "@" + pushConsumer.getDefaultMQPushConsumerImpl().groupName()); + + assertThat(item.getValue().sum()).isGreaterThan(0L); + MessageExt msg = messageAtomic.get(); + assertThat(msg).isNotNull(); + assertThat(msg.getTopic()).isEqualTo(topic); + assertThat(msg.getBody()).isEqualTo(new byte[] {'a'}); + } + + @After + public void terminate() { + pushConsumer.shutdown(); + } + + private PullRequest createPullRequest() { + PullRequest pullRequest = new PullRequest(); + pullRequest.setConsumerGroup(consumerGroup); + pullRequest.setNextOffset(1024); + + MessageQueue messageQueue = new MessageQueue(); + messageQueue.setBrokerName(brokerName); + messageQueue.setQueueId(0); + messageQueue.setTopic(topic); + pullRequest.setMessageQueue(messageQueue); + ProcessQueue processQueue = new ProcessQueue(); + processQueue.setLocked(true); + processQueue.setLastLockTimestamp(System.currentTimeMillis()); + pullRequest.setProcessQueue(processQueue); + + return pullRequest; + } + + private PullResultExt createPullResult(PullMessageRequestHeader requestHeader, PullStatus pullStatus, + List messageExtList) throws Exception { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + for (MessageExt messageExt : messageExtList) { + outputStream.write(MessageDecoder.encode(messageExt, false)); + } + return new PullResultExt(pullStatus, requestHeader.getQueueOffset() + messageExtList.size(), 123, 2048, messageExtList, 0, outputStream.toByteArray()); + } + + @Test + public void testConsumeThreadName() throws Exception { + final CountDownLatch countDownLatch = new CountDownLatch(1); + final AtomicReference consumeThreadName = new AtomicReference<>(); + + StringBuilder consumeGroup2 = new StringBuilder(); + for (int i = 0; i < 101; i++) { + consumeGroup2.append(i).append("#"); + } + pushConsumer.setConsumerGroup(consumeGroup2.toString()); + ConsumeMessageConcurrentlyService normalServie = new ConsumeMessageConcurrentlyService(pushConsumer.getDefaultMQPushConsumerImpl(), new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, + ConsumeConcurrentlyContext context) { + consumeThreadName.set(Thread.currentThread().getName()); + countDownLatch.countDown(); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + pushConsumer.getDefaultMQPushConsumerImpl().setConsumeMessageService(normalServie); + + PullMessageService pullMessageService = mQClientFactory.getPullMessageService(); + pullMessageService.executePullRequestImmediately(createPullRequest()); + countDownLatch.await(); + if (consumeGroup2.length() <= 100) { + assertThat(consumeThreadName.get()).startsWith("ConsumeMessageThread_" + consumeGroup2 + "_"); + } else { + assertThat(consumeThreadName.get()).startsWith("ConsumeMessageThread_" + consumeGroup2.substring(0, 100) + "_"); + } + } +} diff --git a/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageOrderlyServiceTest.java b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageOrderlyServiceTest.java new file mode 100644 index 00000000000..5fa78b70090 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageOrderlyServiceTest.java @@ -0,0 +1,261 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.impl.consumer; + +import java.io.ByteArrayOutputStream; +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.PullCallback; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.consumer.PullStatus; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; +import org.apache.rocketmq.client.impl.CommunicationMode; +import org.apache.rocketmq.client.impl.FindBrokerResult; +import org.apache.rocketmq.client.impl.MQClientAPIImpl; +import org.apache.rocketmq.client.impl.MQClientManager; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.common.message.MessageClientExt; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.body.CMResult; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +public class ConsumeMessageOrderlyServiceTest { + private String consumerGroup; + private String topic = "FooBar"; + private String brokerName = "BrokerA"; + private DefaultMQPushConsumer pushConsumer; + private MQClientInstance mQClientFactory; + + @Mock + private MQClientAPIImpl mQClientAPIImpl; + private PullAPIWrapper pullAPIWrapper; + private RebalancePushImpl rebalancePushImpl; + @Before + public void init() throws Exception { + ConcurrentMap factoryTable = (ConcurrentMap) FieldUtils.readDeclaredField(MQClientManager.getInstance(), "factoryTable", true); + Collection instances = factoryTable.values(); + for (MQClientInstance instance : instances) { + instance.shutdown(); + } + factoryTable.clear(); + consumerGroup = "FooBarGroup" + System.currentTimeMillis(); + pushConsumer = new DefaultMQPushConsumer(consumerGroup); + + pushConsumer.setNamesrvAddr("127.0.0.1:9876"); + pushConsumer.setPullInterval(60 * 1000); + + pushConsumer.registerMessageListener(new MessageListenerOrderly() { + + @Override + public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) { + return ConsumeOrderlyStatus.SUCCESS; + } + }); + + + DefaultMQPushConsumerImpl pushConsumerImpl = pushConsumer.getDefaultMQPushConsumerImpl(); + rebalancePushImpl = spy(new RebalancePushImpl(pushConsumer.getDefaultMQPushConsumerImpl())); + Field field = DefaultMQPushConsumerImpl.class.getDeclaredField("rebalanceImpl"); + field.setAccessible(true); + field.set(pushConsumerImpl, rebalancePushImpl); + pushConsumer.subscribe(topic, "*"); + + // suppress updateTopicRouteInfoFromNameServer + pushConsumer.changeInstanceNameToPID(); + mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(pushConsumer, (RPCHook) FieldUtils.readDeclaredField(pushConsumerImpl, "rpcHook", true)); + mQClientFactory = spy(mQClientFactory); + field = DefaultMQPushConsumerImpl.class.getDeclaredField("mQClientFactory"); + field.setAccessible(true); + field.set(pushConsumerImpl, mQClientFactory); + factoryTable.put(pushConsumer.buildMQClientId(), mQClientFactory); + + mQClientAPIImpl = mock(MQClientAPIImpl.class); + field = MQClientInstance.class.getDeclaredField("mQClientAPIImpl"); + field.setAccessible(true); + field.set(mQClientFactory, mQClientAPIImpl); + + pullAPIWrapper = spy(new PullAPIWrapper(mQClientFactory, consumerGroup, false)); + field = DefaultMQPushConsumerImpl.class.getDeclaredField("pullAPIWrapper"); + field.setAccessible(true); + field.set(pushConsumerImpl, pullAPIWrapper); + + pushConsumer.getDefaultMQPushConsumerImpl().getRebalanceImpl().setmQClientFactory(mQClientFactory); + + when(mQClientFactory.getMQClientAPIImpl().pullMessage(anyString(), any(PullMessageRequestHeader.class), + anyLong(), any(CommunicationMode.class), nullable(PullCallback.class))) + .thenAnswer(new Answer() { + @Override + public PullResult answer(InvocationOnMock mock) throws Throwable { + PullMessageRequestHeader requestHeader = mock.getArgument(1); + MessageClientExt messageClientExt = new MessageClientExt(); + messageClientExt.setTopic(topic); + messageClientExt.setQueueId(0); + messageClientExt.setMsgId("123"); + messageClientExt.setBody(new byte[] {'a'}); + messageClientExt.setOffsetMsgId("234"); + messageClientExt.setBornHost(new InetSocketAddress(8080)); + messageClientExt.setStoreHost(new InetSocketAddress(8080)); + PullResult pullResult = createPullResult(requestHeader, PullStatus.FOUND, Collections.singletonList(messageClientExt)); + ((PullCallback) mock.getArgument(4)).onSuccess(pullResult); + return pullResult; + } + }); + + doReturn(new FindBrokerResult("127.0.0.1:10912", false)).when(mQClientFactory).findBrokerAddressInSubscribe(anyString(), anyLong(), anyBoolean()); + doReturn(false).when(mQClientFactory).updateTopicRouteInfoFromNameServer(anyString()); + Set messageQueueSet = new HashSet<>(); + messageQueueSet.add(createPullRequest().getMessageQueue()); + pushConsumer.getDefaultMQPushConsumerImpl().updateTopicSubscribeInfo(topic, messageQueueSet); + pushConsumer.start(); + } + + @Test + public void testConsumeMessageDirectly_WithNoException() { + Map map = new HashMap(); + map.put(ConsumeOrderlyStatus.SUCCESS, CMResult.CR_SUCCESS); + map.put(ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT, CMResult.CR_LATER); + map.put(ConsumeOrderlyStatus.COMMIT, CMResult.CR_COMMIT); + map.put(ConsumeOrderlyStatus.ROLLBACK, CMResult.CR_ROLLBACK); + map.put(null, CMResult.CR_RETURN_NULL); + + for (ConsumeOrderlyStatus consumeOrderlyStatus : map.keySet()) { + final ConsumeOrderlyStatus status = consumeOrderlyStatus; + MessageListenerOrderly listenerOrderly = new MessageListenerOrderly() { + @Override + public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) { + return status; + } + }; + + ConsumeMessageOrderlyService consumeMessageOrderlyService = new ConsumeMessageOrderlyService(pushConsumer.getDefaultMQPushConsumerImpl(), listenerOrderly); + MessageExt msg = new MessageExt(); + msg.setTopic(topic); + assertTrue(consumeMessageOrderlyService.consumeMessageDirectly(msg, brokerName).getConsumeResult().equals(map.get(consumeOrderlyStatus))); + } + + } + + @Test + public void testConsumeMessageDirectly_WithException() { + MessageListenerOrderly listenerOrderly = new MessageListenerOrderly() { + @Override + public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) { + throw new RuntimeException(); + } + }; + + ConsumeMessageOrderlyService consumeMessageOrderlyService = new ConsumeMessageOrderlyService(pushConsumer.getDefaultMQPushConsumerImpl(), listenerOrderly); + MessageExt msg = new MessageExt(); + msg.setTopic(topic); + assertTrue(consumeMessageOrderlyService.consumeMessageDirectly(msg, brokerName).getConsumeResult().equals(CMResult.CR_THROW_EXCEPTION)); + } + + @Test + public void testConsumeThreadName() throws Exception { + final CountDownLatch countDownLatch = new CountDownLatch(1); + final AtomicReference consumeThreadName = new AtomicReference<>(); + + StringBuilder consumeGroup2 = new StringBuilder(); + for (int i = 0; i < 101; i++) { + consumeGroup2.append(i).append("#"); + } + pushConsumer.setConsumerGroup(consumeGroup2.toString()); + + MessageListenerOrderly listenerOrderly = new MessageListenerOrderly() { + @Override + public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) { + consumeThreadName.set(Thread.currentThread().getName()); + countDownLatch.countDown(); + return ConsumeOrderlyStatus.SUCCESS; + } + }; + ConsumeMessageOrderlyService consumeMessageOrderlyService = new ConsumeMessageOrderlyService(pushConsumer.getDefaultMQPushConsumerImpl(), listenerOrderly); + pushConsumer.getDefaultMQPushConsumerImpl().setConsumeMessageService(consumeMessageOrderlyService); + + + PullMessageService pullMessageService = mQClientFactory.getPullMessageService(); + pullMessageService.executePullRequestImmediately(createPullRequest()); + countDownLatch.await(); + if (consumeGroup2.length() <= 100) { + assertThat(consumeThreadName.get()).startsWith("ConsumeMessageThread_" + consumeGroup2 + "_"); + } else { + assertThat(consumeThreadName.get()).startsWith("ConsumeMessageThread_" + consumeGroup2.substring(0, 100) + "_"); + } + } + + private PullRequest createPullRequest() { + PullRequest pullRequest = new PullRequest(); + pullRequest.setConsumerGroup(consumerGroup); + pullRequest.setNextOffset(1024); + + MessageQueue messageQueue = new MessageQueue(); + messageQueue.setBrokerName(brokerName); + messageQueue.setQueueId(0); + messageQueue.setTopic(topic); + pullRequest.setMessageQueue(messageQueue); + ProcessQueue processQueue = new ProcessQueue(); + processQueue.setLocked(true); + processQueue.setLastLockTimestamp(System.currentTimeMillis()); + pullRequest.setProcessQueue(processQueue); + + return pullRequest; + } + + private PullResultExt createPullResult(PullMessageRequestHeader requestHeader, PullStatus pullStatus, + List messageExtList) throws Exception { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + for (MessageExt messageExt : messageExtList) { + outputStream.write(MessageDecoder.encode(messageExt, false)); + } + return new PullResultExt(pullStatus, requestHeader.getQueueOffset() + messageExtList.size(), 123, 2048, messageExtList, 0, outputStream.toByteArray()); + } + +} diff --git a/client/src/test/java/org/apache/rocketmq/client/impl/consumer/DefaultMQPushConsumerImplTest.java b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/DefaultMQPushConsumerImplTest.java index d4f581231f0..879bbc593c1 100644 --- a/client/src/test/java/org/apache/rocketmq/client/impl/consumer/DefaultMQPushConsumerImplTest.java +++ b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/DefaultMQPushConsumerImplTest.java @@ -23,16 +23,31 @@ import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.hook.ConsumeMessageContext; +import org.apache.rocketmq.client.hook.ConsumeMessageHook; +import org.apache.rocketmq.client.hook.FilterMessageContext; +import org.apache.rocketmq.client.hook.FilterMessageHook; import org.apache.rocketmq.common.message.MessageExt; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) public class DefaultMQPushConsumerImplTest { + @Mock + private DefaultMQPushConsumer defaultMQPushConsumer; @Rule public ExpectedException thrown = ExpectedException.none(); + @Test public void checkConfigTest() throws MQClientException { @@ -50,7 +65,6 @@ public void checkConfigTest() throws MQClientException { consumer.registerMessageListener(new MessageListenerConcurrently() { public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { - System.out.println(" Receive New Messages: " + msgs); return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); @@ -58,4 +72,58 @@ public ConsumeConcurrentlyStatus consumeMessage(List msgs, DefaultMQPushConsumerImpl defaultMQPushConsumerImpl = new DefaultMQPushConsumerImpl(consumer, null); defaultMQPushConsumerImpl.start(); } + + @Test + public void testHook() throws Exception { + DefaultMQPushConsumerImpl defaultMQPushConsumerImpl = new DefaultMQPushConsumerImpl(defaultMQPushConsumer, null); + defaultMQPushConsumerImpl.registerConsumeMessageHook(new ConsumeMessageHook() { + @Override + public String hookName() { + return "consumerHook"; + } + + @Override + public void consumeMessageBefore(ConsumeMessageContext context) { + assertThat(context).isNotNull(); + } + + @Override + public void consumeMessageAfter(ConsumeMessageContext context) { + assertThat(context).isNotNull(); + } + }); + defaultMQPushConsumerImpl.registerFilterMessageHook(new FilterMessageHook() { + @Override + public String hookName() { + return "filterHook"; + } + + @Override + public void filterMessage(FilterMessageContext context) { + assertThat(context).isNotNull(); + } + }); + defaultMQPushConsumerImpl.executeHookBefore(new ConsumeMessageContext()); + defaultMQPushConsumerImpl.executeHookAfter(new ConsumeMessageContext()); + } + + @Ignore + @Test + public void testPush() throws Exception { + when(defaultMQPushConsumer.getMessageListener()).thenReturn(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, + ConsumeConcurrentlyContext context) { + assertThat(msgs).size().isGreaterThan(0); + assertThat(context).isNotNull(); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + DefaultMQPushConsumerImpl defaultMQPushConsumerImpl = new DefaultMQPushConsumerImpl(defaultMQPushConsumer, null); + try { + defaultMQPushConsumerImpl.start(); + } finally { + defaultMQPushConsumerImpl.shutdown(); + } + } } diff --git a/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ProcessQueueTest.java b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ProcessQueueTest.java index d6a6dcf72f6..be0bd29f79f 100644 --- a/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ProcessQueueTest.java +++ b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ProcessQueueTest.java @@ -20,7 +20,8 @@ import java.util.Collections; import java.util.List; import org.apache.rocketmq.common.message.MessageExt; -import org.apache.rocketmq.common.protocol.body.ProcessQueueInfo; +import org.apache.rocketmq.remoting.protocol.body.ProcessQueueInfo; +import org.assertj.core.util.Lists; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; @@ -38,7 +39,7 @@ public void testCachedMessageCount() { assertThat(pq.getMsgCount().get()).isEqualTo(100); - pq.takeMessags(10); + pq.takeMessages(10); pq.commit(); assertThat(pq.getMsgCount().get()).isEqualTo(90); @@ -55,7 +56,7 @@ public void testCachedMessageSize() { assertThat(pq.getMsgSize().get()).isEqualTo(100 * 123); - pq.takeMessags(10); + pq.takeMessages(10); pq.commit(); assertThat(pq.getMsgSize().get()).isEqualTo(90 * 123); @@ -64,6 +65,18 @@ public void testCachedMessageSize() { assertThat(pq.getMsgSize().get()).isEqualTo(89 * 123); } + @Test + public void testContainsMessage() { + ProcessQueue pq = new ProcessQueue(); + final List messageList = createMessageList(2); + final MessageExt message0 = messageList.get(0); + final MessageExt message1 = messageList.get(1); + + pq.putMessage(Lists.list(message0)); + assertThat(pq.containsMessage(message0)).isTrue(); + assertThat(pq.containsMessage(message1)).isFalse(); + } + @Test public void testFillProcessQueueInfo() { ProcessQueue pq = new ProcessQueue(); @@ -74,17 +87,17 @@ public void testFillProcessQueueInfo() { assertThat(processQueueInfo.getCachedMsgSizeInMiB()).isEqualTo(12); - pq.takeMessags(10000); + pq.takeMessages(10000); pq.commit(); pq.fillProcessQueueInfo(processQueueInfo); assertThat(processQueueInfo.getCachedMsgSizeInMiB()).isEqualTo(10); - pq.takeMessags(10000); + pq.takeMessages(10000); pq.commit(); pq.fillProcessQueueInfo(processQueueInfo); assertThat(processQueueInfo.getCachedMsgSizeInMiB()).isEqualTo(9); - pq.takeMessags(80000); + pq.takeMessages(80000); pq.commit(); pq.fillProcessQueueInfo(processQueueInfo); assertThat(processQueueInfo.getCachedMsgSizeInMiB()).isEqualTo(0); @@ -95,7 +108,7 @@ private List createMessageList() { } private List createMessageList(int count) { - List messageExtList = new ArrayList(); + List messageExtList = new ArrayList<>(); for (int i = 0; i < count; i++) { MessageExt messageExt = new MessageExt(); messageExt.setQueueOffset(i); @@ -104,4 +117,4 @@ private List createMessageList(int count) { } return messageExtList; } -} \ No newline at end of file +} diff --git a/client/src/test/java/org/apache/rocketmq/client/impl/consumer/RebalanceLitePullImplTest.java b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/RebalanceLitePullImplTest.java new file mode 100644 index 00000000000..1dbda1c99a8 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/RebalanceLitePullImplTest.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.impl.consumer; + +import org.apache.rocketmq.client.consumer.DefaultLitePullConsumer; +import org.apache.rocketmq.client.consumer.store.OffsetStore; +import org.apache.rocketmq.client.consumer.store.ReadOffsetType; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.impl.MQAdminImpl; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.MessageQueue; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class RebalanceLitePullImplTest { + private MessageQueue mq = new MessageQueue("topic1", "broker1", 0); + private MessageQueue retryMq = new MessageQueue(MixAll.RETRY_GROUP_TOPIC_PREFIX + "group", "broker1", 0); + private DefaultLitePullConsumerImpl consumerImpl = mock(DefaultLitePullConsumerImpl.class); + private RebalanceLitePullImpl rebalanceImpl = new RebalanceLitePullImpl(consumerImpl); + private OffsetStore offsetStore = mock(OffsetStore.class); + private DefaultLitePullConsumer consumer = new DefaultLitePullConsumer(); + private MQClientInstance client = mock(MQClientInstance.class); + private MQAdminImpl admin = mock(MQAdminImpl.class); + + public RebalanceLitePullImplTest() { + when(consumerImpl.getDefaultLitePullConsumer()).thenReturn(consumer); + when(consumerImpl.getOffsetStore()).thenReturn(offsetStore); + rebalanceImpl.setmQClientFactory(client); + when(client.getMQAdminImpl()).thenReturn(admin); + } + + @Test + public void testComputePullFromWhereWithException_ne_minus1() throws MQClientException { + for (ConsumeFromWhere where : new ConsumeFromWhere[]{ + ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET, + ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET, + ConsumeFromWhere.CONSUME_FROM_TIMESTAMP}) { + consumer.setConsumeFromWhere(where); + + when(offsetStore.readOffset(any(MessageQueue.class), any(ReadOffsetType.class))).thenReturn(0L); + assertEquals(0, rebalanceImpl.computePullFromWhereWithException(mq)); + + when(offsetStore.readOffset(any(MessageQueue.class), any(ReadOffsetType.class))).thenReturn(-2L); + assertEquals(-1, rebalanceImpl.computePullFromWhereWithException(mq)); + } + } + + @Test + public void testComputePullFromWhereWithException_eq_minus1_last() throws MQClientException { + when(offsetStore.readOffset(any(MessageQueue.class), any(ReadOffsetType.class))).thenReturn(-1L); + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); + when(admin.maxOffset(any(MessageQueue.class))).thenReturn(12345L); + + assertEquals(12345L, rebalanceImpl.computePullFromWhereWithException(mq)); + + assertEquals(0L, rebalanceImpl.computePullFromWhereWithException(retryMq)); + } + + @Test + public void testComputePullFromWhereWithException_eq_minus1_first() throws MQClientException { + when(offsetStore.readOffset(any(MessageQueue.class), any(ReadOffsetType.class))).thenReturn(-1L); + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + assertEquals(0, rebalanceImpl.computePullFromWhereWithException(mq)); + } + + @Test + public void testComputePullFromWhereWithException_eq_minus1_timestamp() throws MQClientException { + when(offsetStore.readOffset(any(MessageQueue.class), any(ReadOffsetType.class))).thenReturn(-1L); + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_TIMESTAMP); + when(admin.searchOffset(any(MessageQueue.class), anyLong())).thenReturn(12345L); + when(admin.maxOffset(any(MessageQueue.class))).thenReturn(23456L); + + assertEquals(12345L, rebalanceImpl.computePullFromWhereWithException(mq)); + + assertEquals(23456L, rebalanceImpl.computePullFromWhereWithException(retryMq)); + } + + +} diff --git a/client/src/test/java/org/apache/rocketmq/client/impl/consumer/RebalancePushImplTest.java b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/RebalancePushImplTest.java index 796a3943087..f55b5869e56 100644 --- a/client/src/test/java/org/apache/rocketmq/client/impl/consumer/RebalancePushImplTest.java +++ b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/RebalancePushImplTest.java @@ -22,23 +22,27 @@ import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.rebalance.AllocateMessageQueueAveragely; import org.apache.rocketmq.client.consumer.store.OffsetStore; +import org.apache.rocketmq.client.consumer.store.ReadOffsetType; import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.impl.MQAdminImpl; import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Spy; -import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.MockitoJUnitRunner; -import org.mockito.stubbing.Answer; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) @@ -47,10 +51,23 @@ public class RebalancePushImplTest { private DefaultMQPushConsumerImpl defaultMQPushConsumer = new DefaultMQPushConsumerImpl(new DefaultMQPushConsumer("RebalancePushImplTest"), null); @Mock private MQClientInstance mqClientInstance; - @Mock - private OffsetStore offsetStore; + private OffsetStore offsetStore = mock(OffsetStore.class); private String consumerGroup = "CID_RebalancePushImplTest"; private String topic = "TopicA"; + private MessageQueue mq = new MessageQueue("topic1", "broker1", 0); + private MessageQueue retryMq = new MessageQueue(MixAll.RETRY_GROUP_TOPIC_PREFIX + "group", "broker1", 0); + private DefaultMQPushConsumerImpl consumerImpl = mock(DefaultMQPushConsumerImpl.class); + private RebalancePushImpl rebalanceImpl = new RebalancePushImpl(consumerImpl); + private DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(); + private MQClientInstance client = mock(MQClientInstance.class); + private MQAdminImpl admin = mock(MQAdminImpl.class); + + public RebalancePushImplTest() { + when(consumerImpl.getDefaultMQPushConsumer()).thenReturn(consumer); + when(consumerImpl.getOffsetStore()).thenReturn(offsetStore); + rebalanceImpl.setmQClientFactory(client); + when(client.getMQAdminImpl()).thenReturn(admin); + } @Test public void testMessageQueueChanged_CountThreshold() { @@ -60,7 +77,7 @@ public void testMessageQueueChanged_CountThreshold() { // Just set pullThresholdForQueue defaultMQPushConsumer.getDefaultMQPushConsumer().setPullThresholdForQueue(1024); - Set allocateResultSet = new HashSet(); + Set allocateResultSet = new HashSet<>(); allocateResultSet.add(new MessageQueue(topic, "BrokerA", 0)); allocateResultSet.add(new MessageQueue(topic, "BrokerA", 1)); doRebalanceForcibly(rebalancePush, allocateResultSet); @@ -91,13 +108,6 @@ private void init(final RebalancePushImpl rebalancePush) { when(mqClientInstance.findConsumerIdList(anyString(), anyString())).thenReturn(Collections.singletonList(consumerGroup)); when(mqClientInstance.getClientId()).thenReturn(consumerGroup); when(defaultMQPushConsumer.getOffsetStore()).thenReturn(offsetStore); - - doAnswer(new Answer() { - @Override - public Object answer(final InvocationOnMock invocation) throws Throwable { - return null; - } - }).when(defaultMQPushConsumer).executePullRequestImmediately(any(PullRequest.class)); } @Test @@ -108,7 +118,7 @@ public void testMessageQueueChanged_SizeThreshold() { // Just set pullThresholdSizeForQueue defaultMQPushConsumer.getDefaultMQPushConsumer().setPullThresholdSizeForQueue(1024); - Set allocateResultSet = new HashSet(); + Set allocateResultSet = new HashSet<>(); allocateResultSet.add(new MessageQueue(topic, "BrokerA", 0)); allocateResultSet.add(new MessageQueue(topic, "BrokerA", 1)); doRebalanceForcibly(rebalancePush, allocateResultSet); @@ -133,7 +143,7 @@ public void testMessageQueueChanged_ConsumerRuntimeInfo() throws MQClientExcepti defaultMQPushConsumer.getDefaultMQPushConsumer().setPullThresholdSizeForQueue(1024); defaultMQPushConsumer.getDefaultMQPushConsumer().setPullThresholdForQueue(1024); - Set allocateResultSet = new HashSet(); + Set allocateResultSet = new HashSet<>(); allocateResultSet.add(new MessageQueue(topic, "BrokerA", 0)); allocateResultSet.add(new MessageQueue(topic, "BrokerA", 1)); doRebalanceForcibly(rebalancePush, allocateResultSet); @@ -160,4 +170,48 @@ public void testMessageQueueChanged_ConsumerRuntimeInfo() throws MQClientExcepti assertThat(defaultMQPushConsumer.consumerRunningInfo().getProperties().get("pullThresholdSizeForTopic")).isEqualTo("1024"); assertThat(defaultMQPushConsumer.consumerRunningInfo().getProperties().get("pullThresholdForTopic")).isEqualTo("1024"); } -} \ No newline at end of file + + @Test + public void testComputePullFromWhereWithException_ne_minus1() throws MQClientException { + for (ConsumeFromWhere where : new ConsumeFromWhere[]{ + ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET, + ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET, + ConsumeFromWhere.CONSUME_FROM_TIMESTAMP}) { + consumer.setConsumeFromWhere(where); + + when(offsetStore.readOffset(any(MessageQueue.class), any(ReadOffsetType.class))).thenReturn(0L); + assertEquals(0, rebalanceImpl.computePullFromWhereWithException(mq)); + } + } + + @Test + public void testComputePullFromWhereWithException_eq_minus1_last() throws MQClientException { + when(offsetStore.readOffset(any(MessageQueue.class), any(ReadOffsetType.class))).thenReturn(-1L); + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); + when(admin.maxOffset(any(MessageQueue.class))).thenReturn(12345L); + + assertEquals(12345L, rebalanceImpl.computePullFromWhereWithException(mq)); + + assertEquals(0L, rebalanceImpl.computePullFromWhereWithException(retryMq)); + } + + @Test + public void testComputePullFromWhereWithException_eq_minus1_first() throws MQClientException { + when(offsetStore.readOffset(any(MessageQueue.class), any(ReadOffsetType.class))).thenReturn(-1L); + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + assertEquals(0, rebalanceImpl.computePullFromWhereWithException(mq)); + } + + @Test + public void testComputePullFromWhereWithException_eq_minus1_timestamp() throws MQClientException { + when(offsetStore.readOffset(any(MessageQueue.class), any(ReadOffsetType.class))).thenReturn(-1L); + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_TIMESTAMP); + when(admin.searchOffset(any(MessageQueue.class), anyLong())).thenReturn(12345L); + when(admin.maxOffset(any(MessageQueue.class))).thenReturn(23456L); + + assertEquals(12345L, rebalanceImpl.computePullFromWhereWithException(mq)); + + assertEquals(23456L, rebalanceImpl.computePullFromWhereWithException(retryMq)); + } + +} diff --git a/client/src/test/java/org/apache/rocketmq/client/impl/factory/MQClientInstanceTest.java b/client/src/test/java/org/apache/rocketmq/client/impl/factory/MQClientInstanceTest.java index 171a95a8b68..acd792b8625 100644 --- a/client/src/test/java/org/apache/rocketmq/client/impl/factory/MQClientInstanceTest.java +++ b/client/src/test/java/org/apache/rocketmq/client/impl/factory/MQClientInstanceTest.java @@ -19,52 +19,67 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.apache.commons.lang3.reflect.FieldUtils; import org.apache.rocketmq.client.ClientConfig; import org.apache.rocketmq.client.admin.MQAdminExtInner; import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.impl.FindBrokerResult; import org.apache.rocketmq.client.impl.MQClientManager; import org.apache.rocketmq.client.impl.consumer.MQConsumerInner; import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl; import org.apache.rocketmq.client.impl.producer.TopicPublishInfo; -import org.apache.rocketmq.common.protocol.route.BrokerData; -import org.apache.rocketmq.common.protocol.route.QueueData; -import org.apache.rocketmq.common.protocol.route.TopicRouteData; import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class MQClientInstanceTest { - private MQClientInstance mqClientInstance = MQClientManager.getInstance().getAndCreateMQClientInstance(new ClientConfig()); + private MQClientInstance mqClientInstance = MQClientManager.getInstance().getOrCreateMQClientInstance(new ClientConfig()); private String topic = "FooBar"; private String group = "FooBarGroup"; + private ConcurrentMap> brokerAddrTable = new ConcurrentHashMap<>(); + + @Before + public void init() throws Exception { + FieldUtils.writeDeclaredField(mqClientInstance, "brokerAddrTable", brokerAddrTable, true); + } @Test public void testTopicRouteData2TopicPublishInfo() { TopicRouteData topicRouteData = new TopicRouteData(); - topicRouteData.setFilterServerTable(new HashMap>()); - List brokerDataList = new ArrayList(); + topicRouteData.setFilterServerTable(new HashMap<>()); + List brokerDataList = new ArrayList<>(); BrokerData brokerData = new BrokerData(); brokerData.setBrokerName("BrokerA"); brokerData.setCluster("DefaultCluster"); - HashMap brokerAddrs = new HashMap(); + HashMap brokerAddrs = new HashMap<>(); brokerAddrs.put(0L, "127.0.0.1:10911"); brokerData.setBrokerAddrs(brokerAddrs); brokerDataList.add(brokerData); topicRouteData.setBrokerDatas(brokerDataList); - List queueDataList = new ArrayList(); + List queueDataList = new ArrayList<>(); QueueData queueData = new QueueData(); queueData.setBrokerName("BrokerA"); queueData.setPerm(6); queueData.setReadQueueNums(3); queueData.setWriteQueueNums(4); - queueData.setTopicSynFlag(0); + queueData.setTopicSysFlag(0); queueDataList.add(queueData); topicRouteData.setQueueDatas(queueDataList); @@ -74,6 +89,34 @@ public void testTopicRouteData2TopicPublishInfo() { assertThat(topicPublishInfo.getMessageQueueList().size()).isEqualTo(4); } + @Test + public void testFindBrokerAddressInSubscribe() { + // dledger normal case + String brokerName = "BrokerA"; + HashMap addrMap = new HashMap<>(); + addrMap.put(0L, "127.0.0.1:10911"); + addrMap.put(1L, "127.0.0.1:10912"); + addrMap.put(2L, "127.0.0.1:10913"); + brokerAddrTable.put(brokerName, addrMap); + long brokerId = 1; + FindBrokerResult brokerResult = mqClientInstance.findBrokerAddressInSubscribe(brokerName, brokerId, false); + assertThat(brokerResult).isNotNull(); + assertThat(brokerResult.getBrokerAddr()).isEqualTo("127.0.0.1:10912"); + assertThat(brokerResult.isSlave()).isTrue(); + + // dledger case, when node n0 was voted as the leader + brokerName = "BrokerB"; + HashMap addrMapNew = new HashMap<>(); + addrMapNew.put(0L, "127.0.0.1:10911"); + addrMapNew.put(2L, "127.0.0.1:10912"); + addrMapNew.put(3L, "127.0.0.1:10913"); + brokerAddrTable.put(brokerName, addrMapNew); + brokerResult = mqClientInstance.findBrokerAddressInSubscribe(brokerName, brokerId, false); + assertThat(brokerResult).isNotNull(); + assertThat(brokerResult.getBrokerAddr()).isEqualTo("127.0.0.1:10912"); + assertThat(brokerResult.isSlave()).isTrue(); + } + @Test public void testRegisterProducer() { boolean flag = mqClientInstance.registerProducer(group, mock(DefaultMQProducerImpl.class)); @@ -100,6 +143,31 @@ public void testRegisterConsumer() throws RemotingException, InterruptedExceptio assertThat(flag).isTrue(); } + + @Test + public void testConsumerRunningInfoWhenConsumersIsEmptyOrNot() throws RemotingException, InterruptedException, MQBrokerException { + MQConsumerInner mockConsumerInner = mock(MQConsumerInner.class); + ConsumerRunningInfo mockConsumerRunningInfo = mock(ConsumerRunningInfo.class); + when(mockConsumerInner.consumerRunningInfo()).thenReturn(mockConsumerRunningInfo); + when(mockConsumerInner.consumeType()).thenReturn(ConsumeType.CONSUME_PASSIVELY); + Properties properties = new Properties(); + when(mockConsumerRunningInfo.getProperties()).thenReturn(properties); + mqClientInstance.unregisterConsumer(group); + + ConsumerRunningInfo runningInfo = mqClientInstance.consumerRunningInfo(group); + assertThat(runningInfo).isNull(); + boolean flag = mqClientInstance.registerConsumer(group, mockConsumerInner); + assertThat(flag).isTrue(); + + runningInfo = mqClientInstance.consumerRunningInfo(group); + assertThat(runningInfo).isNotNull(); + assertThat(mockConsumerInner.consumerRunningInfo().getProperties().get(ConsumerRunningInfo.PROP_CONSUME_TYPE)).isNotNull(); + + mqClientInstance.unregisterConsumer(group); + flag = mqClientInstance.registerConsumer(group, mock(MQConsumerInner.class)); + assertThat(flag).isTrue(); + } + @Test public void testRegisterAdminExt() { boolean flag = mqClientInstance.registerAdminExt(group, mock(MQAdminExtInner.class)); @@ -112,4 +180,5 @@ public void testRegisterAdminExt() { flag = mqClientInstance.registerAdminExt(group, mock(MQAdminExtInner.class)); assertThat(flag).isTrue(); } -} \ No newline at end of file + +} diff --git a/client/src/test/java/org/apache/rocketmq/client/latency/LatencyFaultToleranceImplTest.java b/client/src/test/java/org/apache/rocketmq/client/latency/LatencyFaultToleranceImplTest.java index 86690e40be6..42ccdae5a48 100644 --- a/client/src/test/java/org/apache/rocketmq/client/latency/LatencyFaultToleranceImplTest.java +++ b/client/src/test/java/org/apache/rocketmq/client/latency/LatencyFaultToleranceImplTest.java @@ -16,11 +16,14 @@ */ package org.apache.rocketmq.client.latency; -import java.util.concurrent.TimeUnit; +import org.awaitility.core.ThrowingRunnable; import org.junit.Before; import org.junit.Test; +import java.util.concurrent.TimeUnit; + import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; public class LatencyFaultToleranceImplTest { private LatencyFaultTolerance latencyFaultTolerance; @@ -29,28 +32,31 @@ public class LatencyFaultToleranceImplTest { @Before public void init() { - latencyFaultTolerance = new LatencyFaultToleranceImpl(); + latencyFaultTolerance = new LatencyFaultToleranceImpl(null, null); } @Test public void testUpdateFaultItem() throws Exception { - latencyFaultTolerance.updateFaultItem(brokerName, 3000, 3000); + latencyFaultTolerance.updateFaultItem(brokerName, 3000, 3000, true); assertThat(latencyFaultTolerance.isAvailable(brokerName)).isFalse(); assertThat(latencyFaultTolerance.isAvailable(anotherBrokerName)).isTrue(); } @Test public void testIsAvailable() throws Exception { - latencyFaultTolerance.updateFaultItem(brokerName, 3000, 50); + latencyFaultTolerance.updateFaultItem(brokerName, 3000, 50, true); assertThat(latencyFaultTolerance.isAvailable(brokerName)).isFalse(); - TimeUnit.MILLISECONDS.sleep(70); - assertThat(latencyFaultTolerance.isAvailable(brokerName)).isTrue(); + await().atMost(500, TimeUnit.MILLISECONDS).untilAsserted(new ThrowingRunnable() { + @Override public void run() throws Throwable { + assertThat(latencyFaultTolerance.isAvailable(brokerName)).isTrue(); + } + }); } @Test public void testRemove() throws Exception { - latencyFaultTolerance.updateFaultItem(brokerName, 3000, 3000); + latencyFaultTolerance.updateFaultItem(brokerName, 3000, 3000, true); assertThat(latencyFaultTolerance.isAvailable(brokerName)).isFalse(); latencyFaultTolerance.remove(brokerName); assertThat(latencyFaultTolerance.isAvailable(brokerName)).isTrue(); @@ -58,10 +64,20 @@ public void testRemove() throws Exception { @Test public void testPickOneAtLeast() throws Exception { - latencyFaultTolerance.updateFaultItem(brokerName, 1000, 3000); + latencyFaultTolerance.updateFaultItem(brokerName, 1000, 3000, true); assertThat(latencyFaultTolerance.pickOneAtLeast()).isEqualTo(brokerName); - latencyFaultTolerance.updateFaultItem(anotherBrokerName, 1001, 3000); - assertThat(latencyFaultTolerance.pickOneAtLeast()).isEqualTo(brokerName); + // Bad case, since pickOneAtLeast's behavior becomes random + // latencyFaultTolerance.updateFaultItem(anotherBrokerName, 1001, 3000, "127.0.0.1:12011", true); + // assertThat(latencyFaultTolerance.pickOneAtLeast()).isEqualTo(brokerName); + } + + @Test + public void testIsReachable() throws Exception { + latencyFaultTolerance.updateFaultItem(brokerName, 1000, 3000, true); + assertThat(latencyFaultTolerance.isReachable(brokerName)).isEqualTo(true); + + latencyFaultTolerance.updateFaultItem(anotherBrokerName, 1001, 3000, false); + assertThat(latencyFaultTolerance.isReachable(anotherBrokerName)).isEqualTo(false); } } \ No newline at end of file diff --git a/client/src/test/java/org/apache/rocketmq/client/log/ClientLoggerTest.java b/client/src/test/java/org/apache/rocketmq/client/log/ClientLoggerTest.java deleted file mode 100644 index d3f3be79b2a..00000000000 --- a/client/src/test/java/org/apache/rocketmq/client/log/ClientLoggerTest.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.client.log; - -import org.apache.rocketmq.common.UtilAll; -import org.junit.After; -import org.junit.Test; -import org.slf4j.Logger; - -import java.io.File; - -import static org.junit.Assert.assertEquals; - -public class ClientLoggerTest { - - public static final String CLIENT_LOG_ROOT = "rocketmq.client.logRoot"; - public static final String LOG_DIR; - - static { - LOG_DIR = System.getProperty(CLIENT_LOG_ROOT, System.getProperty("user.home") + "/logs/rocketmqlogs"); - } - - - @After - public void cleanFiles() { - UtilAll.deleteFile(new File(LOG_DIR)); - } - - // FIXME: Workaround for concrete implementation for slf4j, is there any better solution for all slf4j implementations in one class ? 2017/8/1 - @Test - public void testLog4j2() throws Exception { - Logger logger = ClientLogger.getLog(); - - System.out.println(logger); - assertEquals("org.apache.logging.slf4j.Log4jLogger", logger.getClass().getName()); - } -} diff --git a/client/src/test/java/org/apache/rocketmq/client/producer/DefaultMQProducerTest.java b/client/src/test/java/org/apache/rocketmq/client/producer/DefaultMQProducerTest.java index ded22ada914..d4153c7cd97 100644 --- a/client/src/test/java/org/apache/rocketmq/client/producer/DefaultMQProducerTest.java +++ b/client/src/test/java/org/apache/rocketmq/client/producer/DefaultMQProducerTest.java @@ -21,12 +21,18 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import org.apache.rocketmq.client.ClientConfig; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.exception.RequestTimeoutException; import org.apache.rocketmq.client.hook.SendMessageContext; import org.apache.rocketmq.client.hook.SendMessageHook; import org.apache.rocketmq.client.impl.CommunicationMode; @@ -36,12 +42,14 @@ import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl; import org.apache.rocketmq.client.impl.producer.TopicPublishInfo; import org.apache.rocketmq.common.message.Message; -import org.apache.rocketmq.common.protocol.header.SendMessageRequestHeader; -import org.apache.rocketmq.common.protocol.route.BrokerData; -import org.apache.rocketmq.common.protocol.route.QueueData; -import org.apache.rocketmq.common.protocol.route.TopicRouteData; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; import org.apache.rocketmq.remoting.exception.RemotingException; import org.apache.rocketmq.remoting.netty.NettyRemotingClient; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -62,13 +70,16 @@ @RunWith(MockitoJUnitRunner.class) public class DefaultMQProducerTest { @Spy - private MQClientInstance mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(new ClientConfig()); + private MQClientInstance mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(new ClientConfig()); @Mock private MQClientAPIImpl mQClientAPIImpl; + @Mock + private NettyRemotingClient nettyRemotingClient; private DefaultMQProducer producer; private Message message; private Message zeroMsg; + private Message bigMessage; private String topic = "FooBar"; private String producerGroupPrefix = "FooBar_PID"; @@ -77,8 +88,10 @@ public void init() throws Exception { String producerGroupTemp = producerGroupPrefix + System.currentTimeMillis(); producer = new DefaultMQProducer(producerGroupTemp); producer.setNamesrvAddr("127.0.0.1:9876"); + producer.setCompressMsgBodyOverHowmuch(16); message = new Message(topic, new byte[] {'a'}); zeroMsg = new Message(topic, new byte[] {}); + bigMessage = new Message(topic, "This is a very huge message!".getBytes()); producer.start(); @@ -90,7 +103,7 @@ public void init() throws Exception { field.setAccessible(true); field.set(mQClientFactory, mQClientAPIImpl); - producer.getDefaultMQProducerImpl().getmQClientFactory().registerProducer(producerGroupTemp, producer.getDefaultMQProducerImpl()); + producer.getDefaultMQProducerImpl().getMqClientFactory().registerProducer(producerGroupTemp, producer.getDefaultMQProducerImpl()); when(mQClientAPIImpl.sendMessage(anyString(), anyString(), any(Message.class), any(SendMessageRequestHeader.class), anyLong(), any(CommunicationMode.class), nullable(SendMessageContext.class), any(DefaultMQProducerImpl.class))).thenCallRealMethod(); @@ -116,7 +129,7 @@ public void testSendMessage_ZeroMessage() throws InterruptedException, RemotingE @Test public void testSendMessage_NoNameSrv() throws RemotingException, InterruptedException, MQBrokerException { - when(mQClientAPIImpl.getNameServerAddressList()).thenReturn(new ArrayList()); + when(mQClientAPIImpl.getNameServerAddressList()).thenReturn(new ArrayList<>()); try { producer.send(message); failBecauseExceptionWasNotThrown(MQClientException.class); @@ -146,6 +159,174 @@ public void testSendMessageSync_Success() throws RemotingException, InterruptedE assertThat(sendResult.getQueueOffset()).isEqualTo(456L); } + @Test + public void testSendMessageSync_WithBodyCompressed() throws RemotingException, InterruptedException, MQBrokerException, MQClientException { + when(mQClientAPIImpl.getTopicRouteInfoFromNameServer(anyString(), anyLong())).thenReturn(createTopicRoute()); + SendResult sendResult = producer.send(bigMessage); + + assertThat(sendResult.getSendStatus()).isEqualTo(SendStatus.SEND_OK); + assertThat(sendResult.getOffsetMsgId()).isEqualTo("123"); + assertThat(sendResult.getQueueOffset()).isEqualTo(456L); + } + + @Test + public void testSendMessageAsync_Success() throws RemotingException, InterruptedException, MQBrokerException, MQClientException { + final CountDownLatch countDownLatch = new CountDownLatch(1); + when(mQClientAPIImpl.getTopicRouteInfoFromNameServer(anyString(), anyLong())).thenReturn(createTopicRoute()); + producer.send(message, new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + assertThat(sendResult.getSendStatus()).isEqualTo(SendStatus.SEND_OK); + assertThat(sendResult.getOffsetMsgId()).isEqualTo("123"); + assertThat(sendResult.getQueueOffset()).isEqualTo(456L); + countDownLatch.countDown(); + } + + @Override + public void onException(Throwable e) { + countDownLatch.countDown(); + } + }); + countDownLatch.await(3000L, TimeUnit.MILLISECONDS); + } + + @Test + public void testSendMessageAsync() throws RemotingException, MQClientException, InterruptedException { + final AtomicInteger cc = new AtomicInteger(0); + final CountDownLatch countDownLatch = new CountDownLatch(12); + + when(mQClientAPIImpl.getTopicRouteInfoFromNameServer(anyString(), anyLong())).thenReturn(createTopicRoute()); + SendCallback sendCallback = new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + countDownLatch.countDown(); + } + + @Override + public void onException(Throwable e) { + e.printStackTrace(); + cc.incrementAndGet(); + countDownLatch.countDown(); + } + }; + MessageQueueSelector messageQueueSelector = new MessageQueueSelector() { + @Override + public MessageQueue select(List mqs, Message msg, Object arg) { + return null; + } + }; + + // on enableBackpressureForAsyncMode + producer.setEnableBackpressureForAsyncMode(true); + producer.setBackPressureForAsyncSendNum(5000); + producer.setBackPressureForAsyncSendSize(50 * 1024 * 1024); + Message message = new Message(); + message.setTopic("test"); + message.setBody("hello world".getBytes()); + producer.send(new Message(), sendCallback); + producer.send(message, new MessageQueue(), sendCallback); + producer.send(new Message(), new MessageQueue(), sendCallback, 1000); + producer.send(new Message(), messageQueueSelector, null, sendCallback); + producer.send(message, messageQueueSelector, null, sendCallback, 1000); + //this message is send success + producer.send(message, sendCallback, 1000); + + countDownLatch.await(3000L, TimeUnit.MILLISECONDS); + assertThat(cc.get()).isEqualTo(5); + + // off enableBackpressureForAsyncMode + producer.setEnableBackpressureForAsyncMode(false); + producer.send(new Message(), sendCallback); + producer.send(message, new MessageQueue(), sendCallback); + producer.send(new Message(), new MessageQueue(), sendCallback, 1000); + producer.send(new Message(), messageQueueSelector, null, sendCallback); + producer.send(message, messageQueueSelector, null, sendCallback, 1000); + //this message is send success + producer.send(message, sendCallback, 1000); + + countDownLatch.await(3000L, TimeUnit.MILLISECONDS); + assertThat(cc.get()).isEqualTo(10); + } + + @Test + public void testBatchSendMessageAsync() + throws RemotingException, MQClientException, InterruptedException, MQBrokerException { + final AtomicInteger cc = new AtomicInteger(0); + final CountDownLatch countDownLatch = new CountDownLatch(4); + + when(mQClientAPIImpl.getTopicRouteInfoFromNameServer(anyString(), anyLong())).thenReturn(createTopicRoute()); + SendCallback sendCallback = new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + countDownLatch.countDown(); + } + + @Override + public void onException(Throwable e) { + e.printStackTrace(); + cc.incrementAndGet(); + countDownLatch.countDown(); + } + }; + MessageQueueSelector messageQueueSelector = new MessageQueueSelector() { + @Override + public MessageQueue select(List mqs, Message msg, Object arg) { + return null; + } + }; + + List msgs = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + Message message = new Message(); + message.setTopic("test"); + message.setBody(("hello world" + i).getBytes()); + msgs.add(message); + } + + // on enableBackpressureForAsyncMode + producer.setEnableBackpressureForAsyncMode(true); + producer.send(msgs, sendCallback); + producer.send(msgs, sendCallback, 1000); + MessageQueue mq = new MessageQueue("test", "BrokerA", 1); + producer.send(msgs, mq, sendCallback); + // this message is send failed + producer.send(msgs, new MessageQueue(), sendCallback, 1000); + + countDownLatch.await(3000L, TimeUnit.MILLISECONDS); + assertThat(cc.get()).isEqualTo(1); + + // off enableBackpressureForAsyncMode + producer.setEnableBackpressureForAsyncMode(false); + producer.send(msgs, sendCallback); + producer.send(msgs, sendCallback, 1000); + producer.send(msgs, mq, sendCallback); + // this message is send failed + producer.send(msgs, new MessageQueue(), sendCallback, 1000); + + countDownLatch.await(3000L, TimeUnit.MILLISECONDS); + assertThat(cc.get()).isEqualTo(2); + } + + @Test + public void testSendMessageAsync_BodyCompressed() throws RemotingException, InterruptedException, MQBrokerException, MQClientException { + final CountDownLatch countDownLatch = new CountDownLatch(1); + when(mQClientAPIImpl.getTopicRouteInfoFromNameServer(anyString(), anyLong())).thenReturn(createTopicRoute()); + producer.send(bigMessage, new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + assertThat(sendResult.getSendStatus()).isEqualTo(SendStatus.SEND_OK); + assertThat(sendResult.getOffsetMsgId()).isEqualTo("123"); + assertThat(sendResult.getQueueOffset()).isEqualTo(456L); + countDownLatch.countDown(); + } + + @Override + public void onException(Throwable e) { + } + }); + countDownLatch.await(3000L, TimeUnit.MILLISECONDS); + } + @Test public void testSendMessageSync_SuccessWithHook() throws Throwable { when(mQClientAPIImpl.getTopicRouteInfoFromNameServer(anyString(), anyLong())).thenReturn(createTopicRoute()); @@ -200,7 +381,7 @@ public void run() { @Test public void testSetCallbackExecutor() throws MQClientException { - String producerGroupTemp = producerGroupPrefix + System.currentTimeMillis(); + String producerGroupTemp = "testSetCallbackExecutor_" + System.currentTimeMillis(); producer = new DefaultMQProducer(producerGroupTemp); producer.setNamesrvAddr("127.0.0.1:9876"); producer.start(); @@ -209,32 +390,177 @@ public void testSetCallbackExecutor() throws MQClientException { producer.setCallbackExecutor(customized); NettyRemotingClient remotingClient = (NettyRemotingClient) producer.getDefaultMQProducerImpl() - .getmQClientFactory().getMQClientAPIImpl().getRemotingClient(); + .getMqClientFactory().getMQClientAPIImpl().getRemotingClient(); assertThat(remotingClient.getCallbackExecutor()).isEqualTo(customized); } + @Test + public void testRequestMessage() throws RemotingException, RequestTimeoutException, MQClientException, InterruptedException, MQBrokerException { + when(mQClientAPIImpl.getTopicRouteInfoFromNameServer(anyString(), anyLong())).thenReturn(createTopicRoute()); + final AtomicBoolean finish = new AtomicBoolean(false); + new Thread(new Runnable() { + @Override + public void run() { + ConcurrentHashMap responseMap = RequestFutureHolder.getInstance().getRequestFutureTable(); + assertThat(responseMap).isNotNull(); + while (!finish.get()) { + try { + Thread.sleep(10); + } catch (InterruptedException e) { + } + MessageExt responseMsg = new MessageExt(); + responseMsg.setTopic(message.getTopic()); + responseMsg.setBody(message.getBody()); + for (Map.Entry entry : responseMap.entrySet()) { + RequestResponseFuture future = entry.getValue(); + future.putResponseMessage(responseMsg); + } + } + } + }).start(); + Message result = producer.request(message, 3 * 1000L); + finish.getAndSet(true); + assertThat(result).isExactlyInstanceOf(MessageExt.class); + assertThat(result.getTopic()).isEqualTo("FooBar"); + assertThat(result.getBody()).isEqualTo(new byte[] {'a'}); + } + + @Test(expected = RequestTimeoutException.class) + public void testRequestMessage_RequestTimeoutException() throws RemotingException, RequestTimeoutException, MQClientException, InterruptedException, MQBrokerException { + when(mQClientAPIImpl.getTopicRouteInfoFromNameServer(anyString(), anyLong())).thenReturn(createTopicRoute()); + Message result = producer.request(message, 3 * 1000L); + } + + @Test + public void testAsyncRequest_OnSuccess() throws Exception { + when(mQClientAPIImpl.getTopicRouteInfoFromNameServer(anyString(), anyLong())).thenReturn(createTopicRoute()); + final CountDownLatch countDownLatch = new CountDownLatch(1); + RequestCallback requestCallback = new RequestCallback() { + @Override + public void onSuccess(Message message) { + assertThat(message).isExactlyInstanceOf(MessageExt.class); + assertThat(message.getTopic()).isEqualTo("FooBar"); + assertThat(message.getBody()).isEqualTo(new byte[] {'a'}); + assertThat(message.getFlag()).isEqualTo(1); + countDownLatch.countDown(); + } + + @Override + public void onException(Throwable e) { + } + }; + producer.request(message, requestCallback, 3 * 1000L); + ConcurrentHashMap responseMap = RequestFutureHolder.getInstance().getRequestFutureTable(); + assertThat(responseMap).isNotNull(); + + MessageExt responseMsg = new MessageExt(); + responseMsg.setTopic(message.getTopic()); + responseMsg.setBody(message.getBody()); + responseMsg.setFlag(1); + for (Map.Entry entry : responseMap.entrySet()) { + RequestResponseFuture future = entry.getValue(); + future.setSendRequestOk(true); + future.getRequestCallback().onSuccess(responseMsg); + } + countDownLatch.await(3000L, TimeUnit.MILLISECONDS); + } + + @Test + public void testAsyncRequest_OnException() throws Exception { + final AtomicInteger cc = new AtomicInteger(0); + final CountDownLatch countDownLatch = new CountDownLatch(1); + RequestCallback requestCallback = new RequestCallback() { + @Override + public void onSuccess(Message message) { + + } + + @Override + public void onException(Throwable e) { + cc.incrementAndGet(); + countDownLatch.countDown(); + } + }; + MessageQueueSelector messageQueueSelector = new MessageQueueSelector() { + @Override + public MessageQueue select(List mqs, Message msg, Object arg) { + return null; + } + }; + + try { + producer.request(message, requestCallback, 3 * 1000L); + failBecauseExceptionWasNotThrown(Exception.class); + } catch (Exception e) { + ConcurrentHashMap responseMap = RequestFutureHolder.getInstance().getRequestFutureTable(); + assertThat(responseMap).isNotNull(); + for (Map.Entry entry : responseMap.entrySet()) { + RequestResponseFuture future = entry.getValue(); + future.getRequestCallback().onException(e); + } + } + countDownLatch.await(3000L, TimeUnit.MILLISECONDS); + assertThat(cc.get()).isEqualTo(1); + } + + @Test + public void testBatchSendMessageAsync_Success() throws RemotingException, InterruptedException, MQBrokerException, MQClientException { + final CountDownLatch countDownLatch = new CountDownLatch(1); + when(mQClientAPIImpl.getTopicRouteInfoFromNameServer(anyString(), anyLong())).thenReturn(createTopicRoute()); + producer.setAutoBatch(true); + producer.send(message, new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + assertThat(sendResult.getSendStatus()).isEqualTo(SendStatus.SEND_OK); + assertThat(sendResult.getOffsetMsgId()).isEqualTo("123"); + assertThat(sendResult.getQueueOffset()).isEqualTo(456L); + countDownLatch.countDown(); + } + + @Override + public void onException(Throwable e) { + countDownLatch.countDown(); + } + }); + + countDownLatch.await(3000L, TimeUnit.MILLISECONDS); + producer.setAutoBatch(false); + } + + @Test + public void testBatchSendMessageSync_Success() throws RemotingException, InterruptedException, MQBrokerException, MQClientException { + producer.setAutoBatch(true); + when(mQClientAPIImpl.getTopicRouteInfoFromNameServer(anyString(), anyLong())).thenReturn(createTopicRoute()); + SendResult sendResult = producer.send(message); + + assertThat(sendResult.getSendStatus()).isEqualTo(SendStatus.SEND_OK); + assertThat(sendResult.getOffsetMsgId()).isEqualTo("123"); + assertThat(sendResult.getQueueOffset()).isEqualTo(456L); + producer.setAutoBatch(false); + } + public static TopicRouteData createTopicRoute() { TopicRouteData topicRouteData = new TopicRouteData(); - topicRouteData.setFilterServerTable(new HashMap>()); - List brokerDataList = new ArrayList(); + topicRouteData.setFilterServerTable(new HashMap<>()); + List brokerDataList = new ArrayList<>(); BrokerData brokerData = new BrokerData(); brokerData.setBrokerName("BrokerA"); brokerData.setCluster("DefaultCluster"); - HashMap brokerAddrs = new HashMap(); + HashMap brokerAddrs = new HashMap<>(); brokerAddrs.put(0L, "127.0.0.1:10911"); brokerData.setBrokerAddrs(brokerAddrs); brokerDataList.add(brokerData); topicRouteData.setBrokerDatas(brokerDataList); - List queueDataList = new ArrayList(); + List queueDataList = new ArrayList<>(); QueueData queueData = new QueueData(); queueData.setBrokerName("BrokerA"); queueData.setPerm(6); queueData.setReadQueueNums(3); queueData.setWriteQueueNums(4); - queueData.setTopicSynFlag(0); + queueData.setTopicSysFlag(0); queueDataList.add(queueData); topicRouteData.setQueueDatas(queueDataList); return topicRouteData; @@ -270,4 +596,4 @@ public void run() { } return assertionErrors[0]; } -} \ No newline at end of file +} diff --git a/client/src/test/java/org/apache/rocketmq/client/producer/ProduceAccumulatorTest.java b/client/src/test/java/org/apache/rocketmq/client/producer/ProduceAccumulatorTest.java new file mode 100644 index 00000000000..7074fae243d --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/producer/ProduceAccumulatorTest.java @@ -0,0 +1,176 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client.producer; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageBatch; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ProduceAccumulatorTest { + private boolean compareMessageBatch(MessageBatch a, MessageBatch b) { + if (!a.getTopic().equals(b.getTopic())) { + return false; + } + if (!Arrays.equals(a.getBody(), b.getBody())) { + return false; + } + return true; + } + + private class MockMQProducer extends DefaultMQProducer { + private Message beSendMessage = null; + private MessageQueue beSendMessageQueue = null; + + @Override + public SendResult sendDirect(Message msg, MessageQueue mq, + SendCallback sendCallback) { + this.beSendMessage = msg; + this.beSendMessageQueue = mq; + + SendResult sendResult = new SendResult(); + sendResult.setMsgId("123"); + if (sendCallback != null) { + sendCallback.onSuccess(sendResult); + } + return sendResult; + } + } + + @Test + public void testProduceAccumulator_async() throws MQBrokerException, RemotingException, InterruptedException, MQClientException { + MockMQProducer mockMQProducer = new MockMQProducer(); + + ProduceAccumulator produceAccumulator = new ProduceAccumulator("test"); + produceAccumulator.start(); + + final CountDownLatch countDownLatch = new CountDownLatch(1); + List messages = new ArrayList(); + messages.add(new Message("testTopic", "1".getBytes())); + messages.add(new Message("testTopic", "22".getBytes())); + messages.add(new Message("testTopic", "333".getBytes())); + messages.add(new Message("testTopic", "4444".getBytes())); + messages.add(new Message("testTopic", "55555".getBytes())); + for (Message message : messages) { + produceAccumulator.send(message, new SendCallback() { + final CountDownLatch finalCountDownLatch = countDownLatch; + + @Override + public void onSuccess(SendResult sendResult) { + finalCountDownLatch.countDown(); + } + + @Override + public void onException(Throwable e) { + finalCountDownLatch.countDown(); + } + }, mockMQProducer); + } + assertThat(countDownLatch.await(3000L, TimeUnit.MILLISECONDS)).isTrue(); + assertThat(mockMQProducer.beSendMessage instanceof MessageBatch).isTrue(); + + MessageBatch messageBatch1 = (MessageBatch) mockMQProducer.beSendMessage; + MessageBatch messageBatch2 = MessageBatch.generateFromList(messages); + messageBatch2.setBody(messageBatch2.encode()); + + assertThat(compareMessageBatch(messageBatch1, messageBatch2)).isTrue(); + } + + @Test + public void testProduceAccumulator_sync() throws MQBrokerException, RemotingException, InterruptedException, MQClientException { + final MockMQProducer mockMQProducer = new MockMQProducer(); + + final ProduceAccumulator produceAccumulator = new ProduceAccumulator("test"); + produceAccumulator.start(); + + List messages = new ArrayList(); + messages.add(new Message("testTopic", "1".getBytes())); + messages.add(new Message("testTopic", "22".getBytes())); + messages.add(new Message("testTopic", "333".getBytes())); + messages.add(new Message("testTopic", "4444".getBytes())); + messages.add(new Message("testTopic", "55555".getBytes())); + final CountDownLatch countDownLatch = new CountDownLatch(messages.size()); + + for (final Message message : messages) { + new Thread(new Runnable() { + final ProduceAccumulator finalProduceAccumulator = produceAccumulator; + final CountDownLatch finalCountDownLatch = countDownLatch; + final MockMQProducer finalMockMQProducer = mockMQProducer; + final Message finalMessage = message; + + @Override + public void run() { + try { + finalProduceAccumulator.send(finalMessage, finalMockMQProducer); + finalCountDownLatch.countDown(); + } catch (Exception e) { + e.printStackTrace(); + } + } + }).start(); + } + assertThat(countDownLatch.await(3000L, TimeUnit.MILLISECONDS)).isTrue(); + assertThat(mockMQProducer.beSendMessage instanceof MessageBatch).isTrue(); + + MessageBatch messageBatch1 = (MessageBatch) mockMQProducer.beSendMessage; + MessageBatch messageBatch2 = MessageBatch.generateFromList(messages); + messageBatch2.setBody(messageBatch2.encode()); + + assertThat(messageBatch1.getTopic()).isEqualTo(messageBatch2.getTopic()); + // The execution order is uncertain, just compare the length + assertThat(messageBatch1.getBody().length).isEqualTo(messageBatch2.getBody().length); + } + + @Test + public void testProduceAccumulator_sendWithMessageQueue() throws MQBrokerException, RemotingException, InterruptedException, MQClientException { + MockMQProducer mockMQProducer = new MockMQProducer(); + + MessageQueue messageQueue = new MessageQueue("topicTest", "brokerTest", 0); + final ProduceAccumulator produceAccumulator = new ProduceAccumulator("test"); + produceAccumulator.start(); + + Message message = new Message("testTopic", "1".getBytes()); + produceAccumulator.send(message, messageQueue, mockMQProducer); + assertThat(mockMQProducer.beSendMessageQueue).isEqualTo(messageQueue); + + final CountDownLatch countDownLatch = new CountDownLatch(1); + produceAccumulator.send(message, messageQueue, new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + countDownLatch.countDown(); + } + + @Override + public void onException(Throwable e) { + countDownLatch.countDown(); + } + }, mockMQProducer); + assertThat(countDownLatch.await(3000L, TimeUnit.MILLISECONDS)).isTrue(); + assertThat(mockMQProducer.beSendMessageQueue).isEqualTo(messageQueue); + } +} diff --git a/client/src/test/java/org/apache/rocketmq/client/producer/RequestResponseFutureTest.java b/client/src/test/java/org/apache/rocketmq/client/producer/RequestResponseFutureTest.java new file mode 100644 index 00000000000..68615f3b394 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/producer/RequestResponseFutureTest.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client.producer; + +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.rocketmq.common.message.Message; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RequestResponseFutureTest { + + @Test + public void testExecuteRequestCallback() throws Exception { + final AtomicInteger cc = new AtomicInteger(0); + RequestResponseFuture future = new RequestResponseFuture(UUID.randomUUID().toString(), 3 * 1000L, new RequestCallback() { + @Override + public void onSuccess(Message message) { + cc.incrementAndGet(); + } + + @Override + public void onException(Throwable e) { + } + }); + future.setSendRequestOk(true); + future.executeRequestCallback(); + assertThat(cc.get()).isEqualTo(1); + } + +} diff --git a/client/src/test/java/org/apache/rocketmq/client/producer/selector/SelectMessageQueueByHashTest.java b/client/src/test/java/org/apache/rocketmq/client/producer/selector/SelectMessageQueueByHashTest.java index 056e910bb80..8d5a24b3132 100644 --- a/client/src/test/java/org/apache/rocketmq/client/producer/selector/SelectMessageQueueByHashTest.java +++ b/client/src/test/java/org/apache/rocketmq/client/producer/selector/SelectMessageQueueByHashTest.java @@ -34,7 +34,7 @@ public void testSelect() throws Exception { Message message = new Message(topic, new byte[] {}); - List messageQueues = new ArrayList(); + List messageQueues = new ArrayList<>(); for (int i = 0; i < 10; i++) { MessageQueue messageQueue = new MessageQueue(topic, "DefaultBroker", i); messageQueues.add(messageQueue); @@ -44,6 +44,14 @@ public void testSelect() throws Exception { String anotherOrderId = "234"; MessageQueue selected = selector.select(messageQueues, message, orderId); assertThat(selector.select(messageQueues, message, anotherOrderId)).isNotEqualTo(selected); + + //No exception is thrown while order Id hashcode is Integer.MIN + anotherOrderId = "polygenelubricants"; + selector.select(messageQueues, message, anotherOrderId); + anotherOrderId = "GydZG_"; + selector.select(messageQueues, message, anotherOrderId); + anotherOrderId = "DESIGNING WORKHOUSES"; + selector.select(messageQueues, message, anotherOrderId); } } \ No newline at end of file diff --git a/client/src/test/java/org/apache/rocketmq/client/producer/selector/SelectMessageQueueRetryTest.java b/client/src/test/java/org/apache/rocketmq/client/producer/selector/SelectMessageQueueRetryTest.java new file mode 100644 index 00000000000..df4dd87aca9 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/producer/selector/SelectMessageQueueRetryTest.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.producer.selector; + +import org.apache.rocketmq.client.impl.producer.TopicPublishInfo; +import org.apache.rocketmq.common.message.MessageQueue; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SelectMessageQueueRetryTest { + + private String topic = "TEST"; + + @Test + public void testSelect() throws Exception { + + TopicPublishInfo topicPublishInfo = new TopicPublishInfo(); + List messageQueueList = new ArrayList(); + for (int i = 0; i < 3; i++) { + MessageQueue mq = new MessageQueue(); + mq.setBrokerName("broker-" + i); + mq.setQueueId(0); + mq.setTopic(topic); + messageQueueList.add(mq); + } + + topicPublishInfo.setMessageQueueList(messageQueueList); + + Set retryBrokerNameSet = retryBroker(topicPublishInfo); + //always in Set (broker-0, broker-1, broker-2) + assertThat(retryBroker(topicPublishInfo)).isEqualTo(retryBrokerNameSet); + } + + private Set retryBroker(TopicPublishInfo topicPublishInfo) { + MessageQueue mqTmp = null; + Set retryBrokerNameSet = new HashSet(); + for (int times = 0; times < 3; times++) { + String lastBrokerName = null == mqTmp ? null : mqTmp.getBrokerName(); + mqTmp = topicPublishInfo.selectOneMessageQueue(lastBrokerName); + retryBrokerNameSet.add(mqTmp.getBrokerName()); + } + return retryBrokerNameSet; + } + +} \ No newline at end of file diff --git a/client/src/test/java/org/apache/rocketmq/client/rpchook/NamespaceRpcHookTest.java b/client/src/test/java/org/apache/rocketmq/client/rpchook/NamespaceRpcHookTest.java new file mode 100644 index 00000000000..385c2ceec01 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/rpchook/NamespaceRpcHookTest.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client.rpchook; + +import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NamespaceRpcHookTest { + private NamespaceRpcHook namespaceRpcHook; + private ClientConfig clientConfig; + private String namespace = "namespace"; + + + @Test + public void testDoBeforeRequestWithNamespace() { + clientConfig = new ClientConfig(); + clientConfig.setNamespaceV2(namespace); + namespaceRpcHook = new NamespaceRpcHook(clientConfig); + PullMessageRequestHeader pullMessageRequestHeader = new PullMessageRequestHeader(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, pullMessageRequestHeader); + namespaceRpcHook.doBeforeRequest("", request); + assertThat(request.getExtFields().get(MixAll.RPC_REQUEST_HEADER_NAMESPACED_FIELD)).isEqualTo("true"); + assertThat(request.getExtFields().get(MixAll.RPC_REQUEST_HEADER_NAMESPACE_FIELD)).isEqualTo(namespace); + } + + @Test + public void testDoBeforeRequestWithoutNamespace() { + clientConfig = new ClientConfig(); + namespaceRpcHook = new NamespaceRpcHook(clientConfig); + PullMessageRequestHeader pullMessageRequestHeader = new PullMessageRequestHeader(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, pullMessageRequestHeader); + namespaceRpcHook.doBeforeRequest("", request); + assertThat(request.getExtFields()).isNull(); + } +} \ No newline at end of file diff --git a/client/src/test/java/org/apache/rocketmq/client/trace/DefaultMQConsumerWithOpenTracingTest.java b/client/src/test/java/org/apache/rocketmq/client/trace/DefaultMQConsumerWithOpenTracingTest.java new file mode 100644 index 00000000000..a39ae4a4ded --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/trace/DefaultMQConsumerWithOpenTracingTest.java @@ -0,0 +1,239 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client.trace; + +import io.opentracing.mock.MockSpan; +import io.opentracing.mock.MockTracer; +import io.opentracing.tag.Tags; +import java.io.ByteArrayOutputStream; +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.PullCallback; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.consumer.PullStatus; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.consumer.store.OffsetStore; +import org.apache.rocketmq.client.consumer.store.ReadOffsetType; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.impl.CommunicationMode; +import org.apache.rocketmq.client.impl.FindBrokerResult; +import org.apache.rocketmq.client.impl.MQClientAPIImpl; +import org.apache.rocketmq.client.impl.MQClientManager; +import org.apache.rocketmq.client.impl.consumer.ConsumeMessageConcurrentlyService; +import org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl; +import org.apache.rocketmq.client.impl.consumer.ProcessQueue; +import org.apache.rocketmq.client.impl.consumer.PullAPIWrapper; +import org.apache.rocketmq.client.impl.consumer.PullMessageService; +import org.apache.rocketmq.client.impl.consumer.PullRequest; +import org.apache.rocketmq.client.impl.consumer.PullResultExt; +import org.apache.rocketmq.client.impl.consumer.RebalancePushImpl; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.client.trace.hook.ConsumeMessageOpenTracingHookImpl; +import org.apache.rocketmq.common.message.MessageClientExt; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.waitAtMost; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class DefaultMQConsumerWithOpenTracingTest { + private String consumerGroup; + + private String topic = "FooBar"; + private String brokerName = "BrokerA"; + private MQClientInstance mQClientFactory; + + @Mock + private MQClientAPIImpl mQClientAPIImpl; + private PullAPIWrapper pullAPIWrapper; + private RebalancePushImpl rebalancePushImpl; + private DefaultMQPushConsumer pushConsumer; + private final MockTracer tracer = new MockTracer(); + + @Before + public void init() throws Exception { + ConcurrentMap factoryTable = (ConcurrentMap) FieldUtils.readDeclaredField(MQClientManager.getInstance(), "factoryTable", true); + for (Map.Entry entry : factoryTable.entrySet()) { + entry.getValue().shutdown(); + } + factoryTable.clear(); + + when(mQClientAPIImpl.pullMessage(anyString(), any(PullMessageRequestHeader.class), + anyLong(), any(CommunicationMode.class), nullable(PullCallback.class))) + .thenAnswer(new Answer() { + @Override + public PullResult answer(InvocationOnMock mock) throws Throwable { + PullMessageRequestHeader requestHeader = mock.getArgument(1); + MessageClientExt messageClientExt = new MessageClientExt(); + messageClientExt.setTopic(topic); + messageClientExt.setQueueId(0); + messageClientExt.setMsgId("123"); + messageClientExt.setBody(new byte[] {'a'}); + messageClientExt.setOffsetMsgId("234"); + messageClientExt.setBornHost(new InetSocketAddress(8080)); + messageClientExt.setStoreHost(new InetSocketAddress(8080)); + PullResult pullResult = createPullResult(requestHeader, PullStatus.FOUND, Collections.singletonList(messageClientExt)); + ((PullCallback) mock.getArgument(4)).onSuccess(pullResult); + return pullResult; + } + }); + + consumerGroup = "FooBarGroup" + System.currentTimeMillis(); + pushConsumer = new DefaultMQPushConsumer(consumerGroup); + pushConsumer.getDefaultMQPushConsumerImpl().registerConsumeMessageHook( + new ConsumeMessageOpenTracingHookImpl(tracer)); + pushConsumer.setNamesrvAddr("127.0.0.1:9876"); + pushConsumer.setPullInterval(60 * 1000); + + OffsetStore offsetStore = Mockito.mock(OffsetStore.class); + Mockito.when(offsetStore.readOffset(any(MessageQueue.class), any(ReadOffsetType.class))).thenReturn(0L); + pushConsumer.setOffsetStore(offsetStore); + + pushConsumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, + ConsumeConcurrentlyContext context) { + return null; + } + }); + + DefaultMQPushConsumerImpl pushConsumerImpl = pushConsumer.getDefaultMQPushConsumerImpl(); + + // suppress updateTopicRouteInfoFromNameServer + pushConsumer.changeInstanceNameToPID(); + mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(pushConsumer, (RPCHook) FieldUtils.readDeclaredField(pushConsumerImpl, "rpcHook", true)); + FieldUtils.writeDeclaredField(mQClientFactory, "mQClientAPIImpl", mQClientAPIImpl, true); + mQClientFactory = spy(mQClientFactory); + factoryTable.put(pushConsumer.buildMQClientId(), mQClientFactory); + doReturn(false).when(mQClientFactory).updateTopicRouteInfoFromNameServer(anyString()); + + doReturn(new FindBrokerResult("127.0.0.1:10911", false)).when(mQClientFactory).findBrokerAddressInSubscribe(anyString(), anyLong(), anyBoolean()); + + Set messageQueueSet = new HashSet<>(); + messageQueueSet.add(createPullRequest().getMessageQueue()); + pushConsumerImpl.updateTopicSubscribeInfo(topic, messageQueueSet); + + pushConsumer.subscribe(topic, "*"); + pushConsumer.start(); + } + + @After + public void terminate() { + pushConsumer.shutdown(); + } + + @Test + public void testPullMessage_WithTrace_Success() throws InterruptedException, RemotingException, MQBrokerException, MQClientException { + final CountDownLatch countDownLatch = new CountDownLatch(1); + final AtomicReference messageAtomic = new AtomicReference<>(); + pushConsumer.getDefaultMQPushConsumerImpl().setConsumeMessageService(new ConsumeMessageConcurrentlyService(pushConsumer.getDefaultMQPushConsumerImpl(), new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, + ConsumeConcurrentlyContext context) { + messageAtomic.set(msgs.get(0)); + countDownLatch.countDown(); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + })); + + PullMessageService pullMessageService = mQClientFactory.getPullMessageService(); + pullMessageService.executePullRequestImmediately(createPullRequest()); + countDownLatch.await(30, TimeUnit.SECONDS); + MessageExt msg = messageAtomic.get(); + assertThat(msg).isNotNull(); + assertThat(msg.getTopic()).isEqualTo(topic); + assertThat(msg.getBody()).isEqualTo(new byte[] {'a'}); + + // wait until consumeMessageAfter hook of tracer is done surely. + waitAtMost(1, TimeUnit.SECONDS).until(new Callable() { + @Override + public Object call() throws Exception { + return tracer.finishedSpans().size() == 1; + } + }); + + MockSpan span = tracer.finishedSpans().get(0); + assertThat(span.tags().get(Tags.MESSAGE_BUS_DESTINATION.getKey())).isEqualTo(topic); + assertThat(span.tags().get(Tags.SPAN_KIND.getKey())).isEqualTo(Tags.SPAN_KIND_CONSUMER); + assertThat(span.tags().get(TraceConstants.ROCKETMQ_SUCCESS)).isEqualTo(true); + } + + private PullRequest createPullRequest() { + PullRequest pullRequest = new PullRequest(); + pullRequest.setConsumerGroup(consumerGroup); + pullRequest.setNextOffset(1024); + + MessageQueue messageQueue = new MessageQueue(); + messageQueue.setBrokerName(brokerName); + messageQueue.setQueueId(0); + messageQueue.setTopic(topic); + pullRequest.setMessageQueue(messageQueue); + ProcessQueue processQueue = new ProcessQueue(); + processQueue.setLocked(true); + processQueue.setLastLockTimestamp(System.currentTimeMillis()); + pullRequest.setProcessQueue(processQueue); + + return pullRequest; + } + + private PullResultExt createPullResult(PullMessageRequestHeader requestHeader, PullStatus pullStatus, + List messageExtList) throws Exception { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + for (MessageExt messageExt : messageExtList) { + outputStream.write(MessageDecoder.encode(messageExt, false)); + } + return new PullResultExt(pullStatus, requestHeader.getQueueOffset() + messageExtList.size(), 123, 2048, messageExtList, 0, outputStream.toByteArray()); + } + +} diff --git a/client/src/test/java/org/apache/rocketmq/client/trace/DefaultMQConsumerWithTraceTest.java b/client/src/test/java/org/apache/rocketmq/client/trace/DefaultMQConsumerWithTraceTest.java new file mode 100644 index 00000000000..60aa446bbe9 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/trace/DefaultMQConsumerWithTraceTest.java @@ -0,0 +1,339 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client.trace; + +import java.io.ByteArrayOutputStream; +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.PullCallback; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.consumer.PullStatus; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.impl.CommunicationMode; +import org.apache.rocketmq.client.impl.FindBrokerResult; +import org.apache.rocketmq.client.impl.MQClientAPIImpl; +import org.apache.rocketmq.client.impl.MQClientManager; +import org.apache.rocketmq.client.impl.consumer.ConsumeMessageConcurrentlyService; +import org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl; +import org.apache.rocketmq.client.impl.consumer.ProcessQueue; +import org.apache.rocketmq.client.impl.consumer.PullAPIWrapper; +import org.apache.rocketmq.client.impl.consumer.PullMessageService; +import org.apache.rocketmq.client.impl.consumer.PullRequest; +import org.apache.rocketmq.client.impl.consumer.PullResultExt; +import org.apache.rocketmq.client.impl.consumer.RebalancePushImpl; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.common.message.MessageClientExt; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.Silent.class) +public class DefaultMQConsumerWithTraceTest { + private String consumerGroup; + private String consumerGroupNormal; + private String producerGroupTraceTemp = TopicValidator.RMQ_SYS_TRACE_TOPIC + System.currentTimeMillis(); + + private String topic = "FooBar"; + private String brokerName = "BrokerA"; + private MQClientInstance mQClientFactory; + + @Mock + private MQClientAPIImpl mQClientAPIImpl; + private PullAPIWrapper pullAPIWrapper; + private RebalancePushImpl rebalancePushImpl; + private DefaultMQPushConsumer pushConsumer; + private DefaultMQPushConsumer normalPushConsumer; + private DefaultMQPushConsumer customTraceTopicPushConsumer; + + private AsyncTraceDispatcher asyncTraceDispatcher; + private MQClientInstance mQClientTraceFactory; + @Mock + private MQClientAPIImpl mQClientTraceAPIImpl; + private DefaultMQProducer traceProducer; + private String customerTraceTopic = "rmq_trace_topic_12345"; + + @Before + public void init() throws Exception { + ConcurrentMap factoryTable = (ConcurrentMap) FieldUtils.readDeclaredField(MQClientManager.getInstance(), "factoryTable", true); + for (Map.Entry entry : factoryTable.entrySet()) { + entry.getValue().shutdown(); + } + factoryTable.clear(); + + consumerGroup = "FooBarGroup" + System.currentTimeMillis(); + pushConsumer = new DefaultMQPushConsumer(consumerGroup, true, ""); + consumerGroupNormal = "FooBarGroup" + System.currentTimeMillis(); + normalPushConsumer = new DefaultMQPushConsumer(consumerGroupNormal, false, ""); + customTraceTopicPushConsumer = new DefaultMQPushConsumer(consumerGroup, true, customerTraceTopic); + pushConsumer.setNamesrvAddr("127.0.0.1:9876"); + pushConsumer.setPullInterval(60 * 1000); + + asyncTraceDispatcher = (AsyncTraceDispatcher) pushConsumer.getTraceDispatcher(); + traceProducer = asyncTraceDispatcher.getTraceProducer(); + + pushConsumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, + ConsumeConcurrentlyContext context) { + return null; + } + }); + + DefaultMQPushConsumerImpl pushConsumerImpl = pushConsumer.getDefaultMQPushConsumerImpl(); + + // suppress updateTopicRouteInfoFromNameServer + pushConsumer.changeInstanceNameToPID(); + mQClientFactory = spy(MQClientManager.getInstance().getOrCreateMQClientInstance(pushConsumer, (RPCHook) FieldUtils.readDeclaredField(pushConsumerImpl, "rpcHook", true))); + factoryTable.put(pushConsumer.buildMQClientId(), mQClientFactory); + doReturn(false).when(mQClientFactory).updateTopicRouteInfoFromNameServer(anyString()); + + rebalancePushImpl = spy(new RebalancePushImpl(pushConsumer.getDefaultMQPushConsumerImpl())); + Field field = DefaultMQPushConsumerImpl.class.getDeclaredField("rebalanceImpl"); + field.setAccessible(true); + field.set(pushConsumerImpl, rebalancePushImpl); + pushConsumer.subscribe(topic, "*"); + + pushConsumer.start(); + + mQClientFactory = spy(pushConsumerImpl.getmQClientFactory()); + mQClientTraceFactory = spy(pushConsumerImpl.getmQClientFactory()); + + field = DefaultMQPushConsumerImpl.class.getDeclaredField("mQClientFactory"); + field.setAccessible(true); + field.set(pushConsumerImpl, mQClientFactory); + + field = MQClientInstance.class.getDeclaredField("mQClientAPIImpl"); + field.setAccessible(true); + field.set(mQClientFactory, mQClientAPIImpl); + + Field fieldTrace = DefaultMQProducerImpl.class.getDeclaredField("mQClientFactory"); + fieldTrace.setAccessible(true); + fieldTrace.set(traceProducer.getDefaultMQProducerImpl(), mQClientTraceFactory); + + fieldTrace = MQClientInstance.class.getDeclaredField("mQClientAPIImpl"); + fieldTrace.setAccessible(true); + fieldTrace.set(mQClientTraceFactory, mQClientTraceAPIImpl); + + pullAPIWrapper = spy(new PullAPIWrapper(mQClientFactory, consumerGroup, false)); + field = DefaultMQPushConsumerImpl.class.getDeclaredField("pullAPIWrapper"); + field.setAccessible(true); + field.set(pushConsumerImpl, pullAPIWrapper); + + pushConsumer.getDefaultMQPushConsumerImpl().getRebalanceImpl().setmQClientFactory(mQClientFactory); + mQClientFactory.registerConsumer(consumerGroup, pushConsumerImpl); + + when(mQClientFactory.getMQClientAPIImpl().pullMessage(anyString(), any(PullMessageRequestHeader.class), + anyLong(), any(CommunicationMode.class), nullable(PullCallback.class))) + .thenAnswer(new Answer() { + @Override + public PullResult answer(InvocationOnMock mock) throws Throwable { + PullMessageRequestHeader requestHeader = mock.getArgument(1); + MessageClientExt messageClientExt = new MessageClientExt(); + messageClientExt.setTopic(topic); + messageClientExt.setQueueId(0); + messageClientExt.setMsgId("123"); + messageClientExt.setBody(new byte[] {'a'}); + messageClientExt.setOffsetMsgId("234"); + messageClientExt.setBornHost(new InetSocketAddress(8080)); + messageClientExt.setStoreHost(new InetSocketAddress(8080)); + PullResult pullResult = createPullResult(requestHeader, PullStatus.FOUND, Collections.singletonList(messageClientExt)); + ((PullCallback) mock.getArgument(4)).onSuccess(pullResult); + return pullResult; + } + }); + + doReturn(new FindBrokerResult("127.0.0.1:10911", false)).when(mQClientFactory).findBrokerAddressInSubscribe(anyString(), anyLong(), anyBoolean()); + Set messageQueueSet = new HashSet<>(); + messageQueueSet.add(createPullRequest().getMessageQueue()); + pushConsumer.getDefaultMQPushConsumerImpl().updateTopicSubscribeInfo(topic, messageQueueSet); + } + + @After + public void terminate() { + pushConsumer.shutdown(); + } + + @Test + public void testPullMessage_WithTrace_Success() throws InterruptedException, RemotingException, MQBrokerException, MQClientException { + traceProducer.getDefaultMQProducerImpl().getMqClientFactory().registerProducer(producerGroupTraceTemp, traceProducer.getDefaultMQProducerImpl()); + + final CountDownLatch countDownLatch = new CountDownLatch(1); + final AtomicReference messageAtomic = new AtomicReference<>(); + pushConsumer.getDefaultMQPushConsumerImpl().setConsumeMessageService(new ConsumeMessageConcurrentlyService(pushConsumer.getDefaultMQPushConsumerImpl(), new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, + ConsumeConcurrentlyContext context) { + messageAtomic.set(msgs.get(0)); + countDownLatch.countDown(); + return null; + } + })); + + PullMessageService pullMessageService = mQClientFactory.getPullMessageService(); + pullMessageService.executePullRequestImmediately(createPullRequest()); + countDownLatch.await(30, TimeUnit.SECONDS); + MessageExt msg = messageAtomic.get(); + assertThat(msg).isNotNull(); + assertThat(msg.getTopic()).isEqualTo(topic); + assertThat(msg.getBody()).isEqualTo(new byte[] {'a'}); + } + + @Test + public void testPushConsumerWithTraceTLS() { + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumerGroup", true, null); + consumer.setUseTLS(true); + AsyncTraceDispatcher asyncTraceDispatcher = (AsyncTraceDispatcher) consumer.getTraceDispatcher(); + Assert.assertTrue(asyncTraceDispatcher.getTraceProducer().isUseTLS()); + } + + private PullRequest createPullRequest() { + PullRequest pullRequest = new PullRequest(); + pullRequest.setConsumerGroup(consumerGroup); + pullRequest.setNextOffset(1024); + + MessageQueue messageQueue = new MessageQueue(); + messageQueue.setBrokerName(brokerName); + messageQueue.setQueueId(0); + messageQueue.setTopic(topic); + pullRequest.setMessageQueue(messageQueue); + ProcessQueue processQueue = new ProcessQueue(); + processQueue.setLocked(true); + processQueue.setLastLockTimestamp(System.currentTimeMillis()); + pullRequest.setProcessQueue(processQueue); + + return pullRequest; + } + + private PullResultExt createPullResult(PullMessageRequestHeader requestHeader, PullStatus pullStatus, + List messageExtList) throws Exception { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + for (MessageExt messageExt : messageExtList) { + outputStream.write(MessageDecoder.encode(messageExt, false)); + } + return new PullResultExt(pullStatus, requestHeader.getQueueOffset() + messageExtList.size(), 123, 2048, messageExtList, 0, outputStream.toByteArray()); + } + + public static TopicRouteData createTopicRoute() { + TopicRouteData topicRouteData = new TopicRouteData(); + + topicRouteData.setFilterServerTable(new HashMap<>()); + List brokerDataList = new ArrayList<>(); + BrokerData brokerData = new BrokerData(); + brokerData.setBrokerName("BrokerA"); + brokerData.setCluster("DefaultCluster"); + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(0L, "127.0.0.1:10911"); + brokerData.setBrokerAddrs(brokerAddrs); + brokerDataList.add(brokerData); + topicRouteData.setBrokerDatas(brokerDataList); + + List queueDataList = new ArrayList<>(); + QueueData queueData = new QueueData(); + queueData.setBrokerName("BrokerA"); + queueData.setPerm(6); + queueData.setReadQueueNums(3); + queueData.setWriteQueueNums(4); + queueData.setTopicSysFlag(0); + queueDataList.add(queueData); + topicRouteData.setQueueDatas(queueDataList); + return topicRouteData; + } + + private SendResult createSendResult(SendStatus sendStatus) { + SendResult sendResult = new SendResult(); + sendResult.setMsgId("123"); + sendResult.setOffsetMsgId("123"); + sendResult.setQueueOffset(456); + sendResult.setSendStatus(sendStatus); + sendResult.setRegionId("HZ"); + return sendResult; + } + + public static TopicRouteData createTraceTopicRoute() { + TopicRouteData topicRouteData = new TopicRouteData(); + + topicRouteData.setFilterServerTable(new HashMap<>()); + List brokerDataList = new ArrayList<>(); + BrokerData brokerData = new BrokerData(); + brokerData.setBrokerName("broker-trace"); + brokerData.setCluster("DefaultCluster"); + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(0L, "127.0.0.1:10912"); + brokerData.setBrokerAddrs(brokerAddrs); + brokerDataList.add(brokerData); + topicRouteData.setBrokerDatas(brokerDataList); + + List queueDataList = new ArrayList<>(); + QueueData queueData = new QueueData(); + queueData.setBrokerName("broker-trace"); + queueData.setPerm(6); + queueData.setReadQueueNums(1); + queueData.setWriteQueueNums(1); + queueData.setTopicSysFlag(1); + queueDataList.add(queueData); + topicRouteData.setQueueDatas(queueDataList); + return topicRouteData; + } +} diff --git a/client/src/test/java/org/apache/rocketmq/client/trace/DefaultMQLitePullConsumerWithTraceTest.java b/client/src/test/java/org/apache/rocketmq/client/trace/DefaultMQLitePullConsumerWithTraceTest.java new file mode 100644 index 00000000000..e0573bdfb0b --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/trace/DefaultMQLitePullConsumerWithTraceTest.java @@ -0,0 +1,319 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.client.trace; + +import java.io.ByteArrayOutputStream; +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.client.consumer.DefaultLitePullConsumer; +import org.apache.rocketmq.client.consumer.PullCallback; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.consumer.PullStatus; +import org.apache.rocketmq.client.consumer.store.OffsetStore; +import org.apache.rocketmq.client.consumer.store.ReadOffsetType; +import org.apache.rocketmq.client.impl.CommunicationMode; +import org.apache.rocketmq.client.impl.FindBrokerResult; +import org.apache.rocketmq.client.impl.MQAdminImpl; +import org.apache.rocketmq.client.impl.MQClientAPIImpl; +import org.apache.rocketmq.client.impl.MQClientManager; +import org.apache.rocketmq.client.impl.consumer.DefaultLitePullConsumerImpl; +import org.apache.rocketmq.client.impl.consumer.PullAPIWrapper; +import org.apache.rocketmq.client.impl.consumer.PullResultExt; +import org.apache.rocketmq.client.impl.consumer.RebalanceImpl; +import org.apache.rocketmq.client.impl.consumer.RebalanceService; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.common.message.MessageClientExt; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class DefaultMQLitePullConsumerWithTraceTest { + + @Spy + private MQClientInstance mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(new ClientConfig()); + + @Mock + private MQClientAPIImpl mQClientAPIImpl; + @Mock + private MQAdminImpl mQAdminImpl; + + private AsyncTraceDispatcher asyncTraceDispatcher; + private DefaultMQProducer traceProducer; + private RebalanceImpl rebalanceImpl; + private OffsetStore offsetStore; + private DefaultLitePullConsumerImpl litePullConsumerImpl; + private String consumerGroup = "LitePullConsumerGroup"; + private String topic = "LitePullConsumerTest"; + private String brokerName = "BrokerA"; + private String producerGroupTraceTemp = TopicValidator.RMQ_SYS_TRACE_TOPIC + System.currentTimeMillis(); + + private String customerTraceTopic = "rmq_trace_topic_12345"; + + @BeforeClass + public static void setUpEnv() { + System.setProperty("rocketmq.client.logRoot", System.getProperty("java.io.tmpdir")); + } + + @Before + public void init() throws Exception { + Field field = MQClientInstance.class.getDeclaredField("rebalanceService"); + field.setAccessible(true); + RebalanceService rebalanceService = (RebalanceService) field.get(mQClientFactory); + field = RebalanceService.class.getDeclaredField("waitInterval"); + field.setAccessible(true); + field.set(rebalanceService, 100); + } + + @Test + public void testSubscribe_PollMessageSuccess_WithDefaultTraceTopic() throws Exception { + DefaultLitePullConsumer litePullConsumer = createLitePullConsumerWithDefaultTraceTopic(); + try { + Set messageQueueSet = new HashSet<>(); + messageQueueSet.add(createMessageQueue()); + litePullConsumerImpl.updateTopicSubscribeInfo(topic, messageQueueSet); + litePullConsumer.setPollTimeoutMillis(20 * 1000); + List result = litePullConsumer.poll(); + assertThat(result.get(0).getTopic()).isEqualTo(topic); + assertThat(result.get(0).getBody()).isEqualTo(new byte[] {'a'}); + } finally { + litePullConsumer.shutdown(); + } + } + + @Test + public void testSubscribe_PollMessageSuccess_WithCustomizedTraceTopic() throws Exception { + DefaultLitePullConsumer litePullConsumer = createLitePullConsumerWithCustomizedTraceTopic(); + try { + Set messageQueueSet = new HashSet<>(); + messageQueueSet.add(createMessageQueue()); + litePullConsumerImpl.updateTopicSubscribeInfo(topic, messageQueueSet); + litePullConsumer.setPollTimeoutMillis(20 * 1000); + List result = litePullConsumer.poll(); + assertThat(result.get(0).getTopic()).isEqualTo(topic); + assertThat(result.get(0).getBody()).isEqualTo(new byte[] {'a'}); + } finally { + litePullConsumer.shutdown(); + } + } + + @Test + public void testLitePullConsumerWithTraceTLS() throws Exception { + DefaultLitePullConsumer consumer = new DefaultLitePullConsumer("consumerGroup"); + consumer.setUseTLS(true); + consumer.setEnableMsgTrace(true); + consumer.start(); + AsyncTraceDispatcher asyncTraceDispatcher = (AsyncTraceDispatcher) consumer.getTraceDispatcher(); + Assert.assertTrue(asyncTraceDispatcher.getTraceProducer().isUseTLS()); + } + + private DefaultLitePullConsumer createLitePullConsumerWithDefaultTraceTopic() throws Exception { + DefaultLitePullConsumer litePullConsumer = new DefaultLitePullConsumer(consumerGroup + System.currentTimeMillis()); + litePullConsumer.setEnableMsgTrace(true); + litePullConsumer.setNamesrvAddr("127.0.0.1:9876"); + litePullConsumer.subscribe(topic, "*"); + suppressUpdateTopicRouteInfoFromNameServer(litePullConsumer); + litePullConsumer.start(); + initDefaultLitePullConsumer(litePullConsumer); + return litePullConsumer; + } + + private DefaultLitePullConsumer createLitePullConsumerWithCustomizedTraceTopic() throws Exception { + DefaultLitePullConsumer litePullConsumer = new DefaultLitePullConsumer(consumerGroup + System.currentTimeMillis()); + litePullConsumer.setEnableMsgTrace(true); + litePullConsumer.setCustomizedTraceTopic(customerTraceTopic); + litePullConsumer.setNamesrvAddr("127.0.0.1:9876"); + litePullConsumer.subscribe(topic, "*"); + suppressUpdateTopicRouteInfoFromNameServer(litePullConsumer); + litePullConsumer.start(); + initDefaultLitePullConsumer(litePullConsumer); + return litePullConsumer; + } + + private void initDefaultLitePullConsumer(DefaultLitePullConsumer litePullConsumer) throws Exception { + asyncTraceDispatcher = (AsyncTraceDispatcher) litePullConsumer.getTraceDispatcher(); + traceProducer = asyncTraceDispatcher.getTraceProducer(); + Field field = DefaultLitePullConsumer.class.getDeclaredField("defaultLitePullConsumerImpl"); + field.setAccessible(true); + litePullConsumerImpl = (DefaultLitePullConsumerImpl) field.get(litePullConsumer); + field = DefaultLitePullConsumerImpl.class.getDeclaredField("mQClientFactory"); + field.setAccessible(true); + field.set(litePullConsumerImpl, mQClientFactory); + + PullAPIWrapper pullAPIWrapper = litePullConsumerImpl.getPullAPIWrapper(); + field = PullAPIWrapper.class.getDeclaredField("mQClientFactory"); + field.setAccessible(true); + field.set(pullAPIWrapper, mQClientFactory); + + Field fieldTrace = DefaultMQProducerImpl.class.getDeclaredField("mQClientFactory"); + fieldTrace.setAccessible(true); + fieldTrace.set(traceProducer.getDefaultMQProducerImpl(), mQClientFactory); + + field = MQClientInstance.class.getDeclaredField("mQClientAPIImpl"); + field.setAccessible(true); + field.set(mQClientFactory, mQClientAPIImpl); + + field = MQClientInstance.class.getDeclaredField("mQAdminImpl"); + field.setAccessible(true); + field.set(mQClientFactory, mQAdminImpl); + + field = DefaultLitePullConsumerImpl.class.getDeclaredField("rebalanceImpl"); + field.setAccessible(true); + rebalanceImpl = (RebalanceImpl) field.get(litePullConsumerImpl); + field = RebalanceImpl.class.getDeclaredField("mQClientFactory"); + field.setAccessible(true); + field.set(rebalanceImpl, mQClientFactory); + + offsetStore = spy(litePullConsumerImpl.getOffsetStore()); + field = DefaultLitePullConsumerImpl.class.getDeclaredField("offsetStore"); + field.setAccessible(true); + field.set(litePullConsumerImpl, offsetStore); + + traceProducer.getDefaultMQProducerImpl().getMqClientFactory().registerProducer(producerGroupTraceTemp, traceProducer.getDefaultMQProducerImpl()); + + when(mQClientFactory.getMQClientAPIImpl().pullMessage(anyString(), any(PullMessageRequestHeader.class), + anyLong(), any(CommunicationMode.class), nullable(PullCallback.class))) + .thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock mock) throws Throwable { + PullMessageRequestHeader requestHeader = mock.getArgument(1); + MessageClientExt messageClientExt = new MessageClientExt(); + messageClientExt.setTopic(topic); + messageClientExt.setQueueId(0); + messageClientExt.setMsgId("123"); + messageClientExt.setBody(new byte[] {'a'}); + messageClientExt.setOffsetMsgId("234"); + messageClientExt.setBornHost(new InetSocketAddress(8080)); + messageClientExt.setStoreHost(new InetSocketAddress(8080)); + PullResult pullResult = createPullResult(requestHeader, PullStatus.FOUND, Collections.singletonList(messageClientExt)); + return pullResult; + } + }); + + when(mQClientFactory.findBrokerAddressInSubscribe(anyString(), anyLong(), anyBoolean())).thenReturn(new FindBrokerResult("127.0.0.1:10911", false)); + + doReturn(Collections.singletonList(mQClientFactory.getClientId())).when(mQClientFactory).findConsumerIdList(anyString(), anyString()); + + doReturn(123L).when(offsetStore).readOffset(any(MessageQueue.class), any(ReadOffsetType.class)); + + } + + private PullResultExt createPullResult(PullMessageRequestHeader requestHeader, PullStatus pullStatus, + List messageExtList) throws Exception { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + for (MessageExt messageExt : messageExtList) { + outputStream.write(MessageDecoder.encode(messageExt, false)); + } + return new PullResultExt(pullStatus, requestHeader.getQueueOffset() + messageExtList.size(), 123, 2048, messageExtList, 0, outputStream.toByteArray()); + } + + private MessageQueue createMessageQueue() { + MessageQueue messageQueue = new MessageQueue(); + messageQueue.setBrokerName(brokerName); + messageQueue.setQueueId(0); + messageQueue.setTopic(topic); + return messageQueue; + } + + private TopicRouteData createTopicRoute() { + TopicRouteData topicRouteData = new TopicRouteData(); + + topicRouteData.setFilterServerTable(new HashMap<>()); + List brokerDataList = new ArrayList<>(); + BrokerData brokerData = new BrokerData(); + brokerData.setBrokerName("BrokerA"); + brokerData.setCluster("DefaultCluster"); + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(0L, "127.0.0.1:10911"); + brokerData.setBrokerAddrs(brokerAddrs); + brokerDataList.add(brokerData); + topicRouteData.setBrokerDatas(brokerDataList); + + List queueDataList = new ArrayList<>(); + QueueData queueData = new QueueData(); + queueData.setBrokerName("BrokerA"); + queueData.setPerm(6); + queueData.setReadQueueNums(3); + queueData.setWriteQueueNums(4); + queueData.setTopicSysFlag(0); + queueDataList.add(queueData); + topicRouteData.setQueueDatas(queueDataList); + return topicRouteData; + } + + private SendResult createSendResult(SendStatus sendStatus) { + SendResult sendResult = new SendResult(); + sendResult.setMsgId("123"); + sendResult.setOffsetMsgId("123"); + sendResult.setQueueOffset(456); + sendResult.setSendStatus(sendStatus); + sendResult.setRegionId("HZ"); + return sendResult; + } + + private static void suppressUpdateTopicRouteInfoFromNameServer(DefaultLitePullConsumer litePullConsumer) throws IllegalAccessException { + DefaultLitePullConsumerImpl defaultLitePullConsumerImpl = (DefaultLitePullConsumerImpl) FieldUtils.readDeclaredField(litePullConsumer, "defaultLitePullConsumerImpl", true); + if (litePullConsumer.getMessageModel() == MessageModel.CLUSTERING) { + litePullConsumer.changeInstanceNameToPID(); + } + MQClientInstance mQClientFactory = spy(MQClientManager.getInstance().getOrCreateMQClientInstance(litePullConsumer, (RPCHook) FieldUtils.readDeclaredField(defaultLitePullConsumerImpl, "rpcHook", true))); + ConcurrentMap factoryTable = (ConcurrentMap) FieldUtils.readDeclaredField(MQClientManager.getInstance(), "factoryTable", true); + factoryTable.put(litePullConsumer.buildMQClientId(), mQClientFactory); + doReturn(false).when(mQClientFactory).updateTopicRouteInfoFromNameServer(anyString()); + } + +} diff --git a/client/src/test/java/org/apache/rocketmq/client/trace/DefaultMQProducerWithOpenTracingTest.java b/client/src/test/java/org/apache/rocketmq/client/trace/DefaultMQProducerWithOpenTracingTest.java new file mode 100644 index 00000000000..8fbc70ea44f --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/trace/DefaultMQProducerWithOpenTracingTest.java @@ -0,0 +1,169 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client.trace; + +import io.opentracing.mock.MockSpan; +import io.opentracing.mock.MockTracer; +import io.opentracing.tag.Tags; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.hook.SendMessageContext; +import org.apache.rocketmq.client.impl.CommunicationMode; +import org.apache.rocketmq.client.impl.MQClientAPIImpl; +import org.apache.rocketmq.client.impl.MQClientManager; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl; +import org.apache.rocketmq.client.impl.producer.TopicPublishInfo; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendCallback; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.client.trace.hook.SendMessageOpenTracingHookImpl; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageType; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class DefaultMQProducerWithOpenTracingTest { + + @Spy + private MQClientInstance mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(new ClientConfig()); + @Mock + private MQClientAPIImpl mQClientAPIImpl; + + private DefaultMQProducer producer; + + private Message message; + private String topic = "FooBar"; + private String producerGroupPrefix = "FooBar_PID"; + private String producerGroupTemp = producerGroupPrefix + System.currentTimeMillis(); + private String producerGroupTraceTemp = TopicValidator.RMQ_SYS_TRACE_TOPIC + System.currentTimeMillis(); + private MockTracer tracer = new MockTracer(); + + @Before + public void init() throws Exception { + + producer = new DefaultMQProducer(producerGroupTemp); + producer.getDefaultMQProducerImpl().registerSendMessageHook( + new SendMessageOpenTracingHookImpl(tracer)); + producer.setNamesrvAddr("127.0.0.1:9876"); + message = new Message(topic, new byte[] {'a', 'b', 'c'}); + + producer.start(); + + Field field = DefaultMQProducerImpl.class.getDeclaredField("mQClientFactory"); + field.setAccessible(true); + field.set(producer.getDefaultMQProducerImpl(), mQClientFactory); + + field = MQClientInstance.class.getDeclaredField("mQClientAPIImpl"); + field.setAccessible(true); + field.set(mQClientFactory, mQClientAPIImpl); + + producer.getDefaultMQProducerImpl().getMqClientFactory().registerProducer(producerGroupTemp, producer.getDefaultMQProducerImpl()); + + when(mQClientAPIImpl.sendMessage(anyString(), anyString(), any(Message.class), any(SendMessageRequestHeader.class), anyLong(), any(CommunicationMode.class), + nullable(SendMessageContext.class), any(DefaultMQProducerImpl.class))).thenCallRealMethod(); + when(mQClientAPIImpl.sendMessage(anyString(), anyString(), any(Message.class), any(SendMessageRequestHeader.class), anyLong(), any(CommunicationMode.class), + nullable(SendCallback.class), nullable(TopicPublishInfo.class), nullable(MQClientInstance.class), anyInt(), nullable(SendMessageContext.class), any(DefaultMQProducerImpl.class))) + .thenReturn(createSendResult(SendStatus.SEND_OK)); + + } + + @Test + public void testSendMessageSync_WithTrace_Success() throws RemotingException, InterruptedException, MQBrokerException, MQClientException { + producer.getDefaultMQProducerImpl().getMqClientFactory().registerProducer(producerGroupTraceTemp, producer.getDefaultMQProducerImpl()); + when(mQClientAPIImpl.getTopicRouteInfoFromNameServer(anyString(), anyLong())).thenReturn(createTopicRoute()); + producer.send(message); + assertThat(tracer.finishedSpans().size()).isEqualTo(1); + MockSpan span = tracer.finishedSpans().get(0); + assertThat(span.tags().get(Tags.MESSAGE_BUS_DESTINATION.getKey())).isEqualTo(topic); + assertThat(span.tags().get(Tags.SPAN_KIND.getKey())).isEqualTo(Tags.SPAN_KIND_PRODUCER); + assertThat(span.tags().get(TraceConstants.ROCKETMQ_MSG_ID)).isEqualTo("123"); + assertThat(span.tags().get(TraceConstants.ROCKETMQ_BODY_LENGTH)).isEqualTo(3); + assertThat(span.tags().get(TraceConstants.ROCKETMQ_REGION_ID)).isEqualTo("HZ"); + assertThat(span.tags().get(TraceConstants.ROCKETMQ_MSG_TYPE)).isEqualTo(MessageType.Normal_Msg.name()); + assertThat(span.tags().get(TraceConstants.ROCKETMQ_SOTRE_HOST)).isEqualTo("127.0.0.1:10911"); + } + + @After + public void terminate() { + producer.shutdown(); + } + + public static TopicRouteData createTopicRoute() { + TopicRouteData topicRouteData = new TopicRouteData(); + + topicRouteData.setFilterServerTable(new HashMap<>()); + List brokerDataList = new ArrayList<>(); + BrokerData brokerData = new BrokerData(); + brokerData.setBrokerName("BrokerA"); + brokerData.setCluster("DefaultCluster"); + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(0L, "127.0.0.1:10911"); + brokerData.setBrokerAddrs(brokerAddrs); + brokerDataList.add(brokerData); + topicRouteData.setBrokerDatas(brokerDataList); + + List queueDataList = new ArrayList<>(); + QueueData queueData = new QueueData(); + queueData.setBrokerName("BrokerA"); + queueData.setPerm(6); + queueData.setReadQueueNums(3); + queueData.setWriteQueueNums(4); + queueData.setTopicSysFlag(0); + queueDataList.add(queueData); + topicRouteData.setQueueDatas(queueDataList); + return topicRouteData; + } + + private SendResult createSendResult(SendStatus sendStatus) { + SendResult sendResult = new SendResult(); + sendResult.setMsgId("123"); + sendResult.setOffsetMsgId("123"); + sendResult.setQueueOffset(456); + sendResult.setSendStatus(sendStatus); + sendResult.setRegionId("HZ"); + return sendResult; + } + +} diff --git a/client/src/test/java/org/apache/rocketmq/client/trace/DefaultMQProducerWithTraceTest.java b/client/src/test/java/org/apache/rocketmq/client/trace/DefaultMQProducerWithTraceTest.java new file mode 100644 index 00000000000..ee173351852 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/trace/DefaultMQProducerWithTraceTest.java @@ -0,0 +1,225 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client.trace; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.hook.SendMessageContext; +import org.apache.rocketmq.client.impl.CommunicationMode; +import org.apache.rocketmq.client.impl.MQClientAPIImpl; +import org.apache.rocketmq.client.impl.MQClientManager; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl; +import org.apache.rocketmq.client.impl.producer.TopicPublishInfo; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendCallback; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class DefaultMQProducerWithTraceTest { + + @Spy + private MQClientInstance mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(new ClientConfig()); + @Mock + private MQClientAPIImpl mQClientAPIImpl; + + private AsyncTraceDispatcher asyncTraceDispatcher; + + private DefaultMQProducer producer; + private DefaultMQProducer customTraceTopicproducer; + private DefaultMQProducer traceProducer; + private DefaultMQProducer normalProducer; + + private Message message; + private String topic = "FooBar"; + private String producerGroupPrefix = "FooBar_PID"; + private String producerGroupTemp = producerGroupPrefix + System.currentTimeMillis(); + private String producerGroupTraceTemp = TopicValidator.RMQ_SYS_TRACE_TOPIC + System.currentTimeMillis(); + private String customerTraceTopic = "rmq_trace_topic_12345"; + + @Before + public void init() throws Exception { + + customTraceTopicproducer = new DefaultMQProducer(producerGroupTemp, false, customerTraceTopic); + normalProducer = new DefaultMQProducer(producerGroupTemp, false, ""); + producer = new DefaultMQProducer(producerGroupTemp, true, ""); + producer.setNamesrvAddr("127.0.0.1:9876"); + normalProducer.setNamesrvAddr("127.0.0.1:9877"); + customTraceTopicproducer.setNamesrvAddr("127.0.0.1:9878"); + message = new Message(topic, new byte[] {'a', 'b', 'c'}); + asyncTraceDispatcher = (AsyncTraceDispatcher) producer.getTraceDispatcher(); + asyncTraceDispatcher.setTraceTopicName(customerTraceTopic); + asyncTraceDispatcher.getHostProducer(); + asyncTraceDispatcher.getHostConsumer(); + traceProducer = asyncTraceDispatcher.getTraceProducer(); + + producer.start(); + + Field field = DefaultMQProducerImpl.class.getDeclaredField("mQClientFactory"); + field.setAccessible(true); + field.set(producer.getDefaultMQProducerImpl(), mQClientFactory); + + Field fieldTrace = DefaultMQProducerImpl.class.getDeclaredField("mQClientFactory"); + fieldTrace.setAccessible(true); + fieldTrace.set(traceProducer.getDefaultMQProducerImpl(), mQClientFactory); + + field = MQClientInstance.class.getDeclaredField("mQClientAPIImpl"); + field.setAccessible(true); + field.set(mQClientFactory, mQClientAPIImpl); + + producer.getDefaultMQProducerImpl().getMqClientFactory().registerProducer(producerGroupTemp, producer.getDefaultMQProducerImpl()); + + when(mQClientAPIImpl.sendMessage(anyString(), anyString(), any(Message.class), any(SendMessageRequestHeader.class), anyLong(), any(CommunicationMode.class), + nullable(SendMessageContext.class), any(DefaultMQProducerImpl.class))).thenCallRealMethod(); + when(mQClientAPIImpl.sendMessage(anyString(), anyString(), any(Message.class), any(SendMessageRequestHeader.class), anyLong(), any(CommunicationMode.class), + nullable(SendCallback.class), nullable(TopicPublishInfo.class), nullable(MQClientInstance.class), anyInt(), nullable(SendMessageContext.class), any(DefaultMQProducerImpl.class))) + .thenReturn(createSendResult(SendStatus.SEND_OK)); + + } + + @Test + public void testSendMessageSync_WithTrace_Success() throws RemotingException, InterruptedException, MQBrokerException, MQClientException { + traceProducer.getDefaultMQProducerImpl().getMqClientFactory().registerProducer(producerGroupTraceTemp, traceProducer.getDefaultMQProducerImpl()); + when(mQClientAPIImpl.getTopicRouteInfoFromNameServer(anyString(), anyLong())).thenReturn(createTopicRoute()); + final CountDownLatch countDownLatch = new CountDownLatch(1); + try { + producer.send(message); + } catch (MQClientException e) { + } + countDownLatch.await(3000L, TimeUnit.MILLISECONDS); + + } + + @Test + public void testSendMessageSync_WithTrace_NoBrokerSet_Exception() throws RemotingException, InterruptedException, MQBrokerException, MQClientException { + when(mQClientAPIImpl.getTopicRouteInfoFromNameServer(anyString(), anyLong())).thenReturn(createTopicRoute()); + final CountDownLatch countDownLatch = new CountDownLatch(1); + try { + producer.send(message); + } catch (MQClientException e) { + } + countDownLatch.await(3000L, TimeUnit.MILLISECONDS); + + } + + + @Test + public void testProducerWithTraceTLS() { + DefaultMQProducer producer = new DefaultMQProducer(producerGroupTemp, true, null); + producer.setUseTLS(true); + AsyncTraceDispatcher asyncTraceDispatcher = (AsyncTraceDispatcher) producer.getTraceDispatcher(); + Assert.assertTrue(asyncTraceDispatcher.getTraceProducer().isUseTLS()); + } + + @After + public void terminate() { + producer.shutdown(); + } + + public static TopicRouteData createTopicRoute() { + TopicRouteData topicRouteData = new TopicRouteData(); + + topicRouteData.setFilterServerTable(new HashMap<>()); + List brokerDataList = new ArrayList<>(); + BrokerData brokerData = new BrokerData(); + brokerData.setBrokerName("BrokerA"); + brokerData.setCluster("DefaultCluster"); + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(0L, "127.0.0.1:10911"); + brokerData.setBrokerAddrs(brokerAddrs); + brokerDataList.add(brokerData); + topicRouteData.setBrokerDatas(brokerDataList); + + List queueDataList = new ArrayList<>(); + QueueData queueData = new QueueData(); + queueData.setBrokerName("BrokerA"); + queueData.setPerm(6); + queueData.setReadQueueNums(3); + queueData.setWriteQueueNums(4); + queueData.setTopicSysFlag(0); + queueDataList.add(queueData); + topicRouteData.setQueueDatas(queueDataList); + return topicRouteData; + } + + private SendResult createSendResult(SendStatus sendStatus) { + SendResult sendResult = new SendResult(); + sendResult.setMsgId("123"); + sendResult.setOffsetMsgId("123"); + sendResult.setQueueOffset(456); + sendResult.setSendStatus(sendStatus); + sendResult.setRegionId("HZ"); + return sendResult; + } + + public static TopicRouteData createTraceTopicRoute() { + TopicRouteData topicRouteData = new TopicRouteData(); + + topicRouteData.setFilterServerTable(new HashMap<>()); + List brokerDataList = new ArrayList<>(); + BrokerData brokerData = new BrokerData(); + brokerData.setBrokerName("broker-trace"); + brokerData.setCluster("DefaultCluster"); + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(0L, "127.0.0.1:10912"); + brokerData.setBrokerAddrs(brokerAddrs); + brokerDataList.add(brokerData); + topicRouteData.setBrokerDatas(brokerDataList); + + List queueDataList = new ArrayList<>(); + QueueData queueData = new QueueData(); + queueData.setBrokerName("broker-trace"); + queueData.setPerm(6); + queueData.setReadQueueNums(1); + queueData.setWriteQueueNums(1); + queueData.setTopicSysFlag(1); + queueDataList.add(queueData); + topicRouteData.setQueueDatas(queueDataList); + return topicRouteData; + } +} diff --git a/client/src/test/java/org/apache/rocketmq/client/trace/TraceDataEncoderTest.java b/client/src/test/java/org/apache/rocketmq/client/trace/TraceDataEncoderTest.java new file mode 100644 index 00000000000..26b7bda596b --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/trace/TraceDataEncoderTest.java @@ -0,0 +1,270 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client.trace; + +import org.apache.rocketmq.client.AccessChannel; +import org.apache.rocketmq.client.producer.LocalTransactionState; +import org.apache.rocketmq.common.message.MessageType; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TraceDataEncoderTest { + + private String traceData; + + private long time; + + @Before + public void init() { + time = System.currentTimeMillis(); + traceData = new StringBuilder() + .append("Pub").append(TraceConstants.CONTENT_SPLITOR) + .append(time).append(TraceConstants.CONTENT_SPLITOR) + .append("DefaultRegion").append(TraceConstants.CONTENT_SPLITOR) + .append("PID-test").append(TraceConstants.CONTENT_SPLITOR) + .append("topic-test").append(TraceConstants.CONTENT_SPLITOR) + .append("AC1415116D1418B4AAC217FE1B4E0000").append(TraceConstants.CONTENT_SPLITOR) + .append("Tags").append(TraceConstants.CONTENT_SPLITOR) + .append("Keys").append(TraceConstants.CONTENT_SPLITOR) + .append("127.0.0.1:10911").append(TraceConstants.CONTENT_SPLITOR) + .append(26).append(TraceConstants.CONTENT_SPLITOR) + .append(245).append(TraceConstants.CONTENT_SPLITOR) + .append(MessageType.Normal_Msg.ordinal()).append(TraceConstants.CONTENT_SPLITOR) + .append("0A9A002600002A9F0000000000002329").append(TraceConstants.CONTENT_SPLITOR) + .append(true).append(TraceConstants.FIELD_SPLITOR) + .toString(); + } + + @Test + public void testDecoderFromTraceDataString() { + List contexts = TraceDataEncoder.decoderFromTraceDataString(traceData); + Assert.assertEquals(contexts.size(), 1); + Assert.assertEquals(contexts.get(0).getTraceType(), TraceType.Pub); + } + + @Test + public void testEncoderFromContextBean() { + TraceContext context = new TraceContext(); + context.setTraceType(TraceType.Pub); + context.setGroupName("PID-test"); + context.setRegionId("DefaultRegion"); + context.setCostTime(245); + context.setSuccess(true); + context.setTimeStamp(time); + TraceBean traceBean = new TraceBean(); + traceBean.setTopic("topic-test"); + traceBean.setKeys("Keys"); + traceBean.setTags("Tags"); + traceBean.setMsgId("AC1415116D1418B4AAC217FE1B4E0000"); + traceBean.setOffsetMsgId("0A9A002600002A9F0000000000002329"); + traceBean.setStoreHost("127.0.0.1:10911"); + traceBean.setStoreTime(time); + traceBean.setMsgType(MessageType.Normal_Msg); + traceBean.setBodyLength(26); + List traceBeans = new ArrayList<>(); + traceBeans.add(traceBean); + context.setTraceBeans(traceBeans); + TraceTransferBean traceTransferBean = TraceDataEncoder.encoderFromContextBean(context); + + Assert.assertEquals(traceTransferBean.getTransData(), traceData); + Assert.assertEquals(traceTransferBean.getTransKey().size(), 2); + } + + @Test + public void testEncoderFromContextBean_EndTransaction() { + TraceContext context = new TraceContext(); + context.setTraceType(TraceType.EndTransaction); + context.setGroupName("PID-test"); + context.setRegionId("DefaultRegion"); + context.setTimeStamp(time); + TraceBean traceBean = new TraceBean(); + traceBean.setTopic("topic-test"); + traceBean.setKeys("Keys"); + traceBean.setTags("Tags"); + traceBean.setMsgId("AC1415116D1418B4AAC217FE1B4E0000"); + traceBean.setStoreHost("127.0.0.1:10911"); + traceBean.setMsgType(MessageType.Trans_msg_Commit); + traceBean.setTransactionId("transactionId"); + traceBean.setTransactionState(LocalTransactionState.COMMIT_MESSAGE); + traceBean.setFromTransactionCheck(false); + List traceBeans = new ArrayList<>(); + traceBeans.add(traceBean); + context.setTraceBeans(traceBeans); + TraceTransferBean traceTransferBean = TraceDataEncoder.encoderFromContextBean(context); + + Assert.assertEquals(traceTransferBean.getTransKey().size(), 2); + String traceData = traceTransferBean.getTransData(); + TraceContext contextAfter = TraceDataEncoder.decoderFromTraceDataString(traceData).get(0); + Assert.assertEquals(context.getTraceType(), contextAfter.getTraceType()); + Assert.assertEquals(context.getTimeStamp(), contextAfter.getTimeStamp()); + Assert.assertEquals(context.getGroupName(), contextAfter.getGroupName()); + TraceBean before = context.getTraceBeans().get(0); + TraceBean after = contextAfter.getTraceBeans().get(0); + Assert.assertEquals(before.getTopic(), after.getTopic()); + Assert.assertEquals(before.getMsgId(), after.getMsgId()); + Assert.assertEquals(before.getTags(), after.getTags()); + Assert.assertEquals(before.getKeys(), after.getKeys()); + Assert.assertEquals(before.getStoreHost(), after.getStoreHost()); + Assert.assertEquals(before.getMsgType(), after.getMsgType()); + Assert.assertEquals(before.getClientHost(), after.getClientHost()); + Assert.assertEquals(before.getTransactionId(), after.getTransactionId()); + Assert.assertEquals(before.getTransactionState(), after.getTransactionState()); + Assert.assertEquals(before.isFromTransactionCheck(), after.isFromTransactionCheck()); + } + + @Test + public void testPubTraceDataFormatTest() { + TraceContext pubContext = new TraceContext(); + pubContext.setTraceType(TraceType.Pub); + pubContext.setTimeStamp(time); + pubContext.setRegionId("Default-region"); + pubContext.setGroupName("GroupName-test"); + pubContext.setCostTime(34); + pubContext.setSuccess(true); + TraceBean bean = new TraceBean(); + bean.setTopic("topic-test"); + bean.setMsgId("AC1415116D1418B4AAC217FE1B4E0000"); + bean.setTags("tags"); + bean.setKeys("keys"); + bean.setStoreHost("127.0.0.1:10911"); + bean.setBodyLength(100); + bean.setMsgType(MessageType.Normal_Msg); + bean.setOffsetMsgId("AC1415116D1418B4AAC217FE1B4E0000"); + pubContext.setTraceBeans(new ArrayList<>(1)); + pubContext.getTraceBeans().add(bean); + + TraceTransferBean traceTransferBean = TraceDataEncoder.encoderFromContextBean(pubContext); + String transData = traceTransferBean.getTransData(); + Assert.assertNotNull(transData); + String[] items = transData.split(String.valueOf(TraceConstants.CONTENT_SPLITOR)); + Assert.assertEquals(14, items.length); + + } + + @Test + public void testSubBeforeTraceDataFormatTest() { + TraceContext subBeforeContext = new TraceContext(); + subBeforeContext.setTraceType(TraceType.SubBefore); + subBeforeContext.setTimeStamp(time); + subBeforeContext.setRegionId("Default-region"); + subBeforeContext.setGroupName("GroupName-test"); + subBeforeContext.setRequestId("3455848576927"); + TraceBean bean = new TraceBean(); + bean.setMsgId("AC1415116D1418B4AAC217FE1B4E0000"); + bean.setRetryTimes(0); + bean.setKeys("keys"); + subBeforeContext.setTraceBeans(new ArrayList<>(1)); + subBeforeContext.getTraceBeans().add(bean); + + TraceTransferBean traceTransferBean = TraceDataEncoder.encoderFromContextBean(subBeforeContext); + String transData = traceTransferBean.getTransData(); + Assert.assertNotNull(transData); + String[] items = transData.split(String.valueOf(TraceConstants.CONTENT_SPLITOR)); + Assert.assertEquals(8, items.length); + + } + + @Test + public void testSubAfterTraceDataFormatTest() { + TraceContext subAfterContext = new TraceContext(); + subAfterContext.setTraceType(TraceType.SubAfter); + subAfterContext.setRequestId("3455848576927"); + subAfterContext.setCostTime(20); + subAfterContext.setSuccess(true); + subAfterContext.setTimeStamp(1625883640000L); + subAfterContext.setGroupName("GroupName-test"); + subAfterContext.setContextCode(98623046); + subAfterContext.setAccessChannel(AccessChannel.LOCAL); + TraceBean bean = new TraceBean(); + bean.setMsgId("AC1415116D1418B4AAC217FE1B4E0000"); + bean.setKeys("keys"); + subAfterContext.setTraceBeans(new ArrayList<>(1)); + subAfterContext.getTraceBeans().add(bean); + + TraceTransferBean traceTransferBean = TraceDataEncoder.encoderFromContextBean(subAfterContext); + String transData = traceTransferBean.getTransData(); + Assert.assertNotNull(transData); + String[] items = transData.split(String.valueOf(TraceConstants.CONTENT_SPLITOR)); + Assert.assertEquals(9, items.length); + + } + + @Test + public void testEndTrxTraceDataFormatTest() { + TraceContext endTrxContext = new TraceContext(); + endTrxContext.setTraceType(TraceType.EndTransaction); + endTrxContext.setGroupName("PID-test"); + endTrxContext.setRegionId("DefaultRegion"); + endTrxContext.setTimeStamp(time); + TraceBean endTrxTraceBean = new TraceBean(); + endTrxTraceBean.setTopic("topic-test"); + endTrxTraceBean.setKeys("Keys"); + endTrxTraceBean.setTags("Tags"); + endTrxTraceBean.setMsgId("AC1415116D1418B4AAC217FE1B4E0000"); + endTrxTraceBean.setStoreHost("127.0.0.1:10911"); + endTrxTraceBean.setMsgType(MessageType.Trans_msg_Commit); + endTrxTraceBean.setTransactionId("transactionId"); + endTrxTraceBean.setTransactionState(LocalTransactionState.COMMIT_MESSAGE); + endTrxTraceBean.setFromTransactionCheck(false); + List traceBeans = new ArrayList<>(); + traceBeans.add(endTrxTraceBean); + endTrxContext.setTraceBeans(traceBeans); + + TraceTransferBean traceTransferBean = TraceDataEncoder.encoderFromContextBean(endTrxContext); + String transData = traceTransferBean.getTransData(); + Assert.assertNotNull(transData); + String[] items = transData.split(String.valueOf(TraceConstants.CONTENT_SPLITOR)); + Assert.assertEquals(13, items.length); + + } + + @Test + public void testTraceKeys() { + TraceContext endTrxContext = new TraceContext(); + endTrxContext.setTraceType(TraceType.EndTransaction); + endTrxContext.setGroupName("PID-test"); + endTrxContext.setRegionId("DefaultRegion"); + endTrxContext.setTimeStamp(time); + TraceBean endTrxTraceBean = new TraceBean(); + endTrxTraceBean.setTopic("topic-test"); + endTrxTraceBean.setKeys("Keys Keys2"); + endTrxTraceBean.setTags("Tags"); + endTrxTraceBean.setMsgId("AC1415116D1418B4AAC217FE1B4E0000"); + endTrxTraceBean.setStoreHost("127.0.0.1:10911"); + endTrxTraceBean.setMsgType(MessageType.Trans_msg_Commit); + endTrxTraceBean.setTransactionId("transactionId"); + endTrxTraceBean.setTransactionState(LocalTransactionState.COMMIT_MESSAGE); + endTrxTraceBean.setFromTransactionCheck(false); + List traceBeans = new ArrayList<>(); + traceBeans.add(endTrxTraceBean); + endTrxContext.setTraceBeans(traceBeans); + + TraceTransferBean traceTransferBean = TraceDataEncoder.encoderFromContextBean(endTrxContext); + + Set keys = traceTransferBean.getTransKey(); + assertThat(keys).contains("Keys"); + assertThat(keys).contains("Keys2"); + } +} diff --git a/client/src/test/java/org/apache/rocketmq/client/trace/TraceViewTest.java b/client/src/test/java/org/apache/rocketmq/client/trace/TraceViewTest.java new file mode 100644 index 00000000000..0397db256ab --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/trace/TraceViewTest.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client.trace; + +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageType; +import org.junit.Assert; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class TraceViewTest { + + @Test + public void testDecodeFromTraceTransData() { + String messageBody = new StringBuilder() + .append("Pub").append(TraceConstants.CONTENT_SPLITOR) + .append(System.currentTimeMillis()).append(TraceConstants.CONTENT_SPLITOR) + .append("DefaultRegion").append(TraceConstants.CONTENT_SPLITOR) + .append("PID-test").append(TraceConstants.CONTENT_SPLITOR) + .append("topic-test").append(TraceConstants.CONTENT_SPLITOR) + .append("AC1415116D1418B4AAC217FE1B4E0000").append(TraceConstants.CONTENT_SPLITOR) + .append("Tags").append(TraceConstants.CONTENT_SPLITOR) + .append("Keys").append(TraceConstants.CONTENT_SPLITOR) + .append("127.0.0.1:10911").append(TraceConstants.CONTENT_SPLITOR) + .append(26).append(TraceConstants.CONTENT_SPLITOR) + .append(245).append(TraceConstants.CONTENT_SPLITOR) + .append(MessageType.Normal_Msg.ordinal()).append(TraceConstants.CONTENT_SPLITOR) + .append("0A9A002600002A9F0000000000002329").append(TraceConstants.CONTENT_SPLITOR) + .append(true).append(TraceConstants.FIELD_SPLITOR) + .toString(); + MessageExt message = new MessageExt(); + message.setBody(messageBody.getBytes(StandardCharsets.UTF_8)); + String key = "AC1415116D1418B4AAC217FE1B4E0000"; + List traceViews = TraceView.decodeFromTraceTransData(key, message); + Assert.assertEquals(traceViews.size(), 1); + Assert.assertEquals(traceViews.get(0).getMsgId(), key); + + key = "AD4233434334AAC217FEFFD0000"; + traceViews = TraceView.decodeFromTraceTransData(key, message); + Assert.assertEquals(traceViews.size(), 0); + } +} diff --git a/client/src/test/java/org/apache/rocketmq/client/trace/TransactionMQProducerWithOpenTracingTest.java b/client/src/test/java/org/apache/rocketmq/client/trace/TransactionMQProducerWithOpenTracingTest.java new file mode 100644 index 00000000000..5646a17dbe6 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/trace/TransactionMQProducerWithOpenTracingTest.java @@ -0,0 +1,188 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client.trace; + +import io.opentracing.mock.MockSpan; +import io.opentracing.mock.MockTracer; +import io.opentracing.tag.Tags; +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.hook.SendMessageContext; +import org.apache.rocketmq.client.impl.CommunicationMode; +import org.apache.rocketmq.client.impl.MQClientAPIImpl; +import org.apache.rocketmq.client.impl.MQClientManager; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl; +import org.apache.rocketmq.client.impl.producer.TopicPublishInfo; +import org.apache.rocketmq.client.producer.LocalTransactionState; +import org.apache.rocketmq.client.producer.SendCallback; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.client.producer.TransactionListener; +import org.apache.rocketmq.client.producer.TransactionMQProducer; +import org.apache.rocketmq.client.trace.hook.EndTransactionOpenTracingHookImpl; +import org.apache.rocketmq.client.trace.hook.SendMessageOpenTracingHookImpl; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.message.MessageType; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class TransactionMQProducerWithOpenTracingTest { + + @Spy + private MQClientInstance mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(new ClientConfig()); + @Mock + private MQClientAPIImpl mQClientAPIImpl; + + private TransactionMQProducer producer; + + private Message message; + private String topic = "FooBar"; + private String producerGroupPrefix = "FooBar_PID"; + private String producerGroupTemp = producerGroupPrefix + System.currentTimeMillis(); + private String producerGroupTraceTemp = TopicValidator.RMQ_SYS_TRACE_TOPIC + System.currentTimeMillis(); + private MockTracer tracer = new MockTracer(); + @Before + public void init() throws Exception { + TransactionListener transactionListener = new TransactionListener() { + @Override + public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { + return LocalTransactionState.COMMIT_MESSAGE; + } + + @Override + public LocalTransactionState checkLocalTransaction(MessageExt msg) { + return LocalTransactionState.COMMIT_MESSAGE; + } + }; + producer = new TransactionMQProducer(producerGroupTemp); + producer.getDefaultMQProducerImpl().registerSendMessageHook(new SendMessageOpenTracingHookImpl(tracer)); + producer.getDefaultMQProducerImpl().registerEndTransactionHook(new EndTransactionOpenTracingHookImpl(tracer)); + producer.setTransactionListener(transactionListener); + + producer.setNamesrvAddr("127.0.0.1:9876"); + message = new Message(topic, new byte[] {'a', 'b', 'c'}); + + producer.start(); + + Field field = DefaultMQProducerImpl.class.getDeclaredField("mQClientFactory"); + field.setAccessible(true); + field.set(producer.getDefaultMQProducerImpl(), mQClientFactory); + + field = MQClientInstance.class.getDeclaredField("mQClientAPIImpl"); + field.setAccessible(true); + field.set(mQClientFactory, mQClientAPIImpl); + + producer.getDefaultMQProducerImpl().getMqClientFactory().registerProducer(producerGroupTemp, producer.getDefaultMQProducerImpl()); + + when(mQClientAPIImpl.sendMessage(anyString(), anyString(), any(Message.class), any(SendMessageRequestHeader.class), anyLong(), any(CommunicationMode.class), + nullable(SendMessageContext.class), any(DefaultMQProducerImpl.class))).thenCallRealMethod(); + when(mQClientAPIImpl.sendMessage(anyString(), anyString(), any(Message.class), any(SendMessageRequestHeader.class), anyLong(), any(CommunicationMode.class), + nullable(SendCallback.class), nullable(TopicPublishInfo.class), nullable(MQClientInstance.class), anyInt(), nullable(SendMessageContext.class), any(DefaultMQProducerImpl.class))) + .thenReturn(createSendResult(SendStatus.SEND_OK)); + + } + + @Test + public void testSendMessageSync_WithTrace_Success() throws RemotingException, InterruptedException, MQBrokerException, MQClientException { + producer.getDefaultMQProducerImpl().getMqClientFactory().registerProducer(producerGroupTraceTemp, producer.getDefaultMQProducerImpl()); + when(mQClientAPIImpl.getTopicRouteInfoFromNameServer(anyString(), anyLong())).thenReturn(createTopicRoute()); + producer.sendMessageInTransaction(message, null); + + assertThat(tracer.finishedSpans().size()).isEqualTo(2); + MockSpan span = tracer.finishedSpans().get(1); + assertThat(span.tags().get(Tags.MESSAGE_BUS_DESTINATION.getKey())).isEqualTo(topic); + assertThat(span.tags().get(Tags.SPAN_KIND.getKey())).isEqualTo(Tags.SPAN_KIND_PRODUCER); + assertThat(span.tags().get(TraceConstants.ROCKETMQ_MSG_ID)).isEqualTo("123"); + assertThat(span.tags().get(TraceConstants.ROCKETMQ_MSG_TYPE)).isEqualTo(MessageType.Trans_msg_Commit.name()); + assertThat(span.tags().get(TraceConstants.ROCKETMQ_TRANSACTION_STATE)).isEqualTo(LocalTransactionState.COMMIT_MESSAGE.name()); + assertThat(span.tags().get(TraceConstants.ROCKETMQ_IS_FROM_TRANSACTION_CHECK)).isEqualTo(false); + } + + @After + public void terminate() { + producer.shutdown(); + } + + public static TopicRouteData createTopicRoute() { + TopicRouteData topicRouteData = new TopicRouteData(); + + topicRouteData.setFilterServerTable(new HashMap<>()); + List brokerDataList = new ArrayList<>(); + BrokerData brokerData = new BrokerData(); + brokerData.setBrokerName("BrokerA"); + brokerData.setCluster("DefaultCluster"); + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(0L, "127.0.0.1:10911"); + brokerData.setBrokerAddrs(brokerAddrs); + brokerDataList.add(brokerData); + topicRouteData.setBrokerDatas(brokerDataList); + + List queueDataList = new ArrayList<>(); + QueueData queueData = new QueueData(); + queueData.setBrokerName("BrokerA"); + queueData.setPerm(6); + queueData.setReadQueueNums(3); + queueData.setWriteQueueNums(4); + queueData.setTopicSysFlag(0); + queueDataList.add(queueData); + topicRouteData.setQueueDatas(queueDataList); + return topicRouteData; + } + + private SendResult createSendResult(SendStatus sendStatus) { + SendResult sendResult = new SendResult(); + sendResult.setMsgId("123"); + sendResult.setOffsetMsgId(MessageDecoder.createMessageId(new InetSocketAddress("127.0.0.1", 12), 1)); + sendResult.setQueueOffset(456); + sendResult.setSendStatus(sendStatus); + sendResult.setRegionId("HZ"); + sendResult.setMessageQueue(new MessageQueue(topic, "broker-trace", 0)); + return sendResult; + } + +} diff --git a/client/src/test/java/org/apache/rocketmq/client/trace/TransactionMQProducerWithTraceTest.java b/client/src/test/java/org/apache/rocketmq/client/trace/TransactionMQProducerWithTraceTest.java new file mode 100644 index 00000000000..8cf87444c0c --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/trace/TransactionMQProducerWithTraceTest.java @@ -0,0 +1,212 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client.trace; + +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.hook.EndTransactionContext; +import org.apache.rocketmq.client.hook.EndTransactionHook; +import org.apache.rocketmq.client.hook.SendMessageContext; +import org.apache.rocketmq.client.impl.CommunicationMode; +import org.apache.rocketmq.client.impl.MQClientAPIImpl; +import org.apache.rocketmq.client.impl.MQClientManager; +import org.apache.rocketmq.client.impl.factory.MQClientInstance; +import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl; +import org.apache.rocketmq.client.impl.producer.TopicPublishInfo; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.LocalTransactionState; +import org.apache.rocketmq.client.producer.SendCallback; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.client.producer.TransactionListener; +import org.apache.rocketmq.client.producer.TransactionMQProducer; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class TransactionMQProducerWithTraceTest { + + @Spy + private MQClientInstance mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(new ClientConfig()); + @Mock + private MQClientAPIImpl mQClientAPIImpl; + @Mock + private EndTransactionHook endTransactionHook; + + private AsyncTraceDispatcher asyncTraceDispatcher; + + private TransactionMQProducer producer; + private DefaultMQProducer traceProducer; + + private Message message; + private String topic = "FooBar"; + private String producerGroupPrefix = "FooBar_PID"; + private String producerGroupTemp = producerGroupPrefix + System.currentTimeMillis(); + private String producerGroupTraceTemp = TopicValidator.RMQ_SYS_TRACE_TOPIC + System.currentTimeMillis(); + private String customerTraceTopic = "rmq_trace_topic_12345"; + + @Before + public void init() throws Exception { + TransactionListener transactionListener = new TransactionListener() { + @Override + public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { + return LocalTransactionState.COMMIT_MESSAGE; + } + + @Override + public LocalTransactionState checkLocalTransaction(MessageExt msg) { + return LocalTransactionState.COMMIT_MESSAGE; + } + }; + producer = new TransactionMQProducer(producerGroupTemp, null, true, null); + producer.setTransactionListener(transactionListener); + + producer.setNamesrvAddr("127.0.0.1:9876"); + message = new Message(topic, new byte[] {'a', 'b', 'c'}); + asyncTraceDispatcher = (AsyncTraceDispatcher) producer.getTraceDispatcher(); + traceProducer = asyncTraceDispatcher.getTraceProducer(); + + producer.start(); + + Field field = DefaultMQProducerImpl.class.getDeclaredField("mQClientFactory"); + field.setAccessible(true); + field.set(producer.getDefaultMQProducerImpl(), mQClientFactory); + + Field fieldTrace = DefaultMQProducerImpl.class.getDeclaredField("mQClientFactory"); + fieldTrace.setAccessible(true); + fieldTrace.set(traceProducer.getDefaultMQProducerImpl(), mQClientFactory); + + field = MQClientInstance.class.getDeclaredField("mQClientAPIImpl"); + field.setAccessible(true); + field.set(mQClientFactory, mQClientAPIImpl); + + producer.getDefaultMQProducerImpl().getMqClientFactory().registerProducer(producerGroupTemp, producer.getDefaultMQProducerImpl()); + + Field fieldHooks = DefaultMQProducerImpl.class.getDeclaredField("endTransactionHookList"); + fieldHooks.setAccessible(true); + List hooks = new ArrayList<>(); + hooks.add(endTransactionHook); + fieldHooks.set(producer.getDefaultMQProducerImpl(), hooks); + + when(mQClientAPIImpl.sendMessage(anyString(), anyString(), any(Message.class), any(SendMessageRequestHeader.class), anyLong(), any(CommunicationMode.class), + nullable(SendMessageContext.class), any(DefaultMQProducerImpl.class))).thenCallRealMethod(); + when(mQClientAPIImpl.sendMessage(anyString(), anyString(), any(Message.class), any(SendMessageRequestHeader.class), anyLong(), any(CommunicationMode.class), + nullable(SendCallback.class), nullable(TopicPublishInfo.class), nullable(MQClientInstance.class), anyInt(), nullable(SendMessageContext.class), any(DefaultMQProducerImpl.class))) + .thenReturn(createSendResult(SendStatus.SEND_OK)); + + } + + @Test + public void testSendMessageSync_WithTrace_Success() throws RemotingException, InterruptedException, MQBrokerException, MQClientException { + traceProducer.getDefaultMQProducerImpl().getMqClientFactory().registerProducer(producerGroupTraceTemp, traceProducer.getDefaultMQProducerImpl()); + when(mQClientAPIImpl.getTopicRouteInfoFromNameServer(anyString(), anyLong())).thenReturn(createTopicRoute()); + final AtomicReference context = new AtomicReference<>(); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock mock) throws Throwable { + context.set((EndTransactionContext) mock.getArgument(0)); + return null; + } + + }).when(endTransactionHook).endTransaction(any(EndTransactionContext.class)); + producer.sendMessageInTransaction(message, null); + + EndTransactionContext ctx = context.get(); + assertThat(ctx.getProducerGroup()).isEqualTo(producerGroupTemp); + assertThat(ctx.getMsgId()).isEqualTo("123"); + assertThat(ctx.isFromTransactionCheck()).isFalse(); + assertThat(new String(ctx.getMessage().getBody())).isEqualTo(new String(message.getBody())); + assertThat(ctx.getMessage().getTopic()).isEqualTo(topic); + } + + @After + public void terminate() { + producer.shutdown(); + } + + public static TopicRouteData createTopicRoute() { + TopicRouteData topicRouteData = new TopicRouteData(); + + topicRouteData.setFilterServerTable(new HashMap<>()); + List brokerDataList = new ArrayList<>(); + BrokerData brokerData = new BrokerData(); + brokerData.setBrokerName("BrokerA"); + brokerData.setCluster("DefaultCluster"); + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(0L, "127.0.0.1:10911"); + brokerData.setBrokerAddrs(brokerAddrs); + brokerDataList.add(brokerData); + topicRouteData.setBrokerDatas(brokerDataList); + + List queueDataList = new ArrayList<>(); + QueueData queueData = new QueueData(); + queueData.setBrokerName("BrokerA"); + queueData.setPerm(6); + queueData.setReadQueueNums(3); + queueData.setWriteQueueNums(4); + queueData.setTopicSysFlag(0); + queueDataList.add(queueData); + topicRouteData.setQueueDatas(queueDataList); + return topicRouteData; + } + + private SendResult createSendResult(SendStatus sendStatus) { + SendResult sendResult = new SendResult(); + sendResult.setMsgId("123"); + sendResult.setOffsetMsgId(MessageDecoder.createMessageId(new InetSocketAddress("127.0.0.1", 12), 1)); + sendResult.setQueueOffset(456); + sendResult.setSendStatus(sendStatus); + sendResult.setRegionId("HZ"); + sendResult.setMessageQueue(new MessageQueue(topic, "broker-trace", 0)); + return sendResult; + } + +} diff --git a/client/src/test/java/org/apache/rocketmq/client/utils/MessageUtilsTest.java b/client/src/test/java/org/apache/rocketmq/client/utils/MessageUtilsTest.java new file mode 100644 index 00000000000..803e596fc82 --- /dev/null +++ b/client/src/test/java/org/apache/rocketmq/client/utils/MessageUtilsTest.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.client.utils; + +import java.util.HashMap; +import java.util.Map; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageConst; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Fail.failBecauseExceptionWasNotThrown; + +public class MessageUtilsTest { + + @Test + public void testCreateReplyMessage() throws MQClientException { + Message msg = MessageUtil.createReplyMessage(createReplyMessage("clusterName"), new byte[] {'a'}); + assertThat(msg.getTopic()).isEqualTo("clusterName" + "_" + MixAll.REPLY_TOPIC_POSTFIX); + assertThat(msg.getProperty(MessageConst.PROPERTY_MESSAGE_REPLY_TO_CLIENT)).isEqualTo("127.0.0.1"); + assertThat(msg.getProperty(MessageConst.PROPERTY_MESSAGE_TTL)).isEqualTo("3000"); + } + + @Test + public void testCreateReplyMessage_Exception() throws MQClientException { + try { + Message msg = MessageUtil.createReplyMessage(createReplyMessage(null), new byte[] {'a'}); + failBecauseExceptionWasNotThrown(MQClientException.class); + } catch (MQClientException e) { + assertThat(e).hasMessageContaining("create reply message fail, requestMessage error, property[" + MessageConst.PROPERTY_CLUSTER + "] is null."); + } + } + + @Test + public void testCreateReplyMessage_reqMsgIsNull() throws MQClientException { + try { + Message msg = MessageUtil.createReplyMessage(null, new byte[] {'a'}); + failBecauseExceptionWasNotThrown(MQClientException.class); + } catch (MQClientException e) { + assertThat(e).hasMessageContaining("create reply message fail, requestMessage cannot be null."); + } + } + + @Test + public void testGetReplyToClient() throws MQClientException { + Message msg = createReplyMessage("clusterName"); + String replyToClient = MessageUtil.getReplyToClient(msg); + assertThat(replyToClient).isNotNull(); + assertThat(replyToClient).isEqualTo("127.0.0.1"); + } + + private Message createReplyMessage(String clusterName) { + Message requestMessage = new Message(); + Map map = new HashMap(); + map.put(MessageConst.PROPERTY_MESSAGE_REPLY_TO_CLIENT, "127.0.0.1"); + map.put(MessageConst.PROPERTY_CLUSTER, clusterName); + map.put(MessageConst.PROPERTY_MESSAGE_TTL, "3000"); + MessageAccessor.setProperties(requestMessage, map); + return requestMessage; + } + +} diff --git a/client/src/test/resources/org/powermock/extensions/configuration.properties b/client/src/test/resources/org/powermock/extensions/configuration.properties new file mode 100644 index 00000000000..6389eff4af9 --- /dev/null +++ b/client/src/test/resources/org/powermock/extensions/configuration.properties @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +powermock.global-ignore=javax.management.* \ No newline at end of file diff --git a/client/src/test/resources/rmq.logback-test.xml b/client/src/test/resources/rmq.logback-test.xml new file mode 100644 index 00000000000..8695d52d57c --- /dev/null +++ b/client/src/test/resources/rmq.logback-test.xml @@ -0,0 +1,36 @@ + + + + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + \ No newline at end of file diff --git a/common/BUILD.bazel b/common/BUILD.bazel new file mode 100644 index 00000000000..9a0c31e772f --- /dev/null +++ b/common/BUILD.bazel @@ -0,0 +1,75 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +load("//bazel:GenTestRules.bzl", "GenTestRules") + +java_library( + name = "common", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "@maven//:com_alibaba_fastjson", + "@maven//:com_github_luben_zstd_jni", + "@maven//:com_google_guava_guava", + "@maven//:commons_collections_commons_collections", + "@maven//:commons_codec_commons_codec", + "@maven//:commons_validator_commons_validator", + "@maven//:io_netty_netty_all", + "@maven//:io_opentelemetry_opentelemetry_api", + "@maven//:io_opentelemetry_opentelemetry_context", + "@maven//:io_opentelemetry_opentelemetry_exporter_otlp", + "@maven//:io_opentelemetry_opentelemetry_exporter_prometheus", + "@maven//:io_opentelemetry_opentelemetry_sdk", + "@maven//:io_opentelemetry_opentelemetry_sdk_common", + "@maven//:io_opentelemetry_opentelemetry_sdk_metrics", + "@maven//:io_opentelemetry_opentelemetry_exporter_logging_otlp", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:org_lz4_lz4_java", + "@maven//:io_github_aliyunmq_rocketmq_slf4j_api", + "@maven//:io_github_aliyunmq_rocketmq_logback_classic", + "@maven//:org_apache_rocketmq_rocketmq_rocksdb", + ], +) + +java_library( + name = "tests", + srcs = glob(["src/test/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + ":common", + "//:test_deps", + "@maven//:com_google_guava_guava", + "@maven//:com_alibaba_fastjson", + "@maven//:io_netty_netty_all", + "@maven//:io_opentelemetry_opentelemetry_api", + "@maven//:io_opentelemetry_opentelemetry_context", + "@maven//:io_opentelemetry_opentelemetry_exporter_otlp", + "@maven//:io_opentelemetry_opentelemetry_exporter_prometheus", + "@maven//:io_opentelemetry_opentelemetry_sdk", + "@maven//:io_opentelemetry_opentelemetry_sdk_common", + "@maven//:io_opentelemetry_opentelemetry_sdk_metrics", + "@maven//:org_apache_commons_commons_lang3", + ], + resources = glob(["src/test/resources/certs/*.pem"]) + glob(["src/test/resources/certs/*.key"]) +) + +GenTestRules( + name = "GeneratedTestRules", + test_files = glob(["src/test/java/**/*Test.java"]), + deps = [ + ":tests", + ], +) diff --git a/common/pom.xml b/common/pom.xml index 05c2e221fe5..df4a539da5e 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -19,7 +19,7 @@ org.apache.rocketmq rocketmq-all - 4.2.0 + 5.2.0 4.0.0 @@ -28,14 +28,89 @@ rocketmq-common ${project.version} - 1.6 - 1.6 + ${basedir}/.. - ${project.groupId} - rocketmq-remoting + com.alibaba + fastjson + + + io.netty + netty-all + + + org.apache.commons + commons-lang3 + + + commons-validator + commons-validator + + + com.github.luben + zstd-jni + + + org.lz4 + lz4-java + + + com.google.guava + guava + + + commons-codec + commons-codec + + + io.opentelemetry + opentelemetry-exporter-otlp + + + io.opentelemetry + opentelemetry-exporter-prometheus + + + io.opentelemetry + opentelemetry-exporter-logging + + + io.opentelemetry + opentelemetry-sdk + + + io.opentelemetry + opentelemetry-exporter-logging-otlp + + + io.grpc + grpc-stub + + + io.grpc + grpc-netty-shaded + + + com.squareup.okio + okio-jvm + + + org.apache.tomcat + annotations-api + + + io.github.aliyunmq + rocketmq-slf4j-api + + + io.github.aliyunmq + rocketmq-logback-classic + + + org.apache.rocketmq + rocketmq-rocksdb diff --git a/common/src/main/java/org/apache/rocketmq/common/AbortProcessException.java b/common/src/main/java/org/apache/rocketmq/common/AbortProcessException.java new file mode 100644 index 00000000000..562fdaac63c --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/AbortProcessException.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common; + +import org.apache.rocketmq.common.help.FAQUrl; + +/** + * + * This exception is used for broker hooks only : SendMessageHook, ConsumeMessageHook, RPCHook + * This exception is not ignored while executing hooks and it means that + * certain processor should return an immediate error response to the client. The + * error response code is included in AbortProcessException. it's naming might + * be confusing, so feel free to refactor this class. Also when any class implements + * the 3 hook interface mentioned above we should be careful if we want to throw + * an AbortProcessException, because it will change the control flow of broker + * and cause a RemotingCommand return error immediately. So be aware of the side + * effect before throw AbortProcessException in your implementation. + * + */ +public class AbortProcessException extends RuntimeException { + private static final long serialVersionUID = -5728810933841185841L; + private int responseCode; + private String errorMessage; + + public AbortProcessException(String errorMessage, Throwable cause) { + super(FAQUrl.attachDefaultURL(errorMessage), cause); + this.responseCode = -1; + this.errorMessage = errorMessage; + } + + public AbortProcessException(int responseCode, String errorMessage) { + super(FAQUrl.attachDefaultURL("CODE: " + UtilAll.responseCode2String(responseCode) + " DESC: " + + errorMessage)); + this.responseCode = responseCode; + this.errorMessage = errorMessage; + } + + public int getResponseCode() { + return responseCode; + } + + public AbortProcessException setResponseCode(final int responseCode) { + this.responseCode = responseCode; + return this; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(final String errorMessage) { + this.errorMessage = errorMessage; + } +} \ No newline at end of file diff --git a/common/src/main/java/org/apache/rocketmq/common/AbstractBrokerRunnable.java b/common/src/main/java/org/apache/rocketmq/common/AbstractBrokerRunnable.java new file mode 100644 index 00000000000..34aabc5772c --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/AbstractBrokerRunnable.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common; + +import java.io.File; +import org.apache.rocketmq.logging.org.slf4j.MDC; + +public abstract class AbstractBrokerRunnable implements Runnable { + protected final BrokerIdentity brokerIdentity; + + public AbstractBrokerRunnable(BrokerIdentity brokerIdentity) { + this.brokerIdentity = brokerIdentity; + } + + private static final String MDC_BROKER_CONTAINER_LOG_DIR = "brokerContainerLogDir"; + + /** + * real logic for running + */ + public abstract void run0(); + + @Override + public void run() { + try { + if (brokerIdentity.isInBrokerContainer()) { + MDC.put(MDC_BROKER_CONTAINER_LOG_DIR, File.separator + brokerIdentity.getCanonicalName()); + } + run0(); + } finally { + MDC.clear(); + } + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/AclConfig.java b/common/src/main/java/org/apache/rocketmq/common/AclConfig.java new file mode 100644 index 00000000000..49b9e05e2e1 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/AclConfig.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common; + +import java.util.List; + +public class AclConfig { + + private List globalWhiteAddrs; + + private List plainAccessConfigs; + + + public List getGlobalWhiteAddrs() { + return globalWhiteAddrs; + } + + public void setGlobalWhiteAddrs(List globalWhiteAddrs) { + this.globalWhiteAddrs = globalWhiteAddrs; + } + + public List getPlainAccessConfigs() { + return plainAccessConfigs; + } + + public void setPlainAccessConfigs(List plainAccessConfigs) { + this.plainAccessConfigs = plainAccessConfigs; + } + + @Override + public String toString() { + return "AclConfig{" + + "globalWhiteAddrs=" + globalWhiteAddrs + + ", plainAccessConfigs=" + plainAccessConfigs + + '}'; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/BoundaryType.java b/common/src/main/java/org/apache/rocketmq/common/BoundaryType.java new file mode 100644 index 00000000000..03a01710fa4 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/BoundaryType.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common; + +public enum BoundaryType { + /** + * Indicate that lower boundary is expected. + */ + LOWER("lower"), + + /** + * Indicate that upper boundary is expected. + */ + UPPER("upper"); + + private String name; + + BoundaryType(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public static BoundaryType getType(String name) { + if (BoundaryType.UPPER.getName().equalsIgnoreCase(name)) { + return UPPER; + } + return LOWER; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/BrokerConfig.java b/common/src/main/java/org/apache/rocketmq/common/BrokerConfig.java index efb36b50a7e..d859f965e40 100644 --- a/common/src/main/java/org/apache/rocketmq/common/BrokerConfig.java +++ b/common/src/main/java/org/apache/rocketmq/common/BrokerConfig.java @@ -17,30 +17,35 @@ package org.apache.rocketmq.common; import org.apache.rocketmq.common.annotation.ImportantField; -import org.apache.rocketmq.common.constant.LoggerName; import org.apache.rocketmq.common.constant.PermName; -import org.apache.rocketmq.remoting.common.RemotingUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.common.message.MessageRequestMode; +import org.apache.rocketmq.common.metrics.MetricsExporterType; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.common.utils.NetworkUtil; -import java.net.InetAddress; -import java.net.UnknownHostException; +import java.util.concurrent.TimeUnit; -public class BrokerConfig { - private static final Logger log = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); +public class BrokerConfig extends BrokerIdentity { + + private String brokerConfigPath = null; private String rocketmqHome = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, System.getenv(MixAll.ROCKETMQ_HOME_ENV)); @ImportantField private String namesrvAddr = System.getProperty(MixAll.NAMESRV_ADDR_PROPERTY, System.getenv(MixAll.NAMESRV_ADDR_ENV)); + + /** + * Listen port for single broker + */ @ImportantField - private String brokerIP1 = RemotingUtil.getLocalAddress(); - private String brokerIP2 = RemotingUtil.getLocalAddress(); - @ImportantField - private String brokerName = localHostName(); + private int listenPort = 6888; + @ImportantField - private String brokerClusterName = "DefaultCluster"; + private String brokerIP1 = NetworkUtil.getLocalAddress(); + private String brokerIP2 = NetworkUtil.getLocalAddress(); + @ImportantField - private long brokerId = MixAll.MASTER_ID; + private boolean recoverConcurrently = false; + private int brokerPermission = PermName.PERM_READ | PermName.PERM_WRITE; private int defaultTopicQueueNums = 8; @ImportantField @@ -53,17 +58,34 @@ public class BrokerConfig { private boolean autoCreateSubscriptionGroup = true; private String messageStorePlugIn = ""; + private static final int PROCESSOR_NUMBER = Runtime.getRuntime().availableProcessors(); + @ImportantField + private String msgTraceTopicName = TopicValidator.RMQ_SYS_TRACE_TOPIC; + @ImportantField + private boolean traceTopicEnable = false; /** - * thread numbers for send message thread pool, since spin lock will be used by default since 4.0.x, the default - * value is 1. + * thread numbers for send message thread pool. */ - private int sendMessageThreadPoolNums = 1; //16 + Runtime.getRuntime().availableProcessors() * 4; - private int pullMessageThreadPoolNums = 16 + Runtime.getRuntime().availableProcessors() * 2; - private int queryMessageThreadPoolNums = 8 + Runtime.getRuntime().availableProcessors(); + private int sendMessageThreadPoolNums = Math.min(PROCESSOR_NUMBER, 4); + private int putMessageFutureThreadPoolNums = Math.min(PROCESSOR_NUMBER, 4); + private int pullMessageThreadPoolNums = 16 + PROCESSOR_NUMBER * 2; + private int litePullMessageThreadPoolNums = 16 + PROCESSOR_NUMBER * 2; + private int ackMessageThreadPoolNums = 3; + private int processReplyMessageThreadPoolNums = 16 + PROCESSOR_NUMBER * 2; + private int queryMessageThreadPoolNums = 8 + PROCESSOR_NUMBER; private int adminBrokerThreadPoolNums = 16; private int clientManageThreadPoolNums = 32; private int consumerManageThreadPoolNums = 32; + private int loadBalanceProcessorThreadPoolNums = 32; + private int heartbeatThreadPoolNums = Math.min(32, PROCESSOR_NUMBER); + private int recoverThreadPoolNums = 32; + + /** + * Thread numbers for EndTransactionProcessor + */ + private int endTransactionThreadPoolNums = Math.max(8 + PROCESSOR_NUMBER * 2, + sendMessageThreadPoolNums * 4); private int flushConsumerOffsetInterval = 1000 * 5; @@ -71,15 +93,26 @@ public class BrokerConfig { @ImportantField private boolean rejectTransactionMessage = false; + + @ImportantField + private boolean fetchNameSrvAddrByDnsLookup = false; + @ImportantField private boolean fetchNamesrvAddrByAddressServer = false; + private int sendThreadPoolQueueCapacity = 10000; + private int putThreadPoolQueueCapacity = 10000; private int pullThreadPoolQueueCapacity = 100000; + private int litePullThreadPoolQueueCapacity = 100000; + private int ackThreadPoolQueueCapacity = 100000; + private int replyThreadPoolQueueCapacity = 10000; private int queryThreadPoolQueueCapacity = 20000; private int clientManagerThreadPoolQueueCapacity = 1000000; private int consumerManagerThreadPoolQueueCapacity = 1000000; - - private int filterServerNums = 0; + private int heartbeatThreadPoolQueueCapacity = 50000; + private int endTransactionPoolQueueCapacity = 100000; + private int adminBrokerThreadPoolQueueCapacity = 10000; + private int loadBalanceThreadPoolQueueCapacity = 100000; private boolean longPollingEnable = true; @@ -89,17 +122,19 @@ public class BrokerConfig { private boolean highSpeedMode = false; - private boolean commercialEnable = true; - private int commercialTimerCount = 1; - private int commercialTransCount = 1; - private int commercialBigCount = 1; private int commercialBaseCount = 1; + private int commercialSizePerMsg = 4 * 1024; + + private boolean accountStatsEnable = true; + private boolean accountStatsPrintZeroValues = true; + private boolean transferMsgByHeap = true; - private int maxDelayTime = 40; private String regionId = MixAll.DEFAULT_TRACE_REGION_ID; - private int registerBrokerTimeoutMills = 6000; + private int registerBrokerTimeoutMills = 24000; + + private int sendHeartbeatTimeoutMillis = 1000; private boolean slaveReadEnable = false; @@ -109,6 +144,10 @@ public class BrokerConfig { private boolean brokerFastFailureEnable = true; private long waitTimeMillsInSendQueue = 200; private long waitTimeMillsInPullQueue = 5 * 1000; + private long waitTimeMillsInLitePullQueue = 5 * 1000; + private long waitTimeMillsInHeartbeatQueue = 31 * 1000; + private long waitTimeMillsInTransactionQueue = 3 * 1000; + private long waitTimeMillsInAckQueue = 3000; private long startAcceptSendRequestTimeStamp = 0L; @@ -120,6 +159,9 @@ public class BrokerConfig { // 2. Filter bit map will be saved to consume queue extend file if allowed. private boolean enableCalcFilterBitMap = false; + //Reject the pull consumer instance to pull messages from broker. + private boolean rejectPullConsumerEnable = false; + // Expect num of consumers will use filter. private int expectConsumerNumUseFilter = 32; @@ -133,6 +175,386 @@ public class BrokerConfig { private boolean filterSupportRetry = false; private boolean enablePropertyFilter = false; + private boolean compressedRegister = false; + + private boolean forceRegister = true; + + /** + * This configurable item defines interval of topics registration of broker to name server. Allowing values are + * between 10,000 and 60,000 milliseconds. + */ + private int registerNameServerPeriod = 1000 * 30; + + /** + * the interval to send heartbeat to name server for liveness detection. + */ + private int brokerHeartbeatInterval = 1000; + + /** + * How long the broker will be considered as inactive by nameserver since last heartbeat. Effective only if + * enableSlaveActingMaster is true + */ + private long brokerNotActiveTimeoutMillis = 10 * 1000; + + private boolean enableNetWorkFlowControl = false; + + private boolean enableBroadcastOffsetStore = true; + + private long broadcastOffsetExpireSecond = 2 * 60; + + private long broadcastOffsetExpireMaxSecond = 5 * 60; + + private int popPollingSize = 1024; + private int popPollingMapSize = 100000; + // 20w cost 200M heap memory. + private long maxPopPollingSize = 100000; + private int reviveQueueNum = 8; + private long reviveInterval = 1000; + private long reviveMaxSlow = 3; + private long reviveScanTime = 10000; + private boolean enableSkipLongAwaitingAck = false; + private long reviveAckWaitMs = TimeUnit.MINUTES.toMillis(3); + private boolean enablePopLog = false; + private boolean enablePopBufferMerge = false; + private int popCkStayBufferTime = 10 * 1000; + private int popCkStayBufferTimeOut = 3 * 1000; + private int popCkMaxBufferSize = 200000; + private int popCkOffsetMaxQueueSize = 20000; + private boolean enablePopBatchAck = false; + private boolean enableNotifyAfterPopOrderLockRelease = true; + private boolean initPopOffsetByCheckMsgInMem = true; + // read message from pop retry topic v1, for the compatibility, will be removed in the future version + private boolean retrieveMessageFromPopRetryTopicV1 = true; + private boolean enableRetryTopicV2 = false; + + private boolean realTimeNotifyConsumerChange = true; + + private boolean litePullMessageEnable = true; + + // The period to sync broker member group from namesrv, default value is 1 second + private int syncBrokerMemberGroupPeriod = 1000; + + /** + * the interval of pulling topic information from the named server + */ + private long loadBalancePollNameServerInterval = 1000 * 30; + + /** + * the interval of cleaning + */ + private int cleanOfflineBrokerInterval = 1000 * 30; + + private boolean serverLoadBalancerEnable = true; + + private MessageRequestMode defaultMessageRequestMode = MessageRequestMode.PULL; + + private int defaultPopShareQueueNum = -1; + + /** + * The minimum time of the transactional message to be checked firstly, one message only exceed this time interval + * that can be checked. + */ + @ImportantField + private long transactionTimeOut = 6 * 1000; + + /** + * The maximum number of times the message was checked, if exceed this value, this message will be discarded. + */ + @ImportantField + private int transactionCheckMax = 15; + + /** + * Transaction message check interval. + */ + @ImportantField + private long transactionCheckInterval = 30 * 1000; + + private long transactionMetricFlushInterval = 3 * 1000; + + /** + * transaction batch op message + */ + private int transactionOpMsgMaxSize = 4096; + + private int transactionOpBatchInterval = 3000; + + /** + * Acl feature switch + */ + @ImportantField + private boolean aclEnable = false; + + private boolean storeReplyMessageEnable = true; + + private boolean enableDetailStat = true; + + private boolean autoDeleteUnusedStats = false; + + /** + * Whether to distinguish log paths when multiple brokers are deployed on the same machine + */ + private boolean isolateLogEnable = false; + + private long forwardTimeout = 3 * 1000; + + /** + * Slave will act master when failover. For example, if master down, timer or transaction message which is expire in slave will + * put to master (master of the same process in broker container mode or other masters in cluster when enableFailoverRemotingActing is true) + * when enableSlaveActingMaster is true + */ + private boolean enableSlaveActingMaster = false; + + private boolean enableRemoteEscape = false; + + private boolean skipPreOnline = false; + + private boolean asyncSendEnable = true; + + private boolean useServerSideResetOffset = true; + + private long consumerOffsetUpdateVersionStep = 500; + + private long delayOffsetUpdateVersionStep = 200; + + /** + * Whether to lock quorum replicas. + * + * True: need to lock quorum replicas succeed. False: only need to lock one replica succeed. + */ + private boolean lockInStrictMode = false; + + private boolean compatibleWithOldNameSrv = true; + + /** + * Is startup controller mode, which support auto switch broker's role. + */ + private boolean enableControllerMode = false; + + private String controllerAddr = ""; + + private boolean fetchControllerAddrByDnsLookup = false; + + private long syncBrokerMetadataPeriod = 5 * 1000; + + private long checkSyncStateSetPeriod = 5 * 1000; + + private long syncControllerMetadataPeriod = 10 * 1000; + + private long controllerHeartBeatTimeoutMills = 10 * 1000; + + private boolean validateSystemTopicWhenUpdateTopic = true; + + /** + * It is an important basis for the controller to choose the broker master. + * The lower the value of brokerElectionPriority, the higher the priority of the broker being selected as the master. + * You can set a lower priority for the broker with better machine conditions. + */ + private int brokerElectionPriority = Integer.MAX_VALUE; + + private boolean useStaticSubscription = false; + + private MetricsExporterType metricsExporterType = MetricsExporterType.DISABLE; + + private int metricsOtelCardinalityLimit = 50 * 1000; + private String metricsGrpcExporterTarget = ""; + private String metricsGrpcExporterHeader = ""; + private long metricGrpcExporterTimeOutInMills = 3 * 1000; + private long metricGrpcExporterIntervalInMills = 60 * 1000; + private long metricLoggingExporterIntervalInMills = 10 * 1000; + + private int metricsPromExporterPort = 5557; + private String metricsPromExporterHost = ""; + + // Label pairs in CSV. Each label follows pattern of Key:Value. eg: instance_id:xxx,uid:xxx + private String metricsLabel = ""; + + private boolean metricsInDelta = false; + + private long channelExpiredTimeout = 1000 * 120; + private long subscriptionExpiredTimeout = 1000 * 60 * 10; + + /** + * Estimate accumulation or not when subscription filter type is tag and is not SUB_ALL. + */ + private boolean estimateAccumulation = true; + + private boolean coldCtrStrategyEnable = false; + private boolean usePIDColdCtrStrategy = true; + private long cgColdReadThreshold = 3 * 1024 * 1024; + private long globalColdReadThreshold = 100 * 1024 * 1024; + + /** + * The interval to fetch namesrv addr, default value is 10 second + */ + private long fetchNamesrvAddrInterval = 10 * 1000; + + /** + * Pop response returns the actual retry topic rather than tampering with the original topic + */ + private boolean popResponseReturnActualRetryTopic = false; + + /** + * If both the deleteTopicWithBrokerRegistration flag in the NameServer configuration and this flag are set to true, + * it guarantees the ultimate consistency of data between the broker and the nameserver during topic deletion. + */ + private boolean enableSingleTopicRegister = false; + + private boolean enableMixedMessageType = false; + + /** + * This flag and deleteTopicWithBrokerRegistration flag in the NameServer cannot be set to true at the same time, + * otherwise there will be a loss of routing + */ + private boolean enableSplitRegistration = false; + + private long popInflightMessageThreshold = 10000; + private boolean enablePopMessageThreshold = false; + + private int splitRegistrationSize = 800; + + /** + * Config in this black list will be not allowed to update by command. + * Try to update this config black list by restart process. + * Try to update configures in black list by restart process. + */ + private String configBlackList = "configBlackList;brokerConfigPath"; + + public String getConfigBlackList() { + return configBlackList; + } + + public void setConfigBlackList(String configBlackList) { + this.configBlackList = configBlackList; + } + + public long getMaxPopPollingSize() { + return maxPopPollingSize; + } + + public void setMaxPopPollingSize(long maxPopPollingSize) { + this.maxPopPollingSize = maxPopPollingSize; + } + + public int getReviveQueueNum() { + return reviveQueueNum; + } + + public void setReviveQueueNum(int reviveQueueNum) { + this.reviveQueueNum = reviveQueueNum; + } + + public long getReviveInterval() { + return reviveInterval; + } + + public void setReviveInterval(long reviveInterval) { + this.reviveInterval = reviveInterval; + } + + public int getPopCkStayBufferTime() { + return popCkStayBufferTime; + } + + public void setPopCkStayBufferTime(int popCkStayBufferTime) { + this.popCkStayBufferTime = popCkStayBufferTime; + } + + public int getPopCkStayBufferTimeOut() { + return popCkStayBufferTimeOut; + } + + public void setPopCkStayBufferTimeOut(int popCkStayBufferTimeOut) { + this.popCkStayBufferTimeOut = popCkStayBufferTimeOut; + } + + public int getPopPollingMapSize() { + return popPollingMapSize; + } + + public void setPopPollingMapSize(int popPollingMapSize) { + this.popPollingMapSize = popPollingMapSize; + } + + public long getReviveScanTime() { + return reviveScanTime; + } + + public void setReviveScanTime(long reviveScanTime) { + this.reviveScanTime = reviveScanTime; + } + + public long getReviveMaxSlow() { + return reviveMaxSlow; + } + + public void setReviveMaxSlow(long reviveMaxSlow) { + this.reviveMaxSlow = reviveMaxSlow; + } + + public int getPopPollingSize() { + return popPollingSize; + } + + public void setPopPollingSize(int popPollingSize) { + this.popPollingSize = popPollingSize; + } + + public boolean isEnablePopBufferMerge() { + return enablePopBufferMerge; + } + + public void setEnablePopBufferMerge(boolean enablePopBufferMerge) { + this.enablePopBufferMerge = enablePopBufferMerge; + } + + public int getPopCkMaxBufferSize() { + return popCkMaxBufferSize; + } + + public void setPopCkMaxBufferSize(int popCkMaxBufferSize) { + this.popCkMaxBufferSize = popCkMaxBufferSize; + } + + public int getPopCkOffsetMaxQueueSize() { + return popCkOffsetMaxQueueSize; + } + + public void setPopCkOffsetMaxQueueSize(int popCkOffsetMaxQueueSize) { + this.popCkOffsetMaxQueueSize = popCkOffsetMaxQueueSize; + } + + public boolean isEnablePopBatchAck() { + return enablePopBatchAck; + } + + public void setEnablePopBatchAck(boolean enablePopBatchAck) { + this.enablePopBatchAck = enablePopBatchAck; + } + + public boolean isEnableSkipLongAwaitingAck() { + return enableSkipLongAwaitingAck; + } + + public void setEnableSkipLongAwaitingAck(boolean enableSkipLongAwaitingAck) { + this.enableSkipLongAwaitingAck = enableSkipLongAwaitingAck; + } + + public long getReviveAckWaitMs() { + return reviveAckWaitMs; + } + + public void setReviveAckWaitMs(long reviveAckWaitMs) { + this.reviveAckWaitMs = reviveAckWaitMs; + } + + public boolean isEnablePopLog() { + return enablePopLog; + } + + public void setEnablePopLog(boolean enablePopLog) { + this.enablePopLog = enablePopLog; + } + public boolean isTraceOn() { return traceOn; } @@ -197,16 +619,6 @@ public void setSlaveReadEnable(final boolean slaveReadEnable) { this.slaveReadEnable = slaveReadEnable; } - public static String localHostName() { - try { - return InetAddress.getLocalHost().getHostName(); - } catch (UnknownHostException e) { - log.error("Failed to obtain the host name", e); - } - - return "DEFAULT_BROKER"; - } - public int getRegisterBrokerTimeoutMills() { return registerBrokerTimeoutMills; } @@ -247,22 +659,6 @@ public void setHighSpeedMode(final boolean highSpeedMode) { this.highSpeedMode = highSpeedMode; } - public String getRocketmqHome() { - return rocketmqHome; - } - - public void setRocketmqHome(String rocketmqHome) { - this.rocketmqHome = rocketmqHome; - } - - public String getBrokerName() { - return brokerName; - } - - public void setBrokerName(String brokerName) { - this.brokerName = brokerName; - } - public int getBrokerPermission() { return brokerPermission; } @@ -287,14 +683,6 @@ public void setAutoCreateTopicEnable(boolean autoCreateTopic) { this.autoCreateTopicEnable = autoCreateTopic; } - public String getBrokerClusterName() { - return brokerClusterName; - } - - public void setBrokerClusterName(String brokerClusterName) { - this.brokerClusterName = brokerClusterName; - } - public String getBrokerIP1() { return brokerIP1; } @@ -319,6 +707,14 @@ public void setSendMessageThreadPoolNums(int sendMessageThreadPoolNums) { this.sendMessageThreadPoolNums = sendMessageThreadPoolNums; } + public int getPutMessageFutureThreadPoolNums() { + return putMessageFutureThreadPoolNums; + } + + public void setPutMessageFutureThreadPoolNums(int putMessageFutureThreadPoolNums) { + this.putMessageFutureThreadPoolNums = putMessageFutureThreadPoolNums; + } + public int getPullMessageThreadPoolNums() { return pullMessageThreadPoolNums; } @@ -327,6 +723,22 @@ public void setPullMessageThreadPoolNums(int pullMessageThreadPoolNums) { this.pullMessageThreadPoolNums = pullMessageThreadPoolNums; } + public int getAckMessageThreadPoolNums() { + return ackMessageThreadPoolNums; + } + + public void setAckMessageThreadPoolNums(int ackMessageThreadPoolNums) { + this.ackMessageThreadPoolNums = ackMessageThreadPoolNums; + } + + public int getProcessReplyMessageThreadPoolNums() { + return processReplyMessageThreadPoolNums; + } + + public void setProcessReplyMessageThreadPoolNums(int processReplyMessageThreadPoolNums) { + this.processReplyMessageThreadPoolNums = processReplyMessageThreadPoolNums; + } + public int getQueryMessageThreadPoolNums() { return queryMessageThreadPoolNums; } @@ -375,14 +787,6 @@ public void setNamesrvAddr(String namesrvAddr) { this.namesrvAddr = namesrvAddr; } - public long getBrokerId() { - return brokerId; - } - - public void setBrokerId(long brokerId) { - this.brokerId = brokerId; - } - public boolean isAutoCreateSubscriptionGroup() { return autoCreateSubscriptionGroup; } @@ -391,132 +795,196 @@ public void setAutoCreateSubscriptionGroup(boolean autoCreateSubscriptionGroup) this.autoCreateSubscriptionGroup = autoCreateSubscriptionGroup; } - public boolean isRejectTransactionMessage() { - return rejectTransactionMessage; + public String getBrokerConfigPath() { + return brokerConfigPath; } - public void setRejectTransactionMessage(boolean rejectTransactionMessage) { - this.rejectTransactionMessage = rejectTransactionMessage; + public void setBrokerConfigPath(String brokerConfigPath) { + this.brokerConfigPath = brokerConfigPath; } - public boolean isFetchNamesrvAddrByAddressServer() { - return fetchNamesrvAddrByAddressServer; + public String getRocketmqHome() { + return rocketmqHome; } - public void setFetchNamesrvAddrByAddressServer(boolean fetchNamesrvAddrByAddressServer) { - this.fetchNamesrvAddrByAddressServer = fetchNamesrvAddrByAddressServer; + public void setRocketmqHome(String rocketmqHome) { + this.rocketmqHome = rocketmqHome; } - public int getSendThreadPoolQueueCapacity() { - return sendThreadPoolQueueCapacity; + public int getListenPort() { + return listenPort; } - public void setSendThreadPoolQueueCapacity(int sendThreadPoolQueueCapacity) { - this.sendThreadPoolQueueCapacity = sendThreadPoolQueueCapacity; + public void setListenPort(int listenPort) { + this.listenPort = listenPort; } - public int getPullThreadPoolQueueCapacity() { - return pullThreadPoolQueueCapacity; + public int getLitePullMessageThreadPoolNums() { + return litePullMessageThreadPoolNums; } - public void setPullThreadPoolQueueCapacity(int pullThreadPoolQueueCapacity) { - this.pullThreadPoolQueueCapacity = pullThreadPoolQueueCapacity; + public void setLitePullMessageThreadPoolNums(int litePullMessageThreadPoolNums) { + this.litePullMessageThreadPoolNums = litePullMessageThreadPoolNums; } - public int getQueryThreadPoolQueueCapacity() { - return queryThreadPoolQueueCapacity; + public int getLitePullThreadPoolQueueCapacity() { + return litePullThreadPoolQueueCapacity; } - public void setQueryThreadPoolQueueCapacity(final int queryThreadPoolQueueCapacity) { - this.queryThreadPoolQueueCapacity = queryThreadPoolQueueCapacity; + public void setLitePullThreadPoolQueueCapacity(int litePullThreadPoolQueueCapacity) { + this.litePullThreadPoolQueueCapacity = litePullThreadPoolQueueCapacity; } - public boolean isBrokerTopicEnable() { - return brokerTopicEnable; + public int getAdminBrokerThreadPoolQueueCapacity() { + return adminBrokerThreadPoolQueueCapacity; } - public void setBrokerTopicEnable(boolean brokerTopicEnable) { - this.brokerTopicEnable = brokerTopicEnable; + public void setAdminBrokerThreadPoolQueueCapacity(int adminBrokerThreadPoolQueueCapacity) { + this.adminBrokerThreadPoolQueueCapacity = adminBrokerThreadPoolQueueCapacity; } - public int getFilterServerNums() { - return filterServerNums; + public int getLoadBalanceThreadPoolQueueCapacity() { + return loadBalanceThreadPoolQueueCapacity; } - public void setFilterServerNums(int filterServerNums) { - this.filterServerNums = filterServerNums; + public void setLoadBalanceThreadPoolQueueCapacity(int loadBalanceThreadPoolQueueCapacity) { + this.loadBalanceThreadPoolQueueCapacity = loadBalanceThreadPoolQueueCapacity; } - public boolean isLongPollingEnable() { - return longPollingEnable; + public int getSendHeartbeatTimeoutMillis() { + return sendHeartbeatTimeoutMillis; } - public void setLongPollingEnable(boolean longPollingEnable) { - this.longPollingEnable = longPollingEnable; + public void setSendHeartbeatTimeoutMillis(int sendHeartbeatTimeoutMillis) { + this.sendHeartbeatTimeoutMillis = sendHeartbeatTimeoutMillis; } - public boolean isNotifyConsumerIdsChangedEnable() { - return notifyConsumerIdsChangedEnable; + public long getWaitTimeMillsInLitePullQueue() { + return waitTimeMillsInLitePullQueue; } - public void setNotifyConsumerIdsChangedEnable(boolean notifyConsumerIdsChangedEnable) { - this.notifyConsumerIdsChangedEnable = notifyConsumerIdsChangedEnable; + public void setWaitTimeMillsInLitePullQueue(long waitTimeMillsInLitePullQueue) { + this.waitTimeMillsInLitePullQueue = waitTimeMillsInLitePullQueue; } - public long getShortPollingTimeMills() { - return shortPollingTimeMills; + public boolean isLitePullMessageEnable() { + return litePullMessageEnable; } - public void setShortPollingTimeMills(long shortPollingTimeMills) { - this.shortPollingTimeMills = shortPollingTimeMills; + public void setLitePullMessageEnable(boolean litePullMessageEnable) { + this.litePullMessageEnable = litePullMessageEnable; } - public int getClientManageThreadPoolNums() { - return clientManageThreadPoolNums; + public int getSyncBrokerMemberGroupPeriod() { + return syncBrokerMemberGroupPeriod; } - public void setClientManageThreadPoolNums(int clientManageThreadPoolNums) { - this.clientManageThreadPoolNums = clientManageThreadPoolNums; + public void setSyncBrokerMemberGroupPeriod(int syncBrokerMemberGroupPeriod) { + this.syncBrokerMemberGroupPeriod = syncBrokerMemberGroupPeriod; + } + + public boolean isRejectTransactionMessage() { + return rejectTransactionMessage; + } + + public void setRejectTransactionMessage(boolean rejectTransactionMessage) { + this.rejectTransactionMessage = rejectTransactionMessage; + } + + public boolean isFetchNamesrvAddrByAddressServer() { + return fetchNamesrvAddrByAddressServer; + } + + public void setFetchNamesrvAddrByAddressServer(boolean fetchNamesrvAddrByAddressServer) { + this.fetchNamesrvAddrByAddressServer = fetchNamesrvAddrByAddressServer; + } + + public int getSendThreadPoolQueueCapacity() { + return sendThreadPoolQueueCapacity; + } + + public void setSendThreadPoolQueueCapacity(int sendThreadPoolQueueCapacity) { + this.sendThreadPoolQueueCapacity = sendThreadPoolQueueCapacity; + } + + public int getPutThreadPoolQueueCapacity() { + return putThreadPoolQueueCapacity; + } + + public void setPutThreadPoolQueueCapacity(int putThreadPoolQueueCapacity) { + this.putThreadPoolQueueCapacity = putThreadPoolQueueCapacity; + } + + public int getPullThreadPoolQueueCapacity() { + return pullThreadPoolQueueCapacity; + } + + public void setPullThreadPoolQueueCapacity(int pullThreadPoolQueueCapacity) { + this.pullThreadPoolQueueCapacity = pullThreadPoolQueueCapacity; + } + + public int getAckThreadPoolQueueCapacity() { + return ackThreadPoolQueueCapacity; + } + + public void setAckThreadPoolQueueCapacity(int ackThreadPoolQueueCapacity) { + this.ackThreadPoolQueueCapacity = ackThreadPoolQueueCapacity; + } + + public int getReplyThreadPoolQueueCapacity() { + return replyThreadPoolQueueCapacity; + } + + public void setReplyThreadPoolQueueCapacity(int replyThreadPoolQueueCapacity) { + this.replyThreadPoolQueueCapacity = replyThreadPoolQueueCapacity; } - public boolean isCommercialEnable() { - return commercialEnable; + public int getQueryThreadPoolQueueCapacity() { + return queryThreadPoolQueueCapacity; } - public void setCommercialEnable(final boolean commercialEnable) { - this.commercialEnable = commercialEnable; + public void setQueryThreadPoolQueueCapacity(final int queryThreadPoolQueueCapacity) { + this.queryThreadPoolQueueCapacity = queryThreadPoolQueueCapacity; } - public int getCommercialTimerCount() { - return commercialTimerCount; + public boolean isBrokerTopicEnable() { + return brokerTopicEnable; } - public void setCommercialTimerCount(final int commercialTimerCount) { - this.commercialTimerCount = commercialTimerCount; + public void setBrokerTopicEnable(boolean brokerTopicEnable) { + this.brokerTopicEnable = brokerTopicEnable; } - public int getCommercialTransCount() { - return commercialTransCount; + public boolean isLongPollingEnable() { + return longPollingEnable; } - public void setCommercialTransCount(final int commercialTransCount) { - this.commercialTransCount = commercialTransCount; + public void setLongPollingEnable(boolean longPollingEnable) { + this.longPollingEnable = longPollingEnable; } - public int getCommercialBigCount() { - return commercialBigCount; + public boolean isNotifyConsumerIdsChangedEnable() { + return notifyConsumerIdsChangedEnable; } - public void setCommercialBigCount(final int commercialBigCount) { - this.commercialBigCount = commercialBigCount; + public void setNotifyConsumerIdsChangedEnable(boolean notifyConsumerIdsChangedEnable) { + this.notifyConsumerIdsChangedEnable = notifyConsumerIdsChangedEnable; + } + + public long getShortPollingTimeMills() { + return shortPollingTimeMills; + } + + public void setShortPollingTimeMills(long shortPollingTimeMills) { + this.shortPollingTimeMills = shortPollingTimeMills; } - public int getMaxDelayTime() { - return maxDelayTime; + public int getClientManageThreadPoolNums() { + return clientManageThreadPoolNums; } - public void setMaxDelayTime(final int maxDelayTime) { - this.maxDelayTime = maxDelayTime; + public void setClientManageThreadPoolNums(int clientManageThreadPoolNums) { + this.clientManageThreadPoolNums = clientManageThreadPoolNums; } public int getClientManagerThreadPoolQueueCapacity() { @@ -598,4 +1066,756 @@ public boolean isEnablePropertyFilter() { public void setEnablePropertyFilter(boolean enablePropertyFilter) { this.enablePropertyFilter = enablePropertyFilter; } + + public boolean isCompressedRegister() { + return compressedRegister; + } + + public void setCompressedRegister(boolean compressedRegister) { + this.compressedRegister = compressedRegister; + } + + public boolean isForceRegister() { + return forceRegister; + } + + public void setForceRegister(boolean forceRegister) { + this.forceRegister = forceRegister; + } + + public int getHeartbeatThreadPoolQueueCapacity() { + return heartbeatThreadPoolQueueCapacity; + } + + public void setHeartbeatThreadPoolQueueCapacity(int heartbeatThreadPoolQueueCapacity) { + this.heartbeatThreadPoolQueueCapacity = heartbeatThreadPoolQueueCapacity; + } + + public int getHeartbeatThreadPoolNums() { + return heartbeatThreadPoolNums; + } + + public void setHeartbeatThreadPoolNums(int heartbeatThreadPoolNums) { + this.heartbeatThreadPoolNums = heartbeatThreadPoolNums; + } + + public long getWaitTimeMillsInHeartbeatQueue() { + return waitTimeMillsInHeartbeatQueue; + } + + public void setWaitTimeMillsInHeartbeatQueue(long waitTimeMillsInHeartbeatQueue) { + this.waitTimeMillsInHeartbeatQueue = waitTimeMillsInHeartbeatQueue; + } + + public int getRegisterNameServerPeriod() { + return registerNameServerPeriod; + } + + public void setRegisterNameServerPeriod(int registerNameServerPeriod) { + this.registerNameServerPeriod = registerNameServerPeriod; + } + + public long getTransactionTimeOut() { + return transactionTimeOut; + } + + public void setTransactionTimeOut(long transactionTimeOut) { + this.transactionTimeOut = transactionTimeOut; + } + + public int getTransactionCheckMax() { + return transactionCheckMax; + } + + public void setTransactionCheckMax(int transactionCheckMax) { + this.transactionCheckMax = transactionCheckMax; + } + + public long getTransactionCheckInterval() { + return transactionCheckInterval; + } + + public void setTransactionCheckInterval(long transactionCheckInterval) { + this.transactionCheckInterval = transactionCheckInterval; + } + + public int getEndTransactionThreadPoolNums() { + return endTransactionThreadPoolNums; + } + + public void setEndTransactionThreadPoolNums(int endTransactionThreadPoolNums) { + this.endTransactionThreadPoolNums = endTransactionThreadPoolNums; + } + + public int getEndTransactionPoolQueueCapacity() { + return endTransactionPoolQueueCapacity; + } + + public void setEndTransactionPoolQueueCapacity(int endTransactionPoolQueueCapacity) { + this.endTransactionPoolQueueCapacity = endTransactionPoolQueueCapacity; + } + + public long getWaitTimeMillsInTransactionQueue() { + return waitTimeMillsInTransactionQueue; + } + + public void setWaitTimeMillsInTransactionQueue(long waitTimeMillsInTransactionQueue) { + this.waitTimeMillsInTransactionQueue = waitTimeMillsInTransactionQueue; + } + + public String getMsgTraceTopicName() { + return msgTraceTopicName; + } + + public void setMsgTraceTopicName(String msgTraceTopicName) { + this.msgTraceTopicName = msgTraceTopicName; + } + + public boolean isTraceTopicEnable() { + return traceTopicEnable; + } + + public void setTraceTopicEnable(boolean traceTopicEnable) { + this.traceTopicEnable = traceTopicEnable; + } + + public boolean isAclEnable() { + return aclEnable; + } + + public void setAclEnable(boolean aclEnable) { + this.aclEnable = aclEnable; + } + + public boolean isStoreReplyMessageEnable() { + return storeReplyMessageEnable; + } + + public void setStoreReplyMessageEnable(boolean storeReplyMessageEnable) { + this.storeReplyMessageEnable = storeReplyMessageEnable; + } + + public boolean isEnableDetailStat() { + return enableDetailStat; + } + + public void setEnableDetailStat(boolean enableDetailStat) { + this.enableDetailStat = enableDetailStat; + } + + public boolean isAutoDeleteUnusedStats() { + return autoDeleteUnusedStats; + } + + public void setAutoDeleteUnusedStats(boolean autoDeleteUnusedStats) { + this.autoDeleteUnusedStats = autoDeleteUnusedStats; + } + + public long getLoadBalancePollNameServerInterval() { + return loadBalancePollNameServerInterval; + } + + public void setLoadBalancePollNameServerInterval(long loadBalancePollNameServerInterval) { + this.loadBalancePollNameServerInterval = loadBalancePollNameServerInterval; + } + + public int getCleanOfflineBrokerInterval() { + return cleanOfflineBrokerInterval; + } + + public void setCleanOfflineBrokerInterval(int cleanOfflineBrokerInterval) { + this.cleanOfflineBrokerInterval = cleanOfflineBrokerInterval; + } + + public int getLoadBalanceProcessorThreadPoolNums() { + return loadBalanceProcessorThreadPoolNums; + } + + public void setLoadBalanceProcessorThreadPoolNums(int loadBalanceProcessorThreadPoolNums) { + this.loadBalanceProcessorThreadPoolNums = loadBalanceProcessorThreadPoolNums; + } + + public boolean isServerLoadBalancerEnable() { + return serverLoadBalancerEnable; + } + + public void setServerLoadBalancerEnable(boolean serverLoadBalancerEnable) { + this.serverLoadBalancerEnable = serverLoadBalancerEnable; + } + + public MessageRequestMode getDefaultMessageRequestMode() { + return defaultMessageRequestMode; + } + + public void setDefaultMessageRequestMode(String defaultMessageRequestMode) { + this.defaultMessageRequestMode = MessageRequestMode.valueOf(defaultMessageRequestMode); + } + + public int getDefaultPopShareQueueNum() { + return defaultPopShareQueueNum; + } + + public void setDefaultPopShareQueueNum(int defaultPopShareQueueNum) { + this.defaultPopShareQueueNum = defaultPopShareQueueNum; + } + + public long getForwardTimeout() { + return forwardTimeout; + } + + public void setForwardTimeout(long timeout) { + this.forwardTimeout = timeout; + } + + public int getBrokerHeartbeatInterval() { + return brokerHeartbeatInterval; + } + + public void setBrokerHeartbeatInterval(int brokerHeartbeatInterval) { + this.brokerHeartbeatInterval = brokerHeartbeatInterval; + } + + public long getBrokerNotActiveTimeoutMillis() { + return brokerNotActiveTimeoutMillis; + } + + public void setBrokerNotActiveTimeoutMillis(long brokerNotActiveTimeoutMillis) { + this.brokerNotActiveTimeoutMillis = brokerNotActiveTimeoutMillis; + } + + public boolean isEnableNetWorkFlowControl() { + return enableNetWorkFlowControl; + } + + public void setEnableNetWorkFlowControl(boolean enableNetWorkFlowControl) { + this.enableNetWorkFlowControl = enableNetWorkFlowControl; + } + + public boolean isEnableNotifyAfterPopOrderLockRelease() { + return enableNotifyAfterPopOrderLockRelease; + } + + public void setEnableNotifyAfterPopOrderLockRelease(boolean enableNotifyAfterPopOrderLockRelease) { + this.enableNotifyAfterPopOrderLockRelease = enableNotifyAfterPopOrderLockRelease; + } + + public boolean isInitPopOffsetByCheckMsgInMem() { + return initPopOffsetByCheckMsgInMem; + } + + public void setInitPopOffsetByCheckMsgInMem(boolean initPopOffsetByCheckMsgInMem) { + this.initPopOffsetByCheckMsgInMem = initPopOffsetByCheckMsgInMem; + } + + public boolean isRetrieveMessageFromPopRetryTopicV1() { + return retrieveMessageFromPopRetryTopicV1; + } + + public void setRetrieveMessageFromPopRetryTopicV1(boolean retrieveMessageFromPopRetryTopicV1) { + this.retrieveMessageFromPopRetryTopicV1 = retrieveMessageFromPopRetryTopicV1; + } + + public boolean isEnableRetryTopicV2() { + return enableRetryTopicV2; + } + + public void setEnableRetryTopicV2(boolean enableRetryTopicV2) { + this.enableRetryTopicV2 = enableRetryTopicV2; + } + + public boolean isRealTimeNotifyConsumerChange() { + return realTimeNotifyConsumerChange; + } + + public void setRealTimeNotifyConsumerChange(boolean realTimeNotifyConsumerChange) { + this.realTimeNotifyConsumerChange = realTimeNotifyConsumerChange; + } + + public boolean isEnableSlaveActingMaster() { + return enableSlaveActingMaster; + } + + public void setEnableSlaveActingMaster(boolean enableSlaveActingMaster) { + this.enableSlaveActingMaster = enableSlaveActingMaster; + } + + public boolean isEnableRemoteEscape() { + return enableRemoteEscape; + } + + public void setEnableRemoteEscape(boolean enableRemoteEscape) { + this.enableRemoteEscape = enableRemoteEscape; + } + + public boolean isSkipPreOnline() { + return skipPreOnline; + } + + public void setSkipPreOnline(boolean skipPreOnline) { + this.skipPreOnline = skipPreOnline; + } + + public boolean isAsyncSendEnable() { + return asyncSendEnable; + } + + public void setAsyncSendEnable(boolean asyncSendEnable) { + this.asyncSendEnable = asyncSendEnable; + } + + public long getConsumerOffsetUpdateVersionStep() { + return consumerOffsetUpdateVersionStep; + } + + public void setConsumerOffsetUpdateVersionStep(long consumerOffsetUpdateVersionStep) { + this.consumerOffsetUpdateVersionStep = consumerOffsetUpdateVersionStep; + } + + public long getDelayOffsetUpdateVersionStep() { + return delayOffsetUpdateVersionStep; + } + + public void setDelayOffsetUpdateVersionStep(long delayOffsetUpdateVersionStep) { + this.delayOffsetUpdateVersionStep = delayOffsetUpdateVersionStep; + } + + public int getCommercialSizePerMsg() { + return commercialSizePerMsg; + } + + public void setCommercialSizePerMsg(int commercialSizePerMsg) { + this.commercialSizePerMsg = commercialSizePerMsg; + } + + public long getWaitTimeMillsInAckQueue() { + return waitTimeMillsInAckQueue; + } + + public void setWaitTimeMillsInAckQueue(long waitTimeMillsInAckQueue) { + this.waitTimeMillsInAckQueue = waitTimeMillsInAckQueue; + } + + public boolean isRejectPullConsumerEnable() { + return rejectPullConsumerEnable; + } + + public void setRejectPullConsumerEnable(boolean rejectPullConsumerEnable) { + this.rejectPullConsumerEnable = rejectPullConsumerEnable; + } + + public boolean isAccountStatsEnable() { + return accountStatsEnable; + } + + public void setAccountStatsEnable(boolean accountStatsEnable) { + this.accountStatsEnable = accountStatsEnable; + } + + public boolean isAccountStatsPrintZeroValues() { + return accountStatsPrintZeroValues; + } + + public void setAccountStatsPrintZeroValues(boolean accountStatsPrintZeroValues) { + this.accountStatsPrintZeroValues = accountStatsPrintZeroValues; + } + + public boolean isLockInStrictMode() { + return lockInStrictMode; + } + + public void setLockInStrictMode(boolean lockInStrictMode) { + this.lockInStrictMode = lockInStrictMode; + } + + public boolean isIsolateLogEnable() { + return isolateLogEnable; + } + + public void setIsolateLogEnable(boolean isolateLogEnable) { + this.isolateLogEnable = isolateLogEnable; + } + + public boolean isCompatibleWithOldNameSrv() { + return compatibleWithOldNameSrv; + } + + public void setCompatibleWithOldNameSrv(boolean compatibleWithOldNameSrv) { + this.compatibleWithOldNameSrv = compatibleWithOldNameSrv; + } + + public boolean isEnableControllerMode() { + return enableControllerMode; + } + + public void setEnableControllerMode(boolean enableControllerMode) { + this.enableControllerMode = enableControllerMode; + } + + public String getControllerAddr() { + return controllerAddr; + } + + public void setControllerAddr(String controllerAddr) { + this.controllerAddr = controllerAddr; + } + + public boolean isFetchControllerAddrByDnsLookup() { + return fetchControllerAddrByDnsLookup; + } + + public void setFetchControllerAddrByDnsLookup(boolean fetchControllerAddrByDnsLookup) { + this.fetchControllerAddrByDnsLookup = fetchControllerAddrByDnsLookup; + } + + public long getSyncBrokerMetadataPeriod() { + return syncBrokerMetadataPeriod; + } + + public void setSyncBrokerMetadataPeriod(long syncBrokerMetadataPeriod) { + this.syncBrokerMetadataPeriod = syncBrokerMetadataPeriod; + } + + public long getCheckSyncStateSetPeriod() { + return checkSyncStateSetPeriod; + } + + public void setCheckSyncStateSetPeriod(long checkSyncStateSetPeriod) { + this.checkSyncStateSetPeriod = checkSyncStateSetPeriod; + } + + public long getSyncControllerMetadataPeriod() { + return syncControllerMetadataPeriod; + } + + public void setSyncControllerMetadataPeriod(long syncControllerMetadataPeriod) { + this.syncControllerMetadataPeriod = syncControllerMetadataPeriod; + } + + public int getBrokerElectionPriority() { + return brokerElectionPriority; + } + + public void setBrokerElectionPriority(int brokerElectionPriority) { + this.brokerElectionPriority = brokerElectionPriority; + } + + public long getControllerHeartBeatTimeoutMills() { + return controllerHeartBeatTimeoutMills; + } + + public void setControllerHeartBeatTimeoutMills(long controllerHeartBeatTimeoutMills) { + this.controllerHeartBeatTimeoutMills = controllerHeartBeatTimeoutMills; + } + + public boolean isRecoverConcurrently() { + return recoverConcurrently; + } + + public void setRecoverConcurrently(boolean recoverConcurrently) { + this.recoverConcurrently = recoverConcurrently; + } + + public int getRecoverThreadPoolNums() { + return recoverThreadPoolNums; + } + + public void setRecoverThreadPoolNums(int recoverThreadPoolNums) { + this.recoverThreadPoolNums = recoverThreadPoolNums; + } + + public boolean isFetchNameSrvAddrByDnsLookup() { + return fetchNameSrvAddrByDnsLookup; + } + + public void setFetchNameSrvAddrByDnsLookup(boolean fetchNameSrvAddrByDnsLookup) { + this.fetchNameSrvAddrByDnsLookup = fetchNameSrvAddrByDnsLookup; + } + + public boolean isUseServerSideResetOffset() { + return useServerSideResetOffset; + } + + public void setUseServerSideResetOffset(boolean useServerSideResetOffset) { + this.useServerSideResetOffset = useServerSideResetOffset; + } + + public boolean isEnableBroadcastOffsetStore() { + return enableBroadcastOffsetStore; + } + + public void setEnableBroadcastOffsetStore(boolean enableBroadcastOffsetStore) { + this.enableBroadcastOffsetStore = enableBroadcastOffsetStore; + } + + public long getBroadcastOffsetExpireSecond() { + return broadcastOffsetExpireSecond; + } + + public void setBroadcastOffsetExpireSecond(long broadcastOffsetExpireSecond) { + this.broadcastOffsetExpireSecond = broadcastOffsetExpireSecond; + } + + public long getBroadcastOffsetExpireMaxSecond() { + return broadcastOffsetExpireMaxSecond; + } + + public void setBroadcastOffsetExpireMaxSecond(long broadcastOffsetExpireMaxSecond) { + this.broadcastOffsetExpireMaxSecond = broadcastOffsetExpireMaxSecond; + } + + public MetricsExporterType getMetricsExporterType() { + return metricsExporterType; + } + + public void setMetricsExporterType(MetricsExporterType metricsExporterType) { + this.metricsExporterType = metricsExporterType; + } + + public void setMetricsExporterType(int metricsExporterType) { + this.metricsExporterType = MetricsExporterType.valueOf(metricsExporterType); + } + + public void setMetricsExporterType(String metricsExporterType) { + this.metricsExporterType = MetricsExporterType.valueOf(metricsExporterType); + } + + public int getMetricsOtelCardinalityLimit() { + return metricsOtelCardinalityLimit; + } + + public void setMetricsOtelCardinalityLimit(int metricsOtelCardinalityLimit) { + this.metricsOtelCardinalityLimit = metricsOtelCardinalityLimit; + } + + public String getMetricsGrpcExporterTarget() { + return metricsGrpcExporterTarget; + } + + public void setMetricsGrpcExporterTarget(String metricsGrpcExporterTarget) { + this.metricsGrpcExporterTarget = metricsGrpcExporterTarget; + } + + public String getMetricsGrpcExporterHeader() { + return metricsGrpcExporterHeader; + } + + public void setMetricsGrpcExporterHeader(String metricsGrpcExporterHeader) { + this.metricsGrpcExporterHeader = metricsGrpcExporterHeader; + } + + public long getMetricGrpcExporterTimeOutInMills() { + return metricGrpcExporterTimeOutInMills; + } + + public void setMetricGrpcExporterTimeOutInMills(long metricGrpcExporterTimeOutInMills) { + this.metricGrpcExporterTimeOutInMills = metricGrpcExporterTimeOutInMills; + } + + public long getMetricGrpcExporterIntervalInMills() { + return metricGrpcExporterIntervalInMills; + } + + public void setMetricGrpcExporterIntervalInMills(long metricGrpcExporterIntervalInMills) { + this.metricGrpcExporterIntervalInMills = metricGrpcExporterIntervalInMills; + } + + public long getMetricLoggingExporterIntervalInMills() { + return metricLoggingExporterIntervalInMills; + } + + public void setMetricLoggingExporterIntervalInMills(long metricLoggingExporterIntervalInMills) { + this.metricLoggingExporterIntervalInMills = metricLoggingExporterIntervalInMills; + } + + public String getMetricsLabel() { + return metricsLabel; + } + + public void setMetricsLabel(String metricsLabel) { + this.metricsLabel = metricsLabel; + } + + public boolean isMetricsInDelta() { + return metricsInDelta; + } + + public void setMetricsInDelta(boolean metricsInDelta) { + this.metricsInDelta = metricsInDelta; + } + + public int getMetricsPromExporterPort() { + return metricsPromExporterPort; + } + + public void setMetricsPromExporterPort(int metricsPromExporterPort) { + this.metricsPromExporterPort = metricsPromExporterPort; + } + + public String getMetricsPromExporterHost() { + return metricsPromExporterHost; + } + + public void setMetricsPromExporterHost(String metricsPromExporterHost) { + this.metricsPromExporterHost = metricsPromExporterHost; + } + + public int getTransactionOpMsgMaxSize() { + return transactionOpMsgMaxSize; + } + + public void setTransactionOpMsgMaxSize(int transactionOpMsgMaxSize) { + this.transactionOpMsgMaxSize = transactionOpMsgMaxSize; + } + + public int getTransactionOpBatchInterval() { + return transactionOpBatchInterval; + } + + public void setTransactionOpBatchInterval(int transactionOpBatchInterval) { + this.transactionOpBatchInterval = transactionOpBatchInterval; + } + + public long getChannelExpiredTimeout() { + return channelExpiredTimeout; + } + + public void setChannelExpiredTimeout(long channelExpiredTimeout) { + this.channelExpiredTimeout = channelExpiredTimeout; + } + + public long getSubscriptionExpiredTimeout() { + return subscriptionExpiredTimeout; + } + + public void setSubscriptionExpiredTimeout(long subscriptionExpiredTimeout) { + this.subscriptionExpiredTimeout = subscriptionExpiredTimeout; + } + + public boolean isValidateSystemTopicWhenUpdateTopic() { + return validateSystemTopicWhenUpdateTopic; + } + + public void setValidateSystemTopicWhenUpdateTopic(boolean validateSystemTopicWhenUpdateTopic) { + this.validateSystemTopicWhenUpdateTopic = validateSystemTopicWhenUpdateTopic; + } + + public boolean isEstimateAccumulation() { + return estimateAccumulation; + } + + public void setEstimateAccumulation(boolean estimateAccumulation) { + this.estimateAccumulation = estimateAccumulation; + } + + public boolean isColdCtrStrategyEnable() { + return coldCtrStrategyEnable; + } + + public void setColdCtrStrategyEnable(boolean coldCtrStrategyEnable) { + this.coldCtrStrategyEnable = coldCtrStrategyEnable; + } + + public boolean isUsePIDColdCtrStrategy() { + return usePIDColdCtrStrategy; + } + + public void setUsePIDColdCtrStrategy(boolean usePIDColdCtrStrategy) { + this.usePIDColdCtrStrategy = usePIDColdCtrStrategy; + } + + public long getCgColdReadThreshold() { + return cgColdReadThreshold; + } + + public void setCgColdReadThreshold(long cgColdReadThreshold) { + this.cgColdReadThreshold = cgColdReadThreshold; + } + + public long getGlobalColdReadThreshold() { + return globalColdReadThreshold; + } + + public void setGlobalColdReadThreshold(long globalColdReadThreshold) { + this.globalColdReadThreshold = globalColdReadThreshold; + } + + public boolean isUseStaticSubscription() { + return useStaticSubscription; + } + + public void setUseStaticSubscription(boolean useStaticSubscription) { + this.useStaticSubscription = useStaticSubscription; + } + + public long getFetchNamesrvAddrInterval() { + return fetchNamesrvAddrInterval; + } + + public void setFetchNamesrvAddrInterval(final long fetchNamesrvAddrInterval) { + this.fetchNamesrvAddrInterval = fetchNamesrvAddrInterval; + } + + public boolean isPopResponseReturnActualRetryTopic() { + return popResponseReturnActualRetryTopic; + } + + public void setPopResponseReturnActualRetryTopic(boolean popResponseReturnActualRetryTopic) { + this.popResponseReturnActualRetryTopic = popResponseReturnActualRetryTopic; + } + + public boolean isEnableSingleTopicRegister() { + return enableSingleTopicRegister; + } + + public void setEnableSingleTopicRegister(boolean enableSingleTopicRegister) { + this.enableSingleTopicRegister = enableSingleTopicRegister; + } + + public boolean isEnableMixedMessageType() { + return enableMixedMessageType; + } + + public void setEnableMixedMessageType(boolean enableMixedMessageType) { + this.enableMixedMessageType = enableMixedMessageType; + } + + public boolean isEnableSplitRegistration() { + return enableSplitRegistration; + } + + public void setEnableSplitRegistration(boolean enableSplitRegistration) { + this.enableSplitRegistration = enableSplitRegistration; + } + + public int getSplitRegistrationSize() { + return splitRegistrationSize; + } + + public void setSplitRegistrationSize(int splitRegistrationSize) { + this.splitRegistrationSize = splitRegistrationSize; + } + + public long getTransactionMetricFlushInterval() { + return transactionMetricFlushInterval; + } + + public void setTransactionMetricFlushInterval(long transactionMetricFlushInterval) { + this.transactionMetricFlushInterval = transactionMetricFlushInterval; + } + + public long getPopInflightMessageThreshold() { + return popInflightMessageThreshold; + } + + public void setPopInflightMessageThreshold(long popInflightMessageThreshold) { + this.popInflightMessageThreshold = popInflightMessageThreshold; + } + + public boolean isEnablePopMessageThreshold() { + return enablePopMessageThreshold; + } + + public void setEnablePopMessageThreshold(boolean enablePopMessageThreshold) { + this.enablePopMessageThreshold = enablePopMessageThreshold; + } } diff --git a/common/src/main/java/org/apache/rocketmq/common/BrokerIdentity.java b/common/src/main/java/org/apache/rocketmq/common/BrokerIdentity.java new file mode 100644 index 00000000000..e85a3aac728 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/BrokerIdentity.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.rocketmq.common.annotation.ImportantField; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +public class BrokerIdentity { + private static final String DEFAULT_CLUSTER_NAME = "DefaultCluster"; + + protected static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); + + private static String localHostName; + + static { + try { + localHostName = InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + LOGGER.error("Failed to obtain the host name", e); + } + } + + // load it after the localHostName is initialized + public static final BrokerIdentity BROKER_CONTAINER_IDENTITY = new BrokerIdentity(true); + + @ImportantField + private String brokerName = defaultBrokerName(); + @ImportantField + private String brokerClusterName = DEFAULT_CLUSTER_NAME; + @ImportantField + private volatile long brokerId = MixAll.MASTER_ID; + + private boolean isBrokerContainer = false; + + // Do not set it manually, it depends on the startup mode + // Broker start by BrokerStartup is false, start or add by BrokerContainer is true + private boolean isInBrokerContainer = false; + + public BrokerIdentity() { + } + + public BrokerIdentity(boolean isBrokerContainer) { + this.isBrokerContainer = isBrokerContainer; + } + + public BrokerIdentity(String brokerClusterName, String brokerName, long brokerId) { + this.brokerName = brokerName; + this.brokerClusterName = brokerClusterName; + this.brokerId = brokerId; + } + + public BrokerIdentity(String brokerClusterName, String brokerName, long brokerId, boolean isInBrokerContainer) { + this.brokerName = brokerName; + this.brokerClusterName = brokerClusterName; + this.brokerId = brokerId; + this.isInBrokerContainer = isInBrokerContainer; + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(final String brokerName) { + this.brokerName = brokerName; + } + + public String getBrokerClusterName() { + return brokerClusterName; + } + + public void setBrokerClusterName(final String brokerClusterName) { + this.brokerClusterName = brokerClusterName; + } + + public long getBrokerId() { + return brokerId; + } + + public void setBrokerId(final long brokerId) { + this.brokerId = brokerId; + } + + public boolean isInBrokerContainer() { + return isInBrokerContainer; + } + + public void setInBrokerContainer(boolean inBrokerContainer) { + isInBrokerContainer = inBrokerContainer; + } + + private String defaultBrokerName() { + return StringUtils.isEmpty(localHostName) ? "DEFAULT_BROKER" : localHostName; + } + + public String getCanonicalName() { + return isBrokerContainer ? "BrokerContainer" : String.format("%s_%s_%d", brokerClusterName, brokerName, + brokerId); + } + + public String getIdentifier() { + return "#" + getCanonicalName() + "#"; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + final BrokerIdentity identity = (BrokerIdentity) o; + + return new EqualsBuilder() + .append(brokerId, identity.brokerId) + .append(brokerName, identity.brokerName) + .append(brokerClusterName, identity.brokerClusterName) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(brokerName) + .append(brokerClusterName) + .append(brokerId) + .toHashCode(); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/ConfigManager.java b/common/src/main/java/org/apache/rocketmq/common/ConfigManager.java index c33ebdf324e..5e997596194 100644 --- a/common/src/main/java/org/apache/rocketmq/common/ConfigManager.java +++ b/common/src/main/java/org/apache/rocketmq/common/ConfigManager.java @@ -16,15 +16,19 @@ */ package org.apache.rocketmq.common; -import java.io.IOException; +import org.apache.rocketmq.common.config.RocksDBConfigManager; import org.apache.rocketmq.common.constant.LoggerName; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.rocksdb.Statistics; + +import java.io.IOException; +import java.util.Map; public abstract class ConfigManager { private static final Logger log = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); - public abstract String encode(); + protected RocksDBConfigManager rocksDBConfigManager; public boolean load() { String fileName = null; @@ -36,17 +40,15 @@ public boolean load() { return this.loadBak(); } else { this.decode(jsonString); - log.info("load {} OK", fileName); + log.info("load " + fileName + " OK"); return true; } } catch (Exception e) { - log.error("load [{}] failed, and try to load backup file", fileName, e); + log.error("load " + fileName + " failed, and try to load backup file", e); return this.loadBak(); } } - public abstract String configFilePath(); - private boolean loadBak() { String fileName = null; try { @@ -54,18 +56,26 @@ private boolean loadBak() { String jsonString = MixAll.file2String(fileName + ".bak"); if (jsonString != null && jsonString.length() > 0) { this.decode(jsonString); - log.info("load [{}] OK", fileName); + log.info("load " + fileName + " OK"); return true; } } catch (Exception e) { - log.error("load [{}] Failed", fileName, e); + log.error("load " + fileName + " Failed", e); return false; } return true; } - public abstract void decode(final String jsonString); + public synchronized void persist(String topicName, T t) { + // stub for future + this.persist(); + } + + public synchronized void persist(Map m) { + // stub for future + this.persist(); + } public synchronized void persist() { String jsonString = this.encode(true); @@ -74,10 +84,28 @@ public synchronized void persist() { try { MixAll.string2File(jsonString, fileName); } catch (IOException e) { - log.error("persist file [{}] exception", fileName, e); + log.error("persist file " + fileName + " exception", e); } } } + protected void decode0(final byte[] key, final byte[] body) { + + } + + public boolean stop() { + return true; + } + + public abstract String configFilePath(); + + public abstract String encode(); + public abstract String encode(final boolean prettyFormat); + + public abstract void decode(final String jsonString); + + public Statistics getStatistics() { + return rocksDBConfigManager == null ? null : rocksDBConfigManager.getStatistics(); + } } diff --git a/common/src/main/java/org/apache/rocketmq/common/Configuration.java b/common/src/main/java/org/apache/rocketmq/common/Configuration.java deleted file mode 100644 index 802f6eea2bd..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/Configuration.java +++ /dev/null @@ -1,301 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.common; - -import java.io.IOException; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; -import org.slf4j.Logger; - -public class Configuration { - - private final Logger log; - - private List configObjectList = new ArrayList(4); - private String storePath; - private boolean storePathFromConfig = false; - private Object storePathObject; - private Field storePathField; - private DataVersion dataVersion = new DataVersion(); - private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); - /** - * All properties include configs in object and extend properties. - */ - private Properties allConfigs = new Properties(); - - public Configuration(Logger log) { - this.log = log; - } - - public Configuration(Logger log, Object... configObjects) { - this.log = log; - if (configObjects == null || configObjects.length == 0) { - return; - } - for (Object configObject : configObjects) { - registerConfig(configObject); - } - } - - public Configuration(Logger log, String storePath, Object... configObjects) { - this(log, configObjects); - this.storePath = storePath; - } - - /** - * register config object - * - * @return the current Configuration object - */ - public Configuration registerConfig(Object configObject) { - try { - readWriteLock.writeLock().lockInterruptibly(); - - try { - - Properties registerProps = MixAll.object2Properties(configObject); - - merge(registerProps, this.allConfigs); - - configObjectList.add(configObject); - } finally { - readWriteLock.writeLock().unlock(); - } - } catch (InterruptedException e) { - log.error("registerConfig lock error"); - } - return this; - } - - /** - * register config properties - * - * @return the current Configuration object - */ - public Configuration registerConfig(Properties extProperties) { - if (extProperties == null) { - return this; - } - - try { - readWriteLock.writeLock().lockInterruptibly(); - - try { - merge(extProperties, this.allConfigs); - } finally { - readWriteLock.writeLock().unlock(); - } - } catch (InterruptedException e) { - log.error("register lock error. {}" + extProperties); - } - - return this; - } - - /** - * The store path will be gotten from the field of object. - * - * @throws java.lang.RuntimeException if the field of object is not exist. - */ - public void setStorePathFromConfig(Object object, String fieldName) { - assert object != null; - - try { - readWriteLock.writeLock().lockInterruptibly(); - - try { - this.storePathFromConfig = true; - this.storePathObject = object; - // check - this.storePathField = object.getClass().getDeclaredField(fieldName); - assert this.storePathField != null - && !Modifier.isStatic(this.storePathField.getModifiers()); - this.storePathField.setAccessible(true); - } catch (NoSuchFieldException e) { - throw new RuntimeException(e); - } finally { - readWriteLock.writeLock().unlock(); - } - } catch (InterruptedException e) { - log.error("setStorePathFromConfig lock error"); - } - } - - private String getStorePath() { - String realStorePath = null; - try { - readWriteLock.readLock().lockInterruptibly(); - - try { - realStorePath = this.storePath; - - if (this.storePathFromConfig) { - try { - realStorePath = (String) storePathField.get(this.storePathObject); - } catch (IllegalAccessException e) { - log.error("getStorePath error, ", e); - } - } - } finally { - readWriteLock.readLock().unlock(); - } - } catch (InterruptedException e) { - log.error("getStorePath lock error"); - } - - return realStorePath; - } - - public void setStorePath(final String storePath) { - this.storePath = storePath; - } - - public void update(Properties properties) { - try { - readWriteLock.writeLock().lockInterruptibly(); - - try { - // the property must be exist when update - mergeIfExist(properties, this.allConfigs); - - for (Object configObject : configObjectList) { - // not allConfigs to update... - MixAll.properties2Object(properties, configObject); - } - - this.dataVersion.nextVersion(); - - } finally { - readWriteLock.writeLock().unlock(); - } - } catch (InterruptedException e) { - log.error("update lock error, {}", properties); - return; - } - - persist(); - } - - public void persist() { - try { - readWriteLock.readLock().lockInterruptibly(); - - try { - String allConfigs = getAllConfigsInternal(); - - MixAll.string2File(allConfigs, getStorePath()); - } catch (IOException e) { - log.error("persist string2File error, ", e); - } finally { - readWriteLock.readLock().unlock(); - } - } catch (InterruptedException e) { - log.error("persist lock error"); - } - } - - public String getAllConfigsFormatString() { - try { - readWriteLock.readLock().lockInterruptibly(); - - try { - - return getAllConfigsInternal(); - - } finally { - readWriteLock.readLock().unlock(); - } - } catch (InterruptedException e) { - log.error("getAllConfigsFormatString lock error"); - } - - return null; - } - - public String getDataVersionJson() { - return this.dataVersion.toJson(); - } - - public Properties getAllConfigs() { - try { - readWriteLock.readLock().lockInterruptibly(); - - try { - - return this.allConfigs; - - } finally { - readWriteLock.readLock().unlock(); - } - } catch (InterruptedException e) { - log.error("getAllConfigs lock error"); - } - - return null; - } - - private String getAllConfigsInternal() { - StringBuilder stringBuilder = new StringBuilder(); - - // reload from config object ? - for (Object configObject : this.configObjectList) { - Properties properties = MixAll.object2Properties(configObject); - if (properties != null) { - merge(properties, this.allConfigs); - } else { - log.warn("getAllConfigsInternal object2Properties is null, {}", configObject.getClass()); - } - } - - { - stringBuilder.append(MixAll.properties2String(this.allConfigs)); - } - - return stringBuilder.toString(); - } - - private void merge(Properties from, Properties to) { - for (Object key : from.keySet()) { - Object fromObj = from.get(key), toObj = to.get(key); - if (toObj != null && !toObj.equals(fromObj)) { - log.info("Replace, key: {}, value: {} -> {}", key, toObj, fromObj); - } - to.put(key, fromObj); - } - } - - private void mergeIfExist(Properties from, Properties to) { - for (Object key : from.keySet()) { - if (!to.containsKey(key)) { - continue; - } - - Object fromObj = from.get(key), toObj = to.get(key); - if (toObj != null && !toObj.equals(fromObj)) { - log.info("Replace, key: {}, value: {} -> {}", key, toObj, fromObj); - } - to.put(key, fromObj); - } - } - -} diff --git a/common/src/main/java/org/apache/rocketmq/common/ControllerConfig.java b/common/src/main/java/org/apache/rocketmq/common/ControllerConfig.java new file mode 100644 index 00000000000..85606fc5ee4 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/ControllerConfig.java @@ -0,0 +1,343 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common; + +import java.io.File; +import java.util.Arrays; +import org.apache.rocketmq.common.metrics.MetricsExporterType; + +public class ControllerConfig { + private String rocketmqHome = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, System.getenv(MixAll.ROCKETMQ_HOME_ENV)); + private String configStorePath = System.getProperty("user.home") + File.separator + "controller" + File.separator + "controller.properties"; + public static final String DLEDGER_CONTROLLER = "DLedger"; + public static final String JRAFT_CONTROLLER = "jRaft"; + + private JraftConfig jraftConfig = new JraftConfig(); + + private String controllerType = DLEDGER_CONTROLLER; + /** + * Interval of periodic scanning for non-active broker; + * Unit: millisecond + */ + private long scanNotActiveBrokerInterval = 5 * 1000; + + /** + * Indicates the nums of thread to handle broker or operation requests, like REGISTER_BROKER. + */ + private int controllerThreadPoolNums = 16; + + /** + * Indicates the capacity of queue to hold client requests. + */ + private int controllerRequestThreadPoolQueueCapacity = 50000; + + private String controllerDLegerGroup; + private String controllerDLegerPeers; + private String controllerDLegerSelfId; + private int mappedFileSize = 1024 * 1024 * 1024; + private String controllerStorePath = ""; + + /** + * Max retry count for electing master when failed because of network or system error. + */ + private int electMasterMaxRetryCount = 3; + + + /** + * Whether the controller can elect a master which is not in the syncStateSet. + */ + private boolean enableElectUncleanMaster = false; + + /** + * Whether process read event + */ + private boolean isProcessReadEvent = false; + + /** + * Whether notify broker when its role changed + */ + private volatile boolean notifyBrokerRoleChanged = true; + /** + * Interval of periodic scanning for non-active master in each broker-set; + * Unit: millisecond + */ + private long scanInactiveMasterInterval = 5 * 1000; + + private MetricsExporterType metricsExporterType = MetricsExporterType.DISABLE; + + private String metricsGrpcExporterTarget = ""; + private String metricsGrpcExporterHeader = ""; + private long metricGrpcExporterTimeOutInMills = 3 * 1000; + private long metricGrpcExporterIntervalInMills = 60 * 1000; + private long metricLoggingExporterIntervalInMills = 10 * 1000; + + private int metricsPromExporterPort = 5557; + private String metricsPromExporterHost = ""; + + // Label pairs in CSV. Each label follows pattern of Key:Value. eg: instance_id:xxx,uid:xxx + private String metricsLabel = ""; + + private boolean metricsInDelta = false; + + /** + * Config in this black list will be not allowed to update by command. + * Try to update this config black list by restart process. + * Try to update configures in black list by restart process. + */ + private String configBlackList = "configBlackList;configStorePath"; + + public String getConfigBlackList() { + return configBlackList; + } + + public void setConfigBlackList(String configBlackList) { + this.configBlackList = configBlackList; + } + + public String getRocketmqHome() { + return rocketmqHome; + } + + public void setRocketmqHome(String rocketmqHome) { + this.rocketmqHome = rocketmqHome; + } + + public String getConfigStorePath() { + return configStorePath; + } + + public void setConfigStorePath(String configStorePath) { + this.configStorePath = configStorePath; + } + + public long getScanNotActiveBrokerInterval() { + return scanNotActiveBrokerInterval; + } + + public void setScanNotActiveBrokerInterval(long scanNotActiveBrokerInterval) { + this.scanNotActiveBrokerInterval = scanNotActiveBrokerInterval; + } + + public int getControllerThreadPoolNums() { + return controllerThreadPoolNums; + } + + public void setControllerThreadPoolNums(int controllerThreadPoolNums) { + this.controllerThreadPoolNums = controllerThreadPoolNums; + } + + public int getControllerRequestThreadPoolQueueCapacity() { + return controllerRequestThreadPoolQueueCapacity; + } + + public void setControllerRequestThreadPoolQueueCapacity(int controllerRequestThreadPoolQueueCapacity) { + this.controllerRequestThreadPoolQueueCapacity = controllerRequestThreadPoolQueueCapacity; + } + + public String getControllerDLegerGroup() { + return controllerDLegerGroup; + } + + public void setControllerDLegerGroup(String controllerDLegerGroup) { + this.controllerDLegerGroup = controllerDLegerGroup; + } + + public String getControllerDLegerPeers() { + return controllerDLegerPeers; + } + + public void setControllerDLegerPeers(String controllerDLegerPeers) { + this.controllerDLegerPeers = controllerDLegerPeers; + } + + public String getControllerDLegerSelfId() { + return controllerDLegerSelfId; + } + + public void setControllerDLegerSelfId(String controllerDLegerSelfId) { + this.controllerDLegerSelfId = controllerDLegerSelfId; + } + + public int getMappedFileSize() { + return mappedFileSize; + } + + public void setMappedFileSize(int mappedFileSize) { + this.mappedFileSize = mappedFileSize; + } + + public String getControllerStorePath() { + if (controllerStorePath.isEmpty()) { + controllerStorePath = System.getProperty("user.home") + File.separator + controllerType + "Controller"; + } + return controllerStorePath; + } + + public void setControllerStorePath(String controllerStorePath) { + this.controllerStorePath = controllerStorePath; + } + + public boolean isEnableElectUncleanMaster() { + return enableElectUncleanMaster; + } + + public void setEnableElectUncleanMaster(boolean enableElectUncleanMaster) { + this.enableElectUncleanMaster = enableElectUncleanMaster; + } + + public boolean isProcessReadEvent() { + return isProcessReadEvent; + } + + public void setProcessReadEvent(boolean processReadEvent) { + isProcessReadEvent = processReadEvent; + } + + public boolean isNotifyBrokerRoleChanged() { + return notifyBrokerRoleChanged; + } + + public void setNotifyBrokerRoleChanged(boolean notifyBrokerRoleChanged) { + this.notifyBrokerRoleChanged = notifyBrokerRoleChanged; + } + + public long getScanInactiveMasterInterval() { + return scanInactiveMasterInterval; + } + + public void setScanInactiveMasterInterval(long scanInactiveMasterInterval) { + this.scanInactiveMasterInterval = scanInactiveMasterInterval; + } + + public String getDLedgerAddress() { + return Arrays.stream(this.controllerDLegerPeers.split(";")) + .filter(x -> this.controllerDLegerSelfId.equals(x.split("-")[0])) + .map(x -> x.split("-")[1]).findFirst().get(); + } + + public MetricsExporterType getMetricsExporterType() { + return metricsExporterType; + } + + public void setMetricsExporterType(MetricsExporterType metricsExporterType) { + this.metricsExporterType = metricsExporterType; + } + + public void setMetricsExporterType(int metricsExporterType) { + this.metricsExporterType = MetricsExporterType.valueOf(metricsExporterType); + } + + public void setMetricsExporterType(String metricsExporterType) { + this.metricsExporterType = MetricsExporterType.valueOf(metricsExporterType); + } + + public String getMetricsGrpcExporterTarget() { + return metricsGrpcExporterTarget; + } + + public void setMetricsGrpcExporterTarget(String metricsGrpcExporterTarget) { + this.metricsGrpcExporterTarget = metricsGrpcExporterTarget; + } + + public String getMetricsGrpcExporterHeader() { + return metricsGrpcExporterHeader; + } + + public void setMetricsGrpcExporterHeader(String metricsGrpcExporterHeader) { + this.metricsGrpcExporterHeader = metricsGrpcExporterHeader; + } + + public long getMetricGrpcExporterTimeOutInMills() { + return metricGrpcExporterTimeOutInMills; + } + + public void setMetricGrpcExporterTimeOutInMills(long metricGrpcExporterTimeOutInMills) { + this.metricGrpcExporterTimeOutInMills = metricGrpcExporterTimeOutInMills; + } + + public long getMetricGrpcExporterIntervalInMills() { + return metricGrpcExporterIntervalInMills; + } + + public void setMetricGrpcExporterIntervalInMills(long metricGrpcExporterIntervalInMills) { + this.metricGrpcExporterIntervalInMills = metricGrpcExporterIntervalInMills; + } + + public long getMetricLoggingExporterIntervalInMills() { + return metricLoggingExporterIntervalInMills; + } + + public void setMetricLoggingExporterIntervalInMills(long metricLoggingExporterIntervalInMills) { + this.metricLoggingExporterIntervalInMills = metricLoggingExporterIntervalInMills; + } + + public int getMetricsPromExporterPort() { + return metricsPromExporterPort; + } + + public void setMetricsPromExporterPort(int metricsPromExporterPort) { + this.metricsPromExporterPort = metricsPromExporterPort; + } + + public String getMetricsPromExporterHost() { + return metricsPromExporterHost; + } + + public void setMetricsPromExporterHost(String metricsPromExporterHost) { + this.metricsPromExporterHost = metricsPromExporterHost; + } + + public String getMetricsLabel() { + return metricsLabel; + } + + public void setMetricsLabel(String metricsLabel) { + this.metricsLabel = metricsLabel; + } + + public boolean isMetricsInDelta() { + return metricsInDelta; + } + + public void setMetricsInDelta(boolean metricsInDelta) { + this.metricsInDelta = metricsInDelta; + } + + public String getControllerType() { + return controllerType; + } + + public void setControllerType(String controllerType) { + this.controllerType = controllerType; + } + + public JraftConfig getJraftConfig() { + return jraftConfig; + } + + public void setJraftConfig(JraftConfig jraftConfig) { + this.jraftConfig = jraftConfig; + } + + public int getElectMasterMaxRetryCount() { + return this.electMasterMaxRetryCount; + } + + public void setElectMasterMaxRetryCount(int electMasterMaxRetryCount) { + this.electMasterMaxRetryCount = electMasterMaxRetryCount; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/CountDownLatch2.java b/common/src/main/java/org/apache/rocketmq/common/CountDownLatch2.java index de1d548932c..4e43e5ce3db 100644 --- a/common/src/main/java/org/apache/rocketmq/common/CountDownLatch2.java +++ b/common/src/main/java/org/apache/rocketmq/common/CountDownLatch2.java @@ -21,7 +21,7 @@ import java.util.concurrent.locks.AbstractQueuedSynchronizer; /** - * Add reset feature for @see java.util.concurrent.CountDownLatch2 + * Add reset feature for @see java.util.concurrent.CountDownLatch */ public class CountDownLatch2 { private final Sync sync; @@ -172,10 +172,12 @@ int getCount() { return getState(); } + @Override protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; } + @Override protected boolean tryReleaseShared(int releases) { // Decrement count; signal when transition to zero for (; ; ) { diff --git a/common/src/main/java/org/apache/rocketmq/common/DataVersion.java b/common/src/main/java/org/apache/rocketmq/common/DataVersion.java deleted file mode 100644 index 71b00fdd72d..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/DataVersion.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.common; - -import java.util.concurrent.atomic.AtomicLong; -import org.apache.rocketmq.remoting.protocol.RemotingSerializable; - -public class DataVersion extends RemotingSerializable { - private long timestamp = System.currentTimeMillis(); - private AtomicLong counter = new AtomicLong(0); - - public void assignNewOne(final DataVersion dataVersion) { - this.timestamp = dataVersion.timestamp; - this.counter.set(dataVersion.counter.get()); - } - - public void nextVersion() { - this.timestamp = System.currentTimeMillis(); - this.counter.incrementAndGet(); - } - - public long getTimestamp() { - return timestamp; - } - - public void setTimestamp(long timestamp) { - this.timestamp = timestamp; - } - - public AtomicLong getCounter() { - return counter; - } - - public void setCounter(AtomicLong counter) { - this.counter = counter; - } - - @Override - public boolean equals(final Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - - final DataVersion that = (DataVersion) o; - - if (timestamp != that.timestamp) { - return false; - } - - if (counter != null && that.counter != null) { - return counter.longValue() == that.counter.longValue(); - } - - return (null == counter) && (null == that.counter); - } - - @Override - public int hashCode() { - int result = (int) (timestamp ^ (timestamp >>> 32)); - if (null != counter) { - long l = counter.get(); - result = 31 * result + (int) (l ^ (l >>> 32)); - } - return result; - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/JraftConfig.java b/common/src/main/java/org/apache/rocketmq/common/JraftConfig.java new file mode 100644 index 00000000000..072d0caf241 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/JraftConfig.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common; + +public class JraftConfig { + private int jRaftElectionTimeoutMs = 1000; + + private int jRaftScanWaitTimeoutMs = 1000; + private int jRaftSnapshotIntervalSecs = 3600; + private String jRaftGroupId = "jRaft-Controller"; + private String jRaftServerId = "localhost:9880"; + private String jRaftInitConf = "localhost:9880,localhost:9881,localhost:9882"; + private String jRaftControllerRPCAddr = "localhost:9770,localhost:9771,localhost:9772"; + + public int getjRaftElectionTimeoutMs() { + return jRaftElectionTimeoutMs; + } + + public void setjRaftElectionTimeoutMs(int jRaftElectionTimeoutMs) { + this.jRaftElectionTimeoutMs = jRaftElectionTimeoutMs; + } + + public int getjRaftSnapshotIntervalSecs() { + return jRaftSnapshotIntervalSecs; + } + + public void setjRaftSnapshotIntervalSecs(int jRaftSnapshotIntervalSecs) { + this.jRaftSnapshotIntervalSecs = jRaftSnapshotIntervalSecs; + } + + public String getjRaftGroupId() { + return jRaftGroupId; + } + + public void setjRaftGroupId(String jRaftGroupId) { + this.jRaftGroupId = jRaftGroupId; + } + + public String getjRaftServerId() { + return jRaftServerId; + } + + public void setjRaftServerId(String jRaftServerId) { + this.jRaftServerId = jRaftServerId; + } + + public String getjRaftInitConf() { + return jRaftInitConf; + } + + public void setjRaftInitConf(String jRaftInitConf) { + this.jRaftInitConf = jRaftInitConf; + } + + public String getjRaftControllerRPCAddr() { + return jRaftControllerRPCAddr; + } + + public void setjRaftControllerRPCAddr(String jRaftControllerRPCAddr) { + this.jRaftControllerRPCAddr = jRaftControllerRPCAddr; + } + + public String getjRaftAddress() { + return this.jRaftServerId; + } + + public int getjRaftScanWaitTimeoutMs() { + return jRaftScanWaitTimeoutMs; + } + + public void setjRaftScanWaitTimeoutMs(int jRaftScanWaitTimeoutMs) { + this.jRaftScanWaitTimeoutMs = jRaftScanWaitTimeoutMs; + } +} \ No newline at end of file diff --git a/common/src/main/java/org/apache/rocketmq/common/KeyBuilder.java b/common/src/main/java/org/apache/rocketmq/common/KeyBuilder.java new file mode 100644 index 00000000000..910a73b7137 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/KeyBuilder.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common; + +public class KeyBuilder { + public static final int POP_ORDER_REVIVE_QUEUE = 999; + private static final char POP_RETRY_SEPARATOR_V1 = '_'; + private static final char POP_RETRY_SEPARATOR_V2 = '+'; + private static final String POP_RETRY_REGEX_SEPARATOR_V2 = "\\+"; + + public static String buildPopRetryTopic(String topic, String cid, boolean enableRetryV2) { + if (enableRetryV2) { + return buildPopRetryTopicV2(topic, cid); + } + return buildPopRetryTopicV1(topic, cid); + } + + public static String buildPopRetryTopic(String topic, String cid) { + return MixAll.RETRY_GROUP_TOPIC_PREFIX + cid + POP_RETRY_SEPARATOR_V1 + topic; + } + + public static String buildPopRetryTopicV2(String topic, String cid) { + return MixAll.RETRY_GROUP_TOPIC_PREFIX + cid + POP_RETRY_SEPARATOR_V2 + topic; + } + + public static String buildPopRetryTopicV1(String topic, String cid) { + return MixAll.RETRY_GROUP_TOPIC_PREFIX + cid + POP_RETRY_SEPARATOR_V1 + topic; + } + + public static String parseNormalTopic(String topic, String cid) { + if (topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + if (topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX + cid + POP_RETRY_SEPARATOR_V2)) { + return topic.substring((MixAll.RETRY_GROUP_TOPIC_PREFIX + cid + POP_RETRY_SEPARATOR_V2).length()); + } + return topic.substring((MixAll.RETRY_GROUP_TOPIC_PREFIX + cid + POP_RETRY_SEPARATOR_V1).length()); + } else { + return topic; + } + } + + public static String parseNormalTopic(String retryTopic) { + if (isPopRetryTopicV2(retryTopic)) { + String[] result = retryTopic.split(POP_RETRY_REGEX_SEPARATOR_V2); + if (result.length == 2) { + return result[1]; + } + } + return retryTopic; + } + + public static String parseGroup(String retryTopic) { + if (isPopRetryTopicV2(retryTopic)) { + String[] result = retryTopic.split(POP_RETRY_REGEX_SEPARATOR_V2); + if (result.length == 2) { + return result[0].substring(MixAll.RETRY_GROUP_TOPIC_PREFIX.length()); + } + } + return retryTopic.substring(MixAll.RETRY_GROUP_TOPIC_PREFIX.length()); + } + + public static String buildPollingKey(String topic, String cid, int queueId) { + return topic + PopAckConstants.SPLIT + cid + PopAckConstants.SPLIT + queueId; + } + + public static boolean isPopRetryTopicV2(String retryTopic) { + return retryTopic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX) && retryTopic.contains(String.valueOf(POP_RETRY_SEPARATOR_V2)); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/LifecycleAwareServiceThread.java b/common/src/main/java/org/apache/rocketmq/common/LifecycleAwareServiceThread.java new file mode 100644 index 00000000000..a4fe5da812f --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/LifecycleAwareServiceThread.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +public abstract class LifecycleAwareServiceThread extends ServiceThread { + + private final AtomicBoolean started = new AtomicBoolean(false); + + @Override + public void run() { + started.set(true); + synchronized (started) { + started.notifyAll(); + } + + run0(); + } + + public abstract void run0(); + + /** + * Take spurious wakeup into account. + * + * @param timeout amount of time in milliseconds + * @throws InterruptedException if interrupted + */ + public void awaitStarted(long timeout) throws InterruptedException { + long expire = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeout); + synchronized (started) { + while (!started.get()) { + long duration = expire - System.nanoTime(); + if (duration < TimeUnit.MILLISECONDS.toNanos(1)) { + break; + } + started.wait(TimeUnit.NANOSECONDS.toMillis(duration)); + } + } + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/LockCallback.java b/common/src/main/java/org/apache/rocketmq/common/LockCallback.java new file mode 100644 index 00000000000..9abf36755dc --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/LockCallback.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common; + +import java.util.Set; +import org.apache.rocketmq.common.message.MessageQueue; + +public interface LockCallback { + void onSuccess(final Set lockOKMQSet); + + void onException(final Throwable e); +} diff --git a/common/src/main/java/org/apache/rocketmq/common/MQVersion.java b/common/src/main/java/org/apache/rocketmq/common/MQVersion.java index 7e9322545b7..5eb9dc06ac9 100644 --- a/common/src/main/java/org/apache/rocketmq/common/MQVersion.java +++ b/common/src/main/java/org/apache/rocketmq/common/MQVersion.java @@ -18,7 +18,7 @@ public class MQVersion { - public static final int CURRENT_VERSION = Version.V4_2_0.ordinal(); + public static final int CURRENT_VERSION = Version.V5_2_0.ordinal(); public static String getVersionDesc(int value) { int length = Version.values().length; diff --git a/common/src/main/java/org/apache/rocketmq/common/MixAll.java b/common/src/main/java/org/apache/rocketmq/common/MixAll.java index 3a005e6a545..47b4aac34a4 100644 --- a/common/src/main/java/org/apache/rocketmq/common/MixAll.java +++ b/common/src/main/java/org/apache/rocketmq/common/MixAll.java @@ -19,7 +19,6 @@ import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; -import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.lang.annotation.Annotation; @@ -32,62 +31,111 @@ import java.net.SocketException; import java.net.URL; import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.Set; +import java.util.TreeMap; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Predicate; + +import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.common.annotation.ImportantField; import org.apache.rocketmq.common.constant.LoggerName; import org.apache.rocketmq.common.help.FAQUrl; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.common.utils.IOTinyUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; public class MixAll { - private static final Logger log = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); - public static final String ROCKETMQ_HOME_ENV = "ROCKETMQ_HOME"; public static final String ROCKETMQ_HOME_PROPERTY = "rocketmq.home.dir"; public static final String NAMESRV_ADDR_ENV = "NAMESRV_ADDR"; public static final String NAMESRV_ADDR_PROPERTY = "rocketmq.namesrv.addr"; + public static final String MESSAGE_COMPRESS_TYPE = "rocketmq.message.compressType"; public static final String MESSAGE_COMPRESS_LEVEL = "rocketmq.message.compressLevel"; public static final String DEFAULT_NAMESRV_ADDR_LOOKUP = "jmenv.tbsite.net"; public static final String WS_DOMAIN_NAME = System.getProperty("rocketmq.namesrv.domain", DEFAULT_NAMESRV_ADDR_LOOKUP); public static final String WS_DOMAIN_SUBGROUP = System.getProperty("rocketmq.namesrv.domain.subgroup", "nsaddr"); - //http://jmenv.tbsite.net:8080/rocketmq/nsaddr - //public static final String WS_ADDR = "http://" + WS_DOMAIN_NAME + ":8080/rocketmq/" + WS_DOMAIN_SUBGROUP; - public static final String DEFAULT_TOPIC = "TBW102"; - public static final String BENCHMARK_TOPIC = "BenchmarkTest"; public static final String DEFAULT_PRODUCER_GROUP = "DEFAULT_PRODUCER"; public static final String DEFAULT_CONSUMER_GROUP = "DEFAULT_CONSUMER"; public static final String TOOLS_CONSUMER_GROUP = "TOOLS_CONSUMER"; + public static final String SCHEDULE_CONSUMER_GROUP = "SCHEDULE_CONSUMER"; public static final String FILTERSRV_CONSUMER_GROUP = "FILTERSRV_CONSUMER"; public static final String MONITOR_CONSUMER_GROUP = "__MONITOR_CONSUMER"; public static final String CLIENT_INNER_PRODUCER_GROUP = "CLIENT_INNER_PRODUCER"; public static final String SELF_TEST_PRODUCER_GROUP = "SELF_TEST_P_GROUP"; public static final String SELF_TEST_CONSUMER_GROUP = "SELF_TEST_C_GROUP"; - public static final String SELF_TEST_TOPIC = "SELF_TEST_TOPIC"; - public static final String OFFSET_MOVED_EVENT = "OFFSET_MOVED_EVENT"; public static final String ONS_HTTP_PROXY_GROUP = "CID_ONS-HTTP-PROXY"; public static final String CID_ONSAPI_PERMISSION_GROUP = "CID_ONSAPI_PERMISSION"; public static final String CID_ONSAPI_OWNER_GROUP = "CID_ONSAPI_OWNER"; public static final String CID_ONSAPI_PULL_GROUP = "CID_ONSAPI_PULL"; public static final String CID_RMQ_SYS_PREFIX = "CID_RMQ_SYS_"; - + public static final String IS_SUPPORT_HEART_BEAT_V2 = "IS_SUPPORT_HEART_BEAT_V2"; + public static final String IS_SUB_CHANGE = "IS_SUB_CHANGE"; public static final List LOCAL_INET_ADDRESS = getLocalInetAddress(); public static final String LOCALHOST = localhost(); public static final String DEFAULT_CHARSET = "UTF-8"; public static final long MASTER_ID = 0L; + public static final long FIRST_SLAVE_ID = 1L; + + public static final long FIRST_BROKER_CONTROLLER_ID = 1L; public static final long CURRENT_JVM_PID = getPID(); + public final static int UNIT_PRE_SIZE_FOR_MSG = 28; + public final static int ALL_ACK_IN_SYNC_STATE_SET = -1; public static final String RETRY_GROUP_TOPIC_PREFIX = "%RETRY%"; - public static final String DLQ_GROUP_TOPIC_PREFIX = "%DLQ%"; - public static final String SYSTEM_TOPIC_PREFIX = "rmq_sys_"; + public static final String REPLY_TOPIC_POSTFIX = "REPLY_TOPIC"; public static final String UNIQUE_MSG_QUERY_FLAG = "_UNIQUE_KEY_QUERY"; public static final String DEFAULT_TRACE_REGION_ID = "DefaultRegion"; public static final String CONSUME_CONTEXT_TYPE = "ConsumeContextType"; + public static final String CID_SYS_RMQ_TRANS = "CID_RMQ_SYS_TRANS"; + public static final String ACL_CONF_TOOLS_FILE = "/conf/tools.yml"; + public static final String REPLY_MESSAGE_FLAG = "reply"; + public static final String LMQ_PREFIX = "%LMQ%"; + public static final long LMQ_QUEUE_ID = 0; + public static final String MULTI_DISPATCH_QUEUE_SPLITTER = ","; + public static final String REQ_T = "ReqT"; + public static final String ROCKETMQ_ZONE_ENV = "ROCKETMQ_ZONE"; + public static final String ROCKETMQ_ZONE_PROPERTY = "rocketmq.zone"; + public static final String ROCKETMQ_ZONE_MODE_ENV = "ROCKETMQ_ZONE_MODE"; + public static final String ROCKETMQ_ZONE_MODE_PROPERTY = "rocketmq.zone.mode"; + public static final String ZONE_NAME = "__ZONE_NAME"; + public static final String ZONE_MODE = "__ZONE_MODE"; + public final static String RPC_REQUEST_HEADER_NAMESPACED_FIELD = "nsd"; + public final static String RPC_REQUEST_HEADER_NAMESPACE_FIELD = "ns"; + + private static final Logger log = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); + public static final String LOGICAL_QUEUE_MOCK_BROKER_PREFIX = "__syslo__"; + public static final String METADATA_SCOPE_GLOBAL = "__global__"; + public static final String LOGICAL_QUEUE_MOCK_BROKER_NAME_NOT_EXIST = "__syslo__none__"; + public static final String MULTI_PATH_SPLITTER = System.getProperty("rocketmq.broker.multiPathSplitter", ","); + + private static final String OS = System.getProperty("os.name").toLowerCase(); + + public static boolean isWindows() { + return OS.indexOf("win") >= 0; + } + + public static boolean isMac() { + return OS.indexOf("mac") >= 0; + } + + public static boolean isUnix() { + return OS.indexOf("nix") >= 0 + || OS.indexOf("nux") >= 0 + || OS.indexOf("aix") > 0; + } + + public static boolean isSolaris() { + return OS.indexOf("sunos") >= 0; + } public static String getWSAddr() { String wsDomainName = System.getProperty("rocketmq.namesrv.domain", DEFAULT_NAMESRV_ADDR_LOOKUP); @@ -103,12 +151,12 @@ public static String getRetryTopic(final String consumerGroup) { return RETRY_GROUP_TOPIC_PREFIX + consumerGroup; } - public static boolean isSysConsumerGroup(final String consumerGroup) { - return consumerGroup.startsWith(CID_RMQ_SYS_PREFIX); + public static String getReplyTopic(final String clusterName) { + return clusterName + "_" + REPLY_TOPIC_POSTFIX; } - public static boolean isSystemTopic(final String topic) { - return topic.startsWith(SYSTEM_TOPIC_PREFIX); + public static boolean isSysConsumerGroup(final String consumerGroup) { + return consumerGroup.startsWith(CID_RMQ_SYS_PREFIX); } public static String getDLQTopic(final String consumerGroup) { @@ -117,8 +165,10 @@ public static String getDLQTopic(final String consumerGroup) { public static String brokerVIPChannel(final boolean isChange, final String brokerAddr) { if (isChange) { - String[] ipAndPort = brokerAddr.split(":"); - String brokerAddrNew = ipAndPort[0] + ":" + (Integer.parseInt(ipAndPort[1]) - 2); + int split = brokerAddr.lastIndexOf(":"); + String ip = brokerAddr.substring(0, split); + String port = brokerAddr.substring(split + 1); + String brokerAddrNew = ip + ":" + (Integer.parseInt(port) - 2); return brokerAddrNew; } else { return brokerAddr; @@ -127,7 +177,7 @@ public static String brokerVIPChannel(final boolean isChange, final String broke public static long getPID() { String processName = java.lang.management.ManagementFactory.getRuntimeMXBean().getName(); - if (processName != null && processName.length() > 0) { + if (StringUtils.isNotEmpty(processName)) { try { return Long.parseLong(processName.split("@")[0]); } catch (Exception e) { @@ -138,10 +188,7 @@ public static long getPID() { return 0; } - public static void string2File(final String str, final String fileName) throws IOException { - - String tmpFile = fileName + ".tmp"; - string2FileNotSafe(str, tmpFile); + public static synchronized void string2File(final String str, final String fileName) throws IOException { String bakFile = fileName + ".bak"; String prevContent = file2String(fileName); @@ -149,11 +196,7 @@ public static void string2File(final String str, final String fileName) throws I string2FileNotSafe(prevContent, bakFile); } - File file = new File(fileName); - file.delete(); - - file = new File(tmpFile); - file.renameTo(new File(fileName)); + string2FileNotSafe(str, fileName); } public static void string2FileNotSafe(final String str, final String fileName) throws IOException { @@ -162,18 +205,7 @@ public static void string2FileNotSafe(final String str, final String fileName) t if (fileParent != null) { fileParent.mkdirs(); } - FileWriter fileWriter = null; - - try { - fileWriter = new FileWriter(file); - fileWriter.write(str); - } catch (IOException e) { - throw e; - } finally { - if (fileWriter != null) { - fileWriter.close(); - } - } + IOTinyUtils.writeStringToFile(file, str, "UTF-8"); } public static String file2String(final String fileName) throws IOException { @@ -186,19 +218,13 @@ public static String file2String(final File file) throws IOException { byte[] data = new byte[(int) file.length()]; boolean result; - FileInputStream inputStream = null; - try { - inputStream = new FileInputStream(file); + try (FileInputStream inputStream = new FileInputStream(file)) { int len = inputStream.read(data); result = len == data.length; - } finally { - if (inputStream != null) { - inputStream.close(); - } } if (result) { - return new String(data); + return new String(data, "UTF-8"); } } return null; @@ -213,7 +239,7 @@ public static String file2String(final URL url) { int len = in.available(); byte[] data = new byte[len]; in.read(data, 0, len); - return new String(data, "UTF-8"); + return new String(data, StandardCharsets.UTF_8); } catch (Exception ignored) { } finally { if (null != in) { @@ -238,6 +264,13 @@ public static void printObjectProperties(final Logger logger, final Object objec if (!Modifier.isStatic(field.getModifiers())) { String name = field.getName(); if (!name.startsWith("this")) { + if (onlyImportantField) { + Annotation annotation = field.getAnnotation(ImportantField.class); + if (null == annotation) { + continue; + } + } + Object value = null; try { field.setAccessible(true); @@ -249,16 +282,8 @@ public static void printObjectProperties(final Logger logger, final Object objec log.error("Failed to obtain object properties", e); } - if (onlyImportantField) { - Annotation annotation = field.getAnnotation(ImportantField.class); - if (null == annotation) { - continue; - } - } - if (logger != null) { logger.info(name + "=" + value); - } else { } } } @@ -266,8 +291,13 @@ public static void printObjectProperties(final Logger logger, final Object objec } public static String properties2String(final Properties properties) { + return properties2String(properties, false); + } + + public static String properties2String(final Properties properties, final boolean isSort) { StringBuilder sb = new StringBuilder(); - for (Map.Entry entry : properties.entrySet()) { + Set> entrySet = isSort ? new TreeMap<>(properties).entrySet() : properties.entrySet(); + for (Map.Entry entry : entrySet) { if (entry.getValue() != null) { sb.append(entry.getKey().toString() + "=" + entry.getValue().toString() + "\n"); } @@ -291,24 +321,31 @@ public static Properties string2Properties(final String str) { public static Properties object2Properties(final Object object) { Properties properties = new Properties(); - Field[] fields = object.getClass().getDeclaredFields(); - for (Field field : fields) { - if (!Modifier.isStatic(field.getModifiers())) { - String name = field.getName(); - if (!name.startsWith("this")) { - Object value = null; - try { - field.setAccessible(true); - value = field.get(object); - } catch (IllegalAccessException e) { - log.error("Failed to handle properties", e); - } + Class objectClass = object.getClass(); + while (true) { + Field[] fields = objectClass.getDeclaredFields(); + for (Field field : fields) { + if (!Modifier.isStatic(field.getModifiers())) { + String name = field.getName(); + if (!name.startsWith("this")) { + Object value = null; + try { + field.setAccessible(true); + value = field.get(object); + } catch (IllegalAccessException e) { + log.error("Failed to handle properties", e); + } - if (value != null) { - properties.setProperty(name, value.toString()); + if (value != null) { + properties.setProperty(name, value.toString()); + } } } } + if (objectClass == Object.class || objectClass.getSuperclass() == Object.class) { + break; + } + objectClass = objectClass.getSuperclass(); } return properties; @@ -341,6 +378,7 @@ public static void properties2Object(final Properties p, final Object object) { } else if (cn.equals("float") || cn.equals("Float")) { arg = Float.parseFloat(property); } else if (cn.equals("String")) { + property = property.trim(); arg = property; } else { continue; @@ -358,8 +396,12 @@ public static boolean isPropertiesEqual(final Properties p1, final Properties p2 return p1.equals(p2); } + public static boolean isPropertyValid(Properties props, String key, Predicate validator) { + return validator.test(props.getProperty(key)); + } + public static List getLocalInetAddress() { - List inetAddressList = new ArrayList(); + List inetAddressList = new ArrayList<>(); try { Enumeration enumeration = NetworkInterface.getNetworkInterfaces(); while (enumeration.hasMoreElements()) { @@ -394,7 +436,7 @@ private static String localhost() { //Reverse logic comparing to RemotingUtil method, consider refactor in RocketMQ 5.0 public static String getLocalhostByNetworkInterface() throws SocketException { - List candidatesHost = new ArrayList(); + List candidatesHost = new ArrayList<>(); Enumeration enumeration = NetworkInterface.getNetworkInterfaces(); while (enumeration.hasMoreElements()) { @@ -409,7 +451,7 @@ public static String getLocalhostByNetworkInterface() throws SocketException { if (address.isLoopbackAddress()) { continue; } - //ip4 highter priority + //ip4 higher priority if (address instanceof Inet6Address) { candidatesHost.add(address.getHostAddress()); continue; @@ -421,7 +463,9 @@ public static String getLocalhostByNetworkInterface() throws SocketException { if (!candidatesHost.isEmpty()) { return candidatesHost.get(0); } - return null; + + // Fallback to loopback + return localhost(); } public static boolean compareAndIncreaseOnly(final AtomicLong target, final long value) { @@ -446,4 +490,38 @@ public static String humanReadableByteCount(long bytes, boolean si) { return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); } + public static int compareInteger(int x, int y) { + return Integer.compare(x, y); + } + + public static int compareLong(long x, long y) { + return Long.compare(x, y); + } + + public static boolean isLmq(String lmqMetaData) { + return lmqMetaData != null && lmqMetaData.startsWith(LMQ_PREFIX); + } + + public static String dealFilePath(String aclFilePath) { + Path path = Paths.get(aclFilePath); + return path.normalize().toString(); + } + + public static boolean isSysConsumerGroupForNoColdReadLimit(String consumerGroup) { + if (DEFAULT_CONSUMER_GROUP.equals(consumerGroup) + || TOOLS_CONSUMER_GROUP.equals(consumerGroup) + || SCHEDULE_CONSUMER_GROUP.equals(consumerGroup) + || FILTERSRV_CONSUMER_GROUP.equals(consumerGroup) + || MONITOR_CONSUMER_GROUP.equals(consumerGroup) + || SELF_TEST_CONSUMER_GROUP.equals(consumerGroup) + || ONS_HTTP_PROXY_GROUP.equals(consumerGroup) + || CID_ONSAPI_PERMISSION_GROUP.equals(consumerGroup) + || CID_ONSAPI_OWNER_GROUP.equals(consumerGroup) + || CID_ONSAPI_PULL_GROUP.equals(consumerGroup) + || CID_SYS_RMQ_TRANS.equals(consumerGroup) + || consumerGroup.startsWith(CID_RMQ_SYS_PREFIX)) { + return true; + } + return false; + } } diff --git a/common/src/main/java/org/apache/rocketmq/common/Pair.java b/common/src/main/java/org/apache/rocketmq/common/Pair.java index 30706c0a8e9..10213757cae 100644 --- a/common/src/main/java/org/apache/rocketmq/common/Pair.java +++ b/common/src/main/java/org/apache/rocketmq/common/Pair.java @@ -16,7 +16,9 @@ */ package org.apache.rocketmq.common; -public class Pair { +import java.io.Serializable; + +public class Pair implements Serializable { private T1 object1; private T2 object2; diff --git a/common/src/main/java/org/apache/rocketmq/common/PlainAccessConfig.java b/common/src/main/java/org/apache/rocketmq/common/PlainAccessConfig.java new file mode 100644 index 00000000000..24596daa529 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/PlainAccessConfig.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common; + +import java.io.Serializable; +import java.util.List; +import java.util.Objects; + +public class PlainAccessConfig implements Serializable { + private static final long serialVersionUID = -4517357000307227637L; + + private String accessKey; + + private String secretKey; + + private String whiteRemoteAddress; + + private boolean admin; + + private String defaultTopicPerm; + + private String defaultGroupPerm; + + private List topicPerms; + + private List groupPerms; + + public String getAccessKey() { + return accessKey; + } + + public void setAccessKey(String accessKey) { + this.accessKey = accessKey; + } + + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + + public String getWhiteRemoteAddress() { + return whiteRemoteAddress; + } + + public void setWhiteRemoteAddress(String whiteRemoteAddress) { + this.whiteRemoteAddress = whiteRemoteAddress; + } + + public boolean isAdmin() { + return admin; + } + + public void setAdmin(boolean admin) { + this.admin = admin; + } + + public String getDefaultTopicPerm() { + return defaultTopicPerm; + } + + public void setDefaultTopicPerm(String defaultTopicPerm) { + this.defaultTopicPerm = defaultTopicPerm; + } + + public String getDefaultGroupPerm() { + return defaultGroupPerm; + } + + public void setDefaultGroupPerm(String defaultGroupPerm) { + this.defaultGroupPerm = defaultGroupPerm; + } + + public List getTopicPerms() { + return topicPerms; + } + + public void setTopicPerms(List topicPerms) { + this.topicPerms = topicPerms; + } + + public List getGroupPerms() { + return groupPerms; + } + + public void setGroupPerms(List groupPerms) { + this.groupPerms = groupPerms; + } + + @Override + public String toString() { + return "PlainAccessConfig{" + + "accessKey='" + accessKey + '\'' + + ", whiteRemoteAddress='" + whiteRemoteAddress + '\'' + + ", admin=" + admin + + ", defaultTopicPerm='" + defaultTopicPerm + '\'' + + ", defaultGroupPerm='" + defaultGroupPerm + '\'' + + ", topicPerms=" + topicPerms + + ", groupPerms=" + groupPerms + + '}'; + } + + @Override public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + PlainAccessConfig config = (PlainAccessConfig) o; + return admin == config.admin && Objects.equals(accessKey, config.accessKey) && Objects.equals(secretKey, config.secretKey) && Objects.equals(whiteRemoteAddress, config.whiteRemoteAddress) && Objects.equals(defaultTopicPerm, config.defaultTopicPerm) && Objects.equals(defaultGroupPerm, config.defaultGroupPerm) && Objects.equals(topicPerms, config.topicPerms) && Objects.equals(groupPerms, config.groupPerms); + } + + @Override public int hashCode() { + return Objects.hash(accessKey, secretKey, whiteRemoteAddress, admin, defaultTopicPerm, defaultGroupPerm, topicPerms, groupPerms); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/PopAckConstants.java b/common/src/main/java/org/apache/rocketmq/common/PopAckConstants.java new file mode 100644 index 00000000000..2f979fa15f0 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/PopAckConstants.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common; + +import org.apache.rocketmq.common.topic.TopicValidator; + +public class PopAckConstants { + public static long ackTimeInterval = 1000; + public static final long SECOND = 1000; + + public static long lockTime = 5000; + public static int retryQueueNum = 1; + + public static final String REVIVE_GROUP = MixAll.CID_RMQ_SYS_PREFIX + "REVIVE_GROUP"; + public static final String LOCAL_HOST = "127.0.0.1"; + public static final String REVIVE_TOPIC = TopicValidator.SYSTEM_TOPIC_PREFIX + "REVIVE_LOG_"; + public static final String CK_TAG = "ck"; + public static final String ACK_TAG = "ack"; + public static final String BATCH_ACK_TAG = "bAck"; + public static final String SPLIT = "@"; + + /** + * Build cluster revive topic + * + * @param clusterName cluster name + * @return revive topic + */ + public static String buildClusterReviveTopic(String clusterName) { + return PopAckConstants.REVIVE_TOPIC + clusterName; + } + + public static boolean isStartWithRevivePrefix(String topicName) { + return topicName != null && topicName.startsWith(REVIVE_TOPIC); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/ServiceThread.java b/common/src/main/java/org/apache/rocketmq/common/ServiceThread.java index bf7e724986a..95dc8b9800b 100644 --- a/common/src/main/java/org/apache/rocketmq/common/ServiceThread.java +++ b/common/src/main/java/org/apache/rocketmq/common/ServiceThread.java @@ -18,28 +18,41 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; + import org.apache.rocketmq.common.constant.LoggerName; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; public abstract class ServiceThread implements Runnable { private static final Logger log = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); private static final long JOIN_TIME = 90 * 1000; - protected final Thread thread; + protected Thread thread; protected final CountDownLatch2 waitPoint = new CountDownLatch2(1); protected volatile AtomicBoolean hasNotified = new AtomicBoolean(false); protected volatile boolean stopped = false; + protected boolean isDaemon = false; + + //Make it able to restart the thread + private final AtomicBoolean started = new AtomicBoolean(false); public ServiceThread() { - this.thread = new Thread(this, this.getServiceName()); + } public abstract String getServiceName(); public void start() { + log.info("Try to start service thread:{} started:{} lastThread:{}", getServiceName(), started.get(), thread); + if (!started.compareAndSet(false, true)) { + return; + } + stopped = false; + this.thread = new Thread(this, getServiceName()); + this.thread.setDaemon(isDaemon); this.thread.start(); + log.info("Start service thread:{} started:{} lastThread:{}", getServiceName(), started.get(), thread); } public void shutdown() { @@ -47,12 +60,15 @@ public void shutdown() { } public void shutdown(final boolean interrupt) { + log.info("Try to shutdown service thread:{} started:{} lastThread:{}", getServiceName(), started.get(), thread); + if (!started.compareAndSet(true, false)) { + return; + } this.stopped = true; - log.info("shutdown thread " + this.getServiceName() + " interrupt " + interrupt); + log.info("shutdown thread[{}] interrupt={} ", getServiceName(), interrupt); - if (hasNotified.compareAndSet(false, true)) { - waitPoint.countDown(); // notify - } + //if thead is waiting, wakeup it + wakeup(); try { if (interrupt) { @@ -61,40 +77,25 @@ public void shutdown(final boolean interrupt) { long beginTime = System.currentTimeMillis(); if (!this.thread.isDaemon()) { - this.thread.join(this.getJointime()); + this.thread.join(this.getJoinTime()); } - long eclipseTime = System.currentTimeMillis() - beginTime; - log.info("join thread " + this.getServiceName() + " eclipse time(ms) " + eclipseTime + " " - + this.getJointime()); + long elapsedTime = System.currentTimeMillis() - beginTime; + log.info("join thread[{}], elapsed time: {}ms, join time:{}ms", getServiceName(), elapsedTime, this.getJoinTime()); } catch (InterruptedException e) { log.error("Interrupted", e); } } - public long getJointime() { + public long getJoinTime() { return JOIN_TIME; } - public void stop() { - this.stop(false); - } - - public void stop(final boolean interrupt) { - this.stopped = true; - log.info("stop thread " + this.getServiceName() + " interrupt " + interrupt); - - if (hasNotified.compareAndSet(false, true)) { - waitPoint.countDown(); // notify - } - - if (interrupt) { - this.thread.interrupt(); - } - } - public void makeStop() { + if (!started.get()) { + return; + } this.stopped = true; - log.info("makestop thread " + this.getServiceName()); + log.info("makestop thread[{}] ", this.getServiceName()); } public void wakeup() { @@ -128,4 +129,12 @@ protected void onWaitEnd() { public boolean isStopped() { return stopped; } + + public boolean isDaemon() { + return isDaemon; + } + + public void setDaemon(boolean daemon) { + isDaemon = daemon; + } } diff --git a/common/src/main/java/org/apache/rocketmq/common/SubscriptionGroupAttributes.java b/common/src/main/java/org/apache/rocketmq/common/SubscriptionGroupAttributes.java new file mode 100644 index 00000000000..5b0072401c5 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/SubscriptionGroupAttributes.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common; + +import java.util.HashMap; +import java.util.Map; +import org.apache.rocketmq.common.attribute.Attribute; + +public class SubscriptionGroupAttributes { + public static final Map ALL; + + static { + ALL = new HashMap<>(); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/ThreadFactoryImpl.java b/common/src/main/java/org/apache/rocketmq/common/ThreadFactoryImpl.java index 3860ec3cc72..bb0d141da09 100644 --- a/common/src/main/java/org/apache/rocketmq/common/ThreadFactoryImpl.java +++ b/common/src/main/java/org/apache/rocketmq/common/ThreadFactoryImpl.java @@ -19,18 +19,50 @@ import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicLong; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; public class ThreadFactoryImpl implements ThreadFactory { + + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); + private final AtomicLong threadIndex = new AtomicLong(0); private final String threadNamePrefix; + private final boolean daemon; public ThreadFactoryImpl(final String threadNamePrefix) { + this(threadNamePrefix, false); + } + + public ThreadFactoryImpl(final String threadNamePrefix, boolean daemon) { this.threadNamePrefix = threadNamePrefix; + this.daemon = daemon; + } + + public ThreadFactoryImpl(final String threadNamePrefix, BrokerIdentity brokerIdentity) { + this(threadNamePrefix, false, brokerIdentity); + } + + public ThreadFactoryImpl(final String threadNamePrefix, boolean daemon, BrokerIdentity brokerIdentity) { + this.daemon = daemon; + if (brokerIdentity != null && brokerIdentity.isInBrokerContainer()) { + this.threadNamePrefix = brokerIdentity.getIdentifier() + threadNamePrefix; + } else { + this.threadNamePrefix = threadNamePrefix; + } } @Override public Thread newThread(Runnable r) { - return new Thread(r, threadNamePrefix + this.threadIndex.incrementAndGet()); + Thread thread = new Thread(r, threadNamePrefix + this.threadIndex.incrementAndGet()); + thread.setDaemon(daemon); + + // Log all uncaught exception + thread.setUncaughtExceptionHandler((t, e) -> + LOGGER.error("[BUG] Thread has an uncaught exception, threadId={}, threadName={}", + t.getId(), t.getName(), e)); + return thread; } } diff --git a/common/src/main/java/org/apache/rocketmq/common/TopicAttributes.java b/common/src/main/java/org/apache/rocketmq/common/TopicAttributes.java new file mode 100644 index 00000000000..1f26866e5b3 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/TopicAttributes.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common; + +import java.util.HashMap; +import java.util.Map; +import org.apache.rocketmq.common.attribute.Attribute; +import org.apache.rocketmq.common.attribute.EnumAttribute; +import org.apache.rocketmq.common.attribute.TopicMessageType; + +import static com.google.common.collect.Sets.newHashSet; + +public class TopicAttributes { + public static final EnumAttribute QUEUE_TYPE_ATTRIBUTE = new EnumAttribute( + "queue.type", + false, + newHashSet("BatchCQ", "SimpleCQ"), + "SimpleCQ" + ); + public static final EnumAttribute CLEANUP_POLICY_ATTRIBUTE = new EnumAttribute( + "cleanup.policy", + false, + newHashSet("DELETE", "COMPACTION"), + "DELETE" + ); + public static final EnumAttribute TOPIC_MESSAGE_TYPE_ATTRIBUTE = new EnumAttribute( + "message.type", + true, + TopicMessageType.topicMessageTypeSet(), + TopicMessageType.NORMAL.getValue() + ); + + public static final Map ALL; + + static { + ALL = new HashMap<>(); + ALL.put(QUEUE_TYPE_ATTRIBUTE.getName(), QUEUE_TYPE_ATTRIBUTE); + ALL.put(CLEANUP_POLICY_ATTRIBUTE.getName(), CLEANUP_POLICY_ATTRIBUTE); + ALL.put(TOPIC_MESSAGE_TYPE_ATTRIBUTE.getName(), TOPIC_MESSAGE_TYPE_ATTRIBUTE); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/TopicConfig.java b/common/src/main/java/org/apache/rocketmq/common/TopicConfig.java index 4795cced62d..0bf64905a03 100644 --- a/common/src/main/java/org/apache/rocketmq/common/TopicConfig.java +++ b/common/src/main/java/org/apache/rocketmq/common/TopicConfig.java @@ -16,12 +16,23 @@ */ package org.apache.rocketmq.common; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.TypeReference; +import com.alibaba.fastjson.annotation.JSONField; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.apache.rocketmq.common.attribute.TopicMessageType; import org.apache.rocketmq.common.constant.PermName; +import static org.apache.rocketmq.common.TopicAttributes.TOPIC_MESSAGE_TYPE_ATTRIBUTE; + public class TopicConfig { private static final String SEPARATOR = " "; public static int defaultReadQueueNums = 16; public static int defaultWriteQueueNums = 16; + private static final TypeReference> ATTRIBUTES_TYPE_REFERENCE = new TypeReference>() { + }; private String topicName; private int readQueueNums = defaultReadQueueNums; private int writeQueueNums = defaultWriteQueueNums; @@ -29,6 +40,8 @@ public class TopicConfig { private TopicFilterType topicFilterType = TopicFilterType.SINGLE_TAG; private int topicSysFlag = 0; private boolean order = false; + // Field attributes should not have ' ' char in key or value, otherwise will lead to decode failure. + private Map attributes = new HashMap<>(); public TopicConfig() { } @@ -37,6 +50,12 @@ public TopicConfig(String topicName) { this.topicName = topicName; } + public TopicConfig(String topicName, int readQueueNums, int writeQueueNums) { + this.topicName = topicName; + this.readQueueNums = readQueueNums; + this.writeQueueNums = writeQueueNums; + } + public TopicConfig(String topicName, int readQueueNums, int writeQueueNums, int perm) { this.topicName = topicName; this.readQueueNums = readQueueNums; @@ -44,24 +63,53 @@ public TopicConfig(String topicName, int readQueueNums, int writeQueueNums, int this.perm = perm; } + public TopicConfig(String topicName, int readQueueNums, int writeQueueNums, int perm, int topicSysFlag) { + this.topicName = topicName; + this.readQueueNums = readQueueNums; + this.writeQueueNums = writeQueueNums; + this.perm = perm; + this.topicSysFlag = topicSysFlag; + } + + public TopicConfig(TopicConfig other) { + this.topicName = other.topicName; + this.readQueueNums = other.readQueueNums; + this.writeQueueNums = other.writeQueueNums; + this.perm = other.perm; + this.topicFilterType = other.topicFilterType; + this.topicSysFlag = other.topicSysFlag; + this.order = other.order; + this.attributes = other.attributes; + } + public String encode() { StringBuilder sb = new StringBuilder(); + //[0] sb.append(this.topicName); sb.append(SEPARATOR); + //[1] sb.append(this.readQueueNums); sb.append(SEPARATOR); + //[2] sb.append(this.writeQueueNums); sb.append(SEPARATOR); + //[3] sb.append(this.perm); sb.append(SEPARATOR); + //[4] sb.append(this.topicFilterType); + sb.append(SEPARATOR); + //[5] + if (attributes != null) { + sb.append(JSON.toJSONString(attributes)); + } return sb.toString(); } public boolean decode(final String in) { String[] strs = in.split(SEPARATOR); - if (strs != null && strs.length == 5) { + if (strs.length >= 5) { this.topicName = strs[0]; this.readQueueNums = Integer.parseInt(strs[1]); @@ -72,6 +120,14 @@ public boolean decode(final String in) { this.topicFilterType = TopicFilterType.valueOf(strs[4]); + if (strs.length >= 6) { + try { + this.attributes = JSON.parseObject(strs[5], ATTRIBUTES_TYPE_REFERENCE.getType()); + } catch (Exception e) { + // ignore exception when parse failed, cause map's key/value can have ' ' char. + } + } + return true; } @@ -134,29 +190,64 @@ public void setOrder(boolean isOrder) { this.order = isOrder; } + public Map getAttributes() { + return attributes; + } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + + @JSONField(serialize = false, deserialize = false) + public TopicMessageType getTopicMessageType() { + if (attributes == null) { + return TopicMessageType.NORMAL; + } + String content = attributes.get(TOPIC_MESSAGE_TYPE_ATTRIBUTE.getName()); + if (content == null) { + return TopicMessageType.NORMAL; + } + return TopicMessageType.valueOf(content); + } + + @JSONField(serialize = false, deserialize = false) + public void setTopicMessageType(TopicMessageType topicMessageType) { + attributes.put(TOPIC_MESSAGE_TYPE_ATTRIBUTE.getName(), topicMessageType.getValue()); + } + @Override - public boolean equals(final Object o) { - if (this == o) + public boolean equals(Object o) { + if (this == o) { return true; - if (o == null || getClass() != o.getClass()) + } + if (o == null || getClass() != o.getClass()) { return false; + } - final TopicConfig that = (TopicConfig) o; + TopicConfig that = (TopicConfig) o; - if (readQueueNums != that.readQueueNums) + if (readQueueNums != that.readQueueNums) { return false; - if (writeQueueNums != that.writeQueueNums) + } + if (writeQueueNums != that.writeQueueNums) { return false; - if (perm != that.perm) + } + if (perm != that.perm) { return false; - if (topicSysFlag != that.topicSysFlag) + } + if (topicSysFlag != that.topicSysFlag) { return false; - if (order != that.order) + } + if (order != that.order) { return false; - if (topicName != null ? !topicName.equals(that.topicName) : that.topicName != null) + } + if (!Objects.equals(topicName, that.topicName)) { return false; - return topicFilterType == that.topicFilterType; - + } + if (topicFilterType != that.topicFilterType) { + return false; + } + return Objects.equals(attributes, that.attributes); } @Override @@ -168,6 +259,7 @@ public int hashCode() { result = 31 * result + (topicFilterType != null ? topicFilterType.hashCode() : 0); result = 31 * result + topicSysFlag; result = 31 * result + (order ? 1 : 0); + result = 31 * result + (attributes != null ? attributes.hashCode() : 0); return result; } @@ -175,7 +267,7 @@ public int hashCode() { public String toString() { return "TopicConfig [topicName=" + topicName + ", readQueueNums=" + readQueueNums + ", writeQueueNums=" + writeQueueNums + ", perm=" + PermName.perm2String(perm) - + ", topicFilterType=" + topicFilterType + ", topicSysFlag=" + topicSysFlag + ", order=" - + order + "]"; + + ", topicFilterType=" + topicFilterType + ", topicSysFlag=" + topicSysFlag + ", order=" + order + + ", attributes=" + attributes + "]"; } } diff --git a/common/src/main/java/org/apache/rocketmq/common/TopicQueueId.java b/common/src/main/java/org/apache/rocketmq/common/TopicQueueId.java new file mode 100644 index 00000000000..c5f03877b2c --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/TopicQueueId.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common; + +import com.google.common.base.Objects; + +public class TopicQueueId { + private final String topic; + private final int queueId; + + private final int hash; + + public TopicQueueId(String topic, int queueId) { + this.topic = topic; + this.queueId = queueId; + + this.hash = Objects.hashCode(topic, queueId); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + TopicQueueId broker = (TopicQueueId) o; + return queueId == broker.queueId && Objects.equal(topic, broker.topic); + } + + @Override + public int hashCode() { + return hash; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MessageQueueInBroker{"); + sb.append("topic='").append(topic).append('\''); + sb.append(", queueId=").append(queueId); + sb.append('}'); + return sb.toString(); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/UnlockCallback.java b/common/src/main/java/org/apache/rocketmq/common/UnlockCallback.java new file mode 100644 index 00000000000..e86ec8b4af6 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/UnlockCallback.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common; + +public interface UnlockCallback { + void onSuccess(); + + void onException(final Throwable e); +} diff --git a/common/src/main/java/org/apache/rocketmq/common/UtilAll.java b/common/src/main/java/org/apache/rocketmq/common/UtilAll.java index 9ed8ab881b7..3629ae648bb 100644 --- a/common/src/main/java/org/apache/rocketmq/common/UtilAll.java +++ b/common/src/main/java/org/apache/rocketmq/common/UtilAll.java @@ -16,48 +16,80 @@ */ package org.apache.rocketmq.common; +import io.netty.util.internal.PlatformDependent; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.lang.management.ManagementFactory; -import java.lang.management.RuntimeMXBean; import java.net.Inet4Address; +import java.net.Inet6Address; import java.net.InetAddress; import java.net.NetworkInterface; +import java.nio.ByteBuffer; +import java.nio.file.Files; import java.text.NumberFormat; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.Enumeration; import java.util.Iterator; +import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import java.util.zip.CRC32; import java.util.zip.DeflaterOutputStream; import java.util.zip.InflaterInputStream; - +import java.util.Collections; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.validator.routines.InetAddressValidator; import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.remoting.common.RemotingHelper; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; public class UtilAll { private static final Logger log = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); + private static final Logger STORE_LOG = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); public static final String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss"; public static final String YYYY_MM_DD_HH_MM_SS_SSS = "yyyy-MM-dd#HH:mm:ss:SSS"; public static final String YYYYMMDDHHMMSS = "yyyyMMddHHmmss"; - final static char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + private final static char[] HEX_ARRAY; + private final static int PID; + + static { + HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + Supplier supplier = () -> { + // format: "pid@hostname" + String currentJVM = ManagementFactory.getRuntimeMXBean().getName(); + try { + return Integer.parseInt(currentJVM.substring(0, currentJVM.indexOf('@'))); + } catch (Exception e) { + return -1; + } + }; + PID = supplier.get(); + } public static int getPid() { - RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean(); - String name = runtime.getName(); // format: "pid@hostname" + return PID; + } + + public static void sleep(long sleepMs) { + sleep(sleepMs, TimeUnit.MILLISECONDS); + } + + public static void sleep(long timeOut, TimeUnit timeUnit) { + if (null == timeUnit) { + return; + } try { - return Integer.parseInt(name.substring(0, name.indexOf('@'))); - } catch (Exception e) { - return -1; + timeUnit.sleep(timeOut); + } catch (Throwable ignored) { + } } @@ -80,7 +112,7 @@ public static String offset2FileName(final long offset) { return nf.format(offset); } - public static long computeEclipseTimeMilliseconds(final long beginTime) { + public static long computeElapsedTimeMilliseconds(final long beginTime) { return System.currentTimeMillis() - beginTime; } @@ -111,7 +143,7 @@ public static String timeMillisToHumanString(final long t) { cal.get(Calendar.MILLISECOND)); } - public static long computNextMorningTimeMillis() { + public static long computeNextMorningTimeMillis() { Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(System.currentTimeMillis()); cal.add(Calendar.DAY_OF_MONTH, 1); @@ -123,7 +155,7 @@ public static long computNextMorningTimeMillis() { return cal.getTimeInMillis(); } - public static long computNextMinutesTimeMillis() { + public static long computeNextMinutesTimeMillis() { Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(System.currentTimeMillis()); cal.add(Calendar.DAY_OF_MONTH, 0); @@ -135,7 +167,7 @@ public static long computNextMinutesTimeMillis() { return cal.getTimeInMillis(); } - public static long computNextHourTimeMillis() { + public static long computeNextHourTimeMillis() { Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(System.currentTimeMillis()); cal.add(Calendar.DAY_OF_MONTH, 0); @@ -147,7 +179,7 @@ public static long computNextHourTimeMillis() { return cal.getTimeInMillis(); } - public static long computNextHalfHourTimeMillis() { + public static long computeNextHalfHourTimeMillis() { Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(System.currentTimeMillis()); cal.add(Calendar.DAY_OF_MONTH, 0); @@ -184,31 +216,77 @@ public static String timeMillisToHumanString3(final long t) { cal.get(Calendar.SECOND)); } - public static double getDiskPartitionSpaceUsedPercent(final String path) { + public static long getTotalSpace(final String path) { if (null == path || path.isEmpty()) return -1; + try { + File file = new File(path); + if (!file.exists()) + return -1; + return file.getTotalSpace(); + } catch (Exception e) { + return -1; + } + } + + public static boolean isPathExists(final String path) { + File file = new File(path); + return file.exists(); + } + + public static double getDiskPartitionSpaceUsedPercent(final String path) { + if (null == path || path.isEmpty()) { + STORE_LOG.error("Error when measuring disk space usage, path is null or empty, path : {}", path); + return -1; + } try { File file = new File(path); - if (!file.exists()) + if (!file.exists()) { + STORE_LOG.error("Error when measuring disk space usage, file doesn't exist on this path: {}", path); return -1; + } long totalSpace = file.getTotalSpace(); if (totalSpace > 0) { - long freeSpace = file.getFreeSpace(); - long usedSpace = totalSpace - freeSpace; - - return usedSpace / (double) totalSpace; + long usedSpace = totalSpace - file.getFreeSpace(); + long usableSpace = file.getUsableSpace(); + long entireSpace = usedSpace + usableSpace; + long roundNum = 0; + if (usedSpace * 100 % entireSpace != 0) { + roundNum = 1; + } + long result = usedSpace * 100 / entireSpace + roundNum; + return result / 100.0; } } catch (Exception e) { + STORE_LOG.error("Error when measuring disk space usage, got exception: :", e); return -1; } return -1; } + public static long getDiskPartitionTotalSpace(final String path) { + if (null == path || path.isEmpty()) { + return -1; + } + + try { + File file = new File(path); + + if (!file.exists()) { + return -1; + } + + return file.getTotalSpace() - file.getFreeSpace() + file.getUsableSpace(); + } catch (Exception e) { + return -1; + } + } + public static int crc32(byte[] array) { if (array != null) { return crc32(array, 0, array.length); @@ -223,6 +301,20 @@ public static int crc32(byte[] array, int offset, int length) { return (int) (crc32.getValue() & 0x7FFFFFFF); } + public static int crc32(ByteBuffer byteBuffer) { + CRC32 crc32 = new CRC32(); + crc32.update(byteBuffer); + return (int) (crc32.getValue() & 0x7FFFFFFF); + } + + public static int crc32(ByteBuffer[] byteBuffers) { + CRC32 crc32 = new CRC32(); + for (ByteBuffer buffer : byteBuffers) { + crc32.update(buffer); + } + return (int) (crc32.getValue() & 0x7FFFFFFF); + } + public static String bytes2string(byte[] src) { char[] hexChars = new char[src.length * 2]; for (int j = 0; j < src.length; j++) { @@ -233,6 +325,20 @@ public static String bytes2string(byte[] src) { return new String(hexChars); } + public static void writeInt(char[] buffer, int pos, int value) { + char[] hexArray = HEX_ARRAY; + for (int moveBits = 28; moveBits >= 0; moveBits -= 4) { + buffer[pos++] = hexArray[(value >>> moveBits) & 0x0F]; + } + } + + public static void writeShort(char[] buffer, int pos, int value) { + char[] hexArray = HEX_ARRAY; + for (int moveBits = 12; moveBits >= 0; moveBits -= 4) { + buffer[pos++] = hexArray[(value >>> moveBits) & 0x0F]; + } + } + public static byte[] string2bytes(String hexString) { if (hexString == null || hexString.equals("")) { return null; @@ -252,6 +358,10 @@ private static byte charToByte(char c) { return (byte) "0123456789ABCDEF".indexOf(c); } + /** + * use {@link org.apache.rocketmq.common.compression.Compressor#decompress(byte[])} instead. + */ + @Deprecated public static byte[] uncompress(final byte[] src) throws IOException { byte[] result = src; byte[] uncompressData = new byte[src.length]; @@ -292,6 +402,10 @@ public static byte[] uncompress(final byte[] src) throws IOException { return result; } + /** + * use {@link org.apache.rocketmq.common.compression.Compressor#compress(byte[], int)} instead. + */ + @Deprecated public static byte[] compress(final byte[] src, final int level) throws IOException { byte[] result = src; ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(src.length); @@ -362,16 +476,7 @@ public static String frontStringAtLeast(final String str, final int size) { } public static boolean isBlank(String str) { - int strLen; - if (str == null || (strLen = str.length()) == 0) { - return true; - } - for (int i = 0; i < strLen; i++) { - if (!Character.isWhitespace(str.charAt(i))) { - return false; - } - } - return true; + return StringUtils.isBlank(str); } public static String jstack() { @@ -396,12 +501,27 @@ public static String jstack(Map map) { } } } catch (Throwable e) { - result.append(RemotingHelper.exceptionSimpleDesc(e)); + result.append(exceptionSimpleDesc(e)); } return result.toString(); } + public static String exceptionSimpleDesc(final Throwable e) { + StringBuilder sb = new StringBuilder(); + if (e != null) { + sb.append(e); + + StackTraceElement[] stackTrace = e.getStackTrace(); + if (stackTrace != null && stackTrace.length > 0) { + StackTraceElement element = stackTrace[0]; + sb.append(", "); + sb.append(element.toString()); + } + } + return sb.toString(); + } + public static boolean isInternalIP(byte[] ip) { if (ip.length != 4) { throw new RuntimeException("illegal ipv4 bytes"); @@ -410,8 +530,10 @@ public static boolean isInternalIP(byte[] ip) { //10.0.0.0~10.255.255.255 //172.16.0.0~172.31.255.255 //192.168.0.0~192.168.255.255 + //127.0.0.0~127.255.255.255 if (ip[0] == (byte) 10) { - + return true; + } else if (ip[0] == (byte) 127) { return true; } else if (ip[0] == (byte) 172) { if (ip[1] >= (byte) 16 && ip[1] <= (byte) 31) { @@ -425,40 +547,32 @@ public static boolean isInternalIP(byte[] ip) { return false; } + public static boolean isInternalV6IP(InetAddress inetAddr) { + if (inetAddr.isAnyLocalAddress() // Wild card ipv6 + || inetAddr.isLinkLocalAddress() // Single broadcast ipv6 address: fe80:xx:xx... + || inetAddr.isLoopbackAddress() //Loopback ipv6 address + || inetAddr.isSiteLocalAddress()) { // Site local ipv6 address: fec0:xx:xx... + return true; + } + return false; + } + private static boolean ipCheck(byte[] ip) { if (ip.length != 4) { throw new RuntimeException("illegal ipv4 bytes"); } -// if (ip[0] == (byte)30 && ip[1] == (byte)10 && ip[2] == (byte)163 && ip[3] == (byte)120) { -// } + InetAddressValidator validator = InetAddressValidator.getInstance(); + return validator.isValidInet4Address(ipToIPv4Str(ip)); + } - if (ip[0] >= (byte) 1 && ip[0] <= (byte) 126) { - if (ip[1] == (byte) 1 && ip[2] == (byte) 1 && ip[3] == (byte) 1) { - return false; - } - if (ip[1] == (byte) 0 && ip[2] == (byte) 0 && ip[3] == (byte) 0) { - return false; - } - return true; - } else if (ip[0] >= (byte) 128 && ip[0] <= (byte) 191) { - if (ip[2] == (byte) 1 && ip[3] == (byte) 1) { - return false; - } - if (ip[2] == (byte) 0 && ip[3] == (byte) 0) { - return false; - } - return true; - } else if (ip[0] >= (byte) 192 && ip[0] <= (byte) 223) { - if (ip[3] == (byte) 1) { - return false; - } - if (ip[3] == (byte) 0) { - return false; - } - return true; + private static boolean ipV6Check(byte[] ip) { + if (ip.length != 16) { + throw new RuntimeException("illegal ipv6 bytes"); } - return false; + + InetAddressValidator validator = InetAddressValidator.getInstance(); + return validator.isValidInet6Address(ipToIPv6Str(ip)); } public static String ipToIPv4Str(byte[] ip) { @@ -466,10 +580,29 @@ public static String ipToIPv4Str(byte[] ip) { return null; } return new StringBuilder().append(ip[0] & 0xFF).append(".").append( - ip[1] & 0xFF).append(".").append(ip[2] & 0xFF) + ip[1] & 0xFF).append(".").append(ip[2] & 0xFF) .append(".").append(ip[3] & 0xFF).toString(); } + public static String ipToIPv6Str(byte[] ip) { + if (ip.length != 16) { + return null; + } + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < ip.length; i++) { + String hex = Integer.toHexString(ip[i] & 0xFF); + if (hex.length() < 2) { + sb.append(0); + } + sb.append(hex); + if (i % 2 == 1 && i < ip.length - 1) { + sb.append(":"); + } + } + return sb.toString(); + } + public static byte[] getIP() { try { Enumeration allNetInterfaces = NetworkInterface.getNetworkInterfaces(); @@ -486,11 +619,20 @@ public static byte[] getIP() { if (ipCheck(ipByte)) { if (!isInternalIP(ipByte)) { return ipByte; - } else if (internalIP == null) { + } else if (internalIP == null || internalIP[0] == (byte) 127) { internalIP = ipByte; } } } + } else if (ip != null && ip instanceof Inet6Address) { + byte[] ipByte = ip.getAddress(); + if (ipByte.length == 16) { + if (ipV6Check(ipByte)) { + if (!isInternalV6IP(ip)) { + return ipByte; + } + } + } } } } @@ -518,4 +660,110 @@ public static void deleteFile(File file) { file.delete(); } } + + public static String join(List list, String splitter) { + if (list == null) { + return null; + } + + StringBuilder str = new StringBuilder(); + for (int i = 0; i < list.size(); i++) { + str.append(list.get(i)); + if (i == list.size() - 1) { + break; + } + str.append(splitter); + } + return str.toString(); + } + + public static List split(String str, String splitter) { + if (str == null) { + return null; + } + + if (StringUtils.isBlank(str)) { + return Collections.EMPTY_LIST; + } + + String[] addrArray = str.split(splitter); + return Arrays.asList(addrArray); + } + + public static void deleteEmptyDirectory(File file) { + if (file == null || !file.exists()) { + return; + } + if (!file.isDirectory()) { + return; + } + File[] files = file.listFiles(); + if (files == null || files.length <= 0) { + file.delete(); + STORE_LOG.info("delete empty direct, {}", file.getPath()); + } + } + + /** + * Free direct-buffer's memory actively. + * @param buffer Direct buffer to free. + */ + public static void cleanBuffer(final ByteBuffer buffer) { + if (null == buffer) { + return; + } + + if (!buffer.isDirect()) { + return; + } + + PlatformDependent.freeDirectBuffer(buffer); + } + + public static void ensureDirOK(final String dirName) { + if (dirName != null) { + if (dirName.contains(MixAll.MULTI_PATH_SPLITTER)) { + String[] dirs = dirName.trim().split(MixAll.MULTI_PATH_SPLITTER); + for (String dir : dirs) { + createDirIfNotExist(dir); + } + } else { + createDirIfNotExist(dirName); + } + } + } + + private static void createDirIfNotExist(String dirName) { + File f = new File(dirName); + if (!f.exists()) { + boolean result = f.mkdirs(); + STORE_LOG.info(dirName + " mkdir " + (result ? "OK" : "Failed")); + } + } + + public static long calculateFileSizeInPath(File path) { + long size = 0; + try { + if (!path.exists() || Files.isSymbolicLink(path.toPath())) { + return 0; + } + if (path.isFile()) { + return path.length(); + } + if (path.isDirectory()) { + File[] files = path.listFiles(); + if (files != null && files.length > 0) { + for (File file : files) { + long fileSize = calculateFileSizeInPath(file); + if (fileSize == -1) return -1; + size += fileSize; + } + } + } + } catch (Exception e) { + log.error("calculate all file size in: {} error", path.getAbsolutePath(), e); + return -1; + } + return size; + } } diff --git a/common/src/main/java/org/apache/rocketmq/common/admin/ConsumeStats.java b/common/src/main/java/org/apache/rocketmq/common/admin/ConsumeStats.java deleted file mode 100644 index 6b1c49290f9..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/admin/ConsumeStats.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.common.admin; - -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map.Entry; -import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.remoting.protocol.RemotingSerializable; - -public class ConsumeStats extends RemotingSerializable { - private HashMap offsetTable = new HashMap(); - private double consumeTps = 0; - - public long computeTotalDiff() { - long diffTotal = 0L; - - Iterator> it = this.offsetTable.entrySet().iterator(); - while (it.hasNext()) { - Entry next = it.next(); - long diff = next.getValue().getBrokerOffset() - next.getValue().getConsumerOffset(); - diffTotal += diff; - } - - return diffTotal; - } - - public HashMap getOffsetTable() { - return offsetTable; - } - - public void setOffsetTable(HashMap offsetTable) { - this.offsetTable = offsetTable; - } - - public double getConsumeTps() { - return consumeTps; - } - - public void setConsumeTps(double consumeTps) { - this.consumeTps = consumeTps; - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/attribute/Attribute.java b/common/src/main/java/org/apache/rocketmq/common/attribute/Attribute.java new file mode 100644 index 00000000000..ba9be3bb43d --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/attribute/Attribute.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.attribute; + +public abstract class Attribute { + protected String name; + protected boolean changeable; + + public abstract void verify(String value); + + public Attribute(String name, boolean changeable) { + this.name = name; + this.changeable = changeable; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isChangeable() { + return changeable; + } + + public void setChangeable(boolean changeable) { + this.changeable = changeable; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/attribute/AttributeParser.java b/common/src/main/java/org/apache/rocketmq/common/attribute/AttributeParser.java new file mode 100644 index 00000000000..da98c6dab85 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/attribute/AttributeParser.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.attribute; + +import com.google.common.base.Strings; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class AttributeParser { + + public static final String ATTR_ARRAY_SEPARATOR_COMMA = ","; + + public static final String ATTR_KEY_VALUE_EQUAL_SIGN = "="; + + public static final String ATTR_ADD_PLUS_SIGN = "+"; + + private static final String ATTR_DELETE_MINUS_SIGN = "-"; + + public static Map parseToMap(String attributesModification) { + if (Strings.isNullOrEmpty(attributesModification)) { + return new HashMap<>(); + } + + // format: +key1=value1,+key2=value2,-key3,+key4=value4 + Map attributes = new HashMap<>(); + String[] kvs = attributesModification.split(ATTR_ARRAY_SEPARATOR_COMMA); + for (String kv : kvs) { + String key; + String value; + if (kv.contains(ATTR_KEY_VALUE_EQUAL_SIGN)) { + String[] splits = kv.split(ATTR_KEY_VALUE_EQUAL_SIGN); + key = splits[0]; + value = splits[1]; + if (!key.contains(ATTR_ADD_PLUS_SIGN)) { + throw new RuntimeException("add/alter attribute format is wrong: " + key); + } + } else { + key = kv; + value = ""; + if (!key.contains(ATTR_DELETE_MINUS_SIGN)) { + throw new RuntimeException("delete attribute format is wrong: " + key); + } + } + String old = attributes.put(key, value); + if (old != null) { + throw new RuntimeException("key duplication: " + key); + } + } + return attributes; + } + + public static String parseToString(Map attributes) { + if (attributes == null || attributes.size() == 0) { + return ""; + } + + List kvs = new ArrayList<>(); + for (Map.Entry entry : attributes.entrySet()) { + + String value = entry.getValue(); + if (Strings.isNullOrEmpty(value)) { + kvs.add(entry.getKey()); + } else { + kvs.add(entry.getKey() + ATTR_KEY_VALUE_EQUAL_SIGN + entry.getValue()); + } + } + return String.join(ATTR_ARRAY_SEPARATOR_COMMA, kvs); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/attribute/AttributeUtil.java b/common/src/main/java/org/apache/rocketmq/common/attribute/AttributeUtil.java new file mode 100644 index 00000000000..a3646988c51 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/attribute/AttributeUtil.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.attribute; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class AttributeUtil { + private static final Logger log = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); + public static Map alterCurrentAttributes(boolean create, Map all, + ImmutableMap currentAttributes, ImmutableMap newAttributes) { + + Map init = new HashMap<>(); + Map add = new HashMap<>(); + Map update = new HashMap<>(); + Map delete = new HashMap<>(); + Set keys = new HashSet<>(); + + for (Map.Entry attribute : newAttributes.entrySet()) { + String key = attribute.getKey(); + String realKey = realKey(key); + String value = attribute.getValue(); + + validate(realKey); + duplicationCheck(keys, realKey); + + if (create) { + if (key.startsWith("+")) { + init.put(realKey, value); + } else { + throw new RuntimeException("only add attribute is supported while creating topic. key: " + realKey); + } + } else { + if (key.startsWith("+")) { + if (!currentAttributes.containsKey(realKey)) { + add.put(realKey, value); + } else { + update.put(realKey, value); + } + } else if (key.startsWith("-")) { + if (!currentAttributes.containsKey(realKey)) { + throw new RuntimeException("attempt to delete a nonexistent key: " + realKey); + } + delete.put(realKey, value); + } else { + throw new RuntimeException("wrong format key: " + realKey); + } + } + } + + validateAlter(all, init, true, false); + validateAlter(all, add, false, false); + validateAlter(all, update, false, false); + validateAlter(all, delete, false, true); + + log.info("add: {}, update: {}, delete: {}", add, update, delete); + HashMap finalAttributes = new HashMap<>(currentAttributes); + finalAttributes.putAll(init); + finalAttributes.putAll(add); + finalAttributes.putAll(update); + for (String s : delete.keySet()) { + finalAttributes.remove(s); + } + return finalAttributes; + } + + private static void duplicationCheck(Set keys, String key) { + boolean notExist = keys.add(key); + if (!notExist) { + throw new RuntimeException("alter duplication key. key: " + key); + } + } + + private static void validate(String kvAttribute) { + if (Strings.isNullOrEmpty(kvAttribute)) { + throw new RuntimeException("kv string format wrong."); + } + + if (kvAttribute.contains("+")) { + throw new RuntimeException("kv string format wrong."); + } + + if (kvAttribute.contains("-")) { + throw new RuntimeException("kv string format wrong."); + } + } + + private static void validateAlter(Map all, Map alter, boolean init, boolean delete) { + for (Map.Entry entry : alter.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + + Attribute attribute = all.get(key); + if (attribute == null) { + throw new RuntimeException("unsupported key: " + key); + } + if (!init && !attribute.isChangeable()) { + throw new RuntimeException("attempt to update an unchangeable attribute. key: " + key); + } + + if (!delete) { + attribute.verify(value); + } + } + } + + private static String realKey(String key) { + return key.substring(1); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/attribute/BooleanAttribute.java b/common/src/main/java/org/apache/rocketmq/common/attribute/BooleanAttribute.java new file mode 100644 index 00000000000..41ad7480547 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/attribute/BooleanAttribute.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.attribute; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class BooleanAttribute extends Attribute { + private final boolean defaultValue; + + public BooleanAttribute(String name, boolean changeable, boolean defaultValue) { + super(name, changeable); + this.defaultValue = defaultValue; + } + + @Override + public void verify(String value) { + checkNotNull(value); + + if (!"false".equalsIgnoreCase(value) && !"true".equalsIgnoreCase(value)) { + throw new RuntimeException("boolean attribute format is wrong."); + } + } + + public boolean getDefaultValue() { + return defaultValue; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/attribute/CQType.java b/common/src/main/java/org/apache/rocketmq/common/attribute/CQType.java new file mode 100644 index 00000000000..9148d5a18aa --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/attribute/CQType.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.attribute; + +public enum CQType { + SimpleCQ, + BatchCQ, + RocksDBCQ +} diff --git a/common/src/main/java/org/apache/rocketmq/common/attribute/CleanupPolicy.java b/common/src/main/java/org/apache/rocketmq/common/attribute/CleanupPolicy.java new file mode 100644 index 00000000000..5f289a0a759 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/attribute/CleanupPolicy.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.attribute; + +public enum CleanupPolicy { + DELETE, + COMPACTION +} diff --git a/common/src/main/java/org/apache/rocketmq/common/attribute/EnumAttribute.java b/common/src/main/java/org/apache/rocketmq/common/attribute/EnumAttribute.java new file mode 100644 index 00000000000..5353b8a293d --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/attribute/EnumAttribute.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.attribute; + +import java.util.Set; + +public class EnumAttribute extends Attribute { + private final Set universe; + private final String defaultValue; + + public EnumAttribute(String name, boolean changeable, Set universe, String defaultValue) { + super(name, changeable); + this.universe = universe; + this.defaultValue = defaultValue; + } + + @Override + public void verify(String value) { + if (!this.universe.contains(value)) { + throw new RuntimeException("value is not in set: " + this.universe); + } + } + + public String getDefaultValue() { + return defaultValue; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/attribute/LongRangeAttribute.java b/common/src/main/java/org/apache/rocketmq/common/attribute/LongRangeAttribute.java new file mode 100644 index 00000000000..eeeda72153b --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/attribute/LongRangeAttribute.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.attribute; + +import static java.lang.String.format; + +public class LongRangeAttribute extends Attribute { + private final long min; + private final long max; + private final long defaultValue; + + public LongRangeAttribute(String name, boolean changeable, long min, long max, long defaultValue) { + super(name, changeable); + this.min = min; + this.max = max; + this.defaultValue = defaultValue; + } + + @Override + public void verify(String value) { + long l = Long.parseLong(value); + if (l < min || l > max) { + throw new RuntimeException(format("value is not in range(%d, %d)", min, max)); + } + } + + public long getDefaultValue() { + return defaultValue; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/attribute/TopicMessageType.java b/common/src/main/java/org/apache/rocketmq/common/attribute/TopicMessageType.java new file mode 100644 index 00000000000..9680acec74d --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/attribute/TopicMessageType.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.attribute; + +import com.google.common.collect.Sets; +import java.util.Map; +import java.util.Set; +import org.apache.rocketmq.common.message.MessageConst; + +public enum TopicMessageType { + UNSPECIFIED("UNSPECIFIED"), + NORMAL("NORMAL"), + FIFO("FIFO"), + DELAY("DELAY"), + TRANSACTION("TRANSACTION"), + MIXED("MIXED"); + + private final String value; + TopicMessageType(String value) { + this.value = value; + } + + public static Set topicMessageTypeSet() { + return Sets.newHashSet(UNSPECIFIED.value, NORMAL.value, FIFO.value, DELAY.value, TRANSACTION.value, MIXED.value); + } + + public String getValue() { + return value; + } + + public static TopicMessageType parseFromMessageProperty(Map messageProperty) { + String isTrans = messageProperty.get(MessageConst.PROPERTY_TRANSACTION_PREPARED); + String isTransValue = "true"; + if (isTransValue.equals(isTrans)) { + return TopicMessageType.TRANSACTION; + } else if (messageProperty.get(MessageConst.PROPERTY_DELAY_TIME_LEVEL) != null + || messageProperty.get(MessageConst.PROPERTY_TIMER_DELIVER_MS) != null + || messageProperty.get(MessageConst.PROPERTY_TIMER_DELAY_SEC) != null) { + return TopicMessageType.DELAY; + } else if (messageProperty.get(MessageConst.PROPERTY_SHARDING_KEY) != null) { + return TopicMessageType.FIFO; + } + return TopicMessageType.NORMAL; + } + + public String getMetricsValue() { + return value.toLowerCase(); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/coldctr/AccAndTimeStamp.java b/common/src/main/java/org/apache/rocketmq/common/coldctr/AccAndTimeStamp.java new file mode 100644 index 00000000000..212bc08c485 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/coldctr/AccAndTimeStamp.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.coldctr; + +import java.util.concurrent.atomic.AtomicLong; + +public class AccAndTimeStamp { + + public AtomicLong coldAcc = new AtomicLong(0L); + public Long lastColdReadTimeMills = System.currentTimeMillis(); + public Long createTimeMills = System.currentTimeMillis(); + + public AccAndTimeStamp(AtomicLong coldAcc) { + this.coldAcc = coldAcc; + } + + public AtomicLong getColdAcc() { + return coldAcc; + } + + public void setColdAcc(AtomicLong coldAcc) { + this.coldAcc = coldAcc; + } + + public Long getLastColdReadTimeMills() { + return lastColdReadTimeMills; + } + + public void setLastColdReadTimeMills(Long lastColdReadTimeMills) { + this.lastColdReadTimeMills = lastColdReadTimeMills; + } + + public Long getCreateTimeMills() { + return createTimeMills; + } + + public void setCreateTimeMills(Long createTimeMills) { + this.createTimeMills = createTimeMills; + } + + @Override + public String toString() { + return "AccAndTimeStamp{" + + "coldAcc=" + coldAcc + + ", lastColdReadTimeMills=" + lastColdReadTimeMills + + ", createTimeMills=" + createTimeMills + + '}'; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/compression/CompressionType.java b/common/src/main/java/org/apache/rocketmq/common/compression/CompressionType.java new file mode 100644 index 00000000000..d748fc47a2d --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/compression/CompressionType.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.compression; + +import org.apache.rocketmq.common.sysflag.MessageSysFlag; + +public enum CompressionType { + + /** + * Compression types number can be extended to seven {@link MessageSysFlag} + * + * Benchmarks from https://github.com/facebook/zstd + * + * | Compressor | Ratio | Compression | Decompress | + * |----------------|---------|-------------|------------| + * | zstd 1.5.1 | 2.887 | 530 MB/s | 1700 MB/s | + * | zlib 1.2.11 | 2.743 | 95 MB/s | 400 MB/s | + * | lz4 1.9.3 | 2.101 | 740 MB/s | 4500 MB/s | + * + */ + + LZ4(1), + ZSTD(2), + ZLIB(3); + + private final int value; + + CompressionType(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static CompressionType of(String name) { + switch (name.trim().toUpperCase()) { + case "LZ4": + return CompressionType.LZ4; + case "ZSTD": + return CompressionType.ZSTD; + case "ZLIB": + return CompressionType.ZLIB; + default: + throw new RuntimeException("Unsupported compress type name: " + name); + } + } + + public static CompressionType findByValue(int value) { + switch (value) { + case 1: + return LZ4; + case 2: + return ZSTD; + case 0: // To be compatible for older versions without compression type + case 3: + return ZLIB; + default: + throw new RuntimeException("Unknown compress type value: " + value); + } + } + + public int getCompressionFlag() { + switch (value) { + case 1: + return MessageSysFlag.COMPRESSION_LZ4_TYPE; + case 2: + return MessageSysFlag.COMPRESSION_ZSTD_TYPE; + case 3: + return MessageSysFlag.COMPRESSION_ZLIB_TYPE; + default: + throw new RuntimeException("Unsupported compress type flag: " + value); + } + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/compression/Compressor.java b/common/src/main/java/org/apache/rocketmq/common/compression/Compressor.java new file mode 100644 index 00000000000..c900045b247 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/compression/Compressor.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.compression; + +import java.io.IOException; + +public interface Compressor { + + /** + * Compress message by different compressor. + * + * @param src bytes ready to compress + * @param level compression level used to balance compression rate and time consumption + * @return compressed byte data + * @throws IOException + */ + byte[] compress(byte[] src, int level) throws IOException; + + /** + * Decompress message by different compressor. + * + * @param src bytes ready to decompress + * @return decompressed byte data + * @throws IOException + */ + byte[] decompress(byte[] src) throws IOException; +} diff --git a/common/src/main/java/org/apache/rocketmq/common/compression/CompressorFactory.java b/common/src/main/java/org/apache/rocketmq/common/compression/CompressorFactory.java new file mode 100644 index 00000000000..aace1d956a1 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/compression/CompressorFactory.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.compression; + +import java.util.EnumMap; + +public class CompressorFactory { + private static final EnumMap COMPRESSORS; + + static { + COMPRESSORS = new EnumMap<>(CompressionType.class); + COMPRESSORS.put(CompressionType.LZ4, new Lz4Compressor()); + COMPRESSORS.put(CompressionType.ZSTD, new ZstdCompressor()); + COMPRESSORS.put(CompressionType.ZLIB, new ZlibCompressor()); + } + + public static Compressor getCompressor(CompressionType type) { + return COMPRESSORS.get(type); + } + +} diff --git a/common/src/main/java/org/apache/rocketmq/common/compression/Lz4Compressor.java b/common/src/main/java/org/apache/rocketmq/common/compression/Lz4Compressor.java new file mode 100644 index 00000000000..0bcb9689dda --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/compression/Lz4Compressor.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.compression; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import net.jpountz.lz4.LZ4FrameInputStream; +import net.jpountz.lz4.LZ4FrameOutputStream; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class Lz4Compressor implements Compressor { + private static final Logger log = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); + + @Override + public byte[] compress(byte[] src, int level) throws IOException { + byte[] result = src; + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(src.length); + LZ4FrameOutputStream outputStream = new LZ4FrameOutputStream(byteArrayOutputStream); + try { + outputStream.write(src); + outputStream.flush(); + outputStream.close(); + result = byteArrayOutputStream.toByteArray(); + } catch (IOException e) { + log.error("Failed to compress data by lz4", e); + throw e; + } finally { + try { + byteArrayOutputStream.close(); + } catch (IOException ignored) { + } + } + return result; + } + + @Override + public byte[] decompress(byte[] src) throws IOException { + byte[] result = src; + byte[] uncompressData = new byte[src.length]; + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(src); + LZ4FrameInputStream lz4InputStream = new LZ4FrameInputStream(byteArrayInputStream); + ByteArrayOutputStream resultOutputStream = new ByteArrayOutputStream(src.length); + + try { + while (true) { + int len = lz4InputStream.read(uncompressData, 0, uncompressData.length); + if (len <= 0) { + break; + } + resultOutputStream.write(uncompressData, 0, len); + } + resultOutputStream.flush(); + resultOutputStream.close(); + result = resultOutputStream.toByteArray(); + } catch (IOException e) { + throw e; + } finally { + try { + lz4InputStream.close(); + byteArrayInputStream.close(); + } catch (IOException e) { + log.warn("Failed to close the lz4 compress stream ", e); + } + } + + return result; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/compression/ZlibCompressor.java b/common/src/main/java/org/apache/rocketmq/common/compression/ZlibCompressor.java new file mode 100644 index 00000000000..e64db9b62a6 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/compression/ZlibCompressor.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.compression; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.InflaterInputStream; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class ZlibCompressor implements Compressor { + private static final Logger log = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); + + @Override + public byte[] compress(byte[] src, int level) throws IOException { + byte[] result = src; + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(src.length); + java.util.zip.Deflater defeater = new java.util.zip.Deflater(level); + DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream, defeater); + try { + deflaterOutputStream.write(src); + deflaterOutputStream.finish(); + deflaterOutputStream.close(); + result = byteArrayOutputStream.toByteArray(); + } catch (IOException e) { + defeater.end(); + throw e; + } finally { + try { + byteArrayOutputStream.close(); + } catch (IOException ignored) { + } + + defeater.end(); + } + + return result; + } + + @Override + public byte[] decompress(byte[] src) throws IOException { + byte[] result = src; + byte[] uncompressData = new byte[src.length]; + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(src); + InflaterInputStream inflaterInputStream = new InflaterInputStream(byteArrayInputStream); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(src.length); + + try { + while (true) { + int len = inflaterInputStream.read(uncompressData, 0, uncompressData.length); + if (len <= 0) { + break; + } + byteArrayOutputStream.write(uncompressData, 0, len); + } + byteArrayOutputStream.flush(); + result = byteArrayOutputStream.toByteArray(); + } catch (IOException e) { + throw e; + } finally { + try { + byteArrayInputStream.close(); + } catch (IOException e) { + log.error("Failed to close the stream", e); + } + try { + inflaterInputStream.close(); + } catch (IOException e) { + log.error("Failed to close the stream", e); + } + try { + byteArrayOutputStream.close(); + } catch (IOException e) { + log.error("Failed to close the stream", e); + } + } + + return result; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/compression/ZstdCompressor.java b/common/src/main/java/org/apache/rocketmq/common/compression/ZstdCompressor.java new file mode 100644 index 00000000000..131035c264e --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/compression/ZstdCompressor.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.compression; + +import com.github.luben.zstd.ZstdInputStream; +import com.github.luben.zstd.ZstdOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class ZstdCompressor implements Compressor { + private static final Logger log = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); + + @Override + public byte[] compress(byte[] src, int level) throws IOException { + byte[] result = src; + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(src.length); + ZstdOutputStream outputStream = new ZstdOutputStream(byteArrayOutputStream, level); + try { + outputStream.write(src); + outputStream.flush(); + outputStream.close(); + result = byteArrayOutputStream.toByteArray(); + } catch (IOException e) { + log.error("Failed to compress data by zstd", e); + throw e; + } finally { + try { + byteArrayOutputStream.close(); + } catch (IOException ignored) { + } + } + return result; + } + + @Override + public byte[] decompress(byte[] src) throws IOException { + byte[] result = src; + byte[] uncompressData = new byte[src.length]; + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(src); + ZstdInputStream zstdInputStream = new ZstdInputStream(byteArrayInputStream); + ByteArrayOutputStream resultOutputStream = new ByteArrayOutputStream(src.length); + + try { + while (true) { + int len = zstdInputStream.read(uncompressData, 0, uncompressData.length); + if (len <= 0) { + break; + } + resultOutputStream.write(uncompressData, 0, len); + } + resultOutputStream.flush(); + resultOutputStream.close(); + result = resultOutputStream.toByteArray(); + } catch (IOException e) { + throw e; + } finally { + try { + zstdInputStream.close(); + byteArrayInputStream.close(); + } catch (IOException e) { + log.warn("Failed to close the zstd compress stream", e); + } + } + + return result; + } +} \ No newline at end of file diff --git a/common/src/main/java/org/apache/rocketmq/common/config/AbstractRocksDBStorage.java b/common/src/main/java/org/apache/rocketmq/common/config/AbstractRocksDBStorage.java new file mode 100644 index 00000000000..20319abba3d --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/config/AbstractRocksDBStorage.java @@ -0,0 +1,507 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.config; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.Semaphore; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import com.google.common.collect.Maps; + +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.DataConverter; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.rocksdb.ColumnFamilyDescriptor; +import org.rocksdb.ColumnFamilyHandle; +import org.rocksdb.ColumnFamilyOptions; +import org.rocksdb.CompactRangeOptions; +import org.rocksdb.CompactionOptions; +import org.rocksdb.DBOptions; +import org.rocksdb.FlushOptions; +import org.rocksdb.LiveFileMetaData; +import org.rocksdb.Priority; +import org.rocksdb.ReadOptions; +import org.rocksdb.RocksDB; +import org.rocksdb.RocksDBException; +import org.rocksdb.Statistics; +import org.rocksdb.Status; +import org.rocksdb.WriteBatch; +import org.rocksdb.WriteOptions; + +import static org.rocksdb.RocksDB.NOT_FOUND; + +public abstract class AbstractRocksDBStorage { + protected static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.ROCKSDB_LOGGER_NAME); + + private static final String SPACE = " | "; + + protected String dbPath; + protected boolean readOnly; + protected RocksDB db; + protected DBOptions options; + + protected WriteOptions writeOptions; + protected WriteOptions ableWalWriteOptions; + + protected ReadOptions readOptions; + protected ReadOptions totalOrderReadOptions; + + protected CompactionOptions compactionOptions; + protected CompactRangeOptions compactRangeOptions; + + protected ColumnFamilyHandle defaultCFHandle; + protected final List cfOptions = new ArrayList(); + + protected volatile boolean loaded; + private volatile boolean closed; + + private final Semaphore reloadPermit = new Semaphore(1); + private final ScheduledExecutorService reloadScheduler = ThreadUtils.newScheduledThreadPool(1, new ThreadFactoryImpl("RocksDBStorageReloadService_")); + private final ThreadPoolExecutor manualCompactionThread = (ThreadPoolExecutor) ThreadUtils.newThreadPoolExecutor( + 1, 1, 1000 * 60, TimeUnit.MILLISECONDS, + new ArrayBlockingQueue(1), + new ThreadFactoryImpl("RocksDBManualCompactionService_"), + new ThreadPoolExecutor.DiscardOldestPolicy()); + + static { + RocksDB.loadLibrary(); + } + + public boolean hold() { + if (!this.loaded || this.db == null || this.closed) { + LOGGER.error("hold rocksdb Failed. {}", this.dbPath); + return false; + } else { + return true; + } + } + + public void release() { + } + + protected void put(ColumnFamilyHandle cfHandle, WriteOptions writeOptions, + final byte[] keyBytes, final int keyLen, + final byte[] valueBytes, final int valueLen) throws RocksDBException { + if (!hold()) { + throw new IllegalStateException("rocksDB:" + this + " is not ready"); + } + try { + this.db.put(cfHandle, writeOptions, keyBytes, 0, keyLen, valueBytes, 0, valueLen); + } catch (RocksDBException e) { + scheduleReloadRocksdb(e); + LOGGER.error("put Failed. {}, {}", this.dbPath, getStatusError(e)); + throw e; + } finally { + release(); + } + } + + protected void put(ColumnFamilyHandle cfHandle, WriteOptions writeOptions, + final ByteBuffer keyBB, final ByteBuffer valueBB) throws RocksDBException { + if (!hold()) { + throw new IllegalStateException("rocksDB:" + this + " is not ready"); + } + try { + this.db.put(cfHandle, writeOptions, keyBB, valueBB); + } catch (RocksDBException e) { + scheduleReloadRocksdb(e); + LOGGER.error("put Failed. {}, {}", this.dbPath, getStatusError(e)); + throw e; + } finally { + release(); + } + } + + protected void batchPut(WriteOptions writeOptions, final WriteBatch batch) throws RocksDBException { + try { + this.db.write(writeOptions, batch); + } catch (RocksDBException e) { + scheduleReloadRocksdb(e); + LOGGER.error("batchPut Failed. {}, {}", this.dbPath, getStatusError(e)); + throw e; + } finally { + batch.clear(); + } + } + + protected byte[] get(ColumnFamilyHandle cfHandle, ReadOptions readOptions, byte[] keyBytes) throws RocksDBException { + if (!hold()) { + throw new IllegalStateException("rocksDB:" + this + " is not ready"); + } + try { + return this.db.get(cfHandle, readOptions, keyBytes); + } catch (RocksDBException e) { + LOGGER.error("get Failed. {}, {}", this.dbPath, getStatusError(e)); + throw e; + } finally { + release(); + } + } + + protected boolean get(ColumnFamilyHandle cfHandle, ReadOptions readOptions, + final ByteBuffer keyBB, final ByteBuffer valueBB) throws RocksDBException { + if (!hold()) { + throw new IllegalStateException("rocksDB:" + this + " is not ready"); + } + try { + return this.db.get(cfHandle, readOptions, keyBB, valueBB) != NOT_FOUND; + } catch (RocksDBException e) { + LOGGER.error("get Failed. {}, {}", this.dbPath, getStatusError(e)); + throw e; + } finally { + release(); + } + } + + protected List multiGet(final ReadOptions readOptions, + final List columnFamilyHandleList, + final List keys) throws RocksDBException { + if (!hold()) { + throw new IllegalStateException("rocksDB:" + this + " is not ready"); + } + try { + return this.db.multiGetAsList(readOptions, columnFamilyHandleList, keys); + } catch (RocksDBException e) { + LOGGER.error("multiGet Failed. {}, {}", this.dbPath, getStatusError(e)); + throw e; + } finally { + release(); + } + } + + protected void delete(ColumnFamilyHandle cfHandle, WriteOptions writeOptions, byte[] keyBytes) throws RocksDBException { + if (!hold()) { + throw new IllegalStateException("rocksDB:" + this + " is not ready"); + } + try { + this.db.delete(cfHandle, writeOptions, keyBytes); + } catch (RocksDBException e) { + LOGGER.error("delete Failed. {}, {}", this.dbPath, getStatusError(e)); + throw e; + } finally { + release(); + } + } + + protected void delete(ColumnFamilyHandle cfHandle, WriteOptions writeOptions, ByteBuffer keyBB) throws RocksDBException { + if (!hold()) { + throw new IllegalStateException("rocksDB:" + this + " is not ready"); + } + try { + this.db.delete(cfHandle, writeOptions, keyBB); + } catch (RocksDBException e) { + LOGGER.error("delete Failed. {}, {}", this.dbPath, getStatusError(e)); + throw e; + } finally { + release(); + } + } + + protected void rangeDelete(ColumnFamilyHandle cfHandle, WriteOptions writeOptions, + final byte[] startKey, final byte[] endKey) throws RocksDBException { + if (!hold()) { + throw new IllegalStateException("rocksDB:" + this + " is not ready"); + } + try { + this.db.deleteRange(cfHandle, writeOptions, startKey, endKey); + } catch (RocksDBException e) { + scheduleReloadRocksdb(e); + LOGGER.error("rangeDelete Failed. {}, {}", this.dbPath, getStatusError(e)); + throw e; + } finally { + release(); + } + } + + protected void manualCompactionDefaultCfRange(CompactRangeOptions compactRangeOptions) { + if (!hold()) { + return; + } + long s1 = System.currentTimeMillis(); + boolean result = true; + try { + LOGGER.info("manualCompaction Start. {}", this.dbPath); + this.db.compactRange(this.defaultCFHandle, null, null, compactRangeOptions); + } catch (RocksDBException e) { + result = false; + scheduleReloadRocksdb(e); + LOGGER.error("manualCompaction Failed. {}, {}", this.dbPath, getStatusError(e)); + } finally { + release(); + LOGGER.info("manualCompaction End. {}, rt: {}(ms), result: {}", this.dbPath, System.currentTimeMillis() - s1, result); + } + } + + protected void manualCompaction(long minPhyOffset, final CompactRangeOptions compactRangeOptions) { + this.manualCompactionThread.submit(new Runnable() { + @Override + public void run() { + manualCompactionDefaultCfRange(compactRangeOptions); + } + }); + } + + protected void open(final List cfDescriptors, + final List cfHandles) throws RocksDBException { + if (this.readOnly) { + this.db = RocksDB.openReadOnly(this.options, this.dbPath, cfDescriptors, cfHandles); + } else { + this.db = RocksDB.open(this.options, this.dbPath, cfDescriptors, cfHandles); + } + this.db.getEnv().setBackgroundThreads(8, Priority.HIGH); + this.db.getEnv().setBackgroundThreads(8, Priority.LOW); + + if (this.db == null) { + throw new RocksDBException("open rocksdb null"); + } + } + + protected abstract boolean postLoad(); + + public synchronized boolean start() { + if (this.loaded) { + return true; + } + if (postLoad()) { + this.loaded = true; + LOGGER.info("start OK. {}", this.dbPath); + this.closed = false; + return true; + } else { + return false; + } + } + + protected abstract void preShutdown(); + + public synchronized boolean shutdown() { + try { + if (!this.loaded) { + return true; + } + + final FlushOptions flushOptions = new FlushOptions(); + flushOptions.setWaitForFlush(true); + try { + flush(flushOptions); + } finally { + flushOptions.close(); + } + this.db.cancelAllBackgroundWork(true); + this.db.pauseBackgroundWork(); + //The close order is matter. + //1. close column family handles + preShutdown(); + + this.defaultCFHandle.close(); + //2. close column family options. + for (final ColumnFamilyOptions opt : this.cfOptions) { + opt.close(); + } + //3. close options + if (this.writeOptions != null) { + this.writeOptions.close(); + } + if (this.ableWalWriteOptions != null) { + this.ableWalWriteOptions.close(); + } + if (this.readOptions != null) { + this.readOptions.close(); + } + if (this.totalOrderReadOptions != null) { + this.totalOrderReadOptions.close(); + } + if (this.options != null) { + this.options.close(); + } + //4. close db. + if (db != null && !this.readOnly) { + this.db.syncWal(); + } + if (db != null) { + this.db.closeE(); + } + //5. help gc. + this.cfOptions.clear(); + this.db = null; + this.readOptions = null; + this.totalOrderReadOptions = null; + this.writeOptions = null; + this.ableWalWriteOptions = null; + this.options = null; + + this.loaded = false; + LOGGER.info("shutdown OK. {}", this.dbPath); + } catch (Exception e) { + LOGGER.error("shutdown Failed. {}", this.dbPath, e); + return false; + } + return true; + } + + public void flush(final FlushOptions flushOptions) { + if (!this.loaded || this.readOnly || closed) { + return; + } + + try { + if (db != null) { + this.db.flush(flushOptions); + } + } catch (RocksDBException e) { + scheduleReloadRocksdb(e); + LOGGER.error("flush Failed. {}, {}", this.dbPath, getStatusError(e)); + } + } + + public Statistics getStatistics() { + return this.options.statistics(); + } + + public ColumnFamilyHandle getDefaultCFHandle() { + return defaultCFHandle; + } + + public List getCompactionStatus() { + if (!hold()) { + return null; + } + try { + return this.db.getLiveFilesMetaData(); + } finally { + release(); + } + } + + private void scheduleReloadRocksdb(RocksDBException rocksDBException) { + if (rocksDBException == null || rocksDBException.getStatus() == null) { + return; + } + Status status = rocksDBException.getStatus(); + Status.Code code = status.getCode(); + // Status.Code.Incomplete == code + if (Status.Code.Aborted == code || Status.Code.Corruption == code || Status.Code.Undefined == code) { + LOGGER.error("scheduleReloadRocksdb. {}, {}", this.dbPath, getStatusError(rocksDBException)); + scheduleReloadRocksdb0(); + } + } + + private void scheduleReloadRocksdb0() { + if (!this.reloadPermit.tryAcquire()) { + return; + } + this.closed = true; + this.reloadScheduler.schedule(new Runnable() { + @Override + public void run() { + boolean result = true; + try { + reloadRocksdb(); + } catch (Exception e) { + result = false; + } finally { + reloadPermit.release(); + } + // try to reload rocksdb next time + if (!result) { + LOGGER.info("reload rocksdb Retry. {}", dbPath); + scheduleReloadRocksdb0(); + } + } + }, 10, TimeUnit.SECONDS); + } + + private void reloadRocksdb() throws Exception { + LOGGER.info("reload rocksdb Start. {}", this.dbPath); + if (!shutdown() || !start()) { + LOGGER.error("reload rocksdb Failed. {}", dbPath); + throw new Exception("reload rocksdb Error"); + } + LOGGER.info("reload rocksdb OK. {}", this.dbPath); + } + + public void flushWAL() throws RocksDBException { + this.db.flushWal(true); + } + + private String getStatusError(RocksDBException e) { + if (e == null || e.getStatus() == null) { + return "null"; + } + Status status = e.getStatus(); + StringBuilder sb = new StringBuilder(64); + sb.append("code: "); + if (status.getCode() != null) { + sb.append(status.getCode().name()); + } else { + sb.append("null"); + } + sb.append(", ").append("subCode: "); + if (status.getSubCode() != null) { + sb.append(status.getSubCode().name()); + } else { + sb.append("null"); + } + sb.append(", ").append("state: ").append(status.getState()); + return sb.toString(); + } + + public void statRocksdb(Logger logger) { + try { + + List liveFileMetaDataList = this.getCompactionStatus(); + if (liveFileMetaDataList == null || liveFileMetaDataList.isEmpty()) { + return; + } + Map map = Maps.newHashMap(); + for (LiveFileMetaData metaData : liveFileMetaDataList) { + StringBuilder sb = map.get(metaData.level()); + if (sb == null) { + sb = new StringBuilder(256); + map.put(metaData.level(), sb); + } + sb.append(new String(metaData.columnFamilyName(), DataConverter.CHARSET_UTF8)).append(SPACE). + append(metaData.fileName()).append(SPACE). + append("s: ").append(metaData.size()).append(SPACE). + append("a: ").append(metaData.numEntries()).append(SPACE). + append("r: ").append(metaData.numReadsSampled()).append(SPACE). + append("d: ").append(metaData.numDeletions()).append(SPACE). + append(metaData.beingCompacted()).append("\n"); + } + for (Map.Entry entry : map.entrySet()) { + logger.info("level: {}\n{}", entry.getKey(), entry.getValue().toString()); + } + + String blockCacheMemUsage = this.db.getProperty("rocksdb.block-cache-usage"); + String indexesAndFilterBlockMemUsage = this.db.getProperty("rocksdb.estimate-table-readers-mem"); + String memTableMemUsage = this.db.getProperty("rocksdb.cur-size-all-mem-tables"); + String blocksPinnedByIteratorMemUsage = this.db.getProperty("rocksdb.block-cache-pinned-usage"); + logger.info("MemUsage. blockCache: {}, indexesAndFilterBlock: {}, memtable: {}, blocksPinnedByIterator: {}", + blockCacheMemUsage, indexesAndFilterBlockMemUsage, memTableMemUsage, blocksPinnedByIteratorMemUsage); + } catch (Exception ignored) { + } + } +} \ No newline at end of file diff --git a/common/src/main/java/org/apache/rocketmq/common/config/ConfigRocksDBStorage.java b/common/src/main/java/org/apache/rocketmq/common/config/ConfigRocksDBStorage.java new file mode 100644 index 00000000000..b40f8046e84 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/config/ConfigRocksDBStorage.java @@ -0,0 +1,256 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.config; + +import java.io.File; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.UtilAll; +import org.rocksdb.BlockBasedTableConfig; +import org.rocksdb.BloomFilter; +import org.rocksdb.ColumnFamilyDescriptor; +import org.rocksdb.ColumnFamilyHandle; +import org.rocksdb.ColumnFamilyOptions; +import org.rocksdb.CompactRangeOptions; +import org.rocksdb.CompactRangeOptions.BottommostLevelCompaction; +import org.rocksdb.CompactionOptions; +import org.rocksdb.CompactionStyle; +import org.rocksdb.CompressionType; +import org.rocksdb.DBOptions; +import org.rocksdb.DataBlockIndexType; +import org.rocksdb.IndexType; +import org.rocksdb.InfoLogLevel; +import org.rocksdb.LRUCache; +import org.rocksdb.RateLimiter; +import org.rocksdb.ReadOptions; +import org.rocksdb.RocksDB; +import org.rocksdb.RocksDBException; +import org.rocksdb.RocksIterator; +import org.rocksdb.SkipListMemTableConfig; +import org.rocksdb.Statistics; +import org.rocksdb.StatsLevel; +import org.rocksdb.StringAppendOperator; +import org.rocksdb.WALRecoveryMode; +import org.rocksdb.WriteBatch; +import org.rocksdb.WriteOptions; +import org.rocksdb.util.SizeUnit; + +public class ConfigRocksDBStorage extends AbstractRocksDBStorage { + + public ConfigRocksDBStorage(final String dbPath) { + super(); + this.dbPath = dbPath; + this.readOnly = false; + } + + public ConfigRocksDBStorage(final String dbPath, boolean readOnly) { + super(); + this.dbPath = dbPath; + this.readOnly = readOnly; + } + + private void initOptions() { + this.options = createConfigDBOptions(); + + this.writeOptions = new WriteOptions(); + this.writeOptions.setSync(false); + this.writeOptions.setDisableWAL(true); + this.writeOptions.setNoSlowdown(true); + + this.ableWalWriteOptions = new WriteOptions(); + this.ableWalWriteOptions.setSync(false); + this.ableWalWriteOptions.setDisableWAL(false); + this.ableWalWriteOptions.setNoSlowdown(true); + + this.readOptions = new ReadOptions(); + this.readOptions.setPrefixSameAsStart(true); + this.readOptions.setTotalOrderSeek(false); + this.readOptions.setTailing(false); + + this.totalOrderReadOptions = new ReadOptions(); + this.totalOrderReadOptions.setPrefixSameAsStart(false); + this.totalOrderReadOptions.setTotalOrderSeek(false); + this.totalOrderReadOptions.setTailing(false); + + this.compactRangeOptions = new CompactRangeOptions(); + this.compactRangeOptions.setBottommostLevelCompaction(BottommostLevelCompaction.kForce); + this.compactRangeOptions.setAllowWriteStall(true); + this.compactRangeOptions.setExclusiveManualCompaction(false); + this.compactRangeOptions.setChangeLevel(true); + this.compactRangeOptions.setTargetLevel(-1); + this.compactRangeOptions.setMaxSubcompactions(4); + + this.compactionOptions = new CompactionOptions(); + this.compactionOptions.setCompression(CompressionType.LZ4_COMPRESSION); + this.compactionOptions.setMaxSubcompactions(4); + this.compactionOptions.setOutputFileSizeLimit(4 * 1024 * 1024 * 1024L); + } + + @Override + protected boolean postLoad() { + try { + UtilAll.ensureDirOK(this.dbPath); + + initOptions(); + + final List cfDescriptors = new ArrayList(); + + ColumnFamilyOptions defaultOptions = createConfigOptions(); + this.cfOptions.add(defaultOptions); + cfDescriptors.add(new ColumnFamilyDescriptor(RocksDB.DEFAULT_COLUMN_FAMILY, defaultOptions)); + + final List cfHandles = new ArrayList(); + open(cfDescriptors, cfHandles); + + this.defaultCFHandle = cfHandles.get(0); + } catch (final Exception e) { + AbstractRocksDBStorage.LOGGER.error("postLoad Failed. {}", this.dbPath, e); + return false; + } + return true; + } + + @Override + protected void preShutdown() { + + } + + private ColumnFamilyOptions createConfigOptions() { + BlockBasedTableConfig blockBasedTableConfig = new BlockBasedTableConfig(). + setFormatVersion(5). + setIndexType(IndexType.kBinarySearch). + setDataBlockIndexType(DataBlockIndexType.kDataBlockBinarySearch). + setBlockSize(32 * SizeUnit.KB). + setFilterPolicy(new BloomFilter(16, false)). + // Indicating if we'd put index/filter blocks to the block cache. + setCacheIndexAndFilterBlocks(false). + setCacheIndexAndFilterBlocksWithHighPriority(true). + setPinL0FilterAndIndexBlocksInCache(false). + setPinTopLevelIndexAndFilter(true). + setBlockCache(new LRUCache(4 * SizeUnit.MB, 8, false)). + setWholeKeyFiltering(true); + + ColumnFamilyOptions options = new ColumnFamilyOptions(); + return options.setMaxWriteBufferNumber(2). + // MemTable size, memtable(cache) -> immutable memtable(cache) -> sst(disk) + setWriteBufferSize(8 * SizeUnit.MB). + setMinWriteBufferNumberToMerge(1). + setTableFormatConfig(blockBasedTableConfig). + setMemTableConfig(new SkipListMemTableConfig()). + setCompressionType(CompressionType.NO_COMPRESSION). + setNumLevels(7). + setCompactionStyle(CompactionStyle.LEVEL). + setLevel0FileNumCompactionTrigger(4). + setLevel0SlowdownWritesTrigger(8). + setLevel0StopWritesTrigger(12). + // The target file size for compaction. + setTargetFileSizeBase(64 * SizeUnit.MB). + setTargetFileSizeMultiplier(2). + // The upper-bound of the total size of L1 files in bytes + setMaxBytesForLevelBase(256 * SizeUnit.MB). + setMaxBytesForLevelMultiplier(2). + setMergeOperator(new StringAppendOperator()). + setInplaceUpdateSupport(true); + } + + private DBOptions createConfigDBOptions() { + //Turn based on https://github.com/facebook/rocksdb/wiki/RocksDB-Tuning-Guide + // and http://gitlab.alibaba-inc.com/aloha/aloha/blob/branch_2_5_0/jstorm-core/src/main/java/com/alibaba/jstorm/cache/rocksdb/RocksDbOptionsFactory.java + DBOptions options = new DBOptions(); + Statistics statistics = new Statistics(); + statistics.setStatsLevel(StatsLevel.EXCEPT_DETAILED_TIMERS); + return options. + setDbLogDir(getDBLogDir()). + setInfoLogLevel(InfoLogLevel.INFO_LEVEL). + setWalRecoveryMode(WALRecoveryMode.SkipAnyCorruptedRecords). + setManualWalFlush(true). + setMaxTotalWalSize(500 * SizeUnit.MB). + setWalSizeLimitMB(0). + setWalTtlSeconds(0). + setCreateIfMissing(true). + setCreateMissingColumnFamilies(true). + setMaxOpenFiles(-1). + setMaxLogFileSize(1 * SizeUnit.GB). + setKeepLogFileNum(5). + setMaxManifestFileSize(1 * SizeUnit.GB). + setAllowConcurrentMemtableWrite(false). + setStatistics(statistics). + setStatsDumpPeriodSec(600). + setAtomicFlush(true). + setMaxBackgroundJobs(32). + setMaxSubcompactions(4). + setParanoidChecks(true). + setDelayedWriteRate(16 * SizeUnit.MB). + setRateLimiter(new RateLimiter(100 * SizeUnit.MB)). + setUseDirectIoForFlushAndCompaction(true). + setUseDirectReads(true); + } + + public static String getDBLogDir() { + String rootPath = System.getProperty("user.home"); + if (StringUtils.isEmpty(rootPath)) { + return ""; + } + rootPath = rootPath + File.separator + "logs"; + UtilAll.ensureDirOK(rootPath); + return rootPath + File.separator + "rocketmqlogs" + File.separator; + } + + public void put(final byte[] keyBytes, final int keyLen, final byte[] valueBytes) throws Exception { + put(this.defaultCFHandle, this.ableWalWriteOptions, keyBytes, keyLen, valueBytes, valueBytes.length); + } + + public void put(final ByteBuffer keyBB, final ByteBuffer valueBB) throws Exception { + put(this.defaultCFHandle, this.ableWalWriteOptions, keyBB, valueBB); + } + + public byte[] get(final byte[] keyBytes) throws Exception { + return get(this.defaultCFHandle, this.totalOrderReadOptions, keyBytes); + } + + public void delete(final byte[] keyBytes) throws Exception { + delete(this.defaultCFHandle, this.ableWalWriteOptions, keyBytes); + } + + public List multiGet(final List cfhList, final List keys) throws + RocksDBException { + return multiGet(this.totalOrderReadOptions, cfhList, keys); + } + + public void batchPut(final WriteBatch batch) throws RocksDBException { + batchPut(this.writeOptions, batch); + } + + public void batchPutWithWal(final WriteBatch batch) throws RocksDBException { + batchPut(this.ableWalWriteOptions, batch); + } + + public RocksIterator iterator() { + return this.db.newIterator(this.defaultCFHandle, this.totalOrderReadOptions); + } + + public void rangeDelete(final byte[] startKey, final byte[] endKey) throws RocksDBException { + rangeDelete(this.defaultCFHandle, this.writeOptions, startKey, endKey); + } + + public RocksIterator iterator(ReadOptions readOptions) { + return this.db.newIterator(this.defaultCFHandle, readOptions); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/config/RocksDBConfigManager.java b/common/src/main/java/org/apache/rocketmq/common/config/RocksDBConfigManager.java new file mode 100644 index 00000000000..d1ec894685f --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/config/RocksDBConfigManager.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.config; + +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.rocksdb.FlushOptions; +import org.rocksdb.RocksIterator; +import org.rocksdb.Statistics; +import org.rocksdb.WriteBatch; + +import java.util.function.BiConsumer; + +public class RocksDBConfigManager { + protected static final Logger BROKER_LOG = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + + protected volatile boolean isStop = false; + protected ConfigRocksDBStorage configRocksDBStorage = null; + private FlushOptions flushOptions = null; + private volatile long lastFlushMemTableMicroSecond = 0; + private final long memTableFlushInterval; + + public RocksDBConfigManager(long memTableFlushInterval) { + this.memTableFlushInterval = memTableFlushInterval; + } + + public boolean load(String configFilePath, BiConsumer biConsumer) { + this.isStop = false; + this.configRocksDBStorage = new ConfigRocksDBStorage(configFilePath); + if (!this.configRocksDBStorage.start()) { + return false; + } + RocksIterator iterator = this.configRocksDBStorage.iterator(); + try { + iterator.seekToFirst(); + while (iterator.isValid()) { + biConsumer.accept(iterator.key(), iterator.value()); + iterator.next(); + } + } finally { + iterator.close(); + } + + this.flushOptions = new FlushOptions(); + this.flushOptions.setWaitForFlush(false); + this.flushOptions.setAllowWriteStall(false); + return true; + } + + public void start() { + } + + public boolean stop() { + this.isStop = true; + if (this.configRocksDBStorage != null) { + return this.configRocksDBStorage.shutdown(); + } + if (this.flushOptions != null) { + this.flushOptions.close(); + } + return true; + } + + public void flushWAL() { + try { + if (this.isStop) { + return; + } + if (this.configRocksDBStorage != null) { + this.configRocksDBStorage.flushWAL(); + + long now = System.currentTimeMillis(); + if (now > this.lastFlushMemTableMicroSecond + this.memTableFlushInterval) { + this.configRocksDBStorage.flush(this.flushOptions); + this.lastFlushMemTableMicroSecond = now; + } + } + } catch (Exception e) { + BROKER_LOG.error("kv flush WAL Failed.", e); + } + } + + public void put(final byte[] keyBytes, final int keyLen, final byte[] valueBytes) throws Exception { + this.configRocksDBStorage.put(keyBytes, keyLen, valueBytes); + } + + public void delete(final byte[] keyBytes) throws Exception { + this.configRocksDBStorage.delete(keyBytes); + } + + public void batchPutWithWal(final WriteBatch batch) throws Exception { + this.configRocksDBStorage.batchPutWithWal(batch); + } + + public Statistics getStatistics() { + if (this.configRocksDBStorage == null) { + return null; + } + + return configRocksDBStorage.getStatistics(); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/consistenthash/ConsistentHashRouter.java b/common/src/main/java/org/apache/rocketmq/common/consistenthash/ConsistentHashRouter.java index fca1d877d45..08a58856fb6 100644 --- a/common/src/main/java/org/apache/rocketmq/common/consistenthash/ConsistentHashRouter.java +++ b/common/src/main/java/org/apache/rocketmq/common/consistenthash/ConsistentHashRouter.java @@ -16,6 +16,7 @@ */ package org.apache.rocketmq.common.consistenthash; +import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Collection; @@ -29,7 +30,7 @@ * algorithm */ public class ConsistentHashRouter { - private final SortedMap> ring = new TreeMap>(); + private final SortedMap> ring = new TreeMap<>(); private final HashFunction hashFunction; public ConsistentHashRouter(Collection pNodes, int vNodeCount) { @@ -64,7 +65,7 @@ public void addNode(T pNode, int vNodeCount) { throw new IllegalArgumentException("illegal virtual node counts :" + vNodeCount); int existingReplicas = getExistingReplicas(pNode); for (int i = 0; i < vNodeCount; i++) { - VirtualNode vNode = new VirtualNode(pNode, i + existingReplicas); + VirtualNode vNode = new VirtualNode<>(pNode, i + existingReplicas); ring.put(hashFunction.hash(vNode.getKey()), vNode); } } @@ -122,7 +123,7 @@ public MD5Hash() { @Override public long hash(String key) { instance.reset(); - instance.update(key.getBytes()); + instance.update(key.getBytes(StandardCharsets.UTF_8)); byte[] digest = instance.digest(); long h = 0; diff --git a/common/src/main/java/org/apache/rocketmq/common/constant/ConsumeInitMode.java b/common/src/main/java/org/apache/rocketmq/common/constant/ConsumeInitMode.java new file mode 100644 index 00000000000..b7091fa2767 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/constant/ConsumeInitMode.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.constant; + +public class ConsumeInitMode { + public static final int MIN = 0; + public static final int MAX = 1; +} diff --git a/common/src/main/java/org/apache/rocketmq/common/constant/DBMsgConstants.java b/common/src/main/java/org/apache/rocketmq/common/constant/DBMsgConstants.java index 8273aaa788b..8d2b3449765 100644 --- a/common/src/main/java/org/apache/rocketmq/common/constant/DBMsgConstants.java +++ b/common/src/main/java/org/apache/rocketmq/common/constant/DBMsgConstants.java @@ -18,5 +18,5 @@ package org.apache.rocketmq.common.constant; public class DBMsgConstants { - public static final int MAX_BODY_SIZE = 64 * 1024 * 1204; //64KB + public static final int MAX_BODY_SIZE = 64 * 1024 * 1024; //64KB } diff --git a/common/src/main/java/org/apache/rocketmq/common/constant/FIleReadaheadMode.java b/common/src/main/java/org/apache/rocketmq/common/constant/FIleReadaheadMode.java new file mode 100644 index 00000000000..e23a5f58789 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/constant/FIleReadaheadMode.java @@ -0,0 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.constant; + +public class FIleReadaheadMode { + public static final String READ_AHEAD_MODE = "READ_AHEAD_MODE"; +} \ No newline at end of file diff --git a/common/src/main/java/org/apache/rocketmq/common/constant/HAProxyConstants.java b/common/src/main/java/org/apache/rocketmq/common/constant/HAProxyConstants.java new file mode 100644 index 00000000000..c1ae0cca184 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/constant/HAProxyConstants.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.constant; + +public class HAProxyConstants { + + public static final String PROXY_PROTOCOL_PREFIX = "proxy_protocol_"; + public static final String PROXY_PROTOCOL_ADDR = PROXY_PROTOCOL_PREFIX + "addr"; + public static final String PROXY_PROTOCOL_PORT = PROXY_PROTOCOL_PREFIX + "port"; + public static final String PROXY_PROTOCOL_SERVER_ADDR = PROXY_PROTOCOL_PREFIX + "server_addr"; + public static final String PROXY_PROTOCOL_SERVER_PORT = PROXY_PROTOCOL_PREFIX + "server_port"; + public static final String PROXY_PROTOCOL_TLV_PREFIX = PROXY_PROTOCOL_PREFIX + "tlv_0x"; +} diff --git a/common/src/main/java/org/apache/rocketmq/common/constant/LoggerName.java b/common/src/main/java/org/apache/rocketmq/common/constant/LoggerName.java index 12070ddc34e..61310893f43 100644 --- a/common/src/main/java/org/apache/rocketmq/common/constant/LoggerName.java +++ b/common/src/main/java/org/apache/rocketmq/common/constant/LoggerName.java @@ -19,9 +19,15 @@ public class LoggerName { public static final String FILTERSRV_LOGGER_NAME = "RocketmqFiltersrv"; public static final String NAMESRV_LOGGER_NAME = "RocketmqNamesrv"; + public static final String NAMESRV_CONSOLE_LOGGER_NAME = "RocketmqNamesrvConsole"; + public static final String CONTROLLER_LOGGER_NAME = "RocketmqController"; + public static final String CONTROLLER_CONSOLE_NAME = "RocketmqControllerConsole"; + public static final String NAMESRV_WATER_MARK_LOGGER_NAME = "RocketmqNamesrvWaterMark"; public static final String BROKER_LOGGER_NAME = "RocketmqBroker"; public static final String BROKER_CONSOLE_NAME = "RocketmqConsole"; public static final String CLIENT_LOGGER_NAME = "RocketmqClient"; + public static final String ROCKETMQ_TRAFFIC_NAME = "RocketmqTraffic"; + public static final String ROCKETMQ_REMOTING_NAME = "RocketmqRemoting"; public static final String TOOLS_LOGGER_NAME = "RocketmqTools"; public static final String COMMON_LOGGER_NAME = "RocketmqCommon"; public static final String STORE_LOGGER_NAME = "RocketmqStore"; @@ -29,11 +35,22 @@ public class LoggerName { public static final String TRANSACTION_LOGGER_NAME = "RocketmqTransaction"; public static final String REBALANCE_LOCK_LOGGER_NAME = "RocketmqRebalanceLock"; public static final String ROCKETMQ_STATS_LOGGER_NAME = "RocketmqStats"; + public static final String DLQ_STATS_LOGGER_NAME = "RocketmqDLQStats"; + public static final String DLQ_LOGGER_NAME = "RocketmqDLQ"; + public static final String CONSUMER_STATS_LOGGER_NAME = "RocketmqConsumerStats"; public static final String COMMERCIAL_LOGGER_NAME = "RocketmqCommercial"; + public static final String ACCOUNT_LOGGER_NAME = "RocketmqAccount"; public static final String FLOW_CONTROL_LOGGER_NAME = "RocketmqFlowControl"; public static final String ROCKETMQ_AUTHORIZE_LOGGER_NAME = "RocketmqAuthorize"; public static final String DUPLICATION_LOGGER_NAME = "RocketmqDuplication"; public static final String PROTECTION_LOGGER_NAME = "RocketmqProtection"; public static final String WATER_MARK_LOGGER_NAME = "RocketmqWaterMark"; public static final String FILTER_LOGGER_NAME = "RocketmqFilter"; + public static final String ROCKETMQ_POP_LOGGER_NAME = "RocketmqPop"; + public static final String FAILOVER_LOGGER_NAME = "RocketmqFailover"; + public static final String STDOUT_LOGGER_NAME = "STDOUT"; + public static final String PROXY_LOGGER_NAME = "RocketmqProxy"; + public static final String PROXY_WATER_MARK_LOGGER_NAME = "RocketmqProxyWatermark"; + public static final String ROCKETMQ_COLDCTR_LOGGER_NAME = "RocketmqColdCtr"; + public static final String ROCKSDB_LOGGER_NAME = "RocketmqRocksDB"; } diff --git a/common/src/main/java/org/apache/rocketmq/common/constant/PermName.java b/common/src/main/java/org/apache/rocketmq/common/constant/PermName.java index 200dec24813..d87461d7f5a 100644 --- a/common/src/main/java/org/apache/rocketmq/common/constant/PermName.java +++ b/common/src/main/java/org/apache/rocketmq/common/constant/PermName.java @@ -17,13 +17,19 @@ package org.apache.rocketmq.common.constant; public class PermName { - public static final int PERM_PRIORITY = 0x1 << 3; - public static final int PERM_READ = 0x1 << 2; - public static final int PERM_WRITE = 0x1 << 1; - public static final int PERM_INHERIT = 0x1 << 0; + public static final int INDEX_PERM_PRIORITY = 3; + public static final int INDEX_PERM_READ = 2; + public static final int INDEX_PERM_WRITE = 1; + public static final int INDEX_PERM_INHERIT = 0; + + + public static final int PERM_PRIORITY = 0x1 << INDEX_PERM_PRIORITY; + public static final int PERM_READ = 0x1 << INDEX_PERM_READ; + public static final int PERM_WRITE = 0x1 << INDEX_PERM_WRITE; + public static final int PERM_INHERIT = 0x1 << INDEX_PERM_INHERIT; public static String perm2String(final int perm) { - final StringBuffer sb = new StringBuffer("---"); + final StringBuilder sb = new StringBuilder("---"); if (isReadable(perm)) { sb.replace(0, 1, "R"); } @@ -50,4 +56,16 @@ public static boolean isWriteable(final int perm) { public static boolean isInherited(final int perm) { return (perm & PERM_INHERIT) == PERM_INHERIT; } + + public static boolean isValid(final String perm) { + return isValid(Integer.parseInt(perm)); + } + + public static boolean isValid(final int perm) { + return perm >= 0 && perm < PERM_PRIORITY; + } + + public static boolean isPriority(final int perm) { + return (perm & PERM_PRIORITY) == PERM_PRIORITY; + } } diff --git a/common/src/main/java/org/apache/rocketmq/common/consumer/ReceiptHandle.java b/common/src/main/java/org/apache/rocketmq/common/consumer/ReceiptHandle.java new file mode 100644 index 00000000000..daaaee5600d --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/consumer/ReceiptHandle.java @@ -0,0 +1,237 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.consumer; + +import java.util.Arrays; +import java.util.List; +import org.apache.rocketmq.common.KeyBuilder; +import org.apache.rocketmq.common.message.MessageConst; + +public class ReceiptHandle { + private static final String SEPARATOR = MessageConst.KEY_SEPARATOR; + public static final String NORMAL_TOPIC = "0"; + public static final String RETRY_TOPIC = "1"; + + public static final String RETRY_TOPIC_V2 = "2"; + private final long startOffset; + private final long retrieveTime; + private final long invisibleTime; + private final long nextVisibleTime; + private final int reviveQueueId; + private final String topicType; + private final String brokerName; + private final int queueId; + private final long offset; + private final long commitLogOffset; + private final String receiptHandle; + + public String encode() { + return startOffset + SEPARATOR + retrieveTime + SEPARATOR + invisibleTime + SEPARATOR + reviveQueueId + + SEPARATOR + topicType + SEPARATOR + brokerName + SEPARATOR + queueId + SEPARATOR + offset + SEPARATOR + + commitLogOffset; + } + + public boolean isExpired() { + return nextVisibleTime <= System.currentTimeMillis(); + } + + public static ReceiptHandle decode(String receiptHandle) { + List dataList = Arrays.asList(receiptHandle.split(SEPARATOR)); + if (dataList.size() < 8) { + throw new IllegalArgumentException("Parse failed, dataList size " + dataList.size()); + } + long startOffset = Long.parseLong(dataList.get(0)); + long retrieveTime = Long.parseLong(dataList.get(1)); + long invisibleTime = Long.parseLong(dataList.get(2)); + int reviveQueueId = Integer.parseInt(dataList.get(3)); + String topicType = dataList.get(4); + String brokerName = dataList.get(5); + int queueId = Integer.parseInt(dataList.get(6)); + long offset = Long.parseLong(dataList.get(7)); + long commitLogOffset = -1L; + if (dataList.size() >= 9) { + commitLogOffset = Long.parseLong(dataList.get(8)); + } + + return new ReceiptHandleBuilder() + .startOffset(startOffset) + .retrieveTime(retrieveTime) + .invisibleTime(invisibleTime) + .reviveQueueId(reviveQueueId) + .topicType(topicType) + .brokerName(brokerName) + .queueId(queueId) + .offset(offset) + .commitLogOffset(commitLogOffset) + .receiptHandle(receiptHandle).build(); + } + + ReceiptHandle(final long startOffset, final long retrieveTime, final long invisibleTime, final long nextVisibleTime, + final int reviveQueueId, final String topicType, final String brokerName, final int queueId, final long offset, + final long commitLogOffset, final String receiptHandle) { + this.startOffset = startOffset; + this.retrieveTime = retrieveTime; + this.invisibleTime = invisibleTime; + this.nextVisibleTime = nextVisibleTime; + this.reviveQueueId = reviveQueueId; + this.topicType = topicType; + this.brokerName = brokerName; + this.queueId = queueId; + this.offset = offset; + this.commitLogOffset = commitLogOffset; + this.receiptHandle = receiptHandle; + } + + public static class ReceiptHandleBuilder { + private long startOffset; + private long retrieveTime; + private long invisibleTime; + private int reviveQueueId; + private String topicType; + private String brokerName; + private int queueId; + private long offset; + private long commitLogOffset; + private String receiptHandle; + + ReceiptHandleBuilder() { + } + + public ReceiptHandle.ReceiptHandleBuilder startOffset(final long startOffset) { + this.startOffset = startOffset; + return this; + } + + public ReceiptHandle.ReceiptHandleBuilder retrieveTime(final long retrieveTime) { + this.retrieveTime = retrieveTime; + return this; + } + + public ReceiptHandle.ReceiptHandleBuilder invisibleTime(final long invisibleTime) { + this.invisibleTime = invisibleTime; + return this; + } + + public ReceiptHandle.ReceiptHandleBuilder reviveQueueId(final int reviveQueueId) { + this.reviveQueueId = reviveQueueId; + return this; + } + + public ReceiptHandle.ReceiptHandleBuilder topicType(final String topicType) { + this.topicType = topicType; + return this; + } + + public ReceiptHandle.ReceiptHandleBuilder brokerName(final String brokerName) { + this.brokerName = brokerName; + return this; + } + + public ReceiptHandle.ReceiptHandleBuilder queueId(final int queueId) { + this.queueId = queueId; + return this; + } + + public ReceiptHandle.ReceiptHandleBuilder offset(final long offset) { + this.offset = offset; + return this; + } + + public ReceiptHandle.ReceiptHandleBuilder commitLogOffset(final long commitLogOffset) { + this.commitLogOffset = commitLogOffset; + return this; + } + + public ReceiptHandle.ReceiptHandleBuilder receiptHandle(final String receiptHandle) { + this.receiptHandle = receiptHandle; + return this; + } + + public ReceiptHandle build() { + return new ReceiptHandle(this.startOffset, this.retrieveTime, this.invisibleTime, this.retrieveTime + this.invisibleTime, + this.reviveQueueId, this.topicType, this.brokerName, this.queueId, this.offset, this.commitLogOffset, this.receiptHandle); + } + + @Override + public String toString() { + return "ReceiptHandle.ReceiptHandleBuilder(startOffset=" + this.startOffset + ", retrieveTime=" + this.retrieveTime + ", invisibleTime=" + this.invisibleTime + ", reviveQueueId=" + this.reviveQueueId + ", topic=" + this.topicType + ", brokerName=" + this.brokerName + ", queueId=" + this.queueId + ", offset=" + this.offset + ", commitLogOffset=" + this.commitLogOffset + ", receiptHandle=" + this.receiptHandle + ")"; + } + } + + public static ReceiptHandle.ReceiptHandleBuilder builder() { + return new ReceiptHandle.ReceiptHandleBuilder(); + } + + public long getStartOffset() { + return this.startOffset; + } + + public long getRetrieveTime() { + return this.retrieveTime; + } + + public long getInvisibleTime() { + return this.invisibleTime; + } + + public long getNextVisibleTime() { + return this.nextVisibleTime; + } + + public int getReviveQueueId() { + return this.reviveQueueId; + } + + public String getTopicType() { + return this.topicType; + } + + public String getBrokerName() { + return this.brokerName; + } + + public int getQueueId() { + return this.queueId; + } + + public long getOffset() { + return this.offset; + } + + public long getCommitLogOffset() { + return commitLogOffset; + } + + public String getReceiptHandle() { + return this.receiptHandle; + } + + public boolean isRetryTopic() { + return RETRY_TOPIC.equals(topicType) || RETRY_TOPIC_V2.equals(topicType); + } + + public String getRealTopic(String topic, String groupName) { + if (RETRY_TOPIC.equals(topicType)) { + return KeyBuilder.buildPopRetryTopicV1(topic, groupName); + } + if (RETRY_TOPIC_V2.equals(topicType)) { + return KeyBuilder.buildPopRetryTopicV2(topic, groupName); + } + return topic; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/fastjson/GenericMapSuperclassDeserializer.java b/common/src/main/java/org/apache/rocketmq/common/fastjson/GenericMapSuperclassDeserializer.java new file mode 100644 index 00000000000..80a1554d123 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/fastjson/GenericMapSuperclassDeserializer.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.fastjson; + +import com.alibaba.fastjson.JSONException; +import com.alibaba.fastjson.parser.DefaultJSONParser; +import com.alibaba.fastjson.parser.JSONToken; +import com.alibaba.fastjson.parser.deserializer.MapDeserializer; +import com.alibaba.fastjson.parser.deserializer.ObjectDeserializer; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Map; + +/** + * workaround https://github.com/alibaba/fastjson/issues/3730 + */ +public class GenericMapSuperclassDeserializer implements ObjectDeserializer { + public static final GenericMapSuperclassDeserializer INSTANCE = new GenericMapSuperclassDeserializer(); + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public T deserialze(DefaultJSONParser parser, Type type, Object fieldName) { + Class clz = (Class) type; + Type genericSuperclass = clz.getGenericSuperclass(); + Map map; + try { + map = (Map) clz.newInstance(); + } catch (Exception e) { + throw new JSONException("unsupport type " + type, e); + } + ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass; + Type keyType = parameterizedType.getActualTypeArguments()[0]; + Type valueType = parameterizedType.getActualTypeArguments()[1]; + if (String.class == keyType) { + return (T) MapDeserializer.parseMap(parser, (Map) map, valueType, fieldName); + } else { + return (T) MapDeserializer.parseMap(parser, map, keyType, valueType, fieldName); + } + } + + @Override + public int getFastMatchToken() { + return JSONToken.LBRACE; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/filter/ExpressionType.java b/common/src/main/java/org/apache/rocketmq/common/filter/ExpressionType.java index 3b7940abc7e..bc37733ed8c 100644 --- a/common/src/main/java/org/apache/rocketmq/common/filter/ExpressionType.java +++ b/common/src/main/java/org/apache/rocketmq/common/filter/ExpressionType.java @@ -59,7 +59,7 @@ public class ExpressionType { public static final String TAG = "TAG"; public static boolean isTagType(String type) { - if (type == null || TAG.equals(type)) { + if (type == null || "".equals(type) || TAG.equals(type)) { return true; } return false; diff --git a/common/src/main/java/org/apache/rocketmq/common/filter/FilterAPI.java b/common/src/main/java/org/apache/rocketmq/common/filter/FilterAPI.java deleted file mode 100644 index 9268a6ef45d..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/filter/FilterAPI.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.common.filter; - -import java.net.URL; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; - -public class FilterAPI { - public static URL classFile(final String className) { - final String javaSource = simpleClassName(className) + ".java"; - URL url = FilterAPI.class.getClassLoader().getResource(javaSource); - return url; - } - - public static String simpleClassName(final String className) { - String simple = className; - int index = className.lastIndexOf("."); - if (index >= 0) { - simple = className.substring(index + 1); - } - - return simple; - } - - public static SubscriptionData buildSubscriptionData(final String consumerGroup, String topic, - String subString) throws Exception { - SubscriptionData subscriptionData = new SubscriptionData(); - subscriptionData.setTopic(topic); - subscriptionData.setSubString(subString); - - if (null == subString || subString.equals(SubscriptionData.SUB_ALL) || subString.length() == 0) { - subscriptionData.setSubString(SubscriptionData.SUB_ALL); - } else { - String[] tags = subString.split("\\|\\|"); - if (tags.length > 0) { - for (String tag : tags) { - if (tag.length() > 0) { - String trimString = tag.trim(); - if (trimString.length() > 0) { - subscriptionData.getTagsSet().add(trimString); - subscriptionData.getCodeSet().add(trimString.hashCode()); - } - } - } - } else { - throw new Exception("subString split error"); - } - } - - return subscriptionData; - } - - public static SubscriptionData build(final String topic, final String subString, - final String type) throws Exception { - if (ExpressionType.TAG.equals(type) || type == null) { - return buildSubscriptionData(null, topic, subString); - } - - if (subString == null || subString.length() < 1) { - throw new IllegalArgumentException("Expression can't be null! " + type); - } - - SubscriptionData subscriptionData = new SubscriptionData(); - subscriptionData.setTopic(topic); - subscriptionData.setSubString(subString); - subscriptionData.setExpressionType(type); - - return subscriptionData; - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/filter/impl/PolishExpr.java b/common/src/main/java/org/apache/rocketmq/common/filter/impl/PolishExpr.java index cdc6187a148..e80073f5d40 100644 --- a/common/src/main/java/org/apache/rocketmq/common/filter/impl/PolishExpr.java +++ b/common/src/main/java/org/apache/rocketmq/common/filter/impl/PolishExpr.java @@ -38,8 +38,8 @@ public static List reversePolish(String expression) { * @return the compute result of Shunting-yard algorithm */ public static List reversePolish(List tokens) { - List segments = new ArrayList(); - Stack operatorStack = new Stack(); + List segments = new ArrayList<>(); + Stack operatorStack = new Stack<>(); for (int i = 0; i < tokens.size(); i++) { Op token = tokens.get(i); @@ -87,7 +87,7 @@ public static List reversePolish(List tokens) { * @throws Exception */ private static List participle(String expression) { - List segments = new ArrayList(); + List segments = new ArrayList<>(); int size = expression.length(); int wordStartIndex = -1; @@ -97,8 +97,8 @@ private static List participle(String expression) { for (int i = 0; i < size; i++) { int chValue = (int) expression.charAt(i); - if ((97 <= chValue && chValue <= 122) || (65 <= chValue && chValue <= 90) - || (49 <= chValue && chValue <= 57) || 95 == chValue) { + if (97 <= chValue && chValue <= 122 || 65 <= chValue && chValue <= 90 + || 49 <= chValue && chValue <= 57 || 95 == chValue) { if (Type.OPERATOR == preType || Type.SEPAERATOR == preType || Type.NULL == preType || Type.PARENTHESIS == preType) { diff --git a/broker/src/main/java/org/apache/rocketmq/broker/latency/FutureTaskExt.java b/common/src/main/java/org/apache/rocketmq/common/future/FutureTaskExt.java similarity index 96% rename from broker/src/main/java/org/apache/rocketmq/broker/latency/FutureTaskExt.java rename to common/src/main/java/org/apache/rocketmq/common/future/FutureTaskExt.java index f132efaebcc..1bd46eafa2c 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/latency/FutureTaskExt.java +++ b/common/src/main/java/org/apache/rocketmq/common/future/FutureTaskExt.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.broker.latency; +package org.apache.rocketmq.common.future; import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; diff --git a/common/src/main/java/org/apache/rocketmq/common/help/FAQUrl.java b/common/src/main/java/org/apache/rocketmq/common/help/FAQUrl.java index 5d950bebfb1..4a6588e4124 100644 --- a/common/src/main/java/org/apache/rocketmq/common/help/FAQUrl.java +++ b/common/src/main/java/org/apache/rocketmq/common/help/FAQUrl.java @@ -18,51 +18,39 @@ public class FAQUrl { - public static final String APPLY_TOPIC_URL = - "http://rocketmq.apache.org/docs/faq/"; + public static final String APPLY_TOPIC_URL = "https://rocketmq.apache.org/docs/bestPractice/06FAQ"; - public static final String NAME_SERVER_ADDR_NOT_EXIST_URL = - "http://rocketmq.apache.org/docs/faq/"; + public static final String NAME_SERVER_ADDR_NOT_EXIST_URL = "https://rocketmq.apache.org/docs/bestPractice/06FAQ"; - public static final String GROUP_NAME_DUPLICATE_URL = - "http://rocketmq.apache.org/docs/faq/"; + public static final String GROUP_NAME_DUPLICATE_URL = "https://rocketmq.apache.org/docs/bestPractice/06FAQ"; - public static final String CLIENT_PARAMETER_CHECK_URL = - "http://rocketmq.apache.org/docs/faq/"; + public static final String CLIENT_PARAMETER_CHECK_URL = "https://rocketmq.apache.org/docs/bestPractice/06FAQ"; - public static final String SUBSCRIPTION_GROUP_NOT_EXIST = - "http://rocketmq.apache.org/docs/faq/"; + public static final String SUBSCRIPTION_GROUP_NOT_EXIST = "https://rocketmq.apache.org/docs/bestPractice/06FAQ"; - public static final String CLIENT_SERVICE_NOT_OK = - "http://rocketmq.apache.org/docs/faq/"; + public static final String CLIENT_SERVICE_NOT_OK = "https://rocketmq.apache.org/docs/bestPractice/06FAQ"; // FAQ: No route info of this topic, TopicABC - public static final String NO_TOPIC_ROUTE_INFO = - "http://rocketmq.apache.org/docs/faq/"; + public static final String NO_TOPIC_ROUTE_INFO = "https://rocketmq.apache.org/docs/bestPractice/06FAQ"; - public static final String LOAD_JSON_EXCEPTION = - "http://rocketmq.apache.org/docs/faq/"; + public static final String LOAD_JSON_EXCEPTION = "https://rocketmq.apache.org/docs/bestPractice/06FAQ"; - public static final String SAME_GROUP_DIFFERENT_TOPIC = - "http://rocketmq.apache.org/docs/faq/"; + public static final String SAME_GROUP_DIFFERENT_TOPIC = "https://rocketmq.apache.org/docs/bestPractice/06FAQ"; - public static final String MQLIST_NOT_EXIST = - "http://rocketmq.apache.org/docs/faq/"; + public static final String MQLIST_NOT_EXIST = "https://rocketmq.apache.org/docs/bestPractice/06FAQ"; - public static final String UNEXPECTED_EXCEPTION_URL = - "http://rocketmq.apache.org/docs/faq/"; + public static final String UNEXPECTED_EXCEPTION_URL = "https://rocketmq.apache.org/docs/bestPractice/06FAQ"; - public static final String SEND_MSG_FAILED = - "http://rocketmq.apache.org/docs/faq/"; + public static final String SEND_MSG_FAILED = "https://rocketmq.apache.org/docs/bestPractice/06FAQ"; - public static final String UNKNOWN_HOST_EXCEPTION = - "http://rocketmq.apache.org/docs/faq/"; + public static final String UNKNOWN_HOST_EXCEPTION = "https://rocketmq.apache.org/docs/bestPractice/06FAQ"; private static final String TIP_STRING_BEGIN = "\nSee "; private static final String TIP_STRING_END = " for further details."; + private static final String MORE_INFORMATION = "For more information, please visit the url, "; public static String suggestTodo(final String url) { - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(TIP_STRING_BEGIN.length() + url.length() + TIP_STRING_END.length()); sb.append(TIP_STRING_BEGIN); sb.append(url); sb.append(TIP_STRING_END); @@ -73,10 +61,10 @@ public static String attachDefaultURL(final String errorMessage) { if (errorMessage != null) { int index = errorMessage.indexOf(TIP_STRING_BEGIN); if (-1 == index) { - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(errorMessage.length() + UNEXPECTED_EXCEPTION_URL.length() + MORE_INFORMATION.length() + 1); sb.append(errorMessage); sb.append("\n"); - sb.append("For more information, please visit the url, "); + sb.append(MORE_INFORMATION); sb.append(UNEXPECTED_EXCEPTION_URL); return sb.toString(); } diff --git a/common/src/main/java/org/apache/rocketmq/common/hook/FilterCheckHook.java b/common/src/main/java/org/apache/rocketmq/common/hook/FilterCheckHook.java index e72fb826013..f57df26d6fe 100644 --- a/common/src/main/java/org/apache/rocketmq/common/hook/FilterCheckHook.java +++ b/common/src/main/java/org/apache/rocketmq/common/hook/FilterCheckHook.java @@ -20,7 +20,7 @@ import java.nio.ByteBuffer; public interface FilterCheckHook { - public String hookName(); + String hookName(); - public boolean isFilterMatched(final boolean isUnitMode, final ByteBuffer byteBuffer); + boolean isFilterMatched(final boolean isUnitMode, final ByteBuffer byteBuffer); } diff --git a/common/src/main/java/org/apache/rocketmq/common/logging/DefaultJoranConfiguratorExt.java b/common/src/main/java/org/apache/rocketmq/common/logging/DefaultJoranConfiguratorExt.java new file mode 100644 index 00000000000..19164e3c6d4 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/logging/DefaultJoranConfiguratorExt.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.logging; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.apache.rocketmq.logging.ch.qos.logback.classic.ClassicConstants; +import org.apache.rocketmq.logging.ch.qos.logback.classic.LoggerContext; +import org.apache.rocketmq.logging.ch.qos.logback.classic.util.DefaultJoranConfigurator; +import org.apache.rocketmq.logging.ch.qos.logback.core.LogbackException; +import org.apache.rocketmq.logging.ch.qos.logback.core.joran.spi.JoranException; +import org.apache.rocketmq.logging.ch.qos.logback.core.status.InfoStatus; +import org.apache.rocketmq.logging.ch.qos.logback.core.status.StatusManager; +import org.apache.rocketmq.logging.ch.qos.logback.core.util.Loader; +import org.apache.rocketmq.logging.ch.qos.logback.core.util.OptionHelper; + +public class DefaultJoranConfiguratorExt extends DefaultJoranConfigurator { + + final public static String TEST_AUTOCONFIG_FILE = "rmq.logback-test.xml"; + final public static String AUTOCONFIG_FILE = "rmq.logback.xml"; + + final public static String PROXY_AUTOCONFIG_FILE = "rmq.proxy.logback.xml"; + final public static String BROKER_AUTOCONFIG_FILE = "rmq.broker.logback.xml"; + + final public static String NAMESRV_AUTOCONFIG_FILE = "rmq.namesrv.logback.xml"; + final public static String CONTROLLER_AUTOCONFIG_FILE = "rmq.controller.logback.xml"; + final public static String TOOLS_AUTOCONFIG_FILE = "rmq.tools.logback.xml"; + + final public static String CLIENT_AUTOCONFIG_FILE = "rmq.client.logback.xml"; + + private final List configFiles; + + public DefaultJoranConfiguratorExt() { + this.configFiles = new ArrayList<>(); + configFiles.add(TEST_AUTOCONFIG_FILE); + configFiles.add(AUTOCONFIG_FILE); + configFiles.add(PROXY_AUTOCONFIG_FILE); + configFiles.add(BROKER_AUTOCONFIG_FILE); + configFiles.add(NAMESRV_AUTOCONFIG_FILE); + configFiles.add(CONTROLLER_AUTOCONFIG_FILE); + configFiles.add(TOOLS_AUTOCONFIG_FILE); + configFiles.add(CLIENT_AUTOCONFIG_FILE); + } + + @Override + public ExecutionStatus configure(LoggerContext loggerContext) { + URL url = findURLOfDefaultConfigurationFile(true); + if (url != null) { + try { + configureByResource(url); + } catch (JoranException e) { + e.printStackTrace(); + } + } + // skip other configurator on purpose. + return ExecutionStatus.DO_NOT_INVOKE_NEXT_IF_ANY; + } + + public void configureByResource(URL url) throws JoranException { + if (url == null) { + throw new IllegalArgumentException("URL argument cannot be null"); + } + final String urlString = url.toString(); + if (urlString.endsWith("xml")) { + JoranConfiguratorExt configurator = new JoranConfiguratorExt(); + configurator.setContext(context); + configurator.doConfigure0(url); + } else { + throw new LogbackException( + "Unexpected filename extension of file [" + url + "]. Should be .xml"); + } + } + + public URL findURLOfDefaultConfigurationFile(boolean updateStatus) { + ClassLoader myClassLoader = Loader.getClassLoaderOfObject(this); + URL url = findConfigFileURLFromSystemProperties(myClassLoader, updateStatus); + if (url != null) { + return url; + } + + for (String configFile : configFiles) { + url = getResource(configFile, myClassLoader, updateStatus); + if (url != null) { + return url; + } + } + return null; + } + + private URL findConfigFileURLFromSystemProperties(ClassLoader classLoader, boolean updateStatus) { + String logbackConfigFile = OptionHelper.getSystemProperty(ClassicConstants.CONFIG_FILE_PROPERTY); + if (logbackConfigFile != null) { + URL result = null; + try { + result = new URL(logbackConfigFile); + return result; + } catch (MalformedURLException e) { + // so, resource is not a URL: + // attempt to get the resource from the class path + result = Loader.getResource(logbackConfigFile, classLoader); + if (result != null) { + return result; + } + File f = new File(logbackConfigFile); + if (f.exists() && f.isFile()) { + try { + result = f.toURI().toURL(); + return result; + } catch (MalformedURLException ignored) { + } + } + } finally { + if (updateStatus) { + statusOnResourceSearch(logbackConfigFile, classLoader, result); + } + } + } + return null; + } + + private URL getResource(String filename, ClassLoader myClassLoader, boolean updateStatus) { + URL url = Loader.getResource(filename, myClassLoader); + if (updateStatus) { + statusOnResourceSearch(filename, myClassLoader, url); + } + return url; + } + + private void statusOnResourceSearch(String resourceName, ClassLoader classLoader, URL url) { + StatusManager sm = context.getStatusManager(); + if (url == null) { + sm.add(new InfoStatus("Could NOT find resource [" + resourceName + "]", context)); + } else { + sm.add(new InfoStatus("Found resource [" + resourceName + "] at [" + url.toString() + "]", context)); + multiplicityWarning(resourceName, classLoader); + } + } + + private void multiplicityWarning(String resourceName, ClassLoader classLoader) { + Set urlSet = null; + try { + urlSet = Loader.getResources(resourceName, classLoader); + } catch (IOException e) { + addError("Failed to get url list for resource [" + resourceName + "]", e); + } + if (urlSet != null && urlSet.size() > 1) { + addWarn("Resource [" + resourceName + "] occurs multiple times on the classpath."); + for (URL url : urlSet) { + addWarn("Resource [" + resourceName + "] occurs at [" + url.toString() + "]"); + } + } + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/logging/JoranConfiguratorExt.java b/common/src/main/java/org/apache/rocketmq/common/logging/JoranConfiguratorExt.java new file mode 100644 index 00000000000..6995e9a4934 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/logging/JoranConfiguratorExt.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.logging; + +import com.google.common.io.CharStreams; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import org.apache.rocketmq.logging.ch.qos.logback.classic.joran.JoranConfigurator; +import org.apache.rocketmq.logging.ch.qos.logback.core.joran.spi.JoranException; + +public class JoranConfiguratorExt extends JoranConfigurator { + private InputStream transformXml(InputStream in) throws IOException { + try { + String str = CharStreams.toString(new InputStreamReader(in, StandardCharsets.UTF_8)); + str = str.replace("\"ch.qos.logback", "\"org.apache.rocketmq.logging.ch.qos.logback"); + return new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + } finally { + if (null != in) { + in.close(); + } + } + } + + public final void doConfigure0(URL url) throws JoranException { + InputStream in = null; + try { + informContextOfURLUsedForConfiguration(getContext(), url); + URLConnection urlConnection = url.openConnection(); + // per http://jira.qos.ch/browse/LBCORE-105 + // per http://jira.qos.ch/browse/LBCORE-127 + urlConnection.setUseCaches(false); + + InputStream temp = urlConnection.getInputStream(); + in = transformXml(temp); + + doConfigure(in, url.toExternalForm()); + } catch (IOException ioe) { + String errMsg = "Could not open URL [" + url + "]."; + addError(errMsg, ioe); + throw new JoranException(errMsg, ioe); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException ioe) { + String errMsg = "Could not close input stream"; + addError(errMsg, ioe); + throw new JoranException(errMsg, ioe); + } + } + } + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/message/Message.java b/common/src/main/java/org/apache/rocketmq/common/message/Message.java index 15ba2142c3e..c7997c47318 100644 --- a/common/src/main/java/org/apache/rocketmq/common/message/Message.java +++ b/common/src/main/java/org/apache/rocketmq/common/message/Message.java @@ -17,6 +17,7 @@ package org.apache.rocketmq.common.message; import java.io.Serializable; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -28,6 +29,7 @@ public class Message implements Serializable { private int flag; private Map properties; private byte[] body; + private String transactionId; public Message() { } @@ -41,11 +43,13 @@ public Message(String topic, String tags, String keys, int flag, byte[] body, bo this.flag = flag; this.body = body; - if (tags != null && tags.length() > 0) + if (tags != null && tags.length() > 0) { this.setTags(tags); + } - if (keys != null && keys.length() > 0) + if (keys != null && keys.length() > 0) { this.setKeys(keys); + } this.setWaitStoreMsgOK(waitStoreMsgOK); } @@ -64,7 +68,7 @@ public void setKeys(String keys) { void putProperty(final String name, final String value) { if (null == this.properties) { - this.properties = new HashMap(); + this.properties = new HashMap<>(); } this.properties.put(name, value); @@ -98,7 +102,7 @@ public String getUserProperty(final String name) { public String getProperty(final String name) { if (null == this.properties) { - this.properties = new HashMap(); + this.properties = new HashMap<>(); } return this.properties.get(name); @@ -124,14 +128,10 @@ public String getKeys() { return this.getProperty(MessageConst.PROPERTY_KEYS); } - public void setKeys(Collection keys) { - StringBuffer sb = new StringBuffer(); - for (String k : keys) { - sb.append(k); - sb.append(MessageConst.KEY_SEPARATOR); - } + public void setKeys(Collection keyCollection) { + String keys = String.join(MessageConst.KEY_SEPARATOR, keyCollection); - this.setKeys(sb.toString().trim()); + this.setKeys(keys); } public int getDelayTimeLevel() { @@ -149,8 +149,9 @@ public void setDelayTimeLevel(int level) { public boolean isWaitStoreMsgOK() { String result = this.getProperty(MessageConst.PROPERTY_WAIT_STORE_MSG_OK); - if (null == result) + if (null == result) { return true; + } return Boolean.parseBoolean(result); } @@ -159,6 +160,10 @@ public void setWaitStoreMsgOK(boolean waitStoreMsgOK) { this.putProperty(MessageConst.PROPERTY_WAIT_STORE_MSG_OK, Boolean.toString(waitStoreMsgOK)); } + public void setInstanceId(String instanceId) { + this.putProperty(MessageConst.PROPERTY_INSTANCE_ID, instanceId); + } + public int getFlag() { return flag; } @@ -191,9 +196,58 @@ public void setBuyerId(String buyerId) { putProperty(MessageConst.PROPERTY_BUYER_ID, buyerId); } + public String getTransactionId() { + return transactionId; + } + + public void setTransactionId(String transactionId) { + this.transactionId = transactionId; + } + @Override public String toString() { - return "Message [topic=" + topic + ", flag=" + flag + ", properties=" + properties + ", body=" - + (body != null ? body.length : 0) + "]"; + return "Message{" + + "topic='" + topic + '\'' + + ", flag=" + flag + + ", properties=" + properties + + ", body=" + Arrays.toString(body) + + ", transactionId='" + transactionId + '\'' + + '}'; + } + + public void setDelayTimeSec(long sec) { + this.putProperty(MessageConst.PROPERTY_TIMER_DELAY_SEC, String.valueOf(sec)); + } + + public long getDelayTimeSec() { + String t = this.getProperty(MessageConst.PROPERTY_TIMER_DELAY_SEC); + if (t != null) { + return Long.parseLong(t); + } + return 0; + } + + public void setDelayTimeMs(long timeMs) { + this.putProperty(MessageConst.PROPERTY_TIMER_DELAY_MS, String.valueOf(timeMs)); + } + + public long getDelayTimeMs() { + String t = this.getProperty(MessageConst.PROPERTY_TIMER_DELAY_MS); + if (t != null) { + return Long.parseLong(t); + } + return 0; + } + + public void setDeliverTimeMs(long timeMs) { + this.putProperty(MessageConst.PROPERTY_TIMER_DELIVER_MS, String.valueOf(timeMs)); + } + + public long getDeliverTimeMs() { + String t = this.getProperty(MessageConst.PROPERTY_TIMER_DELIVER_MS); + if (t != null) { + return Long.parseLong(t); + } + return 0; } } diff --git a/common/src/main/java/org/apache/rocketmq/common/message/MessageAccessor.java b/common/src/main/java/org/apache/rocketmq/common/message/MessageAccessor.java index 4cac404b9bd..62e3bbd7e6e 100644 --- a/common/src/main/java/org/apache/rocketmq/common/message/MessageAccessor.java +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageAccessor.java @@ -17,6 +17,7 @@ package org.apache.rocketmq.common.message; +import java.util.HashMap; import java.util.Map; public class MessageAccessor { @@ -89,4 +90,17 @@ public static String getConsumeStartTimeStamp(final Message msg) { return msg.getProperty(MessageConst.PROPERTY_CONSUME_START_TIMESTAMP); } + public static Message cloneMessage(final Message msg) { + Message newMsg = new Message(msg.getTopic(), msg.getBody()); + newMsg.setFlag(msg.getFlag()); + newMsg.setProperties(msg.getProperties()); + return newMsg; + } + + public static Map deepCopyProperties(Map properties) { + if (properties == null) { + return null; + } + return new HashMap<>(properties); + } } diff --git a/common/src/main/java/org/apache/rocketmq/common/message/MessageBatch.java b/common/src/main/java/org/apache/rocketmq/common/message/MessageBatch.java index ca2ce88ec7c..30369b8f372 100644 --- a/common/src/main/java/org/apache/rocketmq/common/message/MessageBatch.java +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageBatch.java @@ -27,7 +27,7 @@ public class MessageBatch extends Message implements Iterable { private static final long serialVersionUID = 621335151046335557L; private final List messages; - private MessageBatch(List messages) { + public MessageBatch(List messages) { this.messages = messages; } @@ -39,14 +39,14 @@ public Iterator iterator() { return messages.iterator(); } - public static MessageBatch generateFromList(Collection messages) { + public static MessageBatch generateFromList(Collection messages) { assert messages != null; assert messages.size() > 0; - List messageList = new ArrayList(messages.size()); + List messageList = new ArrayList<>(messages.size()); Message first = null; for (Message message : messages) { if (message.getDelayTimeLevel() > 0) { - throw new UnsupportedOperationException("TimeDelayLevel in not supported for batching"); + throw new UnsupportedOperationException("TimeDelayLevel is not supported for batching"); } if (message.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { throw new UnsupportedOperationException("Retry Group is not supported for batching"); diff --git a/common/src/main/java/org/apache/rocketmq/common/message/MessageClientIDSetter.java b/common/src/main/java/org/apache/rocketmq/common/message/MessageClientIDSetter.java index d0b202ec64b..4ae5ef59d6d 100644 --- a/common/src/main/java/org/apache/rocketmq/common/message/MessageClientIDSetter.java +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageClientIDSetter.java @@ -23,27 +23,26 @@ import org.apache.rocketmq.common.UtilAll; public class MessageClientIDSetter { - private static final String TOPIC_KEY_SPLITTER = "#"; + private static final int LEN; - private static final String FIX_STRING; + private static final char[] FIX_STRING; private static final AtomicInteger COUNTER; private static long startTime; private static long nextStartTime; static { - LEN = 4 + 2 + 4 + 4 + 2; - ByteBuffer tempBuffer = ByteBuffer.allocate(10); - tempBuffer.position(2); - tempBuffer.putInt(UtilAll.getPid()); - tempBuffer.position(0); + byte[] ip; try { - tempBuffer.put(UtilAll.getIP()); + ip = UtilAll.getIP(); } catch (Exception e) { - tempBuffer.put(createFakeIP()); + ip = createFakeIP(); } - tempBuffer.position(6); + LEN = ip.length + 2 + 4 + 4 + 2; + ByteBuffer tempBuffer = ByteBuffer.allocate(ip.length + 2 + 4); + tempBuffer.put(ip); + tempBuffer.putShort((short) UtilAll.getPid()); tempBuffer.putInt(MessageClientIDSetter.class.getClassLoader().hashCode()); - FIX_STRING = UtilAll.bytes2string(tempBuffer.array()); + FIX_STRING = UtilAll.bytes2string(tempBuffer.array()).toCharArray(); setStartTime(System.currentTimeMillis()); COUNTER = new AtomicInteger(0); } @@ -64,11 +63,12 @@ private synchronized static void setStartTime(long millis) { public static Date getNearlyTimeFromID(String msgID) { ByteBuffer buf = ByteBuffer.allocate(8); byte[] bytes = UtilAll.string2bytes(msgID); + int ipLength = bytes.length == 28 ? 16 : 4; buf.put((byte) 0); buf.put((byte) 0); buf.put((byte) 0); buf.put((byte) 0); - buf.put(bytes, 10, 4); + buf.put(bytes, ipLength + 2 + 4, 4); buf.position(0); long spanMS = buf.getLong(); Calendar cal = Calendar.getInstance(); @@ -89,33 +89,45 @@ public static Date getNearlyTimeFromID(String msgID) { public static String getIPStrFromID(String msgID) { byte[] ipBytes = getIPFromID(msgID); - return UtilAll.ipToIPv4Str(ipBytes); + if (ipBytes.length == 16) { + return UtilAll.ipToIPv6Str(ipBytes); + } else { + return UtilAll.ipToIPv4Str(ipBytes); + } } public static byte[] getIPFromID(String msgID) { - byte[] result = new byte[4]; byte[] bytes = UtilAll.string2bytes(msgID); - System.arraycopy(bytes, 0, result, 0, 4); + int ipLength = bytes.length == 28 ? 16 : 4; + byte[] result = new byte[ipLength]; + System.arraycopy(bytes, 0, result, 0, ipLength); return result; } - public static String createUniqID() { - StringBuilder sb = new StringBuilder(LEN * 2); - sb.append(FIX_STRING); - sb.append(UtilAll.bytes2string(createUniqIDBuffer())); - return sb.toString(); + public static int getPidFromID(String msgID) { + byte[] bytes = UtilAll.string2bytes(msgID); + ByteBuffer wrap = ByteBuffer.wrap(bytes); + int value = wrap.getShort(bytes.length - 2 - 4 - 4 - 2); + return value & 0x0000FFFF; } - private static byte[] createUniqIDBuffer() { - ByteBuffer buffer = ByteBuffer.allocate(4 + 2); + public static String createUniqID() { + char[] sb = new char[LEN * 2]; + System.arraycopy(FIX_STRING, 0, sb, 0, FIX_STRING.length); long current = System.currentTimeMillis(); if (current >= nextStartTime) { setStartTime(current); } - buffer.position(0); - buffer.putInt((int) (System.currentTimeMillis() - startTime)); - buffer.putShort((short) COUNTER.getAndIncrement()); - return buffer.array(); + int diff = (int)(current - startTime); + if (diff < 0 && diff > -1000_000) { + // may cause by NTP + diff = 0; + } + int pos = FIX_STRING.length; + UtilAll.writeInt(sb, pos, diff); + pos += 8; + UtilAll.writeShort(sb, pos, COUNTER.getAndIncrement()); + return new String(sb); } public static void setUniqID(final Message msg) { @@ -137,4 +149,3 @@ public static byte[] createFakeIP() { return fakeIP; } } - diff --git a/common/src/main/java/org/apache/rocketmq/common/message/MessageConst.java b/common/src/main/java/org/apache/rocketmq/common/message/MessageConst.java index 1edbbec70c6..24f7bdb99a5 100644 --- a/common/src/main/java/org/apache/rocketmq/common/message/MessageConst.java +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageConst.java @@ -39,12 +39,71 @@ public class MessageConst { public static final String PROPERTY_MSG_REGION = "MSG_REGION"; public static final String PROPERTY_TRACE_SWITCH = "TRACE_ON"; public static final String PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX = "UNIQ_KEY"; + public static final String PROPERTY_EXTEND_UNIQ_INFO = "EXTEND_UNIQ_INFO"; public static final String PROPERTY_MAX_RECONSUME_TIMES = "MAX_RECONSUME_TIMES"; public static final String PROPERTY_CONSUME_START_TIMESTAMP = "CONSUME_START_TIME"; + public static final String PROPERTY_INNER_NUM = "INNER_NUM"; + public static final String PROPERTY_INNER_BASE = "INNER_BASE"; + public static final String DUP_INFO = "DUP_INFO"; + public static final String PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS = "CHECK_IMMUNITY_TIME_IN_SECONDS"; + public static final String PROPERTY_TRANSACTION_PREPARED_QUEUE_OFFSET = "TRAN_PREPARED_QUEUE_OFFSET"; + public static final String PROPERTY_TRANSACTION_ID = "__transactionId__"; + public static final String PROPERTY_TRANSACTION_CHECK_TIMES = "TRANSACTION_CHECK_TIMES"; + public static final String PROPERTY_INSTANCE_ID = "INSTANCE_ID"; + public static final String PROPERTY_CORRELATION_ID = "CORRELATION_ID"; + public static final String PROPERTY_MESSAGE_REPLY_TO_CLIENT = "REPLY_TO_CLIENT"; + public static final String PROPERTY_MESSAGE_TTL = "TTL"; + public static final String PROPERTY_REPLY_MESSAGE_ARRIVE_TIME = "ARRIVE_TIME"; + public static final String PROPERTY_PUSH_REPLY_TIME = "PUSH_REPLY_TIME"; + public static final String PROPERTY_CLUSTER = "CLUSTER"; + public static final String PROPERTY_MESSAGE_TYPE = "MSG_TYPE"; + public static final String PROPERTY_POP_CK = "POP_CK"; + public static final String PROPERTY_POP_CK_OFFSET = "POP_CK_OFFSET"; + public static final String PROPERTY_FIRST_POP_TIME = "1ST_POP_TIME"; + public static final String PROPERTY_SHARDING_KEY = "__SHARDINGKEY"; + public static final String PROPERTY_FORWARD_QUEUE_ID = "PROPERTY_FORWARD_QUEUE_ID"; + public static final String PROPERTY_REDIRECT = "REDIRECT"; + public static final String PROPERTY_INNER_MULTI_DISPATCH = "INNER_MULTI_DISPATCH"; + public static final String PROPERTY_INNER_MULTI_QUEUE_OFFSET = "INNER_MULTI_QUEUE_OFFSET"; + public static final String PROPERTY_TRACE_CONTEXT = "TRACE_CONTEXT"; + public static final String PROPERTY_TIMER_DELAY_SEC = "TIMER_DELAY_SEC"; + public static final String PROPERTY_TIMER_DELIVER_MS = "TIMER_DELIVER_MS"; + public static final String PROPERTY_BORN_HOST = "__BORNHOST"; + public static final String PROPERTY_BORN_TIMESTAMP = "BORN_TIMESTAMP"; + + /** + * property which name starts with "__RMQ.TRANSIENT." is called transient one that will not stored in broker disks. + */ + public static final String PROPERTY_TRANSIENT_PREFIX = "__RMQ.TRANSIENT."; + + /** + * the transient property key of topicSysFlag (set by client when pulling messages) + */ + public static final String PROPERTY_TRANSIENT_TOPIC_CONFIG = PROPERTY_TRANSIENT_PREFIX + "TOPIC_SYS_FLAG"; + + /** + * the transient property key of groupSysFlag (set by client when pulling messages) + */ + public static final String PROPERTY_TRANSIENT_GROUP_CONFIG = PROPERTY_TRANSIENT_PREFIX + "GROUP_SYS_FLAG"; public static final String KEY_SEPARATOR = " "; - public static final HashSet STRING_HASH_SET = new HashSet(); + public static final HashSet STRING_HASH_SET = new HashSet<>(64); + + public static final String PROPERTY_TIMER_ENQUEUE_MS = "TIMER_ENQUEUE_MS"; + public static final String PROPERTY_TIMER_DEQUEUE_MS = "TIMER_DEQUEUE_MS"; + public static final String PROPERTY_TIMER_ROLL_TIMES = "TIMER_ROLL_TIMES"; + public static final String PROPERTY_TIMER_OUT_MS = "TIMER_OUT_MS"; + public static final String PROPERTY_TIMER_DEL_UNIQKEY = "TIMER_DEL_UNIQKEY"; + public static final String PROPERTY_TIMER_DELAY_LEVEL = "TIMER_DELAY_LEVEL"; + public static final String PROPERTY_TIMER_DELAY_MS = "TIMER_DELAY_MS"; + public static final String PROPERTY_CRC32 = "__CRC32#"; + + /** + * properties for DLQ + */ + public static final String PROPERTY_DLQ_ORIGIN_TOPIC = "DLQ_ORIGIN_TOPIC"; + public static final String PROPERTY_DLQ_ORIGIN_MESSAGE_ID = "DLQ_ORIGIN_MESSAGE_ID"; static { STRING_HASH_SET.add(PROPERTY_TRACE_SWITCH); @@ -69,5 +128,34 @@ public class MessageConst { STRING_HASH_SET.add(PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX); STRING_HASH_SET.add(PROPERTY_MAX_RECONSUME_TIMES); STRING_HASH_SET.add(PROPERTY_CONSUME_START_TIMESTAMP); + STRING_HASH_SET.add(PROPERTY_POP_CK); + STRING_HASH_SET.add(PROPERTY_POP_CK_OFFSET); + STRING_HASH_SET.add(PROPERTY_FIRST_POP_TIME); + STRING_HASH_SET.add(PROPERTY_TRANSACTION_PREPARED_QUEUE_OFFSET); + STRING_HASH_SET.add(DUP_INFO); + STRING_HASH_SET.add(PROPERTY_EXTEND_UNIQ_INFO); + STRING_HASH_SET.add(PROPERTY_INSTANCE_ID); + STRING_HASH_SET.add(PROPERTY_CORRELATION_ID); + STRING_HASH_SET.add(PROPERTY_MESSAGE_REPLY_TO_CLIENT); + STRING_HASH_SET.add(PROPERTY_MESSAGE_TTL); + STRING_HASH_SET.add(PROPERTY_REPLY_MESSAGE_ARRIVE_TIME); + STRING_HASH_SET.add(PROPERTY_PUSH_REPLY_TIME); + STRING_HASH_SET.add(PROPERTY_CLUSTER); + STRING_HASH_SET.add(PROPERTY_MESSAGE_TYPE); + STRING_HASH_SET.add(PROPERTY_INNER_MULTI_QUEUE_OFFSET); + STRING_HASH_SET.add(PROPERTY_TIMER_DELAY_MS); + STRING_HASH_SET.add(PROPERTY_TIMER_DELAY_SEC); + STRING_HASH_SET.add(PROPERTY_TIMER_DELIVER_MS); + STRING_HASH_SET.add(PROPERTY_TIMER_ENQUEUE_MS); + STRING_HASH_SET.add(PROPERTY_TIMER_DEQUEUE_MS); + STRING_HASH_SET.add(PROPERTY_TIMER_ROLL_TIMES); + STRING_HASH_SET.add(PROPERTY_TIMER_OUT_MS); + STRING_HASH_SET.add(PROPERTY_TIMER_DEL_UNIQKEY); + STRING_HASH_SET.add(PROPERTY_TIMER_DELAY_LEVEL); + STRING_HASH_SET.add(PROPERTY_BORN_HOST); + STRING_HASH_SET.add(PROPERTY_BORN_TIMESTAMP); + STRING_HASH_SET.add(PROPERTY_DLQ_ORIGIN_TOPIC); + STRING_HASH_SET.add(PROPERTY_DLQ_ORIGIN_MESSAGE_ID); + STRING_HASH_SET.add(PROPERTY_CRC32); } } diff --git a/common/src/main/java/org/apache/rocketmq/common/message/MessageDecoder.java b/common/src/main/java/org/apache/rocketmq/common/message/MessageDecoder.java index 2d7bcbbfd45..b053f827597 100644 --- a/common/src/main/java/org/apache/rocketmq/common/message/MessageDecoder.java +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageDecoder.java @@ -16,49 +16,64 @@ */ package org.apache.rocketmq.common.message; -import org.apache.rocketmq.common.UtilAll; -import org.apache.rocketmq.common.sysflag.MessageSysFlag; - +import io.netty.buffer.ByteBuf; +import java.io.IOException; +import java.net.Inet4Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.compression.Compressor; +import org.apache.rocketmq.common.compression.CompressorFactory; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; public class MessageDecoder { - public final static int MSG_ID_LENGTH = 8 + 8; - - public final static Charset CHARSET_UTF8 = Charset.forName("UTF-8"); - public final static int MESSAGE_MAGIC_CODE_POSTION = 4; - public final static int MESSAGE_FLAG_POSTION = 16; - public final static int MESSAGE_PHYSIC_OFFSET_POSTION = 28; - public final static int MESSAGE_STORE_TIMESTAMP_POSTION = 56; - public final static int MESSAGE_MAGIC_CODE = 0xAABBCCDD ^ 1880681586 + 8; +// public final static int MSG_ID_LENGTH = 8 + 8; + + public final static Charset CHARSET_UTF8 = StandardCharsets.UTF_8; + public final static int MESSAGE_MAGIC_CODE_POSITION = 4; + public final static int MESSAGE_FLAG_POSITION = 16; + public final static int MESSAGE_PHYSIC_OFFSET_POSITION = 28; + public final static int MESSAGE_STORE_TIMESTAMP_POSITION = 56; + + // Set message magic code v2 if topic length > 127 + public final static int MESSAGE_MAGIC_CODE = -626843481; + public final static int MESSAGE_MAGIC_CODE_V2 = -626843477; + + // End of file empty MAGIC CODE cbd43194 + public final static int BLANK_MAGIC_CODE = -875286124; public static final char NAME_VALUE_SEPARATOR = 1; public static final char PROPERTY_SEPARATOR = 2; - public static final int BODY_SIZE_POSITION = 4 // 1 TOTALSIZE - + 4 // 2 MAGICCODE - + 4 // 3 BODYCRC - + 4 // 4 QUEUEID - + 4 // 5 FLAG - + 8 // 6 QUEUEOFFSET - + 8 // 7 PHYSICALOFFSET - + 4 // 8 SYSFLAG - + 8 // 9 BORNTIMESTAMP - + 8 // 10 BORNHOST - + 8 // 11 STORETIMESTAMP - + 8 // 12 STOREHOSTADDRESS - + 4 // 13 RECONSUMETIMES - + 8; // 14 Prepared Transaction Offset + public static final int PHY_POS_POSITION = 4 + 4 + 4 + 4 + 4 + 8; + public static final int QUEUE_OFFSET_POSITION = 4 + 4 + 4 + 4 + 4; + public static final int SYSFLAG_POSITION = 4 + 4 + 4 + 4 + 4 + 8 + 8; +// public static final int BODY_SIZE_POSITION = 4 // 1 TOTALSIZE +// + 4 // 2 MAGICCODE +// + 4 // 3 BODYCRC +// + 4 // 4 QUEUEID +// + 4 // 5 FLAG +// + 8 // 6 QUEUEOFFSET +// + 8 // 7 PHYSICALOFFSET +// + 4 // 8 SYSFLAG +// + 8 // 9 BORNTIMESTAMP +// + 8 // 10 BORNHOST +// + 8 // 11 STORETIMESTAMP +// + 8 // 12 STOREHOSTADDRESS +// + 4 // 13 RECONSUMETIMES +// + 8; // 14 Prepared Transaction Offset public static String createMessageId(final ByteBuffer input, final ByteBuffer addr, final long offset) { input.flip(); - input.limit(MessageDecoder.MSG_ID_LENGTH); + int msgIDLength = addr.limit() == 8 ? 16 : 28; + input.limit(msgIDLength); input.put(addr); input.putLong(offset); @@ -67,8 +82,9 @@ public static String createMessageId(final ByteBuffer input, final ByteBuffer ad } public static String createMessageId(SocketAddress socketAddress, long transactionIdhashCode) { - ByteBuffer byteBuffer = ByteBuffer.allocate(MessageDecoder.MSG_ID_LENGTH); InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress; + int msgIDLength = inetSocketAddress.getAddress() instanceof Inet4Address ? 16 : 28; + ByteBuffer byteBuffer = ByteBuffer.allocate(msgIDLength); byteBuffer.put(inetSocketAddress.getAddress().getAddress()); byteBuffer.putInt(inetSocketAddress.getPort()); byteBuffer.putLong(transactionIdhashCode); @@ -77,19 +93,17 @@ public static String createMessageId(SocketAddress socketAddress, long transacti } public static MessageId decodeMessageId(final String msgId) throws UnknownHostException { - SocketAddress address; - long offset; + byte[] bytes = UtilAll.string2bytes(msgId); + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); - byte[] ip = UtilAll.string2bytes(msgId.substring(0, 8)); - byte[] port = UtilAll.string2bytes(msgId.substring(8, 16)); - ByteBuffer bb = ByteBuffer.wrap(port); - int portInt = bb.getInt(0); - address = new InetSocketAddress(InetAddress.getByAddress(ip), portInt); + // address(ip+port) + byte[] ip = new byte[msgId.length() == 32 ? 4 : 16]; + byteBuffer.get(ip); + int port = byteBuffer.getInt(); + SocketAddress address = new InetSocketAddress(InetAddress.getByAddress(ip), port); // offset - byte[] data = UtilAll.string2bytes(msgId.substring(16, 32)); - bb = ByteBuffer.wrap(data); - offset = bb.getLong(0); + long offset = byteBuffer.getLong(); return new MessageId(address, offset); } @@ -99,34 +113,83 @@ public static MessageId decodeMessageId(final String msgId) throws UnknownHostEx * * @param byteBuffer msg commit log buffer. */ - public static Map decodeProperties(java.nio.ByteBuffer byteBuffer) { - int topicLengthPosition = BODY_SIZE_POSITION + 4 + byteBuffer.getInt(BODY_SIZE_POSITION); - - byte topicLength = byteBuffer.get(topicLengthPosition); - - short propertiesLength = byteBuffer.getShort(topicLengthPosition + 1 + topicLength); - - byteBuffer.position(topicLengthPosition + 1 + topicLength + 2); + public static Map decodeProperties(ByteBuffer byteBuffer) { + int sysFlag = byteBuffer.getInt(SYSFLAG_POSITION); + int magicCode = byteBuffer.getInt(MESSAGE_MAGIC_CODE_POSITION); + MessageVersion version = MessageVersion.valueOfMagicCode(magicCode); + + int bornhostLength = (sysFlag & MessageSysFlag.BORNHOST_V6_FLAG) == 0 ? 8 : 20; + int storehostAddressLength = (sysFlag & MessageSysFlag.STOREHOSTADDRESS_V6_FLAG) == 0 ? 8 : 20; + int bodySizePosition = 4 // 1 TOTALSIZE + + 4 // 2 MAGICCODE + + 4 // 3 BODYCRC + + 4 // 4 QUEUEID + + 4 // 5 FLAG + + 8 // 6 QUEUEOFFSET + + 8 // 7 PHYSICALOFFSET + + 4 // 8 SYSFLAG + + 8 // 9 BORNTIMESTAMP + + bornhostLength // 10 BORNHOST + + 8 // 11 STORETIMESTAMP + + storehostAddressLength // 12 STOREHOSTADDRESS + + 4 // 13 RECONSUMETIMES + + 8; // 14 Prepared Transaction Offset + + int topicLengthPosition = bodySizePosition + 4 + byteBuffer.getInt(bodySizePosition); + byteBuffer.position(topicLengthPosition); + int topicLengthSize = version.getTopicLengthSize(); + int topicLength = version.getTopicLength(byteBuffer); + + int propertiesPosition = topicLengthPosition + topicLengthSize + topicLength; + short propertiesLength = byteBuffer.getShort(propertiesPosition); + byteBuffer.position(propertiesPosition + 2); if (propertiesLength > 0) { byte[] properties = new byte[propertiesLength]; byteBuffer.get(properties); String propertiesString = new String(properties, CHARSET_UTF8); - Map map = string2messageProperties(propertiesString); - return map; + return string2messageProperties(propertiesString); } return null; } - public static MessageExt decode(java.nio.ByteBuffer byteBuffer) { + public static void createCrc32(final ByteBuffer input, int crc32) { + input.put(MessageConst.PROPERTY_CRC32.getBytes(StandardCharsets.UTF_8)); + input.put((byte) NAME_VALUE_SEPARATOR); + for (int i = 0; i < 10; i++) { + byte b = '0'; + if (crc32 > 0) { + b += (byte) (crc32 % 10); + crc32 /= 10; + } + input.put(b); + } + input.put((byte) PROPERTY_SEPARATOR); + } + + public static void createCrc32(final ByteBuf input, int crc32) { + input.writeBytes(MessageConst.PROPERTY_CRC32.getBytes(StandardCharsets.UTF_8)); + input.writeByte((byte) NAME_VALUE_SEPARATOR); + for (int i = 0; i < 10; i++) { + byte b = '0'; + if (crc32 > 0) { + b += (byte) (crc32 % 10); + crc32 /= 10; + } + input.writeByte(b); + } + input.writeByte((byte) PROPERTY_SEPARATOR); + } + + public static MessageExt decode(ByteBuffer byteBuffer) { return decode(byteBuffer, true, true, false); } - public static MessageExt clientDecode(java.nio.ByteBuffer byteBuffer, final boolean readBody) { + public static MessageExt clientDecode(ByteBuffer byteBuffer, final boolean readBody) { return decode(byteBuffer, readBody, true, true); } - public static MessageExt decode(java.nio.ByteBuffer byteBuffer, final boolean readBody) { + public static MessageExt decode(ByteBuffer byteBuffer, final boolean readBody) { return decode(byteBuffer, readBody, true, false); } @@ -138,9 +201,12 @@ public static byte[] encode(MessageExt messageExt, boolean needCompress) throws byte[] propertiesBytes = properties.getBytes(CHARSET_UTF8); short propertiesLength = (short) propertiesBytes.length; int sysFlag = messageExt.getSysFlag(); + int bornhostLength = (sysFlag & MessageSysFlag.BORNHOST_V6_FLAG) == 0 ? 8 : 20; + int storehostAddressLength = (sysFlag & MessageSysFlag.STOREHOSTADDRESS_V6_FLAG) == 0 ? 8 : 20; byte[] newBody = messageExt.getBody(); if (needCompress && (sysFlag & MessageSysFlag.COMPRESSED_FLAG) == MessageSysFlag.COMPRESSED_FLAG) { - newBody = UtilAll.compress(body, 5); + Compressor compressor = CompressorFactory.getCompressor(MessageSysFlag.getCompressionType(sysFlag)); + newBody = compressor.compress(body, 5); } int bodyLength = newBody.length; int storeSize = messageExt.getStoreSize(); @@ -157,9 +223,9 @@ public static byte[] encode(MessageExt messageExt, boolean needCompress) throws + 8 // 7 PHYSICALOFFSET + 4 // 8 SYSFLAG + 8 // 9 BORNTIMESTAMP - + 8 // 10 BORNHOST + + bornhostLength // 10 BORNHOST + 8 // 11 STORETIMESTAMP - + 8 // 12 STOREHOSTADDRESS + + storehostAddressLength // 12 STOREHOSTADDRESS + 4 // 13 RECONSUMETIMES + 8 // 14 Prepared Transaction Offset + 4 + bodyLength // 14 BODY @@ -238,13 +304,132 @@ public static byte[] encode(MessageExt messageExt, boolean needCompress) throws return byteBuffer.array(); } + /** + * Encode without store timestamp and store host, skip blank msg. + * + * @param messageExt msg + * @param needCompress need compress or not + * @return byte array + * @throws IOException when compress failed + */ + public static byte[] encodeUniquely(MessageExt messageExt, boolean needCompress) throws IOException { + byte[] body = messageExt.getBody(); + byte[] topics = messageExt.getTopic().getBytes(CHARSET_UTF8); + byte topicLen = (byte) topics.length; + String properties = messageProperties2String(messageExt.getProperties()); + byte[] propertiesBytes = properties.getBytes(CHARSET_UTF8); + short propertiesLength = (short) propertiesBytes.length; + int sysFlag = messageExt.getSysFlag(); + int bornhostLength = (sysFlag & MessageSysFlag.BORNHOST_V6_FLAG) == 0 ? 8 : 20; + byte[] newBody = messageExt.getBody(); + if (needCompress && (sysFlag & MessageSysFlag.COMPRESSED_FLAG) == MessageSysFlag.COMPRESSED_FLAG) { + newBody = UtilAll.compress(body, 5); + } + int bodyLength = newBody.length; + int storeSize = messageExt.getStoreSize(); + ByteBuffer byteBuffer; + if (storeSize > 0) { + byteBuffer = ByteBuffer.allocate(storeSize - 8); // except size for store timestamp + } else { + storeSize = 4 + // 1 TOTALSIZE + 4 + // 2 MAGICCODE + 4 + // 3 BODYCRC + 4 + // 4 QUEUEID + 4 + // 5 FLAG + 8 + // 6 QUEUEOFFSET + 8 + // 7 PHYSICALOFFSET + 4 + // 8 SYSFLAG + 8 + // 9 BORNTIMESTAMP + bornhostLength + // 10 BORNHOST + 4 + // 11 RECONSUMETIMES + 8 + // 12 Prepared Transaction Offset + 4 + bodyLength + // 13 BODY + +1 + topicLen + // 14 TOPIC + 2 + propertiesLength // 15 propertiesLength + ; + byteBuffer = ByteBuffer.allocate(storeSize); + } + + // 1 TOTALSIZE + byteBuffer.putInt(storeSize); + + // 2 MAGICCODE + byteBuffer.putInt(MESSAGE_MAGIC_CODE); + + // 3 BODYCRC + int bodyCRC = messageExt.getBodyCRC(); + byteBuffer.putInt(bodyCRC); + + // 4 QUEUEID + int queueId = messageExt.getQueueId(); + byteBuffer.putInt(queueId); + + // 5 FLAG + int flag = messageExt.getFlag(); + byteBuffer.putInt(flag); + + // 6 QUEUEOFFSET + long queueOffset = messageExt.getQueueOffset(); + byteBuffer.putLong(queueOffset); + + // 7 PHYSICALOFFSET + long physicOffset = messageExt.getCommitLogOffset(); + byteBuffer.putLong(physicOffset); + + // 8 SYSFLAG + byteBuffer.putInt(sysFlag); + + // 9 BORNTIMESTAMP + long bornTimeStamp = messageExt.getBornTimestamp(); + byteBuffer.putLong(bornTimeStamp); + + // 10 BORNHOST + InetSocketAddress bornHost = (InetSocketAddress) messageExt.getBornHost(); + byteBuffer.put(bornHost.getAddress().getAddress()); + byteBuffer.putInt(bornHost.getPort()); + + // 11 RECONSUMETIMES + int reconsumeTimes = messageExt.getReconsumeTimes(); + byteBuffer.putInt(reconsumeTimes); + + // 12 Prepared Transaction Offset + long preparedTransactionOffset = messageExt.getPreparedTransactionOffset(); + byteBuffer.putLong(preparedTransactionOffset); + + // 13 BODY + byteBuffer.putInt(bodyLength); + byteBuffer.put(newBody); + + // 14 TOPIC + byteBuffer.put(topicLen); + byteBuffer.put(topics); + + // 15 properties + byteBuffer.putShort(propertiesLength); + byteBuffer.put(propertiesBytes); + + return byteBuffer.array(); + } + public static MessageExt decode( - java.nio.ByteBuffer byteBuffer, final boolean readBody, final boolean deCompressBody) { + ByteBuffer byteBuffer, final boolean readBody, final boolean deCompressBody) { return decode(byteBuffer, readBody, deCompressBody, false); } public static MessageExt decode( java.nio.ByteBuffer byteBuffer, final boolean readBody, final boolean deCompressBody, final boolean isClient) { + return decode(byteBuffer, readBody, deCompressBody, isClient, false, false); + } + + public static MessageExt decode( + java.nio.ByteBuffer byteBuffer, final boolean readBody, final boolean deCompressBody, final boolean isClient, + final boolean isSetPropertiesString) { + return decode(byteBuffer, readBody, deCompressBody, isClient, isSetPropertiesString, false); + } + + public static MessageExt decode( + java.nio.ByteBuffer byteBuffer, final boolean readBody, final boolean deCompressBody, final boolean isClient, + final boolean isSetPropertiesString, final boolean checkCRC) { try { MessageExt msgExt; @@ -259,7 +444,8 @@ public static MessageExt decode( msgExt.setStoreSize(storeSize); // 2 MAGICCODE - byteBuffer.getInt(); + int magicCode = byteBuffer.getInt(); + MessageVersion version = MessageVersion.valueOfMagicCode(magicCode); // 3 BODYCRC int bodyCRC = byteBuffer.getInt(); @@ -290,8 +476,9 @@ public static MessageExt decode( msgExt.setBornTimestamp(bornTimeStamp); // 10 BORNHOST - byte[] bornHost = new byte[4]; - byteBuffer.get(bornHost, 0, 4); + int bornhostIPLength = (sysFlag & MessageSysFlag.BORNHOST_V6_FLAG) == 0 ? 4 : 16; + byte[] bornHost = new byte[bornhostIPLength]; + byteBuffer.get(bornHost, 0, bornhostIPLength); int port = byteBuffer.getInt(); msgExt.setBornHost(new InetSocketAddress(InetAddress.getByAddress(bornHost), port)); @@ -300,8 +487,9 @@ public static MessageExt decode( msgExt.setStoreTimestamp(storeTimestamp); // 12 STOREHOST - byte[] storeHost = new byte[4]; - byteBuffer.get(storeHost, 0, 4); + int storehostIPLength = (sysFlag & MessageSysFlag.STOREHOSTADDRESS_V6_FLAG) == 0 ? 4 : 16; + byte[] storeHost = new byte[storehostIPLength]; + byteBuffer.get(storeHost, 0, storehostIPLength); port = byteBuffer.getInt(); msgExt.setStoreHost(new InetSocketAddress(InetAddress.getByAddress(storeHost), port)); @@ -320,9 +508,18 @@ public static MessageExt decode( byte[] body = new byte[bodyLen]; byteBuffer.get(body); + if (checkCRC) { + //crc body + int crc = UtilAll.crc32(body, 0, bodyLen); + if (crc != bodyCRC) { + throw new Exception("Msg crc is error!"); + } + } + // uncompress body if (deCompressBody && (sysFlag & MessageSysFlag.COMPRESSED_FLAG) == MessageSysFlag.COMPRESSED_FLAG) { - body = UtilAll.uncompress(body); + Compressor compressor = CompressorFactory.getCompressor(MessageSysFlag.getCompressionType(sysFlag)); + body = compressor.decompress(body); } msgExt.setBody(body); @@ -332,8 +529,8 @@ public static MessageExt decode( } // 16 TOPIC - byte topicLen = byteBuffer.get(); - byte[] topic = new byte[(int) topicLen]; + int topicLen = version.getTopicLength(byteBuffer); + byte[] topic = new byte[topicLen]; byteBuffer.get(topic); msgExt.setTopic(new String(topic, CHARSET_UTF8)); @@ -343,11 +540,18 @@ public static MessageExt decode( byte[] properties = new byte[propertiesLength]; byteBuffer.get(properties); String propertiesString = new String(properties, CHARSET_UTF8); - Map map = string2messageProperties(propertiesString); - msgExt.setProperties(map); + if (!isSetPropertiesString) { + Map map = string2messageProperties(propertiesString); + msgExt.setProperties(map); + } else { + Map map = string2messageProperties(propertiesString); + map.put("propertiesString", propertiesString); + msgExt.setProperties(map); + } } - ByteBuffer byteBufferMsgId = ByteBuffer.allocate(MSG_ID_LENGTH); + int msgIDLength = storehostIPLength + 4 + 8; + ByteBuffer byteBufferMsgId = ByteBuffer.allocate(msgIDLength); String msgId = createMessageId(byteBufferMsgId, msgExt.getStoreHostBytes(), msgExt.getCommitLogOffset()); msgExt.setMsgId(msgId); @@ -363,12 +567,28 @@ public static MessageExt decode( return null; } - public static List decodes(java.nio.ByteBuffer byteBuffer) { + public static List decodes(ByteBuffer byteBuffer) { return decodes(byteBuffer, true); } - public static List decodes(java.nio.ByteBuffer byteBuffer, final boolean readBody) { - List msgExts = new ArrayList(); + public static List decodesBatch(ByteBuffer byteBuffer, + final boolean readBody, + final boolean decompressBody, + final boolean isClient) { + List msgExts = new ArrayList<>(); + while (byteBuffer.hasRemaining()) { + MessageExt msgExt = decode(byteBuffer, readBody, decompressBody, isClient); + if (null != msgExt) { + msgExts.add(msgExt); + } else { + break; + } + } + return msgExts; + } + + public static List decodes(ByteBuffer byteBuffer, final boolean readBody) { + List msgExts = new ArrayList<>(); while (byteBuffer.hasRemaining()) { MessageExt msgExt = clientDecode(byteBuffer, readBody); if (null != msgExt) { @@ -381,30 +601,57 @@ public static List decodes(java.nio.ByteBuffer byteBuffer, final boo } public static String messageProperties2String(Map properties) { - StringBuilder sb = new StringBuilder(); - if (properties != null) { - for (final Map.Entry entry : properties.entrySet()) { - final String name = entry.getKey(); - final String value = entry.getValue(); - - sb.append(name); - sb.append(NAME_VALUE_SEPARATOR); - sb.append(value); - sb.append(PROPERTY_SEPARATOR); + if (properties == null) { + return ""; + } + int len = 0; + for (final Map.Entry entry : properties.entrySet()) { + final String name = entry.getKey(); + final String value = entry.getValue(); + if (value == null) { + continue; + } + if (name != null) { + len += name.length(); + } + len += value.length(); + len += 2; // separator + } + StringBuilder sb = new StringBuilder(len); + for (final Map.Entry entry : properties.entrySet()) { + final String name = entry.getKey(); + final String value = entry.getValue(); + + if (value == null) { + continue; } + sb.append(name); + sb.append(NAME_VALUE_SEPARATOR); + sb.append(value); + sb.append(PROPERTY_SEPARATOR); } return sb.toString(); } public static Map string2messageProperties(final String properties) { - Map map = new HashMap(); + Map map = new HashMap<>(128); if (properties != null) { - String[] items = properties.split(String.valueOf(PROPERTY_SEPARATOR)); - for (String i : items) { - String[] nv = i.split(String.valueOf(NAME_VALUE_SEPARATOR)); - if (2 == nv.length) { - map.put(nv[0], nv[1]); + int len = properties.length(); + int index = 0; + while (index < len) { + int newIndex = properties.indexOf(PROPERTY_SEPARATOR, index); + if (newIndex < 0) { + newIndex = len; } + if (newIndex - index >= 3) { + int kvSepIndex = properties.indexOf(NAME_VALUE_SEPARATOR, index); + if (kvSepIndex > index && kvSepIndex < newIndex - 1) { + String k = properties.substring(index, kvSepIndex); + String v = properties.substring(kvSepIndex + 1, newIndex); + map.put(k, v); + } + } + index = newIndex + 1; } } @@ -484,7 +731,7 @@ public static Message decodeMessage(ByteBuffer byteBuffer) throws Exception { public static byte[] encodeMessages(List messages) { //TO DO refactor, accumulate in one buffer, avoid copies - List encodedMessages = new ArrayList(messages.size()); + List encodedMessages = new ArrayList<>(messages.size()); int allSize = 0; for (Message message : messages) { byte[] tmp = encodeMessage(message); @@ -502,11 +749,44 @@ public static byte[] encodeMessages(List messages) { public static List decodeMessages(ByteBuffer byteBuffer) throws Exception { //TO DO add a callback for processing, avoid creating lists - List msgs = new ArrayList(); + List msgs = new ArrayList<>(); while (byteBuffer.hasRemaining()) { Message msg = decodeMessage(byteBuffer); msgs.add(msg); } return msgs; } + + public static void decodeMessage(MessageExt messageExt, List list) throws Exception { + List messages = MessageDecoder.decodeMessages(ByteBuffer.wrap(messageExt.getBody())); + for (int i = 0; i < messages.size(); i++) { + Message message = messages.get(i); + MessageClientExt messageClientExt = new MessageClientExt(); + messageClientExt.setTopic(messageExt.getTopic()); + messageClientExt.setQueueOffset(messageExt.getQueueOffset() + i); + messageClientExt.setQueueId(messageExt.getQueueId()); + messageClientExt.setFlag(message.getFlag()); + MessageAccessor.setProperties(messageClientExt, message.getProperties()); + messageClientExt.setBody(message.getBody()); + messageClientExt.setStoreHost(messageExt.getStoreHost()); + messageClientExt.setBornHost(messageExt.getBornHost()); + messageClientExt.setBornTimestamp(messageExt.getBornTimestamp()); + messageClientExt.setStoreTimestamp(messageExt.getStoreTimestamp()); + messageClientExt.setSysFlag(messageExt.getSysFlag()); + messageClientExt.setCommitLogOffset(messageExt.getCommitLogOffset()); + messageClientExt.setWaitStoreMsgOK(messageExt.isWaitStoreMsgOK()); + list.add(messageClientExt); + } + } + + public static int countInnerMsgNum(ByteBuffer buffer) { + int count = 0; + while (buffer.hasRemaining()) { + count++; + int currPos = buffer.position(); + int size = buffer.getInt(); + buffer.position(currPos + size); + } + return count; + } } diff --git a/common/src/main/java/org/apache/rocketmq/common/message/MessageExt.java b/common/src/main/java/org/apache/rocketmq/common/message/MessageExt.java index 3f77767eb8d..5905d28f41b 100644 --- a/common/src/main/java/org/apache/rocketmq/common/message/MessageExt.java +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageExt.java @@ -16,6 +16,8 @@ */ package org.apache.rocketmq.common.message; +import java.net.Inet4Address; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.ByteBuffer; @@ -25,6 +27,8 @@ public class MessageExt extends Message { private static final long serialVersionUID = 5720810158625748049L; + private String brokerName; + private int queueId; private int storeSize; @@ -66,14 +70,26 @@ public static TopicFilterType parseTopicFilterType(final int sysFlag) { public static ByteBuffer socketAddress2ByteBuffer(final SocketAddress socketAddress, final ByteBuffer byteBuffer) { InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress; - byteBuffer.put(inetSocketAddress.getAddress().getAddress(), 0, 4); + InetAddress address = inetSocketAddress.getAddress(); + if (address instanceof Inet4Address) { + byteBuffer.put(inetSocketAddress.getAddress().getAddress(), 0, 4); + } else { + byteBuffer.put(inetSocketAddress.getAddress().getAddress(), 0, 16); + } byteBuffer.putInt(inetSocketAddress.getPort()); byteBuffer.flip(); return byteBuffer; } public static ByteBuffer socketAddress2ByteBuffer(SocketAddress socketAddress) { - ByteBuffer byteBuffer = ByteBuffer.allocate(8); + InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress; + InetAddress address = inetSocketAddress.getAddress(); + ByteBuffer byteBuffer; + if (address instanceof Inet4Address) { + byteBuffer = ByteBuffer.allocate(4 + 4); + } else { + byteBuffer = ByteBuffer.allocate(16 + 4); + } return socketAddress2ByteBuffer(socketAddress, byteBuffer); } @@ -93,6 +109,14 @@ public ByteBuffer getStoreHostBytes(ByteBuffer byteBuffer) { return socketAddress2ByteBuffer(this.storeHost, byteBuffer); } + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + public int getQueueId() { return queueId; } @@ -118,18 +142,24 @@ public void setBornHost(SocketAddress bornHost) { } public String getBornHostString() { - if (this.bornHost != null) { - InetSocketAddress inetSocketAddress = (InetSocketAddress) this.bornHost; - return inetSocketAddress.getAddress().getHostAddress(); + if (null != this.bornHost) { + InetAddress inetAddress = ((InetSocketAddress) this.bornHost).getAddress(); + + return null != inetAddress ? inetAddress.getHostAddress() : null; } return null; } public String getBornHostNameString() { - if (this.bornHost != null) { - InetSocketAddress inetSocketAddress = (InetSocketAddress) this.bornHost; - return inetSocketAddress.getAddress().getHostName(); + if (null != this.bornHost) { + if (bornHost instanceof InetSocketAddress) { + // without reverse dns lookup + return ((InetSocketAddress) bornHost).getHostString(); + } + InetAddress inetAddress = ((InetSocketAddress) this.bornHost).getAddress(); + + return null != inetAddress ? inetAddress.getHostName() : null; } return null; @@ -167,6 +197,10 @@ public void setSysFlag(int sysFlag) { this.sysFlag = sysFlag; } + public void setStoreHostAddressV6Flag() { this.sysFlag = this.sysFlag | MessageSysFlag.STOREHOSTADDRESS_V6_FLAG; } + + public void setBornHostV6Flag() { this.sysFlag = this.sysFlag | MessageSysFlag.BORNHOST_V6_FLAG; } + public int getBodyCRC() { return bodyCRC; } @@ -215,9 +249,64 @@ public void setPreparedTransactionOffset(long preparedTransactionOffset) { this.preparedTransactionOffset = preparedTransactionOffset; } + /** + * + * achieves topicSysFlag value from transient properties + * + * @return + */ + public Integer getTopicSysFlag() { + String topicSysFlagString = getProperty(MessageConst.PROPERTY_TRANSIENT_TOPIC_CONFIG); + if (topicSysFlagString != null && topicSysFlagString.length() > 0) { + return Integer.valueOf(topicSysFlagString); + } + return null; + } + + /** + * set topicSysFlag to transient properties, or clear it + * + * @param topicSysFlag + */ + public void setTopicSysFlag(Integer topicSysFlag) { + if (topicSysFlag == null) { + clearProperty(MessageConst.PROPERTY_TRANSIENT_TOPIC_CONFIG); + } else { + putProperty(MessageConst.PROPERTY_TRANSIENT_TOPIC_CONFIG, String.valueOf(topicSysFlag)); + } + } + + /** + * + * achieves groupSysFlag value from transient properties + * + * @return + */ + public Integer getGroupSysFlag() { + String groupSysFlagString = getProperty(MessageConst.PROPERTY_TRANSIENT_GROUP_CONFIG); + if (groupSysFlagString != null && groupSysFlagString.length() > 0) { + return Integer.valueOf(groupSysFlagString); + } + return null; + } + + /** + * + * set groupSysFlag to transient properties, or clear it + * + * @param groupSysFlag + */ + public void setGroupSysFlag(Integer groupSysFlag) { + if (groupSysFlag == null) { + clearProperty(MessageConst.PROPERTY_TRANSIENT_GROUP_CONFIG); + } else { + putProperty(MessageConst.PROPERTY_TRANSIENT_GROUP_CONFIG, String.valueOf(groupSysFlag)); + } + } + @Override public String toString() { - return "MessageExt [queueId=" + queueId + ", storeSize=" + storeSize + ", queueOffset=" + queueOffset + return "MessageExt [brokerName=" + brokerName + ", queueId=" + queueId + ", storeSize=" + storeSize + ", queueOffset=" + queueOffset + ", sysFlag=" + sysFlag + ", bornTimestamp=" + bornTimestamp + ", bornHost=" + bornHost + ", storeTimestamp=" + storeTimestamp + ", storeHost=" + storeHost + ", msgId=" + msgId + ", commitLogOffset=" + commitLogOffset + ", bodyCRC=" + bodyCRC + ", reconsumeTimes=" diff --git a/common/src/main/java/org/apache/rocketmq/common/message/MessageExtBatch.java b/common/src/main/java/org/apache/rocketmq/common/message/MessageExtBatch.java index a2713cb4c3d..42f98e45bd8 100644 --- a/common/src/main/java/org/apache/rocketmq/common/message/MessageExtBatch.java +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageExtBatch.java @@ -19,15 +19,28 @@ import java.nio.ByteBuffer; -public class MessageExtBatch extends MessageExt { +public class MessageExtBatch extends MessageExtBrokerInner { private static final long serialVersionUID = -2353110995348498537L; + /** + * Inner batch means the batch does not need to be unwrapped + */ + private boolean isInnerBatch = false; + public ByteBuffer wrap() { assert getBody() != null; return ByteBuffer.wrap(getBody(), 0, getBody().length); } + public boolean isInnerBatch() { + return isInnerBatch; + } + + public void setInnerBatch(boolean innerBatch) { + isInnerBatch = innerBatch; + } + private ByteBuffer encodedBuff; public ByteBuffer getEncodedBuff() { diff --git a/common/src/main/java/org/apache/rocketmq/common/message/MessageExtBrokerInner.java b/common/src/main/java/org/apache/rocketmq/common/message/MessageExtBrokerInner.java new file mode 100644 index 00000000000..147f23f1234 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageExtBrokerInner.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.message; + +import java.nio.ByteBuffer; + +import org.apache.rocketmq.common.TopicFilterType; +import org.apache.rocketmq.common.utils.MessageUtils; + +public class MessageExtBrokerInner extends MessageExt { + private static final long serialVersionUID = 7256001576878700634L; + private String propertiesString; + private long tagsCode; + + private ByteBuffer encodedBuff; + + private volatile boolean encodeCompleted; + + private MessageVersion version = MessageVersion.MESSAGE_VERSION_V1; + + public ByteBuffer getEncodedBuff() { + return encodedBuff; + } + + public void setEncodedBuff(ByteBuffer encodedBuff) { + this.encodedBuff = encodedBuff; + } + + public static long tagsString2tagsCode(final TopicFilterType filter, final String tags) { + if (null == tags || tags.length() == 0) { return 0; } + + return tags.hashCode(); + } + + public static long tagsString2tagsCode(final String tags) { + return tagsString2tagsCode(null, tags); + } + + public String getPropertiesString() { + return propertiesString; + } + + public void setPropertiesString(String propertiesString) { + this.propertiesString = propertiesString; + } + + + public void deleteProperty(String name) { + super.clearProperty(name); + if (propertiesString != null) { + this.setPropertiesString(MessageUtils.deleteProperty(propertiesString, name)); + } + } + + public long getTagsCode() { + return tagsCode; + } + + public void setTagsCode(long tagsCode) { + this.tagsCode = tagsCode; + } + + public MessageVersion getVersion() { + return version; + } + + public void setVersion(MessageVersion version) { + this.version = version; + } + + public void removeWaitStorePropertyString() { + if (this.getProperties().containsKey(MessageConst.PROPERTY_WAIT_STORE_MSG_OK)) { + // There is no need to store "WAIT=true", remove it from propertiesString to save 9 bytes for each message. + // It works for most case. In some cases msgInner.setPropertiesString invoked later and replace it. + String waitStoreMsgOKValue = this.getProperties().remove(MessageConst.PROPERTY_WAIT_STORE_MSG_OK); + this.setPropertiesString(MessageDecoder.messageProperties2String(this.getProperties())); + // Reput to properties, since msgInner.isWaitStoreMsgOK() will be invoked later + this.getProperties().put(MessageConst.PROPERTY_WAIT_STORE_MSG_OK, waitStoreMsgOKValue); + } else { + this.setPropertiesString(MessageDecoder.messageProperties2String(this.getProperties())); + } + } + + public boolean isEncodeCompleted() { + return encodeCompleted; + } + + public void setEncodeCompleted(boolean encodeCompleted) { + this.encodeCompleted = encodeCompleted; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/message/MessageQueue.java b/common/src/main/java/org/apache/rocketmq/common/message/MessageQueue.java index 03ba2027e03..7926b7327d6 100644 --- a/common/src/main/java/org/apache/rocketmq/common/message/MessageQueue.java +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageQueue.java @@ -28,6 +28,12 @@ public MessageQueue() { } + public MessageQueue(MessageQueue other) { + this.topic = other.topic; + this.brokerName = other.brokerName; + this.queueId = other.queueId; + } + public MessageQueue(String topic, String brokerName, int queueId) { this.topic = topic; this.brokerName = brokerName; diff --git a/common/src/main/java/org/apache/rocketmq/common/message/MessageQueueAssignment.java b/common/src/main/java/org/apache/rocketmq/common/message/MessageQueueAssignment.java new file mode 100644 index 00000000000..fcd9f5802e2 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageQueueAssignment.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.message; + +import java.io.Serializable; +import java.util.Map; + +public class MessageQueueAssignment implements Serializable { + + private static final long serialVersionUID = 8092600270527861645L; + + private MessageQueue messageQueue; + + private MessageRequestMode mode = MessageRequestMode.PULL; + + private Map attachments; + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((messageQueue == null) ? 0 : messageQueue.hashCode()); + result = prime * result + ((mode == null) ? 0 : mode.hashCode()); + result = prime * result + ((attachments == null) ? 0 : attachments.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + MessageQueueAssignment other = (MessageQueueAssignment) obj; + return messageQueue.equals(other.messageQueue); + } + + @Override + public String toString() { + return "MessageQueueAssignment [MessageQueue=" + messageQueue + ", Mode=" + mode + "]"; + } + + public MessageQueue getMessageQueue() { + return messageQueue; + } + + public void setMessageQueue(MessageQueue messageQueue) { + this.messageQueue = messageQueue; + } + + public MessageRequestMode getMode() { + return mode; + } + + public void setMode(MessageRequestMode mode) { + this.mode = mode; + } + + public Map getAttachments() { + return attachments; + } + + public void setAttachments(Map attachments) { + this.attachments = attachments; + } + +} diff --git a/common/src/main/java/org/apache/rocketmq/common/message/MessageRequestMode.java b/common/src/main/java/org/apache/rocketmq/common/message/MessageRequestMode.java new file mode 100644 index 00000000000..35a166a6761 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageRequestMode.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.message; + +/** + * Message Request Mode + */ +public enum MessageRequestMode { + + /** + * pull + */ + PULL("PULL"), + + /** + * pop, consumer working in pop mode could share MessageQueue + */ + POP("POP"); + + private String name; + + MessageRequestMode(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/message/MessageType.java b/common/src/main/java/org/apache/rocketmq/common/message/MessageType.java index 08091f47f19..5ffd2f65d0a 100644 --- a/common/src/main/java/org/apache/rocketmq/common/message/MessageType.java +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageType.java @@ -18,8 +18,28 @@ package org.apache.rocketmq.common.message; public enum MessageType { - Normal_Msg, - Trans_Msg_Half, - Trans_msg_Commit, - Delay_Msg, + Normal_Msg("Normal"), + Trans_Msg_Half("Trans"), + Trans_msg_Commit("TransCommit"), + Delay_Msg("Delay"), + Order_Msg("Order"); + + private final String shortName; + + MessageType(String shortName) { + this.shortName = shortName; + } + + public String getShortName() { + return shortName; + } + + public static MessageType getByShortName(String shortName) { + for (MessageType msgType : MessageType.values()) { + if (msgType.getShortName().equals(shortName)) { + return msgType; + } + } + return Normal_Msg; + } } diff --git a/common/src/main/java/org/apache/rocketmq/common/message/MessageVersion.java b/common/src/main/java/org/apache/rocketmq/common/message/MessageVersion.java new file mode 100644 index 00000000000..bb1c2e8d64b --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/message/MessageVersion.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.message; + +import java.nio.ByteBuffer; + +public enum MessageVersion { + + MESSAGE_VERSION_V1(MessageDecoder.MESSAGE_MAGIC_CODE) { + @Override + public int getTopicLengthSize() { + return 1; + } + + @Override + public int getTopicLength(ByteBuffer buffer) { + return buffer.get(); + } + + @Override + public int getTopicLength(ByteBuffer buffer, int index) { + return buffer.get(index); + } + + @Override + public void putTopicLength(ByteBuffer buffer, int topicLength) { + buffer.put((byte) topicLength); + } + }, + + MESSAGE_VERSION_V2(MessageDecoder.MESSAGE_MAGIC_CODE_V2) { + @Override + public int getTopicLengthSize() { + return 2; + } + + @Override + public int getTopicLength(ByteBuffer buffer) { + return buffer.getShort(); + } + + @Override + public int getTopicLength(ByteBuffer buffer, int index) { + return buffer.getShort(index); + } + + @Override + public void putTopicLength(ByteBuffer buffer, int topicLength) { + buffer.putShort((short) topicLength); + } + }; + + private final int magicCode; + + MessageVersion(int magicCode) { + this.magicCode = magicCode; + } + + public static MessageVersion valueOfMagicCode(int magicCode) { + for (MessageVersion version : MessageVersion.values()) { + if (version.getMagicCode() == magicCode) { + return version; + } + } + + throw new IllegalArgumentException("Invalid magicCode " + magicCode); + } + + public int getMagicCode() { + return magicCode; + } + + public abstract int getTopicLengthSize(); + + public abstract int getTopicLength(java.nio.ByteBuffer buffer); + public abstract int getTopicLength(java.nio.ByteBuffer buffer, int index); + public abstract void putTopicLength(java.nio.ByteBuffer buffer, int topicLength); +} diff --git a/common/src/main/java/org/apache/rocketmq/common/metrics/MetricsExporterType.java b/common/src/main/java/org/apache/rocketmq/common/metrics/MetricsExporterType.java new file mode 100644 index 00000000000..5f065b45342 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/metrics/MetricsExporterType.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.metrics; + + +public enum MetricsExporterType { + DISABLE(0), + OTLP_GRPC(1), + PROM(2), + LOG(3); + + private final int value; + + MetricsExporterType(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static MetricsExporterType valueOf(int value) { + switch (value) { + case 1: + return OTLP_GRPC; + case 2: + return PROM; + case 3: + return LOG; + default: + return DISABLE; + } + } + + public boolean isEnable() { + return this.value > 0; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/metrics/NopLongCounter.java b/common/src/main/java/org/apache/rocketmq/common/metrics/NopLongCounter.java new file mode 100644 index 00000000000..a281216ab34 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/metrics/NopLongCounter.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.metrics; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.context.Context; + +public class NopLongCounter implements LongCounter { + @Override public void add(long l) { + + } + + @Override public void add(long l, Attributes attributes) { + + } + + @Override public void add(long l, Attributes attributes, Context context) { + + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/metrics/NopLongHistogram.java b/common/src/main/java/org/apache/rocketmq/common/metrics/NopLongHistogram.java new file mode 100644 index 00000000000..e967c63f281 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/metrics/NopLongHistogram.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.metrics; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.context.Context; + +public class NopLongHistogram implements LongHistogram { + @Override public void record(long l) { + + } + + @Override public void record(long l, Attributes attributes) { + + } + + @Override public void record(long l, Attributes attributes, Context context) { + + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/metrics/NopLongUpDownCounter.java b/common/src/main/java/org/apache/rocketmq/common/metrics/NopLongUpDownCounter.java new file mode 100644 index 00000000000..3e8be197641 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/metrics/NopLongUpDownCounter.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.metrics; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.LongUpDownCounter; +import io.opentelemetry.context.Context; + +public class NopLongUpDownCounter implements LongUpDownCounter { + @Override public void add(long l) { + + } + + @Override public void add(long l, Attributes attributes) { + + } + + @Override public void add(long l, Attributes attributes, Context context) { + + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/metrics/NopObservableDoubleGauge.java b/common/src/main/java/org/apache/rocketmq/common/metrics/NopObservableDoubleGauge.java new file mode 100644 index 00000000000..899ac14a9a1 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/metrics/NopObservableDoubleGauge.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.metrics; + +import io.opentelemetry.api.metrics.ObservableDoubleGauge; + +public class NopObservableDoubleGauge implements ObservableDoubleGauge { +} diff --git a/common/src/main/java/org/apache/rocketmq/common/metrics/NopObservableLongGauge.java b/common/src/main/java/org/apache/rocketmq/common/metrics/NopObservableLongGauge.java new file mode 100644 index 00000000000..091fa72de63 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/metrics/NopObservableLongGauge.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.metrics; + +import io.opentelemetry.api.metrics.ObservableLongGauge; + +public class NopObservableLongGauge implements ObservableLongGauge { +} diff --git a/common/src/main/java/org/apache/rocketmq/common/namesrv/DefaultTopAddressing.java b/common/src/main/java/org/apache/rocketmq/common/namesrv/DefaultTopAddressing.java new file mode 100644 index 00000000000..179e200ae91 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/namesrv/DefaultTopAddressing.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.namesrv; + +import com.google.common.base.Strings; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.ServiceLoader; +import java.util.Map; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.help.FAQUrl; +import org.apache.rocketmq.common.utils.HttpTinyClient; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class DefaultTopAddressing implements TopAddressing { + + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); + + private String nsAddr; + private String wsAddr; + private String unitName; + private Map para; + private List topAddressingList; + + public DefaultTopAddressing(final String wsAddr) { + this(wsAddr, null); + } + + public DefaultTopAddressing(final String wsAddr, final String unitName) { + this.wsAddr = wsAddr; + this.unitName = unitName; + this.topAddressingList = loadCustomTopAddressing(); + } + + public DefaultTopAddressing(final String unitName, final Map para, final String wsAddr) { + this.wsAddr = wsAddr; + this.unitName = unitName; + this.para = para; + this.topAddressingList = loadCustomTopAddressing(); + } + + private static String clearNewLine(final String str) { + String newString = str.trim(); + int index = newString.indexOf("\r"); + if (index != -1) { + return newString.substring(0, index); + } + + index = newString.indexOf("\n"); + if (index != -1) { + return newString.substring(0, index); + } + + return newString; + } + + private List loadCustomTopAddressing() { + ServiceLoader serviceLoader = ServiceLoader.load(TopAddressing.class); + Iterator iterator = serviceLoader.iterator(); + List topAddressingList = new ArrayList<>(); + if (iterator.hasNext()) { + topAddressingList.add(iterator.next()); + } + return topAddressingList; + } + + @Override + public final String fetchNSAddr() { + if (!topAddressingList.isEmpty()) { + for (TopAddressing topAddressing : topAddressingList) { + String nsAddress = topAddressing.fetchNSAddr(); + if (!Strings.isNullOrEmpty(nsAddress)) { + return nsAddress; + } + } + } + // Return result of default implementation + return fetchNSAddr(true, 3000); + } + + @Override + public void registerChangeCallBack(NameServerUpdateCallback changeCallBack) { + if (!topAddressingList.isEmpty()) { + for (TopAddressing topAddressing : topAddressingList) { + topAddressing.registerChangeCallBack(changeCallBack); + } + } + } + + public final String fetchNSAddr(boolean verbose, long timeoutMills) { + String url = this.wsAddr; + try { + if (null != para && para.size() > 0) { + if (!UtilAll.isBlank(this.unitName)) { + url = url + "-" + this.unitName + "?nofix=1&"; + } + else { + url = url + "?"; + } + for (Map.Entry entry : this.para.entrySet()) { + url += entry.getKey() + "=" + entry.getValue() + "&"; + } + url = url.substring(0, url.length() - 1); + } + else { + if (!UtilAll.isBlank(this.unitName)) { + url = url + "-" + this.unitName + "?nofix=1"; + } + } + + HttpTinyClient.HttpResult result = HttpTinyClient.httpGet(url, null, null, "UTF-8", timeoutMills); + if (200 == result.code) { + String responseStr = result.content; + if (responseStr != null) { + return clearNewLine(responseStr); + } else { + LOGGER.error("fetch nameserver address is null"); + } + } else { + LOGGER.error("fetch nameserver address failed. statusCode=" + result.code); + } + } catch (IOException e) { + if (verbose) { + LOGGER.error("fetch name server address exception", e); + } + } + + if (verbose) { + String errorMsg = + "connect to " + url + " failed, maybe the domain name " + MixAll.getWSAddr() + " not bind in /etc/hosts"; + errorMsg += FAQUrl.suggestTodo(FAQUrl.NAME_SERVER_ADDR_NOT_EXIST_URL); + + LOGGER.warn(errorMsg); + } + return null; + } + + public String getNsAddr() { + return nsAddr; + } + + public void setNsAddr(String nsAddr) { + this.nsAddr = nsAddr; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/namesrv/NameServerUpdateCallback.java b/common/src/main/java/org/apache/rocketmq/common/namesrv/NameServerUpdateCallback.java new file mode 100644 index 00000000000..67ce2b86823 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/namesrv/NameServerUpdateCallback.java @@ -0,0 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.namesrv; + +public interface NameServerUpdateCallback { + String onNameServerAddressChange(String namesrvAddress); +} diff --git a/common/src/main/java/org/apache/rocketmq/common/namesrv/NamesrvConfig.java b/common/src/main/java/org/apache/rocketmq/common/namesrv/NamesrvConfig.java index 6f740f7fd2a..d1cdc7631c2 100644 --- a/common/src/main/java/org/apache/rocketmq/common/namesrv/NamesrvConfig.java +++ b/common/src/main/java/org/apache/rocketmq/common/namesrv/NamesrvConfig.java @@ -22,12 +22,8 @@ import java.io.File; import org.apache.rocketmq.common.MixAll; -import org.apache.rocketmq.common.constant.LoggerName; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class NamesrvConfig { - private static final Logger log = LoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME); private String rocketmqHome = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, System.getenv(MixAll.ROCKETMQ_HOME_ENV)); private String kvConfigPath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "kvConfig.json"; @@ -35,6 +31,79 @@ public class NamesrvConfig { private String productEnvName = "center"; private boolean clusterTest = false; private boolean orderMessageEnable = false; + private boolean returnOrderTopicConfigToBroker = true; + + /** + * Indicates the nums of thread to handle client requests, like GET_ROUTEINTO_BY_TOPIC. + */ + private int clientRequestThreadPoolNums = 8; + /** + * Indicates the nums of thread to handle broker or operation requests, like REGISTER_BROKER. + */ + private int defaultThreadPoolNums = 16; + /** + * Indicates the capacity of queue to hold client requests. + */ + private int clientRequestThreadPoolQueueCapacity = 50000; + /** + * Indicates the capacity of queue to hold broker or operation requests. + */ + private int defaultThreadPoolQueueCapacity = 10000; + /** + * Interval of periodic scanning for non-active broker; + */ + private long scanNotActiveBrokerInterval = 5 * 1000; + + private int unRegisterBrokerQueueCapacity = 3000; + + /** + * Support acting master or not. + * + * The slave can be an acting master when master node is down to support following operations: + * 1. support lock/unlock message queue operation. + * 2. support searchOffset, query maxOffset/minOffset operation. + * 3. support query earliest msg store time. + */ + private boolean supportActingMaster = false; + + private volatile boolean enableAllTopicList = true; + + + private volatile boolean enableTopicList = true; + + private volatile boolean notifyMinBrokerIdChanged = false; + + /** + * Is startup the controller in this name-srv + */ + private boolean enableControllerInNamesrv = false; + + private volatile boolean needWaitForService = false; + + private int waitSecondsForService = 45; + + /** + * If enable this flag, the topics that don't exist in broker registration payload will be deleted from name server. + * + * WARNING: + * 1. Enable this flag and "enableSingleTopicRegister" of broker config meanwhile to avoid losing topic route info unexpectedly. + * 2. This flag does not support static topic currently. + */ + private boolean deleteTopicWithBrokerRegistration = false; + /** + * Config in this black list will be not allowed to update by command. + * Try to update this config black list by restart process. + * Try to update configures in black list by restart process. + */ + private String configBlackList = "configBlackList;configStorePath;kvConfigPath"; + + public String getConfigBlackList() { + return configBlackList; + } + + public void setConfigBlackList(String configBlackList) { + this.configBlackList = configBlackList; + } public boolean isOrderMessageEnable() { return orderMessageEnable; @@ -83,4 +152,124 @@ public String getConfigStorePath() { public void setConfigStorePath(final String configStorePath) { this.configStorePath = configStorePath; } + + public boolean isReturnOrderTopicConfigToBroker() { + return returnOrderTopicConfigToBroker; + } + + public void setReturnOrderTopicConfigToBroker(boolean returnOrderTopicConfigToBroker) { + this.returnOrderTopicConfigToBroker = returnOrderTopicConfigToBroker; + } + + public int getClientRequestThreadPoolNums() { + return clientRequestThreadPoolNums; + } + + public void setClientRequestThreadPoolNums(final int clientRequestThreadPoolNums) { + this.clientRequestThreadPoolNums = clientRequestThreadPoolNums; + } + + public int getDefaultThreadPoolNums() { + return defaultThreadPoolNums; + } + + public void setDefaultThreadPoolNums(final int defaultThreadPoolNums) { + this.defaultThreadPoolNums = defaultThreadPoolNums; + } + + public int getClientRequestThreadPoolQueueCapacity() { + return clientRequestThreadPoolQueueCapacity; + } + + public void setClientRequestThreadPoolQueueCapacity(final int clientRequestThreadPoolQueueCapacity) { + this.clientRequestThreadPoolQueueCapacity = clientRequestThreadPoolQueueCapacity; + } + + public int getDefaultThreadPoolQueueCapacity() { + return defaultThreadPoolQueueCapacity; + } + + public void setDefaultThreadPoolQueueCapacity(final int defaultThreadPoolQueueCapacity) { + this.defaultThreadPoolQueueCapacity = defaultThreadPoolQueueCapacity; + } + + public long getScanNotActiveBrokerInterval() { + return scanNotActiveBrokerInterval; + } + + public void setScanNotActiveBrokerInterval(long scanNotActiveBrokerInterval) { + this.scanNotActiveBrokerInterval = scanNotActiveBrokerInterval; + } + + public int getUnRegisterBrokerQueueCapacity() { + return unRegisterBrokerQueueCapacity; + } + + public void setUnRegisterBrokerQueueCapacity(final int unRegisterBrokerQueueCapacity) { + this.unRegisterBrokerQueueCapacity = unRegisterBrokerQueueCapacity; + } + + public boolean isSupportActingMaster() { + return supportActingMaster; + } + + public void setSupportActingMaster(final boolean supportActingMaster) { + this.supportActingMaster = supportActingMaster; + } + + public boolean isEnableAllTopicList() { + return enableAllTopicList; + } + + public void setEnableAllTopicList(boolean enableAllTopicList) { + this.enableAllTopicList = enableAllTopicList; + } + + public boolean isEnableTopicList() { + return enableTopicList; + } + + public void setEnableTopicList(boolean enableTopicList) { + this.enableTopicList = enableTopicList; + } + + public boolean isNotifyMinBrokerIdChanged() { + return notifyMinBrokerIdChanged; + } + + public void setNotifyMinBrokerIdChanged(boolean notifyMinBrokerIdChanged) { + this.notifyMinBrokerIdChanged = notifyMinBrokerIdChanged; + } + + public boolean isEnableControllerInNamesrv() { + return enableControllerInNamesrv; + } + + public void setEnableControllerInNamesrv(boolean enableControllerInNamesrv) { + this.enableControllerInNamesrv = enableControllerInNamesrv; + } + + public boolean isNeedWaitForService() { + return needWaitForService; + } + + public void setNeedWaitForService(boolean needWaitForService) { + this.needWaitForService = needWaitForService; + } + + public int getWaitSecondsForService() { + return waitSecondsForService; + } + + public void setWaitSecondsForService(int waitSecondsForService) { + this.waitSecondsForService = waitSecondsForService; + } + + public boolean isDeleteTopicWithBrokerRegistration() { + return deleteTopicWithBrokerRegistration; + } + + public void setDeleteTopicWithBrokerRegistration(boolean deleteTopicWithBrokerRegistration) { + this.deleteTopicWithBrokerRegistration = deleteTopicWithBrokerRegistration; + } } diff --git a/common/src/main/java/org/apache/rocketmq/common/namesrv/TopAddressing.java b/common/src/main/java/org/apache/rocketmq/common/namesrv/TopAddressing.java index 57af0e709bd..3ca182825b2 100644 --- a/common/src/main/java/org/apache/rocketmq/common/namesrv/TopAddressing.java +++ b/common/src/main/java/org/apache/rocketmq/common/namesrv/TopAddressing.java @@ -14,94 +14,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -/** - * $Id: TopAddressing.java 1831 2013-05-16 01:39:51Z vintagewang@apache.org $ - */ package org.apache.rocketmq.common.namesrv; -import java.io.IOException; -import org.apache.rocketmq.common.MixAll; -import org.apache.rocketmq.common.UtilAll; -import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.common.help.FAQUrl; -import org.apache.rocketmq.common.utils.HttpTinyClient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class TopAddressing { - private static final Logger log = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); - - private String nsAddr; - private String wsAddr; - private String unitName; - - public TopAddressing(final String wsAddr) { - this(wsAddr, null); - } - - public TopAddressing(final String wsAddr, final String unitName) { - this.wsAddr = wsAddr; - this.unitName = unitName; - } - - private static String clearNewLine(final String str) { - String newString = str.trim(); - int index = newString.indexOf("\r"); - if (index != -1) { - return newString.substring(0, index); - } - - index = newString.indexOf("\n"); - if (index != -1) { - return newString.substring(0, index); - } - - return newString; - } - - public final String fetchNSAddr() { - return fetchNSAddr(true, 3000); - } - - public final String fetchNSAddr(boolean verbose, long timeoutMills) { - String url = this.wsAddr; - try { - if (!UtilAll.isBlank(this.unitName)) { - url = url + "-" + this.unitName + "?nofix=1"; - } - HttpTinyClient.HttpResult result = HttpTinyClient.httpGet(url, null, null, "UTF-8", timeoutMills); - if (200 == result.code) { - String responseStr = result.content; - if (responseStr != null) { - return clearNewLine(responseStr); - } else { - log.error("fetch nameserver address is null"); - } - } else { - log.error("fetch nameserver address failed. statusCode={}", result.code); - } - } catch (IOException e) { - if (verbose) { - log.error("fetch name server address exception", e); - } - } - - if (verbose) { - String errorMsg = - "connect to " + url + " failed, maybe the domain name " + MixAll.getWSAddr() + " not bind in /etc/hosts"; - errorMsg += FAQUrl.suggestTodo(FAQUrl.NAME_SERVER_ADDR_NOT_EXIST_URL); - log.warn(errorMsg); - } - return null; - } +public interface TopAddressing { - public String getNsAddr() { - return nsAddr; - } + String fetchNSAddr(); - public void setNsAddr(String nsAddr) { - this.nsAddr = nsAddr; - } + void registerChangeCallBack(NameServerUpdateCallback changeCallBack); } diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/RequestCode.java b/common/src/main/java/org/apache/rocketmq/common/protocol/RequestCode.java deleted file mode 100644 index 5900c0b9d0c..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/RequestCode.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.common.protocol; - -public class RequestCode { - - public static final int SEND_MESSAGE = 10; - - public static final int PULL_MESSAGE = 11; - - public static final int QUERY_MESSAGE = 12; - public static final int QUERY_BROKER_OFFSET = 13; - public static final int QUERY_CONSUMER_OFFSET = 14; - public static final int UPDATE_CONSUMER_OFFSET = 15; - public static final int UPDATE_AND_CREATE_TOPIC = 17; - public static final int GET_ALL_TOPIC_CONFIG = 21; - public static final int GET_TOPIC_CONFIG_LIST = 22; - - public static final int GET_TOPIC_NAME_LIST = 23; - - public static final int UPDATE_BROKER_CONFIG = 25; - - public static final int GET_BROKER_CONFIG = 26; - - public static final int TRIGGER_DELETE_FILES = 27; - - public static final int GET_BROKER_RUNTIME_INFO = 28; - public static final int SEARCH_OFFSET_BY_TIMESTAMP = 29; - public static final int GET_MAX_OFFSET = 30; - public static final int GET_MIN_OFFSET = 31; - - public static final int GET_EARLIEST_MSG_STORETIME = 32; - - public static final int VIEW_MESSAGE_BY_ID = 33; - - public static final int HEART_BEAT = 34; - - public static final int UNREGISTER_CLIENT = 35; - - public static final int CONSUMER_SEND_MSG_BACK = 36; - - public static final int END_TRANSACTION = 37; - public static final int GET_CONSUMER_LIST_BY_GROUP = 38; - - public static final int CHECK_TRANSACTION_STATE = 39; - - public static final int NOTIFY_CONSUMER_IDS_CHANGED = 40; - - public static final int LOCK_BATCH_MQ = 41; - - public static final int UNLOCK_BATCH_MQ = 42; - public static final int GET_ALL_CONSUMER_OFFSET = 43; - - public static final int GET_ALL_DELAY_OFFSET = 45; - - public static final int CHECK_CLIENT_CONFIG = 46; - - public static final int PUT_KV_CONFIG = 100; - - public static final int GET_KV_CONFIG = 101; - - public static final int DELETE_KV_CONFIG = 102; - - public static final int REGISTER_BROKER = 103; - - public static final int UNREGISTER_BROKER = 104; - public static final int GET_ROUTEINTO_BY_TOPIC = 105; - - public static final int GET_BROKER_CLUSTER_INFO = 106; - public static final int UPDATE_AND_CREATE_SUBSCRIPTIONGROUP = 200; - public static final int GET_ALL_SUBSCRIPTIONGROUP_CONFIG = 201; - public static final int GET_TOPIC_STATS_INFO = 202; - public static final int GET_CONSUMER_CONNECTION_LIST = 203; - public static final int GET_PRODUCER_CONNECTION_LIST = 204; - public static final int WIPE_WRITE_PERM_OF_BROKER = 205; - - public static final int GET_ALL_TOPIC_LIST_FROM_NAMESERVER = 206; - - public static final int DELETE_SUBSCRIPTIONGROUP = 207; - public static final int GET_CONSUME_STATS = 208; - - public static final int SUSPEND_CONSUMER = 209; - - public static final int RESUME_CONSUMER = 210; - public static final int RESET_CONSUMER_OFFSET_IN_CONSUMER = 211; - public static final int RESET_CONSUMER_OFFSET_IN_BROKER = 212; - - public static final int ADJUST_CONSUMER_THREAD_POOL = 213; - - public static final int WHO_CONSUME_THE_MESSAGE = 214; - - public static final int DELETE_TOPIC_IN_BROKER = 215; - - public static final int DELETE_TOPIC_IN_NAMESRV = 216; - public static final int GET_KVLIST_BY_NAMESPACE = 219; - - public static final int RESET_CONSUMER_CLIENT_OFFSET = 220; - - public static final int GET_CONSUMER_STATUS_FROM_CLIENT = 221; - - public static final int INVOKE_BROKER_TO_RESET_OFFSET = 222; - - public static final int INVOKE_BROKER_TO_GET_CONSUMER_STATUS = 223; - - public static final int QUERY_TOPIC_CONSUME_BY_WHO = 300; - - public static final int GET_TOPICS_BY_CLUSTER = 224; - - public static final int REGISTER_FILTER_SERVER = 301; - public static final int REGISTER_MESSAGE_FILTER_CLASS = 302; - - public static final int QUERY_CONSUME_TIME_SPAN = 303; - - public static final int GET_SYSTEM_TOPIC_LIST_FROM_NS = 304; - public static final int GET_SYSTEM_TOPIC_LIST_FROM_BROKER = 305; - - public static final int CLEAN_EXPIRED_CONSUMEQUEUE = 306; - - public static final int GET_CONSUMER_RUNNING_INFO = 307; - - public static final int QUERY_CORRECTION_OFFSET = 308; - public static final int CONSUME_MESSAGE_DIRECTLY = 309; - - public static final int SEND_MESSAGE_V2 = 310; - - public static final int GET_UNIT_TOPIC_LIST = 311; - - public static final int GET_HAS_UNIT_SUB_TOPIC_LIST = 312; - - public static final int GET_HAS_UNIT_SUB_UNUNIT_TOPIC_LIST = 313; - - public static final int CLONE_GROUP_OFFSET = 314; - - public static final int VIEW_BROKER_STATS_DATA = 315; - - public static final int CLEAN_UNUSED_TOPIC = 316; - - public static final int GET_BROKER_CONSUME_STATS = 317; - - /** - * update the config of name server - */ - public static final int UPDATE_NAMESRV_CONFIG = 318; - - /** - * get config from name server - */ - public static final int GET_NAMESRV_CONFIG = 319; - - public static final int SEND_BATCH_MESSAGE = 320; - - public static final int QUERY_CONSUME_QUEUE = 321; -} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/ResponseCode.java b/common/src/main/java/org/apache/rocketmq/common/protocol/ResponseCode.java deleted file mode 100644 index f62c4ea6187..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/ResponseCode.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.common.protocol; - -import org.apache.rocketmq.remoting.protocol.RemotingSysResponseCode; - -public class ResponseCode extends RemotingSysResponseCode { - - public static final int FLUSH_DISK_TIMEOUT = 10; - - public static final int SLAVE_NOT_AVAILABLE = 11; - - public static final int FLUSH_SLAVE_TIMEOUT = 12; - - public static final int MESSAGE_ILLEGAL = 13; - - public static final int SERVICE_NOT_AVAILABLE = 14; - - public static final int VERSION_NOT_SUPPORTED = 15; - - public static final int NO_PERMISSION = 16; - - public static final int TOPIC_NOT_EXIST = 17; - public static final int TOPIC_EXIST_ALREADY = 18; - public static final int PULL_NOT_FOUND = 19; - - public static final int PULL_RETRY_IMMEDIATELY = 20; - - public static final int PULL_OFFSET_MOVED = 21; - - public static final int QUERY_NOT_FOUND = 22; - - public static final int SUBSCRIPTION_PARSE_FAILED = 23; - - public static final int SUBSCRIPTION_NOT_EXIST = 24; - - public static final int SUBSCRIPTION_NOT_LATEST = 25; - - public static final int SUBSCRIPTION_GROUP_NOT_EXIST = 26; - - public static final int FILTER_DATA_NOT_EXIST = 27; - - public static final int FILTER_DATA_NOT_LATEST = 28; - - public static final int TRANSACTION_SHOULD_COMMIT = 200; - - public static final int TRANSACTION_SHOULD_ROLLBACK = 201; - - public static final int TRANSACTION_STATE_UNKNOW = 202; - - public static final int TRANSACTION_STATE_GROUP_WRONG = 203; - public static final int NO_BUYER_ID = 204; - - public static final int NOT_IN_CURRENT_UNIT = 205; - - public static final int CONSUMER_NOT_ONLINE = 206; - - public static final int CONSUME_MSG_TIMEOUT = 207; - - public static final int NO_MESSAGE = 208; -} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ClusterInfo.java b/common/src/main/java/org/apache/rocketmq/common/protocol/body/ClusterInfo.java deleted file mode 100644 index 76c64a8508c..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ClusterInfo.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.common.protocol.body; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Set; -import org.apache.rocketmq.common.protocol.route.BrokerData; -import org.apache.rocketmq.remoting.protocol.RemotingSerializable; - -public class ClusterInfo extends RemotingSerializable { - private HashMap brokerAddrTable; - private HashMap> clusterAddrTable; - - public HashMap getBrokerAddrTable() { - return brokerAddrTable; - } - - public void setBrokerAddrTable(HashMap brokerAddrTable) { - this.brokerAddrTable = brokerAddrTable; - } - - public HashMap> getClusterAddrTable() { - return clusterAddrTable; - } - - public void setClusterAddrTable(HashMap> clusterAddrTable) { - this.clusterAddrTable = clusterAddrTable; - } - - public String[] retrieveAllAddrByCluster(String cluster) { - List addrs = new ArrayList(); - if (clusterAddrTable.containsKey(cluster)) { - Set brokerNames = clusterAddrTable.get(cluster); - for (String brokerName : brokerNames) { - BrokerData brokerData = brokerAddrTable.get(brokerName); - if (null != brokerData) { - addrs.addAll(brokerData.getBrokerAddrs().values()); - } - } - } - - return addrs.toArray(new String[] {}); - } - - public String[] retrieveAllClusterNames() { - return clusterAddrTable.keySet().toArray(new String[] {}); - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/LockBatchRequestBody.java b/common/src/main/java/org/apache/rocketmq/common/protocol/body/LockBatchRequestBody.java deleted file mode 100644 index 480862b8f47..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/LockBatchRequestBody.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.common.protocol.body; - -import java.util.HashSet; -import java.util.Set; -import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.remoting.protocol.RemotingSerializable; - -public class LockBatchRequestBody extends RemotingSerializable { - private String consumerGroup; - private String clientId; - private Set mqSet = new HashSet(); - - public String getConsumerGroup() { - return consumerGroup; - } - - public void setConsumerGroup(String consumerGroup) { - this.consumerGroup = consumerGroup; - } - - public String getClientId() { - return clientId; - } - - public void setClientId(String clientId) { - this.clientId = clientId; - } - - public Set getMqSet() { - return mqSet; - } - - public void setMqSet(Set mqSet) { - this.mqSet = mqSet; - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/RegisterBrokerBody.java b/common/src/main/java/org/apache/rocketmq/common/protocol/body/RegisterBrokerBody.java deleted file mode 100644 index c220927c23b..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/RegisterBrokerBody.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.common.protocol.body; - -import java.util.ArrayList; -import java.util.List; -import org.apache.rocketmq.remoting.protocol.RemotingSerializable; - -public class RegisterBrokerBody extends RemotingSerializable { - private TopicConfigSerializeWrapper topicConfigSerializeWrapper = new TopicConfigSerializeWrapper(); - private List filterServerList = new ArrayList(); - - public TopicConfigSerializeWrapper getTopicConfigSerializeWrapper() { - return topicConfigSerializeWrapper; - } - - public void setTopicConfigSerializeWrapper(TopicConfigSerializeWrapper topicConfigSerializeWrapper) { - this.topicConfigSerializeWrapper = topicConfigSerializeWrapper; - } - - public List getFilterServerList() { - return filterServerList; - } - - public void setFilterServerList(List filterServerList) { - this.filterServerList = filterServerList; - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/UnlockBatchRequestBody.java b/common/src/main/java/org/apache/rocketmq/common/protocol/body/UnlockBatchRequestBody.java deleted file mode 100644 index baf40713065..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/UnlockBatchRequestBody.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.common.protocol.body; - -import java.util.HashSet; -import java.util.Set; -import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.remoting.protocol.RemotingSerializable; - -public class UnlockBatchRequestBody extends RemotingSerializable { - private String consumerGroup; - private String clientId; - private Set mqSet = new HashSet(); - - public String getConsumerGroup() { - return consumerGroup; - } - - public void setConsumerGroup(String consumerGroup) { - this.consumerGroup = consumerGroup; - } - - public String getClientId() { - return clientId; - } - - public void setClientId(String clientId) { - this.clientId = clientId; - } - - public Set getMqSet() { - return mqSet; - } - - public void setMqSet(Set mqSet) { - this.mqSet = mqSet; - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/CheckTransactionStateRequestHeader.java b/common/src/main/java/org/apache/rocketmq/common/protocol/header/CheckTransactionStateRequestHeader.java deleted file mode 100644 index 76c7b42791f..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/CheckTransactionStateRequestHeader.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * $Id: EndTransactionRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ - */ -package org.apache.rocketmq.common.protocol.header; - -import org.apache.rocketmq.remoting.CommandCustomHeader; -import org.apache.rocketmq.remoting.annotation.CFNotNull; -import org.apache.rocketmq.remoting.exception.RemotingCommandException; - -public class CheckTransactionStateRequestHeader implements CommandCustomHeader { - @CFNotNull - private Long tranStateTableOffset; - @CFNotNull - private Long commitLogOffset; - private String msgId; - private String transactionId; - - @Override - public void checkFields() throws RemotingCommandException { - } - - public Long getTranStateTableOffset() { - return tranStateTableOffset; - } - - public void setTranStateTableOffset(Long tranStateTableOffset) { - this.tranStateTableOffset = tranStateTableOffset; - } - - public Long getCommitLogOffset() { - return commitLogOffset; - } - - public void setCommitLogOffset(Long commitLogOffset) { - this.commitLogOffset = commitLogOffset; - } - - public String getMsgId() { - return msgId; - } - - public void setMsgId(String msgId) { - this.msgId = msgId; - } - - public String getTransactionId() { - return transactionId; - } - - public void setTransactionId(String transactionId) { - this.transactionId = transactionId; - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/ConsumeMessageDirectlyResultRequestHeader.java b/common/src/main/java/org/apache/rocketmq/common/protocol/header/ConsumeMessageDirectlyResultRequestHeader.java deleted file mode 100644 index 1bd089d7a7b..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/ConsumeMessageDirectlyResultRequestHeader.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.common.protocol.header; - -import org.apache.rocketmq.remoting.CommandCustomHeader; -import org.apache.rocketmq.remoting.annotation.CFNotNull; -import org.apache.rocketmq.remoting.annotation.CFNullable; -import org.apache.rocketmq.remoting.exception.RemotingCommandException; - -public class ConsumeMessageDirectlyResultRequestHeader implements CommandCustomHeader { - @CFNotNull - private String consumerGroup; - @CFNullable - private String clientId; - @CFNullable - private String msgId; - @CFNullable - private String brokerName; - - @Override - public void checkFields() throws RemotingCommandException { - } - - public String getConsumerGroup() { - return consumerGroup; - } - - public void setConsumerGroup(String consumerGroup) { - this.consumerGroup = consumerGroup; - } - - public String getBrokerName() { - return brokerName; - } - - public void setBrokerName(String brokerName) { - this.brokerName = brokerName; - } - - public String getClientId() { - return clientId; - } - - public void setClientId(String clientId) { - this.clientId = clientId; - } - - public String getMsgId() { - return msgId; - } - - public void setMsgId(String msgId) { - this.msgId = msgId; - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/CreateTopicRequestHeader.java b/common/src/main/java/org/apache/rocketmq/common/protocol/header/CreateTopicRequestHeader.java deleted file mode 100644 index 8894d0b938f..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/CreateTopicRequestHeader.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * $Id: CreateTopicRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ - */ -package org.apache.rocketmq.common.protocol.header; - -import org.apache.rocketmq.common.TopicFilterType; -import org.apache.rocketmq.remoting.CommandCustomHeader; -import org.apache.rocketmq.remoting.annotation.CFNotNull; -import org.apache.rocketmq.remoting.exception.RemotingCommandException; - -public class CreateTopicRequestHeader implements CommandCustomHeader { - @CFNotNull - private String topic; - @CFNotNull - private String defaultTopic; - @CFNotNull - private Integer readQueueNums; - @CFNotNull - private Integer writeQueueNums; - @CFNotNull - private Integer perm; - @CFNotNull - private String topicFilterType; - private Integer topicSysFlag; - @CFNotNull - private Boolean order = false; - - @Override - public void checkFields() throws RemotingCommandException { - try { - TopicFilterType.valueOf(this.topicFilterType); - } catch (Exception e) { - throw new RemotingCommandException("topicFilterType = [" + topicFilterType + "] value invalid", e); - } - } - - public TopicFilterType getTopicFilterTypeEnum() { - return TopicFilterType.valueOf(this.topicFilterType); - } - - public String getTopic() { - return topic; - } - - public void setTopic(String topic) { - this.topic = topic; - } - - public String getDefaultTopic() { - return defaultTopic; - } - - public void setDefaultTopic(String defaultTopic) { - this.defaultTopic = defaultTopic; - } - - public Integer getReadQueueNums() { - return readQueueNums; - } - - public void setReadQueueNums(Integer readQueueNums) { - this.readQueueNums = readQueueNums; - } - - public Integer getWriteQueueNums() { - return writeQueueNums; - } - - public void setWriteQueueNums(Integer writeQueueNums) { - this.writeQueueNums = writeQueueNums; - } - - public Integer getPerm() { - return perm; - } - - public void setPerm(Integer perm) { - this.perm = perm; - } - - public String getTopicFilterType() { - return topicFilterType; - } - - public void setTopicFilterType(String topicFilterType) { - this.topicFilterType = topicFilterType; - } - - public Integer getTopicSysFlag() { - return topicSysFlag; - } - - public void setTopicSysFlag(Integer topicSysFlag) { - this.topicSysFlag = topicSysFlag; - } - - public Boolean getOrder() { - return order; - } - - public void setOrder(Boolean order) { - this.order = order; - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetMaxOffsetRequestHeader.java b/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetMaxOffsetRequestHeader.java deleted file mode 100644 index 871309de6ce..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetMaxOffsetRequestHeader.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * $Id: GetMaxOffsetRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ - */ -package org.apache.rocketmq.common.protocol.header; - -import org.apache.rocketmq.remoting.CommandCustomHeader; -import org.apache.rocketmq.remoting.annotation.CFNotNull; -import org.apache.rocketmq.remoting.exception.RemotingCommandException; - -public class GetMaxOffsetRequestHeader implements CommandCustomHeader { - @CFNotNull - private String topic; - @CFNotNull - private Integer queueId; - - @Override - public void checkFields() throws RemotingCommandException { - } - - public String getTopic() { - return topic; - } - - public void setTopic(String topic) { - this.topic = topic; - } - - public Integer getQueueId() { - return queueId; - } - - public void setQueueId(Integer queueId) { - this.queueId = queueId; - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/PullMessageRequestHeader.java b/common/src/main/java/org/apache/rocketmq/common/protocol/header/PullMessageRequestHeader.java deleted file mode 100644 index 106e89e511c..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/PullMessageRequestHeader.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * $Id: PullMessageRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ - */ -package org.apache.rocketmq.common.protocol.header; - -import org.apache.rocketmq.remoting.CommandCustomHeader; -import org.apache.rocketmq.remoting.annotation.CFNotNull; -import org.apache.rocketmq.remoting.annotation.CFNullable; -import org.apache.rocketmq.remoting.exception.RemotingCommandException; - -public class PullMessageRequestHeader implements CommandCustomHeader { - @CFNotNull - private String consumerGroup; - @CFNotNull - private String topic; - @CFNotNull - private Integer queueId; - @CFNotNull - private Long queueOffset; - @CFNotNull - private Integer maxMsgNums; - @CFNotNull - private Integer sysFlag; - @CFNotNull - private Long commitOffset; - @CFNotNull - private Long suspendTimeoutMillis; - @CFNullable - private String subscription; - @CFNotNull - private Long subVersion; - private String expressionType; - - @Override - public void checkFields() throws RemotingCommandException { - } - - public String getConsumerGroup() { - return consumerGroup; - } - - public void setConsumerGroup(String consumerGroup) { - this.consumerGroup = consumerGroup; - } - - public String getTopic() { - return topic; - } - - public void setTopic(String topic) { - this.topic = topic; - } - - public Integer getQueueId() { - return queueId; - } - - public void setQueueId(Integer queueId) { - this.queueId = queueId; - } - - public Long getQueueOffset() { - return queueOffset; - } - - public void setQueueOffset(Long queueOffset) { - this.queueOffset = queueOffset; - } - - public Integer getMaxMsgNums() { - return maxMsgNums; - } - - public void setMaxMsgNums(Integer maxMsgNums) { - this.maxMsgNums = maxMsgNums; - } - - public Integer getSysFlag() { - return sysFlag; - } - - public void setSysFlag(Integer sysFlag) { - this.sysFlag = sysFlag; - } - - public Long getCommitOffset() { - return commitOffset; - } - - public void setCommitOffset(Long commitOffset) { - this.commitOffset = commitOffset; - } - - public Long getSuspendTimeoutMillis() { - return suspendTimeoutMillis; - } - - public void setSuspendTimeoutMillis(Long suspendTimeoutMillis) { - this.suspendTimeoutMillis = suspendTimeoutMillis; - } - - public String getSubscription() { - return subscription; - } - - public void setSubscription(String subscription) { - this.subscription = subscription; - } - - public Long getSubVersion() { - return subVersion; - } - - public void setSubVersion(Long subVersion) { - this.subVersion = subVersion; - } - - public String getExpressionType() { - return expressionType; - } - - public void setExpressionType(String expressionType) { - this.expressionType = expressionType; - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/PullMessageResponseHeader.java b/common/src/main/java/org/apache/rocketmq/common/protocol/header/PullMessageResponseHeader.java deleted file mode 100644 index 0112f7da8d6..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/PullMessageResponseHeader.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * $Id: PullMessageResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ - */ -package org.apache.rocketmq.common.protocol.header; - -import org.apache.rocketmq.remoting.CommandCustomHeader; -import org.apache.rocketmq.remoting.annotation.CFNotNull; -import org.apache.rocketmq.remoting.exception.RemotingCommandException; - -public class PullMessageResponseHeader implements CommandCustomHeader { - @CFNotNull - private Long suggestWhichBrokerId; - @CFNotNull - private Long nextBeginOffset; - @CFNotNull - private Long minOffset; - @CFNotNull - private Long maxOffset; - - @Override - public void checkFields() throws RemotingCommandException { - } - - public Long getNextBeginOffset() { - return nextBeginOffset; - } - - public void setNextBeginOffset(Long nextBeginOffset) { - this.nextBeginOffset = nextBeginOffset; - } - - public Long getMinOffset() { - return minOffset; - } - - public void setMinOffset(Long minOffset) { - this.minOffset = minOffset; - } - - public Long getMaxOffset() { - return maxOffset; - } - - public void setMaxOffset(Long maxOffset) { - this.maxOffset = maxOffset; - } - - public Long getSuggestWhichBrokerId() { - return suggestWhichBrokerId; - } - - public void setSuggestWhichBrokerId(Long suggestWhichBrokerId) { - this.suggestWhichBrokerId = suggestWhichBrokerId; - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/QueryConsumerOffsetRequestHeader.java b/common/src/main/java/org/apache/rocketmq/common/protocol/header/QueryConsumerOffsetRequestHeader.java deleted file mode 100644 index 3b7f627c35a..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/QueryConsumerOffsetRequestHeader.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * $Id: QueryConsumerOffsetRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ - */ -package org.apache.rocketmq.common.protocol.header; - -import org.apache.rocketmq.remoting.CommandCustomHeader; -import org.apache.rocketmq.remoting.annotation.CFNotNull; -import org.apache.rocketmq.remoting.exception.RemotingCommandException; - -public class QueryConsumerOffsetRequestHeader implements CommandCustomHeader { - @CFNotNull - private String consumerGroup; - @CFNotNull - private String topic; - @CFNotNull - private Integer queueId; - - @Override - public void checkFields() throws RemotingCommandException { - } - - public String getConsumerGroup() { - return consumerGroup; - } - - public void setConsumerGroup(String consumerGroup) { - this.consumerGroup = consumerGroup; - } - - public String getTopic() { - return topic; - } - - public void setTopic(String topic) { - this.topic = topic; - } - - public Integer getQueueId() { - return queueId; - } - - public void setQueueId(Integer queueId) { - this.queueId = queueId; - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/SearchOffsetRequestHeader.java b/common/src/main/java/org/apache/rocketmq/common/protocol/header/SearchOffsetRequestHeader.java deleted file mode 100644 index 5ea2e24bfc8..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/SearchOffsetRequestHeader.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * $Id: SearchOffsetRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ - */ -package org.apache.rocketmq.common.protocol.header; - -import org.apache.rocketmq.remoting.CommandCustomHeader; -import org.apache.rocketmq.remoting.annotation.CFNotNull; -import org.apache.rocketmq.remoting.exception.RemotingCommandException; - -public class SearchOffsetRequestHeader implements CommandCustomHeader { - @CFNotNull - private String topic; - @CFNotNull - private Integer queueId; - @CFNotNull - private Long timestamp; - - @Override - public void checkFields() throws RemotingCommandException { - - } - - public String getTopic() { - return topic; - } - - public void setTopic(String topic) { - this.topic = topic; - } - - public Integer getQueueId() { - return queueId; - } - - public void setQueueId(Integer queueId) { - this.queueId = queueId; - } - - public Long getTimestamp() { - return timestamp; - } - - public void setTimestamp(Long timestamp) { - this.timestamp = timestamp; - } - -} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/SendMessageRequestHeader.java b/common/src/main/java/org/apache/rocketmq/common/protocol/header/SendMessageRequestHeader.java deleted file mode 100644 index 2df31e6bb2a..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/SendMessageRequestHeader.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * $Id: SendMessageRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ - */ -package org.apache.rocketmq.common.protocol.header; - -import org.apache.rocketmq.remoting.CommandCustomHeader; -import org.apache.rocketmq.remoting.annotation.CFNotNull; -import org.apache.rocketmq.remoting.annotation.CFNullable; -import org.apache.rocketmq.remoting.exception.RemotingCommandException; - -public class SendMessageRequestHeader implements CommandCustomHeader { - @CFNotNull - private String producerGroup; - @CFNotNull - private String topic; - @CFNotNull - private String defaultTopic; - @CFNotNull - private Integer defaultTopicQueueNums; - @CFNotNull - private Integer queueId; - @CFNotNull - private Integer sysFlag; - @CFNotNull - private Long bornTimestamp; - @CFNotNull - private Integer flag; - @CFNullable - private String properties; - @CFNullable - private Integer reconsumeTimes; - @CFNullable - private boolean unitMode = false; - @CFNullable - private boolean batch = false; - private Integer maxReconsumeTimes; - - @Override - public void checkFields() throws RemotingCommandException { - } - - public String getProducerGroup() { - return producerGroup; - } - - public void setProducerGroup(String producerGroup) { - this.producerGroup = producerGroup; - } - - public String getTopic() { - return topic; - } - - public void setTopic(String topic) { - this.topic = topic; - } - - public String getDefaultTopic() { - return defaultTopic; - } - - public void setDefaultTopic(String defaultTopic) { - this.defaultTopic = defaultTopic; - } - - public Integer getDefaultTopicQueueNums() { - return defaultTopicQueueNums; - } - - public void setDefaultTopicQueueNums(Integer defaultTopicQueueNums) { - this.defaultTopicQueueNums = defaultTopicQueueNums; - } - - public Integer getQueueId() { - return queueId; - } - - public void setQueueId(Integer queueId) { - this.queueId = queueId; - } - - public Integer getSysFlag() { - return sysFlag; - } - - public void setSysFlag(Integer sysFlag) { - this.sysFlag = sysFlag; - } - - public Long getBornTimestamp() { - return bornTimestamp; - } - - public void setBornTimestamp(Long bornTimestamp) { - this.bornTimestamp = bornTimestamp; - } - - public Integer getFlag() { - return flag; - } - - public void setFlag(Integer flag) { - this.flag = flag; - } - - public String getProperties() { - return properties; - } - - public void setProperties(String properties) { - this.properties = properties; - } - - public Integer getReconsumeTimes() { - return reconsumeTimes; - } - - public void setReconsumeTimes(Integer reconsumeTimes) { - this.reconsumeTimes = reconsumeTimes; - } - - public boolean isUnitMode() { - return unitMode; - } - - public void setUnitMode(boolean isUnitMode) { - this.unitMode = isUnitMode; - } - - public Integer getMaxReconsumeTimes() { - return maxReconsumeTimes; - } - - public void setMaxReconsumeTimes(final Integer maxReconsumeTimes) { - this.maxReconsumeTimes = maxReconsumeTimes; - } - - public boolean isBatch() { - return batch; - } - - public void setBatch(boolean batch) { - this.batch = batch; - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/SendMessageRequestHeaderV2.java b/common/src/main/java/org/apache/rocketmq/common/protocol/header/SendMessageRequestHeaderV2.java deleted file mode 100644 index 4e0098b5f0e..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/SendMessageRequestHeaderV2.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.common.protocol.header; - -import org.apache.rocketmq.remoting.CommandCustomHeader; -import org.apache.rocketmq.remoting.annotation.CFNotNull; -import org.apache.rocketmq.remoting.annotation.CFNullable; -import org.apache.rocketmq.remoting.exception.RemotingCommandException; - -/** - * Use short variable name to speed up FastJson deserialization process. - */ -public class SendMessageRequestHeaderV2 implements CommandCustomHeader { - @CFNotNull - private String a; // producerGroup; - @CFNotNull - private String b; // topic; - @CFNotNull - private String c; // defaultTopic; - @CFNotNull - private Integer d; // defaultTopicQueueNums; - @CFNotNull - private Integer e; // queueId; - @CFNotNull - private Integer f; // sysFlag; - @CFNotNull - private Long g; // bornTimestamp; - @CFNotNull - private Integer h; // flag; - @CFNullable - private String i; // properties; - @CFNullable - private Integer j; // reconsumeTimes; - @CFNullable - private boolean k; // unitMode = false; - - private Integer l; // consumeRetryTimes - - @CFNullable - private boolean m; //batch - - public static SendMessageRequestHeader createSendMessageRequestHeaderV1(final SendMessageRequestHeaderV2 v2) { - SendMessageRequestHeader v1 = new SendMessageRequestHeader(); - v1.setProducerGroup(v2.a); - v1.setTopic(v2.b); - v1.setDefaultTopic(v2.c); - v1.setDefaultTopicQueueNums(v2.d); - v1.setQueueId(v2.e); - v1.setSysFlag(v2.f); - v1.setBornTimestamp(v2.g); - v1.setFlag(v2.h); - v1.setProperties(v2.i); - v1.setReconsumeTimes(v2.j); - v1.setUnitMode(v2.k); - v1.setMaxReconsumeTimes(v2.l); - v1.setBatch(v2.m); - return v1; - } - - public static SendMessageRequestHeaderV2 createSendMessageRequestHeaderV2(final SendMessageRequestHeader v1) { - SendMessageRequestHeaderV2 v2 = new SendMessageRequestHeaderV2(); - v2.a = v1.getProducerGroup(); - v2.b = v1.getTopic(); - v2.c = v1.getDefaultTopic(); - v2.d = v1.getDefaultTopicQueueNums(); - v2.e = v1.getQueueId(); - v2.f = v1.getSysFlag(); - v2.g = v1.getBornTimestamp(); - v2.h = v1.getFlag(); - v2.i = v1.getProperties(); - v2.j = v1.getReconsumeTimes(); - v2.k = v1.isUnitMode(); - v2.l = v1.getMaxReconsumeTimes(); - v2.m = v1.isBatch(); - return v2; - } - - @Override - public void checkFields() throws RemotingCommandException { - } - - public String getA() { - return a; - } - - public void setA(String a) { - this.a = a; - } - - public String getB() { - return b; - } - - public void setB(String b) { - this.b = b; - } - - public String getC() { - return c; - } - - public void setC(String c) { - this.c = c; - } - - public Integer getD() { - return d; - } - - public void setD(Integer d) { - this.d = d; - } - - public Integer getE() { - return e; - } - - public void setE(Integer e) { - this.e = e; - } - - public Integer getF() { - return f; - } - - public void setF(Integer f) { - this.f = f; - } - - public Long getG() { - return g; - } - - public void setG(Long g) { - this.g = g; - } - - public Integer getH() { - return h; - } - - public void setH(Integer h) { - this.h = h; - } - - public String getI() { - return i; - } - - public void setI(String i) { - this.i = i; - } - - public Integer getJ() { - return j; - } - - public void setJ(Integer j) { - this.j = j; - } - - public boolean isK() { - return k; - } - - public void setK(boolean k) { - this.k = k; - } - - public Integer getL() { - return l; - } - - public void setL(final Integer l) { - this.l = l; - } - - public boolean isM() { - return m; - } - - public void setM(boolean m) { - this.m = m; - } -} \ No newline at end of file diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/SendMessageResponseHeader.java b/common/src/main/java/org/apache/rocketmq/common/protocol/header/SendMessageResponseHeader.java deleted file mode 100644 index 6834881ec8a..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/SendMessageResponseHeader.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * $Id: SendMessageResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ - */ -package org.apache.rocketmq.common.protocol.header; - -import org.apache.rocketmq.remoting.CommandCustomHeader; -import org.apache.rocketmq.remoting.annotation.CFNotNull; -import org.apache.rocketmq.remoting.exception.RemotingCommandException; - -public class SendMessageResponseHeader implements CommandCustomHeader { - @CFNotNull - private String msgId; - @CFNotNull - private Integer queueId; - @CFNotNull - private Long queueOffset; - private String transactionId; - - @Override - public void checkFields() throws RemotingCommandException { - } - - public String getMsgId() { - return msgId; - } - - public void setMsgId(String msgId) { - this.msgId = msgId; - } - - public Integer getQueueId() { - return queueId; - } - - public void setQueueId(Integer queueId) { - this.queueId = queueId; - } - - public Long getQueueOffset() { - return queueOffset; - } - - public void setQueueOffset(Long queueOffset) { - this.queueOffset = queueOffset; - } - - public String getTransactionId() { - return transactionId; - } - - public void setTransactionId(String transactionId) { - this.transactionId = transactionId; - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/filtersrv/RegisterFilterServerRequestHeader.java b/common/src/main/java/org/apache/rocketmq/common/protocol/header/filtersrv/RegisterFilterServerRequestHeader.java deleted file mode 100644 index 0b5ac1e13c0..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/filtersrv/RegisterFilterServerRequestHeader.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.common.protocol.header.filtersrv; - -import org.apache.rocketmq.remoting.CommandCustomHeader; -import org.apache.rocketmq.remoting.annotation.CFNotNull; -import org.apache.rocketmq.remoting.exception.RemotingCommandException; - -public class RegisterFilterServerRequestHeader implements CommandCustomHeader { - @CFNotNull - private String filterServerAddr; - - @Override - public void checkFields() throws RemotingCommandException { - } - - public String getFilterServerAddr() { - return filterServerAddr; - } - - public void setFilterServerAddr(String filterServerAddr) { - this.filterServerAddr = filterServerAddr; - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/filtersrv/RegisterFilterServerResponseHeader.java b/common/src/main/java/org/apache/rocketmq/common/protocol/header/filtersrv/RegisterFilterServerResponseHeader.java deleted file mode 100644 index edfe7b70da5..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/filtersrv/RegisterFilterServerResponseHeader.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.common.protocol.header.filtersrv; - -import org.apache.rocketmq.remoting.CommandCustomHeader; -import org.apache.rocketmq.remoting.annotation.CFNotNull; -import org.apache.rocketmq.remoting.exception.RemotingCommandException; - -public class RegisterFilterServerResponseHeader implements CommandCustomHeader { - @CFNotNull - private String brokerName; - @CFNotNull - private long brokerId; - - @Override - public void checkFields() throws RemotingCommandException { - } - - public long getBrokerId() { - return brokerId; - } - - public void setBrokerId(long brokerId) { - this.brokerId = brokerId; - } - - public String getBrokerName() { - return brokerName; - } - - public void setBrokerName(String brokerName) { - this.brokerName = brokerName; - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/filtersrv/RegisterMessageFilterClassRequestHeader.java b/common/src/main/java/org/apache/rocketmq/common/protocol/header/filtersrv/RegisterMessageFilterClassRequestHeader.java deleted file mode 100644 index e9c0e46fc8d..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/filtersrv/RegisterMessageFilterClassRequestHeader.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.common.protocol.header.filtersrv; - -import org.apache.rocketmq.remoting.CommandCustomHeader; -import org.apache.rocketmq.remoting.annotation.CFNotNull; -import org.apache.rocketmq.remoting.exception.RemotingCommandException; - -public class RegisterMessageFilterClassRequestHeader implements CommandCustomHeader { - @CFNotNull - private String consumerGroup; - @CFNotNull - private String topic; - @CFNotNull - private String className; - @CFNotNull - private Integer classCRC; - - @Override - public void checkFields() throws RemotingCommandException { - } - - public String getConsumerGroup() { - return consumerGroup; - } - - public void setConsumerGroup(String consumerGroup) { - this.consumerGroup = consumerGroup; - } - - public String getTopic() { - return topic; - } - - public void setTopic(String topic) { - this.topic = topic; - } - - public String getClassName() { - return className; - } - - public void setClassName(String className) { - this.className = className; - } - - public Integer getClassCRC() { - return classCRC; - } - - public void setClassCRC(Integer classCRC) { - this.classCRC = classCRC; - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/DeleteTopicInNamesrvRequestHeader.java b/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/DeleteTopicInNamesrvRequestHeader.java deleted file mode 100644 index ab768ee33c2..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/DeleteTopicInNamesrvRequestHeader.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.common.protocol.header.namesrv; - -import org.apache.rocketmq.remoting.CommandCustomHeader; -import org.apache.rocketmq.remoting.annotation.CFNotNull; -import org.apache.rocketmq.remoting.exception.RemotingCommandException; - -public class DeleteTopicInNamesrvRequestHeader implements CommandCustomHeader { - @CFNotNull - private String topic; - - @Override - public void checkFields() throws RemotingCommandException { - } - - public String getTopic() { - return topic; - } - - public void setTopic(String topic) { - this.topic = topic; - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/GetRouteInfoRequestHeader.java b/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/GetRouteInfoRequestHeader.java deleted file mode 100644 index a2806e62897..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/GetRouteInfoRequestHeader.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * $Id: GetRouteInfoRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ - */ -package org.apache.rocketmq.common.protocol.header.namesrv; - -import org.apache.rocketmq.remoting.CommandCustomHeader; -import org.apache.rocketmq.remoting.annotation.CFNotNull; -import org.apache.rocketmq.remoting.exception.RemotingCommandException; - -public class GetRouteInfoRequestHeader implements CommandCustomHeader { - @CFNotNull - private String topic; - - @Override - public void checkFields() throws RemotingCommandException { - } - - public String getTopic() { - return topic; - } - - public void setTopic(String topic) { - this.topic = topic; - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/RegisterBrokerRequestHeader.java b/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/RegisterBrokerRequestHeader.java deleted file mode 100644 index 45d5b6e9ebd..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/RegisterBrokerRequestHeader.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * $Id: RegisterBrokerRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ - */ -package org.apache.rocketmq.common.protocol.header.namesrv; - -import org.apache.rocketmq.remoting.CommandCustomHeader; -import org.apache.rocketmq.remoting.annotation.CFNotNull; -import org.apache.rocketmq.remoting.exception.RemotingCommandException; - -public class RegisterBrokerRequestHeader implements CommandCustomHeader { - @CFNotNull - private String brokerName; - @CFNotNull - private String brokerAddr; - @CFNotNull - private String clusterName; - @CFNotNull - private String haServerAddr; - @CFNotNull - private Long brokerId; - - @Override - public void checkFields() throws RemotingCommandException { - } - - public String getBrokerName() { - return brokerName; - } - - public void setBrokerName(String brokerName) { - this.brokerName = brokerName; - } - - public String getBrokerAddr() { - return brokerAddr; - } - - public void setBrokerAddr(String brokerAddr) { - this.brokerAddr = brokerAddr; - } - - public String getClusterName() { - return clusterName; - } - - public void setClusterName(String clusterName) { - this.clusterName = clusterName; - } - - public String getHaServerAddr() { - return haServerAddr; - } - - public void setHaServerAddr(String haServerAddr) { - this.haServerAddr = haServerAddr; - } - - public Long getBrokerId() { - return brokerId; - } - - public void setBrokerId(Long brokerId) { - this.brokerId = brokerId; - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/heartbeat/HeartbeatData.java b/common/src/main/java/org/apache/rocketmq/common/protocol/heartbeat/HeartbeatData.java deleted file mode 100644 index 47ae542a33f..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/heartbeat/HeartbeatData.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * $Id: HeartbeatData.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ - */ -package org.apache.rocketmq.common.protocol.heartbeat; - -import java.util.HashSet; -import java.util.Set; -import org.apache.rocketmq.remoting.protocol.RemotingSerializable; - -public class HeartbeatData extends RemotingSerializable { - private String clientID; - private Set producerDataSet = new HashSet(); - private Set consumerDataSet = new HashSet(); - - public String getClientID() { - return clientID; - } - - public void setClientID(String clientID) { - this.clientID = clientID; - } - - public Set getProducerDataSet() { - return producerDataSet; - } - - public void setProducerDataSet(Set producerDataSet) { - this.producerDataSet = producerDataSet; - } - - public Set getConsumerDataSet() { - return consumerDataSet; - } - - public void setConsumerDataSet(Set consumerDataSet) { - this.consumerDataSet = consumerDataSet; - } - - @Override - public String toString() { - return "HeartbeatData [clientID=" + clientID + ", producerDataSet=" + producerDataSet - + ", consumerDataSet=" + consumerDataSet + "]"; - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/route/BrokerData.java b/common/src/main/java/org/apache/rocketmq/common/protocol/route/BrokerData.java deleted file mode 100644 index 36599fbc874..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/route/BrokerData.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.common.protocol.route; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Random; -import org.apache.rocketmq.common.MixAll; - -public class BrokerData implements Comparable { - private String cluster; - private String brokerName; - private HashMap brokerAddrs; - - private final Random random = new Random(); - - public BrokerData() { - - } - - public BrokerData(String cluster, String brokerName, HashMap brokerAddrs) { - this.cluster = cluster; - this.brokerName = brokerName; - this.brokerAddrs = brokerAddrs; - } - - /** - * Selects a (preferably master) broker address from the registered list. - * If the master's address cannot be found, a slave broker address is selected in a random manner. - * - * @return Broker address. - */ - public String selectBrokerAddr() { - String addr = this.brokerAddrs.get(MixAll.MASTER_ID); - - if (addr == null) { - List addrs = new ArrayList(brokerAddrs.values()); - return addrs.get(random.nextInt(addrs.size())); - } - - return addr; - } - - public HashMap getBrokerAddrs() { - return brokerAddrs; - } - - public void setBrokerAddrs(HashMap brokerAddrs) { - this.brokerAddrs = brokerAddrs; - } - - public String getCluster() { - return cluster; - } - - public void setCluster(String cluster) { - this.cluster = cluster; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((brokerAddrs == null) ? 0 : brokerAddrs.hashCode()); - result = prime * result + ((brokerName == null) ? 0 : brokerName.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - BrokerData other = (BrokerData) obj; - if (brokerAddrs == null) { - if (other.brokerAddrs != null) - return false; - } else if (!brokerAddrs.equals(other.brokerAddrs)) - return false; - if (brokerName == null) { - if (other.brokerName != null) - return false; - } else if (!brokerName.equals(other.brokerName)) - return false; - return true; - } - - @Override - public String toString() { - return "BrokerData [brokerName=" + brokerName + ", brokerAddrs=" + brokerAddrs + "]"; - } - - @Override - public int compareTo(BrokerData o) { - return this.brokerName.compareTo(o.getBrokerName()); - } - - public String getBrokerName() { - return brokerName; - } - - public void setBrokerName(String brokerName) { - this.brokerName = brokerName; - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/route/TopicRouteData.java b/common/src/main/java/org/apache/rocketmq/common/protocol/route/TopicRouteData.java deleted file mode 100644 index e8f54b8d73e..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/route/TopicRouteData.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * $Id: TopicRouteData.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ - */ -package org.apache.rocketmq.common.protocol.route; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import org.apache.rocketmq.remoting.protocol.RemotingSerializable; - -public class TopicRouteData extends RemotingSerializable { - private String orderTopicConf; - private List queueDatas; - private List brokerDatas; - private HashMap/* Filter Server */> filterServerTable; - - public TopicRouteData cloneTopicRouteData() { - TopicRouteData topicRouteData = new TopicRouteData(); - topicRouteData.setQueueDatas(new ArrayList()); - topicRouteData.setBrokerDatas(new ArrayList()); - topicRouteData.setFilterServerTable(new HashMap>()); - topicRouteData.setOrderTopicConf(this.orderTopicConf); - - if (this.queueDatas != null) { - topicRouteData.getQueueDatas().addAll(this.queueDatas); - } - - if (this.brokerDatas != null) { - topicRouteData.getBrokerDatas().addAll(this.brokerDatas); - } - - if (this.filterServerTable != null) { - topicRouteData.getFilterServerTable().putAll(this.filterServerTable); - } - - return topicRouteData; - } - - public List getQueueDatas() { - return queueDatas; - } - - public void setQueueDatas(List queueDatas) { - this.queueDatas = queueDatas; - } - - public List getBrokerDatas() { - return brokerDatas; - } - - public void setBrokerDatas(List brokerDatas) { - this.brokerDatas = brokerDatas; - } - - public HashMap> getFilterServerTable() { - return filterServerTable; - } - - public void setFilterServerTable(HashMap> filterServerTable) { - this.filterServerTable = filterServerTable; - } - - public String getOrderTopicConf() { - return orderTopicConf; - } - - public void setOrderTopicConf(String orderTopicConf) { - this.orderTopicConf = orderTopicConf; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((brokerDatas == null) ? 0 : brokerDatas.hashCode()); - result = prime * result + ((orderTopicConf == null) ? 0 : orderTopicConf.hashCode()); - result = prime * result + ((queueDatas == null) ? 0 : queueDatas.hashCode()); - result = prime * result + ((filterServerTable == null) ? 0 : filterServerTable.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - TopicRouteData other = (TopicRouteData) obj; - if (brokerDatas == null) { - if (other.brokerDatas != null) - return false; - } else if (!brokerDatas.equals(other.brokerDatas)) - return false; - if (orderTopicConf == null) { - if (other.orderTopicConf != null) - return false; - } else if (!orderTopicConf.equals(other.orderTopicConf)) - return false; - if (queueDatas == null) { - if (other.queueDatas != null) - return false; - } else if (!queueDatas.equals(other.queueDatas)) - return false; - if (filterServerTable == null) { - if (other.filterServerTable != null) - return false; - } else if (!filterServerTable.equals(other.filterServerTable)) - return false; - return true; - } - - @Override - public String toString() { - return "TopicRouteData [orderTopicConf=" + orderTopicConf + ", queueDatas=" + queueDatas - + ", brokerDatas=" + brokerDatas + ", filterServerTable=" + filterServerTable + "]"; - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/queue/ConcurrentTreeMap.java b/common/src/main/java/org/apache/rocketmq/common/queue/ConcurrentTreeMap.java index aab95150438..1df2f96c79b 100644 --- a/common/src/main/java/org/apache/rocketmq/common/queue/ConcurrentTreeMap.java +++ b/common/src/main/java/org/apache/rocketmq/common/queue/ConcurrentTreeMap.java @@ -22,8 +22,8 @@ import java.util.TreeMap; import java.util.concurrent.locks.ReentrantLock; import org.apache.rocketmq.common.constant.LoggerName; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; /** * thread safe @@ -35,8 +35,8 @@ public class ConcurrentTreeMap { private RoundQueue roundQueue; public ConcurrentTreeMap(int capacity, Comparator comparator) { - tree = new TreeMap(comparator); - roundQueue = new RoundQueue(capacity); + tree = new TreeMap<>(comparator); + roundQueue = new RoundQueue<>(capacity); lock = new ReentrantLock(true); } @@ -58,7 +58,7 @@ public V putIfAbsentAndRetExsit(K key, V value) { tree.put(key, value); exsit = value; } - log.warn("putIfAbsentAndRetExsit success. {}", key); + log.warn("putIfAbsentAndRetExsit success. " + key); return exsit; } else { V exsit = tree.get(key); diff --git a/common/src/main/java/org/apache/rocketmq/common/queue/RoundQueue.java b/common/src/main/java/org/apache/rocketmq/common/queue/RoundQueue.java index a2bbe9dcd70..8fc5f68791f 100644 --- a/common/src/main/java/org/apache/rocketmq/common/queue/RoundQueue.java +++ b/common/src/main/java/org/apache/rocketmq/common/queue/RoundQueue.java @@ -30,7 +30,7 @@ public class RoundQueue { public RoundQueue(int capacity) { this.capacity = capacity; - queue = new LinkedList(); + queue = new LinkedList<>(); } public boolean put(E e) { diff --git a/common/src/main/java/org/apache/rocketmq/common/state/StateEventListener.java b/common/src/main/java/org/apache/rocketmq/common/state/StateEventListener.java new file mode 100644 index 00000000000..aed04dc31d1 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/state/StateEventListener.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.state; + +public interface StateEventListener { + void fireEvent(T event); +} diff --git a/common/src/main/java/org/apache/rocketmq/common/statistics/FutureHolder.java b/common/src/main/java/org/apache/rocketmq/common/statistics/FutureHolder.java new file mode 100644 index 00000000000..1fcf0b8bf04 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/statistics/FutureHolder.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.statistics; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; + +public class FutureHolder { + private ConcurrentMap> futureMap = new ConcurrentHashMap<>(8); + + public void addFuture(T t, Future future) { + BlockingQueue list = futureMap.get(t); + if (list == null) { + list = new LinkedBlockingQueue<>(); + BlockingQueue old = futureMap.putIfAbsent(t, list); + if (old != null) { + list = old; + } + } + list.add(future); + } + + public void removeAllFuture(T t) { + cancelAll(t, false); + futureMap.remove(t); + } + + private void cancelAll(T t, boolean mayInterruptIfRunning) { + BlockingQueue list = futureMap.get(t); + if (list != null) { + for (Future future : list) { + future.cancel(mayInterruptIfRunning); + } + } + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/statistics/Interceptor.java b/common/src/main/java/org/apache/rocketmq/common/statistics/Interceptor.java new file mode 100644 index 00000000000..0af55e05910 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/statistics/Interceptor.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.statistics; + +/** + * interceptor + */ +public interface Interceptor { + /** + * increase multiple values + * + * @param deltas + */ + void inc(long... deltas); + + void reset(); +} diff --git a/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsBrief.java b/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsBrief.java new file mode 100644 index 00000000000..970bff98a08 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsBrief.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.statistics; + +import org.apache.commons.lang3.ArrayUtils; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class StatisticsBrief { + public static final int META_RANGE_INDEX = 0; + public static final int META_SLOT_NUM_INDEX = 1; + + // TopPercentile + private long[][] topPercentileMeta; + private AtomicInteger[] counts; + private AtomicLong totalCount; + + // max min avg total + private long max; + private long min; + private long total; + + public StatisticsBrief(long[][] topPercentileMeta) { + if (!isLegalMeta(topPercentileMeta)) { + throw new IllegalArgumentException("illegal topPercentileMeta"); + } + + this.topPercentileMeta = topPercentileMeta; + this.counts = new AtomicInteger[slotNum(topPercentileMeta)]; + this.totalCount = new AtomicLong(0); + reset(); + } + + public void reset() { + for (int i = 0; i < counts.length; i++) { + if (counts[i] == null) { + counts[i] = new AtomicInteger(0); + } else { + counts[i].set(0); + } + } + totalCount.set(0); + + synchronized (this) { + max = 0; + min = Long.MAX_VALUE; + total = 0; + } + } + + private static boolean isLegalMeta(long[][] meta) { + if (ArrayUtils.isEmpty(meta)) { + return false; + } + + for (long[] line : meta) { + if (ArrayUtils.isEmpty(line) || line.length != 2) { + return false; + } + } + + return true; + } + + private static int slotNum(long[][] meta) { + int ret = 1; + for (long[] line : meta) { + ret += line[META_SLOT_NUM_INDEX]; + } + return ret; + } + + public void sample(long value) { + int index = getSlotIndex(value); + counts[index].incrementAndGet(); + totalCount.incrementAndGet(); + + synchronized (this) { + max = Math.max(max, value); + min = Math.min(min, value); + total += value; + } + } + + public long tp999() { + return getTPValue(0.999f); + } + + public long getTPValue(float ratio) { + if (ratio <= 0 || ratio >= 1) { + ratio = 0.99f; + } + long count = totalCount.get(); + long excludes = (long)(count - count * ratio); + if (excludes == 0) { + return getMax(); + } + + int tmp = 0; + for (int i = counts.length - 1; i > 0; i--) { + tmp += counts[i].get(); + if (tmp > excludes) { + return Math.min(getSlotTPValue(i), getMax()); + } + } + return 0; + } + + private long getSlotTPValue(int index) { + int slotNumLeft = index; + for (int i = 0; i < topPercentileMeta.length; i++) { + int slotNum = (int)topPercentileMeta[i][META_SLOT_NUM_INDEX]; + if (slotNumLeft < slotNum) { + long metaRangeMax = topPercentileMeta[i][META_RANGE_INDEX]; + long metaRangeMin = 0; + if (i > 0) { + metaRangeMin = topPercentileMeta[i - 1][META_RANGE_INDEX]; + } + + return metaRangeMin + (metaRangeMax - metaRangeMin) / slotNum * (slotNumLeft + 1); + } else { + slotNumLeft -= slotNum; + } + } + // MAX_VALUE: the last slot + return Integer.MAX_VALUE; + } + + private int getSlotIndex(long num) { + int index = 0; + for (int i = 0; i < topPercentileMeta.length; i++) { + long rangeMax = topPercentileMeta[i][META_RANGE_INDEX]; + int slotNum = (int)topPercentileMeta[i][META_SLOT_NUM_INDEX]; + long rangeMin = (i > 0) ? topPercentileMeta[i - 1][META_RANGE_INDEX] : 0; + if (rangeMin <= num && num < rangeMax) { + index += (num - rangeMin) / ((rangeMax - rangeMin) / slotNum); + break; + } + + index += slotNum; + } + return index; + } + + /** + * Getters + * + * @return + */ + public long getMax() { + return max; + } + + public long getMin() { + return totalCount.get() > 0 ? min : 0; + } + + public long getTotal() { + return total; + } + + public long getCnt() { + return totalCount.get(); + } + + public double getAvg() { + return totalCount.get() != 0 ? ((double)total) / totalCount.get() : 0; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsBriefInterceptor.java b/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsBriefInterceptor.java new file mode 100644 index 00000000000..b0b69378442 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsBriefInterceptor.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.statistics; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.tuple.Pair; + +/** + * interceptor to generate statistics brief + */ +public class StatisticsBriefInterceptor implements Interceptor { + private int[] indexOfItems; + + private StatisticsBrief[] statisticsBriefs; + + public StatisticsBriefInterceptor(StatisticsItem item, Pair[] briefMetas) { + indexOfItems = new int[briefMetas.length]; + statisticsBriefs = new StatisticsBrief[briefMetas.length]; + for (int i = 0; i < briefMetas.length; i++) { + String name = briefMetas[i].getKey(); + int index = ArrayUtils.indexOf(item.getItemNames(), name); + if (index < 0) { + throw new IllegalArgumentException("illegal breifItemName: " + name); + } + indexOfItems[i] = index; + statisticsBriefs[i] = new StatisticsBrief(briefMetas[i].getValue()); + } + } + + @Override + public void inc(long... itemValues) { + for (int i = 0; i < indexOfItems.length; i++) { + int indexOfItem = indexOfItems[i]; + if (indexOfItem < itemValues.length) { + statisticsBriefs[i].sample(itemValues[indexOfItem]); + } + } + } + + @Override + public void reset() { + for (StatisticsBrief brief : statisticsBriefs) { + brief.reset(); + } + } + + public int[] getIndexOfItems() { + return indexOfItems; + } + + public void setIndexOfItems(int[] indexOfItems) { + this.indexOfItems = indexOfItems; + } + + public StatisticsBrief[] getStatisticsBriefs() { + return statisticsBriefs; + } + + public void setStatisticsBriefs(StatisticsBrief[] statisticsBriefs) { + this.statisticsBriefs = statisticsBriefs; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsItem.java b/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsItem.java new file mode 100644 index 00000000000..23633dc5bf1 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsItem.java @@ -0,0 +1,175 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.statistics; + +import java.security.InvalidParameterException; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.commons.lang3.ArrayUtils; + +/** + * Statistics Item + */ +public class StatisticsItem { + private String statKind; + private String statObject; + + private String[] itemNames; + private AtomicLong[] itemAccumulates; + private AtomicLong invokeTimes; + + private Interceptor interceptor; + + /** + * last timestamp when the item was updated + */ + private AtomicLong lastTimeStamp; + + public StatisticsItem(String statKind, String statObject, String... itemNames) { + if (itemNames == null || itemNames.length <= 0) { + throw new InvalidParameterException("StatisticsItem \"itemNames\" is empty"); + } + + this.statKind = statKind; + this.statObject = statObject; + this.itemNames = itemNames; + + AtomicLong[] accs = new AtomicLong[itemNames.length]; + for (int i = 0; i < itemNames.length; i++) { + accs[i] = new AtomicLong(0); + } + + this.itemAccumulates = accs; + this.invokeTimes = new AtomicLong(); + this.lastTimeStamp = new AtomicLong(System.currentTimeMillis()); + } + + public void incItems(long... itemIncs) { + int len = Math.min(itemIncs.length, itemAccumulates.length); + for (int i = 0; i < len; i++) { + itemAccumulates[i].addAndGet(itemIncs[i]); + } + + invokeTimes.addAndGet(1); + lastTimeStamp.set(System.currentTimeMillis()); + + if (interceptor != null) { + interceptor.inc(itemIncs); + } + } + + public boolean allZeros() { + if (invokeTimes.get() == 0) { + return true; + } + + for (AtomicLong acc : itemAccumulates) { + if (acc.get() != 0) { + return false; + } + } + return true; + } + + public String getStatKind() { + return statKind; + } + + public String getStatObject() { + return statObject; + } + + public String[] getItemNames() { + return itemNames; + } + + public AtomicLong[] getItemAccumulates() { + return itemAccumulates; + } + + public AtomicLong getInvokeTimes() { + return invokeTimes; + } + + public AtomicLong getLastTimeStamp() { + return lastTimeStamp; + } + + public AtomicLong getItemAccumulate(String itemName) { + int index = ArrayUtils.indexOf(itemNames, itemName); + if (index < 0) { + return new AtomicLong(0); + } + return itemAccumulates[index]; + } + + /** + * get snapshot + *

+ * Warning: no guarantee of itemAccumulates consistency + * + * @return + */ + public StatisticsItem snapshot() { + StatisticsItem ret = new StatisticsItem(statKind, statObject, itemNames); + + ret.itemAccumulates = new AtomicLong[itemAccumulates.length]; + for (int i = 0; i < itemAccumulates.length; i++) { + ret.itemAccumulates[i] = new AtomicLong(itemAccumulates[i].get()); + } + + ret.invokeTimes = new AtomicLong(invokeTimes.longValue()); + ret.lastTimeStamp = new AtomicLong(lastTimeStamp.longValue()); + + return ret; + } + + /** + * subtract another StatisticsItem + * + * @param item + * @return + */ + public StatisticsItem subtract(StatisticsItem item) { + if (item == null) { + return snapshot(); + } + + if (!statKind.equals(item.statKind) || !statObject.equals(item.statObject) || !Arrays.equals(itemNames, + item.itemNames)) { + throw new IllegalArgumentException("StatisticsItem's kind, key and itemNames must be exactly the same"); + } + + StatisticsItem ret = new StatisticsItem(statKind, statObject, itemNames); + ret.invokeTimes = new AtomicLong(invokeTimes.get() - item.invokeTimes.get()); + ret.itemAccumulates = new AtomicLong[itemAccumulates.length]; + for (int i = 0; i < itemAccumulates.length; i++) { + ret.itemAccumulates[i] = new AtomicLong(itemAccumulates[i].get() - item.itemAccumulates[i].get()); + } + return ret; + } + + public Interceptor getInterceptor() { + return interceptor; + } + + public void setInterceptor(Interceptor interceptor) { + this.interceptor = interceptor; + } + +} diff --git a/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsItemFormatter.java b/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsItemFormatter.java new file mode 100644 index 00000000000..f3a9bdb7205 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsItemFormatter.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.statistics; + +import java.util.concurrent.atomic.AtomicLong; + +public class StatisticsItemFormatter { + public String format(StatisticsItem statItem) { + final String separator = "|"; + StringBuilder sb = new StringBuilder(); + sb.append(statItem.getStatKind()).append(separator); + sb.append(statItem.getStatObject()).append(separator); + for (AtomicLong acc : statItem.getItemAccumulates()) { + sb.append(acc.get()).append(separator); + } + sb.append(statItem.getInvokeTimes()); + return sb.toString(); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsItemPrinter.java b/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsItemPrinter.java new file mode 100644 index 00000000000..1f46590a10d --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsItemPrinter.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.statistics; + +import org.apache.rocketmq.logging.org.slf4j.Logger; + +public class StatisticsItemPrinter { + private Logger log; + + private StatisticsItemFormatter formatter; + + public StatisticsItemPrinter(StatisticsItemFormatter formatter, Logger log) { + this.formatter = formatter; + this.log = log; + } + + public void log(Logger log) { + this.log = log; + } + + public void formatter(StatisticsItemFormatter formatter) { + this.formatter = formatter; + } + + public void print(String prefix, StatisticsItem statItem, String... suffixs) { + StringBuilder suffix = new StringBuilder(); + for (String str : suffixs) { + suffix.append(str); + } + + if (log != null) { + log.info("{}{}{}", prefix, formatter.format(statItem), suffix.toString()); + } + // System.out.printf("%s %s%s%s\n", new Date().toString(), prefix, formatter.format(statItem), suffix.toString()); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsItemScheduledIncrementPrinter.java b/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsItemScheduledIncrementPrinter.java new file mode 100644 index 00000000000..2890e6e15cd --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsItemScheduledIncrementPrinter.java @@ -0,0 +1,290 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.statistics; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class StatisticsItemScheduledIncrementPrinter extends StatisticsItemScheduledPrinter { + + private String[] tpsItemNames; + + public static final int TPS_INITIAL_DELAY = 0; + public static final int TPS_INTREVAL = 1000; + public static final String SEPARATOR = "|"; + + /** + * last snapshots of all scheduled items + */ + private final ConcurrentHashMap> lastItemSnapshots + = new ConcurrentHashMap<>(); + + private final ConcurrentHashMap> sampleBriefs + = new ConcurrentHashMap<>(); + + public StatisticsItemScheduledIncrementPrinter(String name, StatisticsItemPrinter printer, + ScheduledExecutorService executor, InitialDelay initialDelay, + long interval, String[] tpsItemNames, Valve valve) { + super(name, printer, executor, initialDelay, interval, valve); + this.tpsItemNames = tpsItemNames; + } + + /** + * schedule a StatisticsItem to print the Increments periodically + */ + @Override + public void schedule(final StatisticsItem item) { + setItemSampleBrief(item.getStatKind(), item.getStatObject(), new StatisticsItemSampleBrief(item, tpsItemNames)); + + // print log every ${interval} miliseconds + ScheduledFuture future = executor.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + if (!enabled()) { + return; + } + + StatisticsItem snapshot = item.snapshot(); + StatisticsItem lastSnapshot = getItemSnapshot(lastItemSnapshots, item.getStatKind(), + item.getStatObject()); + StatisticsItem increment = snapshot.subtract(lastSnapshot); + + Interceptor inteceptor = item.getInterceptor(); + String inteceptorStr = formatInterceptor(inteceptor); + if (inteceptor != null) { + inteceptor.reset(); + } + + StatisticsItemSampleBrief brief = getSampleBrief(item.getStatKind(), item.getStatObject()); + if (brief != null && (!increment.allZeros() || printZeroLine())) { + printer.print(name, increment, inteceptorStr, brief.toString()); + } + + setItemSnapshot(lastItemSnapshots, snapshot); + + if (brief != null) { + brief.reset(); + } + } + }, getInitialDelay(), interval, TimeUnit.MILLISECONDS); + addFuture(item, future); + + // sample every TPS_INTREVAL + ScheduledFuture futureSample = executor.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + if (!enabled()) { + return; + } + + StatisticsItem snapshot = item.snapshot(); + StatisticsItemSampleBrief brief = getSampleBrief(item.getStatKind(), item.getStatObject()); + if (brief != null) { + brief.sample(snapshot); + } + } + }, TPS_INTREVAL, TPS_INTREVAL, TimeUnit.MILLISECONDS); + addFuture(item, futureSample); + } + + @Override + public void remove(StatisticsItem item) { + // remove task + removeAllFuture(item); + + String kind = item.getStatKind(); + String key = item.getStatObject(); + + ConcurrentHashMap lastItemMap = lastItemSnapshots.get(kind); + if (lastItemMap != null) { + lastItemMap.remove(key); + } + + ConcurrentHashMap briefMap = sampleBriefs.get(kind); + if (briefMap != null) { + briefMap.remove(key); + } + } + + private StatisticsItem getItemSnapshot( + ConcurrentHashMap> snapshots, + String kind, String key) { + ConcurrentHashMap itemMap = snapshots.get(kind); + return (itemMap != null) ? itemMap.get(key) : null; + } + + private StatisticsItemSampleBrief getSampleBrief(String kind, String key) { + ConcurrentHashMap itemMap = sampleBriefs.get(kind); + return (itemMap != null) ? itemMap.get(key) : null; + } + + private void setItemSnapshot(ConcurrentHashMap> snapshots, + StatisticsItem item) { + String kind = item.getStatKind(); + String key = item.getStatObject(); + ConcurrentHashMap itemMap = snapshots.get(kind); + if (itemMap == null) { + itemMap = new ConcurrentHashMap<>(); + ConcurrentHashMap oldItemMap = snapshots.putIfAbsent(kind, itemMap); + if (oldItemMap != null) { + itemMap = oldItemMap; + } + } + + itemMap.put(key, item); + } + + private void setItemSampleBrief(String kind, String key, + StatisticsItemSampleBrief brief) { + ConcurrentHashMap itemMap = sampleBriefs.get(kind); + if (itemMap == null) { + itemMap = new ConcurrentHashMap<>(); + ConcurrentHashMap oldItemMap = sampleBriefs.putIfAbsent(kind, itemMap); + if (oldItemMap != null) { + itemMap = oldItemMap; + } + } + + itemMap.put(key, brief); + } + + private String formatInterceptor(Interceptor interceptor) { + if (interceptor == null) { + return ""; + } + + if (interceptor instanceof StatisticsBriefInterceptor) { + StringBuilder sb = new StringBuilder(); + StatisticsBriefInterceptor briefInterceptor = (StatisticsBriefInterceptor)interceptor; + for (StatisticsBrief brief : briefInterceptor.getStatisticsBriefs()) { + long max = brief.getMax(); + long tp999 = Math.min(brief.tp999(), max); + //sb.append(SEPARATOR).append(brief.getTotal()); + sb.append(SEPARATOR).append(max); + //sb.append(SEPARATOR).append(brief.getMin()); + sb.append(SEPARATOR).append(String.format("%.2f", brief.getAvg())); + sb.append(SEPARATOR).append(tp999); + } + return sb.toString(); + } + return ""; + } + + public static class StatisticsItemSampleBrief { + private StatisticsItem lastSnapshot; + + public String[] itemNames; + public ItemSampleBrief[] briefs; + + public StatisticsItemSampleBrief(StatisticsItem statItem, String[] itemNames) { + this.lastSnapshot = statItem.snapshot(); + this.itemNames = itemNames; + this.briefs = new ItemSampleBrief[itemNames.length]; + for (int i = 0; i < itemNames.length; i++) { + this.briefs[i] = new ItemSampleBrief(); + } + } + + public synchronized void reset() { + for (ItemSampleBrief brief : briefs) { + brief.reset(); + } + } + + public synchronized void sample(StatisticsItem snapshot) { + if (snapshot == null) { + return; + } + + for (int i = 0; i < itemNames.length; i++) { + String name = itemNames[i]; + + long lastValue = lastSnapshot != null ? lastSnapshot.getItemAccumulate(name).get() : 0; + long increment = snapshot.getItemAccumulate(name).get() - lastValue; + briefs[i].sample(increment); + } + lastSnapshot = snapshot; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < briefs.length; i++) { + ItemSampleBrief brief = briefs[i]; + sb.append(SEPARATOR).append(brief.getMax()); + //sb.append(SEPARATOR).append(brief.getMin()); + sb.append(SEPARATOR).append(String.format("%.2f", brief.getAvg())); + } + return sb.toString(); + } + } + + /** + * sample brief of a item for a period of time + */ + public static class ItemSampleBrief { + private long max; + private long min; + private long total; + private long cnt; + + public ItemSampleBrief() { + reset(); + } + + public void sample(long value) { + max = Math.max(max, value); + min = Math.min(min, value); + total += value; + cnt++; + } + + public void reset() { + max = 0; + min = Long.MAX_VALUE; + total = 0; + cnt = 0; + } + + /** + * Getters + * + * @return + */ + public long getMax() { + return max; + } + + public long getMin() { + return cnt > 0 ? min : 0; + } + + public long getTotal() { + return total; + } + + public long getCnt() { + return cnt; + } + + public double getAvg() { + return cnt != 0 ? ((double)total) / cnt : 0; + } + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsItemScheduledPrinter.java b/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsItemScheduledPrinter.java new file mode 100644 index 00000000000..799c02ccb26 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsItemScheduledPrinter.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.statistics; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class StatisticsItemScheduledPrinter extends FutureHolder { + protected String name; + + protected StatisticsItemPrinter printer; + protected ScheduledExecutorService executor; + protected long interval; + protected InitialDelay initialDelay; + protected Valve valve; + + public StatisticsItemScheduledPrinter(String name, StatisticsItemPrinter printer, + ScheduledExecutorService executor, InitialDelay initialDelay, + long interval, Valve valve) { + this.name = name; + this.printer = printer; + this.executor = executor; + this.initialDelay = initialDelay; + this.interval = interval; + this.valve = valve; + } + + /** + * schedule a StatisticsItem to print all the values periodically + */ + public void schedule(final StatisticsItem statisticsItem) { + ScheduledFuture future = executor.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + if (enabled()) { + printer.print(name, statisticsItem); + } + } + }, getInitialDelay(), interval, TimeUnit.MILLISECONDS); + + addFuture(statisticsItem, future); + } + + public void remove(final StatisticsItem statisticsItem) { + removeAllFuture(statisticsItem); + } + + public interface InitialDelay { + /** + * Get initial delay value + * @return + */ + long get(); + } + + public interface Valve { + /** + * whether enabled + * @return + */ + boolean enabled(); + + /** + * whether print zero lines + * @return + */ + boolean printZeroLine(); + } + + protected long getInitialDelay() { + return initialDelay != null ? initialDelay.get() : 0; + } + + protected boolean enabled() { + return valve != null ? valve.enabled() : false; + } + + protected boolean printZeroLine() { + return valve != null ? valve.printZeroLine() : false; + } + +} diff --git a/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsItemStateGetter.java b/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsItemStateGetter.java new file mode 100644 index 00000000000..3b16d00e12b --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsItemStateGetter.java @@ -0,0 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.statistics; + +public interface StatisticsItemStateGetter { + boolean online(StatisticsItem item); +} diff --git a/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsKindMeta.java b/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsKindMeta.java new file mode 100644 index 00000000000..27bee19b21b --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsKindMeta.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.statistics; + +/** + * Statistics Kind Metadata + */ +public class StatisticsKindMeta { + private String name; + private String[] itemNames; + private StatisticsItemScheduledPrinter scheduledPrinter; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String[] getItemNames() { + return itemNames; + } + + public void setItemNames(String[] itemNames) { + this.itemNames = itemNames; + } + + public StatisticsItemScheduledPrinter getScheduledPrinter() { + return scheduledPrinter; + } + + public void setScheduledPrinter(StatisticsItemScheduledPrinter scheduledPrinter) { + this.scheduledPrinter = scheduledPrinter; + } +} \ No newline at end of file diff --git a/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsManager.java b/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsManager.java new file mode 100644 index 00000000000..8d6bdb73a5e --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/statistics/StatisticsManager.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.statistics; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.lang3.tuple.Pair; +import org.apache.rocketmq.common.utils.ThreadUtils; + +public class StatisticsManager { + + /** + * Set of Statistics Kind Metadata + */ + private Map kindMetaMap; + + /** + * item names to calculate statistics brief + */ + private Pair[] briefMetas; + + /** + * Statistics + */ + private final ConcurrentHashMap> statsTable + = new ConcurrentHashMap<>(); + + private static final int MAX_IDLE_TIME = 10 * 60 * 1000; + private final ScheduledExecutorService executor = ThreadUtils.newSingleThreadScheduledExecutor( + "StatisticsManagerCleaner", true); + + private StatisticsItemStateGetter statisticsItemStateGetter; + + public StatisticsManager() { + kindMetaMap = new HashMap<>(); + start(); + } + + public StatisticsManager(Map kindMeta) { + this.kindMetaMap = kindMeta; + start(); + } + + public void addStatisticsKindMeta(StatisticsKindMeta kindMeta) { + kindMetaMap.put(kindMeta.getName(), kindMeta); + statsTable.putIfAbsent(kindMeta.getName(), new ConcurrentHashMap<>(16)); + } + + public void setBriefMeta(Pair[] briefMetas) { + this.briefMetas = briefMetas; + } + + private void start() { + int maxIdleTime = MAX_IDLE_TIME; + executor.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + Iterator>> iter + = statsTable.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry> entry = iter.next(); + String kind = entry.getKey(); + ConcurrentHashMap itemMap = entry.getValue(); + + if (itemMap == null || itemMap.isEmpty()) { + continue; + } + + HashMap tmpItemMap = new HashMap<>(itemMap); + for (StatisticsItem item : tmpItemMap.values()) { + // remove when expired + if (System.currentTimeMillis() - item.getLastTimeStamp().get() > MAX_IDLE_TIME + && (statisticsItemStateGetter == null || !statisticsItemStateGetter.online(item))) { + remove(item); + } + } + } + } + }, maxIdleTime, maxIdleTime / 3, TimeUnit.MILLISECONDS); + } + + /** + * Increment a StatisticsItem + * + * @param kind + * @param key + * @param itemAccumulates + */ + public boolean inc(String kind, String key, long... itemAccumulates) { + ConcurrentHashMap itemMap = statsTable.get(kind); + if (itemMap != null) { + StatisticsItem item = itemMap.get(key); + + // if not exist, create and schedule + if (item == null) { + item = new StatisticsItem(kind, key, kindMetaMap.get(kind).getItemNames()); + item.setInterceptor(new StatisticsBriefInterceptor(item, briefMetas)); + StatisticsItem oldItem = itemMap.putIfAbsent(key, item); + if (oldItem != null) { + item = oldItem; + } else { + scheduleStatisticsItem(item); + } + } + + // do increment + item.incItems(itemAccumulates); + + return true; + } + + return false; + } + + private void scheduleStatisticsItem(StatisticsItem item) { + kindMetaMap.get(item.getStatKind()).getScheduledPrinter().schedule(item); + } + + public void remove(StatisticsItem item) { + ConcurrentHashMap itemMap = statsTable.get(item.getStatKind()); + if (itemMap != null) { + itemMap.remove(item.getStatObject(), item); + } + + StatisticsKindMeta kindMeta = kindMetaMap.get(item.getStatKind()); + if (kindMeta != null && kindMeta.getScheduledPrinter() != null) { + kindMeta.getScheduledPrinter().remove(item); + } + } + + public StatisticsItemStateGetter getStatisticsItemStateGetter() { + return statisticsItemStateGetter; + } + + public void setStatisticsItemStateGetter(StatisticsItemStateGetter statisticsItemStateGetter) { + this.statisticsItemStateGetter = statisticsItemStateGetter; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/stats/MomentStatsItem.java b/common/src/main/java/org/apache/rocketmq/common/stats/MomentStatsItem.java index 5f3229b38ba..d38281bf83a 100644 --- a/common/src/main/java/org/apache/rocketmq/common/stats/MomentStatsItem.java +++ b/common/src/main/java/org/apache/rocketmq/common/stats/MomentStatsItem.java @@ -21,7 +21,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import org.apache.rocketmq.common.UtilAll; -import org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.Logger; public class MomentStatsItem { @@ -51,7 +51,7 @@ public void run() { } catch (Throwable e) { } } - }, Math.abs(UtilAll.computNextMinutesTimeMillis() - System.currentTimeMillis()), 1000 * 60 * 5, TimeUnit.MILLISECONDS); + }, Math.abs(UtilAll.computeNextMinutesTimeMillis() - System.currentTimeMillis()), 1000 * 60 * 5, TimeUnit.MILLISECONDS); } public void printAtMinutes() { diff --git a/common/src/main/java/org/apache/rocketmq/common/stats/MomentStatsItemSet.java b/common/src/main/java/org/apache/rocketmq/common/stats/MomentStatsItemSet.java index 57dfc386f4f..a4571d7b8ad 100644 --- a/common/src/main/java/org/apache/rocketmq/common/stats/MomentStatsItemSet.java +++ b/common/src/main/java/org/apache/rocketmq/common/stats/MomentStatsItemSet.java @@ -24,11 +24,11 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.apache.rocketmq.common.UtilAll; -import org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.Logger; public class MomentStatsItemSet { private final ConcurrentMap statsItemTable = - new ConcurrentHashMap(128); + new ConcurrentHashMap<>(128); private final String statsName; private final ScheduledExecutorService scheduledExecutorService; private final Logger log; @@ -58,7 +58,7 @@ public void run() { } catch (Throwable ignored) { } } - }, Math.abs(UtilAll.computNextMinutesTimeMillis() - System.currentTimeMillis()), 1000 * 60 * 5, TimeUnit.MILLISECONDS); + }, Math.abs(UtilAll.computeNextMinutesTimeMillis() - System.currentTimeMillis()), 1000 * 60 * 5, TimeUnit.MILLISECONDS); } private void printAtMinutes() { @@ -74,15 +74,35 @@ public void setValue(final String statsKey, final int value) { statsItem.getValue().set(value); } + public void delValueByInfixKey(final String statsKey, String separator) { + Iterator> it = this.statsItemTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + if (next.getKey().contains(separator + statsKey + separator)) { + it.remove(); + } + } + } + + public void delValueBySuffixKey(final String statsKey, String separator) { + Iterator> it = this.statsItemTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + if (next.getKey().endsWith(separator + statsKey)) { + it.remove(); + } + } + } + public MomentStatsItem getAndCreateStatsItem(final String statsKey) { MomentStatsItem statsItem = this.statsItemTable.get(statsKey); if (null == statsItem) { statsItem = new MomentStatsItem(this.statsName, statsKey, this.scheduledExecutorService, this.log); - MomentStatsItem prev = this.statsItemTable.put(statsKey, statsItem); - - if (null == prev) { + MomentStatsItem prev = this.statsItemTable.putIfAbsent(statsKey, statsItem); + if (null != prev) { + statsItem = prev; // statsItem.init(); } } diff --git a/common/src/main/java/org/apache/rocketmq/common/stats/RTStatsItem.java b/common/src/main/java/org/apache/rocketmq/common/stats/RTStatsItem.java new file mode 100644 index 00000000000..b3317cf0b49 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/stats/RTStatsItem.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.stats; + +import java.util.concurrent.ScheduledExecutorService; +import org.apache.rocketmq.logging.org.slf4j.Logger; + +/** + * A StatItem for response time, the only difference between from StatsItem is it has a different log output. + */ +public class RTStatsItem extends StatsItem { + + public RTStatsItem(String statsName, String statsKey, ScheduledExecutorService scheduledExecutorService, + Logger logger) { + super(statsName, statsKey, scheduledExecutorService, logger); + } + + /** + * For Response Time stat Item, the print detail should be a little different, TPS and SUM makes no sense. + * And we give a name "AVGRT" rather than AVGPT for value getAvgpt() + */ + @Override + protected String statPrintDetail(StatsSnapshot ss) { + return String.format("TIMES: %d AVGRT: %.2f", ss.getTimes(), ss.getAvgpt()); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/stats/Stats.java b/common/src/main/java/org/apache/rocketmq/common/stats/Stats.java new file mode 100644 index 00000000000..b70f96e412e --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/stats/Stats.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.stats; + +public class Stats { + + public static final String QUEUE_PUT_NUMS = "QUEUE_PUT_NUMS"; + public static final String QUEUE_PUT_SIZE = "QUEUE_PUT_SIZE"; + public static final String QUEUE_GET_NUMS = "QUEUE_GET_NUMS"; + public static final String QUEUE_GET_SIZE = "QUEUE_GET_SIZE"; + public static final String TOPIC_PUT_NUMS = "TOPIC_PUT_NUMS"; + public static final String TOPIC_PUT_SIZE = "TOPIC_PUT_SIZE"; + public static final String GROUP_GET_NUMS = "GROUP_GET_NUMS"; + public static final String GROUP_GET_SIZE = "GROUP_GET_SIZE"; + public static final String SNDBCK_PUT_NUMS = "SNDBCK_PUT_NUMS"; + public static final String BROKER_PUT_NUMS = "BROKER_PUT_NUMS"; + public static final String BROKER_GET_NUMS = "BROKER_GET_NUMS"; + public static final String GROUP_GET_FROM_DISK_NUMS = "GROUP_GET_FROM_DISK_NUMS"; + public static final String GROUP_GET_FROM_DISK_SIZE = "GROUP_GET_FROM_DISK_SIZE"; + public static final String BROKER_GET_FROM_DISK_NUMS = "BROKER_GET_FROM_DISK_NUMS"; + public static final String BROKER_GET_FROM_DISK_SIZE = "BROKER_GET_FROM_DISK_SIZE"; + public static final String COMMERCIAL_SEND_TIMES = "COMMERCIAL_SEND_TIMES"; + public static final String COMMERCIAL_SNDBCK_TIMES = "COMMERCIAL_SNDBCK_TIMES"; + public static final String COMMERCIAL_RCV_TIMES = "COMMERCIAL_RCV_TIMES"; + public static final String COMMERCIAL_RCV_EPOLLS = "COMMERCIAL_RCV_EPOLLS"; + public static final String COMMERCIAL_SEND_SIZE = "COMMERCIAL_SEND_SIZE"; + public static final String COMMERCIAL_RCV_SIZE = "COMMERCIAL_RCV_SIZE"; + public static final String COMMERCIAL_PERM_FAILURES = "COMMERCIAL_PERM_FAILURES"; + + public static final String GROUP_GET_FALL_SIZE = "GROUP_GET_FALL_SIZE"; + public static final String GROUP_GET_FALL_TIME = "GROUP_GET_FALL_TIME"; + public static final String GROUP_GET_LATENCY = "GROUP_GET_LATENCY"; +} diff --git a/common/src/main/java/org/apache/rocketmq/common/stats/StatsItem.java b/common/src/main/java/org/apache/rocketmq/common/stats/StatsItem.java index 9b37f800d90..8307c20aa68 100644 --- a/common/src/main/java/org/apache/rocketmq/common/stats/StatsItem.java +++ b/common/src/main/java/org/apache/rocketmq/common/stats/StatsItem.java @@ -20,33 +20,33 @@ import java.util.LinkedList; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.LongAdder; + import org.apache.rocketmq.common.UtilAll; -import org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.Logger; public class StatsItem { + private final LongAdder value = new LongAdder(); - private final AtomicLong value = new AtomicLong(0); - - private final AtomicLong times = new AtomicLong(0); + private final LongAdder times = new LongAdder(); - private final LinkedList csListMinute = new LinkedList(); + private final LinkedList csListMinute = new LinkedList<>(); - private final LinkedList csListHour = new LinkedList(); + private final LinkedList csListHour = new LinkedList<>(); - private final LinkedList csListDay = new LinkedList(); + private final LinkedList csListDay = new LinkedList<>(); private final String statsName; private final String statsKey; private final ScheduledExecutorService scheduledExecutorService; - private final Logger log; - public StatsItem(String statsName, String statsKey, ScheduledExecutorService scheduledExecutorService, - Logger log) { + private final Logger logger; + + public StatsItem(String statsName, String statsKey, ScheduledExecutorService scheduledExecutorService, Logger logger) { this.statsName = statsName; this.statsKey = statsKey; this.scheduledExecutorService = scheduledExecutorService; - this.log = log; + this.logger = logger; } private static StatsSnapshot computeStatsData(final LinkedList csList) { @@ -55,13 +55,14 @@ private static StatsSnapshot computeStatsData(final LinkedList csL double tps = 0; double avgpt = 0; long sum = 0; + long timesDiff = 0; if (!csList.isEmpty()) { CallSnapshot first = csList.getFirst(); CallSnapshot last = csList.getLast(); sum = last.getValue() - first.getValue(); tps = (sum * 1000.0d) / (last.getTimestamp() - first.getTimestamp()); - long timesDiff = last.getTimes() - first.getTimes(); + timesDiff = last.getTimes() - first.getTimes(); if (timesDiff > 0) { avgpt = (sum * 1.0d) / timesDiff; } @@ -70,6 +71,7 @@ private static StatsSnapshot computeStatsData(final LinkedList csL statsSnapshot.setSum(sum); statsSnapshot.setTps(tps); statsSnapshot.setAvgpt(avgpt); + statsSnapshot.setTimes(timesDiff); } return statsSnapshot; @@ -127,7 +129,7 @@ public void run() { } catch (Throwable ignored) { } } - }, Math.abs(UtilAll.computNextMinutesTimeMillis() - System.currentTimeMillis()), 1000 * 60, TimeUnit.MILLISECONDS); + }, Math.abs(UtilAll.computeNextMinutesTimeMillis() - System.currentTimeMillis()), 1000 * 60, TimeUnit.MILLISECONDS); this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { @Override @@ -137,7 +139,7 @@ public void run() { } catch (Throwable ignored) { } } - }, Math.abs(UtilAll.computNextHourTimeMillis() - System.currentTimeMillis()), 1000 * 60 * 60, TimeUnit.MILLISECONDS); + }, Math.abs(UtilAll.computeNextHourTimeMillis() - System.currentTimeMillis()), 1000 * 60 * 60, TimeUnit.MILLISECONDS); this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { @Override @@ -147,13 +149,16 @@ public void run() { } catch (Throwable ignored) { } } - }, Math.abs(UtilAll.computNextMorningTimeMillis() - System.currentTimeMillis()) - 2000, 1000 * 60 * 60 * 24, TimeUnit.MILLISECONDS); + }, Math.abs(UtilAll.computeNextMorningTimeMillis() - System.currentTimeMillis()) - 2000, 1000 * 60 * 60 * 24, TimeUnit.MILLISECONDS); } public void samplingInSeconds() { synchronized (this.csListMinute) { - this.csListMinute.add(new CallSnapshot(System.currentTimeMillis(), this.times.get(), this.value - .get())); + if (this.csListMinute.size() == 0) { + this.csListMinute.add(new CallSnapshot(System.currentTimeMillis() - 10 * 1000, 0, 0)); + } + this.csListMinute.add(new CallSnapshot(System.currentTimeMillis(), this.times.sum(), this.value + .sum())); if (this.csListMinute.size() > 7) { this.csListMinute.removeFirst(); } @@ -162,8 +167,11 @@ public void samplingInSeconds() { public void samplingInMinutes() { synchronized (this.csListHour) { - this.csListHour.add(new CallSnapshot(System.currentTimeMillis(), this.times.get(), this.value - .get())); + if (this.csListHour.size() == 0) { + this.csListHour.add(new CallSnapshot(System.currentTimeMillis() - 10 * 60 * 1000, 0, 0)); + } + this.csListHour.add(new CallSnapshot(System.currentTimeMillis(), this.times.sum(), this.value + .sum())); if (this.csListHour.size() > 7) { this.csListHour.removeFirst(); } @@ -172,8 +180,11 @@ public void samplingInMinutes() { public void samplingInHour() { synchronized (this.csListDay) { - this.csListDay.add(new CallSnapshot(System.currentTimeMillis(), this.times.get(), this.value - .get())); + if (this.csListDay.size() == 0) { + this.csListDay.add(new CallSnapshot(System.currentTimeMillis() - 1 * 60 * 60 * 1000, 0, 0)); + } + this.csListDay.add(new CallSnapshot(System.currentTimeMillis(), this.times.sum(), this.value + .sum())); if (this.csListDay.size() > 25) { this.csListDay.removeFirst(); } @@ -182,35 +193,28 @@ public void samplingInHour() { public void printAtMinutes() { StatsSnapshot ss = computeStatsData(this.csListMinute); - log.info(String.format("[%s] [%s] Stats In One Minute, SUM: %d TPS: %.2f AVGPT: %.2f", - this.statsName, - this.statsKey, - ss.getSum(), - ss.getTps(), - ss.getAvgpt())); + logger.info(String.format("[%s] [%s] Stats In One Minute, ", this.statsName, this.statsKey) + statPrintDetail(ss)); } public void printAtHour() { StatsSnapshot ss = computeStatsData(this.csListHour); - log.info(String.format("[%s] [%s] Stats In One Hour, SUM: %d TPS: %.2f AVGPT: %.2f", - this.statsName, - this.statsKey, - ss.getSum(), - ss.getTps(), - ss.getAvgpt())); + logger.info(String.format("[%s] [%s] Stats In One Hour, ", this.statsName, this.statsKey) + statPrintDetail(ss)); + } public void printAtDay() { StatsSnapshot ss = computeStatsData(this.csListDay); - log.info(String.format("[%s] [%s] Stats In One Day, SUM: %d TPS: %.2f AVGPT: %.2f", - this.statsName, - this.statsKey, - ss.getSum(), - ss.getTps(), - ss.getAvgpt())); + logger.info(String.format("[%s] [%s] Stats In One Day, ", this.statsName, this.statsKey) + statPrintDetail(ss)); + } + + protected String statPrintDetail(StatsSnapshot ss) { + return String.format("SUM: %d TPS: %.2f AVGPT: %.2f", + ss.getSum(), + ss.getTps(), + ss.getAvgpt()); } - public AtomicLong getValue() { + public LongAdder getValue() { return value; } @@ -222,7 +226,7 @@ public String getStatsName() { return statsName; } - public AtomicLong getTimes() { + public LongAdder getTimes() { return times; } } diff --git a/common/src/main/java/org/apache/rocketmq/common/stats/StatsItemSet.java b/common/src/main/java/org/apache/rocketmq/common/stats/StatsItemSet.java index 17dbf0d2a83..c5b140b5cc1 100644 --- a/common/src/main/java/org/apache/rocketmq/common/stats/StatsItemSet.java +++ b/common/src/main/java/org/apache/rocketmq/common/stats/StatsItemSet.java @@ -24,20 +24,21 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.apache.rocketmq.common.UtilAll; -import org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.Logger; public class StatsItemSet { private final ConcurrentMap statsItemTable = - new ConcurrentHashMap(128); + new ConcurrentHashMap<>(128); private final String statsName; private final ScheduledExecutorService scheduledExecutorService; - private final Logger log; - public StatsItemSet(String statsName, ScheduledExecutorService scheduledExecutorService, Logger log) { + private final Logger logger; + + public StatsItemSet(String statsName, ScheduledExecutorService scheduledExecutorService, Logger logger) { + this.logger = logger; this.statsName = statsName; this.scheduledExecutorService = scheduledExecutorService; - this.log = log; this.init(); } @@ -81,7 +82,7 @@ public void run() { } catch (Throwable ignored) { } } - }, Math.abs(UtilAll.computNextMinutesTimeMillis() - System.currentTimeMillis()), 1000 * 60, TimeUnit.MILLISECONDS); + }, Math.abs(UtilAll.computeNextMinutesTimeMillis() - System.currentTimeMillis()), 1000 * 60, TimeUnit.MILLISECONDS); this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { @Override @@ -91,7 +92,7 @@ public void run() { } catch (Throwable ignored) { } } - }, Math.abs(UtilAll.computNextHourTimeMillis() - System.currentTimeMillis()), 1000 * 60 * 60, TimeUnit.MILLISECONDS); + }, Math.abs(UtilAll.computeNextHourTimeMillis() - System.currentTimeMillis()), 1000 * 60 * 60, TimeUnit.MILLISECONDS); this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { @Override @@ -101,7 +102,7 @@ public void run() { } catch (Throwable ignored) { } } - }, Math.abs(UtilAll.computNextMorningTimeMillis() - System.currentTimeMillis()), 1000 * 60 * 60 * 24, TimeUnit.MILLISECONDS); + }, Math.abs(UtilAll.computeNextMorningTimeMillis() - System.currentTimeMillis()), 1000 * 60 * 60 * 24, TimeUnit.MILLISECONDS); } private void samplingInSeconds() { @@ -154,18 +155,73 @@ private void printAtDay() { public void addValue(final String statsKey, final int incValue, final int incTimes) { StatsItem statsItem = this.getAndCreateStatsItem(statsKey); - statsItem.getValue().addAndGet(incValue); - statsItem.getTimes().addAndGet(incTimes); + statsItem.getValue().add(incValue); + statsItem.getTimes().add(incTimes); + } + + public void addRTValue(final String statsKey, final int incValue, final int incTimes) { + StatsItem statsItem = this.getAndCreateRTStatsItem(statsKey); + statsItem.getValue().add(incValue); + statsItem.getTimes().add(incTimes); + } + + public void delValue(final String statsKey) { + StatsItem statsItem = this.statsItemTable.get(statsKey); + if (null != statsItem) { + this.statsItemTable.remove(statsKey); + } + } + + public void delValueByPrefixKey(final String statsKey, String separator) { + Iterator> it = this.statsItemTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + if (next.getKey().startsWith(statsKey + separator)) { + it.remove(); + } + } + } + + public void delValueByInfixKey(final String statsKey, String separator) { + Iterator> it = this.statsItemTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + if (next.getKey().contains(separator + statsKey + separator)) { + it.remove(); + } + } + } + + public void delValueBySuffixKey(final String statsKey, String separator) { + Iterator> it = this.statsItemTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + if (next.getKey().endsWith(separator + statsKey)) { + it.remove(); + } + } } public StatsItem getAndCreateStatsItem(final String statsKey) { + return getAndCreateItem(statsKey, false); + } + + public StatsItem getAndCreateRTStatsItem(final String statsKey) { + return getAndCreateItem(statsKey, true); + } + + public StatsItem getAndCreateItem(final String statsKey, boolean rtItem) { StatsItem statsItem = this.statsItemTable.get(statsKey); if (null == statsItem) { - statsItem = new StatsItem(this.statsName, statsKey, this.scheduledExecutorService, this.log); - StatsItem prev = this.statsItemTable.put(statsKey, statsItem); - - if (null == prev) { + if (rtItem) { + statsItem = new RTStatsItem(this.statsName, statsKey, this.scheduledExecutorService, logger); + } else { + statsItem = new StatsItem(this.statsName, statsKey, this.scheduledExecutorService, logger); + } + StatsItem prev = this.statsItemTable.putIfAbsent(statsKey, statsItem); + if (null != prev) { + statsItem = prev; // statsItem.init(); } } diff --git a/common/src/main/java/org/apache/rocketmq/common/stats/StatsSnapshot.java b/common/src/main/java/org/apache/rocketmq/common/stats/StatsSnapshot.java index 136f21a7c8e..0cecce99359 100644 --- a/common/src/main/java/org/apache/rocketmq/common/stats/StatsSnapshot.java +++ b/common/src/main/java/org/apache/rocketmq/common/stats/StatsSnapshot.java @@ -20,6 +20,8 @@ public class StatsSnapshot { private long sum; private double tps; + + private long times; private double avgpt; public long getSum() { @@ -45,4 +47,12 @@ public double getAvgpt() { public void setAvgpt(double avgpt) { this.avgpt = avgpt; } + + public long getTimes() { + return times; + } + + public void setTimes(long times) { + this.times = times; + } } diff --git a/common/src/main/java/org/apache/rocketmq/common/subscription/SubscriptionGroupConfig.java b/common/src/main/java/org/apache/rocketmq/common/subscription/SubscriptionGroupConfig.java deleted file mode 100644 index 8f4703f6c80..00000000000 --- a/common/src/main/java/org/apache/rocketmq/common/subscription/SubscriptionGroupConfig.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.common.subscription; - -import org.apache.rocketmq.common.MixAll; - -public class SubscriptionGroupConfig { - - private String groupName; - - private boolean consumeEnable = true; - private boolean consumeFromMinEnable = true; - - private boolean consumeBroadcastEnable = true; - - private int retryQueueNums = 1; - - private int retryMaxTimes = 16; - - private long brokerId = MixAll.MASTER_ID; - - private long whichBrokerWhenConsumeSlowly = 1; - - private boolean notifyConsumerIdsChangedEnable = true; - - public String getGroupName() { - return groupName; - } - - public void setGroupName(String groupName) { - this.groupName = groupName; - } - - public boolean isConsumeEnable() { - return consumeEnable; - } - - public void setConsumeEnable(boolean consumeEnable) { - this.consumeEnable = consumeEnable; - } - - public boolean isConsumeFromMinEnable() { - return consumeFromMinEnable; - } - - public void setConsumeFromMinEnable(boolean consumeFromMinEnable) { - this.consumeFromMinEnable = consumeFromMinEnable; - } - - public boolean isConsumeBroadcastEnable() { - return consumeBroadcastEnable; - } - - public void setConsumeBroadcastEnable(boolean consumeBroadcastEnable) { - this.consumeBroadcastEnable = consumeBroadcastEnable; - } - - public int getRetryQueueNums() { - return retryQueueNums; - } - - public void setRetryQueueNums(int retryQueueNums) { - this.retryQueueNums = retryQueueNums; - } - - public int getRetryMaxTimes() { - return retryMaxTimes; - } - - public void setRetryMaxTimes(int retryMaxTimes) { - this.retryMaxTimes = retryMaxTimes; - } - - public long getBrokerId() { - return brokerId; - } - - public void setBrokerId(long brokerId) { - this.brokerId = brokerId; - } - - public long getWhichBrokerWhenConsumeSlowly() { - return whichBrokerWhenConsumeSlowly; - } - - public void setWhichBrokerWhenConsumeSlowly(long whichBrokerWhenConsumeSlowly) { - this.whichBrokerWhenConsumeSlowly = whichBrokerWhenConsumeSlowly; - } - - public boolean isNotifyConsumerIdsChangedEnable() { - return notifyConsumerIdsChangedEnable; - } - - public void setNotifyConsumerIdsChangedEnable(final boolean notifyConsumerIdsChangedEnable) { - this.notifyConsumerIdsChangedEnable = notifyConsumerIdsChangedEnable; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + (int) (brokerId ^ (brokerId >>> 32)); - result = prime * result + (consumeBroadcastEnable ? 1231 : 1237); - result = prime * result + (consumeEnable ? 1231 : 1237); - result = prime * result + (consumeFromMinEnable ? 1231 : 1237); - result = prime * result + (notifyConsumerIdsChangedEnable ? 1231 : 1237); - result = prime * result + ((groupName == null) ? 0 : groupName.hashCode()); - result = prime * result + retryMaxTimes; - result = prime * result + retryQueueNums; - result = - prime * result + (int) (whichBrokerWhenConsumeSlowly ^ (whichBrokerWhenConsumeSlowly >>> 32)); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - SubscriptionGroupConfig other = (SubscriptionGroupConfig) obj; - if (brokerId != other.brokerId) - return false; - if (consumeBroadcastEnable != other.consumeBroadcastEnable) - return false; - if (consumeEnable != other.consumeEnable) - return false; - if (consumeFromMinEnable != other.consumeFromMinEnable) - return false; - if (groupName == null) { - if (other.groupName != null) - return false; - } else if (!groupName.equals(other.groupName)) - return false; - if (retryMaxTimes != other.retryMaxTimes) - return false; - if (retryQueueNums != other.retryQueueNums) - return false; - if (whichBrokerWhenConsumeSlowly != other.whichBrokerWhenConsumeSlowly) - return false; - if (notifyConsumerIdsChangedEnable != other.notifyConsumerIdsChangedEnable) - return false; - return true; - } - - @Override - public String toString() { - return "SubscriptionGroupConfig [groupName=" + groupName + ", consumeEnable=" + consumeEnable - + ", consumeFromMinEnable=" + consumeFromMinEnable + ", consumeBroadcastEnable=" - + consumeBroadcastEnable + ", retryQueueNums=" + retryQueueNums + ", retryMaxTimes=" - + retryMaxTimes + ", brokerId=" + brokerId + ", whichBrokerWhenConsumeSlowly=" - + whichBrokerWhenConsumeSlowly + ", notifyConsumerIdsChangedEnable=" - + notifyConsumerIdsChangedEnable + "]"; - } -} diff --git a/common/src/main/java/org/apache/rocketmq/common/sysflag/MessageSysFlag.java b/common/src/main/java/org/apache/rocketmq/common/sysflag/MessageSysFlag.java index f1397706cc5..bf60602145d 100644 --- a/common/src/main/java/org/apache/rocketmq/common/sysflag/MessageSysFlag.java +++ b/common/src/main/java/org/apache/rocketmq/common/sysflag/MessageSysFlag.java @@ -16,13 +16,37 @@ */ package org.apache.rocketmq.common.sysflag; +import org.apache.rocketmq.common.compression.CompressionType; + public class MessageSysFlag { + + /** + * Meaning of each bit in the system flag + * + * | bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | + * |--------|---|---|-----------|----------|-------------|------------------|------------------|------------------| + * | byte 1 | | | STOREHOST | BORNHOST | TRANSACTION | TRANSACTION | MULTI_TAGS | COMPRESSED | + * | byte 2 | | | | | | COMPRESSION_TYPE | COMPRESSION_TYPE | COMPRESSION_TYPE | + * | byte 3 | | | | | | | | | + * | byte 4 | | | | | | | | | + */ public final static int COMPRESSED_FLAG = 0x1; public final static int MULTI_TAGS_FLAG = 0x1 << 1; public final static int TRANSACTION_NOT_TYPE = 0; public final static int TRANSACTION_PREPARED_TYPE = 0x1 << 2; public final static int TRANSACTION_COMMIT_TYPE = 0x2 << 2; public final static int TRANSACTION_ROLLBACK_TYPE = 0x3 << 2; + public final static int BORNHOST_V6_FLAG = 0x1 << 4; + public final static int STOREHOSTADDRESS_V6_FLAG = 0x1 << 5; + //Mark the flag for batch to avoid conflict + public final static int NEED_UNWRAP_FLAG = 0x1 << 6; + public final static int INNER_BATCH_FLAG = 0x1 << 7; + + // COMPRESSION_TYPE + public final static int COMPRESSION_LZ4_TYPE = 0x1 << 8; + public final static int COMPRESSION_ZSTD_TYPE = 0x2 << 8; + public final static int COMPRESSION_ZLIB_TYPE = 0x3 << 8; + public final static int COMPRESSION_TYPE_COMPARATOR = 0x7 << 8; public static int getTransactionValue(final int flag) { return flag & TRANSACTION_ROLLBACK_TYPE; @@ -35,4 +59,14 @@ public static int resetTransactionValue(final int flag, final int type) { public static int clearCompressedFlag(final int flag) { return flag & (~COMPRESSED_FLAG); } + + // To match the compression type + public static CompressionType getCompressionType(final int flag) { + return CompressionType.findByValue((flag & COMPRESSION_TYPE_COMPARATOR) >> 8); + } + + public static boolean check(int flag, int expectedFlag) { + return (flag & expectedFlag) != 0; + } + } diff --git a/common/src/main/java/org/apache/rocketmq/common/sysflag/PullSysFlag.java b/common/src/main/java/org/apache/rocketmq/common/sysflag/PullSysFlag.java index e2edca46bf9..15d56dde78e 100644 --- a/common/src/main/java/org/apache/rocketmq/common/sysflag/PullSysFlag.java +++ b/common/src/main/java/org/apache/rocketmq/common/sysflag/PullSysFlag.java @@ -17,10 +17,11 @@ package org.apache.rocketmq.common.sysflag; public class PullSysFlag { - private final static int FLAG_COMMIT_OFFSET = 0x1 << 0; + private final static int FLAG_COMMIT_OFFSET = 0x1; private final static int FLAG_SUSPEND = 0x1 << 1; private final static int FLAG_SUBSCRIPTION = 0x1 << 2; private final static int FLAG_CLASS_FILTER = 0x1 << 3; + private final static int FLAG_LITE_PULL_MESSAGE = 0x1 << 4; public static int buildSysFlag(final boolean commitOffset, final boolean suspend, final boolean subscription, final boolean classFilter) { @@ -45,6 +46,17 @@ public static int buildSysFlag(final boolean commitOffset, final boolean suspend return flag; } + public static int buildSysFlag(final boolean commitOffset, final boolean suspend, + final boolean subscription, final boolean classFilter, final boolean litePull) { + int flag = buildSysFlag(commitOffset, suspend, subscription, classFilter); + + if (litePull) { + flag |= FLAG_LITE_PULL_MESSAGE; + } + + return flag; + } + public static int clearCommitOffsetFlag(final int sysFlag) { return sysFlag & (~FLAG_COMMIT_OFFSET); } @@ -57,11 +69,23 @@ public static boolean hasSuspendFlag(final int sysFlag) { return (sysFlag & FLAG_SUSPEND) == FLAG_SUSPEND; } + public static int clearSuspendFlag(final int sysFlag) { + return sysFlag & (~FLAG_SUSPEND); + } + public static boolean hasSubscriptionFlag(final int sysFlag) { return (sysFlag & FLAG_SUBSCRIPTION) == FLAG_SUBSCRIPTION; } + public static int buildSysFlagWithSubscription(final int sysFlag) { + return sysFlag | FLAG_SUBSCRIPTION; + } + public static boolean hasClassFilterFlag(final int sysFlag) { return (sysFlag & FLAG_CLASS_FILTER) == FLAG_CLASS_FILTER; } + + public static boolean hasLitePullFlag(final int sysFlag) { + return (sysFlag & FLAG_LITE_PULL_MESSAGE) == FLAG_LITE_PULL_MESSAGE; + } } diff --git a/common/src/main/java/org/apache/rocketmq/common/sysflag/TopicSysFlag.java b/common/src/main/java/org/apache/rocketmq/common/sysflag/TopicSysFlag.java index cc23ff1a3f3..a2bb5084051 100644 --- a/common/src/main/java/org/apache/rocketmq/common/sysflag/TopicSysFlag.java +++ b/common/src/main/java/org/apache/rocketmq/common/sysflag/TopicSysFlag.java @@ -59,7 +59,4 @@ public static int clearUnitSubFlag(final int sysFlag) { public static boolean hasUnitSubFlag(final int sysFlag) { return (sysFlag & FLAG_UNIT_SUB) == FLAG_UNIT_SUB; } - - public static void main(String[] args) { - } } diff --git a/common/src/main/java/org/apache/rocketmq/common/thread/FutureTaskExtThreadPoolExecutor.java b/common/src/main/java/org/apache/rocketmq/common/thread/FutureTaskExtThreadPoolExecutor.java new file mode 100644 index 00000000000..7b68873a99f --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/thread/FutureTaskExtThreadPoolExecutor.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.thread; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.RunnableFuture; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.common.future.FutureTaskExt; + +public class FutureTaskExtThreadPoolExecutor extends ThreadPoolExecutor { + + public FutureTaskExtThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + ThreadFactory threadFactory, + RejectedExecutionHandler handler) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler); + } + + @Override + protected RunnableFuture newTaskFor(final Runnable runnable, final T value) { + return new FutureTaskExt<>(runnable, value); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/thread/ThreadPoolMonitor.java b/common/src/main/java/org/apache/rocketmq/common/thread/ThreadPoolMonitor.java new file mode 100644 index 00000000000..1bfabbffedd --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/thread/ThreadPoolMonitor.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.thread; + +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class ThreadPoolMonitor { + private static Logger jstackLogger = LoggerFactory.getLogger(ThreadPoolMonitor.class); + private static Logger waterMarkLogger = LoggerFactory.getLogger(ThreadPoolMonitor.class); + + private static final List MONITOR_EXECUTOR = new CopyOnWriteArrayList<>(); + private static final ScheduledExecutorService MONITOR_SCHEDULED = ThreadUtils.newSingleThreadScheduledExecutor( + new ThreadFactoryBuilder().setNameFormat("ThreadPoolMonitor-%d").build() + ); + + private static volatile long threadPoolStatusPeriodTime = TimeUnit.SECONDS.toMillis(3); + private static volatile boolean enablePrintJstack = true; + private static volatile long jstackPeriodTime = 60000; + private static volatile long jstackTime = System.currentTimeMillis(); + + public static void config(Logger jstackLoggerConfig, Logger waterMarkLoggerConfig, + boolean enablePrintJstack, long jstackPeriodTimeConfig, long threadPoolStatusPeriodTimeConfig) { + jstackLogger = jstackLoggerConfig; + waterMarkLogger = waterMarkLoggerConfig; + threadPoolStatusPeriodTime = threadPoolStatusPeriodTimeConfig; + ThreadPoolMonitor.enablePrintJstack = enablePrintJstack; + jstackPeriodTime = jstackPeriodTimeConfig; + } + + public static ThreadPoolExecutor createAndMonitor(int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + String name, + int queueCapacity) { + return createAndMonitor(corePoolSize, maximumPoolSize, keepAliveTime, unit, name, queueCapacity, Collections.emptyList()); + } + + public static ThreadPoolExecutor createAndMonitor(int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + String name, + int queueCapacity, + ThreadPoolStatusMonitor... threadPoolStatusMonitors) { + return createAndMonitor(corePoolSize, maximumPoolSize, keepAliveTime, unit, name, queueCapacity, + Lists.newArrayList(threadPoolStatusMonitors)); + } + + public static ThreadPoolExecutor createAndMonitor(int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + String name, + int queueCapacity, + List threadPoolStatusMonitors) { + ThreadPoolExecutor executor = (ThreadPoolExecutor) ThreadUtils.newThreadPoolExecutor( + corePoolSize, + maximumPoolSize, + keepAliveTime, + unit, + new LinkedBlockingQueue<>(queueCapacity), + new ThreadFactoryBuilder().setNameFormat(name + "-%d").build(), + new ThreadPoolExecutor.DiscardOldestPolicy()); + List printers = Lists.newArrayList(new ThreadPoolQueueSizeMonitor(queueCapacity)); + printers.addAll(threadPoolStatusMonitors); + + MONITOR_EXECUTOR.add(ThreadPoolWrapper.builder() + .name(name) + .threadPoolExecutor(executor) + .statusPrinters(printers) + .build()); + return executor; + } + + public static void logThreadPoolStatus() { + for (ThreadPoolWrapper threadPoolWrapper : MONITOR_EXECUTOR) { + List monitors = threadPoolWrapper.getStatusPrinters(); + for (ThreadPoolStatusMonitor monitor : monitors) { + double value = monitor.value(threadPoolWrapper.getThreadPoolExecutor()); + waterMarkLogger.info("\t{}\t{}\t{}", threadPoolWrapper.getName(), + monitor.describe(), + value); + + if (enablePrintJstack) { + if (monitor.needPrintJstack(threadPoolWrapper.getThreadPoolExecutor(), value) && + System.currentTimeMillis() - jstackTime > jstackPeriodTime) { + jstackTime = System.currentTimeMillis(); + jstackLogger.warn("jstack start\n{}", UtilAll.jstack()); + } + } + } + } + } + + public static void init() { + MONITOR_SCHEDULED.scheduleAtFixedRate(ThreadPoolMonitor::logThreadPoolStatus, 20, + threadPoolStatusPeriodTime, TimeUnit.MILLISECONDS); + } + + public static void shutdown() { + MONITOR_SCHEDULED.shutdown(); + } +} \ No newline at end of file diff --git a/common/src/main/java/org/apache/rocketmq/common/thread/ThreadPoolQueueSizeMonitor.java b/common/src/main/java/org/apache/rocketmq/common/thread/ThreadPoolQueueSizeMonitor.java new file mode 100644 index 00000000000..9e2e2f675ca --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/thread/ThreadPoolQueueSizeMonitor.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.thread; + +import java.util.concurrent.ThreadPoolExecutor; + +public class ThreadPoolQueueSizeMonitor implements ThreadPoolStatusMonitor { + + private final int maxQueueCapacity; + + public ThreadPoolQueueSizeMonitor(int maxQueueCapacity) { + this.maxQueueCapacity = maxQueueCapacity; + } + + @Override + public String describe() { + return "queueSize"; + } + + @Override + public double value(ThreadPoolExecutor executor) { + return executor.getQueue().size(); + } + + @Override + public boolean needPrintJstack(ThreadPoolExecutor executor, double value) { + return value > maxQueueCapacity * 0.85; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/thread/ThreadPoolStatusMonitor.java b/common/src/main/java/org/apache/rocketmq/common/thread/ThreadPoolStatusMonitor.java new file mode 100644 index 00000000000..548fec52ec0 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/thread/ThreadPoolStatusMonitor.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.thread; + +import java.util.concurrent.ThreadPoolExecutor; + +public interface ThreadPoolStatusMonitor { + + String describe(); + + double value(ThreadPoolExecutor executor); + + boolean needPrintJstack(ThreadPoolExecutor executor, double value); +} \ No newline at end of file diff --git a/common/src/main/java/org/apache/rocketmq/common/thread/ThreadPoolWrapper.java b/common/src/main/java/org/apache/rocketmq/common/thread/ThreadPoolWrapper.java new file mode 100644 index 00000000000..e41859dbf54 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/thread/ThreadPoolWrapper.java @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.thread; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import java.util.List; +import java.util.concurrent.ThreadPoolExecutor; + +public class ThreadPoolWrapper { + private String name; + private ThreadPoolExecutor threadPoolExecutor; + private List statusPrinters; + + ThreadPoolWrapper(final String name, final ThreadPoolExecutor threadPoolExecutor, + final List statusPrinters) { + this.name = name; + this.threadPoolExecutor = threadPoolExecutor; + this.statusPrinters = statusPrinters; + } + + public static class ThreadPoolWrapperBuilder { + private String name; + private ThreadPoolExecutor threadPoolExecutor; + private List statusPrinters; + + ThreadPoolWrapperBuilder() { + } + + public ThreadPoolWrapper.ThreadPoolWrapperBuilder name(final String name) { + this.name = name; + return this; + } + + public ThreadPoolWrapper.ThreadPoolWrapperBuilder threadPoolExecutor( + final ThreadPoolExecutor threadPoolExecutor) { + this.threadPoolExecutor = threadPoolExecutor; + return this; + } + + public ThreadPoolWrapper.ThreadPoolWrapperBuilder statusPrinters( + final List statusPrinters) { + this.statusPrinters = statusPrinters; + return this; + } + + public ThreadPoolWrapper build() { + return new ThreadPoolWrapper(this.name, this.threadPoolExecutor, this.statusPrinters); + } + + @java.lang.Override + public java.lang.String toString() { + return "ThreadPoolWrapper.ThreadPoolWrapperBuilder(name=" + this.name + ", threadPoolExecutor=" + this.threadPoolExecutor + ", statusPrinters=" + this.statusPrinters + ")"; + } + } + + public static ThreadPoolWrapper.ThreadPoolWrapperBuilder builder() { + return new ThreadPoolWrapper.ThreadPoolWrapperBuilder(); + } + + public String getName() { + return this.name; + } + + public ThreadPoolExecutor getThreadPoolExecutor() { + return this.threadPoolExecutor; + } + + public List getStatusPrinters() { + return this.statusPrinters; + } + + public void setName(final String name) { + this.name = name; + } + + public void setThreadPoolExecutor(final ThreadPoolExecutor threadPoolExecutor) { + this.threadPoolExecutor = threadPoolExecutor; + } + + public void setStatusPrinters(final List statusPrinters) { + this.statusPrinters = statusPrinters; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ThreadPoolWrapper wrapper = (ThreadPoolWrapper) o; + return Objects.equal(name, wrapper.name) && Objects.equal(threadPoolExecutor, wrapper.threadPoolExecutor) && Objects.equal(statusPrinters, wrapper.statusPrinters); + } + + @Override + public int hashCode() { + return Objects.hashCode(name, threadPoolExecutor, statusPrinters); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("name", name) + .add("threadPoolExecutor", threadPoolExecutor) + .add("statusPrinters", statusPrinters) + .toString(); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/topic/TopicValidator.java b/common/src/main/java/org/apache/rocketmq/common/topic/TopicValidator.java new file mode 100644 index 00000000000..c19592a44c3 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/topic/TopicValidator.java @@ -0,0 +1,158 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.topic; + +import java.util.HashSet; +import java.util.Set; +import org.apache.rocketmq.common.UtilAll; + +public class TopicValidator { + + public static final String AUTO_CREATE_TOPIC_KEY_TOPIC = "TBW102"; // Will be created at broker when isAutoCreateTopicEnable + public static final String RMQ_SYS_SCHEDULE_TOPIC = "SCHEDULE_TOPIC_XXXX"; + public static final String RMQ_SYS_BENCHMARK_TOPIC = "BenchmarkTest"; + public static final String RMQ_SYS_TRANS_HALF_TOPIC = "RMQ_SYS_TRANS_HALF_TOPIC"; + public static final String RMQ_SYS_TRACE_TOPIC = "RMQ_SYS_TRACE_TOPIC"; + public static final String RMQ_SYS_TRANS_OP_HALF_TOPIC = "RMQ_SYS_TRANS_OP_HALF_TOPIC"; + public static final String RMQ_SYS_TRANS_CHECK_MAX_TIME_TOPIC = "TRANS_CHECK_MAX_TIME_TOPIC"; + public static final String RMQ_SYS_SELF_TEST_TOPIC = "SELF_TEST_TOPIC"; + public static final String RMQ_SYS_OFFSET_MOVED_EVENT = "OFFSET_MOVED_EVENT"; + public static final String RMQ_SYS_ROCKSDB_OFFSET_TOPIC = "CHECKPOINT_TOPIC"; + + public static final String SYSTEM_TOPIC_PREFIX = "rmq_sys_"; + public static final String SYNC_BROKER_MEMBER_GROUP_PREFIX = SYSTEM_TOPIC_PREFIX + "SYNC_BROKER_MEMBER_"; + + public static final boolean[] VALID_CHAR_BIT_MAP = new boolean[128]; + private static final int TOPIC_MAX_LENGTH = 127; + + private static final Set SYSTEM_TOPIC_SET = new HashSet<>(); + + /** + * Topics'set which client can not send msg! + */ + private static final Set NOT_ALLOWED_SEND_TOPIC_SET = new HashSet<>(); + + static { + SYSTEM_TOPIC_SET.add(AUTO_CREATE_TOPIC_KEY_TOPIC); + SYSTEM_TOPIC_SET.add(RMQ_SYS_SCHEDULE_TOPIC); + SYSTEM_TOPIC_SET.add(RMQ_SYS_BENCHMARK_TOPIC); + SYSTEM_TOPIC_SET.add(RMQ_SYS_TRANS_HALF_TOPIC); + SYSTEM_TOPIC_SET.add(RMQ_SYS_TRACE_TOPIC); + SYSTEM_TOPIC_SET.add(RMQ_SYS_TRANS_OP_HALF_TOPIC); + SYSTEM_TOPIC_SET.add(RMQ_SYS_TRANS_CHECK_MAX_TIME_TOPIC); + SYSTEM_TOPIC_SET.add(RMQ_SYS_SELF_TEST_TOPIC); + SYSTEM_TOPIC_SET.add(RMQ_SYS_OFFSET_MOVED_EVENT); + SYSTEM_TOPIC_SET.add(RMQ_SYS_ROCKSDB_OFFSET_TOPIC); + + NOT_ALLOWED_SEND_TOPIC_SET.add(RMQ_SYS_SCHEDULE_TOPIC); + NOT_ALLOWED_SEND_TOPIC_SET.add(RMQ_SYS_TRANS_HALF_TOPIC); + NOT_ALLOWED_SEND_TOPIC_SET.add(RMQ_SYS_TRANS_OP_HALF_TOPIC); + NOT_ALLOWED_SEND_TOPIC_SET.add(RMQ_SYS_TRANS_CHECK_MAX_TIME_TOPIC); + NOT_ALLOWED_SEND_TOPIC_SET.add(RMQ_SYS_SELF_TEST_TOPIC); + NOT_ALLOWED_SEND_TOPIC_SET.add(RMQ_SYS_OFFSET_MOVED_EVENT); + + // regex: ^[%|a-zA-Z0-9_-]+$ + // % + VALID_CHAR_BIT_MAP['%'] = true; + // - + VALID_CHAR_BIT_MAP['-'] = true; + // _ + VALID_CHAR_BIT_MAP['_'] = true; + // | + VALID_CHAR_BIT_MAP['|'] = true; + for (int i = 0; i < VALID_CHAR_BIT_MAP.length; i++) { + if (i >= '0' && i <= '9') { + // 0-9 + VALID_CHAR_BIT_MAP[i] = true; + } else if (i >= 'A' && i <= 'Z') { + // A-Z + VALID_CHAR_BIT_MAP[i] = true; + } else if (i >= 'a' && i <= 'z') { + // a-z + VALID_CHAR_BIT_MAP[i] = true; + } + } + } + + public static boolean isTopicOrGroupIllegal(String str) { + int strLen = str.length(); + int len = VALID_CHAR_BIT_MAP.length; + boolean[] bitMap = VALID_CHAR_BIT_MAP; + for (int i = 0; i < strLen; i++) { + char ch = str.charAt(i); + if (ch >= len || !bitMap[ch]) { + return true; + } + } + return false; + } + + public static ValidateTopicResult validateTopic(String topic) { + + if (UtilAll.isBlank(topic)) { + return new ValidateTopicResult(false, "The specified topic is blank."); + } + + if (isTopicOrGroupIllegal(topic)) { + return new ValidateTopicResult(false, "The specified topic contains illegal characters, allowing only ^[%|a-zA-Z0-9_-]+$"); + } + + if (topic.length() > TOPIC_MAX_LENGTH) { + return new ValidateTopicResult(false, "The specified topic is longer than topic max length."); + } + + return new ValidateTopicResult(true, ""); + } + + public static class ValidateTopicResult { + private final boolean valid; + private final String remark; + + public ValidateTopicResult(boolean valid, String remark) { + this.valid = valid; + this.remark = remark; + } + + public boolean isValid() { + return valid; + } + + public String getRemark() { + return remark; + } + } + + public static boolean isSystemTopic(String topic) { + return SYSTEM_TOPIC_SET.contains(topic) || topic.startsWith(SYSTEM_TOPIC_PREFIX); + } + + public static boolean isNotAllowedSendTopic(String topic) { + return NOT_ALLOWED_SEND_TOPIC_SET.contains(topic); + } + + public static void addSystemTopic(String systemTopic) { + SYSTEM_TOPIC_SET.add(systemTopic); + } + + public static Set getSystemTopicSet() { + return SYSTEM_TOPIC_SET; + } + + public static Set getNotAllowedSendTopicSet() { + return NOT_ALLOWED_SEND_TOPIC_SET; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/utils/AbstractStartAndShutdown.java b/common/src/main/java/org/apache/rocketmq/common/utils/AbstractStartAndShutdown.java new file mode 100644 index 00000000000..78147b69032 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/utils/AbstractStartAndShutdown.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.utils; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +public abstract class AbstractStartAndShutdown implements StartAndShutdown { + + protected List startAndShutdownList = new CopyOnWriteArrayList<>(); + + protected void appendStartAndShutdown(StartAndShutdown startAndShutdown) { + this.startAndShutdownList.add(startAndShutdown); + } + + @Override + public void start() throws Exception { + for (StartAndShutdown startAndShutdown : startAndShutdownList) { + startAndShutdown.start(); + } + } + + @Override + public void shutdown() throws Exception { + int index = startAndShutdownList.size() - 1; + for (; index >= 0; index--) { + startAndShutdownList.get(index).shutdown(); + } + } + + @Override + public void preShutdown() throws Exception { + int index = startAndShutdownList.size() - 1; + for (; index >= 0; index--) { + startAndShutdownList.get(index).preShutdown(); + } + } + + public void appendStart(Start start) { + this.appendStartAndShutdown(new StartAndShutdown() { + @Override + public void shutdown() throws Exception { + + } + + @Override + public void start() throws Exception { + start.start(); + } + }); + } + + public void appendShutdown(Shutdown shutdown) { + this.appendStartAndShutdown(new StartAndShutdown() { + @Override + public void shutdown() throws Exception { + shutdown.shutdown(); + } + + @Override + public void start() throws Exception { + + } + }); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/utils/BinaryUtil.java b/common/src/main/java/org/apache/rocketmq/common/utils/BinaryUtil.java new file mode 100644 index 00000000000..68d15e0708a --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/utils/BinaryUtil.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.utils; + +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import org.apache.commons.codec.binary.Hex; + +public class BinaryUtil { + public static byte[] calculateMd5(byte[] binaryData) { + MessageDigest messageDigest = null; + try { + messageDigest = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("MD5 algorithm not found."); + } + messageDigest.update(binaryData); + return messageDigest.digest(); + } + + public static String generateMd5(String bodyStr) { + byte[] bytes = calculateMd5(bodyStr.getBytes(Charset.forName("UTF-8"))); + return Hex.encodeHexString(bytes, false); + } + + public static String generateMd5(byte[] content) { + byte[] bytes = calculateMd5(content); + return Hex.encodeHexString(bytes, false); + } + + /** + * Returns true if subject contains only bytes that are spec-compliant ASCII characters. + * @param subject + * @return + */ + public static boolean isAscii(byte[] subject) { + if (subject == null) { + return false; + } + for (byte b : subject) { + if (b < 32 || b > 126) { + return false; + } + } + return true; + } +} \ No newline at end of file diff --git a/common/src/main/java/org/apache/rocketmq/common/utils/CheckpointFile.java b/common/src/main/java/org/apache/rocketmq/common/utils/CheckpointFile.java new file mode 100644 index 00000000000..1cb85d05eee --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/utils/CheckpointFile.java @@ -0,0 +1,155 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.utils; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.collections.CollectionUtils; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.UtilAll; + +/** + * Entry Checkpoint file util + * Format: + *

  • First line: Entries size + *
  • Second line: Entries crc32 + *
  • Next: Entry data per line + *

    + * Example: + *

  • 2 (size) + *
  • 773307083 (crc32) + *
  • 7-7000 (entry data) + *
  • 8-8000 (entry data) + */ +public class CheckpointFile { + + /** + * Not check crc32 when value is 0 + */ + private static final int NOT_CHECK_CRC_MAGIC_CODE = 0; + private final String filePath; + private final CheckpointSerializer serializer; + + public interface CheckpointSerializer { + /** + * Serialize entry to line + */ + String toLine(final T entry); + + /** + * DeSerialize line to entry + */ + T fromLine(final String line); + } + + public CheckpointFile(final String filePath, final CheckpointSerializer serializer) { + this.filePath = filePath; + this.serializer = serializer; + } + + public String getBackFilePath() { + return this.filePath + ".bak"; + } + + /** + * Write entries to file + */ + public void write(final List entries) throws IOException { + if (entries.isEmpty()) { + return; + } + synchronized (this) { + StringBuilder entryContent = new StringBuilder(); + for (T entry : entries) { + final String line = this.serializer.toLine(entry); + if (line != null && !line.isEmpty()) { + entryContent.append(line); + entryContent.append(System.lineSeparator()); + } + } + int crc32 = UtilAll.crc32(entryContent.toString().getBytes(StandardCharsets.UTF_8)); + + String content = entries.size() + System.lineSeparator() + + crc32 + System.lineSeparator() + entryContent; + MixAll.string2File(content, this.filePath); + } + } + + private List read(String filePath) throws IOException { + final ArrayList result = new ArrayList<>(); + synchronized (this) { + final File file = new File(filePath); + if (!file.exists()) { + return result; + } + try (BufferedReader reader = Files.newBufferedReader(file.toPath())) { + // Read size + int expectedLines = Integer.parseInt(reader.readLine()); + + // Read block crc + int expectedCrc32 = Integer.parseInt(reader.readLine()); + + // Read entries + StringBuilder sb = new StringBuilder(); + String line = reader.readLine(); + while (line != null) { + sb.append(line).append(System.lineSeparator()); + final T entry = this.serializer.fromLine(line); + if (entry != null) { + result.add(entry); + } + line = reader.readLine(); + } + int truthCrc32 = UtilAll.crc32(sb.toString().getBytes(StandardCharsets.UTF_8)); + + if (result.size() != expectedLines) { + final String err = String.format( + "Expect %d entries, only found %d entries", expectedLines, result.size()); + throw new IOException(err); + } + + if (NOT_CHECK_CRC_MAGIC_CODE != expectedCrc32 && truthCrc32 != expectedCrc32) { + final String err = String.format( + "Entries crc32 not match, file=%s, truth=%s", expectedCrc32, truthCrc32); + throw new IOException(err); + } + return result; + } + } + } + + /** + * Read entries from file + */ + public List read() throws IOException { + try { + List result = this.read(this.filePath); + if (CollectionUtils.isEmpty(result)) { + result = this.read(this.getBackFilePath()); + } + return result; + } catch (IOException e) { + return this.read(this.getBackFilePath()); + } + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/utils/CleanupPolicyUtils.java b/common/src/main/java/org/apache/rocketmq/common/utils/CleanupPolicyUtils.java new file mode 100644 index 00000000000..b73ece4f2c1 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/utils/CleanupPolicyUtils.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.utils; + +import org.apache.rocketmq.common.TopicAttributes; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.attribute.CleanupPolicy; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public class CleanupPolicyUtils { + public static boolean isCompaction(Optional topicConfig) { + return Objects.equals(CleanupPolicy.COMPACTION, getDeletePolicy(topicConfig)); + } + + public static CleanupPolicy getDeletePolicy(Optional topicConfig) { + if (!topicConfig.isPresent()) { + return CleanupPolicy.valueOf(TopicAttributes.CLEANUP_POLICY_ATTRIBUTE.getDefaultValue()); + } + + String attributeName = TopicAttributes.CLEANUP_POLICY_ATTRIBUTE.getName(); + + Map attributes = topicConfig.get().getAttributes(); + if (attributes == null || attributes.size() == 0) { + return CleanupPolicy.valueOf(TopicAttributes.CLEANUP_POLICY_ATTRIBUTE.getDefaultValue()); + } + + if (attributes.containsKey(attributeName)) { + return CleanupPolicy.valueOf(attributes.get(attributeName)); + } else { + return CleanupPolicy.valueOf(TopicAttributes.CLEANUP_POLICY_ATTRIBUTE.getDefaultValue()); + } + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/utils/ConcurrentHashMapUtils.java b/common/src/main/java/org/apache/rocketmq/common/utils/ConcurrentHashMapUtils.java new file mode 100644 index 00000000000..3ab1cecebc4 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/utils/ConcurrentHashMapUtils.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.utils; + +import java.util.Objects; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; + +public abstract class ConcurrentHashMapUtils { + + private static boolean isJdk8; + + static { + // Java 8 + // Java 9+: 9,11,17 + try { + isJdk8 = System.getProperty("java.version").startsWith("1.8."); + } catch (Exception ignore) { + isJdk8 = true; + } + } + + /** + * A temporary workaround for Java 8 specific performance issue JDK-8161372 .
    Use implementation of + * ConcurrentMap.computeIfAbsent instead. + * + * Requirement: The mapping function should not modify this map during computation. + * + * @see https://bugs.openjdk.java.net/browse/JDK-8161372 + */ + public static V computeIfAbsent(ConcurrentMap map, K key, Function func) { + Objects.requireNonNull(func); + if (isJdk8) { + V v = map.get(key); + if (null == v) { + // this bug fix methods maybe cause `func.apply` multiple calls. + v = func.apply(key); + if (null == v) { + return null; + } + final V res = map.putIfAbsent(key, v); + if (null != res) { + // if pre value present, means other thread put value already, and putIfAbsent not effect + // return exist value + return res; + } + } + return v; + } else { + return map.computeIfAbsent(key, func); + } + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/utils/CorrelationIdUtil.java b/common/src/main/java/org/apache/rocketmq/common/utils/CorrelationIdUtil.java new file mode 100644 index 00000000000..86d1fd4d42d --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/utils/CorrelationIdUtil.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.utils; + +import java.util.UUID; + +public class CorrelationIdUtil { + public static String createCorrelationId() { + return UUID.randomUUID().toString(); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/utils/DataConverter.java b/common/src/main/java/org/apache/rocketmq/common/utils/DataConverter.java new file mode 100644 index 00000000000..cc96770b22a --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/utils/DataConverter.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.utils; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +public class DataConverter { + public static final Charset CHARSET_UTF8 = Charset.forName("UTF-8"); + + public static byte[] Long2Byte(Long v) { + ByteBuffer tmp = ByteBuffer.allocate(8); + tmp.putLong(v); + return tmp.array(); + } + + public static int setBit(int value, int index, boolean flag) { + if (flag) { + return (int) (value | (1L << index)); + } else { + return (int) (value & ~(1L << index)); + } + } + + public static boolean getBit(int value, int index) { + return (value & (1L << index)) != 0; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/utils/FastJsonSerializer.java b/common/src/main/java/org/apache/rocketmq/common/utils/FastJsonSerializer.java new file mode 100644 index 00000000000..600054b40b8 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/utils/FastJsonSerializer.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.utils; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.support.config.FastJsonConfig; +import org.apache.commons.lang3.SerializationException; + +/** + * The object serializer based on fastJson + */ +public class FastJsonSerializer implements Serializer { + private FastJsonConfig fastJsonConfig = new FastJsonConfig(); + + public FastJsonConfig getFastJsonConfig() { + return this.fastJsonConfig; + } + + public void setFastJsonConfig(FastJsonConfig fastJsonConfig) { + this.fastJsonConfig = fastJsonConfig; + } + + @Override + public byte[] serialize(T t) throws SerializationException { + if (t == null) { + return new byte[0]; + } else { + try { + return JSON.toJSONBytes(this.fastJsonConfig.getCharset(), t, this.fastJsonConfig.getSerializeConfig(), this.fastJsonConfig.getSerializeFilters(), this.fastJsonConfig.getDateFormat(), JSON.DEFAULT_GENERATE_FEATURE, this.fastJsonConfig.getSerializerFeatures()); + } catch (Exception var3) { + throw new SerializationException("Could not serialize: " + var3.getMessage(), var3); + } + } + } + + @Override + public T deserialize(byte[] bytes, Class type) throws SerializationException { + if (bytes != null && bytes.length != 0) { + try { + return JSON.parseObject(bytes, this.fastJsonConfig.getCharset(), type, this.fastJsonConfig.getParserConfig(), this.fastJsonConfig.getParseProcess(), JSON.DEFAULT_PARSER_FEATURE, this.fastJsonConfig.getFeatures()); + } catch (Exception var3) { + throw new SerializationException("Could not deserialize: " + var3.getMessage(), var3); + } + } else { + return null; + } + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/utils/IOTinyUtils.java b/common/src/main/java/org/apache/rocketmq/common/utils/IOTinyUtils.java index 5ed82ae6e67..acba540d3a5 100644 --- a/common/src/main/java/org/apache/rocketmq/common/utils/IOTinyUtils.java +++ b/common/src/main/java/org/apache/rocketmq/common/utils/IOTinyUtils.java @@ -29,14 +29,14 @@ import java.io.Reader; import java.io.Writer; import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; -import org.apache.rocketmq.remoting.common.RemotingHelper; public class IOTinyUtils { static public String toString(InputStream input, String encoding) throws IOException { - return (null == encoding) ? toString(new InputStreamReader(input, RemotingHelper.DEFAULT_CHARSET)) : toString(new InputStreamReader( + return (null == encoding) ? toString(new InputStreamReader(input, StandardCharsets.UTF_8)) : toString(new InputStreamReader( input, encoding)); } @@ -58,7 +58,7 @@ static public long copy(Reader input, Writer output) throws IOException { static public List readLines(Reader input) throws IOException { BufferedReader reader = toBufferedReader(input); - List list = new ArrayList(); + List list = new ArrayList<>(); String line; for (; ; ) { line = reader.readLine(); diff --git a/common/src/main/java/org/apache/rocketmq/common/utils/MessageUtils.java b/common/src/main/java/org/apache/rocketmq/common/utils/MessageUtils.java new file mode 100644 index 00000000000..a6563bc922b --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/utils/MessageUtils.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.utils; + +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import com.google.common.hash.Hashing; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; + +import static org.apache.rocketmq.common.message.MessageDecoder.NAME_VALUE_SEPARATOR; +import static org.apache.rocketmq.common.message.MessageDecoder.PROPERTY_SEPARATOR; + +public class MessageUtils { + + public static int getShardingKeyIndex(String shardingKey, int indexSize) { + return Math.abs(Hashing.murmur3_32().hashBytes(shardingKey.getBytes(StandardCharsets.UTF_8)).asInt() % indexSize); + } + + public static int getShardingKeyIndexByMsg(MessageExt msg, int indexSize) { + String shardingKey = msg.getProperty(MessageConst.PROPERTY_SHARDING_KEY); + if (shardingKey == null) { + shardingKey = ""; + } + + return getShardingKeyIndex(shardingKey, indexSize); + } + + public static Set getShardingKeyIndexes(Collection msgs, int indexSize) { + Set indexSet = new HashSet<>(indexSize); + for (MessageExt msg : msgs) { + indexSet.add(getShardingKeyIndexByMsg(msg, indexSize)); + } + return indexSet; + } + + public static String deleteProperty(String propertiesString, String name) { + if (propertiesString != null) { + int idx0 = 0; + int idx1; + int idx2; + idx1 = propertiesString.indexOf(name, idx0); + if (idx1 != -1) { + // cropping may be required + StringBuilder stringBuilder = new StringBuilder(propertiesString.length()); + while (true) { + int startIdx = idx0; + while (true) { + idx1 = propertiesString.indexOf(name, startIdx); + if (idx1 == -1) { + break; + } + startIdx = idx1 + name.length(); + if (idx1 == 0 || propertiesString.charAt(idx1 - 1) == PROPERTY_SEPARATOR) { + if (propertiesString.length() > idx1 + name.length() + && propertiesString.charAt(idx1 + name.length()) == NAME_VALUE_SEPARATOR) { + break; + } + } + } + if (idx1 == -1) { + // there are no characters that need to be skipped. Append all remaining characters. + stringBuilder.append(propertiesString, idx0, propertiesString.length()); + break; + } + // there are characters that need to be cropped + stringBuilder.append(propertiesString, idx0, idx1); + // move idx2 to the end of the cropped character + idx2 = propertiesString.indexOf(PROPERTY_SEPARATOR, idx1 + name.length() + 1); + // all subsequent characters will be cropped + if (idx2 == -1) { + break; + } + idx0 = idx2 + 1; + } + return stringBuilder.toString(); + } + } + return propertiesString; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/utils/NameServerAddressUtils.java b/common/src/main/java/org/apache/rocketmq/common/utils/NameServerAddressUtils.java new file mode 100644 index 00000000000..68f883935ae --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/utils/NameServerAddressUtils.java @@ -0,0 +1,47 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE + * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file + * to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.apache.rocketmq.common.utils; + +import java.util.regex.Pattern; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.MixAll; + +public class NameServerAddressUtils { + public static final String INSTANCE_PREFIX = "MQ_INST_"; + public static final String INSTANCE_REGEX = INSTANCE_PREFIX + "\\w+_\\w+"; + public static final String ENDPOINT_PREFIX = "(\\w+://|)"; + public static final Pattern NAMESRV_ENDPOINT_PATTERN = Pattern.compile("^http://.*"); + public static final Pattern INST_ENDPOINT_PATTERN = Pattern.compile("^" + ENDPOINT_PREFIX + INSTANCE_REGEX + "\\..*"); + + public static String getNameServerAddresses() { + return System.getProperty(MixAll.NAMESRV_ADDR_PROPERTY, System.getenv(MixAll.NAMESRV_ADDR_ENV)); + } + + public static boolean validateInstanceEndpoint(String endpoint) { + return INST_ENDPOINT_PATTERN.matcher(endpoint).matches(); + } + + public static String parseInstanceIdFromEndpoint(String endpoint) { + if (StringUtils.isEmpty(endpoint)) { + return null; + } + return endpoint.substring(endpoint.lastIndexOf("/") + 1, endpoint.indexOf('.')); + } + + public static String getNameSrvAddrFromNamesrvEndpoint(String nameSrvEndpoint) { + if (StringUtils.isEmpty(nameSrvEndpoint)) { + return null; + } + return nameSrvEndpoint.substring(nameSrvEndpoint.lastIndexOf('/') + 1); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/utils/NetworkUtil.java b/common/src/main/java/org/apache/rocketmq/common/utils/NetworkUtil.java new file mode 100644 index 00000000000..7dd83e61799 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/utils/NetworkUtil.java @@ -0,0 +1,181 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.utils; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.NetworkInterface; +import java.net.SocketAddress; +import java.nio.channels.Selector; +import java.nio.channels.spi.SelectorProvider; +import java.util.ArrayList; +import java.util.Enumeration; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class NetworkUtil { + public static final String OS_NAME = System.getProperty("os.name"); + + private static final Logger log = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); + private static boolean isLinuxPlatform = false; + private static boolean isWindowsPlatform = false; + + static { + if (OS_NAME != null && OS_NAME.toLowerCase().contains("linux")) { + isLinuxPlatform = true; + } + + if (OS_NAME != null && OS_NAME.toLowerCase().contains("windows")) { + isWindowsPlatform = true; + } + } + + public static boolean isWindowsPlatform() { + return isWindowsPlatform; + } + + public static Selector openSelector() throws IOException { + Selector result = null; + + if (isLinuxPlatform()) { + try { + final Class providerClazz = Class.forName("sun.nio.ch.EPollSelectorProvider"); + if (providerClazz != null) { + try { + final Method method = providerClazz.getMethod("provider"); + if (method != null) { + final SelectorProvider selectorProvider = (SelectorProvider) method.invoke(null); + if (selectorProvider != null) { + result = selectorProvider.openSelector(); + } + } + } catch (final Exception e) { + log.warn("Open ePoll Selector for linux platform exception", e); + } + } + } catch (final Exception e) { + // ignore + } + } + + if (result == null) { + result = Selector.open(); + } + + return result; + } + + public static boolean isLinuxPlatform() { + return isLinuxPlatform; + } + + public static String getLocalAddress() { + try { + // Traversal Network interface to get the first non-loopback and non-private address + Enumeration enumeration = NetworkInterface.getNetworkInterfaces(); + ArrayList ipv4Result = new ArrayList<>(); + ArrayList ipv6Result = new ArrayList<>(); + while (enumeration.hasMoreElements()) { + final NetworkInterface nif = enumeration.nextElement(); + if (isBridge(nif) || nif.isVirtual() || nif.isPointToPoint() || !nif.isUp()) { + continue; + } + + final Enumeration en = nif.getInetAddresses(); + while (en.hasMoreElements()) { + final InetAddress address = en.nextElement(); + if (!address.isLoopbackAddress()) { + if (address instanceof Inet6Address) { + ipv6Result.add(normalizeHostAddress(address)); + } else { + ipv4Result.add(normalizeHostAddress(address)); + } + } + } + } + + // prefer ipv4 + if (!ipv4Result.isEmpty()) { + for (String ip : ipv4Result) { + if (ip.startsWith("127.0") || ip.startsWith("192.168") || ip.startsWith("0.")) { + continue; + } + + return ip; + } + + return ipv4Result.get(ipv4Result.size() - 1); + } else if (!ipv6Result.isEmpty()) { + return ipv6Result.get(0); + } + //If failed to find,fall back to localhost + final InetAddress localHost = InetAddress.getLocalHost(); + return normalizeHostAddress(localHost); + } catch (Exception e) { + log.error("Failed to obtain local address", e); + } + + return null; + } + + public static String normalizeHostAddress(final InetAddress localHost) { + if (localHost instanceof Inet6Address) { + return "[" + localHost.getHostAddress() + "]"; + } else { + return localHost.getHostAddress(); + } + } + + public static SocketAddress string2SocketAddress(final String addr) { + int split = addr.lastIndexOf(":"); + String host = addr.substring(0, split); + String port = addr.substring(split + 1); + InetSocketAddress isa = new InetSocketAddress(host, Integer.parseInt(port)); + return isa; + } + + public static String socketAddress2String(final SocketAddress addr) { + StringBuilder sb = new StringBuilder(); + InetSocketAddress inetSocketAddress = (InetSocketAddress) addr; + sb.append(inetSocketAddress.getAddress().getHostAddress()); + sb.append(":"); + sb.append(inetSocketAddress.getPort()); + return sb.toString(); + } + + public static String convert2IpString(final String addr) { + return socketAddress2String(string2SocketAddress(addr)); + } + + private static boolean isBridge(NetworkInterface networkInterface) { + try { + if (isLinuxPlatform()) { + String interfaceName = networkInterface.getName(); + File file = new File("/sys/class/net/" + interfaceName + "/bridge"); + return file.exists(); + } + } catch (SecurityException e) { + //Ignore + } + return false; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/utils/PositiveAtomicCounter.java b/common/src/main/java/org/apache/rocketmq/common/utils/PositiveAtomicCounter.java new file mode 100644 index 00000000000..105b88c5770 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/utils/PositiveAtomicCounter.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.utils; + +import java.util.concurrent.atomic.AtomicInteger; + +public class PositiveAtomicCounter { + private static final int MASK = 0x7FFFFFFF; + private final AtomicInteger atom; + + + public PositiveAtomicCounter() { + atom = new AtomicInteger(0); + } + + + public final int incrementAndGet() { + final int rt = atom.incrementAndGet(); + return rt & MASK; + } + + + public int intValue() { + return atom.intValue(); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/utils/QueueTypeUtils.java b/common/src/main/java/org/apache/rocketmq/common/utils/QueueTypeUtils.java new file mode 100644 index 00000000000..e2f006e12d2 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/utils/QueueTypeUtils.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.utils; + +import org.apache.rocketmq.common.TopicAttributes; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.attribute.CQType; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public class QueueTypeUtils { + + public static boolean isBatchCq(Optional topicConfig) { + return Objects.equals(CQType.BatchCQ, getCQType(topicConfig)); + } + + public static CQType getCQType(Optional topicConfig) { + if (!topicConfig.isPresent()) { + return CQType.valueOf(TopicAttributes.QUEUE_TYPE_ATTRIBUTE.getDefaultValue()); + } + + String attributeName = TopicAttributes.QUEUE_TYPE_ATTRIBUTE.getName(); + + Map attributes = topicConfig.get().getAttributes(); + if (attributes == null || attributes.size() == 0) { + return CQType.valueOf(TopicAttributes.QUEUE_TYPE_ATTRIBUTE.getDefaultValue()); + } + + if (attributes.containsKey(attributeName)) { + return CQType.valueOf(attributes.get(attributeName)); + } else { + return CQType.valueOf(TopicAttributes.QUEUE_TYPE_ATTRIBUTE.getDefaultValue()); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/org/apache/rocketmq/common/utils/Serializer.java b/common/src/main/java/org/apache/rocketmq/common/utils/Serializer.java new file mode 100644 index 00000000000..a98d2454d44 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/utils/Serializer.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.utils; + +import org.apache.commons.lang3.SerializationException; + +/** + * Serializer + */ +public interface Serializer { + + /** + * Serialize object t to byte[] + */ + byte[] serialize(T t) throws SerializationException; + + /** + * De-serialize bytes to T + */ + T deserialize(byte[] bytes, Class type) throws SerializationException; +} diff --git a/common/src/main/java/org/apache/rocketmq/common/utils/ServiceProvider.java b/common/src/main/java/org/apache/rocketmq/common/utils/ServiceProvider.java new file mode 100644 index 00000000000..65dea47b5ea --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/utils/ServiceProvider.java @@ -0,0 +1,198 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.utils; + +import java.nio.charset.StandardCharsets; + +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +public class ServiceProvider { + private static final Logger LOG = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); + /** + * A reference to the classloader that loaded this class. It's more efficient to compute it once and cache it here. + */ + private static ClassLoader thisClassLoader; + + /** + * JDK1.3+ 'Service Provider' + * specification. + */ + public static final String PREFIX = "META-INF/service/"; + + static { + thisClassLoader = getClassLoader(ServiceProvider.class); + } + + /** + * Returns a string that uniquely identifies the specified object, including its class. + *

    + * The returned string is of form "classname@hashcode", ie is the same as the return value of the Object.toString() + * method, but works even when the specified object's class has overidden the toString method. + * + * @param o may be null. + * @return a string of form classname@hashcode, or "null" if param o is null. + */ + protected static String objectId(Object o) { + if (o == null) { + return "null"; + } else { + return o.getClass().getName() + "@" + System.identityHashCode(o); + } + } + + protected static ClassLoader getClassLoader(Class clazz) { + try { + return clazz.getClassLoader(); + } catch (SecurityException e) { + LOG.error("Unable to get classloader for class {} due to security restrictions , error info {}", + clazz, e.getMessage()); + throw e; + } + } + + protected static ClassLoader getContextClassLoader() { + ClassLoader classLoader = null; + try { + classLoader = Thread.currentThread().getContextClassLoader(); + } catch (SecurityException ex) { + /** + * The getContextClassLoader() method throws SecurityException when the context + * class loader isn't an ancestor of the calling class's class + * loader, or if security permissions are restricted. + */ + } + return classLoader; + } + + protected static InputStream getResourceAsStream(ClassLoader loader, String name) { + if (loader != null) { + return loader.getResourceAsStream(name); + } else { + return ClassLoader.getSystemResourceAsStream(name); + } + } + + public static List load(Class clazz) { + String fullName = PREFIX + clazz.getName(); + return load(fullName, clazz); + } + + public static List load(String name, Class clazz) { + LOG.info("Looking for a resource file of name [{}] ...", name); + List services = new ArrayList<>(); + InputStream is = getResourceAsStream(getContextClassLoader(), name); + if (is == null) { + LOG.warn("No resource file with name [{}] found.", name); + return services; + } + try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + String serviceName = reader.readLine(); + List names = new ArrayList<>(); + while (serviceName != null && !"".equals(serviceName)) { + LOG.info( + "Creating an instance as specified by file {} which was present in the path of the context classloader.", + name); + if (!names.contains(serviceName)) { + names.add(serviceName); + services.add(initService(getContextClassLoader(), serviceName, clazz)); + } + serviceName = reader.readLine(); + } + } catch (Exception e) { + LOG.error("Error occurred when looking for resource file " + name, e); + } + return services; + } + + public static T loadClass(Class clazz) { + String fullName = PREFIX + clazz.getName(); + return loadClass(fullName, clazz); + } + + public static T loadClass(String name, Class clazz) { + LOG.info("Looking for a resource file of name [{}] ...", name); + T s = null; + InputStream is = getResourceAsStream(getContextClassLoader(), name); + if (is == null) { + LOG.warn("No resource file with name [{}] found.", name); + return null; + } + try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + String serviceName = reader.readLine(); + if (serviceName != null && !"".equals(serviceName)) { + s = initService(getContextClassLoader(), serviceName, clazz); + } else { + LOG.warn("ServiceName is empty!"); + } + } catch (Exception e) { + LOG.warn("Error occurred when looking for resource file " + name, e); + } + return s; + } + + protected static T initService(ClassLoader classLoader, String serviceName, Class clazz) { + Class serviceClazz = null; + try { + if (classLoader != null) { + try { + // Warning: must typecast here & allow exception to be generated/caught & recast properly + serviceClazz = classLoader.loadClass(serviceName); + if (clazz.isAssignableFrom(serviceClazz)) { + LOG.info("Loaded class {} from classloader {}", serviceClazz.getName(), + objectId(classLoader)); + } else { + // This indicates a problem with the ClassLoader tree. An incompatible ClassLoader was used to load the implementation. + LOG.error( + "Class {} loaded from classloader {} does not extend {} as loaded by this classloader.", + serviceClazz.getName(), + objectId(serviceClazz.getClassLoader()), clazz.getName()); + } + return (T) serviceClazz.getDeclaredConstructor().newInstance(); + } catch (ClassNotFoundException ex) { + if (classLoader == thisClassLoader) { + // Nothing more to try, onwards. + LOG.warn("Unable to locate any class {} via classloader {}", serviceName, + objectId(classLoader)); + throw ex; + } + // Ignore exception, continue + } catch (NoClassDefFoundError e) { + if (classLoader == thisClassLoader) { + // Nothing more to try, onwards. + LOG.warn( + "Class {} cannot be loaded via classloader {}.it depends on some other class that cannot be found.", + serviceClazz, objectId(classLoader)); + throw e; + } + // Ignore exception, continue + } + } + } catch (Exception e) { + LOG.error("Unable to init service.", e); + } + return (T) serviceClazz; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/utils/Shutdown.java b/common/src/main/java/org/apache/rocketmq/common/utils/Shutdown.java new file mode 100644 index 00000000000..07dc5f6767b --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/utils/Shutdown.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.utils; + +public interface Shutdown { + void shutdown() throws Exception; +} diff --git a/common/src/main/java/org/apache/rocketmq/common/utils/Start.java b/common/src/main/java/org/apache/rocketmq/common/utils/Start.java new file mode 100644 index 00000000000..1e700dd70bc --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/utils/Start.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.utils; + +public interface Start { + void start() throws Exception; +} diff --git a/common/src/main/java/org/apache/rocketmq/common/utils/StartAndShutdown.java b/common/src/main/java/org/apache/rocketmq/common/utils/StartAndShutdown.java new file mode 100644 index 00000000000..28999385a77 --- /dev/null +++ b/common/src/main/java/org/apache/rocketmq/common/utils/StartAndShutdown.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.utils; + +public interface StartAndShutdown extends Start, Shutdown { + default void preShutdown() throws Exception {} +} diff --git a/common/src/main/java/org/apache/rocketmq/common/utils/ThreadUtils.java b/common/src/main/java/org/apache/rocketmq/common/utils/ThreadUtils.java index 8c28d70027e..1644c6360ec 100644 --- a/common/src/main/java/org/apache/rocketmq/common/utils/ThreadUtils.java +++ b/common/src/main/java/org/apache/rocketmq/common/utils/ThreadUtils.java @@ -20,38 +20,94 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; +import org.apache.rocketmq.common.ThreadFactoryImpl; import org.apache.rocketmq.common.constant.LoggerName; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.common.thread.FutureTaskExtThreadPoolExecutor; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; public final class ThreadUtils { - private static final Logger log = LoggerFactory.getLogger(LoggerName.TOOLS_LOGGER_NAME); + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.TOOLS_LOGGER_NAME); - public static ExecutorService newThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, - TimeUnit unit, BlockingQueue workQueue, String processName, boolean isDaemon) { - return new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, newThreadFactory(processName, isDaemon)); + public static ExecutorService newSingleThreadExecutor(String processName, boolean isDaemon) { + return ThreadUtils.newSingleThreadExecutor(newThreadFactory(processName, isDaemon)); } - public static ExecutorService newSingleThreadExecutor(String processName, boolean isDaemon) { - return Executors.newSingleThreadExecutor(newThreadFactory(processName, isDaemon)); + public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { + return ThreadUtils.newThreadPoolExecutor(1, threadFactory); + } + + public static ExecutorService newThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory) { + return ThreadUtils.newThreadPoolExecutor(corePoolSize, corePoolSize, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(), + threadFactory); + } + + public static ExecutorService newThreadPoolExecutor(int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, BlockingQueue workQueue, + String processName, + boolean isDaemon) { + return ThreadUtils.newThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, newThreadFactory(processName, isDaemon)); + } + + public static ExecutorService newThreadPoolExecutor(final int corePoolSize, + final int maximumPoolSize, + final long keepAliveTime, + final TimeUnit unit, + final BlockingQueue workQueue, + final ThreadFactory threadFactory) { + return ThreadUtils.newThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, new ThreadPoolExecutor.AbortPolicy()); + } + + public static ExecutorService newThreadPoolExecutor(int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + ThreadFactory threadFactory, + RejectedExecutionHandler handler) { + return new FutureTaskExtThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler); } public static ScheduledExecutorService newSingleThreadScheduledExecutor(String processName, boolean isDaemon) { - return Executors.newSingleThreadScheduledExecutor(newThreadFactory(processName, isDaemon)); + return ThreadUtils.newScheduledThreadPool(1, processName, isDaemon); + } + + public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) { + return ThreadUtils.newScheduledThreadPool(1, threadFactory); + } + + public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { + return ThreadUtils.newScheduledThreadPool(corePoolSize, Executors.defaultThreadFactory()); } - public static ScheduledExecutorService newFixedThreadScheduledPool(int nThreads, String processName, + public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, String processName, boolean isDaemon) { - return Executors.newScheduledThreadPool(nThreads, newThreadFactory(processName, isDaemon)); + return ThreadUtils.newScheduledThreadPool(corePoolSize, newThreadFactory(processName, isDaemon)); + } + + public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory) { + return ThreadUtils.newScheduledThreadPool(corePoolSize, threadFactory, new ThreadPoolExecutor.AbortPolicy()); + } + + public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, + ThreadFactory threadFactory, + RejectedExecutionHandler handler) { + return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory, handler); } public static ThreadFactory newThreadFactory(String processName, boolean isDaemon) { - return newGenericThreadFactory("Remoting-" + processName, isDaemon); + return newGenericThreadFactory("ThreadUtils-" + processName, isDaemon); } public static ThreadFactory newGenericThreadFactory(String processName) { @@ -63,38 +119,20 @@ public static ThreadFactory newGenericThreadFactory(String processName, int thre } public static ThreadFactory newGenericThreadFactory(final String processName, final boolean isDaemon) { - return new ThreadFactory() { - private AtomicInteger threadIndex = new AtomicInteger(0); - - @Override - public Thread newThread(Runnable r) { - Thread thread = new Thread(r, String.format("%s_%d", processName, this.threadIndex.incrementAndGet())); - thread.setDaemon(isDaemon); - return thread; - } - }; + return new ThreadFactoryImpl(processName + "_", isDaemon); } public static ThreadFactory newGenericThreadFactory(final String processName, final int threads, final boolean isDaemon) { - return new ThreadFactory() { - private AtomicInteger threadIndex = new AtomicInteger(0); - - @Override - public Thread newThread(Runnable r) { - Thread thread = new Thread(r, String.format("%s_%d_%d", processName, threads, this.threadIndex.incrementAndGet())); - thread.setDaemon(isDaemon); - return thread; - } - }; + return new ThreadFactoryImpl(String.format("%s_%d_", processName, threads), isDaemon); } /** * Create a new thread * - * @param name The name of the thread + * @param name The name of the thread * @param runnable The work for the thread to do - * @param daemon Should the thread block JVM stop? + * @param daemon Should the thread block JVM stop? * @return The unstarted thread */ public static Thread newThread(String name, Runnable runnable, boolean daemon) { @@ -102,7 +140,7 @@ public static Thread newThread(String name, Runnable runnable, boolean daemon) { thread.setDaemon(daemon); thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { public void uncaughtException(Thread t, Throwable e) { - log.error("Uncaught exception in thread '" + t.getName() + "':", e); + LOGGER.error("Uncaught exception in thread '" + t.getName() + "':", e); } }); return thread; @@ -121,7 +159,7 @@ public static void shutdownGracefully(final Thread t) { * Shutdown passed thread using isAlive and join. * * @param millis Pass 0 if we're to wait forever. - * @param t Thread to stop + * @param t Thread to stop */ public static void shutdownGracefully(final Thread t, final long millis) { if (t == null) @@ -141,7 +179,7 @@ public static void shutdownGracefully(final Thread t, final long millis) { * {@link ExecutorService}. * * @param executor executor - * @param timeout timeout + * @param timeout timeout * @param timeUnit timeUnit */ public static void shutdownGracefully(ExecutorService executor, long timeout, TimeUnit timeUnit) { @@ -153,7 +191,7 @@ public static void shutdownGracefully(ExecutorService executor, long timeout, Ti executor.shutdownNow(); // Wait a while for tasks to respond to being cancelled. if (!executor.awaitTermination(timeout, timeUnit)) { - log.warn(String.format("%s didn't terminate!", executor)); + LOGGER.warn(String.format("%s didn't terminate!", executor)); } } } catch (InterruptedException ie) { @@ -164,6 +202,17 @@ public static void shutdownGracefully(ExecutorService executor, long timeout, Ti } } + /** + * Shutdown the specific ExecutorService + * + * @param executorService the executor + */ + public static void shutdown(ExecutorService executorService) { + if (executorService != null) { + executorService.shutdown(); + } + } + /** * A constructor to stop this class being constructed. */ diff --git a/common/src/main/resources/META-INF/services/org.apache.rocketmq.logging.ch.qos.logback.classic.spi.Configurator b/common/src/main/resources/META-INF/services/org.apache.rocketmq.logging.ch.qos.logback.classic.spi.Configurator new file mode 100644 index 00000000000..b90cd30997f --- /dev/null +++ b/common/src/main/resources/META-INF/services/org.apache.rocketmq.logging.ch.qos.logback.classic.spi.Configurator @@ -0,0 +1 @@ +org.apache.rocketmq.common.logging.DefaultJoranConfiguratorExt \ No newline at end of file diff --git a/common/src/test/java/org/apache/rocketmq/common/BrokerConfigTest.java b/common/src/test/java/org/apache/rocketmq/common/BrokerConfigTest.java index 0fb9b3afb12..07e132ff815 100644 --- a/common/src/test/java/org/apache/rocketmq/common/BrokerConfigTest.java +++ b/common/src/test/java/org/apache/rocketmq/common/BrokerConfigTest.java @@ -27,4 +27,23 @@ public void testConsumerFallBehindThresholdOverflow() { long expect = 1024L * 1024 * 1024 * 16; assertThat(new BrokerConfig().getConsumerFallbehindThreshold()).isEqualTo(expect); } + + @Test + public void testBrokerConfigAttribute() { + BrokerConfig brokerConfig = new BrokerConfig(); + brokerConfig.setNamesrvAddr("127.0.0.1:9876"); + brokerConfig.setAutoCreateTopicEnable(false); + brokerConfig.setBrokerName("broker-a"); + brokerConfig.setBrokerId(0); + brokerConfig.setBrokerClusterName("DefaultCluster"); + brokerConfig.setMsgTraceTopicName("RMQ_SYS_TRACE_TOPIC4"); + brokerConfig.setAutoDeleteUnusedStats(true); + assertThat(brokerConfig.getBrokerClusterName()).isEqualTo("DefaultCluster"); + assertThat(brokerConfig.getNamesrvAddr()).isEqualTo("127.0.0.1:9876"); + assertThat(brokerConfig.getMsgTraceTopicName()).isEqualTo("RMQ_SYS_TRACE_TOPIC4"); + assertThat(brokerConfig.getBrokerId()).isEqualTo(0); + assertThat(brokerConfig.getBrokerName()).isEqualTo("broker-a"); + assertThat(brokerConfig.isAutoCreateTopicEnable()).isEqualTo(false); + assertThat(brokerConfig.isAutoDeleteUnusedStats()).isEqualTo(true); + } } \ No newline at end of file diff --git a/common/src/test/java/org/apache/rocketmq/common/ConfigManagerTest.java b/common/src/test/java/org/apache/rocketmq/common/ConfigManagerTest.java new file mode 100644 index 00000000000..a61ec4c0525 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/ConfigManagerTest.java @@ -0,0 +1,100 @@ +package org.apache.rocketmq.common;/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.File; +import java.io.PrintWriter; +import java.lang.reflect.Method; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class ConfigManagerTest { + private static final String PATH_FILE = System.getProperty("java.io.tmpdir") + File.separator + "org.apache.rocketmq.common.ConfigManagerTest"; + private static final String CONTENT_ENCODE = "Encode content for ConfigManager"; + + @Test + public void testLoad() throws Exception { + ConfigManager testConfigManager = buildTestConfigManager(); + File file = createAndWriteFile(testConfigManager.configFilePath()); + assertTrue(testConfigManager.load()); + file.delete(); + File fileBak = createAndWriteFile(testConfigManager.configFilePath() + ".bak"); + assertTrue(testConfigManager.load()); + fileBak.delete(); + } + + @Test + public void testLoadBak() throws Exception { + ConfigManager testConfigManager = buildTestConfigManager(); + File file = createAndWriteFile(testConfigManager.configFilePath() + ".bak"); + // invoke private method "loadBak()" + Method declaredMethod = ConfigManager.class.getDeclaredMethod("loadBak"); + declaredMethod.setAccessible(true); + Boolean loadBakResult = (Boolean) declaredMethod.invoke(testConfigManager); + assertTrue(loadBakResult); + file.delete(); + + Boolean loadBakResult2 = (Boolean) declaredMethod.invoke(testConfigManager); + assertTrue(loadBakResult2); + declaredMethod.setAccessible(false); + } + + @Test + public void testPersist() throws Exception { + ConfigManager testConfigManager = buildTestConfigManager(); + testConfigManager.persist(); + File file = new File(testConfigManager.configFilePath()); + assertEquals(CONTENT_ENCODE, MixAll.file2String(file)); + } + + private ConfigManager buildTestConfigManager() { + return new ConfigManager() { + @Override + public String encode() { + return encode(false); + } + + @Override + public String configFilePath() { + return PATH_FILE; + } + + @Override + public void decode(String jsonString) { + + } + + @Override + public String encode(boolean prettyFormat) { + return CONTENT_ENCODE; + } + }; + } + + private File createAndWriteFile(String fileName) throws Exception { + File file = new File(fileName); + if (file.exists()) { + file.delete(); + } + file.createNewFile(); + PrintWriter out = new PrintWriter(fileName); + out.write("TestForConfigManager"); + out.close(); + return file; + } +} \ No newline at end of file diff --git a/common/src/test/java/org/apache/rocketmq/common/CountDownLatch2Test.java b/common/src/test/java/org/apache/rocketmq/common/CountDownLatch2Test.java new file mode 100644 index 00000000000..3570db65e2f --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/CountDownLatch2Test.java @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common; + +import org.junit.Test; + +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.StringContains.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * CountDownLatch2 Unit Test + * + * @see CountDownLatch2 + */ +public class CountDownLatch2Test { + + /** + * test constructor with invalid init param + * + * @see CountDownLatch2#CountDownLatch2(int) + */ + @Test + public void testConstructorError() { + int count = -1; + try { + CountDownLatch2 latch = new CountDownLatch2(count); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage(), is("count < 0")); + } + } + + /** + * test constructor with valid init param + * + * @see CountDownLatch2#CountDownLatch2(int) + */ + @Test + public void testConstructor() { + int count = 10; + CountDownLatch2 latch = new CountDownLatch2(count); + assertEquals("Expected equal", count, latch.getCount()); + assertThat("Expected contain", latch.toString(), containsString("[Count = " + count + "]")); + } + + /** + * test await timeout + * + * @see CountDownLatch2#await(long, TimeUnit) + */ + @Test + public void testAwaitTimeout() throws InterruptedException { + int count = 1; + CountDownLatch2 latch = new CountDownLatch2(count); + boolean await = latch.await(10, TimeUnit.MILLISECONDS); + assertFalse("Expected false", await); + + latch.countDown(); + boolean await2 = latch.await(10, TimeUnit.MILLISECONDS); + assertTrue("Expected true", await2); + } + + + /** + * test reset + * + * @see CountDownLatch2#countDown() + */ + @Test(timeout = 1000) + public void testCountDownAndGetCount() throws InterruptedException { + int count = 2; + CountDownLatch2 latch = new CountDownLatch2(count); + assertEquals("Expected equal", count, latch.getCount()); + latch.countDown(); + assertEquals("Expected equal", count - 1, latch.getCount()); + latch.countDown(); + latch.await(); + assertEquals("Expected equal", 0, latch.getCount()); + } + + + /** + * test reset + * + * @see CountDownLatch2#reset() + */ + @Test + public void testReset() throws InterruptedException { + int count = 2; + CountDownLatch2 latch = new CountDownLatch2(count); + latch.countDown(); + assertEquals("Expected equal", count - 1, latch.getCount()); + latch.reset(); + assertEquals("Expected equal", count, latch.getCount()); + latch.countDown(); + latch.countDown(); + latch.await(); + assertEquals("Expected equal", 0, latch.getCount()); + // coverage Sync#tryReleaseShared, c==0 + latch.countDown(); + assertEquals("Expected equal", 0, latch.getCount()); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/KeyBuilderTest.java b/common/src/test/java/org/apache/rocketmq/common/KeyBuilderTest.java new file mode 100644 index 00000000000..47191c907fe --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/KeyBuilderTest.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class KeyBuilderTest { + String topic = "test-topic"; + String group = "test-group"; + + @Test + public void testBuildPopRetryTopic() { + assertThat(KeyBuilder.buildPopRetryTopicV2(topic, group)).isEqualTo(MixAll.RETRY_GROUP_TOPIC_PREFIX + group + "+" + topic); + } + + @Test + public void testBuildPopRetryTopicV1() { + assertThat(KeyBuilder.buildPopRetryTopicV1(topic, group)).isEqualTo(MixAll.RETRY_GROUP_TOPIC_PREFIX + group + "_" + topic); + } + + @Test + public void testParseNormalTopic() { + String popRetryTopic = KeyBuilder.buildPopRetryTopicV2(topic, group); + assertThat(KeyBuilder.parseNormalTopic(popRetryTopic, group)).isEqualTo(topic); + + String popRetryTopicV1 = KeyBuilder.buildPopRetryTopicV1(topic, group); + assertThat(KeyBuilder.parseNormalTopic(popRetryTopicV1, group)).isEqualTo(topic); + + popRetryTopic = KeyBuilder.buildPopRetryTopicV2(topic, group); + assertThat(KeyBuilder.parseNormalTopic(popRetryTopic)).isEqualTo(topic); + } + + @Test + public void testParseGroup() { + String popRetryTopic = KeyBuilder.buildPopRetryTopicV2(topic, group); + assertThat(KeyBuilder.parseGroup(popRetryTopic)).isEqualTo(group); + } + + @Test + public void testIsPopRetryTopicV2() { + String popRetryTopic = KeyBuilder.buildPopRetryTopicV2(topic, group); + assertThat(KeyBuilder.isPopRetryTopicV2(popRetryTopic)).isEqualTo(true); + String popRetryTopicV1 = KeyBuilder.buildPopRetryTopicV1(topic, group); + assertThat(KeyBuilder.isPopRetryTopicV2(popRetryTopicV1)).isEqualTo(false); + } +} \ No newline at end of file diff --git a/common/src/test/java/org/apache/rocketmq/common/MessageBatchTest.java b/common/src/test/java/org/apache/rocketmq/common/MessageBatchTest.java index f264420760b..5876cbdd881 100644 --- a/common/src/test/java/org/apache/rocketmq/common/MessageBatchTest.java +++ b/common/src/test/java/org/apache/rocketmq/common/MessageBatchTest.java @@ -26,7 +26,7 @@ public class MessageBatchTest { public List generateMessages() { - List messages = new ArrayList(); + List messages = new ArrayList<>(); Message message1 = new Message("topic1", "body".getBytes()); Message message2 = new Message("topic1", "body".getBytes()); diff --git a/common/src/test/java/org/apache/rocketmq/common/MessageEncodeDecodeTest.java b/common/src/test/java/org/apache/rocketmq/common/MessageEncodeDecodeTest.java index 42d3909d99e..7c73147e0c7 100644 --- a/common/src/test/java/org/apache/rocketmq/common/MessageEncodeDecodeTest.java +++ b/common/src/test/java/org/apache/rocketmq/common/MessageEncodeDecodeTest.java @@ -46,7 +46,7 @@ public void testEncodeDecodeSingle() throws Exception { @Test public void testEncodeDecodeList() throws Exception { - List messages = new ArrayList(128); + List messages = new ArrayList<>(128); for (int i = 0; i < 100; i++) { Message message = new Message("topic", ("body" + i).getBytes()); message.setFlag(i); diff --git a/common/src/test/java/org/apache/rocketmq/common/MessageExtBrokerInnerTest.java b/common/src/test/java/org/apache/rocketmq/common/MessageExtBrokerInnerTest.java new file mode 100644 index 00000000000..77d69e5ad76 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/MessageExtBrokerInnerTest.java @@ -0,0 +1,93 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common; + +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MessageExtBrokerInnerTest { + @Test + public void testDeleteProperty() { + MessageExtBrokerInner messageExtBrokerInner = new MessageExtBrokerInner(); + String propertiesString = ""; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo(""); + + propertiesString = "KeyA\u0001ValueA"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo(""); + + propertiesString = "KeyA\u0001ValueA\u0002"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo(""); + + propertiesString = "KeyA\u0001ValueA\u0002KeyA\u0001ValueA"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo(""); + + propertiesString = "KeyA\u0001ValueA\u0002KeyA\u0001ValueA\u0002"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo(""); + + propertiesString = "KeyB\u0001ValueB\u0002KeyA\u0001ValueA"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo("KeyB\u0001ValueB\u0002"); + + propertiesString = "KeyB\u0001ValueB\u0002KeyA\u0001ValueA\u0002"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo("KeyB\u0001ValueB\u0002"); + + propertiesString = "KeyB\u0001ValueB\u0002KeyA\u0001ValueA\u0002KeyB\u0001ValueB\u0002"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo("KeyB\u0001ValueB\u0002KeyB\u0001ValueB\u0002"); + + propertiesString = "KeyA\u0001ValueA\u0002KeyB\u0001ValueB\u0002"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo("KeyB\u0001ValueB\u0002"); + + propertiesString = "KeyA\u0001ValueA\u0002KeyB\u0001ValueB"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo("KeyB\u0001ValueB"); + + propertiesString = "KeyA\u0001ValueA\u0002KeyB\u0001ValueBKeyA\u0001ValueA\u0002"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo("KeyB\u0001ValueBKeyA\u0001ValueA\u0002"); + + propertiesString = "KeyA\u0001ValueA\u0002KeyB\u0001ValueBKeyA\u0001"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo("KeyB\u0001ValueBKeyA\u0001"); + + propertiesString = "KeyA\u0001ValueA\u0002KeyB\u0001ValueBKeyA"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo("KeyB\u0001ValueBKeyA"); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/MixAllTest.java b/common/src/test/java/org/apache/rocketmq/common/MixAllTest.java index 3f0487202f0..5b358ca8e61 100644 --- a/common/src/test/java/org/apache/rocketmq/common/MixAllTest.java +++ b/common/src/test/java/org/apache/rocketmq/common/MixAllTest.java @@ -17,14 +17,13 @@ package org.apache.rocketmq.common; -import org.junit.Test; - import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.net.InetAddress; import java.util.List; import java.util.concurrent.atomic.AtomicLong; +import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -34,7 +33,7 @@ public void testGetLocalInetAddress() throws Exception { List localInetAddress = MixAll.getLocalInetAddress(); String local = InetAddress.getLocalHost().getHostAddress(); assertThat(localInetAddress).contains("127.0.0.1"); - assertThat(localInetAddress).contains(local); + assertThat(local).isNotNull(); } @Test @@ -68,22 +67,6 @@ public void testFile2String() throws IOException { file.delete(); } - @Test - public void testFile2String_WithChinese() throws IOException { - String fileName = System.getProperty("java.io.tmpdir") + File.separator + "MixAllTest" + System.currentTimeMillis(); - File file = new File(fileName); - if (file.exists()) { - file.delete(); - } - file.createNewFile(); - PrintWriter out = new PrintWriter(fileName); - out.write("TestForMixAll_中文"); - out.close(); - String string = MixAll.file2String(fileName); - assertThat(string).isEqualTo("TestForMixAll_中文"); - file.delete(); - } - @Test public void testString2File() throws IOException { String fileName = System.getProperty("java.io.tmpdir") + File.separator + "MixAllTest" + System.currentTimeMillis(); @@ -92,8 +75,14 @@ public void testString2File() throws IOException { } @Test - public void testGetLocalhostByNetworkInterface() throws Exception { - assertThat(MixAll.LOCALHOST).isNotNull(); - assertThat(MixAll.getLocalhostByNetworkInterface()).isNotNull(); + public void testIsLmq() { + String testLmq = null; + assertThat(MixAll.isLmq(testLmq)).isFalse(); + testLmq = "lmq"; + assertThat(MixAll.isLmq(testLmq)).isFalse(); + testLmq = "%LMQ%queue123"; + assertThat(MixAll.isLmq(testLmq)).isTrue(); + testLmq = "%LMQ%GID_TEST"; + assertThat(MixAll.isLmq(testLmq)).isTrue(); } } diff --git a/common/src/test/java/org/apache/rocketmq/common/NetworkUtilTest.java b/common/src/test/java/org/apache/rocketmq/common/NetworkUtilTest.java new file mode 100644 index 00000000000..a1b225323fd --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/NetworkUtilTest.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common; + +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NetworkUtilTest { + @Test + public void testGetLocalAddress() { + String localAddress = NetworkUtil.getLocalAddress(); + assertThat(localAddress).isNotNull(); + assertThat(localAddress.length()).isGreaterThan(0); + } + + @Test + public void testConvert2IpStringWithIp() { + String result = NetworkUtil.convert2IpString("127.0.0.1:9876"); + assertThat(result).isEqualTo("127.0.0.1:9876"); + } + + @Test + public void testConvert2IpStringWithHost() { + String result = NetworkUtil.convert2IpString("localhost:9876"); + assertThat(result).isEqualTo("127.0.0.1:9876"); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/RemotingUtilTest.java b/common/src/test/java/org/apache/rocketmq/common/RemotingUtilTest.java deleted file mode 100644 index 586689637b3..00000000000 --- a/common/src/test/java/org/apache/rocketmq/common/RemotingUtilTest.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.common; - -import org.apache.rocketmq.remoting.common.RemotingUtil; -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -public class RemotingUtilTest { - @Test - public void testGetLocalAddress() throws Exception { - String localAddress = RemotingUtil.getLocalAddress(); - assertThat(localAddress).isNotNull(); - assertThat(localAddress.length()).isGreaterThan(0); - } -} diff --git a/common/src/test/java/org/apache/rocketmq/common/ServiceThreadTest.java b/common/src/test/java/org/apache/rocketmq/common/ServiceThreadTest.java new file mode 100644 index 00000000000..93208bcb7f2 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/ServiceThreadTest.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class ServiceThreadTest { + + @Test + public void testShutdown() { + shutdown(false, false); + shutdown(false, true); + shutdown(true, false); + shutdown(true, true); + } + + @Test + public void testMakeStop() { + ServiceThread testServiceThread = startTestServiceThread(); + testServiceThread.makeStop(); + assertEquals(true, testServiceThread.isStopped()); + } + + @Test + public void testWakeup() { + ServiceThread testServiceThread = startTestServiceThread(); + testServiceThread.wakeup(); + assertEquals(true, testServiceThread.hasNotified.get()); + assertEquals(0, testServiceThread.waitPoint.getCount()); + } + + @Test + public void testWaitForRunning() { + ServiceThread testServiceThread = startTestServiceThread(); + // test waitForRunning + testServiceThread.waitForRunning(1000); + assertEquals(false, testServiceThread.hasNotified.get()); + assertEquals(1, testServiceThread.waitPoint.getCount()); + // test wake up + testServiceThread.wakeup(); + assertEquals(true, testServiceThread.hasNotified.get()); + assertEquals(0, testServiceThread.waitPoint.getCount()); + // repeat waitForRunning + testServiceThread.waitForRunning(1000); + assertEquals(false, testServiceThread.hasNotified.get()); + assertEquals(0, testServiceThread.waitPoint.getCount()); + // repeat waitForRunning again + testServiceThread.waitForRunning(1000); + assertEquals(false, testServiceThread.hasNotified.get()); + assertEquals(1, testServiceThread.waitPoint.getCount()); + } + + private ServiceThread startTestServiceThread() { + return startTestServiceThread(false); + } + + private ServiceThread startTestServiceThread(boolean daemon) { + ServiceThread testServiceThread = new ServiceThread() { + + @Override + public void run() { + doNothing(); + } + + private void doNothing() {} + + @Override + public String getServiceName() { + return "TestServiceThread"; + } + }; + testServiceThread.setDaemon(daemon); + // test start + testServiceThread.start(); + assertEquals(false, testServiceThread.isStopped()); + return testServiceThread; + } + + public void shutdown(boolean daemon, boolean interrupt) { + ServiceThread testServiceThread = startTestServiceThread(daemon); + shutdown0(interrupt, testServiceThread); + // repeat + shutdown0(interrupt, testServiceThread); + } + + private void shutdown0(boolean interrupt, ServiceThread testServiceThread) { + if (interrupt) { + testServiceThread.shutdown(true); + } else { + testServiceThread.shutdown(); + } + assertEquals(true, testServiceThread.isStopped()); + assertEquals(true, testServiceThread.hasNotified.get()); + assertEquals(0, testServiceThread.waitPoint.getCount()); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/TopicConfigTest.java b/common/src/test/java/org/apache/rocketmq/common/TopicConfigTest.java new file mode 100644 index 00000000000..3df93a0bfb3 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/TopicConfigTest.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common; + +import org.apache.rocketmq.common.attribute.TopicMessageType; +import org.apache.rocketmq.common.constant.PermName; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TopicConfigTest { + String topicName = "topic"; + int queueNums = 8; + int perm = PermName.PERM_READ | PermName.PERM_WRITE; + TopicFilterType topicFilterType = TopicFilterType.SINGLE_TAG; + + @Test + public void testEncode() { + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName(topicName); + topicConfig.setReadQueueNums(queueNums); + topicConfig.setWriteQueueNums(queueNums); + topicConfig.setPerm(perm); + topicConfig.setTopicFilterType(topicFilterType); + topicConfig.setTopicMessageType(TopicMessageType.FIFO); + + String encode = topicConfig.encode(); + assertThat(encode).isEqualTo("topic 8 8 6 SINGLE_TAG {\"message.type\":\"FIFO\"}"); + } + + @Test + public void testDecode() { + String encode = "topic 8 8 6 SINGLE_TAG {\"message.type\":\"FIFO\"}"; + TopicConfig decodeTopicConfig = new TopicConfig(); + decodeTopicConfig.decode(encode); + + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName(topicName); + topicConfig.setReadQueueNums(queueNums); + topicConfig.setWriteQueueNums(queueNums); + topicConfig.setPerm(perm); + topicConfig.setTopicFilterType(topicFilterType); + topicConfig.setTopicMessageType(TopicMessageType.FIFO); + + assertThat(decodeTopicConfig).isEqualTo(topicConfig); + } + + @Test + public void testDecodeWhenCompatible() { + String encode = "topic 8 8 6 SINGLE_TAG"; + TopicConfig decodeTopicConfig = new TopicConfig(); + decodeTopicConfig.decode(encode); + + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName(topicName); + topicConfig.setReadQueueNums(queueNums); + topicConfig.setWriteQueueNums(queueNums); + topicConfig.setPerm(perm); + topicConfig.setTopicFilterType(topicFilterType); + + assertThat(decodeTopicConfig).isEqualTo(topicConfig); + } +} \ No newline at end of file diff --git a/common/src/test/java/org/apache/rocketmq/common/UtilAllTest.java b/common/src/test/java/org/apache/rocketmq/common/UtilAllTest.java index 265645213e4..a2b498f0770 100644 --- a/common/src/test/java/org/apache/rocketmq/common/UtilAllTest.java +++ b/common/src/test/java/org/apache/rocketmq/common/UtilAllTest.java @@ -17,13 +17,28 @@ package org.apache.rocketmq.common; +import java.io.File; +import java.io.FileOutputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Properties; + +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.within; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; public class UtilAllTest { + @Rule + public TemporaryFolder tempDir = new TemporaryFolder(); @Test public void testCurrentStackTrace() { @@ -49,16 +64,26 @@ public void testProperties2Object() { @Test public void testProperties2String() { - DemoConfig demoConfig = new DemoConfig(); + DemoSubConfig demoConfig = new DemoSubConfig(); demoConfig.setDemoLength(123); demoConfig.setDemoWidth(456); demoConfig.setDemoName("TestDemo"); demoConfig.setDemoOK(true); + + demoConfig.setSubField0("1"); + demoConfig.setSubField1(false); + Properties properties = MixAll.object2Properties(demoConfig); assertThat(properties.getProperty("demoLength")).isEqualTo("123"); assertThat(properties.getProperty("demoWidth")).isEqualTo("456"); assertThat(properties.getProperty("demoOK")).isEqualTo("true"); assertThat(properties.getProperty("demoName")).isEqualTo("TestDemo"); + + assertThat(properties.getProperty("subField0")).isEqualTo("1"); + assertThat(properties.getProperty("subField1")).isEqualTo("false"); + + properties = MixAll.object2Properties(new Object()); + assertEquals(0, properties.size()); } @Test @@ -98,6 +123,34 @@ public void testIsBlank() { assertThat(UtilAll.isBlank("Hello")).isFalse(); } + @Test + public void testIPv6Check() throws UnknownHostException { + InetAddress nonInternal = InetAddress.getByName("2408:4004:0180:8100:3FAA:1DDE:2B3F:898A"); + InetAddress internal = InetAddress.getByName("FE80:0000:0000:0000:0000:0000:0000:FFFF"); + assertThat(UtilAll.isInternalV6IP(nonInternal)).isFalse(); + assertThat(UtilAll.isInternalV6IP(internal)).isTrue(); + assertThat(UtilAll.ipToIPv6Str(nonInternal.getAddress()).toUpperCase()).isEqualTo("2408:4004:0180:8100:3FAA:1DDE:2B3F:898A"); + } + + @Test + public void testJoin() { + List list = Arrays.asList("groupA=DENY", "groupB=PUB|SUB", "groupC=SUB"); + String comma = ","; + assertEquals("groupA=DENY,groupB=PUB|SUB,groupC=SUB", UtilAll.join(list, comma)); + assertEquals(null, UtilAll.join(null, comma)); + List objects = Collections.emptyList(); + assertEquals("", UtilAll.join(objects, comma)); + } + + @Test + public void testSplit() { + List list = Arrays.asList("groupA=DENY", "groupB=PUB|SUB", "groupC=SUB"); + String comma = ","; + assertEquals(list, UtilAll.split("groupA=DENY,groupB=PUB|SUB,groupC=SUB", comma)); + assertEquals(null, UtilAll.split(null, comma)); + assertEquals(Collections.EMPTY_LIST, UtilAll.split("", comma)); + } + static class DemoConfig { private int demoWidth = 0; private int demoLength = 0; @@ -146,4 +199,85 @@ public String toString() { '}'; } } + + static class DemoSubConfig extends DemoConfig { + private String subField0 = "0"; + public boolean subField1 = true; + + public String getSubField0() { + return subField0; + } + + public void setSubField0(String subField0) { + this.subField0 = subField0; + } + + public boolean isSubField1() { + return subField1; + } + + public void setSubField1(boolean subField1) { + this.subField1 = subField1; + } + } + + @Test + public void testCleanBuffer() { + UtilAll.cleanBuffer(null); + UtilAll.cleanBuffer(ByteBuffer.allocateDirect(10)); + UtilAll.cleanBuffer(ByteBuffer.allocateDirect(0)); + UtilAll.cleanBuffer(ByteBuffer.allocate(10)); + } + + @Test + public void testCalculateFileSizeInPath() throws Exception { + /** + * testCalculateFileSizeInPath + * - file_0 + * - dir_1 + * - file_1_0 + * - file_1_1 + * - dir_1_2 + * - file_1_2_0 + * - dir_2 + */ + File baseFile = tempDir.getRoot(); + + // test empty path + assertEquals(0, UtilAll.calculateFileSizeInPath(baseFile)); + + File file0 = new File(baseFile, "file_0"); + assertTrue(file0.createNewFile()); + writeFixedBytesToFile(file0, 1313); + + assertEquals(1313, UtilAll.calculateFileSizeInPath(baseFile)); + + // build a file tree like above + File dir1 = new File(baseFile, "dir_1"); + dir1.mkdirs(); + File file10 = new File(dir1, "file_1_0"); + File file11 = new File(dir1, "file_1_1"); + File dir12 = new File(dir1, "dir_1_2"); + dir12.mkdirs(); + File file120 = new File(dir12, "file_1_2_0"); + File dir2 = new File(baseFile, "dir_2"); + dir2.mkdirs(); + + // write all file with 1313 bytes data + assertTrue(file10.createNewFile()); + writeFixedBytesToFile(file10, 1313); + assertTrue(file11.createNewFile()); + writeFixedBytesToFile(file11, 1313); + assertTrue(file120.createNewFile()); + writeFixedBytesToFile(file120, 1313); + + assertEquals(1313 * 4, UtilAll.calculateFileSizeInPath(baseFile)); + } + + private void writeFixedBytesToFile(File file, int size) throws Exception { + FileOutputStream outputStream = new FileOutputStream(file); + byte[] bytes = new byte[size]; + outputStream.write(bytes, 0, size); + outputStream.close(); + } } diff --git a/common/src/test/java/org/apache/rocketmq/common/attribute/AttributeParserTest.java b/common/src/test/java/org/apache/rocketmq/common/attribute/AttributeParserTest.java new file mode 100644 index 00000000000..12398100bec --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/attribute/AttributeParserTest.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.attribute; + +import com.google.common.collect.Maps; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.google.common.collect.Maps.newHashMap; +import static org.junit.Assert.assertTrue; + +public class AttributeParserTest { + @Test + public void testParseToMap() { + Assert.assertEquals(0, AttributeParser.parseToMap(null).size()); + AttributeParser.parseToMap("++=++"); + AttributeParser.parseToMap("--"); + Assert.assertThrows(RuntimeException.class, () -> AttributeParser.parseToMap("x")); + Assert.assertThrows(RuntimeException.class, () -> AttributeParser.parseToMap("+")); + Assert.assertThrows(RuntimeException.class, () -> AttributeParser.parseToMap("++")); + } + + @Test + public void testParseToString() { + Assert.assertEquals("", AttributeParser.parseToString(null)); + Assert.assertEquals("", AttributeParser.parseToString(newHashMap())); + HashMap map = new HashMap<>(); + int addSize = 10; + for (int i = 0; i < addSize; i++) { + map.put("+add.key" + i, "value" + i); + } + int deleteSize = 10; + for (int i = 0; i < deleteSize; i++) { + map.put("-delete.key" + i, ""); + } + Assert.assertEquals(addSize + deleteSize, AttributeParser.parseToString(map).split(",").length); + } + + @Test + public void testParseBetweenStringAndMapWithoutDistortion() { + List testCases = Arrays.asList("-a", "+a=b,+c=d,+z=z,+e=e", "+a=b,-d", "+a=b", "-a,-b"); + for (String testCase : testCases) { + assertTrue(Maps.difference(AttributeParser.parseToMap(testCase), AttributeParser.parseToMap(parse(testCase))).areEqual()); + } + } + + private String parse(String original) { + Map stringStringMap = AttributeParser.parseToMap(original); + return AttributeParser.parseToString(stringStringMap); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/attribute/AttributeTest.java b/common/src/test/java/org/apache/rocketmq/common/attribute/AttributeTest.java new file mode 100644 index 00000000000..39a12b97ef4 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/attribute/AttributeTest.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.attribute; + +import org.junit.Assert; +import org.junit.Test; + +import static com.google.common.collect.Sets.newHashSet; + +public class AttributeTest { + + @Test + public void testEnumAttribute() { + EnumAttribute enumAttribute = new EnumAttribute("enum.key", true, newHashSet("enum-1", "enum-2", "enum-3"), "enum-1"); + + Assert.assertThrows(RuntimeException.class, () -> enumAttribute.verify("")); + Assert.assertThrows(RuntimeException.class, () -> enumAttribute.verify("x")); + Assert.assertThrows(RuntimeException.class, () -> enumAttribute.verify("enum-4")); + + enumAttribute.verify("enum-1"); + enumAttribute.verify("enum-2"); + enumAttribute.verify("enum-3"); + } + + @Test + public void testLongRangeAttribute() { + LongRangeAttribute longRangeAttribute = new LongRangeAttribute("long.range.key", true, 10, 20, 15); + Assert.assertThrows(RuntimeException.class, () -> longRangeAttribute.verify("")); + Assert.assertThrows(RuntimeException.class, () -> longRangeAttribute.verify(",")); + Assert.assertThrows(RuntimeException.class, () -> longRangeAttribute.verify("a")); + Assert.assertThrows(RuntimeException.class, () -> longRangeAttribute.verify("-1")); + Assert.assertThrows(RuntimeException.class, () -> longRangeAttribute.verify("21")); + + longRangeAttribute.verify("11"); + longRangeAttribute.verify("10"); + longRangeAttribute.verify("20"); + } + + @Test + public void testBooleanAttribute() { + BooleanAttribute booleanAttribute = new BooleanAttribute("bool.key", false, false); + + Assert.assertThrows(RuntimeException.class, () -> booleanAttribute.verify("")); + Assert.assertThrows(RuntimeException.class, () -> booleanAttribute.verify("a")); + Assert.assertThrows(RuntimeException.class, () -> booleanAttribute.verify(",")); + Assert.assertThrows(RuntimeException.class, () -> booleanAttribute.verify("checked")); + Assert.assertThrows(RuntimeException.class, () -> booleanAttribute.verify("1")); + Assert.assertThrows(RuntimeException.class, () -> booleanAttribute.verify("0")); + Assert.assertThrows(RuntimeException.class, () -> booleanAttribute.verify("-1")); + + booleanAttribute.verify("true"); + booleanAttribute.verify("tRue"); + booleanAttribute.verify("false"); + booleanAttribute.verify("falSe"); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/compression/CompressionTest.java b/common/src/test/java/org/apache/rocketmq/common/compression/CompressionTest.java new file mode 100644 index 00000000000..8110cfb6e38 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/compression/CompressionTest.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.compression; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Random; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CompressionTest { + + private int level; + private Compressor zstd; + private Compressor zlib; + private Compressor lz4; + + @Before + public void setUp() { + level = 5; + zstd = CompressorFactory.getCompressor(CompressionType.ZSTD); + zlib = CompressorFactory.getCompressor(CompressionType.ZLIB); + lz4 = CompressorFactory.getCompressor(CompressionType.LZ4); + } + + @Test + public void testCompressionZlib() throws IOException { + assertThat(CompressionType.of("zlib")).isEqualTo(CompressionType.ZLIB); + assertThat(CompressionType.of(" ZLiB ")).isEqualTo(CompressionType.ZLIB); + assertThat(CompressionType.of("ZLIB")).isEqualTo(CompressionType.ZLIB); + + int randomKB = 4096; + Random random = new Random(); + for (int i = 0; i < 5; ++i) { + String message = RandomStringUtils.randomAlphanumeric(random.nextInt(randomKB) * 1024); + byte[] srcBytes = message.getBytes(StandardCharsets.UTF_8); + byte[] compressed = zlib.compress(srcBytes, level); + byte[] decompressed = zlib.decompress(compressed); + // compression ratio may be negative for some random string data + // assertThat(compressed.length).isLessThan(srcBytes.length); + assertThat(decompressed).isEqualTo(srcBytes); + assertThat(new String(decompressed)).isEqualTo(message); + } + } + + @Test + public void testCompressionZstd() throws IOException { + assertThat(CompressionType.of("zstd")).isEqualTo(CompressionType.ZSTD); + assertThat(CompressionType.of("ZStd ")).isEqualTo(CompressionType.ZSTD); + assertThat(CompressionType.of("ZSTD")).isEqualTo(CompressionType.ZSTD); + + int randomKB = 4096; + Random random = new Random(); + for (int i = 0; i < 5; ++i) { + String message = RandomStringUtils.randomAlphanumeric(random.nextInt(randomKB) * 1024); + byte[] srcBytes = message.getBytes(StandardCharsets.UTF_8); + byte[] compressed = zstd.compress(srcBytes, level); + byte[] decompressed = zstd.decompress(compressed); + // compression ratio may be negative for some random string data + // assertThat(compressed.length).isLessThan(srcBytes.length); + assertThat(decompressed).isEqualTo(srcBytes); + assertThat(new String(decompressed)).isEqualTo(message); + } + } + + @Test + public void testCompressionLz4() throws IOException { + assertThat(CompressionType.of("lz4")).isEqualTo(CompressionType.LZ4); + + int randomKB = 4096; + Random random = new Random(); + for (int i = 0; i < 5; ++i) { + String message = RandomStringUtils.randomAlphanumeric(random.nextInt(randomKB) * 1024); + byte[] srcBytes = message.getBytes(StandardCharsets.UTF_8); + byte[] compressed = lz4.compress(srcBytes, level); + byte[] decompressed = lz4.decompress(compressed); + // compression ratio may be negative for some random string data + // assertThat(compressed.length).isLessThan(srcBytes.length); + assertThat(decompressed).isEqualTo(srcBytes); + assertThat(new String(decompressed)).isEqualTo(message); + } + } + + @Test(expected = RuntimeException.class) + public void testCompressionUnsupportedType() { + CompressionType.of("snappy"); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/message/MessageClientIDSetterTest.java b/common/src/test/java/org/apache/rocketmq/common/message/MessageClientIDSetterTest.java new file mode 100644 index 00000000000..22e6f3c27a1 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/message/MessageClientIDSetterTest.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.message; + +import org.apache.rocketmq.common.UtilAll; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MessageClientIDSetterTest { + + @Test + public void testGetTimeFromID() { + long t = System.currentTimeMillis(); + String uniqID = MessageClientIDSetter.createUniqID(); + long t2 = MessageClientIDSetter.getNearlyTimeFromID(uniqID).getTime(); + assertThat(t2 - t < 20).isTrue(); + } + + @Test + public void testGetCountFromID() { + String uniqID = MessageClientIDSetter.createUniqID(); + String uniqID2 = MessageClientIDSetter.createUniqID(); + String idHex = uniqID.substring(uniqID.length() - 4); + String idHex2 = uniqID2.substring(uniqID2.length() - 4); + int s1 = Integer.parseInt(idHex, 16); + int s2 = Integer.parseInt(idHex2, 16); + assertThat(s1 == s2 - 1).isTrue(); + } + + + @Test + public void testGetIPStrFromID() { + byte[] ip = UtilAll.getIP(); + String ipStr = (4 == ip.length) ? UtilAll.ipToIPv4Str(ip) : UtilAll.ipToIPv6Str(ip); + + String uniqID = MessageClientIDSetter.createUniqID(); + String ipStrFromID = MessageClientIDSetter.getIPStrFromID(uniqID); + + assertThat(ipStr).isEqualTo(ipStrFromID); + } + + + @Test + public void testGetPidFromID() { + // Temporary fix on MacOS + short pid = (short) UtilAll.getPid(); + + String uniqID = MessageClientIDSetter.createUniqID(); + short pidFromID = (short) MessageClientIDSetter.getPidFromID(uniqID); + + assertThat(pid).isEqualTo(pidFromID); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/message/MessageDecoderTest.java b/common/src/test/java/org/apache/rocketmq/common/message/MessageDecoderTest.java index d14d6b060c8..39bfbf5fb3f 100644 --- a/common/src/test/java/org/apache/rocketmq/common/message/MessageDecoderTest.java +++ b/common/src/test/java/org/apache/rocketmq/common/message/MessageDecoderTest.java @@ -17,6 +17,8 @@ package org.apache.rocketmq.common.message; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; import org.junit.Test; import java.net.InetAddress; @@ -25,6 +27,10 @@ import java.nio.ByteBuffer; import java.util.Map; +import static org.apache.rocketmq.common.message.MessageDecoder.NAME_VALUE_SEPARATOR; +import static org.apache.rocketmq.common.message.MessageDecoder.PROPERTY_SEPARATOR; +import static org.apache.rocketmq.common.message.MessageDecoder.createMessageId; +import static org.apache.rocketmq.common.message.MessageDecoder.decodeMessageId; import static org.assertj.core.api.Assertions.assertThat; public class MessageDecoderTest { @@ -59,6 +65,79 @@ public void testDecodeProperties() { messageExt.putUserProperty("b", "hello"); messageExt.putUserProperty("c", "3.14"); + { + byte[] msgBytes = new byte[0]; + try { + msgBytes = MessageDecoder.encode(messageExt, false); + } catch (Exception e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + + ByteBuffer byteBuffer = ByteBuffer.allocate(msgBytes.length); + byteBuffer.put(msgBytes); + + Map properties = MessageDecoder.decodeProperties(byteBuffer); + + assertThat(properties).isNotNull(); + assertThat("123").isEqualTo(properties.get("a")); + assertThat("hello").isEqualTo(properties.get("b")); + assertThat("3.14").isEqualTo(properties.get("c")); + } + + { + byte[] msgBytes = new byte[0]; + try { + msgBytes = MessageDecoder.encode(messageExt, false); + } catch (Exception e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + + ByteBuffer byteBuffer = ByteBuffer.allocate(msgBytes.length); + byteBuffer.put(msgBytes); + + Map properties = MessageDecoder.decodeProperties(byteBuffer); + + assertThat(properties).isNotNull(); + assertThat("123").isEqualTo(properties.get("a")); + assertThat("hello").isEqualTo(properties.get("b")); + assertThat("3.14").isEqualTo(properties.get("c")); + } + } + + @Test + public void testDecodePropertiesOnIPv6Host() { + MessageExt messageExt = new MessageExt(); + + messageExt.setMsgId("24084004018081003FAA1DDE2B3F898A00002A9F0000000000000CA0"); + messageExt.setBornHostV6Flag(); + messageExt.setStoreHostAddressV6Flag(); + messageExt.setTopic("abc"); + messageExt.setBody("hello!q!".getBytes()); + try { + messageExt.setBornHost(new InetSocketAddress(InetAddress.getByName("1050:0000:0000:0000:0005:0600:300c:326b"), 0)); + } catch (UnknownHostException e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + messageExt.setBornTimestamp(System.currentTimeMillis()); + messageExt.setCommitLogOffset(123456); + messageExt.setPreparedTransactionOffset(0); + messageExt.setQueueId(0); + messageExt.setQueueOffset(123); + messageExt.setReconsumeTimes(0); + try { + messageExt.setStoreHost(new InetSocketAddress(InetAddress.getByName("::1"), 0)); + } catch (UnknownHostException e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + + messageExt.putUserProperty("a", "123"); + messageExt.putUserProperty("b", "hello"); + messageExt.putUserProperty("c", "3.14"); + byte[] msgBytes = new byte[0]; try { msgBytes = MessageDecoder.encode(messageExt, false); @@ -77,4 +156,276 @@ public void testDecodeProperties() { assertThat("hello").isEqualTo(properties.get("b")); assertThat("3.14").isEqualTo(properties.get("c")); } -} + + @Test + public void testEncodeAndDecode() { + MessageExt messageExt = new MessageExt(); + + messageExt.setMsgId("645100FA00002A9F000000489A3AA09E"); + messageExt.setTopic("abc"); + messageExt.setBody("hello!q!".getBytes()); + try { + messageExt.setBornHost(new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0)); + } catch (UnknownHostException e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + messageExt.setBornTimestamp(System.currentTimeMillis()); + messageExt.setCommitLogOffset(123456); + messageExt.setPreparedTransactionOffset(0); + messageExt.setQueueId(1); + messageExt.setQueueOffset(123); + messageExt.setReconsumeTimes(0); + try { + messageExt.setStoreHost(new InetSocketAddress(InetAddress.getLocalHost(), 0)); + } catch (UnknownHostException e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + + messageExt.putUserProperty("a", "123"); + messageExt.putUserProperty("b", "hello"); + messageExt.putUserProperty("c", "3.14"); + + messageExt.setBodyCRC(UtilAll.crc32(messageExt.getBody())); + + byte[] msgBytes = new byte[0]; + try { + msgBytes = MessageDecoder.encode(messageExt, false); + } catch (Exception e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + + ByteBuffer byteBuffer = ByteBuffer.allocate(msgBytes.length); + byteBuffer.put(msgBytes); + + byteBuffer.flip(); + MessageExt decodedMsg = MessageDecoder.decode(byteBuffer); + + assertThat(decodedMsg).isNotNull(); + assertThat(1).isEqualTo(decodedMsg.getQueueId()); + assertThat(123456L).isEqualTo(decodedMsg.getCommitLogOffset()); + assertThat("hello!q!".getBytes()).isEqualTo(decodedMsg.getBody()); + + int msgIDLength = 4 + 4 + 8; + ByteBuffer byteBufferMsgId = ByteBuffer.allocate(msgIDLength); + String msgId = createMessageId(byteBufferMsgId, messageExt.getStoreHostBytes(), messageExt.getCommitLogOffset()); + assertThat(msgId).isEqualTo(decodedMsg.getMsgId()); + + assertThat("abc").isEqualTo(decodedMsg.getTopic()); + } + + @Test + public void testEncodeAndDecodeOnIPv6Host() { + MessageExt messageExt = new MessageExt(); + + messageExt.setMsgId("24084004018081003FAA1DDE2B3F898A00002A9F0000000000000CA0"); + messageExt.setBornHostV6Flag(); + messageExt.setStoreHostAddressV6Flag(); + messageExt.setTopic("abc"); + messageExt.setBody("hello!q!".getBytes()); + try { + messageExt.setBornHost(new InetSocketAddress(InetAddress.getByName("1050:0000:0000:0000:0005:0600:300c:326b"), 0)); + } catch (UnknownHostException e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + messageExt.setBornTimestamp(System.currentTimeMillis()); + messageExt.setCommitLogOffset(123456); + messageExt.setPreparedTransactionOffset(0); + messageExt.setQueueId(1); + messageExt.setQueueOffset(123); + messageExt.setReconsumeTimes(0); + try { + messageExt.setStoreHost(new InetSocketAddress(InetAddress.getByName("::1"), 0)); + } catch (UnknownHostException e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + + messageExt.putUserProperty("a", "123"); + messageExt.putUserProperty("b", "hello"); + messageExt.putUserProperty("c", "3.14"); + + messageExt.setBodyCRC(UtilAll.crc32(messageExt.getBody())); + + byte[] msgBytes = new byte[0]; + try { + msgBytes = MessageDecoder.encode(messageExt, false); + } catch (Exception e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + + ByteBuffer byteBuffer = ByteBuffer.allocate(msgBytes.length); + byteBuffer.put(msgBytes); + + byteBuffer.flip(); + MessageExt decodedMsg = MessageDecoder.decode(byteBuffer); + + assertThat(decodedMsg).isNotNull(); + assertThat(1).isEqualTo(decodedMsg.getQueueId()); + assertThat(123456L).isEqualTo(decodedMsg.getCommitLogOffset()); + assertThat("hello!q!".getBytes()).isEqualTo(decodedMsg.getBody()); + // assertThat(48).isEqualTo(decodedMsg.getSysFlag()); + assertThat(MessageSysFlag.check(messageExt.getSysFlag(), MessageSysFlag.STOREHOSTADDRESS_V6_FLAG)).isTrue(); + + int msgIDLength = 16 + 4 + 8; + ByteBuffer byteBufferMsgId = ByteBuffer.allocate(msgIDLength); + String msgId = createMessageId(byteBufferMsgId, messageExt.getStoreHostBytes(), messageExt.getCommitLogOffset()); + assertThat(msgId).isEqualTo(decodedMsg.getMsgId()); + + assertThat("abc").isEqualTo(decodedMsg.getTopic()); + } + + @Test + public void testNullValueProperty() throws Exception { + MessageExt msg = new MessageExt(); + msg.setBody("x".getBytes()); + msg.setTopic("x"); + msg.setBornHost(new InetSocketAddress("127.0.0.1", 9000)); + msg.setStoreHost(new InetSocketAddress("127.0.0.1", 9000)); + String key = "NullValueKey"; + msg.putProperty(key, null); + try { + byte[] encode = MessageDecoder.encode(msg, false); + MessageExt decode = MessageDecoder.decode(ByteBuffer.wrap(encode)); + assertThat(decode.getProperty(key)).isNull(); + } catch (Exception e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + } + + @Test + public void testString2messageProperties() { + StringBuilder sb = new StringBuilder(); + sb.append("k1").append(NAME_VALUE_SEPARATOR).append("v1"); + Map m = MessageDecoder.string2messageProperties(sb.toString()); + assertThat(m).size().isEqualTo(1); + assertThat(m.get("k1")).isEqualTo("v1"); + + m = MessageDecoder.string2messageProperties(""); + assertThat(m).size().isEqualTo(0); + + m = MessageDecoder.string2messageProperties(" "); + assertThat(m).size().isEqualTo(0); + + m = MessageDecoder.string2messageProperties("aaa"); + assertThat(m).size().isEqualTo(0); + + sb.setLength(0); + sb.append("k1").append(NAME_VALUE_SEPARATOR); + m = MessageDecoder.string2messageProperties(sb.toString()); + assertThat(m).size().isEqualTo(0); + + sb.setLength(0); + sb.append(NAME_VALUE_SEPARATOR).append("v1"); + m = MessageDecoder.string2messageProperties(sb.toString()); + assertThat(m).size().isEqualTo(0); + + sb.setLength(0); + sb.append("k1").append(NAME_VALUE_SEPARATOR).append("v1").append(PROPERTY_SEPARATOR); + m = MessageDecoder.string2messageProperties(sb.toString()); + assertThat(m).size().isEqualTo(1); + assertThat(m.get("k1")).isEqualTo("v1"); + + sb.setLength(0); + sb.append("k1").append(NAME_VALUE_SEPARATOR).append("v1").append(PROPERTY_SEPARATOR) + .append("k2").append(NAME_VALUE_SEPARATOR).append("v2"); + m = MessageDecoder.string2messageProperties(sb.toString()); + assertThat(m).size().isEqualTo(2); + assertThat(m.get("k1")).isEqualTo("v1"); + assertThat(m.get("k2")).isEqualTo("v2"); + + sb.setLength(0); + sb.append("k1").append(NAME_VALUE_SEPARATOR).append("v1").append(PROPERTY_SEPARATOR) + .append(NAME_VALUE_SEPARATOR).append("v2"); + m = MessageDecoder.string2messageProperties(sb.toString()); + assertThat(m).size().isEqualTo(1); + assertThat(m.get("k1")).isEqualTo("v1"); + + sb.setLength(0); + sb.append("k1").append(NAME_VALUE_SEPARATOR).append("v1").append(PROPERTY_SEPARATOR) + .append("k2").append(NAME_VALUE_SEPARATOR); + m = MessageDecoder.string2messageProperties(sb.toString()); + assertThat(m).size().isEqualTo(1); + assertThat(m.get("k1")).isEqualTo("v1"); + + sb.setLength(0); + sb.append(NAME_VALUE_SEPARATOR).append("v1").append(PROPERTY_SEPARATOR) + .append("k2").append(NAME_VALUE_SEPARATOR).append("v2"); + m = MessageDecoder.string2messageProperties(sb.toString()); + assertThat(m).size().isEqualTo(1); + assertThat(m.get("k2")).isEqualTo("v2"); + + sb.setLength(0); + sb.append("k1").append(NAME_VALUE_SEPARATOR).append(PROPERTY_SEPARATOR) + .append("k2").append(NAME_VALUE_SEPARATOR).append("v2"); + m = MessageDecoder.string2messageProperties(sb.toString()); + assertThat(m).size().isEqualTo(1); + assertThat(m.get("k2")).isEqualTo("v2"); + + sb.setLength(0); + sb.append("1").append(NAME_VALUE_SEPARATOR).append("1").append(PROPERTY_SEPARATOR) + .append("2").append(NAME_VALUE_SEPARATOR).append("2"); + m = MessageDecoder.string2messageProperties(sb.toString()); + assertThat(m).size().isEqualTo(2); + assertThat(m.get("1")).isEqualTo("1"); + assertThat(m.get("2")).isEqualTo("2"); + + sb.setLength(0); + sb.append("1").append(NAME_VALUE_SEPARATOR).append(PROPERTY_SEPARATOR) + .append("2").append(NAME_VALUE_SEPARATOR).append("2"); + m = MessageDecoder.string2messageProperties(sb.toString()); + assertThat(m).size().isEqualTo(1); + assertThat(m.get("2")).isEqualTo("2"); + + sb.setLength(0); + sb.append(NAME_VALUE_SEPARATOR).append("1").append(PROPERTY_SEPARATOR) + .append("2").append(NAME_VALUE_SEPARATOR).append("2"); + m = MessageDecoder.string2messageProperties(sb.toString()); + assertThat(m).size().isEqualTo(1); + assertThat(m.get("2")).isEqualTo("2"); + + sb.setLength(0); + sb.append("1").append(NAME_VALUE_SEPARATOR).append("1").append(PROPERTY_SEPARATOR) + .append("2").append(NAME_VALUE_SEPARATOR); + m = MessageDecoder.string2messageProperties(sb.toString()); + assertThat(m).size().isEqualTo(1); + assertThat(m.get("1")).isEqualTo("1"); + + sb.setLength(0); + sb.append("1").append(NAME_VALUE_SEPARATOR).append("1").append(PROPERTY_SEPARATOR) + .append(NAME_VALUE_SEPARATOR).append("2"); + m = MessageDecoder.string2messageProperties(sb.toString()); + assertThat(m).size().isEqualTo(1); + assertThat(m.get("1")).isEqualTo("1"); + } + + @Test + public void testMessageId() throws Exception { + // ipv4 messageId test + MessageExt msgExt = new MessageExt(); + msgExt.setStoreHost(new InetSocketAddress("127.0.0.1", 9103)); + msgExt.setCommitLogOffset(123456); + verifyMessageId(msgExt); + + // ipv6 messageId test + msgExt.setStoreHostAddressV6Flag(); + msgExt.setStoreHost(new InetSocketAddress(InetAddress.getByName("::1"), 0)); + verifyMessageId(msgExt); + } + + private void verifyMessageId(MessageExt msgExt) throws UnknownHostException { + int storehostIPLength = (msgExt.getSysFlag() & MessageSysFlag.STOREHOSTADDRESS_V6_FLAG) == 0 ? 4 : 16; + int msgIDLength = storehostIPLength + 4 + 8; + ByteBuffer byteBufferMsgId = ByteBuffer.allocate(msgIDLength); + String msgId = createMessageId(byteBufferMsgId, msgExt.getStoreHostBytes(), msgExt.getCommitLogOffset()); + + MessageId messageId = decodeMessageId(msgId); + assertThat(messageId.getAddress()).isEqualTo(msgExt.getStoreHost()); + assertThat(messageId.getOffset()).isEqualTo(msgExt.getCommitLogOffset()); + } +} \ No newline at end of file diff --git a/common/src/test/java/org/apache/rocketmq/common/message/MessageTest.java b/common/src/test/java/org/apache/rocketmq/common/message/MessageTest.java index c867360f85b..c950970980b 100644 --- a/common/src/test/java/org/apache/rocketmq/common/message/MessageTest.java +++ b/common/src/test/java/org/apache/rocketmq/common/message/MessageTest.java @@ -20,7 +20,6 @@ import org.junit.Test; import static org.apache.rocketmq.common.message.MessageConst.PROPERTY_TRACE_SWITCH; -import static org.junit.Assert.*; public class MessageTest { @Test(expected = RuntimeException.class) diff --git a/common/src/test/java/org/apache/rocketmq/common/stats/StatsItemSetTest.java b/common/src/test/java/org/apache/rocketmq/common/stats/StatsItemSetTest.java new file mode 100644 index 00000000000..b02fb60ae49 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/stats/StatsItemSetTest.java @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.stats; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.LongAdder; + +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.junit.After; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class StatsItemSetTest { + + private ThreadPoolExecutor executor; + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + + @Test + public void test_getAndCreateStatsItem_multiThread() throws InterruptedException { + assertEquals(20L, test_unit().longValue()); + } + + @Test + public void test_getAndCreateMomentStatsItem_multiThread() throws InterruptedException { + assertEquals(10, test_unit_moment().longValue()); + } + + @Test + public void test_statsOfFirstStatisticsCycle() throws InterruptedException { + final String tpsStatKey = "tpsTest"; + final String rtStatKey = "rtTest"; + final StatsItemSet statsItemSet = new StatsItemSet(tpsStatKey, scheduler, null); + executor = new ThreadPoolExecutor(10, 20, 10, TimeUnit.SECONDS, + new ArrayBlockingQueue<>(100), new ThreadFactoryImpl("testMultiThread")); + for (int i = 0; i < 10; i++) { + executor.submit(new Runnable() { + @Override + public void run() { + statsItemSet.addValue(tpsStatKey, 2, 1); + statsItemSet.addRTValue(rtStatKey, 2, 1); + } + }); + } + while (true) { + if (executor.getCompletedTaskCount() == 10) { + break; + } + Thread.sleep(1000); + } + // simulate schedule task execution , tps stat + { + statsItemSet.getStatsItem(tpsStatKey).samplingInSeconds(); + statsItemSet.getStatsItem(tpsStatKey).samplingInMinutes(); + statsItemSet.getStatsItem(tpsStatKey).samplingInHour(); + + assertEquals(20L, statsItemSet.getStatsDataInMinute(tpsStatKey).getSum()); + assertEquals(20L, statsItemSet.getStatsDataInHour(tpsStatKey).getSum()); + assertEquals(20L, statsItemSet.getStatsDataInDay(tpsStatKey).getSum()); + assertEquals(10L, statsItemSet.getStatsDataInDay(tpsStatKey).getTimes()); + assertEquals(10L, statsItemSet.getStatsDataInHour(tpsStatKey).getTimes()); + assertEquals(10L, statsItemSet.getStatsDataInDay(tpsStatKey).getTimes()); + } + + // simulate schedule task execution , rt stat + { + statsItemSet.getStatsItem(rtStatKey).samplingInSeconds(); + statsItemSet.getStatsItem(rtStatKey).samplingInMinutes(); + statsItemSet.getStatsItem(rtStatKey).samplingInHour(); + + assertEquals(20L, statsItemSet.getStatsDataInMinute(rtStatKey).getSum()); + assertEquals(20L, statsItemSet.getStatsDataInHour(rtStatKey).getSum()); + assertEquals(20L, statsItemSet.getStatsDataInDay(rtStatKey).getSum()); + assertEquals(10L, statsItemSet.getStatsDataInDay(rtStatKey).getTimes()); + assertEquals(10L, statsItemSet.getStatsDataInHour(rtStatKey).getTimes()); + assertEquals(10L, statsItemSet.getStatsDataInDay(rtStatKey).getTimes()); + } + } + + private LongAdder test_unit() throws InterruptedException { + final StatsItemSet statsItemSet = new StatsItemSet("topicTest", scheduler, null); + executor = new ThreadPoolExecutor(10, 20, 10, TimeUnit.SECONDS, + new ArrayBlockingQueue<>(100), new ThreadFactoryImpl("testMultiThread")); + for (int i = 0; i < 10; i++) { + executor.submit(new Runnable() { + @Override + public void run() { + statsItemSet.addValue("topicTest", 2, 1); + } + }); + } + while (true) { + if (executor.getCompletedTaskCount() == 10) { + break; + } + Thread.sleep(1000); + } + return statsItemSet.getStatsItem("topicTest").getValue(); + } + + private AtomicLong test_unit_moment() throws InterruptedException { + final MomentStatsItemSet statsItemSet = new MomentStatsItemSet("topicTest", scheduler, null); + executor = new ThreadPoolExecutor(10, 20, 10, TimeUnit.SECONDS, + new ArrayBlockingQueue<>(100), new ThreadFactoryImpl("testMultiThread")); + for (int i = 0; i < 10; i++) { + executor.submit(new Runnable() { + @Override + public void run() { + statsItemSet.setValue("test", 10); + } + }); + } + while (true) { + if (executor.getCompletedTaskCount() == 10) { + break; + } + Thread.sleep(1000); + } + return statsItemSet.getAndCreateStatsItem("test").getValue(); + } + + @After + public void shutdown() { + executor.shutdown(); + } +} \ No newline at end of file diff --git a/common/src/test/java/org/apache/rocketmq/common/sysflag/CompressionFlagTest.java b/common/src/test/java/org/apache/rocketmq/common/sysflag/CompressionFlagTest.java new file mode 100644 index 00000000000..8590d569513 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/sysflag/CompressionFlagTest.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.sysflag; + +import org.apache.rocketmq.common.compression.CompressionType; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CompressionFlagTest { + + @Test + public void testCompressionFlag() { + int flag = 0; + flag |= MessageSysFlag.COMPRESSED_FLAG; + + assertThat(MessageSysFlag.getCompressionType(flag)).isEqualTo(CompressionType.ZLIB); + + flag |= MessageSysFlag.COMPRESSION_LZ4_TYPE; + assertThat(MessageSysFlag.getCompressionType(flag)).isEqualTo(CompressionType.LZ4); + + flag &= ~MessageSysFlag.COMPRESSION_TYPE_COMPARATOR; + flag |= MessageSysFlag.COMPRESSION_ZSTD_TYPE; + assertThat(MessageSysFlag.getCompressionType(flag)).isEqualTo(CompressionType.ZSTD); + + + flag &= ~MessageSysFlag.COMPRESSION_TYPE_COMPARATOR; + flag |= MessageSysFlag.COMPRESSION_ZLIB_TYPE; + assertThat(MessageSysFlag.getCompressionType(flag)).isEqualTo(CompressionType.ZLIB); + } + + @Test(expected = RuntimeException.class) + public void testCompressionFlagNotMatch() { + int flag = 0; + flag |= MessageSysFlag.COMPRESSED_FLAG; + flag |= 0x4 << 8; + + MessageSysFlag.getCompressionType(flag); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/sysflag/PullSysFlagTest.java b/common/src/test/java/org/apache/rocketmq/common/sysflag/PullSysFlagTest.java new file mode 100644 index 00000000000..60e18123c47 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/sysflag/PullSysFlagTest.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.sysflag; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PullSysFlagTest { + + @Test + public void testLitePullFlag() { + int flag = PullSysFlag.buildSysFlag(false, false, false, false, true); + assertThat(PullSysFlag.hasLitePullFlag(flag)).isTrue(); + } + + @Test + public void testLitePullFlagFalse() { + int flag = PullSysFlag.buildSysFlag(false, false, false, false, false); + assertThat(PullSysFlag.hasLitePullFlag(flag)).isFalse(); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/topic/TopicValidatorTest.java b/common/src/test/java/org/apache/rocketmq/common/topic/TopicValidatorTest.java new file mode 100644 index 00000000000..65954fa932e --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/topic/TopicValidatorTest.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.common.topic; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TopicValidatorTest { + + @Test + public void testTopicValidator_NotPass() { + TopicValidator.ValidateTopicResult res = TopicValidator.validateTopic(""); + assertThat(res.isValid()).isFalse(); + assertThat(res.getRemark()).contains("The specified topic is blank"); + + res = TopicValidator.validateTopic("../TopicTest"); + assertThat(res.isValid()).isFalse(); + assertThat(res.getRemark()).contains("The specified topic contains illegal characters"); + + res = TopicValidator.validateTopic(generateString(128)); + assertThat(res.isValid()).isFalse(); + assertThat(res.getRemark()).contains("The specified topic is longer than topic max length."); + } + + @Test + public void testTopicValidator_Pass() { + TopicValidator.ValidateTopicResult res = TopicValidator.validateTopic("TestTopic"); + assertThat(res.isValid()).isTrue(); + assertThat(res.getRemark()).isEmpty(); + } + + @Test + public void testAddSystemTopic() { + String topic = "SYSTEM_TOPIC_TEST"; + TopicValidator.addSystemTopic(topic); + assertThat(TopicValidator.getSystemTopicSet()).contains(topic); + } + + @Test + public void testIsSystemTopic() { + boolean res; + for (String topic : TopicValidator.getSystemTopicSet()) { + res = TopicValidator.isSystemTopic(topic); + assertThat(res).isTrue(); + } + + String topic = TopicValidator.SYSTEM_TOPIC_PREFIX + "_test"; + res = TopicValidator.isSystemTopic(topic); + assertThat(res).isTrue(); + + topic = "test_not_system_topic"; + res = TopicValidator.isSystemTopic(topic); + assertThat(res).isFalse(); + } + + @Test + public void testIsSystemTopicWithResponse() { + boolean res; + for (String topic : TopicValidator.getSystemTopicSet()) { + res = TopicValidator.isSystemTopic(topic); + assertThat(res).isTrue(); + } + + String topic = "test_not_system_topic"; + res = TopicValidator.isSystemTopic(topic); + assertThat(res).isFalse(); + } + + @Test + public void testIsNotAllowedSendTopic() { + boolean res; + for (String topic : TopicValidator.getNotAllowedSendTopicSet()) { + res = TopicValidator.isNotAllowedSendTopic(topic); + assertThat(res).isTrue(); + } + + String topic = "test_allowed_send_topic"; + res = TopicValidator.isNotAllowedSendTopic(topic); + assertThat(res).isFalse(); + } + + @Test + public void testIsNotAllowedSendTopicWithResponse() { + boolean res; + for (String topic : TopicValidator.getNotAllowedSendTopicSet()) { + res = TopicValidator.isNotAllowedSendTopic(topic); + assertThat(res).isTrue(); + } + + String topic = "test_allowed_send_topic"; + res = TopicValidator.isNotAllowedSendTopic(topic); + assertThat(res).isFalse(); + } + + private static String generateString(int length) { + StringBuilder stringBuffer = new StringBuilder(); + String tmpStr = "0123456789"; + for (int i = 0; i < length; i++) { + stringBuffer.append(tmpStr); + } + return stringBuffer.toString(); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/utils/ConcurrentHashMapUtilsTest.java b/common/src/test/java/org/apache/rocketmq/common/utils/ConcurrentHashMapUtilsTest.java new file mode 100644 index 00000000000..778c6f25da0 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/utils/ConcurrentHashMapUtilsTest.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.utils; + +import java.util.concurrent.ConcurrentHashMap; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class ConcurrentHashMapUtilsTest { + + @Test + public void computeIfAbsent() { + ConcurrentHashMap map = new ConcurrentHashMap<>(); + map.put("123", "1111"); + String value = ConcurrentHashMapUtils.computeIfAbsent(map, "123", k -> "234"); + assertEquals("1111", value); + String value1 = ConcurrentHashMapUtils.computeIfAbsent(map, "1232", k -> "2342"); + assertEquals("2342", value1); + String value2 = ConcurrentHashMapUtils.computeIfAbsent(map, "123", k -> "2342"); + assertEquals("1111", value2); + } +} \ No newline at end of file diff --git a/common/src/test/java/org/apache/rocketmq/common/utils/IOTinyUtilsTest.java b/common/src/test/java/org/apache/rocketmq/common/utils/IOTinyUtilsTest.java new file mode 100644 index 00000000000..732013179b2 --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/utils/IOTinyUtilsTest.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.utils; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.CharArrayReader; +import java.io.CharArrayWriter; +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; +import java.io.Writer; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.util.List; +import org.apache.rocketmq.common.UtilAll; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class IOTinyUtilsTest { + + /** + * https://bazel.build/reference/test-encyclopedia#filesystem + */ + private String testRootDir = System.getProperty("java.io.tmpdir") + File.separator + "iotinyutilstest"; + + @Before + public void init() { + File dir = new File(testRootDir); + if (dir.exists()) { + UtilAll.deleteFile(dir); + } + + dir.mkdirs(); + } + + @After + public void destroy() { + File file = new File(testRootDir); + UtilAll.deleteFile(file); + } + + @Test + public void testToString() throws Exception { + byte[] b = "testToString".getBytes(StandardCharsets.UTF_8); + InputStream is = new ByteArrayInputStream(b); + + String str = IOTinyUtils.toString(is, null); + assertEquals("testToString", str); + + is = new ByteArrayInputStream(b); + str = IOTinyUtils.toString(is, StandardCharsets.UTF_8.name()); + assertEquals("testToString", str); + + is = new ByteArrayInputStream(b); + Reader isr = new InputStreamReader(is, StandardCharsets.UTF_8); + str = IOTinyUtils.toString(isr); + assertEquals("testToString", str); + } + + + @Test + public void testCopy() throws Exception { + char[] arr = "testToString".toCharArray(); + Reader reader = new CharArrayReader(arr); + Writer writer = new CharArrayWriter(); + + long count = IOTinyUtils.copy(reader, writer); + assertEquals(arr.length, count); + } + + @Test + public void testReadLines() throws Exception { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 10; i++) { + sb.append("testReadLines").append("\n"); + } + + StringReader reader = new StringReader(sb.toString()); + List lines = IOTinyUtils.readLines(reader); + + assertEquals(10, lines.size()); + } + + @Test + public void testToBufferedReader() throws Exception { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 10; i++) { + sb.append("testToBufferedReader").append("\n"); + } + + StringReader reader = new StringReader(sb.toString()); + Method method = IOTinyUtils.class.getDeclaredMethod("toBufferedReader", new Class[]{Reader.class}); + method.setAccessible(true); + Object bReader = method.invoke(IOTinyUtils.class, reader); + + assertTrue(bReader instanceof BufferedReader); + } + + @Test + public void testWriteStringToFile() throws Exception { + File file = new File(testRootDir, "testWriteStringToFile"); + assertTrue(!file.exists()); + + IOTinyUtils.writeStringToFile(file, "testWriteStringToFile", StandardCharsets.UTF_8.name()); + + assertTrue(file.exists()); + } + + @Test + public void testCleanDirectory() throws Exception { + for (int i = 0; i < 10; i++) { + IOTinyUtils.writeStringToFile(new File(testRootDir, "testCleanDirectory" + i), "testCleanDirectory", StandardCharsets.UTF_8.name()); + } + + File dir = new File(testRootDir); + assertTrue(dir.exists() && dir.isDirectory()); + assertTrue(dir.listFiles().length > 0); + + IOTinyUtils.cleanDirectory(new File(testRootDir)); + + assertTrue(dir.listFiles().length == 0); + } + + @Test + public void testDelete() throws Exception { + for (int i = 0; i < 10; i++) { + IOTinyUtils.writeStringToFile(new File(testRootDir, "testDelete" + i), "testCleanDirectory", StandardCharsets.UTF_8.name()); + } + + File dir = new File(testRootDir); + assertTrue(dir.exists() && dir.isDirectory()); + assertTrue(dir.listFiles().length > 0); + + IOTinyUtils.delete(new File(testRootDir)); + + assertTrue(!dir.exists()); + } + + @Test + public void testCopyFile() throws Exception { + File source = new File(testRootDir, "source"); + String target = testRootDir + File.separator + "dest"; + + IOTinyUtils.writeStringToFile(source, "testCopyFile", StandardCharsets.UTF_8.name()); + + IOTinyUtils.copyFile(source.getCanonicalPath(), target); + + File dest = new File(target); + assertTrue(dest.exists()); + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/utils/NameServerAddressUtilsTest.java b/common/src/test/java/org/apache/rocketmq/common/utils/NameServerAddressUtilsTest.java new file mode 100644 index 00000000000..38cffdba9dd --- /dev/null +++ b/common/src/test/java/org/apache/rocketmq/common/utils/NameServerAddressUtilsTest.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.common.utils; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NameServerAddressUtilsTest { + + private static String endpoint1 = "http://127.0.0.1:9876"; + private static String endpoint2 = "127.0.0.1:9876"; + private static String endpoint3 + = "http://MQ_INST_123456789_BXXUzaee.xxx:80"; + private static String endpoint4 = "MQ_INST_123456789_BXXUzaee.xxx:80"; + + @Test + public void testValidateInstanceEndpoint() { + assertThat(NameServerAddressUtils.validateInstanceEndpoint(endpoint1)).isEqualTo(false); + assertThat(NameServerAddressUtils.validateInstanceEndpoint(endpoint2)).isEqualTo(false); + assertThat(NameServerAddressUtils.validateInstanceEndpoint(endpoint3)).isEqualTo(true); + assertThat(NameServerAddressUtils.validateInstanceEndpoint(endpoint4)).isEqualTo(true); + } + + @Test + public void testParseInstanceIdFromEndpoint() { + assertThat(NameServerAddressUtils.parseInstanceIdFromEndpoint(endpoint3)).isEqualTo( + "MQ_INST_123456789_BXXUzaee"); + assertThat(NameServerAddressUtils.parseInstanceIdFromEndpoint(endpoint4)).isEqualTo( + "MQ_INST_123456789_BXXUzaee"); + } + + @Test + public void testGetNameSrvAddrFromNamesrvEndpoint() { + assertThat(NameServerAddressUtils.getNameSrvAddrFromNamesrvEndpoint(endpoint1)) + .isEqualTo("127.0.0.1:9876"); + assertThat(NameServerAddressUtils.getNameSrvAddrFromNamesrvEndpoint(endpoint2)) + .isEqualTo("127.0.0.1:9876"); + assertThat(NameServerAddressUtils.getNameSrvAddrFromNamesrvEndpoint(endpoint3)) + .isEqualTo("MQ_INST_123456789_BXXUzaee.xxx:80"); + assertThat(NameServerAddressUtils.getNameSrvAddrFromNamesrvEndpoint(endpoint4)) + .isEqualTo("MQ_INST_123456789_BXXUzaee.xxx:80"); + } +} diff --git a/common/src/test/resources/rmq.logback-test.xml b/common/src/test/resources/rmq.logback-test.xml new file mode 100644 index 00000000000..8695d52d57c --- /dev/null +++ b/common/src/test/resources/rmq.logback-test.xml @@ -0,0 +1,36 @@ + + + + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + \ No newline at end of file diff --git a/container/BUILD.bazel b/container/BUILD.bazel new file mode 100644 index 00000000000..059d7c2252c --- /dev/null +++ b/container/BUILD.bazel @@ -0,0 +1,82 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +load("//bazel:GenTestRules.bzl", "GenTestRules") + +java_library( + name = "container", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//broker", + "//common", + "//remoting", + "//client", + "//srvutil", + "//store", + "@maven//:io_openmessaging_storage_dledger", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:commons_validator_commons_validator", + "@maven//:commons_collections_commons_collections", + "@maven//:commons_codec_commons_codec", + "@maven//:com_github_luben_zstd_jni", + "@maven//:org_lz4_lz4_java", + "@maven//:com_alibaba_fastjson", + "@maven//:io_netty_netty_all", + "@maven//:com_google_guava_guava", + "@maven//:org_slf4j_slf4j_api", + "@maven//:ch_qos_logback_logback_core", + "@maven//:ch_qos_logback_logback_classic", + "@maven//:commons_cli_commons_cli", + "@maven//:io_github_aliyunmq_rocketmq_slf4j_api", + "@maven//:io_github_aliyunmq_rocketmq_logback_classic", + ], +) + +java_library( + name = "tests", + srcs = glob(["src/test/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + ":container", + "//broker", + "//common", + "//remoting", + "//client", + "//srvutil", + "//store", + "@maven//:io_openmessaging_storage_dledger", + "//:test_deps", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:io_netty_netty_all", + "@maven//:com_google_guava_guava", + "@maven//:com_alibaba_fastjson", + ], + resources = glob(["src/test/resources/certs/*.pem"]) + glob(["src/test/resources/certs/*.key"]) +) + +GenTestRules( + name = "GeneratedTestRules", + test_files = glob(["src/test/java/**/*Test.java"]), + deps = [ + ":tests", + ], +# The following tests are flaky, fix them later. + exclude_tests = [ + "src/test/java/org/apache/rocketmq/container/BrokerContainerStartupTest", + "src/test/java/org/apache/rocketmq/container/BrokerContainerTest", + ], +) diff --git a/container/pom.xml b/container/pom.xml new file mode 100644 index 00000000000..c536dd2394d --- /dev/null +++ b/container/pom.xml @@ -0,0 +1,39 @@ + + + + org.apache.rocketmq + rocketmq-all + 5.2.0 + + + 4.0.0 + jar + rocketmq-container + rocketmq-container ${project.version} + + + ${basedir}/.. + + + + + org.apache.rocketmq + rocketmq-broker + + + diff --git a/container/src/main/java/org/apache/rocketmq/container/BrokerBootHook.java b/container/src/main/java/org/apache/rocketmq/container/BrokerBootHook.java new file mode 100644 index 00000000000..fe126af3a27 --- /dev/null +++ b/container/src/main/java/org/apache/rocketmq/container/BrokerBootHook.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.container; + +import java.util.Properties; +import org.apache.rocketmq.broker.BrokerController; + +public interface BrokerBootHook { + /** + * Name of the hook. + * + * @return name of the hook + */ + String hookName(); + + /** + * Code to execute before broker start. + * + * @param brokerController broker to start + * @param properties broker properties + * @throws Exception when execute hook + */ + void executeBeforeStart(BrokerController brokerController, Properties properties) throws Exception; + + /** + * Code to execute after broker start. + * + * @param brokerController broker to start + * @param properties broker properties + * @throws Exception when execute hook + */ + void executeAfterStart(BrokerController brokerController, Properties properties) throws Exception; +} diff --git a/container/src/main/java/org/apache/rocketmq/container/BrokerContainer.java b/container/src/main/java/org/apache/rocketmq/container/BrokerContainer.java new file mode 100644 index 00000000000..d0a550be635 --- /dev/null +++ b/container/src/main/java/org/apache/rocketmq/container/BrokerContainer.java @@ -0,0 +1,485 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.container; + +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.BrokerPathConfigHelper; +import org.apache.rocketmq.broker.out.BrokerOuterAPI; +import org.apache.rocketmq.common.AbstractBrokerRunnable; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.BrokerIdentity; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.container.logback.BrokerLogbackConfigurator; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.Configuration; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.RemotingServer; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyRemotingServer; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.store.config.BrokerRole; +import org.apache.rocketmq.store.config.MessageStoreConfig; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class BrokerContainer implements IBrokerContainer { + private static final Logger LOG = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + + private final ScheduledExecutorService scheduledExecutorService = ThreadUtils.newScheduledThreadPool(1, + new BasicThreadFactory.Builder() + .namingPattern("BrokerContainerScheduledThread") + .daemon(true) + .build()); + private final NettyServerConfig nettyServerConfig; + private final NettyClientConfig nettyClientConfig; + private final BrokerOuterAPI brokerOuterAPI; + private final ContainerClientHouseKeepingService containerClientHouseKeepingService; + + private final ConcurrentMap slaveBrokerControllers = new ConcurrentHashMap<>(); + private final ConcurrentMap masterBrokerControllers = new ConcurrentHashMap<>(); + private final ConcurrentMap dLedgerBrokerControllers = new ConcurrentHashMap<>(); + private final List brokerBootHookList = new ArrayList<>(); + private final BrokerContainerProcessor brokerContainerProcessor; + private final Configuration configuration; + private final BrokerContainerConfig brokerContainerConfig; + + private RemotingServer remotingServer; + private RemotingServer fastRemotingServer; + private ExecutorService brokerContainerExecutor; + + public BrokerContainer( + final BrokerContainerConfig brokerContainerConfig, + final NettyServerConfig nettyServerConfig, + final NettyClientConfig nettyClientConfig + ) { + this.brokerContainerConfig = brokerContainerConfig; + this.nettyServerConfig = nettyServerConfig; + this.nettyClientConfig = nettyClientConfig; + + this.brokerOuterAPI = new BrokerOuterAPI(nettyClientConfig); + + this.brokerContainerProcessor = new BrokerContainerProcessor(this); + this.brokerContainerProcessor.registerBrokerBootHook(this.brokerBootHookList); + this.containerClientHouseKeepingService = new ContainerClientHouseKeepingService(this); + + this.configuration = new Configuration( + LOG, + BrokerPathConfigHelper.getBrokerConfigPath(), + this.brokerContainerConfig, this.nettyServerConfig, this.nettyClientConfig); + } + + @Override + public String getBrokerContainerAddr() { + return this.brokerContainerConfig.getBrokerContainerIP() + ":" + this.nettyServerConfig.getListenPort(); + } + + @Override + public BrokerContainerConfig getBrokerContainerConfig() { + return brokerContainerConfig; + } + + @Override + public NettyServerConfig getNettyServerConfig() { + return nettyServerConfig; + } + + @Override + public NettyClientConfig getNettyClientConfig() { + return nettyClientConfig; + } + + @Override + public BrokerOuterAPI getBrokerOuterAPI() { + return brokerOuterAPI; + } + + @Override + public RemotingServer getRemotingServer() { + return remotingServer; + } + + public Configuration getConfiguration() { + return this.configuration; + } + + private void updateNamesrvAddr() { + if (this.brokerContainerConfig.isFetchNameSrvAddrByDnsLookup()) { + this.brokerOuterAPI.updateNameServerAddressListByDnsLookup(this.brokerContainerConfig.getNamesrvAddr()); + } else { + this.brokerOuterAPI.updateNameServerAddressList(this.brokerContainerConfig.getNamesrvAddr()); + } + } + + public boolean initialize() { + this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.containerClientHouseKeepingService); + this.fastRemotingServer = this.remotingServer.newRemotingServer(this.nettyServerConfig.getListenPort() - 2); + + this.brokerContainerExecutor = ThreadUtils.newThreadPoolExecutor( + 1, + 1, + 1000 * 60, + TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(10000), + new ThreadFactoryImpl("SharedBrokerThread_")); + + this.registerProcessor(); + + if (this.brokerContainerConfig.getNamesrvAddr() != null) { + this.updateNamesrvAddr(); + LOG.info("Set user specified name server address: {}", this.brokerContainerConfig.getNamesrvAddr()); + // also auto update namesrv if specify + this.scheduledExecutorService.scheduleAtFixedRate(new AbstractBrokerRunnable(BrokerIdentity.BROKER_CONTAINER_IDENTITY) { + @Override + public void run0() { + try { + BrokerContainer.this.updateNamesrvAddr(); + } catch (Throwable e) { + LOG.error("ScheduledTask fetchNameServerAddr exception", e); + } + } + }, 1000 * 10, this.brokerContainerConfig.getUpdateNamesrvAddrInterval(), TimeUnit.MILLISECONDS); + } else if (this.brokerContainerConfig.isFetchNamesrvAddrByAddressServer()) { + this.scheduledExecutorService.scheduleAtFixedRate(new AbstractBrokerRunnable(BrokerIdentity.BROKER_CONTAINER_IDENTITY) { + + @Override + public void run0() { + try { + BrokerContainer.this.brokerOuterAPI.fetchNameServerAddr(); + } catch (Throwable e) { + LOG.error("ScheduledTask fetchNameServerAddr exception", e); + } + } + }, 1000 * 10, this.brokerContainerConfig.getFetchNamesrvAddrInterval(), TimeUnit.MILLISECONDS); + } + + this.scheduledExecutorService.scheduleAtFixedRate(new AbstractBrokerRunnable(BrokerIdentity.BROKER_CONTAINER_IDENTITY) { + @Override + public void run0() { + try { + BrokerContainer.this.brokerOuterAPI.refreshMetadata(); + } catch (Exception e) { + LOG.error("ScheduledTask refresh metadata exception", e); + } + } + }, 10, 5, TimeUnit.SECONDS); + + return true; + } + + private void registerProcessor() { + remotingServer.registerDefaultProcessor(brokerContainerProcessor, this.brokerContainerExecutor); + fastRemotingServer.registerDefaultProcessor(brokerContainerProcessor, this.brokerContainerExecutor); + } + + @Override + public void start() throws Exception { + if (this.remotingServer != null) { + this.remotingServer.start(); + } + + if (this.fastRemotingServer != null) { + this.fastRemotingServer.start(); + } + + if (this.brokerOuterAPI != null) { + this.brokerOuterAPI.start(); + } + } + + @Override + public void shutdown() { + // Shutdown slave brokers + for (InnerSalveBrokerController slaveBrokerController : slaveBrokerControllers.values()) { + slaveBrokerController.shutdown(); + } + + slaveBrokerControllers.clear(); + + // Shutdown master brokers + for (BrokerController masterBrokerController : masterBrokerControllers.values()) { + masterBrokerController.shutdown(); + } + + masterBrokerControllers.clear(); + + // Shutdown dLedger brokers + dLedgerBrokerControllers.values().forEach(InnerBrokerController::shutdown); + dLedgerBrokerControllers.clear(); + + // Shutdown the remoting server with a high priority to avoid further traffic + if (this.remotingServer != null) { + this.remotingServer.shutdown(); + } + + if (this.fastRemotingServer != null) { + this.fastRemotingServer.shutdown(); + } + + // Shutdown the request executors + ThreadUtils.shutdown(this.brokerContainerExecutor); + + if (this.brokerOuterAPI != null) { + this.brokerOuterAPI.shutdown(); + } + } + + public void registerClientRPCHook(RPCHook rpcHook) { + this.getBrokerOuterAPI().registerRPCHook(rpcHook); + } + + public void clearClientRPCHook() { + this.getBrokerOuterAPI().clearRPCHook(); + } + + public List getBrokerBootHookList() { + return brokerBootHookList; + } + + public void registerBrokerBootHook(BrokerBootHook brokerBootHook) { + this.brokerBootHookList.add(brokerBootHook); + LOG.info("register BrokerBootHook, {}", brokerBootHook.hookName()); + } + + @Override + public InnerBrokerController addBroker(final BrokerConfig brokerConfig, + final MessageStoreConfig storeConfig) throws Exception { + if (storeConfig.isEnableDLegerCommitLog()) { + return this.addDLedgerBroker(brokerConfig, storeConfig); + } else { + if (brokerConfig.getBrokerId() == MixAll.MASTER_ID && storeConfig.getBrokerRole() != BrokerRole.SLAVE) { + return this.addMasterBroker(brokerConfig, storeConfig); + } + if (brokerConfig.getBrokerId() != MixAll.MASTER_ID && storeConfig.getBrokerRole() == BrokerRole.SLAVE) { + return this.addSlaveBroker(brokerConfig, storeConfig); + } + } + + return null; + } + + public InnerBrokerController addDLedgerBroker(final BrokerConfig brokerConfig, final MessageStoreConfig storeConfig) throws Exception { + brokerConfig.setInBrokerContainer(true); + if (storeConfig.isDuplicationEnable()) { + LOG.error("Can not add broker to container when duplicationEnable is true currently"); + throw new Exception("Can not add broker to container when duplicationEnable is true currently"); + } + InnerBrokerController brokerController = new InnerBrokerController(this, brokerConfig, storeConfig); + BrokerIdentity brokerIdentity = brokerController.getBrokerIdentity(); + final BrokerController previousBroker = dLedgerBrokerControllers.putIfAbsent(brokerIdentity, brokerController); + if (previousBroker == null) { + // New dLedger broker added, start it + try { + BrokerLogbackConfigurator.doConfigure(brokerIdentity); + final boolean initResult = brokerController.initialize(); + if (!initResult) { + dLedgerBrokerControllers.remove(brokerIdentity); + brokerController.shutdown(); + throw new Exception("Failed to init dLedger broker " + brokerIdentity.getCanonicalName()); + } + } catch (Exception e) { + // Remove the failed dLedger broker and throw the exception + dLedgerBrokerControllers.remove(brokerIdentity); + brokerController.shutdown(); + throw new Exception("Failed to initialize dLedger broker " + brokerIdentity.getCanonicalName(), e); + } + return brokerController; + } + throw new Exception(brokerIdentity.getCanonicalName() + " has already been added to current broker container"); + } + + public InnerBrokerController addMasterBroker(final BrokerConfig masterBrokerConfig, + final MessageStoreConfig storeConfig) throws Exception { + + masterBrokerConfig.setInBrokerContainer(true); + if (storeConfig.isDuplicationEnable()) { + LOG.error("Can not add broker to container when duplicationEnable is true currently"); + throw new Exception("Can not add broker to container when duplicationEnable is true currently"); + } + InnerBrokerController masterBroker = new InnerBrokerController(this, masterBrokerConfig, storeConfig); + BrokerIdentity brokerIdentity = masterBroker.getBrokerIdentity(); + final BrokerController previousBroker = masterBrokerControllers.putIfAbsent(brokerIdentity, masterBroker); + if (previousBroker == null) { + // New master broker added, start it + try { + BrokerLogbackConfigurator.doConfigure(masterBrokerConfig); + final boolean initResult = masterBroker.initialize(); + if (!initResult) { + masterBrokerControllers.remove(brokerIdentity); + masterBroker.shutdown(); + throw new Exception("Failed to init master broker " + masterBrokerConfig.getCanonicalName()); + } + + for (InnerSalveBrokerController slaveBroker : this.getSlaveBrokers()) { + if (slaveBroker.getMessageStore().getMasterStoreInProcess() == null) { + slaveBroker.getMessageStore().setMasterStoreInProcess(masterBroker.getMessageStore()); + } + } + } catch (Exception e) { + // Remove the failed master broker and throw the exception + masterBrokerControllers.remove(brokerIdentity); + masterBroker.shutdown(); + throw new Exception("Failed to initialize master broker " + masterBrokerConfig.getCanonicalName(), e); + } + return masterBroker; + } + throw new Exception(masterBrokerConfig.getCanonicalName() + " has already been added to current broker container"); + } + + /** + * This function will create a slave broker along with the main broker, and start it with a different port. + * + * @param slaveBrokerConfig the specific slave broker config + * @throws Exception is thrown if an error occurs + */ + public InnerSalveBrokerController addSlaveBroker(final BrokerConfig slaveBrokerConfig, + final MessageStoreConfig storeConfig) throws Exception { + + slaveBrokerConfig.setInBrokerContainer(true); + if (storeConfig.isDuplicationEnable()) { + LOG.error("Can not add broker to container when duplicationEnable is true currently"); + throw new Exception("Can not add broker to container when duplicationEnable is true currently"); + } + + int ratio = storeConfig.getAccessMessageInMemoryMaxRatio() - 10; + storeConfig.setAccessMessageInMemoryMaxRatio(Math.max(ratio, 0)); + InnerSalveBrokerController slaveBroker = new InnerSalveBrokerController(this, slaveBrokerConfig, storeConfig); + BrokerIdentity brokerIdentity = slaveBroker.getBrokerIdentity(); + final InnerSalveBrokerController previousBroker = slaveBrokerControllers.putIfAbsent(brokerIdentity, slaveBroker); + if (previousBroker == null) { + // New slave broker added, start it + try { + BrokerLogbackConfigurator.doConfigure(slaveBrokerConfig); + final boolean initResult = slaveBroker.initialize(); + if (!initResult) { + slaveBrokerControllers.remove(brokerIdentity); + slaveBroker.shutdown(); + throw new Exception("Failed to init slave broker " + slaveBrokerConfig.getCanonicalName()); + } + BrokerController masterBroker = this.peekMasterBroker(); + if (slaveBroker.getMessageStore().getMasterStoreInProcess() == null && masterBroker != null) { + slaveBroker.getMessageStore().setMasterStoreInProcess(masterBroker.getMessageStore()); + } + } catch (Exception e) { + // Remove the failed slave broker and throw the exception + slaveBrokerControllers.remove(brokerIdentity); + slaveBroker.shutdown(); + throw new Exception("Failed to initialize slave broker " + slaveBrokerConfig.getCanonicalName(), e); + } + return slaveBroker; + } + throw new Exception(slaveBrokerConfig.getCanonicalName() + " has already been added to current broker container"); + } + + @Override + public BrokerController removeBroker(final BrokerIdentity brokerIdentity) throws Exception { + + InnerBrokerController dLedgerController = dLedgerBrokerControllers.remove(brokerIdentity); + if (dLedgerController != null) { + dLedgerController.shutdown(); + return dLedgerController; + } + + InnerSalveBrokerController slaveBroker = slaveBrokerControllers.remove(brokerIdentity); + if (slaveBroker != null) { + slaveBroker.shutdown(); + return slaveBroker; + } + + BrokerController masterBroker = masterBrokerControllers.remove(brokerIdentity); + + BrokerController nextMasterBroker = this.peekMasterBroker(); + for (InnerSalveBrokerController slave : this.getSlaveBrokers()) { + if (nextMasterBroker == null) { + slave.getMessageStore().setMasterStoreInProcess(null); + } else { + slave.getMessageStore().setMasterStoreInProcess(nextMasterBroker.getMessageStore()); + } + + } + + if (masterBroker != null) { + masterBroker.shutdown(); + return masterBroker; + } + + return null; + } + + @Override + public BrokerController getBroker(final BrokerIdentity brokerIdentity) { + InnerSalveBrokerController slaveBroker = slaveBrokerControllers.get(brokerIdentity); + if (slaveBroker != null) { + return slaveBroker; + } + + return masterBrokerControllers.get(brokerIdentity); + } + + @Override + public Collection getMasterBrokers() { + return masterBrokerControllers.values(); + } + + @Override + public Collection getSlaveBrokers() { + return slaveBrokerControllers.values(); + } + + @Override + public List getBrokerControllers() { + List brokerControllers = new ArrayList<>(); + brokerControllers.addAll(this.getMasterBrokers()); + brokerControllers.addAll(this.getSlaveBrokers()); + return brokerControllers; + } + + @Override + public BrokerController peekMasterBroker() { + if (!masterBrokerControllers.isEmpty()) { + return masterBrokerControllers.values().iterator().next(); + } + return null; + } + + public BrokerController findBrokerControllerByBrokerName(String brokerName) { + for (BrokerController brokerController : masterBrokerControllers.values()) { + if (brokerController.getBrokerConfig().getBrokerName().equals(brokerName)) { + return brokerController; + } + } + + for (BrokerController brokerController : slaveBrokerControllers.values()) { + if (brokerController.getBrokerConfig().getBrokerName().equals(brokerName)) { + return brokerController; + } + } + return null; + } +} diff --git a/container/src/main/java/org/apache/rocketmq/container/BrokerContainerConfig.java b/container/src/main/java/org/apache/rocketmq/container/BrokerContainerConfig.java new file mode 100644 index 00000000000..03b4b263f96 --- /dev/null +++ b/container/src/main/java/org/apache/rocketmq/container/BrokerContainerConfig.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.container; + +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.annotation.ImportantField; +import org.apache.rocketmq.common.utils.NetworkUtil; + +public class BrokerContainerConfig { + + private String rocketmqHome = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, System.getenv(MixAll.ROCKETMQ_HOME_ENV)); + + @ImportantField + private String namesrvAddr = System.getProperty(MixAll.NAMESRV_ADDR_PROPERTY, System.getenv(MixAll.NAMESRV_ADDR_ENV)); + + @ImportantField + private boolean fetchNameSrvAddrByDnsLookup = false; + + @ImportantField + private boolean fetchNamesrvAddrByAddressServer = false; + + @ImportantField + private String brokerContainerIP = NetworkUtil.getLocalAddress(); + + private String brokerConfigPaths = null; + + /** + * The interval to fetch namesrv addr, default value is 10 second + */ + private long fetchNamesrvAddrInterval = 10 * 1000; + + /** + * The interval to update namesrv addr, default value is 120 second + */ + private long updateNamesrvAddrInterval = 60 * 2 * 1000; + + + /** + * Config in this black list will be not allowed to update by command. + * Try to update this config black list by restart process. + * Try to update configures in black list by restart process. + */ + private String configBlackList = "configBlackList;brokerConfigPaths"; + + public String getRocketmqHome() { + return rocketmqHome; + } + + public void setRocketmqHome(String rocketmqHome) { + this.rocketmqHome = rocketmqHome; + } + + public String getNamesrvAddr() { + return namesrvAddr; + } + + public void setNamesrvAddr(String namesrvAddr) { + this.namesrvAddr = namesrvAddr; + } + + public boolean isFetchNameSrvAddrByDnsLookup() { + return fetchNameSrvAddrByDnsLookup; + } + + public void setFetchNameSrvAddrByDnsLookup(boolean fetchNameSrvAddrByDnsLookup) { + this.fetchNameSrvAddrByDnsLookup = fetchNameSrvAddrByDnsLookup; + } + + public boolean isFetchNamesrvAddrByAddressServer() { + return fetchNamesrvAddrByAddressServer; + } + + public void setFetchNamesrvAddrByAddressServer(boolean fetchNamesrvAddrByAddressServer) { + this.fetchNamesrvAddrByAddressServer = fetchNamesrvAddrByAddressServer; + } + + public String getBrokerContainerIP() { + return brokerContainerIP; + } + + public String getBrokerConfigPaths() { + return brokerConfigPaths; + } + + public void setBrokerConfigPaths(String brokerConfigPaths) { + this.brokerConfigPaths = brokerConfigPaths; + } + + public long getFetchNamesrvAddrInterval() { + return fetchNamesrvAddrInterval; + } + + public void setFetchNamesrvAddrInterval(final long fetchNamesrvAddrInterval) { + this.fetchNamesrvAddrInterval = fetchNamesrvAddrInterval; + } + + public long getUpdateNamesrvAddrInterval() { + return updateNamesrvAddrInterval; + } + + public void setUpdateNamesrvAddrInterval(long updateNamesrvAddrInterval) { + this.updateNamesrvAddrInterval = updateNamesrvAddrInterval; + } + + public String getConfigBlackList() { + return configBlackList; + } + + public void setConfigBlackList(String configBlackList) { + this.configBlackList = configBlackList; + } +} diff --git a/container/src/main/java/org/apache/rocketmq/container/BrokerContainerProcessor.java b/container/src/main/java/org/apache/rocketmq/container/BrokerContainerProcessor.java new file mode 100644 index 00000000000..5ced0825761 --- /dev/null +++ b/container/src/main/java/org/apache/rocketmq/container/BrokerContainerProcessor.java @@ -0,0 +1,313 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.container; + +import io.netty.channel.ChannelHandlerContext; +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.List; +import java.util.Properties; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.BrokerStartup; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.BrokerIdentity; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.AddBrokerRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetBrokerConfigResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.RemoveBrokerRequestHeader; +import org.apache.rocketmq.store.config.MessageStoreConfig; + +public class BrokerContainerProcessor implements NettyRequestProcessor { + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + private final BrokerContainer brokerContainer; + private List brokerBootHookList; + + private final Set configBlackList = new HashSet<>(); + + public BrokerContainerProcessor(BrokerContainer brokerContainer) { + this.brokerContainer = brokerContainer; + initConfigBlackList(); + } + + private void initConfigBlackList() { + configBlackList.add("brokerConfigPaths"); + configBlackList.add("rocketmqHome"); + configBlackList.add("configBlackList"); + String[] configArray = brokerContainer.getBrokerContainerConfig().getConfigBlackList().split(";"); + configBlackList.addAll(Arrays.asList(configArray)); + } + + @Override + public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) throws Exception { + switch (request.getCode()) { + case RequestCode.ADD_BROKER: + return this.addBroker(ctx, request); + case RequestCode.REMOVE_BROKER: + return this.removeBroker(ctx, request); + case RequestCode.GET_BROKER_CONFIG: + return this.getBrokerConfig(ctx, request); + case RequestCode.UPDATE_BROKER_CONFIG: + return this.updateBrokerConfig(ctx, request); + default: + break; + } + return null; + } + + @Override + public boolean rejectRequest() { + return false; + } + + private synchronized RemotingCommand addBroker(ChannelHandlerContext ctx, + RemotingCommand request) throws Exception { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + final AddBrokerRequestHeader requestHeader = (AddBrokerRequestHeader) request.decodeCommandCustomHeader(AddBrokerRequestHeader.class); + + LOGGER.info("addBroker called by {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + + Properties brokerProperties = null; + String configPath = requestHeader.getConfigPath(); + + if (configPath != null && !configPath.isEmpty()) { + BrokerStartup.SystemConfigFileHelper configFileHelper = new BrokerStartup.SystemConfigFileHelper(); + configFileHelper.setFile(configPath); + + try { + brokerProperties = configFileHelper.loadConfig(); + } catch (Exception e) { + LOGGER.error("addBroker load config from {} failed, {}", configPath, e); + } + } else { + LOGGER.error("addBroker config path is empty"); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("addBroker config path is empty"); + return response; + } + + if (brokerProperties == null) { + LOGGER.error("addBroker properties empty"); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("addBroker properties empty"); + return response; + } + + BrokerConfig brokerConfig = new BrokerConfig(); + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + MixAll.properties2Object(brokerProperties, brokerConfig); + MixAll.properties2Object(brokerProperties, messageStoreConfig); + + messageStoreConfig.setHaListenPort(brokerConfig.getListenPort() + 1); + + if (configPath != null && !configPath.isEmpty()) { + brokerConfig.setBrokerConfigPath(configPath); + } + + if (!messageStoreConfig.isEnableDLegerCommitLog()) { + if (!brokerConfig.isEnableControllerMode()) { + switch (messageStoreConfig.getBrokerRole()) { + case ASYNC_MASTER: + case SYNC_MASTER: + brokerConfig.setBrokerId(MixAll.MASTER_ID); + break; + case SLAVE: + if (brokerConfig.getBrokerId() <= 0) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("slave broker id must be > 0"); + return response; + } + break; + default: + break; + + } + } + + if (messageStoreConfig.getTotalReplicas() < messageStoreConfig.getInSyncReplicas() + || messageStoreConfig.getTotalReplicas() < messageStoreConfig.getMinInSyncReplicas() + || messageStoreConfig.getInSyncReplicas() < messageStoreConfig.getMinInSyncReplicas()) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("invalid replicas number"); + return response; + } + } + + BrokerController brokerController; + try { + brokerController = this.brokerContainer.addBroker(brokerConfig, messageStoreConfig); + } catch (Exception e) { + LOGGER.error("addBroker exception {}", e); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(e.getMessage()); + return response; + } + if (brokerController != null) { + brokerController.getConfiguration().registerConfig(brokerProperties); + try { + for (BrokerBootHook brokerBootHook : brokerBootHookList) { + brokerBootHook.executeBeforeStart(brokerController, brokerProperties); + } + brokerController.start(); + + for (BrokerBootHook brokerBootHook : brokerBootHookList) { + brokerBootHook.executeAfterStart(brokerController, brokerProperties); + } + } catch (Exception e) { + LOGGER.error("start broker exception {}", e); + BrokerIdentity brokerIdentity; + if (messageStoreConfig.isEnableDLegerCommitLog()) { + brokerIdentity = new BrokerIdentity(brokerConfig.getBrokerClusterName(), + brokerConfig.getBrokerName(), Integer.parseInt(messageStoreConfig.getdLegerSelfId().substring(1))); + } else { + brokerIdentity = new BrokerIdentity(brokerConfig.getBrokerClusterName(), + brokerConfig.getBrokerName(), brokerConfig.getBrokerId()); + } + this.brokerContainer.removeBroker(brokerIdentity); + brokerController.shutdown(); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("start broker failed, " + e); + return response; + } + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + } else { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("add broker return null"); + } + + return response; + } + + private synchronized RemotingCommand removeBroker(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + final RemoveBrokerRequestHeader requestHeader = (RemoveBrokerRequestHeader) request.decodeCommandCustomHeader(RemoveBrokerRequestHeader.class); + + LOGGER.info("removeBroker called by {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + + BrokerIdentity brokerIdentity = new BrokerIdentity(requestHeader.getBrokerClusterName(), requestHeader.getBrokerName(), requestHeader.getBrokerId()); + + BrokerController brokerController; + try { + brokerController = this.brokerContainer.removeBroker(brokerIdentity); + } catch (Exception e) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(e.getMessage()); + return response; + } + + if (brokerController != null) { + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + } else { + response.setCode(ResponseCode.BROKER_NOT_EXIST); + response.setRemark("Broker not exist"); + } + return response; + } + + public void registerBrokerBootHook(List brokerBootHookList) { + this.brokerBootHookList = brokerBootHookList; + } + + private RemotingCommand updateBrokerConfig(ChannelHandlerContext ctx, RemotingCommand request) { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + LOGGER.info("updateSharedBrokerConfig called by {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + + byte[] body = request.getBody(); + if (body != null) { + try { + String bodyStr = new String(body, MixAll.DEFAULT_CHARSET); + Properties properties = MixAll.string2Properties(bodyStr); + + if (properties == null) { + LOGGER.error("string2Properties error"); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("string2Properties error"); + return response; + } + + if (validateBlackListConfigExist(properties)) { + response.setCode(ResponseCode.NO_PERMISSION); + response.setRemark("Can not update config in black list."); + return response; + } + + + LOGGER.info("updateBrokerContainerConfig, new config: [{}] client: {} ", properties, ctx.channel().remoteAddress()); + this.brokerContainer.getConfiguration().update(properties); + + } catch (UnsupportedEncodingException e) { + LOGGER.error("", e); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("UnsupportedEncodingException " + e); + return response; + } + } + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + private boolean validateBlackListConfigExist(Properties properties) { + for (String blackConfig : configBlackList) { + if (properties.containsKey(blackConfig)) { + return true; + } + } + return false; + } + + private RemotingCommand getBrokerConfig(ChannelHandlerContext ctx, RemotingCommand request) { + + final RemotingCommand response = RemotingCommand.createResponseCommand(GetBrokerConfigResponseHeader.class); + final GetBrokerConfigResponseHeader responseHeader = (GetBrokerConfigResponseHeader) response.readCustomHeader(); + + String content = this.brokerContainer.getConfiguration().getAllConfigsFormatString(); + if (content != null && content.length() > 0) { + try { + response.setBody(content.getBytes(MixAll.DEFAULT_CHARSET)); + } catch (UnsupportedEncodingException e) { + LOGGER.error("", e); + + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("UnsupportedEncodingException " + e); + return response; + } + } + + responseHeader.setVersion(this.brokerContainer.getConfiguration().getDataVersionJson()); + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } +} diff --git a/container/src/main/java/org/apache/rocketmq/container/BrokerContainerStartup.java b/container/src/main/java/org/apache/rocketmq/container/BrokerContainerStartup.java new file mode 100644 index 00000000000..f909e623b21 --- /dev/null +++ b/container/src/main/java/org/apache/rocketmq/container/BrokerContainerStartup.java @@ -0,0 +1,428 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.container; + +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.BrokerPathConfigHelper; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.netty.NettySystemConfig; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.store.config.MessageStoreConfig; + +public class BrokerContainerStartup { + private static final String BROKER_CONTAINER_CONFIG_OPTION = "c"; + private static final String BROKER_CONFIG_OPTION = "b"; + private static final String PRINT_PROPERTIES_OPTION = "p"; + private static final String PRINT_IMPORTANT_PROPERTIES_OPTION = "m"; + public static Properties properties = null; + public static CommandLine commandLine = null; + public static String configFile = null; + public static Logger log; + public static final SystemConfigFileHelper CONFIG_FILE_HELPER = new SystemConfigFileHelper(); + public static String rocketmqHome = null; + + public static void main(String[] args) { + final BrokerContainer brokerContainer = startBrokerContainer(createBrokerContainer(args)); + createAndStartBrokers(brokerContainer); + } + + public static BrokerController createBrokerController(String[] args) { + final BrokerContainer brokerContainer = startBrokerContainer(createBrokerContainer(args)); + return createAndInitializeBroker(brokerContainer, configFile, properties); + } + + public static List createAndStartBrokers(BrokerContainer brokerContainer) { + String[] configPaths = parseBrokerConfigPath(); + List brokerControllerList = new ArrayList<>(); + + if (configPaths != null && configPaths.length > 0) { + SystemConfigFileHelper configFileHelper = new SystemConfigFileHelper(); + for (String configPath : configPaths) { + System.out.printf("Start broker from config file path %s%n", configPath); + configFileHelper.setFile(configPath); + + Properties brokerProperties = null; + try { + brokerProperties = configFileHelper.loadConfig(); + } catch (Exception e) { + e.printStackTrace(); + System.exit(-1); + } + + final BrokerController brokerController = createAndInitializeBroker(brokerContainer, configPath, brokerProperties); + if (brokerController != null) { + brokerControllerList.add(brokerController); + startBrokerController(brokerContainer, brokerController, brokerProperties); + } + } + } + + return brokerControllerList; + } + + public static String[] parseBrokerConfigPath() { + String brokerConfigList = null; + if (commandLine.hasOption(BROKER_CONFIG_OPTION)) { + brokerConfigList = commandLine.getOptionValue(BROKER_CONFIG_OPTION); + + } else if (commandLine.hasOption(BROKER_CONTAINER_CONFIG_OPTION)) { + String brokerContainerConfigPath = commandLine.getOptionValue(BROKER_CONTAINER_CONFIG_OPTION); + if (brokerContainerConfigPath != null) { + BrokerContainerConfig brokerContainerConfig = new BrokerContainerConfig(); + SystemConfigFileHelper configFileHelper = new SystemConfigFileHelper(); + configFileHelper.setFile(brokerContainerConfigPath); + Properties brokerContainerProperties = null; + try { + brokerContainerProperties = configFileHelper.loadConfig(); + } catch (Exception e) { + e.printStackTrace(); + System.exit(-1); + } + if (brokerContainerProperties != null) { + MixAll.properties2Object(brokerContainerProperties, brokerContainerConfig); + } + brokerConfigList = brokerContainerConfig.getBrokerConfigPaths(); + } + } + + if (brokerConfigList != null) { + return brokerConfigList.split(":"); + } + return null; + } + + public static BrokerController createAndInitializeBroker(BrokerContainer brokerContainer, + String filePath, Properties brokerProperties) { + + final BrokerConfig brokerConfig = new BrokerConfig(); + final MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + + if (brokerProperties != null) { + properties2SystemEnv(brokerProperties); + MixAll.properties2Object(brokerProperties, brokerConfig); + MixAll.properties2Object(brokerProperties, messageStoreConfig); + } + + messageStoreConfig.setHaListenPort(brokerConfig.getListenPort() + 1); + + MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), brokerConfig); + + if (!brokerConfig.isEnableControllerMode()) { + switch (messageStoreConfig.getBrokerRole()) { + case ASYNC_MASTER: + case SYNC_MASTER: + brokerConfig.setBrokerId(MixAll.MASTER_ID); + break; + case SLAVE: + if (brokerConfig.getBrokerId() <= 0) { + System.out.printf("Slave's brokerId must be > 0%n"); + System.exit(-3); + } + + break; + default: + break; + } + } + + if (messageStoreConfig.getTotalReplicas() < messageStoreConfig.getInSyncReplicas() + || messageStoreConfig.getTotalReplicas() < messageStoreConfig.getMinInSyncReplicas() + || messageStoreConfig.getInSyncReplicas() < messageStoreConfig.getMinInSyncReplicas()) { + System.out.printf("invalid replicas number%n"); + System.exit(-3); + } + + brokerConfig.setBrokerConfigPath(filePath); + + log = LoggerFactory.getLogger(brokerConfig.getIdentifier() + LoggerName.BROKER_LOGGER_NAME); + MixAll.printObjectProperties(log, brokerConfig); + MixAll.printObjectProperties(log, messageStoreConfig); + + try { + BrokerController brokerController = brokerContainer.addBroker(brokerConfig, messageStoreConfig); + if (brokerController != null) { + brokerController.getConfiguration().registerConfig(brokerProperties); + return brokerController; + } else { + System.out.printf("Add broker [%s-%s] failed.%n", brokerConfig.getBrokerName(), brokerConfig.getBrokerId()); + } + } catch (Exception e) { + e.printStackTrace(); + System.exit(-1); + } + return null; + } + + public static BrokerContainer startBrokerContainer(BrokerContainer brokerContainer) { + try { + + brokerContainer.start(); + + String tip = "The broker container boot success. serializeType=" + RemotingCommand.getSerializeTypeConfigInThisServer(); + + if (null != brokerContainer.getBrokerContainerConfig().getNamesrvAddr()) { + tip += " and name server is " + brokerContainer.getBrokerContainerConfig().getNamesrvAddr(); + } + + log.info(tip); + System.out.printf("%s%n", tip); + return brokerContainer; + } catch (Throwable e) { + e.printStackTrace(); + System.exit(-1); + } + + return null; + } + + public static void startBrokerController(BrokerContainer brokerContainer, + BrokerController brokerController, Properties brokerProperties) { + try { + for (BrokerBootHook hook : brokerContainer.getBrokerBootHookList()) { + hook.executeBeforeStart(brokerController, brokerProperties); + } + + brokerController.start(); + + for (BrokerBootHook hook : brokerContainer.getBrokerBootHookList()) { + hook.executeAfterStart(brokerController, brokerProperties); + } + + String tip = String.format("Broker [%s-%s] boot success. serializeType=%s", + brokerController.getBrokerConfig().getBrokerName(), + brokerController.getBrokerConfig().getBrokerId(), + RemotingCommand.getSerializeTypeConfigInThisServer()); + + log.info(tip); + System.out.printf("%s%n", tip); + } catch (Throwable e) { + e.printStackTrace(); + System.exit(-1); + } + } + + public static void shutdown(final BrokerContainer controller) { + if (null != controller) { + controller.shutdown(); + } + } + + public static BrokerContainer createBrokerContainer(String[] args) { + System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION)); + + if (null == System.getProperty(NettySystemConfig.COM_ROCKETMQ_REMOTING_SOCKET_SNDBUF_SIZE)) { + NettySystemConfig.socketSndbufSize = 131072; + } + + if (null == System.getProperty(NettySystemConfig.COM_ROCKETMQ_REMOTING_SOCKET_RCVBUF_SIZE)) { + NettySystemConfig.socketRcvbufSize = 131072; + } + + try { + //PackageConflictDetect.detectFastjson(); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + commandLine = ServerUtil.parseCmdLine("mqbroker", args, buildCommandlineOptions(options), + new DefaultParser()); + if (null == commandLine) { + System.exit(-1); + } + + final BrokerContainerConfig containerConfig = new BrokerContainerConfig(); + final NettyServerConfig nettyServerConfig = new NettyServerConfig(); + final NettyClientConfig nettyClientConfig = new NettyClientConfig(); + nettyServerConfig.setListenPort(10811); + + if (commandLine.hasOption(BROKER_CONTAINER_CONFIG_OPTION)) { + String file = commandLine.getOptionValue(BROKER_CONTAINER_CONFIG_OPTION); + if (file != null) { + CONFIG_FILE_HELPER.setFile(file); + configFile = file; + BrokerPathConfigHelper.setBrokerConfigPath(file); + } + } + + properties = CONFIG_FILE_HELPER.loadConfig(); + if (properties != null) { + properties2SystemEnv(properties); + MixAll.properties2Object(properties, containerConfig); + MixAll.properties2Object(properties, nettyServerConfig); + MixAll.properties2Object(properties, nettyClientConfig); + } + + MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), containerConfig); + + if (null == containerConfig.getRocketmqHome()) { + System.out.printf("Please set the %s variable in your environment to match the location of the RocketMQ installation", MixAll.ROCKETMQ_HOME_ENV); + System.exit(-2); + } + rocketmqHome = containerConfig.getRocketmqHome(); + + String namesrvAddr = containerConfig.getNamesrvAddr(); + if (null != namesrvAddr) { + try { + String[] addrArray = namesrvAddr.split(";"); + for (String addr : addrArray) { + NetworkUtil.string2SocketAddress(addr); + } + } catch (Exception e) { + System.out.printf( + "The Name Server Address[%s] illegal, please set it as follows, \"127.0.0.1:9876;192.168.0.1:9876\"%n", + namesrvAddr); + System.exit(-3); + } + } + + if (commandLine.hasOption(PRINT_PROPERTIES_OPTION)) { + Logger console = LoggerFactory.getLogger(LoggerName.BROKER_CONSOLE_NAME); + MixAll.printObjectProperties(console, containerConfig); + MixAll.printObjectProperties(console, nettyServerConfig); + MixAll.printObjectProperties(console, nettyClientConfig); + System.exit(0); + } else if (commandLine.hasOption(PRINT_IMPORTANT_PROPERTIES_OPTION)) { + Logger console = LoggerFactory.getLogger(LoggerName.BROKER_CONSOLE_NAME); + MixAll.printObjectProperties(console, containerConfig, true); + MixAll.printObjectProperties(console, nettyServerConfig, true); + MixAll.printObjectProperties(console, nettyClientConfig, true); + System.exit(0); + } + + log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + MixAll.printObjectProperties(log, containerConfig); + MixAll.printObjectProperties(log, nettyServerConfig); + MixAll.printObjectProperties(log, nettyClientConfig); + + final BrokerContainer brokerContainer = new BrokerContainer( + containerConfig, + nettyServerConfig, + nettyClientConfig); + // remember all configs to prevent discard + brokerContainer.getConfiguration().registerConfig(properties); + + boolean initResult = brokerContainer.initialize(); + if (!initResult) { + brokerContainer.shutdown(); + System.exit(-3); + } + + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + private volatile boolean hasShutdown = false; + private AtomicInteger shutdownTimes = new AtomicInteger(0); + + @Override + public void run() { + synchronized (this) { + log.info("Shutdown hook was invoked, {}", this.shutdownTimes.incrementAndGet()); + if (!this.hasShutdown) { + this.hasShutdown = true; + long beginTime = System.currentTimeMillis(); + brokerContainer.shutdown(); + long consumingTimeTotal = System.currentTimeMillis() - beginTime; + log.info("Shutdown hook over, consuming total time(ms): {}", consumingTimeTotal); + } + } + } + }, "ShutdownHook")); + + return brokerContainer; + } catch (Throwable e) { + e.printStackTrace(); + System.exit(-1); + } + + return null; + } + + private static void properties2SystemEnv(Properties properties) { + if (properties == null) { + return; + } + String rmqAddressServerDomain = properties.getProperty("rmqAddressServerDomain", MixAll.WS_DOMAIN_NAME); + String rmqAddressServerSubGroup = properties.getProperty("rmqAddressServerSubGroup", MixAll.WS_DOMAIN_SUBGROUP); + System.setProperty("rocketmq.namesrv.domain", rmqAddressServerDomain); + System.setProperty("rocketmq.namesrv.domain.subgroup", rmqAddressServerSubGroup); + } + + private static Options buildCommandlineOptions(final Options options) { + Option opt = new Option(BROKER_CONTAINER_CONFIG_OPTION, "configFile", true, "Config file for shared broker"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option(PRINT_PROPERTIES_OPTION, "printConfigItem", false, "Print all config item"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option(PRINT_IMPORTANT_PROPERTIES_OPTION, "printImportantConfig", false, "Print important config item"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option(BROKER_CONFIG_OPTION, "brokerConfigFiles", true, "The path of broker config files, split by ':'"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + public static class SystemConfigFileHelper { + private static final Logger LOGGER = LoggerFactory.getLogger(SystemConfigFileHelper.class); + + private String file; + + public SystemConfigFileHelper() { + } + + public Properties loadConfig() throws Exception { + InputStream in = new BufferedInputStream(new FileInputStream(file)); + Properties properties = new Properties(); + properties.load(in); + in.close(); + return properties; + } + + public void update(Properties properties) throws Exception { + LOGGER.error("[SystemConfigFileHelper] update no thing."); + } + + public void setFile(String file) { + this.file = file; + } + + public String getFile() { + return file; + } + } + +} diff --git a/container/src/main/java/org/apache/rocketmq/container/ContainerClientHouseKeepingService.java b/container/src/main/java/org/apache/rocketmq/container/ContainerClientHouseKeepingService.java new file mode 100644 index 00000000000..90c912247ef --- /dev/null +++ b/container/src/main/java/org/apache/rocketmq/container/ContainerClientHouseKeepingService.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.container; + +import io.netty.channel.Channel; +import java.util.Collection; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.remoting.ChannelEventListener; + +public class ContainerClientHouseKeepingService implements ChannelEventListener { + private final IBrokerContainer brokerContainer; + + public ContainerClientHouseKeepingService(final IBrokerContainer brokerContainer) { + this.brokerContainer = brokerContainer; + } + + @Override + public void onChannelConnect(String remoteAddr, Channel channel) { + onChannelOperation(CallbackCode.CONNECT, remoteAddr, channel); + } + + @Override + public void onChannelClose(String remoteAddr, Channel channel) { + onChannelOperation(CallbackCode.CLOSE, remoteAddr, channel); + } + + @Override + public void onChannelException(String remoteAddr, Channel channel) { + onChannelOperation(CallbackCode.EXCEPTION, remoteAddr, channel); + } + + @Override + public void onChannelIdle(String remoteAddr, Channel channel) { + onChannelOperation(CallbackCode.IDLE, remoteAddr, channel); + } + + @Override + public void onChannelActive(String remoteAddr, Channel channel) { + onChannelOperation(CallbackCode.ACTIVE, remoteAddr, channel); + } + + private void onChannelOperation(CallbackCode callbackCode, String remoteAddr, Channel channel) { + Collection masterBrokers = this.brokerContainer.getMasterBrokers(); + Collection slaveBrokers = this.brokerContainer.getSlaveBrokers(); + + for (BrokerController masterBroker : masterBrokers) { + brokerOperation(masterBroker, callbackCode, remoteAddr, channel); + } + + for (InnerSalveBrokerController slaveBroker : slaveBrokers) { + brokerOperation(slaveBroker, callbackCode, remoteAddr, channel); + } + } + + private void brokerOperation(BrokerController brokerController, CallbackCode callbackCode, String remoteAddr, + Channel channel) { + if (callbackCode == CallbackCode.CONNECT) { + brokerController.getBrokerStatsManager().incChannelConnectNum(); + return; + } + boolean removed = brokerController.getProducerManager().doChannelCloseEvent(remoteAddr, channel); + removed &= brokerController.getConsumerManager().doChannelCloseEvent(remoteAddr, channel); + if (removed) { + switch (callbackCode) { + case CLOSE: + brokerController.getBrokerStatsManager().incChannelCloseNum(); + break; + case EXCEPTION: + brokerController.getBrokerStatsManager().incChannelExceptionNum(); + break; + case IDLE: + brokerController.getBrokerStatsManager().incChannelIdleNum(); + break; + default: + break; + } + } + } + + public enum CallbackCode { + /** + * onChannelConnect + */ + CONNECT, + /** + * onChannelClose + */ + CLOSE, + /** + * onChannelException + */ + EXCEPTION, + /** + * onChannelIdle + */ + IDLE, + /** + * onChannelActive + */ + ACTIVE + } +} diff --git a/container/src/main/java/org/apache/rocketmq/container/IBrokerContainer.java b/container/src/main/java/org/apache/rocketmq/container/IBrokerContainer.java new file mode 100644 index 00000000000..d3cdc05b87b --- /dev/null +++ b/container/src/main/java/org/apache/rocketmq/container/IBrokerContainer.java @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.container; + +import java.util.Collection; +import java.util.List; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.out.BrokerOuterAPI; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.BrokerIdentity; +import org.apache.rocketmq.remoting.RemotingServer; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.store.config.MessageStoreConfig; + +/** + * An interface for broker container to hold multiple master and slave brokers. + */ +public interface IBrokerContainer { + + /** + * Start broker container + */ + void start() throws Exception; + + /** + * Shutdown broker container and all the brokers inside. + */ + void shutdown(); + + /** + * Add a broker to this container with specific broker config. + * + * @param brokerConfig the specified broker config + * @param storeConfig the specified store config + * @return the added BrokerController or null if the broker already exists + * @throws Exception when initialize broker + */ + BrokerController addBroker(BrokerConfig brokerConfig, MessageStoreConfig storeConfig) throws Exception; + + /** + * Remove the broker from this container associated with the specific broker identity + * + * @param brokerIdentity the specific broker identity + * @return the removed BrokerController or null if the broker doesn't exists + */ + BrokerController removeBroker(BrokerIdentity brokerIdentity) throws Exception; + + /** + * Return the broker controller associated with the specific broker identity + * + * @param brokerIdentity the specific broker identity + * @return the associated messaging broker or null + */ + BrokerController getBroker(BrokerIdentity brokerIdentity); + + /** + * Return all the master brokers belong to this container + * + * @return the master broker list + */ + Collection getMasterBrokers(); + + /** + * Return all the slave brokers belong to this container + * + * @return the slave broker list + */ + Collection getSlaveBrokers(); + + /** + * Return all broker controller in this container + * + * @return all broker controller + */ + List getBrokerControllers(); + + /** + * Return the address of broker container. + * + * @return broker container address. + */ + String getBrokerContainerAddr(); + + /** + * Peek the first master broker in container. + * + * @return the first master broker in container + */ + BrokerController peekMasterBroker(); + + /** + * Return the config of the broker container + * + * @return the broker container config + */ + BrokerContainerConfig getBrokerContainerConfig(); + + /** + * Get netty server config. + * + * @return netty server config + */ + NettyServerConfig getNettyServerConfig(); + + /** + * Get netty client config. + * + * @return netty client config + */ + NettyClientConfig getNettyClientConfig(); + + /** + * Return the shared BrokerOuterAPI + * + * @return the shared BrokerOuterAPI + */ + BrokerOuterAPI getBrokerOuterAPI(); + + /** + * Return the shared RemotingServer + * + * @return the shared RemotingServer + */ + RemotingServer getRemotingServer(); +} diff --git a/container/src/main/java/org/apache/rocketmq/container/InnerBrokerController.java b/container/src/main/java/org/apache/rocketmq/container/InnerBrokerController.java new file mode 100644 index 00000000000..a1c1eecf590 --- /dev/null +++ b/container/src/main/java/org/apache/rocketmq/container/InnerBrokerController.java @@ -0,0 +1,185 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.container; + +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.out.BrokerOuterAPI; +import org.apache.rocketmq.common.AbstractBrokerRunnable; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.config.MessageStoreConfig; + +public class InnerBrokerController extends BrokerController { + protected BrokerContainer brokerContainer; + + public InnerBrokerController( + final BrokerContainer brokerContainer, + final BrokerConfig brokerConfig, + final MessageStoreConfig messageStoreConfig + ) { + super(brokerConfig, messageStoreConfig); + this.brokerContainer = brokerContainer; + this.brokerOuterAPI = this.brokerContainer.getBrokerOuterAPI(); + } + + @Override + protected void initializeRemotingServer() { + this.remotingServer = this.brokerContainer.getRemotingServer().newRemotingServer(brokerConfig.getListenPort()); + this.fastRemotingServer = this.brokerContainer.getRemotingServer().newRemotingServer(brokerConfig.getListenPort() - 2); + } + + @Override + protected void initializeScheduledTasks() { + initializeBrokerScheduledTasks(); + } + + @Override + public void start() throws Exception { + this.shouldStartTime = System.currentTimeMillis() + messageStoreConfig.getDisappearTimeAfterStart(); + + if (messageStoreConfig.getTotalReplicas() > 1 && this.brokerConfig.isEnableSlaveActingMaster()) { + isIsolated = true; + } + + startBasicService(); + + if (!isIsolated && !this.messageStoreConfig.isEnableDLegerCommitLog() && !this.messageStoreConfig.isDuplicationEnable()) { + changeSpecialServiceStatus(this.brokerConfig.getBrokerId() == MixAll.MASTER_ID); + this.registerBrokerAll(true, false, true); + } + + scheduledFutures.add(this.scheduledExecutorService.scheduleAtFixedRate(new AbstractBrokerRunnable(this.getBrokerIdentity()) { + @Override + public void run0() { + try { + if (System.currentTimeMillis() < shouldStartTime) { + BrokerController.LOG.info("Register to namesrv after {}", shouldStartTime); + return; + } + if (isIsolated) { + BrokerController.LOG.info("Skip register for broker is isolated"); + return; + } + InnerBrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister()); + } catch (Throwable e) { + BrokerController.LOG.error("registerBrokerAll Exception", e); + } + } + }, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)), TimeUnit.MILLISECONDS)); + + if (this.brokerConfig.isEnableSlaveActingMaster()) { + scheduleSendHeartbeat(); + + scheduledFutures.add(this.syncBrokerMemberGroupExecutorService.scheduleAtFixedRate(new AbstractBrokerRunnable(this.getBrokerIdentity()) { + @Override + public void run0() { + try { + InnerBrokerController.this.syncBrokerMemberGroup(); + } catch (Throwable e) { + BrokerController.LOG.error("sync BrokerMemberGroup error. ", e); + } + } + }, 1000, this.brokerConfig.getSyncBrokerMemberGroupPeriod(), TimeUnit.MILLISECONDS)); + } + + if (this.brokerConfig.isEnableControllerMode()) { + scheduleSendHeartbeat(); + } + + if (brokerConfig.isSkipPreOnline()) { + startServiceWithoutCondition(); + } + } + + @Override + public void shutdown() { + + shutdownBasicService(); + + for (ScheduledFuture scheduledFuture : scheduledFutures) { + scheduledFuture.cancel(true); + } + + if (this.remotingServer != null) { + this.brokerContainer.getRemotingServer().removeRemotingServer(brokerConfig.getListenPort()); + } + + if (this.fastRemotingServer != null) { + this.brokerContainer.getRemotingServer().removeRemotingServer(brokerConfig.getListenPort() - 2); + } + } + + @Override + public String getBrokerAddr() { + return this.brokerConfig.getBrokerIP1() + ":" + this.brokerConfig.getListenPort(); + } + + @Override + public String getHAServerAddr() { + return this.brokerConfig.getBrokerIP2() + ":" + this.messageStoreConfig.getHaListenPort(); + } + + @Override + public long getMinBrokerIdInGroup() { + return this.minBrokerIdInGroup; + } + + @Override + public int getListenPort() { + return this.brokerConfig.getListenPort(); + } + + public BrokerOuterAPI getBrokerOuterAPI() { + return brokerContainer.getBrokerOuterAPI(); + } + + public BrokerContainer getBrokerContainer() { + return this.brokerContainer; + } + + public NettyServerConfig getNettyServerConfig() { + return brokerContainer.getNettyServerConfig(); + } + + public NettyClientConfig getNettyClientConfig() { + return brokerContainer.getNettyClientConfig(); + } + + public MessageStore getMessageStoreByBrokerName(String brokerName) { + if (this.brokerConfig.getBrokerName().equals(brokerName)) { + return this.getMessageStore(); + } + BrokerController brokerController = this.brokerContainer.findBrokerControllerByBrokerName(brokerName); + if (brokerController != null) { + return brokerController.getMessageStore(); + } + return null; + } + + @Override + public BrokerController peekMasterBroker() { + if (brokerConfig.getBrokerId() == MixAll.MASTER_ID) { + return this; + } + return this.brokerContainer.peekMasterBroker(); + } +} diff --git a/container/src/main/java/org/apache/rocketmq/container/InnerSalveBrokerController.java b/container/src/main/java/org/apache/rocketmq/container/InnerSalveBrokerController.java new file mode 100644 index 00000000000..a7901bc7dc7 --- /dev/null +++ b/container/src/main/java/org/apache/rocketmq/container/InnerSalveBrokerController.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.container; + +import com.google.common.base.Preconditions; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.store.config.MessageStoreConfig; + +public class InnerSalveBrokerController extends InnerBrokerController { + + private final Lock lock = new ReentrantLock(); + + public InnerSalveBrokerController(final BrokerContainer brokerContainer, + final BrokerConfig brokerConfig, + final MessageStoreConfig storeConfig) { + super(brokerContainer, brokerConfig, storeConfig); + // Check configs + checkSlaveBrokerConfig(); + } + + private void checkSlaveBrokerConfig() { + Preconditions.checkNotNull(brokerConfig.getBrokerClusterName()); + Preconditions.checkNotNull(brokerConfig.getBrokerName()); + Preconditions.checkArgument(brokerConfig.getBrokerId() != MixAll.MASTER_ID); + } +} diff --git a/container/src/main/java/org/apache/rocketmq/container/logback/BrokerLogbackConfigurator.java b/container/src/main/java/org/apache/rocketmq/container/logback/BrokerLogbackConfigurator.java new file mode 100644 index 00000000000..bf51819542d --- /dev/null +++ b/container/src/main/java/org/apache/rocketmq/container/logback/BrokerLogbackConfigurator.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.container.logback; + +import java.util.HashSet; +import java.util.Set; + +import org.apache.rocketmq.common.BrokerIdentity; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class BrokerLogbackConfigurator { + private static final Logger LOG = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + + private static final Set CONFIGURED_BROKER_LIST = new HashSet<>(); + + public static final String ROCKETMQ_LOGS = "rocketmqlogs"; + public static final String ROCKETMQ_PREFIX = "Rocketmq"; + public static final String SUFFIX_CONSOLE = "Console"; + public static final String SUFFIX_APPENDER = "Appender"; + public static final String SUFFIX_INNER_APPENDER = "_inner"; + + public static void doConfigure(BrokerIdentity brokerIdentity) { + } +} diff --git a/container/src/test/java/org/apache/rocketmq/container/BrokerContainerStartupTest.java b/container/src/test/java/org/apache/rocketmq/container/BrokerContainerStartupTest.java new file mode 100644 index 00000000000..1b9ef6d0d27 --- /dev/null +++ b/container/src/test/java/org/apache/rocketmq/container/BrokerContainerStartupTest.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.container; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.assertj.core.util.Arrays; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.assertj.core.api.Java6Assertions.assertThat; + +@RunWith(MockitoJUnitRunner.class) +public class BrokerContainerStartupTest { + private static final List TMP_FILE_LIST = new ArrayList<>(); + private static final String BROKER_NAME_PREFIX = "TestBroker"; + private static final String SHARED_BROKER_NAME_PREFIX = "TestBrokerContainer"; + private static String brokerConfigPath; + private static String brokerContainerConfigPath; + + @Mock + private BrokerConfig brokerConfig; + private String storePathRootDir = "store/test"; + @Mock + private NettyClientConfig nettyClientConfig; + @Mock + private NettyServerConfig nettyServerConfig; + + @Before + public void init() throws IOException { + String brokerName = BROKER_NAME_PREFIX + "_" + System.currentTimeMillis(); + BrokerConfig brokerConfig = new BrokerConfig(); + brokerConfig.setBrokerName(brokerName); + if (brokerConfig.getRocketmqHome() == null) { + brokerConfig.setRocketmqHome("../distribution"); + } + MessageStoreConfig storeConfig = new MessageStoreConfig(); + String baseDir = createBaseDir(brokerConfig.getBrokerName() + "_" + brokerConfig.getBrokerId()).getAbsolutePath(); + storeConfig.setStorePathRootDir(baseDir); + storeConfig.setStorePathCommitLog(baseDir + File.separator + "commitlog"); + + brokerConfigPath = "/tmp/" + brokerName; + brokerConfig.setBrokerConfigPath(brokerConfigPath); + File file = new File(brokerConfigPath); + TMP_FILE_LIST.add(file); + Properties brokerConfigProp = MixAll.object2Properties(brokerConfig); + Properties storeConfigProp = MixAll.object2Properties(storeConfig); + + for (Object key : storeConfigProp.keySet()) { + brokerConfigProp.put(key, storeConfigProp.get(key)); + } + MixAll.string2File(MixAll.properties2String(brokerConfigProp), brokerConfigPath); + + brokerContainerConfigPath = "/tmp/" + SHARED_BROKER_NAME_PREFIX + System.currentTimeMillis(); + BrokerContainerConfig brokerContainerConfig = new BrokerContainerConfig(); + brokerContainerConfig.setBrokerConfigPaths(brokerConfigPath); + if (brokerContainerConfig.getRocketmqHome() == null) { + brokerContainerConfig.setRocketmqHome("../distribution"); + } + File file1 = new File(brokerContainerConfigPath); + TMP_FILE_LIST.add(file1); + Properties brokerContainerConfigProp = MixAll.object2Properties(brokerContainerConfig); + MixAll.string2File(MixAll.properties2String(brokerContainerConfigProp), brokerContainerConfigPath); + } + + @After + public void destroy() { + for (File file : TMP_FILE_LIST) { + UtilAll.deleteFile(file); + } + } + + @Test + public void testStartBrokerContainer() { + BrokerContainer brokerContainer = BrokerContainerStartup.startBrokerContainer( + BrokerContainerStartup.createBrokerContainer(Arrays.array("-c", brokerContainerConfigPath))); + assertThat(brokerContainer).isNotNull(); + List brokers = BrokerContainerStartup.createAndStartBrokers(brokerContainer); + assertThat(brokers.size()).isEqualTo(1); + + brokerContainer.shutdown(); + assertThat(brokerContainer.getBrokerControllers().size()).isEqualTo(0); + } + + private static File createBaseDir(String prefix) { + final File file; + try { + file = Files.createTempDirectory(prefix).toFile(); + TMP_FILE_LIST.add(file); + System.out.printf("create file at %s%n", file.getAbsolutePath()); + return file; + } catch (IOException e) { + throw new RuntimeException("Couldn't create tmp folder", e); + } + } + + @Before + public void clear() { + UtilAll.deleteFile(new File(storePathRootDir)); + } + + @After + public void tearDown() { + File configFile = new File(storePathRootDir); + UtilAll.deleteFile(configFile); + UtilAll.deleteEmptyDirectory(configFile); + UtilAll.deleteEmptyDirectory(configFile.getParentFile()); + } +} \ No newline at end of file diff --git a/container/src/test/java/org/apache/rocketmq/container/BrokerContainerTest.java b/container/src/test/java/org/apache/rocketmq/container/BrokerContainerTest.java new file mode 100644 index 00000000000..e02d9ac3b88 --- /dev/null +++ b/container/src/test/java/org/apache/rocketmq/container/BrokerContainerTest.java @@ -0,0 +1,379 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.container; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.out.BrokerOuterAPI; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.BrokerIdentity; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.constant.PermName; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigSerializeWrapper; +import org.apache.rocketmq.store.config.BrokerRole; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.After; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.Mockito.anyBoolean; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class BrokerContainerTest { + private static final List TMP_FILE_LIST = new ArrayList<>(); + private static final Random RANDOM = new Random(); + private static final Set PORTS_IN_USE = new HashSet<>(); + + /** + * Tests if the controller can be properly stopped and started. + * + * @throws Exception If fails. + */ + @Test + public void testBrokerContainerRestart() throws Exception { + BrokerContainer brokerController = new BrokerContainer( + new BrokerContainerConfig(), + new NettyServerConfig(), + new NettyClientConfig()); + assertThat(brokerController.initialize()).isTrue(); + brokerController.start(); + brokerController.shutdown(); + } + + @Test + public void testRegisterIncrementBrokerData() throws Exception { + BrokerController brokerController = new BrokerController( + new BrokerConfig(), + new NettyServerConfig(), + new NettyClientConfig(), + new MessageStoreConfig()); + + brokerController.getBrokerConfig().setEnableSlaveActingMaster(true); + + BrokerOuterAPI brokerOuterAPI = mock(BrokerOuterAPI.class); + Field field = BrokerController.class.getDeclaredField("brokerOuterAPI"); + field.setAccessible(true); + field.set(brokerController, brokerOuterAPI); + + List topicConfigList = new ArrayList<>(2); + for (int i = 0; i < 2; i++) { + topicConfigList.add(new TopicConfig("topic-" + i)); + } + DataVersion dataVersion = new DataVersion(); + + // Check normal condition. + testRegisterIncrementBrokerDataWithPerm(brokerController, brokerOuterAPI, + topicConfigList, dataVersion, PermName.PERM_READ | PermName.PERM_WRITE, 1); + // Check unwritable broker. + testRegisterIncrementBrokerDataWithPerm(brokerController, brokerOuterAPI, + topicConfigList, dataVersion, PermName.PERM_READ, 2); + // Check unreadable broker. + testRegisterIncrementBrokerDataWithPerm(brokerController, brokerOuterAPI, + topicConfigList, dataVersion, PermName.PERM_WRITE, 3); + } + + @Test + public void testRegisterIncrementBrokerDataPerm() throws Exception { + BrokerController brokerController = new BrokerController( + new BrokerConfig(), + new NettyServerConfig(), + new NettyClientConfig(), + new MessageStoreConfig()); + + brokerController.getBrokerConfig().setEnableSlaveActingMaster(true); + + BrokerOuterAPI brokerOuterAPI = mock(BrokerOuterAPI.class); + Field field = BrokerController.class.getDeclaredField("brokerOuterAPI"); + field.setAccessible(true); + field.set(brokerController, brokerOuterAPI); + + List topicConfigList = new ArrayList<>(2); + for (int i = 0; i < 2; i++) { + topicConfigList.add(new TopicConfig("topic-" + i)); + } + DataVersion dataVersion = new DataVersion(); + + brokerController.getBrokerConfig().setBrokerPermission(4); + + brokerController.registerIncrementBrokerData(topicConfigList, dataVersion); + // Get topicConfigSerializeWrapper created by registerIncrementBrokerData() from brokerOuterAPI.registerBrokerAll() + ArgumentCaptor captor = ArgumentCaptor.forClass(TopicConfigSerializeWrapper.class); + ArgumentCaptor brokerIdentityCaptor = ArgumentCaptor.forClass(BrokerIdentity.class); + verify(brokerOuterAPI).registerBrokerAll(anyString(), anyString(), anyString(), anyLong(), anyString(), + captor.capture(), ArgumentMatchers.anyList(), anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), anyLong(), brokerIdentityCaptor.capture()); + TopicConfigSerializeWrapper wrapper = captor.getValue(); + for (Map.Entry entry : wrapper.getTopicConfigTable().entrySet()) { + assertThat(entry.getValue().getPerm()).isEqualTo(brokerController.getBrokerConfig().getBrokerPermission()); + } + + } + + @Test + public void testMasterScaleOut() throws Exception { + BrokerContainer brokerContainer = new BrokerContainer( + new BrokerContainerConfig(), + new NettyServerConfig(), + new NettyClientConfig()); + assertThat(brokerContainer.initialize()).isTrue(); + brokerContainer.getBrokerContainerConfig().setNamesrvAddr("127.0.0.1:9876"); + brokerContainer.start(); + + BrokerConfig masterBrokerConfig = new BrokerConfig(); + + String baseDir = createBaseDir("unnittest-master").getAbsolutePath(); + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setStorePathRootDir(baseDir); + messageStoreConfig.setStorePathCommitLog(baseDir + File.separator + "commitlog"); + InnerBrokerController brokerController = brokerContainer.addBroker(masterBrokerConfig, messageStoreConfig); + assertThat(brokerController.isIsolated()).isFalse(); + + brokerContainer.shutdown(); + brokerController.getMessageStore().destroy(); + } + + @Test + public void testAddMasterFailed() throws Exception { + BrokerContainer brokerContainer = new BrokerContainer( + new BrokerContainerConfig(), + new NettyServerConfig(), + new NettyClientConfig()); + assertThat(brokerContainer.initialize()).isTrue(); + brokerContainer.start(); + + BrokerConfig masterBrokerConfig = new BrokerConfig(); + masterBrokerConfig.setListenPort(brokerContainer.getNettyServerConfig().getListenPort()); + boolean exceptionCaught = false; + try { + String baseDir = createBaseDir("unnittest-master").getAbsolutePath(); + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setStorePathRootDir(baseDir); + messageStoreConfig.setStorePathCommitLog(baseDir + File.separator + "commitlog"); + brokerContainer.addBroker(masterBrokerConfig, messageStoreConfig); + } catch (Exception e) { + exceptionCaught = true; + } finally { + brokerContainer.shutdown(); + + } + + assertThat(exceptionCaught).isTrue(); + } + + @Test + public void testAddSlaveFailed() throws Exception { + BrokerContainer sharedBrokerController = new BrokerContainer( + new BrokerContainerConfig(), + new NettyServerConfig(), + new NettyClientConfig()); + assertThat(sharedBrokerController.initialize()).isTrue(); + sharedBrokerController.start(); + + BrokerConfig slaveBrokerConfig = new BrokerConfig(); + slaveBrokerConfig.setBrokerId(1); + slaveBrokerConfig.setListenPort(sharedBrokerController.getNettyServerConfig().getListenPort()); + MessageStoreConfig slaveMessageStoreConfig = new MessageStoreConfig(); + slaveMessageStoreConfig.setBrokerRole(BrokerRole.SLAVE); + String baseDir = createBaseDir("unnittest-slave").getAbsolutePath(); + slaveMessageStoreConfig.setStorePathRootDir(baseDir); + slaveMessageStoreConfig.setStorePathCommitLog(baseDir + File.separator + "commitlog"); + boolean exceptionCaught = false; + try { + sharedBrokerController.addBroker(slaveBrokerConfig, slaveMessageStoreConfig); + } catch (Exception e) { + exceptionCaught = true; + } finally { + sharedBrokerController.shutdown(); + } + + assertThat(exceptionCaught).isTrue(); + } + + @Test + public void testAddAndRemoveMaster() throws Exception { + BrokerContainer brokerContainer = new BrokerContainer( + new BrokerContainerConfig(), + new NettyServerConfig(), + new NettyClientConfig()); + assertThat(brokerContainer.initialize()).isTrue(); + brokerContainer.start(); + + BrokerConfig masterBrokerConfig = new BrokerConfig(); + String baseDir = createBaseDir("unnittest-master").getAbsolutePath(); + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setStorePathRootDir(baseDir); + messageStoreConfig.setStorePathCommitLog(baseDir + File.separator + "commitlog"); + InnerBrokerController master = brokerContainer.addBroker(masterBrokerConfig, messageStoreConfig); + assertThat(master).isNotNull(); + master.start(); + assertThat(master.isIsolated()).isFalse(); + + brokerContainer.removeBroker(new BrokerIdentity(masterBrokerConfig.getBrokerClusterName(), masterBrokerConfig.getBrokerName(), masterBrokerConfig.getBrokerId())); + assertThat(brokerContainer.getMasterBrokers().size()).isEqualTo(0); + + brokerContainer.shutdown(); + master.getMessageStore().destroy(); + } + + @Test + public void testAddAndRemoveDLedgerBroker() throws Exception { + BrokerContainer brokerContainer = new BrokerContainer( + new BrokerContainerConfig(), + new NettyServerConfig(), + new NettyClientConfig()); + assertThat(brokerContainer.initialize()).isTrue(); + brokerContainer.start(); + + BrokerConfig dLedgerBrokerConfig = new BrokerConfig(); + String baseDir = createBaseDir("unnittest-dLedger").getAbsolutePath(); + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setStorePathRootDir(baseDir); + messageStoreConfig.setStorePathCommitLog(baseDir + File.separator + "commitlog"); + messageStoreConfig.setEnableDLegerCommitLog(true); + messageStoreConfig.setdLegerSelfId("n0"); + messageStoreConfig.setdLegerGroup("group"); + messageStoreConfig.setdLegerPeers(String.format("n0-localhost:%d", generatePort(30900, 10000))); + InnerBrokerController dLedger = brokerContainer.addBroker(dLedgerBrokerConfig, messageStoreConfig); + assertThat(dLedger).isNotNull(); + dLedger.start(); + assertThat(dLedger.isIsolated()).isFalse(); + + brokerContainer.removeBroker(new BrokerIdentity(dLedgerBrokerConfig.getBrokerClusterName(), dLedgerBrokerConfig.getBrokerName(), Integer.parseInt(messageStoreConfig.getdLegerSelfId().substring(1)))); + assertThat(brokerContainer.getMasterBrokers().size()).isEqualTo(0); + + brokerContainer.shutdown(); + dLedger.getMessageStore().destroy(); + } + + @Test + public void testAddAndRemoveSlaveSuccess() throws Exception { + BrokerContainer brokerContainer = new BrokerContainer( + new BrokerContainerConfig(), + new NettyServerConfig(), + new NettyClientConfig()); + assertThat(brokerContainer.initialize()).isTrue(); + brokerContainer.start(); + + BrokerConfig masterBrokerConfig = new BrokerConfig(); + String baseDir = createBaseDir("unnittest-master").getAbsolutePath(); + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setStorePathRootDir(baseDir); + messageStoreConfig.setStorePathCommitLog(baseDir + File.separator + "commitlog"); + InnerBrokerController master = brokerContainer.addBroker(masterBrokerConfig, messageStoreConfig); + assertThat(master).isNotNull(); + master.start(); + assertThat(master.isIsolated()).isFalse(); + + BrokerConfig slaveBrokerConfig = new BrokerConfig(); + slaveBrokerConfig.setListenPort(generatePort(masterBrokerConfig.getListenPort(), 10000)); + slaveBrokerConfig.setBrokerId(1); + MessageStoreConfig slaveMessageStoreConfig = new MessageStoreConfig(); + slaveMessageStoreConfig.setBrokerRole(BrokerRole.SLAVE); + slaveMessageStoreConfig.setHaListenPort(generatePort(messageStoreConfig.getHaListenPort(), 10000)); + baseDir = createBaseDir("unnittest-slave").getAbsolutePath(); + slaveMessageStoreConfig.setStorePathRootDir(baseDir); + slaveMessageStoreConfig.setStorePathCommitLog(baseDir + File.separator + "commitlog"); + InnerBrokerController slave = brokerContainer.addBroker(slaveBrokerConfig, slaveMessageStoreConfig); + assertThat(slave).isNotNull(); + slave.start(); + assertThat(slave.isIsolated()).isFalse(); + + brokerContainer.removeBroker(new BrokerIdentity(slaveBrokerConfig.getBrokerClusterName(), slaveBrokerConfig.getBrokerName(), slaveBrokerConfig.getBrokerId())); + assertThat(brokerContainer.getSlaveBrokers().size()).isEqualTo(0); + + brokerContainer.removeBroker(new BrokerIdentity(masterBrokerConfig.getBrokerClusterName(), masterBrokerConfig.getBrokerName(), masterBrokerConfig.getBrokerId())); + assertThat(brokerContainer.getMasterBrokers().size()).isEqualTo(0); + + brokerContainer.shutdown(); + slave.getMessageStore().destroy(); + master.getMessageStore().destroy(); + } + + private static File createBaseDir(String prefix) { + final File file; + try { + file = Files.createTempDirectory(prefix).toFile(); + TMP_FILE_LIST.add(file); + return file; + } catch (IOException e) { + throw new RuntimeException("Couldn't create tmp folder", e); + } + } + + public static int generatePort(int base, int range) { + int result = base + RANDOM.nextInt(range); + while (PORTS_IN_USE.contains(result) || PORTS_IN_USE.contains(result - 2)) { + result = base + RANDOM.nextInt(range); + } + PORTS_IN_USE.add(result); + PORTS_IN_USE.add(result - 2); + return result; + } + + @After + public void destroy() { + for (File file : TMP_FILE_LIST) { + UtilAll.deleteFile(file); + } + } + + private void testRegisterIncrementBrokerDataWithPerm(BrokerController brokerController, + BrokerOuterAPI brokerOuterAPI, + List topicConfigList, DataVersion dataVersion, int perm, int times) { + brokerController.getBrokerConfig().setBrokerPermission(perm); + + brokerController.registerIncrementBrokerData(topicConfigList, dataVersion); + // Get topicConfigSerializeWrapper created by registerIncrementBrokerData() from brokerOuterAPI.registerBrokerAll() + ArgumentCaptor captor = ArgumentCaptor.forClass(TopicConfigSerializeWrapper.class); + ArgumentCaptor brokerIdentityCaptor = ArgumentCaptor.forClass(BrokerIdentity.class); + verify(brokerOuterAPI, times(times)).registerBrokerAll(anyString(), anyString(), anyString(), anyLong(), + anyString(), captor.capture(), ArgumentMatchers.anyList(), anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), anyLong(), brokerIdentityCaptor.capture()); + TopicConfigSerializeWrapper wrapper = captor.getValue(); + + for (TopicConfig topicConfig : topicConfigList) { + topicConfig.setPerm(perm); + } + assertThat(wrapper.getDataVersion()).isEqualTo(dataVersion); + assertThat(wrapper.getTopicConfigTable()).containsExactly( + entry("topic-0", topicConfigList.get(0)), + entry("topic-1", topicConfigList.get(1))); + for (TopicConfig topicConfig : topicConfigList) { + topicConfig.setPerm(PermName.PERM_READ | PermName.PERM_WRITE); + } + } +} diff --git a/container/src/test/java/org/apache/rocketmq/container/BrokerPreOnlineTest.java b/container/src/test/java/org/apache/rocketmq/container/BrokerPreOnlineTest.java new file mode 100644 index 00000000000..2158b8d9aca --- /dev/null +++ b/container/src/test/java/org/apache/rocketmq/container/BrokerPreOnlineTest.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.container; + +import java.lang.reflect.Field; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.BrokerPreOnlineService; +import org.apache.rocketmq.broker.out.BrokerOuterAPI; +import org.apache.rocketmq.broker.transaction.TransactionalMessageCheckService; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.remoting.protocol.body.BrokerMemberGroup; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.awaitility.Awaitility.await; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class BrokerPreOnlineTest { + @Mock + private BrokerContainer brokerContainer; + + private InnerBrokerController innerBrokerController; + + @Mock + private BrokerOuterAPI brokerOuterAPI; + + public void init() throws Exception { + when(brokerContainer.getBrokerOuterAPI()).thenReturn(brokerOuterAPI); + + BrokerMemberGroup brokerMemberGroup1 = new BrokerMemberGroup(); + Map brokerAddrMap = new HashMap<>(); + brokerAddrMap.put(1L, "127.0.0.1:20911"); + brokerMemberGroup1.setBrokerAddrs(brokerAddrMap); + + BrokerMemberGroup brokerMemberGroup2 = new BrokerMemberGroup(); + brokerMemberGroup2.setBrokerAddrs(new HashMap<>()); + +// when(brokerOuterAPI.syncBrokerMemberGroup(anyString(), anyString())) +// .thenReturn(brokerMemberGroup1) +// .thenReturn(brokerMemberGroup2); +// doNothing().when(brokerOuterAPI).sendBrokerHaInfo(anyString(), anyString(), anyLong(), anyString()); + + DefaultMessageStore defaultMessageStore = mock(DefaultMessageStore.class); + when(defaultMessageStore.getMessageStoreConfig()).thenReturn(new MessageStoreConfig()); + when(defaultMessageStore.getBrokerConfig()).thenReturn(new BrokerConfig()); + +// HAService haService = new DefaultHAService(); +// haService.init(defaultMessageStore); +// haService.start(); +// +// when(defaultMessageStore.getHaService()).thenReturn(haService); + + innerBrokerController = new InnerBrokerController(brokerContainer, + defaultMessageStore.getBrokerConfig(), + defaultMessageStore.getMessageStoreConfig()); + + innerBrokerController.setTransactionalMessageCheckService(new TransactionalMessageCheckService(innerBrokerController)); + + Field field = BrokerController.class.getDeclaredField("isIsolated"); + field.setAccessible(true); + field.set(innerBrokerController, true); + + field = BrokerController.class.getDeclaredField("messageStore"); + field.setAccessible(true); + field.set(innerBrokerController, defaultMessageStore); + } + + @Test + public void testMasterOnlineConnTimeout() throws Exception { + init(); + BrokerPreOnlineService brokerPreOnlineService = new BrokerPreOnlineService(innerBrokerController); + + brokerPreOnlineService.start(); + + await().atMost(Duration.ofSeconds(30)).until(() -> !innerBrokerController.isIsolated()); + } +} diff --git a/container/src/test/resources/rmq.logback-test.xml b/container/src/test/resources/rmq.logback-test.xml new file mode 100644 index 00000000000..8695d52d57c --- /dev/null +++ b/container/src/test/resources/rmq.logback-test.xml @@ -0,0 +1,36 @@ + + + + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + \ No newline at end of file diff --git a/controller/BUILD.bazel b/controller/BUILD.bazel new file mode 100644 index 00000000000..73c2cf3395c --- /dev/null +++ b/controller/BUILD.bazel @@ -0,0 +1,93 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +load("//bazel:GenTestRules.bzl", "GenTestRules") + +java_library( + name = "controller", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//common", + "//remoting", + "//client", + "//srvutil", + "@maven//:io_openmessaging_storage_dledger", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:commons_validator_commons_validator", + "@maven//:commons_collections_commons_collections", + "@maven//:commons_codec_commons_codec", + "@maven//:com_github_luben_zstd_jni", + "@maven//:org_lz4_lz4_java", + "@maven//:com_alibaba_fastjson", + "@maven//:io_netty_netty_all", + "@maven//:com_google_guava_guava", + "@maven//:org_slf4j_slf4j_api", + "@maven//:ch_qos_logback_logback_core", + "@maven//:ch_qos_logback_logback_classic", + "@maven//:commons_cli_commons_cli", + "@maven//:io_github_aliyunmq_rocketmq_slf4j_api", + "@maven//:io_github_aliyunmq_rocketmq_logback_classic", + "@maven//:io_opentelemetry_opentelemetry_api", + "@maven//:io_opentelemetry_opentelemetry_context", + "@maven//:io_opentelemetry_opentelemetry_exporter_otlp", + "@maven//:io_opentelemetry_opentelemetry_exporter_prometheus", + "@maven//:io_opentelemetry_opentelemetry_sdk", + "@maven//:io_opentelemetry_opentelemetry_sdk_common", + "@maven//:io_opentelemetry_opentelemetry_sdk_metrics", + "@maven//:io_opentelemetry_opentelemetry_exporter_logging", + "@maven//:io_opentelemetry_opentelemetry_exporter_logging_otlp", + "@maven//:org_slf4j_jul_to_slf4j", + "@maven//:com_alipay_sofa_jraft_core", + "@maven//:com_alipay_sofa_hessian", + "@maven//:commons_io_commons_io", + ], +) + +java_library( + name = "tests", + srcs = glob(["src/test/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + ":controller", + "//common", + "//remoting", + "//client", + "//srvutil", + "@maven//:io_openmessaging_storage_dledger", + "//:test_deps", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:io_netty_netty_all", + "@maven//:com_google_guava_guava", + "@maven//:com_alibaba_fastjson", + ], + resources = glob(["src/test/resources/certs/*.pem"]) + glob(["src/test/resources/certs/*.key"]) +) + +GenTestRules( + name = "GeneratedTestRules", + test_files = glob(["src/test/java/**/*Test.java"]), + deps = [ + ":tests", + ], + exclude_tests = [ + # This test is buggy, exclude it. + "src/test/java/org/apache/rocketmq/controller/impl/DLedgerControllerTest", + ], + medium_tests = [ + "src/test/java/org/apache/rocketmq/controller/impl/controller/impl/DLedgerControllerTest", + ], +) diff --git a/controller/pom.xml b/controller/pom.xml new file mode 100644 index 00000000000..f147e6f6705 --- /dev/null +++ b/controller/pom.xml @@ -0,0 +1,74 @@ + + + + + rocketmq-all + org.apache.rocketmq + 5.2.0 + + 4.0.0 + jar + rocketmq-controller + rocketmq-controller ${project.version} + + + ${basedir}/.. + + + + + io.openmessaging.storage + dledger + + + org.apache.rocketmq + rocketmq-remoting + + + + + org.slf4j + slf4j-api + + + io.github.aliyunmq + rocketmq-shaded-slf4j-api-bridge + + + ${project.groupId} + rocketmq-client + test + + + ${project.groupId} + rocketmq-srvutil + + + org.slf4j + jul-to-slf4j + + + com.alipay.sofa + jraft-core + + + com.google.protobuf + protobuf-java-util + + + \ No newline at end of file diff --git a/controller/src/main/java/org/apache/rocketmq/controller/BrokerHeartbeatManager.java b/controller/src/main/java/org/apache/rocketmq/controller/BrokerHeartbeatManager.java new file mode 100644 index 00000000000..dea393f3c5a --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/BrokerHeartbeatManager.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller; + +import io.netty.channel.Channel; +import org.apache.rocketmq.common.ControllerConfig; +import org.apache.rocketmq.controller.helper.BrokerLifecycleListener; +import org.apache.rocketmq.controller.impl.heartbeat.BrokerLiveInfo; +import org.apache.rocketmq.controller.impl.heartbeat.DefaultBrokerHeartbeatManager; +import org.apache.rocketmq.controller.impl.heartbeat.RaftBrokerHeartBeatManager; + +import java.util.Map; + +public interface BrokerHeartbeatManager { + public static final long DEFAULT_BROKER_CHANNEL_EXPIRED_TIME = 1000 * 10; + + public static BrokerHeartbeatManager newBrokerHeartbeatManager(ControllerConfig controllerConfig) { + if (controllerConfig.getControllerType().equals(ControllerConfig.JRAFT_CONTROLLER)) { + return new RaftBrokerHeartBeatManager(controllerConfig); + } else { + return new DefaultBrokerHeartbeatManager(controllerConfig); + } + } + + /** + * initialize the resources + * + * @return + */ + void initialize(); + + /** + * Broker new heartbeat. + */ + void onBrokerHeartbeat(final String clusterName, final String brokerName, final String brokerAddr, + final Long brokerId, final Long timeoutMillis, final Channel channel, final Integer epoch, + final Long maxOffset, final Long confirmOffset, final Integer electionPriority); + + /** + * Start heartbeat manager. + */ + void start(); + + /** + * Shutdown heartbeat manager. + */ + void shutdown(); + + /** + * Add BrokerLifecycleListener. + */ + void registerBrokerLifecycleListener(final BrokerLifecycleListener listener); + + /** + * Broker channel close + */ + void onBrokerChannelClose(final Channel channel); + + /** + * Get broker live information by clusterName and brokerAddr + */ + BrokerLiveInfo getBrokerLiveInfo(String clusterName, String brokerName, Long brokerId); + + /** + * Check whether broker active + */ + boolean isBrokerActive(final String clusterName, final String brokerName, final Long brokerId); + + /** + * Count the number of active brokers in each broker-set of each cluster + * + * @return active brokers count + */ + Map> getActiveBrokersNum(); +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/BrokerHousekeepingService.java b/controller/src/main/java/org/apache/rocketmq/controller/BrokerHousekeepingService.java new file mode 100644 index 00000000000..d22d0b6069b --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/BrokerHousekeepingService.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller; + +import io.netty.channel.Channel; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.ChannelEventListener; + +public class BrokerHousekeepingService implements ChannelEventListener { + private static final Logger log = LoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME); + private final ControllerManager controllerManager; + + public BrokerHousekeepingService(ControllerManager controllerManager) { + this.controllerManager = controllerManager; + } + + @Override + public void onChannelConnect(String remoteAddr, Channel channel) { + } + + @Override + public void onChannelClose(String remoteAddr, Channel channel) { + this.controllerManager.getHeartbeatManager().onBrokerChannelClose(channel); + } + + @Override + public void onChannelException(String remoteAddr, Channel channel) { + this.controllerManager.getHeartbeatManager().onBrokerChannelClose(channel); + } + + @Override + public void onChannelIdle(String remoteAddr, Channel channel) { + this.controllerManager.getHeartbeatManager().onBrokerChannelClose(channel); + } + + @Override + public void onChannelActive(String remoteAddr, Channel channel) { + + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/Controller.java b/controller/src/main/java/org/apache/rocketmq/controller/Controller.java new file mode 100644 index 00000000000..cda613091e3 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/Controller.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.controller; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.apache.rocketmq.controller.helper.BrokerLifecycleListener; +import org.apache.rocketmq.remoting.RemotingServer; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.body.SyncStateSet; +import org.apache.rocketmq.remoting.protocol.header.controller.AlterSyncStateSetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.admin.CleanControllerBrokerDataRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetReplicaInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.ApplyBrokerIdRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.GetNextBrokerIdRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.RegisterBrokerToControllerRequestHeader; + +/** + * The api for controller + */ +public interface Controller { + + /** + * Startup controller + */ + void startup(); + + /** + * Shutdown controller + */ + void shutdown(); + + /** + * Start scheduling controller events, this function only will be triggered when the controller becomes leader. + */ + void startScheduling(); + + /** + * Stop scheduling controller events, this function only will be triggered when the controller lose leadership. + */ + void stopScheduling(); + + /** + * Whether this controller is in leader state. + */ + boolean isLeaderState(); + + /** + * Alter SyncStateSet of broker replicas. + * + * @param request AlterSyncStateSetRequestHeader + * @return RemotingCommand(AlterSyncStateSetResponseHeader) + */ + CompletableFuture alterSyncStateSet( + final AlterSyncStateSetRequestHeader request, final SyncStateSet syncStateSet); + + /** + * Elect new master for a broker. + * + * @param request ElectMasterRequest + * @return RemotingCommand(ElectMasterResponseHeader) + */ + CompletableFuture electMaster(final ElectMasterRequestHeader request); + + CompletableFuture getNextBrokerId(final GetNextBrokerIdRequestHeader request); + + CompletableFuture applyBrokerId(final ApplyBrokerIdRequestHeader request); + + /** + * Register broker with unique brokerId and now broker address + * + * @param request RegisterBrokerToControllerRequest + * @return RemotingCommand(RegisterBrokerToControllerResponseHeader) + */ + CompletableFuture registerBroker(final RegisterBrokerToControllerRequestHeader request); + + /** + * Get the Replica Info for a target broker. + * + * @param request GetRouteInfoRequest + * @return RemotingCommand(GetReplicaInfoResponseHeader) + */ + CompletableFuture getReplicaInfo(final GetReplicaInfoRequestHeader request); + + /** + * Get Metadata of controller + * + * @return RemotingCommand(GetControllerMetadataResponseHeader) + */ + RemotingCommand getControllerMetadata(); + + /** + * Get SyncStateData for target brokers, this api is used for admin tools. + */ + CompletableFuture getSyncStateData(final List brokerNames); + + /** + * Add broker's lifecycle listener + * @param listener listener + */ + void registerBrokerLifecycleListener(final BrokerLifecycleListener listener); + + /** + * Get the remotingServer used by the controller, the upper layer will reuse this remotingServer. + */ + RemotingServer getRemotingServer(); + + /** + * Clean controller broker data + * + */ + CompletableFuture cleanBrokerData(final CleanControllerBrokerDataRequestHeader requestHeader); +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/ControllerManager.java b/controller/src/main/java/org/apache/rocketmq/controller/ControllerManager.java new file mode 100644 index 00000000000..6dad631405c --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/ControllerManager.java @@ -0,0 +1,383 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller; + +import java.io.IOException; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.ControllerConfig; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.controller.elect.impl.DefaultElectPolicy; +import org.apache.rocketmq.controller.impl.DLedgerController; +import org.apache.rocketmq.controller.impl.JRaftController; +import org.apache.rocketmq.controller.impl.heartbeat.RaftBrokerHeartBeatManager; +import org.apache.rocketmq.controller.metrics.ControllerMetricsManager; +import org.apache.rocketmq.controller.processor.ControllerRequestProcessor; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.Configuration; +import org.apache.rocketmq.remoting.RemotingClient; +import org.apache.rocketmq.remoting.RemotingServer; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyRemotingClient; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.BrokerMemberGroup; +import org.apache.rocketmq.remoting.protocol.body.RoleChangeNotifyEntry; +import org.apache.rocketmq.remoting.protocol.body.SyncStateSet; +import org.apache.rocketmq.remoting.protocol.header.NotifyBrokerRoleChangedRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetReplicaInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetReplicaInfoResponseHeader; + +public class ControllerManager { + private static final Logger log = LoggerFactory.getLogger(LoggerName.CONTROLLER_LOGGER_NAME); + + private final ControllerConfig controllerConfig; + private final NettyServerConfig nettyServerConfig; + private final NettyClientConfig nettyClientConfig; + private final BrokerHousekeepingService brokerHousekeepingService; + private final Configuration configuration; + private final RemotingClient remotingClient; + private Controller controller; + private final BrokerHeartbeatManager heartbeatManager; + private ExecutorService controllerRequestExecutor; + private BlockingQueue controllerRequestThreadPoolQueue; + private final NotifyService notifyService; + private ControllerMetricsManager controllerMetricsManager; + + public ControllerManager(ControllerConfig controllerConfig, NettyServerConfig nettyServerConfig, + NettyClientConfig nettyClientConfig) { + this.controllerConfig = controllerConfig; + this.nettyServerConfig = nettyServerConfig; + this.nettyClientConfig = nettyClientConfig; + this.brokerHousekeepingService = new BrokerHousekeepingService(this); + this.configuration = new Configuration(log, this.controllerConfig, this.nettyServerConfig); + this.configuration.setStorePathFromConfig(this.controllerConfig, "configStorePath"); + this.remotingClient = new NettyRemotingClient(nettyClientConfig); + this.heartbeatManager = BrokerHeartbeatManager.newBrokerHeartbeatManager(controllerConfig); + this.notifyService = new NotifyService(); + } + + public boolean initialize() { + this.controllerRequestThreadPoolQueue = new LinkedBlockingQueue<>(this.controllerConfig.getControllerRequestThreadPoolQueueCapacity()); + this.controllerRequestExecutor = ThreadUtils.newThreadPoolExecutor( + this.controllerConfig.getControllerThreadPoolNums(), + this.controllerConfig.getControllerThreadPoolNums(), + 1000 * 60, + TimeUnit.MILLISECONDS, + this.controllerRequestThreadPoolQueue, + new ThreadFactoryImpl("ControllerRequestExecutorThread_")); + + this.notifyService.initialize(); + + if (controllerConfig.getControllerType().equals(ControllerConfig.JRAFT_CONTROLLER)) { + if (StringUtils.isEmpty(this.controllerConfig.getJraftConfig().getjRaftInitConf())) { + throw new IllegalArgumentException("Attribute value jRaftInitConf of ControllerConfig is null or empty"); + } + if (StringUtils.isEmpty(this.controllerConfig.getJraftConfig().getjRaftServerId())) { + throw new IllegalArgumentException("Attribute value jRaftServerId of ControllerConfig is null or empty"); + } + try { + this.controller = new JRaftController(controllerConfig, this.brokerHousekeepingService); + ((RaftBrokerHeartBeatManager) this.heartbeatManager).setController((JRaftController) this.controller); + } catch (IOException e) { + throw new RuntimeException(e); + } + } else { + if (StringUtils.isEmpty(this.controllerConfig.getControllerDLegerPeers())) { + throw new IllegalArgumentException("Attribute value controllerDLegerPeers of ControllerConfig is null or empty"); + } + if (StringUtils.isEmpty(this.controllerConfig.getControllerDLegerSelfId())) { + throw new IllegalArgumentException("Attribute value controllerDLegerSelfId of ControllerConfig is null or empty"); + } + this.controller = new DLedgerController(this.controllerConfig, this.heartbeatManager::isBrokerActive, + this.nettyServerConfig, this.nettyClientConfig, this.brokerHousekeepingService, + new DefaultElectPolicy(this.heartbeatManager::isBrokerActive, this.heartbeatManager::getBrokerLiveInfo)); + } + + // Initialize the basic resources + this.heartbeatManager.initialize(); + + // Register broker inactive listener + this.heartbeatManager.registerBrokerLifecycleListener(this::onBrokerInactive); + this.controller.registerBrokerLifecycleListener(this::onBrokerInactive); + registerProcessor(); + this.controllerMetricsManager = ControllerMetricsManager.getInstance(this); + return true; + } + + /** + * When the heartbeatManager detects the "Broker is not active", we call this method to elect a master and do + * something else. + * + * @param clusterName The cluster name of this inactive broker + * @param brokerName The inactive broker name + * @param brokerId The inactive broker id, null means that the election forced to be triggered + */ + private void onBrokerInactive(String clusterName, String brokerName, Long brokerId) { + log.info("Controller Manager received broker inactive event, clusterName: {}, brokerName: {}, brokerId: {}", + clusterName, brokerName, brokerId); + if (controller.isLeaderState()) { + if (brokerId == null) { + // Means that force triggering election for this broker-set + triggerElectMaster(brokerName); + return; + } + final CompletableFuture replicaInfoFuture = controller.getReplicaInfo(new GetReplicaInfoRequestHeader(brokerName)); + replicaInfoFuture.whenCompleteAsync((replicaInfoResponse, err) -> { + if (err != null || replicaInfoResponse == null) { + log.error("Failed to get replica-info for broker-set: {} when OnBrokerInactive", brokerName, err); + return; + } + final GetReplicaInfoResponseHeader replicaInfoResponseHeader = (GetReplicaInfoResponseHeader) replicaInfoResponse.readCustomHeader(); + // Not master broker offline + if (!brokerId.equals(replicaInfoResponseHeader.getMasterBrokerId())) { + log.warn("The broker with brokerId: {} in broker-set: {} has been inactive", brokerId, brokerName); + return; + } + // Trigger election + triggerElectMaster(brokerName); + }); + } else { + log.warn("The broker with brokerId: {} in broker-set: {} has been inactive", brokerId, brokerName); + } + } + + private CompletableFuture triggerElectMaster0(String brokerName) { + final CompletableFuture electMasterFuture = controller.electMaster(ElectMasterRequestHeader.ofControllerTrigger(brokerName)); + return electMasterFuture.handleAsync((electMasterResponse, err) -> { + if (err != null || electMasterResponse == null || electMasterResponse.getCode() != ResponseCode.SUCCESS) { + log.error("Failed to trigger elect-master in broker-set: {}", brokerName, err); + return false; + } + if (electMasterResponse.getCode() == ResponseCode.SUCCESS) { + log.info("Elect a new master in broker-set: {} done, result: {}", brokerName, electMasterResponse); + if (controllerConfig.isNotifyBrokerRoleChanged()) { + notifyBrokerRoleChanged(RoleChangeNotifyEntry.convert(electMasterResponse)); + } + return true; + } + //default is false + return false; + }); + } + + private void triggerElectMaster(String brokerName) { + int maxRetryCount = controllerConfig.getElectMasterMaxRetryCount(); + for (int i = 0; i < maxRetryCount; i++) { + try { + Boolean electResult = triggerElectMaster0(brokerName).get(3, TimeUnit.SECONDS); + if (electResult) { + return; + } + } catch (Exception e) { + log.warn("Failed to trigger elect-master in broker-set: {}, retryCount: {}", brokerName, i, e); + } + } + } + + /** + * Notify master and all slaves for a broker that the master role changed. + */ + public void notifyBrokerRoleChanged(final RoleChangeNotifyEntry entry) { + final BrokerMemberGroup memberGroup = entry.getBrokerMemberGroup(); + if (memberGroup != null) { + final Long masterBrokerId = entry.getMasterBrokerId(); + String clusterName = memberGroup.getCluster(); + String brokerName = memberGroup.getBrokerName(); + if (masterBrokerId == null) { + log.warn("Notify broker role change failed, because member group is not null but the new master brokerId is empty, entry:{}", entry); + return; + } + // Inform all active brokers + final Map brokerAddrs = memberGroup.getBrokerAddrs(); + brokerAddrs.entrySet().stream().filter(x -> this.heartbeatManager.isBrokerActive(clusterName, brokerName, x.getKey())) + .forEach(x -> this.notifyService.notifyBroker(x.getValue(), entry)); + } + } + + /** + * Notify broker that there are roles-changing in controller + * + * @param brokerAddr target broker's address to notify + * @param entry role change entry + */ + public void doNotifyBrokerRoleChanged(final String brokerAddr, final RoleChangeNotifyEntry entry) { + if (StringUtils.isNoneEmpty(brokerAddr)) { + log.info("Try notify broker {} that role changed, RoleChangeNotifyEntry:{}", brokerAddr, entry); + final NotifyBrokerRoleChangedRequestHeader requestHeader = new NotifyBrokerRoleChangedRequestHeader(entry.getMasterAddress(), entry.getMasterBrokerId(), + entry.getMasterEpoch(), entry.getSyncStateSetEpoch()); + final RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.NOTIFY_BROKER_ROLE_CHANGED, requestHeader); + request.setBody(new SyncStateSet(entry.getSyncStateSet(), entry.getSyncStateSetEpoch()).encode()); + try { + this.remotingClient.invokeOneway(brokerAddr, request, 3000); + } catch (final Exception e) { + log.error("Failed to notify broker {} that role changed", brokerAddr, e); + } + } + } + + public void registerProcessor() { + final ControllerRequestProcessor controllerRequestProcessor = new ControllerRequestProcessor(this); + RemotingServer controllerRemotingServer = this.controller.getRemotingServer(); + assert controllerRemotingServer != null; + controllerRemotingServer.registerProcessor(RequestCode.CONTROLLER_ALTER_SYNC_STATE_SET, controllerRequestProcessor, this.controllerRequestExecutor); + controllerRemotingServer.registerProcessor(RequestCode.CONTROLLER_ELECT_MASTER, controllerRequestProcessor, this.controllerRequestExecutor); + controllerRemotingServer.registerProcessor(RequestCode.CONTROLLER_REGISTER_BROKER, controllerRequestProcessor, this.controllerRequestExecutor); + controllerRemotingServer.registerProcessor(RequestCode.CONTROLLER_GET_REPLICA_INFO, controllerRequestProcessor, this.controllerRequestExecutor); + controllerRemotingServer.registerProcessor(RequestCode.CONTROLLER_GET_METADATA_INFO, controllerRequestProcessor, this.controllerRequestExecutor); + controllerRemotingServer.registerProcessor(RequestCode.CONTROLLER_GET_SYNC_STATE_DATA, controllerRequestProcessor, this.controllerRequestExecutor); + controllerRemotingServer.registerProcessor(RequestCode.BROKER_HEARTBEAT, controllerRequestProcessor, this.controllerRequestExecutor); + controllerRemotingServer.registerProcessor(RequestCode.UPDATE_CONTROLLER_CONFIG, controllerRequestProcessor, this.controllerRequestExecutor); + controllerRemotingServer.registerProcessor(RequestCode.GET_CONTROLLER_CONFIG, controllerRequestProcessor, this.controllerRequestExecutor); + controllerRemotingServer.registerProcessor(RequestCode.CLEAN_BROKER_DATA, controllerRequestProcessor, this.controllerRequestExecutor); + controllerRemotingServer.registerProcessor(RequestCode.CONTROLLER_GET_NEXT_BROKER_ID, controllerRequestProcessor, this.controllerRequestExecutor); + controllerRemotingServer.registerProcessor(RequestCode.CONTROLLER_APPLY_BROKER_ID, controllerRequestProcessor, this.controllerRequestExecutor); + } + + public void start() { + this.controller.startup(); + this.heartbeatManager.start(); + this.remotingClient.start(); + } + + public void shutdown() { + this.heartbeatManager.shutdown(); + this.controllerRequestExecutor.shutdown(); + this.notifyService.shutdown(); + this.controller.shutdown(); + this.remotingClient.shutdown(); + } + + public BrokerHeartbeatManager getHeartbeatManager() { + return heartbeatManager; + } + + public ControllerConfig getControllerConfig() { + return controllerConfig; + } + + public Controller getController() { + return controller; + } + + public NettyServerConfig getNettyServerConfig() { + return nettyServerConfig; + } + + public NettyClientConfig getNettyClientConfig() { + return nettyClientConfig; + } + + public BrokerHousekeepingService getBrokerHousekeepingService() { + return brokerHousekeepingService; + } + + public Configuration getConfiguration() { + return configuration; + } + + class NotifyService { + private ExecutorService executorService; + + private Map currentNotifyFutures; + + public NotifyService() { + } + + public void initialize() { + this.executorService = Executors.newFixedThreadPool(3, new ThreadFactoryImpl("ControllerManager_NotifyService_")); + this.currentNotifyFutures = new ConcurrentHashMap<>(); + } + + public void notifyBroker(String brokerAddress, RoleChangeNotifyEntry entry) { + int masterEpoch = entry.getMasterEpoch(); + NotifyTask oldTask = this.currentNotifyFutures.get(brokerAddress); + if (oldTask != null && masterEpoch > oldTask.getMasterEpoch()) { + // cancel current future + Future oldFuture = oldTask.getFuture(); + if (oldFuture != null && !oldFuture.isDone()) { + oldFuture.cancel(true); + } + } + final NotifyTask task = new NotifyTask(masterEpoch, null); + Runnable runnable = () -> { + doNotifyBrokerRoleChanged(brokerAddress, entry); + this.currentNotifyFutures.remove(brokerAddress, task); + }; + this.currentNotifyFutures.put(brokerAddress, task); + Future future = this.executorService.submit(runnable); + task.setFuture(future); + } + + public void shutdown() { + if (!this.executorService.isShutdown()) { + this.executorService.shutdownNow(); + } + } + + class NotifyTask extends Pair { + public NotifyTask(Integer masterEpoch, Future future) { + super(masterEpoch, future); + } + + public Integer getMasterEpoch() { + return super.getObject1(); + } + + public Future getFuture() { + return super.getObject2(); + } + + public void setFuture(Future future) { + super.setObject2(future); + } + + @Override + public int hashCode() { + return Objects.hashCode(super.getObject1()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof NotifyTask)) { + return false; + } + NotifyTask task = (NotifyTask) obj; + return super.getObject1().equals(task.getObject1()); + } + } + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/ControllerStartup.java b/controller/src/main/java/org/apache/rocketmq/controller/ControllerStartup.java new file mode 100644 index 00000000000..275058dbfca --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/ControllerStartup.java @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller; + +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import java.util.concurrent.Callable; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.ControllerConfig; +import org.apache.rocketmq.common.JraftConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.srvutil.ServerUtil; +import org.apache.rocketmq.srvutil.ShutdownHookThread; + +public class ControllerStartup { + + private static Logger log; + private static Properties properties = null; + private static CommandLine commandLine = null; + + public static void main(String[] args) { + main0(args); + } + + public static ControllerManager main0(String[] args) { + + try { + ControllerManager controller = createControllerManager(args); + start(controller); + String tip = "The Controller Server boot success. serializeType=" + RemotingCommand.getSerializeTypeConfigInThisServer(); + log.info(tip); + System.out.printf("%s%n", tip); + return controller; + } catch (Throwable e) { + e.printStackTrace(); + System.exit(-1); + } + + return null; + } + + public static ControllerManager createControllerManager(String[] args) throws IOException { + Options options = ServerUtil.buildCommandlineOptions(new Options()); + commandLine = ServerUtil.parseCmdLine("mqcontroller", args, buildCommandlineOptions(options), new DefaultParser()); + if (null == commandLine) { + System.exit(-1); + return null; + } + + final ControllerConfig controllerConfig = new ControllerConfig(); + final JraftConfig jraftConfig = new JraftConfig(); + controllerConfig.setJraftConfig(jraftConfig); + final NettyServerConfig nettyServerConfig = new NettyServerConfig(); + final NettyClientConfig nettyClientConfig = new NettyClientConfig(); + nettyServerConfig.setListenPort(19876); + + if (commandLine.hasOption('c')) { + String file = commandLine.getOptionValue('c'); + if (file != null) { + InputStream in = new BufferedInputStream(new FileInputStream(file)); + properties = new Properties(); + properties.load(in); + MixAll.properties2Object(properties, controllerConfig); + MixAll.properties2Object(properties, jraftConfig); + MixAll.properties2Object(properties, nettyServerConfig); + MixAll.properties2Object(properties, nettyClientConfig); + + System.out.printf("load config properties file OK, %s%n", file); + in.close(); + } + } + + if (commandLine.hasOption('p')) { + Logger console = LoggerFactory.getLogger(LoggerName.CONTROLLER_CONSOLE_NAME); + MixAll.printObjectProperties(console, controllerConfig); + MixAll.printObjectProperties(console, jraftConfig); + MixAll.printObjectProperties(console, nettyServerConfig); + MixAll.printObjectProperties(console, nettyClientConfig); + System.exit(0); + } + + MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), controllerConfig); + + if (StringUtils.isEmpty(controllerConfig.getRocketmqHome())) { + System.out.printf("Please set the %s or %s variable in your environment!%n", MixAll.ROCKETMQ_HOME_ENV, MixAll.ROCKETMQ_HOME_PROPERTY); + System.exit(-1); + } + + log = LoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME); + + MixAll.printObjectProperties(log, controllerConfig); + MixAll.printObjectProperties(log, nettyServerConfig); + + final ControllerManager controllerManager = new ControllerManager(controllerConfig, nettyServerConfig, nettyClientConfig); + // remember all configs to prevent discard + controllerManager.getConfiguration().registerConfig(properties); + + return controllerManager; + } + + public static ControllerManager start(final ControllerManager controller) throws Exception { + + if (null == controller) { + throw new IllegalArgumentException("ControllerManager is null"); + } + + boolean initResult = controller.initialize(); + if (!initResult) { + controller.shutdown(); + System.exit(-3); + } + + Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, (Callable) () -> { + controller.shutdown(); + return null; + })); + + controller.start(); + + return controller; + } + + public static void shutdown(final ControllerManager controller) { + controller.shutdown(); + } + + public static Options buildCommandlineOptions(final Options options) { + Option opt = new Option("c", "configFile", true, "Controller config properties file"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("p", "printConfigItem", false, "Print all config items"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/elect/ElectPolicy.java b/controller/src/main/java/org/apache/rocketmq/controller/elect/ElectPolicy.java new file mode 100644 index 00000000000..d087f0da330 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/elect/ElectPolicy.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.elect; + +import java.util.Set; + +public interface ElectPolicy { + + /** + * elect a master + * + * @param clusterName the broker group belongs to + * @param brokerName the broker group name + * @param syncStateBrokers all broker replicas in syncStateSet + * @param allReplicaBrokers all broker replicas + * @param oldMaster old master + * @param brokerId broker id(can be used as prefer or assigned in some elect policy) + * @return new master's broker id + */ + Long elect(String clusterName, String brokerName, Set syncStateBrokers, Set allReplicaBrokers, + Long oldMaster, Long brokerId); + +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/elect/impl/DefaultElectPolicy.java b/controller/src/main/java/org/apache/rocketmq/controller/elect/impl/DefaultElectPolicy.java new file mode 100644 index 00000000000..da3b3ed30e4 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/elect/impl/DefaultElectPolicy.java @@ -0,0 +1,133 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.elect.impl; + +import org.apache.rocketmq.controller.elect.ElectPolicy; +import org.apache.rocketmq.controller.impl.heartbeat.BrokerLiveInfo; +import org.apache.rocketmq.controller.helper.BrokerLiveInfoGetter; +import org.apache.rocketmq.controller.helper.BrokerValidPredicate; + +import java.util.Comparator; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +public class DefaultElectPolicy implements ElectPolicy { + + // , Used to judge whether a broker + // has preliminary qualification to be selected as master + private BrokerValidPredicate validPredicate; + + // , Used to obtain the BrokerLiveInfo information of a broker + private BrokerLiveInfoGetter brokerLiveInfoGetter; + + // Sort in descending order according to, and sort in ascending order according to priority + private final Comparator comparator = (o1, o2) -> { + if (o1.getEpoch() == o2.getEpoch()) { + return o1.getMaxOffset() == o2.getMaxOffset() ? o1.getElectionPriority() - o2.getElectionPriority() : + (int) (o2.getMaxOffset() - o1.getMaxOffset()); + } else { + return o2.getEpoch() - o1.getEpoch(); + } + }; + + public DefaultElectPolicy(BrokerValidPredicate validPredicate, BrokerLiveInfoGetter brokerLiveInfoGetter) { + this.validPredicate = validPredicate; + this.brokerLiveInfoGetter = brokerLiveInfoGetter; + } + + public DefaultElectPolicy() { + + } + + /** + * We will try to select a new master from syncStateBrokers and allReplicaBrokers in turn. + * The strategies are as follows: + * - Filter alive brokers by 'validPredicate'. + * - Check whether the old master is still valid. + * - If preferBrokerAddr is not empty and valid, select it as master. + * - Otherwise, we will sort the array of 'brokerLiveInfo' according to (epoch, offset, electionPriority), and select the best candidate as the new master. + * + * @param clusterName the brokerGroup belongs + * @param syncStateBrokers all broker replicas in syncStateSet + * @param allReplicaBrokers all broker replicas + * @param oldMaster old master's broker id + * @param preferBrokerId the broker id prefer to be elected + * @return master elected by our own policy + */ + @Override + public Long elect(String clusterName, String brokerName, Set syncStateBrokers, Set allReplicaBrokers, + Long oldMaster, Long preferBrokerId) { + Long newMaster = null; + // try to elect in syncStateBrokers + if (syncStateBrokers != null) { + newMaster = tryElect(clusterName, brokerName, syncStateBrokers, oldMaster, preferBrokerId); + } + if (newMaster != null) { + return newMaster; + } + + // try to elect in all allReplicaBrokers + if (allReplicaBrokers != null) { + newMaster = tryElect(clusterName, brokerName, allReplicaBrokers, oldMaster, preferBrokerId); + } + return newMaster; + } + + private Long tryElect(String clusterName, String brokerName, Set brokers, Long oldMaster, + Long preferBrokerId) { + if (this.validPredicate != null) { + brokers = brokers.stream().filter(brokerAddr -> this.validPredicate.check(clusterName, brokerName, brokerAddr)).collect(Collectors.toSet()); + } + if (!brokers.isEmpty()) { + // if old master is still valid, and preferBrokerAddr is blank or is equals to oldMaster + if (brokers.contains(oldMaster) && (preferBrokerId == null || preferBrokerId.equals(oldMaster))) { + return oldMaster; + } + + // if preferBrokerAddr is valid, we choose it, otherwise we choose nothing + if (preferBrokerId != null) { + return brokers.contains(preferBrokerId) ? preferBrokerId : null; + } + + if (this.brokerLiveInfoGetter != null) { + // sort brokerLiveInfos by (epoch,maxOffset) + TreeSet brokerLiveInfos = new TreeSet<>(this.comparator); + brokers.forEach(brokerAddr -> brokerLiveInfos.add(this.brokerLiveInfoGetter.get(clusterName, brokerName, brokerAddr))); + if (brokerLiveInfos.size() >= 1) { + return brokerLiveInfos.first().getBrokerId(); + } + } + // elect random + return brokers.iterator().next(); + } + return null; + } + + + public void setBrokerLiveInfoGetter(BrokerLiveInfoGetter brokerLiveInfoGetter) { + this.brokerLiveInfoGetter = brokerLiveInfoGetter; + } + + public void setValidPredicate(BrokerValidPredicate validPredicate) { + this.validPredicate = validPredicate; + } + + public BrokerLiveInfoGetter getBrokerLiveInfoGetter() { + return brokerLiveInfoGetter; + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/helper/BrokerLifecycleListener.java b/controller/src/main/java/org/apache/rocketmq/controller/helper/BrokerLifecycleListener.java new file mode 100644 index 00000000000..31fa47632b7 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/helper/BrokerLifecycleListener.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.controller.helper; + +public interface BrokerLifecycleListener { + /** + * Trigger when broker inactive. + */ + void onBrokerInactive(final String clusterName, final String brokerName, final Long brokerId); +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/helper/BrokerLiveInfoGetter.java b/controller/src/main/java/org/apache/rocketmq/controller/helper/BrokerLiveInfoGetter.java new file mode 100644 index 00000000000..afdb2700ac2 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/helper/BrokerLiveInfoGetter.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.controller.helper; + +import org.apache.rocketmq.controller.impl.heartbeat.BrokerLiveInfo; + +public interface BrokerLiveInfoGetter { + + BrokerLiveInfo get(String clusterName, String brokerName, Long brokerId); + +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/helper/BrokerValidPredicate.java b/controller/src/main/java/org/apache/rocketmq/controller/helper/BrokerValidPredicate.java new file mode 100644 index 00000000000..d8c6a2f6580 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/helper/BrokerValidPredicate.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.helper; + +public interface BrokerValidPredicate { + + boolean check(String clusterName, String brokerName, Long brokerId); +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/DLedgerController.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/DLedgerController.java new file mode 100644 index 00000000000..a032b7b6211 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/DLedgerController.java @@ -0,0 +1,600 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl; + +import com.google.common.base.Stopwatch; +import io.openmessaging.storage.dledger.AppendFuture; +import io.openmessaging.storage.dledger.DLedgerConfig; +import io.openmessaging.storage.dledger.DLedgerLeaderElector; +import io.openmessaging.storage.dledger.DLedgerServer; +import io.openmessaging.storage.dledger.MemberState; +import io.openmessaging.storage.dledger.protocol.AppendEntryRequest; +import io.openmessaging.storage.dledger.protocol.AppendEntryResponse; +import io.openmessaging.storage.dledger.protocol.BatchAppendEntryRequest; +import io.opentelemetry.api.common.AttributesBuilder; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; +import org.apache.rocketmq.common.ControllerConfig; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.controller.Controller; +import org.apache.rocketmq.controller.elect.ElectPolicy; +import org.apache.rocketmq.controller.elect.impl.DefaultElectPolicy; +import org.apache.rocketmq.controller.helper.BrokerLifecycleListener; +import org.apache.rocketmq.controller.helper.BrokerValidPredicate; +import org.apache.rocketmq.controller.impl.event.ControllerResult; +import org.apache.rocketmq.controller.impl.event.EventMessage; +import org.apache.rocketmq.controller.impl.event.EventSerializer; +import org.apache.rocketmq.controller.impl.manager.ReplicasInfoManager; +import org.apache.rocketmq.controller.metrics.ControllerMetricsConstant; +import org.apache.rocketmq.controller.metrics.ControllerMetricsManager; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.ChannelEventListener; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.RemotingServer; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.SyncStateSet; +import org.apache.rocketmq.remoting.protocol.header.controller.AlterSyncStateSetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.admin.CleanControllerBrokerDataRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetMetaDataResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetReplicaInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.ApplyBrokerIdRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.GetNextBrokerIdRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.RegisterBrokerToControllerRequestHeader; + +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.LABEL_BROKER_SET; +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.LABEL_CLUSTER_NAME; +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.LABEL_DLEDGER_OPERATION; +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.LABEL_DLEDGER_OPERATION_STATUS; +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.LABEL_ELECTION_RESULT; + +/** + * The implementation of controller, based on DLedger (raft). + */ +public class DLedgerController implements Controller { + + private static final Logger log = LoggerFactory.getLogger(LoggerName.CONTROLLER_LOGGER_NAME); + private final DLedgerServer dLedgerServer; + private final ControllerConfig controllerConfig; + private final DLedgerConfig dLedgerConfig; + private final ReplicasInfoManager replicasInfoManager; + private final EventScheduler scheduler; + private final EventSerializer eventSerializer; + private final RoleChangeHandler roleHandler; + private final DLedgerControllerStateMachine statemachine; + private final ScheduledExecutorService scanInactiveMasterService; + + private ScheduledFuture scanInactiveMasterFuture; + + private final List brokerLifecycleListeners; + + // Usr for checking whether the broker is alive + private BrokerValidPredicate brokerAlivePredicate; + // use for elect a master + private ElectPolicy electPolicy; + + private final AtomicBoolean isScheduling = new AtomicBoolean(false); + + public DLedgerController(final ControllerConfig config, final BrokerValidPredicate brokerAlivePredicate) { + this(config, brokerAlivePredicate, null, null, null, null); + } + + public DLedgerController(final ControllerConfig controllerConfig, + final BrokerValidPredicate brokerAlivePredicate, final NettyServerConfig nettyServerConfig, + final NettyClientConfig nettyClientConfig, final ChannelEventListener channelEventListener, + final ElectPolicy electPolicy) { + this.controllerConfig = controllerConfig; + this.eventSerializer = new EventSerializer(); + this.scheduler = new EventScheduler(); + this.brokerAlivePredicate = brokerAlivePredicate; + this.electPolicy = electPolicy == null ? new DefaultElectPolicy() : electPolicy; + this.dLedgerConfig = new DLedgerConfig(); + this.dLedgerConfig.setGroup(controllerConfig.getControllerDLegerGroup()); + this.dLedgerConfig.setPeers(controllerConfig.getControllerDLegerPeers()); + this.dLedgerConfig.setSelfId(controllerConfig.getControllerDLegerSelfId()); + this.dLedgerConfig.setStoreBaseDir(controllerConfig.getControllerStorePath()); + this.dLedgerConfig.setMappedFileSizeForEntryData(controllerConfig.getMappedFileSize()); + + this.roleHandler = new RoleChangeHandler(dLedgerConfig.getSelfId()); + this.replicasInfoManager = new ReplicasInfoManager(controllerConfig); + this.statemachine = new DLedgerControllerStateMachine(replicasInfoManager, this.eventSerializer, dLedgerConfig.getGroup(), dLedgerConfig.getSelfId()); + + // Register statemachine and role handler. + this.dLedgerServer = new DLedgerServer(dLedgerConfig, nettyServerConfig, nettyClientConfig, channelEventListener); + this.dLedgerServer.registerStateMachine(this.statemachine); + this.dLedgerServer.getDLedgerLeaderElector().addRoleChangeHandler(this.roleHandler); + this.scanInactiveMasterService = ThreadUtils.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("DLedgerController_scanInactiveService_")); + this.brokerLifecycleListeners = new ArrayList<>(); + } + + @Override + public void startup() { + this.dLedgerServer.startup(); + } + + @Override + public void shutdown() { + this.cancelScanInactiveFuture(); + this.dLedgerServer.shutdown(); + } + + @Override + public void startScheduling() { + if (this.isScheduling.compareAndSet(false, true)) { + log.info("Start scheduling controller events"); + this.scheduler.start(); + } + } + + @Override + public void stopScheduling() { + if (this.isScheduling.compareAndSet(true, false)) { + log.info("Stop scheduling controller events"); + this.scheduler.shutdown(true); + } + } + + @Override + public boolean isLeaderState() { + return this.roleHandler.isLeaderState(); + } + + public ControllerConfig getControllerConfig() { + return controllerConfig; + } + + @Override + public CompletableFuture alterSyncStateSet(AlterSyncStateSetRequestHeader request, + final SyncStateSet syncStateSet) { + return this.scheduler.appendEvent("alterSyncStateSet", + () -> this.replicasInfoManager.alterSyncStateSet(request, syncStateSet, this.brokerAlivePredicate), true); + } + + @Override + public CompletableFuture electMaster(final ElectMasterRequestHeader request) { + return this.scheduler.appendEvent("electMaster", + () -> { + ControllerResult electResult = this.replicasInfoManager.electMaster(request, this.electPolicy); + AttributesBuilder attributesBuilder = ControllerMetricsManager.newAttributesBuilder() + .put(LABEL_CLUSTER_NAME, request.getClusterName()) + .put(LABEL_BROKER_SET, request.getBrokerName()); + switch (electResult.getResponseCode()) { + case ResponseCode.SUCCESS: + ControllerMetricsManager.electionTotal.add(1, + attributesBuilder.put(LABEL_ELECTION_RESULT, ControllerMetricsConstant.ElectionResult.NEW_MASTER_ELECTED.getLowerCaseName()).build()); + break; + case ResponseCode.CONTROLLER_MASTER_STILL_EXIST: + ControllerMetricsManager.electionTotal.add(1, + attributesBuilder.put(LABEL_ELECTION_RESULT, ControllerMetricsConstant.ElectionResult.KEEP_CURRENT_MASTER.getLowerCaseName()).build()); + break; + case ResponseCode.CONTROLLER_MASTER_NOT_AVAILABLE: + case ResponseCode.CONTROLLER_ELECT_MASTER_FAILED: + ControllerMetricsManager.electionTotal.add(1, + attributesBuilder.put(LABEL_ELECTION_RESULT, ControllerMetricsConstant.ElectionResult.NO_MASTER_ELECTED.getLowerCaseName()).build()); + break; + default: + break; + } + return electResult; + }, true); + } + + @Override + public CompletableFuture getNextBrokerId(GetNextBrokerIdRequestHeader request) { + return this.scheduler.appendEvent("getNextBrokerId", () -> this.replicasInfoManager.getNextBrokerId(request), false); + } + + @Override + public CompletableFuture applyBrokerId(ApplyBrokerIdRequestHeader request) { + return this.scheduler.appendEvent("applyBrokerId", () -> this.replicasInfoManager.applyBrokerId(request), true); + } + + @Override + public CompletableFuture registerBroker(RegisterBrokerToControllerRequestHeader request) { + return this.scheduler.appendEvent("registerSuccess", () -> this.replicasInfoManager.registerBroker(request, brokerAlivePredicate), true); + } + + @Override + public CompletableFuture getReplicaInfo(final GetReplicaInfoRequestHeader request) { + return this.scheduler.appendEvent("getReplicaInfo", + () -> this.replicasInfoManager.getReplicaInfo(request), false); + } + + @Override + public CompletableFuture getSyncStateData(List brokerNames) { + return this.scheduler.appendEvent("getSyncStateData", + () -> this.replicasInfoManager.getSyncStateData(brokerNames, brokerAlivePredicate), false); + } + + @Override + public void registerBrokerLifecycleListener(BrokerLifecycleListener listener) { + this.brokerLifecycleListeners.add(listener); + } + + @Override + public RemotingCommand getControllerMetadata() { + final MemberState state = getMemberState(); + final Map peers = state.getPeerMap(); + final StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : peers.entrySet()) { + final String peer = entry.getKey() + ":" + entry.getValue(); + sb.append(peer).append(";"); + } + return RemotingCommand.createResponseCommandWithHeader(ResponseCode.SUCCESS, new GetMetaDataResponseHeader( + state.getGroup(), state.getLeaderId(), state.getLeaderAddr(), state.isLeader(), sb.toString())); + } + + @Override + public RemotingServer getRemotingServer() { + return this.dLedgerServer.getRemotingServer(); + } + + @Override + public CompletableFuture cleanBrokerData( + final CleanControllerBrokerDataRequestHeader requestHeader) { + return this.scheduler.appendEvent("cleanBrokerData", + () -> this.replicasInfoManager.cleanBrokerData(requestHeader, this.brokerAlivePredicate), true); + } + + /** + * Scan all broker-set in statemachine, find that the broker-set which + * its master has been timeout but still has at least one broker keep alive with controller, + * and we trigger an election to update its state. + */ + private void scanInactiveMasterAndTriggerReelect() { + if (!this.roleHandler.isLeaderState()) { + cancelScanInactiveFuture(); + return; + } + List brokerSets = this.replicasInfoManager.scanNeedReelectBrokerSets(this.brokerAlivePredicate); + for (String brokerName : brokerSets) { + // Notify ControllerManager + this.brokerLifecycleListeners.forEach(listener -> listener.onBrokerInactive(null, brokerName, null)); + } + } + + /** + * Append the request to DLedger, and wait for DLedger to commit the request. + */ + private boolean appendToDLedgerAndWait(final AppendEntryRequest request) { + if (request != null) { + request.setGroup(this.dLedgerConfig.getGroup()); + request.setRemoteId(this.dLedgerConfig.getSelfId()); + Stopwatch stopwatch = Stopwatch.createStarted(); + AttributesBuilder attributesBuilder = ControllerMetricsManager.newAttributesBuilder() + .put(LABEL_DLEDGER_OPERATION, ControllerMetricsConstant.DLedgerOperation.APPEND.getLowerCaseName()); + try { + final AppendFuture dLedgerFuture = (AppendFuture) dLedgerServer.handleAppend(request); + if (dLedgerFuture.getPos() == -1) { + ControllerMetricsManager.dLedgerOpTotal.add(1, + attributesBuilder.put(LABEL_DLEDGER_OPERATION_STATUS, ControllerMetricsConstant.DLedgerOperationStatus.FAILED.getLowerCaseName()).build()); + return false; + } + dLedgerFuture.get(5, TimeUnit.SECONDS); + ControllerMetricsManager.dLedgerOpTotal.add(1, + attributesBuilder.put(LABEL_DLEDGER_OPERATION_STATUS, ControllerMetricsConstant.DLedgerOperationStatus.SUCCESS.getLowerCaseName()).build()); + ControllerMetricsManager.dLedgerOpLatency.record(stopwatch.elapsed(TimeUnit.MICROSECONDS), + attributesBuilder.build()); + } catch (Exception e) { + log.error("Failed to append entry to DLedger", e); + if (e instanceof TimeoutException) { + ControllerMetricsManager.dLedgerOpTotal.add(1, + attributesBuilder.put(LABEL_DLEDGER_OPERATION_STATUS, ControllerMetricsConstant.DLedgerOperationStatus.TIMEOUT.getLowerCaseName()).build()); + } else { + ControllerMetricsManager.dLedgerOpTotal.add(1, + attributesBuilder.put(LABEL_DLEDGER_OPERATION_STATUS, ControllerMetricsConstant.DLedgerOperationStatus.FAILED.getLowerCaseName()).build()); + } + return false; + } + return true; + } + return false; + } + + // Only for test + public MemberState getMemberState() { + return this.dLedgerServer.getMemberState(); + } + + public void setBrokerAlivePredicate(BrokerValidPredicate brokerAlivePredicate) { + this.brokerAlivePredicate = brokerAlivePredicate; + } + + public void setElectPolicy(ElectPolicy electPolicy) { + this.electPolicy = electPolicy; + } + + private void cancelScanInactiveFuture() { + if (this.scanInactiveMasterFuture != null) { + this.scanInactiveMasterFuture.cancel(true); + this.scanInactiveMasterFuture = null; + } + } + + /** + * Event handler that handle event + */ + interface EventHandler { + /** + * Run the controller event + */ + void run() throws Throwable; + + /** + * Return the completableFuture + */ + CompletableFuture future(); + + /** + * Handle Exception. + */ + void handleException(final Throwable t); + } + + /** + * Event scheduler, schedule event handler from event queue + */ + class EventScheduler extends ServiceThread { + private final BlockingQueue eventQueue; + + public EventScheduler() { + this.eventQueue = new LinkedBlockingQueue<>(1024); + } + + @Override + public String getServiceName() { + return EventScheduler.class.getName(); + } + + @Override + public void run() { + log.info("Start event scheduler."); + while (!isStopped()) { + EventHandler handler; + try { + handler = this.eventQueue.poll(5, TimeUnit.SECONDS); + } catch (final InterruptedException e) { + continue; + } + try { + if (handler != null) { + handler.run(); + } + } catch (final Throwable e) { + handler.handleException(e); + } + } + } + + public CompletableFuture appendEvent(final String name, + final Supplier> supplier, boolean isWriteEvent) { + if (isStopped() || !DLedgerController.this.roleHandler.isLeaderState()) { + final RemotingCommand command = RemotingCommand.createResponseCommand(ResponseCode.CONTROLLER_NOT_LEADER, "The controller is not in leader state"); + final CompletableFuture future = new CompletableFuture<>(); + future.complete(command); + return future; + } + + final EventHandler event = new ControllerEventHandler<>(name, supplier, isWriteEvent); + int tryTimes = 0; + while (true) { + try { + if (!this.eventQueue.offer(event, 5, TimeUnit.SECONDS)) { + continue; + } + return event.future(); + } catch (final InterruptedException e) { + log.error("Error happen in EventScheduler when append event", e); + tryTimes++; + if (tryTimes > 3) { + return null; + } + } + } + } + } + + /** + * Event handler, get events from supplier, and append events to DLedger + */ + class ControllerEventHandler implements EventHandler { + private final String name; + private final Supplier> supplier; + private final CompletableFuture future; + private final boolean isWriteEvent; + + ControllerEventHandler(final String name, final Supplier> supplier, + final boolean isWriteEvent) { + this.name = name; + this.supplier = supplier; + this.future = new CompletableFuture<>(); + this.isWriteEvent = isWriteEvent; + } + + @Override + public void run() throws Throwable { + final ControllerResult result = this.supplier.get(); + log.info("Event queue run event {}, get the result {}", this.name, result); + boolean appendSuccess = true; + + if (!this.isWriteEvent || result.getEvents() == null || result.getEvents().isEmpty()) { + // read event, or write event with empty events in response which also equals to read event + if (DLedgerController.this.controllerConfig.isProcessReadEvent()) { + // Now the DLedger don't have the function of Read-Index or Lease-Read, + // So we still need to propose an empty request to DLedger. + final AppendEntryRequest request = new AppendEntryRequest(); + request.setBody(new byte[0]); + appendSuccess = appendToDLedgerAndWait(request); + } + } else { + // write event + final List events = result.getEvents(); + final List eventBytes = new ArrayList<>(events.size()); + for (final EventMessage event : events) { + if (event != null) { + final byte[] data = DLedgerController.this.eventSerializer.serialize(event); + if (data != null && data.length > 0) { + eventBytes.add(data); + } + } + } + // Append events to DLedger + if (!eventBytes.isEmpty()) { + // batch append events + final BatchAppendEntryRequest request = new BatchAppendEntryRequest(); + request.setBatchMsgs(eventBytes); + appendSuccess = appendToDLedgerAndWait(request); + } + } + + if (appendSuccess) { + final RemotingCommand response = RemotingCommand.createResponseCommandWithHeader(result.getResponseCode(), (CommandCustomHeader) result.getResponse()); + if (result.getBody() != null) { + response.setBody(result.getBody()); + } + if (result.getRemark() != null) { + response.setRemark(result.getRemark()); + } + this.future.complete(response); + } else { + log.error("Failed to append event to DLedger, the response is {}, try cancel the future", result.getResponse()); + this.future.cancel(true); + } + } + + @Override + public CompletableFuture future() { + return this.future; + } + + @Override + public void handleException(final Throwable t) { + log.error("Error happen when handle event {}", this.name, t); + this.future.completeExceptionally(t); + } + } + + /** + * Role change handler, trigger the startScheduling() and stopScheduling() when role change. + */ + class RoleChangeHandler implements DLedgerLeaderElector.RoleChangeHandler { + + private final String selfId; + private final ExecutorService executorService = ThreadUtils.newSingleThreadExecutor(new ThreadFactoryImpl("DLedgerControllerRoleChangeHandler_")); + private volatile MemberState.Role currentRole = MemberState.Role.FOLLOWER; + + public RoleChangeHandler(final String selfId) { + this.selfId = selfId; + } + + @Override + public void handle(long term, MemberState.Role role) { + Runnable runnable = () -> { + switch (role) { + case CANDIDATE: + ControllerMetricsManager.recordRole(role, this.currentRole); + this.currentRole = MemberState.Role.CANDIDATE; + log.info("Controller {} change role to candidate", this.selfId); + DLedgerController.this.stopScheduling(); + DLedgerController.this.cancelScanInactiveFuture(); + break; + case FOLLOWER: + ControllerMetricsManager.recordRole(role, this.currentRole); + this.currentRole = MemberState.Role.FOLLOWER; + log.info("Controller {} change role to Follower, leaderId:{}", this.selfId, getMemberState().getLeaderId()); + DLedgerController.this.stopScheduling(); + DLedgerController.this.cancelScanInactiveFuture(); + break; + case LEADER: { + log.info("Controller {} change role to leader, try process a initial proposal", this.selfId); + // Because the role becomes to leader, but the memory statemachine of the controller is still in the old point, + // some committed logs have not been applied. Therefore, we must first process an empty request to DLedger, + // and after the request is committed, the controller can provide services(startScheduling). + int tryTimes = 0; + while (true) { + final AppendEntryRequest request = new AppendEntryRequest(); + request.setBody(new byte[0]); + try { + if (appendToDLedgerAndWait(request)) { + ControllerMetricsManager.recordRole(role, this.currentRole); + this.currentRole = MemberState.Role.LEADER; + DLedgerController.this.startScheduling(); + if (DLedgerController.this.scanInactiveMasterFuture == null) { + long scanInactiveMasterInterval = DLedgerController.this.controllerConfig.getScanInactiveMasterInterval(); + DLedgerController.this.scanInactiveMasterFuture = + DLedgerController.this.scanInactiveMasterService.scheduleAtFixedRate(DLedgerController.this::scanInactiveMasterAndTriggerReelect, + scanInactiveMasterInterval, scanInactiveMasterInterval, TimeUnit.MILLISECONDS); + } + break; + } + } catch (final Throwable e) { + log.error("Error happen when controller leader append initial request to DLedger", e); + } + if (!DLedgerController.this.getMemberState().isLeader()) { + // now is not a leader + log.error("Append a initial log failed because current state is not leader"); + break; + } + tryTimes++; + log.error(String.format("Controller leader append initial log failed, try %d times", tryTimes)); + if (tryTimes % 3 == 0) { + log.warn("Controller leader append initial log failed too many times, please wait a while"); + } + } + break; + } + } + }; + this.executorService.submit(runnable); + } + + @Override + public void startup() { + } + + @Override + public void shutdown() { + if (this.currentRole == MemberState.Role.LEADER) { + DLedgerController.this.stopScheduling(); + } + this.executorService.shutdown(); + } + + public boolean isLeaderState() { + return this.currentRole == MemberState.Role.LEADER; + } + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/DLedgerControllerStateMachine.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/DLedgerControllerStateMachine.java new file mode 100644 index 00000000000..70c65c00f40 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/DLedgerControllerStateMachine.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl; + +import io.openmessaging.storage.dledger.entry.DLedgerEntry; +import io.openmessaging.storage.dledger.snapshot.SnapshotReader; +import io.openmessaging.storage.dledger.snapshot.SnapshotWriter; +import io.openmessaging.storage.dledger.statemachine.CommittedEntryIterator; +import io.openmessaging.storage.dledger.statemachine.StateMachine; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.controller.impl.event.EventMessage; +import org.apache.rocketmq.controller.impl.event.EventSerializer; +import org.apache.rocketmq.controller.impl.manager.ReplicasInfoManager; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +import java.util.concurrent.CompletableFuture; + +/** + * The state machine implementation of the dledger controller + */ +public class DLedgerControllerStateMachine implements StateMachine { + private static final Logger log = LoggerFactory.getLogger(LoggerName.CONTROLLER_LOGGER_NAME); + private final ReplicasInfoManager replicasInfoManager; + private final EventSerializer eventSerializer; + private final String dLedgerId; + + public DLedgerControllerStateMachine(final ReplicasInfoManager replicasInfoManager, + final EventSerializer eventSerializer, final String dLedgerGroupId, final String dLedgerSelfId) { + this.replicasInfoManager = replicasInfoManager; + this.eventSerializer = eventSerializer; + this.dLedgerId = generateDLedgerId(dLedgerGroupId, dLedgerSelfId); + } + + @Override + public void onApply(CommittedEntryIterator iterator) { + int applyingSize = 0; + long firstApplyIndex = -1; + long lastApplyIndex = -1; + while (iterator.hasNext()) { + final DLedgerEntry entry = iterator.next(); + final byte[] body = entry.getBody(); + if (body != null && body.length > 0) { + final EventMessage event = this.eventSerializer.deserialize(body); + this.replicasInfoManager.applyEvent(event); + } + firstApplyIndex = firstApplyIndex == -1 ? entry.getIndex() : firstApplyIndex; + lastApplyIndex = entry.getIndex(); + applyingSize++; + } + log.info("Apply {} events index from {} to {} on controller {}", applyingSize, firstApplyIndex, lastApplyIndex, this.dLedgerId); + } + + @Override + public void onSnapshotSave(SnapshotWriter writer, CompletableFuture future) { + } + + @Override + public boolean onSnapshotLoad(SnapshotReader reader) { + return false; + } + + @Override + public void onShutdown() { + } + + @Override + public String getBindDLedgerId() { + return this.dLedgerId; + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/JRaftController.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/JRaftController.java new file mode 100644 index 00000000000..e40a6349450 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/JRaftController.java @@ -0,0 +1,279 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl; + +import com.alipay.sofa.jraft.Node; +import com.alipay.sofa.jraft.RaftGroupService; +import com.alipay.sofa.jraft.Status; +import com.alipay.sofa.jraft.conf.Configuration; +import com.alipay.sofa.jraft.entity.NodeId; +import com.alipay.sofa.jraft.entity.PeerId; +import com.alipay.sofa.jraft.entity.Task; +import com.alipay.sofa.jraft.option.NodeOptions; +import org.apache.commons.io.FileUtils; +import org.apache.rocketmq.common.ControllerConfig; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.controller.Controller; +import org.apache.rocketmq.controller.helper.BrokerLifecycleListener; +import org.apache.rocketmq.controller.impl.closure.ControllerClosure; +import org.apache.rocketmq.controller.impl.task.BrokerCloseChannelRequest; +import org.apache.rocketmq.controller.impl.task.CheckNotActiveBrokerRequest; +import org.apache.rocketmq.controller.impl.task.GetBrokerLiveInfoRequest; +import org.apache.rocketmq.controller.impl.task.GetSyncStateDataRequest; +import org.apache.rocketmq.controller.impl.task.RaftBrokerHeartBeatEventRequest; +import org.apache.rocketmq.remoting.ChannelEventListener; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.RemotingServer; +import org.apache.rocketmq.remoting.netty.NettyRemotingServer; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.SyncStateSet; +import org.apache.rocketmq.remoting.protocol.header.controller.AlterSyncStateSetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetMetaDataResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetReplicaInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.admin.CleanControllerBrokerDataRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.ApplyBrokerIdRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.GetNextBrokerIdRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.RegisterBrokerToControllerRequestHeader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public class JRaftController implements Controller { + private static final Logger log = LoggerFactory.getLogger(LoggerName.CONTROLLER_LOGGER_NAME); + private final RaftGroupService raftGroupService; + private Node node; + private final JRaftControllerStateMachine stateMachine; + private final ControllerConfig controllerConfig; + private final List brokerLifecycleListeners; + private final Map peerIdToAddr; + private final NettyRemotingServer remotingServer; + + public JRaftController(ControllerConfig controllerConfig, + final ChannelEventListener channelEventListener) throws IOException { + this.controllerConfig = controllerConfig; + this.brokerLifecycleListeners = new ArrayList<>(); + + final NodeOptions nodeOptions = new NodeOptions(); + nodeOptions.setElectionTimeoutMs(controllerConfig.getJraftConfig().getjRaftElectionTimeoutMs()); + nodeOptions.setSnapshotIntervalSecs(controllerConfig.getJraftConfig().getjRaftSnapshotIntervalSecs()); + final PeerId serverId = new PeerId(); + if (!serverId.parse(controllerConfig.getJraftConfig().getjRaftServerId())) { + throw new IllegalArgumentException("Fail to parse serverId:" + controllerConfig.getJraftConfig().getjRaftServerId()); + } + final Configuration initConf = new Configuration(); + if (!initConf.parse(controllerConfig.getJraftConfig().getjRaftInitConf())) { + throw new IllegalArgumentException("Fail to parse initConf:" + controllerConfig.getJraftConfig().getjRaftInitConf()); + } + nodeOptions.setInitialConf(initConf); + + FileUtils.forceMkdir(new File(controllerConfig.getControllerStorePath())); + nodeOptions.setLogUri(controllerConfig.getControllerStorePath() + File.separator + "log"); + nodeOptions.setRaftMetaUri(controllerConfig.getControllerStorePath() + File.separator + "raft_meta"); + nodeOptions.setSnapshotUri(controllerConfig.getControllerStorePath() + File.separator + "snapshot"); + + this.stateMachine = new JRaftControllerStateMachine(controllerConfig, new NodeId(controllerConfig.getJraftConfig().getjRaftGroupId(), serverId)); + this.stateMachine.registerOnLeaderStart(this::onLeaderStart); + this.stateMachine.registerOnLeaderStop(this::onLeaderStop); + nodeOptions.setFsm(this.stateMachine); + + this.raftGroupService = new RaftGroupService(controllerConfig.getJraftConfig().getjRaftGroupId(), serverId, nodeOptions); + + this.peerIdToAddr = new HashMap<>(); + initPeerIdMap(); + + NettyServerConfig nettyServerConfig = new NettyServerConfig(); + nettyServerConfig.setListenPort(Integer.parseInt(this.peerIdToAddr.get(serverId).split(":")[1])); + remotingServer = new NettyRemotingServer(nettyServerConfig, channelEventListener); + } + + private void initPeerIdMap() { + String[] peers = this.controllerConfig.getJraftConfig().getjRaftInitConf().split(","); + String[] rpcAddrs = this.controllerConfig.getJraftConfig().getjRaftControllerRPCAddr().split(","); + for (int i = 0; i < peers.length; i++) { + PeerId peerId = new PeerId(); + if (!peerId.parse(peers[i])) { + throw new IllegalArgumentException("Fail to parse peerId:" + peers[i]); + } + this.peerIdToAddr.put(peerId, rpcAddrs[i]); + } + } + + @Override + public void startup() { + this.remotingServer.start(); + this.node = this.raftGroupService.start(); + log.info("Controller {} started.", node.getNodeId()); + } + + @Override + public void shutdown() { + this.stopScheduling(); + this.raftGroupService.shutdown(); + this.remotingServer.shutdown(); + log.info("Controller {} stopped.", node.getNodeId()); + } + + @Override + public void startScheduling() { + } + + @Override + public void stopScheduling() { + } + + @Override + public boolean isLeaderState() { + return node.isLeader(); + } + + private CompletableFuture applyToJRaft(RemotingCommand request) { + if (!isLeaderState()) { + final RemotingCommand command = RemotingCommand.createResponseCommand(ResponseCode.CONTROLLER_NOT_LEADER, "The controller is not in leader state"); + final CompletableFuture future = new CompletableFuture<>(); + future.complete(command); + log.warn("Apply to none leader controller, controller state is {}", node.getNodeState()); + return future; + } + ControllerClosure closure = new ControllerClosure(request); + Task task = closure.taskWithThisClosure(); + if (task != null) { + node.apply(task); + return closure.getFuture(); + } else { + log.error("Apply task failed, task is null."); + return CompletableFuture.completedFuture(RemotingCommand.createResponseCommand(ResponseCode.CONTROLLER_JRAFT_INTERNAL_ERROR, "Apply task failed, Please see the server log.")); + } + } + + @Override + public CompletableFuture alterSyncStateSet(AlterSyncStateSetRequestHeader request, + SyncStateSet syncStateSet) { + final RemotingCommand requestCommand = RemotingCommand.createRequestCommand(RequestCode.CONTROLLER_ALTER_SYNC_STATE_SET, request); + requestCommand.setBody(syncStateSet.encode()); + return applyToJRaft(requestCommand); + } + + @Override + public CompletableFuture electMaster(ElectMasterRequestHeader request) { + final RemotingCommand requestCommand = RemotingCommand.createRequestCommand(RequestCode.CONTROLLER_ELECT_MASTER, request); + return applyToJRaft(requestCommand); + } + + @Override + public CompletableFuture getNextBrokerId(GetNextBrokerIdRequestHeader request) { + final RemotingCommand requestCommand = RemotingCommand.createRequestCommand(RequestCode.CONTROLLER_GET_NEXT_BROKER_ID, request); + return applyToJRaft(requestCommand); + } + + @Override + public CompletableFuture applyBrokerId(ApplyBrokerIdRequestHeader request) { + final RemotingCommand requestCommand = RemotingCommand.createRequestCommand(RequestCode.CONTROLLER_APPLY_BROKER_ID, request); + return applyToJRaft(requestCommand); + } + + @Override + public CompletableFuture registerBroker(RegisterBrokerToControllerRequestHeader request) { + final RemotingCommand requestCommand = RemotingCommand.createRequestCommand(RequestCode.CONTROLLER_REGISTER_BROKER, request); + return applyToJRaft(requestCommand); + } + + @Override + public CompletableFuture getReplicaInfo(GetReplicaInfoRequestHeader request) { + final RemotingCommand requestCommand = RemotingCommand.createRequestCommand(RequestCode.CONTROLLER_GET_REPLICA_INFO, request); + return applyToJRaft(requestCommand); + } + + @Override + public CompletableFuture getSyncStateData(List brokerNames) { + final RemotingCommand requestCommand = RemotingCommand.createRequestCommand(RequestCode.CONTROLLER_GET_SYNC_STATE_DATA, new GetSyncStateDataRequest()); + requestCommand.setBody(RemotingSerializable.encode(brokerNames)); + return applyToJRaft(requestCommand); + } + + @Override + public CompletableFuture cleanBrokerData(CleanControllerBrokerDataRequestHeader requestHeader) { + final RemotingCommand requestCommand = RemotingCommand.createRequestCommand(RequestCode.CLEAN_BROKER_DATA, requestHeader); + return applyToJRaft(requestCommand); + } + + @Override + public void registerBrokerLifecycleListener(BrokerLifecycleListener listener) { + this.brokerLifecycleListeners.add(listener); + } + + @Override + public RemotingCommand getControllerMetadata() { + List peers = node.getOptions().getInitialConf().getPeers(); + final StringBuilder sb = new StringBuilder(); + for (PeerId peer : peers) { + sb.append(peerIdToAddr.get(peer)).append(";"); + } + return RemotingCommand.createResponseCommandWithHeader(ResponseCode.SUCCESS, new GetMetaDataResponseHeader( + node.getGroupId(), + node.getLeaderId() == null ? "" : node.getLeaderId().toString(), + this.peerIdToAddr.get(node.getLeaderId()), + node.isLeader(), + sb.toString() + )); + } + + @Override + public RemotingServer getRemotingServer() { + return remotingServer; + } + + public void onLeaderStart(long term) { + log.info("Controller start leadership, term: {}.", term); + } + + public void onLeaderStop(Status status) { + log.info("Controller {} stop leadership, status: {}.", node.getNodeId(), status); + this.stopScheduling(); + } + + public CompletableFuture getBrokerLiveInfo(GetBrokerLiveInfoRequest requestHeader) { + final RemotingCommand requestCommand = RemotingCommand.createRequestCommand(RequestCode.GET_BROKER_LIVE_INFO_REQUEST, requestHeader); + return applyToJRaft(requestCommand); + } + + public CompletableFuture onBrokerHeartBeat(RaftBrokerHeartBeatEventRequest requestHeader) { + final RemotingCommand requestCommand = RemotingCommand.createRequestCommand(RequestCode.RAFT_BROKER_HEART_BEAT_EVENT_REQUEST, requestHeader); + return applyToJRaft(requestCommand); + } + + public CompletableFuture onBrokerCloseChannel(BrokerCloseChannelRequest requestHeader) { + final RemotingCommand requestCommand = RemotingCommand.createRequestCommand(RequestCode.BROKER_CLOSE_CHANNEL_REQUEST, requestHeader); + return applyToJRaft(requestCommand); + } + + public CompletableFuture checkNotActiveBroker(CheckNotActiveBrokerRequest requestHeader) { + final RemotingCommand requestCommand = RemotingCommand.createRequestCommand(RequestCode.CHECK_NOT_ACTIVE_BROKER_REQUEST, requestHeader); + return applyToJRaft(requestCommand); + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/JRaftControllerStateMachine.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/JRaftControllerStateMachine.java new file mode 100644 index 00000000000..02420385a27 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/JRaftControllerStateMachine.java @@ -0,0 +1,331 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl; + +import com.alipay.sofa.jraft.Closure; +import com.alipay.sofa.jraft.Iterator; +import com.alipay.sofa.jraft.StateMachine; +import com.alipay.sofa.jraft.Status; +import com.alipay.sofa.jraft.conf.Configuration; +import com.alipay.sofa.jraft.entity.LeaderChangeContext; +import com.alipay.sofa.jraft.entity.NodeId; +import com.alipay.sofa.jraft.error.RaftError; +import com.alipay.sofa.jraft.error.RaftException; +import com.alipay.sofa.jraft.storage.snapshot.SnapshotReader; +import com.alipay.sofa.jraft.storage.snapshot.SnapshotWriter; +import com.alipay.sofa.jraft.util.Utils; +import io.opentelemetry.api.common.AttributesBuilder; +import org.apache.commons.io.FileUtils; +import org.apache.rocketmq.common.ControllerConfig; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.controller.elect.impl.DefaultElectPolicy; +import org.apache.rocketmq.controller.impl.closure.ControllerClosure; +import org.apache.rocketmq.controller.impl.event.ControllerResult; +import org.apache.rocketmq.controller.impl.manager.RaftReplicasInfoManager; +import org.apache.rocketmq.controller.impl.task.BrokerCloseChannelRequest; +import org.apache.rocketmq.controller.impl.task.CheckNotActiveBrokerRequest; +import org.apache.rocketmq.controller.impl.task.GetBrokerLiveInfoRequest; +import org.apache.rocketmq.controller.impl.task.GetSyncStateDataRequest; +import org.apache.rocketmq.controller.impl.task.RaftBrokerHeartBeatEventRequest; +import org.apache.rocketmq.controller.metrics.ControllerMetricsConstant; +import org.apache.rocketmq.controller.metrics.ControllerMetricsManager; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.SyncStateSet; +import org.apache.rocketmq.remoting.protocol.header.controller.AlterSyncStateSetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.AlterSyncStateSetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetReplicaInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetReplicaInfoResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.admin.CleanControllerBrokerDataRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.ApplyBrokerIdRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.ApplyBrokerIdResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.GetNextBrokerIdRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.GetNextBrokerIdResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.RegisterBrokerToControllerRequestHeader; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.LABEL_BROKER_SET; +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.LABEL_CLUSTER_NAME; +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.LABEL_ELECTION_RESULT; + +public class JRaftControllerStateMachine implements StateMachine { + private static final Logger log = LoggerFactory.getLogger(LoggerName.CONTROLLER_LOGGER_NAME); + private final List> onLeaderStartCallbacks; + private final List> onLeaderStopCallbacks; + private final RaftReplicasInfoManager replicasInfoManager; + private final NodeId nodeId; + + public JRaftControllerStateMachine(ControllerConfig controllerConfig, NodeId nodeId) { + this.replicasInfoManager = new RaftReplicasInfoManager(controllerConfig); + this.nodeId = nodeId; + this.onLeaderStartCallbacks = new ArrayList<>(); + this.onLeaderStopCallbacks = new ArrayList<>(); + } + + @Override + public void onApply(Iterator iter) { + while (iter.hasNext()) { + byte[] data = iter.getData().array(); + ControllerClosure controllerClosure = (ControllerClosure) iter.done(); + processEvent(controllerClosure, data, iter.getTerm(), iter.getIndex()); + + iter.next(); + } + } + + private void processEvent(ControllerClosure controllerClosure, byte[] data, long term, long index) { + RemotingCommand request; + ControllerResult result; + try { + if (controllerClosure != null) { + request = controllerClosure.getRequestEvent(); + } else { + request = RemotingCommand.decode(Arrays.copyOfRange(data, 4, data.length)); + } + log.info("process event: term {}, index {}, request code {}", term, index, request.getCode()); + switch (request.getCode()) { + case RequestCode.CONTROLLER_ALTER_SYNC_STATE_SET: + AlterSyncStateSetRequestHeader requestHeader = (AlterSyncStateSetRequestHeader) request.decodeCommandCustomHeader(AlterSyncStateSetRequestHeader.class); + SyncStateSet syncStateSet = RemotingSerializable.decode(request.getBody(), SyncStateSet.class); + result = alterSyncStateSet(requestHeader, syncStateSet); + break; + case RequestCode.CONTROLLER_ELECT_MASTER: + ElectMasterRequestHeader electMasterRequestHeader = (ElectMasterRequestHeader) request.decodeCommandCustomHeader(ElectMasterRequestHeader.class); + result = electMaster(electMasterRequestHeader); + break; + case RequestCode.CONTROLLER_GET_NEXT_BROKER_ID: + GetNextBrokerIdRequestHeader getNextBrokerIdRequestHeader = (GetNextBrokerIdRequestHeader) request.decodeCommandCustomHeader(GetNextBrokerIdRequestHeader.class); + result = getNextBrokerId(getNextBrokerIdRequestHeader); + break; + case RequestCode.CONTROLLER_APPLY_BROKER_ID: + ApplyBrokerIdRequestHeader applyBrokerIdRequestHeader = (ApplyBrokerIdRequestHeader) request.decodeCommandCustomHeader(ApplyBrokerIdRequestHeader.class); + result = applyBrokerId(applyBrokerIdRequestHeader); + break; + case RequestCode.CONTROLLER_REGISTER_BROKER: + RegisterBrokerToControllerRequestHeader registerBrokerToControllerRequestHeader = (RegisterBrokerToControllerRequestHeader) request.decodeCommandCustomHeader(RegisterBrokerToControllerRequestHeader.class); + result = registerBroker(registerBrokerToControllerRequestHeader); + break; + case RequestCode.CONTROLLER_GET_REPLICA_INFO: + GetReplicaInfoRequestHeader getReplicaInfoRequestHeader = (GetReplicaInfoRequestHeader) request.decodeCommandCustomHeader(GetReplicaInfoRequestHeader.class); + result = getReplicaInfo(getReplicaInfoRequestHeader); + break; + case RequestCode.CONTROLLER_GET_SYNC_STATE_DATA: + List brokerNames = RemotingSerializable.decode(request.getBody(), List.class); + GetSyncStateDataRequest getSyncStateDataRequest = (GetSyncStateDataRequest) request.decodeCommandCustomHeader(GetSyncStateDataRequest.class); + result = getSyncStateData(brokerNames, getSyncStateDataRequest.getInvokeTime()); + break; + case RequestCode.CLEAN_BROKER_DATA: + CleanControllerBrokerDataRequestHeader cleanBrokerDataRequestHeader = (CleanControllerBrokerDataRequestHeader) request.decodeCommandCustomHeader(CleanControllerBrokerDataRequestHeader.class); + result = cleanBrokerData(cleanBrokerDataRequestHeader); + break; + case RequestCode.GET_BROKER_LIVE_INFO_REQUEST: + GetBrokerLiveInfoRequest getBrokerLiveInfoRequest = (GetBrokerLiveInfoRequest) request.decodeCommandCustomHeader(GetBrokerLiveInfoRequest.class); + result = replicasInfoManager.getBrokerLiveInfo(getBrokerLiveInfoRequest); + break; + case RequestCode.RAFT_BROKER_HEART_BEAT_EVENT_REQUEST: + RaftBrokerHeartBeatEventRequest brokerHeartbeatRequestHeader = (RaftBrokerHeartBeatEventRequest) request.decodeCommandCustomHeader(RaftBrokerHeartBeatEventRequest.class); + result = replicasInfoManager.onBrokerHeartBeat(brokerHeartbeatRequestHeader); + break; + case RequestCode.BROKER_CLOSE_CHANNEL_REQUEST: + BrokerCloseChannelRequest brokerCloseChannelRequest = (BrokerCloseChannelRequest) request.decodeCommandCustomHeader(BrokerCloseChannelRequest.class); + result = replicasInfoManager.onBrokerCloseChannel(brokerCloseChannelRequest); + break; + case RequestCode.CHECK_NOT_ACTIVE_BROKER_REQUEST: + CheckNotActiveBrokerRequest checkNotActiveBrokerRequest = (CheckNotActiveBrokerRequest) request.decodeCommandCustomHeader(CheckNotActiveBrokerRequest.class); + result = replicasInfoManager.checkNotActiveBroker(checkNotActiveBrokerRequest); + break; + default: + throw new RemotingCommandException("Unknown request code: " + request.getCode()); + } + result.getEvents().forEach(replicasInfoManager::applyEvent); + } catch (RemotingCommandException e) { + log.error("Fail to process event", e); + if (controllerClosure != null) { + controllerClosure.run(new Status(RaftError.EINTERNAL, e.getMessage())); + } + return; + } + log.info("process event: term {}, index {}, request code {} success with result {}", term, index, request.getCode(), result.toString()); + if (controllerClosure != null) { + controllerClosure.setControllerResult(result); + controllerClosure.run(Status.OK()); + } + } + + private ControllerResult alterSyncStateSet( + AlterSyncStateSetRequestHeader requestHeader, SyncStateSet syncStateSet) { + return replicasInfoManager.alterSyncStateSet(requestHeader, syncStateSet, new RaftReplicasInfoManager.BrokerValidPredicateWithInvokeTime(requestHeader.getInvokeTime(), this.replicasInfoManager)); + } + + private ControllerResult electMaster(ElectMasterRequestHeader request) { + ControllerResult electResult = this.replicasInfoManager.electMaster(request, new DefaultElectPolicy( + (clusterName, brokerName, brokerId) -> replicasInfoManager.isBrokerActive(clusterName, brokerName, brokerId, request.getInvokeTime()), + replicasInfoManager::getBrokerLiveInfo + )); + log.info("elect master, request :{}, result: {}", request.toString(), electResult.toString()); + AttributesBuilder attributesBuilder = ControllerMetricsManager.newAttributesBuilder() + .put(LABEL_CLUSTER_NAME, request.getClusterName()) + .put(LABEL_BROKER_SET, request.getBrokerName()); + switch (electResult.getResponseCode()) { + case ResponseCode.SUCCESS: + ControllerMetricsManager.electionTotal.add(1, + attributesBuilder.put(LABEL_ELECTION_RESULT, ControllerMetricsConstant.ElectionResult.NEW_MASTER_ELECTED.getLowerCaseName()).build()); + break; + case ResponseCode.CONTROLLER_MASTER_STILL_EXIST: + ControllerMetricsManager.electionTotal.add(1, + attributesBuilder.put(LABEL_ELECTION_RESULT, ControllerMetricsConstant.ElectionResult.KEEP_CURRENT_MASTER.getLowerCaseName()).build()); + break; + case ResponseCode.CONTROLLER_MASTER_NOT_AVAILABLE: + case ResponseCode.CONTROLLER_ELECT_MASTER_FAILED: + ControllerMetricsManager.electionTotal.add(1, + attributesBuilder.put(LABEL_ELECTION_RESULT, ControllerMetricsConstant.ElectionResult.NO_MASTER_ELECTED.getLowerCaseName()).build()); + break; + default: + break; + } + return electResult; + } + + private ControllerResult getNextBrokerId( + GetNextBrokerIdRequestHeader requestHeader) { + return replicasInfoManager.getNextBrokerId(requestHeader); + } + + private ControllerResult applyBrokerId(ApplyBrokerIdRequestHeader requestHeader) { + return replicasInfoManager.applyBrokerId(requestHeader); + } + + private ControllerResult registerBroker(RegisterBrokerToControllerRequestHeader request) { + return replicasInfoManager.registerBroker(request, new RaftReplicasInfoManager.BrokerValidPredicateWithInvokeTime(request.getInvokeTime(), this.replicasInfoManager)); + } + + private ControllerResult getReplicaInfo(GetReplicaInfoRequestHeader request) { + return replicasInfoManager.getReplicaInfo(request); + } + + private ControllerResult getSyncStateData(List brokerNames, long invokeTile) { + return replicasInfoManager.getSyncStateData(brokerNames, new RaftReplicasInfoManager.BrokerValidPredicateWithInvokeTime(invokeTile, this.replicasInfoManager)); + } + + private ControllerResult cleanBrokerData(CleanControllerBrokerDataRequestHeader requestHeader) { + return replicasInfoManager.cleanBrokerData(requestHeader, new RaftReplicasInfoManager.BrokerValidPredicateWithInvokeTime(requestHeader.getInvokeTime(), this.replicasInfoManager)); + } + + @Override + public void onShutdown() { + log.info("StateMachine {} node {} onShutdown", getClass().getName(), nodeId.toString()); + } + + @Override + public void onSnapshotSave(SnapshotWriter writer, Closure done) { + byte[] data; + try { + data = this.replicasInfoManager.serialize(); + } catch (Throwable e) { + done.run(new Status(RaftError.EIO, "Fail to serialize replicasInfoManager state machine data")); + return; + } + Utils.runInThread(() -> { + try { + FileUtils.writeByteArrayToFile(new File(writer.getPath() + File.separator + "data"), data); + if (writer.addFile("data")) { + log.info("Save snapshot, path={}", writer.getPath()); + done.run(Status.OK()); + } else { + throw new IOException("Fail to add file to writer"); + } + } catch (IOException e) { + log.error("Fail to save snapshot", e); + done.run(new Status(RaftError.EIO, "Fail to save snapshot")); + } + }); + } + + @Override + public boolean onSnapshotLoad(SnapshotReader reader) { + if (reader.getFileMeta("data") == null) { + log.error("Fail to find data file in {}", reader.getPath()); + return false; + } + try { + byte[] data = FileUtils.readFileToByteArray(new File(reader.getPath() + File.separator + "data")); + this.replicasInfoManager.deserializeFrom(data); + log.info("Load snapshot from {}", reader.getPath()); + return true; + } catch (Throwable e) { + log.error("Fail to load snapshot from {}", reader.getPath(), e); + return false; + } + } + + @Override + public void onLeaderStart(long term) { + for (Consumer callback : onLeaderStartCallbacks) { + callback.accept(term); + } + log.info("node {} Start Leader, term={}", nodeId.toString(), term); + } + + @Override + public void onLeaderStop(Status status) { + for (Consumer callback : onLeaderStopCallbacks) { + callback.accept(status); + } + log.info("node {} Stop Leader, status={}", nodeId.toString(), status); + } + + public void registerOnLeaderStart(Consumer callback) { + onLeaderStartCallbacks.add(callback); + } + + public void registerOnLeaderStop(Consumer callback) { + onLeaderStopCallbacks.add(callback); + } + + @Override + public void onError(RaftException e) { + log.error("Encountered an error={} on StateMachine {}, node {}, raft may stop working since some error occurs, you should figure out the cause and repair or remove this node.", e.getStatus(), this.getClass().getName(), nodeId.toString(), e); + } + + @Override + public void onConfigurationCommitted(Configuration conf) { + log.info("Configuration committed, conf={}", conf); + } + + @Override + public void onStopFollowing(LeaderChangeContext ctx) { + log.info("Stop following, ctx={}", ctx); + } + + @Override + public void onStartFollowing(LeaderChangeContext ctx) { + log.info("Start following, ctx={}", ctx); + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/closure/ControllerClosure.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/closure/ControllerClosure.java new file mode 100644 index 00000000000..c9c470ca912 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/closure/ControllerClosure.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.closure; + +import com.alipay.sofa.jraft.Closure; +import com.alipay.sofa.jraft.Status; +import com.alipay.sofa.jraft.entity.Task; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.controller.impl.event.ControllerResult; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.ResponseCode; + +import java.util.concurrent.CompletableFuture; + +public class ControllerClosure implements Closure { + private static final Logger log = LoggerFactory.getLogger(LoggerName.CONTROLLER_LOGGER_NAME); + private final RemotingCommand requestEvent; + private final CompletableFuture future; + private ControllerResult controllerResult; + private Task task; + + public ControllerClosure(RemotingCommand requestEvent) { + this.requestEvent = requestEvent; + this.future = new CompletableFuture<>(); + this.task = null; + } + + public CompletableFuture getFuture() { + return future; + } + + public void setControllerResult(ControllerResult controllerResult) { + this.controllerResult = controllerResult; + } + + @Override + public void run(Status status) { + if (status.isOk()) { + final RemotingCommand response = RemotingCommand.createResponseCommandWithHeader(controllerResult.getResponseCode(), (CommandCustomHeader) controllerResult.getResponse()); + if (controllerResult.getBody() != null) { + response.setBody(controllerResult.getBody()); + } + if (controllerResult.getRemark() != null) { + response.setRemark(controllerResult.getRemark()); + } + future.complete(response); + } else { + log.error("Failed to append to jRaft node, error is: {}.", status); + future.complete(RemotingCommand.createResponseCommand(ResponseCode.CONTROLLER_JRAFT_INTERNAL_ERROR, status.getErrorMsg())); + } + } + + public Task taskWithThisClosure() { + if (task != null) { + return task; + } + task = new Task(); + task.setDone(this); + task.setData(requestEvent.encode()); + return task; + } + + public RemotingCommand getRequestEvent() { + return requestEvent; + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/event/AlterSyncStateSetEvent.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/event/AlterSyncStateSetEvent.java new file mode 100644 index 00000000000..6af44b7229c --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/event/AlterSyncStateSetEvent.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.event; + +import java.util.HashSet; +import java.util.Set; + +/** + * The event alters the syncStateSet of target broker. + * Triggered by the AlterSyncStateSetApi. + */ +public class AlterSyncStateSetEvent implements EventMessage { + + private final String brokerName; + private final Set newSyncStateSet; + + public AlterSyncStateSetEvent(String brokerName, Set newSyncStateSet) { + this.brokerName = brokerName; + this.newSyncStateSet = new HashSet<>(newSyncStateSet); + } + + @Override + public EventType getEventType() { + return EventType.ALTER_SYNC_STATE_SET_EVENT; + } + + public String getBrokerName() { + return brokerName; + } + + public Set getNewSyncStateSet() { + return new HashSet<>(newSyncStateSet); + } + + @Override + public String toString() { + return "AlterSyncStateSetEvent{" + + "brokerName='" + brokerName + '\'' + + ", newSyncStateSet=" + newSyncStateSet + + '}'; + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/event/ApplyBrokerIdEvent.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/event/ApplyBrokerIdEvent.java new file mode 100644 index 00000000000..bb8c9f5a347 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/event/ApplyBrokerIdEvent.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.event; + +/** + * The event trys to apply a new id for a new broker. + * Triggered by the RegisterBrokerApi. + */ +public class ApplyBrokerIdEvent implements EventMessage { + private final String clusterName; + private final String brokerName; + private final String brokerAddress; + + private final String registerCheckCode; + + private final long newBrokerId; + + public ApplyBrokerIdEvent(String clusterName, String brokerName, String brokerAddress, long newBrokerId, + String registerCheckCode) { + this.clusterName = clusterName; + this.brokerName = brokerName; + this.brokerAddress = brokerAddress; + this.newBrokerId = newBrokerId; + this.registerCheckCode = registerCheckCode; + } + + @Override + public EventType getEventType() { + return EventType.APPLY_BROKER_ID_EVENT; + } + + public String getBrokerName() { + return brokerName; + } + + public String getBrokerAddress() { + return brokerAddress; + } + + public long getNewBrokerId() { + return newBrokerId; + } + + public String getClusterName() { + return clusterName; + } + + public String getRegisterCheckCode() { + return registerCheckCode; + } + + @Override + public String toString() { + return "ApplyBrokerIdEvent{" + + "clusterName='" + clusterName + '\'' + + ", brokerName='" + brokerName + '\'' + + ", brokerAddress='" + brokerAddress + '\'' + + ", registerCheckCode='" + registerCheckCode + '\'' + + ", newBrokerId=" + newBrokerId + + '}'; + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/event/CleanBrokerDataEvent.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/event/CleanBrokerDataEvent.java new file mode 100644 index 00000000000..6992fa16e0b --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/event/CleanBrokerDataEvent.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.controller.impl.event; + +import java.util.Set; + +public class CleanBrokerDataEvent implements EventMessage { + + private String brokerName; + + private Set brokerIdSetToClean; + + public CleanBrokerDataEvent(String brokerName, Set brokerIdSetToClean) { + this.brokerName = brokerName; + this.brokerIdSetToClean = brokerIdSetToClean; + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + public void setBrokerIdSetToClean(Set brokerIdSetToClean) { + this.brokerIdSetToClean = brokerIdSetToClean; + } + + public Set getBrokerIdSetToClean() { + return brokerIdSetToClean; + } + + /** + * Returns the event type of this message + */ + @Override + public EventType getEventType() { + return EventType.CLEAN_BROKER_DATA_EVENT; + } + + @Override + public String toString() { + return "CleanBrokerDataEvent{" + + "brokerName='" + brokerName + '\'' + + ", brokerIdSetToClean=" + brokerIdSetToClean + + '}'; + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/event/ControllerResult.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/event/ControllerResult.java new file mode 100644 index 00000000000..d661d73e125 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/event/ControllerResult.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.event; + +import java.util.ArrayList; +import java.util.List; +import org.apache.rocketmq.remoting.protocol.ResponseCode; + +public class ControllerResult { + private final List events; + private final T response; + private byte[] body; + private int responseCode = ResponseCode.SUCCESS; + private String remark; + + public ControllerResult() { + this(null); + } + + public ControllerResult(T response) { + this.events = new ArrayList<>(); + this.response = response; + } + + public ControllerResult(List events, T response) { + this.events = new ArrayList<>(events); + this.response = response; + } + + public static ControllerResult of(List events, T response) { + return new ControllerResult<>(events, response); + } + + public List getEvents() { + return new ArrayList<>(events); + } + + public T getResponse() { + return response; + } + + public byte[] getBody() { + return body; + } + + public void setBody(byte[] body) { + this.body = body; + } + + public void setCodeAndRemark(int responseCode, String remark) { + this.responseCode = responseCode; + this.remark = remark; + } + + public int getResponseCode() { + return responseCode; + } + + public String getRemark() { + return remark; + } + + public void addEvent(EventMessage event) { + this.events.add(event); + } + + @Override + public String toString() { + return "ControllerResult{" + + "events=" + events + + ", response=" + response + + '}'; + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/event/ElectMasterEvent.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/event/ElectMasterEvent.java new file mode 100644 index 00000000000..c61e792de2a --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/event/ElectMasterEvent.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.event; + +/** + * The event trys to elect a new master for target broker. + * Triggered by the ElectMasterApi. + */ +public class ElectMasterEvent implements EventMessage { + // Mark whether a new master was elected. + private final boolean newMasterElected; + private final String brokerName; + private final Long newMasterBrokerId; + + public ElectMasterEvent(boolean newMasterElected, String brokerName) { + this(newMasterElected, brokerName, null); + } + + public ElectMasterEvent(String brokerName, Long newMasterBrokerId) { + this(true, brokerName, newMasterBrokerId); + } + + public ElectMasterEvent(boolean newMasterElected, String brokerName, Long newMasterBrokerId) { + this.newMasterElected = newMasterElected; + this.brokerName = brokerName; + this.newMasterBrokerId = newMasterBrokerId; + } + + @Override + public EventType getEventType() { + return EventType.ELECT_MASTER_EVENT; + } + + public boolean getNewMasterElected() { + return newMasterElected; + } + + public String getBrokerName() { + return brokerName; + } + + public Long getNewMasterBrokerId() { + return newMasterBrokerId; + } + + @Override + public String toString() { + return "ElectMasterEvent{" + + "newMasterElected=" + newMasterElected + + ", brokerName='" + brokerName + '\'' + + ", newMasterBrokerId=" + newMasterBrokerId + + '}'; + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/event/EventMessage.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/event/EventMessage.java new file mode 100644 index 00000000000..8a31393e1cd --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/event/EventMessage.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.event; + +/** + * The parent class of Event, the subclass needs to indicate eventType. + */ +public interface EventMessage { + + /** + * Returns the event type of this message + */ + EventType getEventType(); +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/event/EventSerializer.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/event/EventSerializer.java new file mode 100644 index 00000000000..b5358c7c3ed --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/event/EventSerializer.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.event; + +import org.apache.commons.lang3.SerializationException; +import org.apache.rocketmq.common.utils.FastJsonSerializer; + +/** + * EventMessage serializer + */ +public class EventSerializer { + private final FastJsonSerializer serializer; + + public EventSerializer() { + this.serializer = new FastJsonSerializer(); + } + + private void putShort(byte[] memory, int index, int value) { + memory[index] = (byte) (value >>> 8); + memory[index + 1] = (byte) value; + } + + private short getShort(byte[] memory, int index) { + return (short) (memory[index] << 8 | memory[index + 1] & 0xFF); + } + + public byte[] serialize(EventMessage message) throws SerializationException { + final short eventType = message.getEventType().getId(); + final byte[] data = this.serializer.serialize(message); + if (data != null && data.length > 0) { + final byte[] result = new byte[2 + data.length]; + putShort(result, 0, eventType); + System.arraycopy(data, 0, result, 2, data.length); + return result; + } + return null; + } + + public EventMessage deserialize(byte[] bytes) throws SerializationException { + if (bytes.length < 2) { + return null; + } + final short eventId = getShort(bytes, 0); + if (eventId > 0) { + final byte[] data = new byte[bytes.length - 2]; + System.arraycopy(bytes, 2, data, 0, data.length); + final EventType eventType = EventType.from(eventId); + if (eventType != null) { + switch (eventType) { + case ALTER_SYNC_STATE_SET_EVENT: + return this.serializer.deserialize(data, AlterSyncStateSetEvent.class); + case APPLY_BROKER_ID_EVENT: + return this.serializer.deserialize(data, ApplyBrokerIdEvent.class); + case ELECT_MASTER_EVENT: + return this.serializer.deserialize(data, ElectMasterEvent.class); + case CLEAN_BROKER_DATA_EVENT: + return this.serializer.deserialize(data, CleanBrokerDataEvent.class); + case UPDATE_BROKER_ADDRESS: + return this.serializer.deserialize(data, UpdateBrokerAddressEvent.class); + default: + break; + } + } + } + return null; + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/event/EventType.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/event/EventType.java new file mode 100644 index 00000000000..2b4cefb1d73 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/event/EventType.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.event; + +/** + * Event type (name, id); + */ +public enum EventType { + ALTER_SYNC_STATE_SET_EVENT("AlterSyncStateSetEvent", (short) 1), + APPLY_BROKER_ID_EVENT("ApplyBrokerIdEvent", (short) 2), + ELECT_MASTER_EVENT("ElectMasterEvent", (short) 3), + READ_EVENT("ReadEvent", (short) 4), + CLEAN_BROKER_DATA_EVENT("CleanBrokerDataEvent", (short) 5), + + UPDATE_BROKER_ADDRESS("UpdateBrokerAddressEvent", (short) 6); + + private final String name; + private final short id; + + EventType(String name, short id) { + this.name = name; + this.id = id; + } + + public static EventType from(short id) { + switch (id) { + case 1: + return ALTER_SYNC_STATE_SET_EVENT; + case 2: + return APPLY_BROKER_ID_EVENT; + case 3: + return ELECT_MASTER_EVENT; + case 4: + return READ_EVENT; + case 5: + return CLEAN_BROKER_DATA_EVENT; + case 6: + return UPDATE_BROKER_ADDRESS; + } + return null; + } + + public String getName() { + return name; + } + + public short getId() { + return id; + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/event/ListEventSerializer.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/event/ListEventSerializer.java new file mode 100644 index 00000000000..ead4895195e --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/event/ListEventSerializer.java @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.event; + +import org.apache.commons.lang3.SerializationException; +import org.apache.rocketmq.common.utils.FastJsonSerializer; +import org.apache.rocketmq.common.utils.Serializer; +import org.apache.rocketmq.logging.org.slf4j.Logger; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.List; + +public class ListEventSerializer { + private ListEventSerializer() { + } + + private static final Serializer SERIALIZER = new FastJsonSerializer(); + + private static void putShort(byte[] memory, int index, int value) { + memory[index] = (byte) (value >>> 8); + memory[index + 1] = (byte) value; + } + + private static void putShort(ByteArrayOutputStream outputStream, int value) { + outputStream.write((byte) (value >>> 8)); + outputStream.write((byte) value); + } + + private static short getShort(byte[] memory, int index) { + return (short) (memory[index] << 8 | memory[index + 1] & 0xFF); + } + + private static void putInt(byte[] memory, int index, int value) { + memory[index] = (byte) (value >>> 24); + memory[index + 1] = (byte) (value >>> 16); + memory[index + 2] = (byte) (value >>> 8); + memory[index + 3] = (byte) value; + } + + private static void putInt(ByteArrayOutputStream outputStream, int value) { + outputStream.write((byte) (value >>> 24)); + outputStream.write((byte) (value >>> 16)); + outputStream.write((byte) (value >>> 8)); + outputStream.write((byte) value); + } + + private static int getInt(byte[] memory, int index) { + return memory[index] << 24 | (memory[index + 1] & 0xFF) << 16 | (memory[index + 2] & 0xFF) << 8 | memory[index + 3] & 0xFF; + } + + public static byte[] serialize(List message, Logger log) throws SerializationException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + for (EventMessage eventMessage : message) { + final short eventType = eventMessage.getEventType().getId(); + final byte[] data = SERIALIZER.serialize(eventMessage); + if (data != null && data.length > 0) { + putShort(outputStream, eventType); + putInt(outputStream, data.length); + outputStream.write(data, 0, data.length); + } else { + log.error("serialize event message error, event: {}, this event will be discard", eventMessage); + } + } + return outputStream.toByteArray(); + } + + public static List deserialize(byte[] bytes, Logger log) throws SerializationException { + List eventMessages = new ArrayList<>(); + if (bytes == null || bytes.length <= 6) { + return eventMessages; + } + int index = 0; + while (index < bytes.length) { + final short eventId = getShort(bytes, index); + index += 2; + final int dataLength = getInt(bytes, index); + index += 4; + if (dataLength > 0) { + final byte[] data = new byte[dataLength]; + System.arraycopy(bytes, index, data, 0, dataLength); + final EventType eventType = EventType.from(eventId); + if (eventType != null) { + switch (eventType) { + case ALTER_SYNC_STATE_SET_EVENT: + eventMessages.add(SERIALIZER.deserialize(data, AlterSyncStateSetEvent.class)); + break; + case APPLY_BROKER_ID_EVENT: + eventMessages.add(SERIALIZER.deserialize(data, ApplyBrokerIdEvent.class)); + break; + case ELECT_MASTER_EVENT: + eventMessages.add(SERIALIZER.deserialize(data, ElectMasterEvent.class)); + break; + case CLEAN_BROKER_DATA_EVENT: + eventMessages.add(SERIALIZER.deserialize(data, CleanBrokerDataEvent.class)); + break; + case UPDATE_BROKER_ADDRESS: + eventMessages.add(SERIALIZER.deserialize(data, UpdateBrokerAddressEvent.class)); + break; + default: + log.error("deserialize event message error, event id: {}, data: {}", eventId, data); + break; + } + } else { + log.error("deserialize event message error, event id: {}, data: {}", eventId, data); + } + index += dataLength; + } else { + log.error("deserialize event message error, event id: {}, data length: {}", eventId, dataLength); + } + } + return eventMessages; + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/event/UpdateBrokerAddressEvent.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/event/UpdateBrokerAddressEvent.java new file mode 100644 index 00000000000..7f1085c4ac6 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/event/UpdateBrokerAddressEvent.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.controller.impl.event; + +public class UpdateBrokerAddressEvent implements EventMessage { + + private String clusterName; + + private String brokerName; + + private String brokerAddress; + + private Long brokerId; + + public UpdateBrokerAddressEvent(String clusterName, String brokerName, String brokerAddress, Long brokerId) { + this.clusterName = clusterName; + this.brokerName = brokerName; + this.brokerAddress = brokerAddress; + this.brokerId = brokerId; + } + + public String getClusterName() { + return clusterName; + } + + public String getBrokerName() { + return brokerName; + } + + public String getBrokerAddress() { + return brokerAddress; + } + + public Long getBrokerId() { + return brokerId; + } + + @Override + public String toString() { + return "UpdateBrokerAddressEvent{" + + "clusterName='" + clusterName + '\'' + + ", brokerName='" + brokerName + '\'' + + ", brokerAddress='" + brokerAddress + '\'' + + ", brokerId=" + brokerId + + '}'; + } + + @Override + public EventType getEventType() { + return EventType.UPDATE_BROKER_ADDRESS; + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/heartbeat/BrokerIdentityInfo.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/heartbeat/BrokerIdentityInfo.java new file mode 100644 index 00000000000..8fc04957e68 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/heartbeat/BrokerIdentityInfo.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.heartbeat; + +import java.io.Serializable; +import org.apache.rocketmq.common.UtilAll; + +import java.util.Objects; + +public class BrokerIdentityInfo implements Serializable { + + private static final long serialVersionUID = 883597359635995567L; + private final String clusterName; + + private final String brokerName; + + private final Long brokerId; + + public BrokerIdentityInfo(String clusterName, String brokerName, Long brokerId) { + this.clusterName = clusterName; + this.brokerName = brokerName; + this.brokerId = brokerId; + } + + public String getClusterName() { + return clusterName; + } + + public Long getBrokerId() { + return brokerId; + } + + public String getBrokerName() { + return brokerName; + } + + public boolean isEmpty() { + return UtilAll.isBlank(clusterName) && UtilAll.isBlank(brokerName) && brokerId == null; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + + if (obj instanceof BrokerIdentityInfo) { + BrokerIdentityInfo addr = (BrokerIdentityInfo) obj; + return clusterName.equals(addr.clusterName) && brokerName.equals(addr.brokerName) && brokerId.equals(addr.brokerId); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(this.clusterName, this.brokerName, this.brokerId); + } + + @Override + public String toString() { + return "BrokerIdentityInfo{" + + "clusterName='" + clusterName + '\'' + + ", brokerName='" + brokerName + '\'' + + ", brokerId=" + brokerId + + '}'; + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/heartbeat/BrokerLiveInfo.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/heartbeat/BrokerLiveInfo.java new file mode 100644 index 00000000000..187f3bab5e8 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/heartbeat/BrokerLiveInfo.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.heartbeat; + +import io.netty.channel.Channel; +import java.io.Serializable; + +public class BrokerLiveInfo implements Serializable { + private static final long serialVersionUID = 3612173344946510993L; + private final String brokerName; + + private String brokerAddr; + private long heartbeatTimeoutMillis; + private Channel channel; + private long brokerId; + private long lastUpdateTimestamp; + private int epoch; + private long maxOffset; + private long confirmOffset; + private Integer electionPriority; + + public BrokerLiveInfo(String brokerName, String brokerAddr, long brokerId, long lastUpdateTimestamp, + long heartbeatTimeoutMillis, Channel channel, int epoch, long maxOffset, Integer electionPriority) { + this.brokerName = brokerName; + this.brokerAddr = brokerAddr; + this.brokerId = brokerId; + this.lastUpdateTimestamp = lastUpdateTimestamp; + this.heartbeatTimeoutMillis = heartbeatTimeoutMillis; + this.channel = channel; + this.epoch = epoch; + this.electionPriority = electionPriority; + this.maxOffset = maxOffset; + } + + public BrokerLiveInfo(String brokerName, String brokerAddr, long brokerId, long lastUpdateTimestamp, + long heartbeatTimeoutMillis, Channel channel, int epoch, long maxOffset, Integer electionPriority, + long confirmOffset) { + this.brokerName = brokerName; + this.brokerAddr = brokerAddr; + this.brokerId = brokerId; + this.lastUpdateTimestamp = lastUpdateTimestamp; + this.heartbeatTimeoutMillis = heartbeatTimeoutMillis; + this.channel = channel; + this.epoch = epoch; + this.maxOffset = maxOffset; + this.electionPriority = electionPriority; + this.confirmOffset = confirmOffset; + } + + @Override + public String toString() { + return "BrokerLiveInfo{" + + "brokerName='" + brokerName + '\'' + + ", brokerAddr='" + brokerAddr + '\'' + + ", heartbeatTimeoutMillis=" + heartbeatTimeoutMillis + + ", channel=" + channel + + ", brokerId=" + brokerId + + ", lastUpdateTimestamp=" + lastUpdateTimestamp + + ", epoch=" + epoch + + ", maxOffset=" + maxOffset + + ", confirmOffset=" + confirmOffset + + '}'; + } + + public String getBrokerName() { + return brokerName; + } + + public long getHeartbeatTimeoutMillis() { + return heartbeatTimeoutMillis; + } + + public void setHeartbeatTimeoutMillis(long heartbeatTimeoutMillis) { + this.heartbeatTimeoutMillis = heartbeatTimeoutMillis; + } + + public Channel getChannel() { + return channel; + } + + public long getBrokerId() { + return brokerId; + } + + public void setBrokerId(long brokerId) { + this.brokerId = brokerId; + } + + public long getLastUpdateTimestamp() { + return lastUpdateTimestamp; + } + + public void setLastUpdateTimestamp(long lastUpdateTimestamp) { + this.lastUpdateTimestamp = lastUpdateTimestamp; + } + + public int getEpoch() { + return epoch; + } + + public void setEpoch(int epoch) { + this.epoch = epoch; + } + + public long getMaxOffset() { + return maxOffset; + } + + public void setMaxOffset(long maxOffset) { + this.maxOffset = maxOffset; + } + + public String getBrokerAddr() { + return brokerAddr; + } + + public void setConfirmOffset(long confirmOffset) { + this.confirmOffset = confirmOffset; + } + + public void setElectionPriority(Integer electionPriority) { + this.electionPriority = electionPriority; + } + + public Integer getElectionPriority() { + return electionPriority; + } + + public long getConfirmOffset() { + return confirmOffset; + } + + public void setBrokerAddr(String brokerAddr) { + this.brokerAddr = brokerAddr; + } + + public void setChannel(Channel channel) { + this.channel = channel; + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/heartbeat/DefaultBrokerHeartbeatManager.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/heartbeat/DefaultBrokerHeartbeatManager.java new file mode 100644 index 00000000000..5ec298a383e --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/heartbeat/DefaultBrokerHeartbeatManager.java @@ -0,0 +1,192 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.heartbeat; + +import io.netty.channel.Channel; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.common.ControllerConfig; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.controller.BrokerHeartbeatManager; +import org.apache.rocketmq.controller.helper.BrokerLifecycleListener; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.common.RemotingHelper; + +public class DefaultBrokerHeartbeatManager implements BrokerHeartbeatManager { + private static final Logger log = LoggerFactory.getLogger(LoggerName.CONTROLLER_LOGGER_NAME); + + private ScheduledExecutorService scheduledService; + private ExecutorService executor; + + private final ControllerConfig controllerConfig; + private final Map brokerLiveTable; + private final List brokerLifecycleListeners; + + public DefaultBrokerHeartbeatManager(final ControllerConfig controllerConfig) { + this.controllerConfig = controllerConfig; + this.brokerLiveTable = new ConcurrentHashMap<>(256); + this.brokerLifecycleListeners = new ArrayList<>(); + } + + @Override + public void start() { + this.scheduledService.scheduleAtFixedRate(this::scanNotActiveBroker, 2000, this.controllerConfig.getScanNotActiveBrokerInterval(), TimeUnit.MILLISECONDS); + } + + @Override + public void shutdown() { + this.scheduledService.shutdown(); + this.executor.shutdown(); + } + + @Override + public void initialize() { + this.scheduledService = ThreadUtils.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("DefaultBrokerHeartbeatManager_scheduledService_")); + this.executor = Executors.newFixedThreadPool(2, new ThreadFactoryImpl("DefaultBrokerHeartbeatManager_executorService_")); + } + + public void scanNotActiveBroker() { + try { + log.info("start scanNotActiveBroker"); + final Iterator> iterator = this.brokerLiveTable.entrySet().iterator(); + while (iterator.hasNext()) { + final Map.Entry next = iterator.next(); + long last = next.getValue().getLastUpdateTimestamp(); + long timeoutMillis = next.getValue().getHeartbeatTimeoutMillis(); + if (System.currentTimeMillis() - last > timeoutMillis) { + final Channel channel = next.getValue().getChannel(); + iterator.remove(); + if (channel != null) { + RemotingHelper.closeChannel(channel); + } + this.executor.submit(() -> + notifyBrokerInActive(next.getKey().getClusterName(), next.getValue().getBrokerName(), next.getValue().getBrokerId())); + log.warn("The broker channel {} expired, brokerInfo {}, expired {}ms", next.getValue().getChannel(), next.getKey(), timeoutMillis); + } + } + } catch (Exception e) { + log.error("scanNotActiveBroker exception", e); + } + } + + private void notifyBrokerInActive(String clusterName, String brokerName, Long brokerId) { + for (BrokerLifecycleListener listener : this.brokerLifecycleListeners) { + listener.onBrokerInactive(clusterName, brokerName, brokerId); + } + } + + @Override + public void registerBrokerLifecycleListener(BrokerLifecycleListener listener) { + this.brokerLifecycleListeners.add(listener); + } + + @Override + public void onBrokerHeartbeat(String clusterName, String brokerName, String brokerAddr, Long brokerId, + Long timeoutMillis, Channel channel, Integer epoch, Long maxOffset, Long confirmOffset, + Integer electionPriority) { + BrokerIdentityInfo brokerIdentityInfo = new BrokerIdentityInfo(clusterName, brokerName, brokerId); + BrokerLiveInfo prev = this.brokerLiveTable.get(brokerIdentityInfo); + int realEpoch = Optional.ofNullable(epoch).orElse(-1); + long realBrokerId = Optional.ofNullable(brokerId).orElse(-1L); + long realMaxOffset = Optional.ofNullable(maxOffset).orElse(-1L); + long realConfirmOffset = Optional.ofNullable(confirmOffset).orElse(-1L); + long realTimeoutMillis = Optional.ofNullable(timeoutMillis).orElse(DEFAULT_BROKER_CHANNEL_EXPIRED_TIME); + int realElectionPriority = Optional.ofNullable(electionPriority).orElse(Integer.MAX_VALUE); + if (null == prev) { + this.brokerLiveTable.put(brokerIdentityInfo, + new BrokerLiveInfo(brokerName, + brokerAddr, + realBrokerId, + System.currentTimeMillis(), + realTimeoutMillis, + channel, + realEpoch, + realMaxOffset, + realElectionPriority)); + log.info("new broker registered, {}, brokerId:{}", brokerIdentityInfo, realBrokerId); + } else { + prev.setLastUpdateTimestamp(System.currentTimeMillis()); + prev.setHeartbeatTimeoutMillis(realTimeoutMillis); + prev.setElectionPriority(realElectionPriority); + if (realEpoch > prev.getEpoch() || realEpoch == prev.getEpoch() && realMaxOffset > prev.getMaxOffset()) { + prev.setEpoch(realEpoch); + prev.setMaxOffset(realMaxOffset); + prev.setConfirmOffset(realConfirmOffset); + } + } + + } + + @Override + public void onBrokerChannelClose(Channel channel) { + BrokerIdentityInfo addrInfo = null; + for (Map.Entry entry : this.brokerLiveTable.entrySet()) { + if (entry.getValue().getChannel() == channel) { + log.info("Channel {} inactive, broker {}, addr:{}, id:{}", entry.getValue().getChannel(), entry.getValue().getBrokerName(), entry.getValue().getBrokerAddr(), entry.getValue().getBrokerId()); + addrInfo = entry.getKey(); + this.executor.submit(() -> + notifyBrokerInActive(entry.getKey().getClusterName(), entry.getValue().getBrokerName(), entry.getValue().getBrokerId())); + break; + } + } + if (addrInfo != null) { + this.brokerLiveTable.remove(addrInfo); + } + } + + @Override + public BrokerLiveInfo getBrokerLiveInfo(String clusterName, String brokerName, Long brokerId) { + return this.brokerLiveTable.get(new BrokerIdentityInfo(clusterName, brokerName, brokerId)); + } + + @Override + public boolean isBrokerActive(String clusterName, String brokerName, Long brokerId) { + final BrokerLiveInfo info = this.brokerLiveTable.get(new BrokerIdentityInfo(clusterName, brokerName, brokerId)); + if (info != null) { + long last = info.getLastUpdateTimestamp(); + long timeoutMillis = info.getHeartbeatTimeoutMillis(); + return (last + timeoutMillis) >= System.currentTimeMillis(); + } + return false; + } + + @Override + public Map> getActiveBrokersNum() { + Map> map = new HashMap<>(); + this.brokerLiveTable.keySet().stream() + .filter(brokerIdentity -> this.isBrokerActive(brokerIdentity.getClusterName(), brokerIdentity.getBrokerName(), brokerIdentity.getBrokerId())) + .forEach(id -> { + map.computeIfAbsent(id.getClusterName(), k -> new HashMap<>()); + map.get(id.getClusterName()).compute(id.getBrokerName(), (broker, num) -> + num == null ? 0 : num + 1 + ); + }); + return map; + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/heartbeat/RaftBrokerHeartBeatManager.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/heartbeat/RaftBrokerHeartBeatManager.java new file mode 100644 index 00000000000..99f7b34d4a4 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/heartbeat/RaftBrokerHeartBeatManager.java @@ -0,0 +1,278 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.heartbeat; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.TypeReference; +import io.netty.channel.Channel; +import org.apache.rocketmq.common.ControllerConfig; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.controller.BrokerHeartbeatManager; +import org.apache.rocketmq.controller.helper.BrokerLifecycleListener; +import org.apache.rocketmq.controller.impl.JRaftController; +import org.apache.rocketmq.controller.impl.task.BrokerCloseChannelRequest; +import org.apache.rocketmq.controller.impl.task.CheckNotActiveBrokerRequest; +import org.apache.rocketmq.controller.impl.task.GetBrokerLiveInfoRequest; +import org.apache.rocketmq.controller.impl.task.GetBrokerLiveInfoResponse; +import org.apache.rocketmq.controller.impl.task.RaftBrokerHeartBeatEventRequest; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.ResponseCode; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class RaftBrokerHeartBeatManager implements BrokerHeartbeatManager { + private static final Logger log = LoggerFactory.getLogger(LoggerName.CONTROLLER_LOGGER_NAME); + private JRaftController controller; + private final List brokerLifecycleListeners = new ArrayList<>(); + private final ScheduledExecutorService scheduledService; + private final ExecutorService executor; + private final ControllerConfig controllerConfig; + + private final Map brokerChannelIdentityInfoMap = new HashMap<>(); + + + // resolve the scene + // when controller all down and startup again, we wait for some time to avoid electing a new leader,which is not necessary + private long firstReceivedHeartbeatTime = -1; + + public RaftBrokerHeartBeatManager(ControllerConfig controllerConfig) { + this.scheduledService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("RaftBrokerHeartbeatManager_scheduledService_")); + this.executor = Executors.newFixedThreadPool(2, new ThreadFactoryImpl("RaftBrokerHeartbeatManager_executorService_")); + this.controllerConfig = controllerConfig; + } + + public void setController(JRaftController controller) { + this.controller = controller; + } + + @Override + public void initialize() { + + } + + @Override + public void start() { + this.scheduledService.scheduleAtFixedRate(this::scanNotActiveBroker, 2000, this.controllerConfig.getScanNotActiveBrokerInterval(), TimeUnit.MILLISECONDS); + } + + @Override + public void shutdown() { + this.scheduledService.shutdown(); + this.executor.shutdown(); + } + + @Override + public void registerBrokerLifecycleListener(BrokerLifecycleListener listener) { + brokerLifecycleListeners.add(listener); + } + + @Override + public void onBrokerHeartbeat(String clusterName, String brokerName, String brokerAddr, Long brokerId, + Long timeoutMillis, Channel channel, Integer epoch, Long maxOffset, Long confirmOffset, + Integer electionPriority) { + + if (firstReceivedHeartbeatTime == -1) { + firstReceivedHeartbeatTime = System.currentTimeMillis(); + } + + BrokerIdentityInfo brokerIdentityInfo = new BrokerIdentityInfo(clusterName, brokerName, brokerId); + int realEpoch = Optional.ofNullable(epoch).orElse(-1); + long realBrokerId = Optional.ofNullable(brokerId).orElse(-1L); + long realMaxOffset = Optional.ofNullable(maxOffset).orElse(-1L); + long realConfirmOffset = Optional.ofNullable(confirmOffset).orElse(-1L); + long realTimeoutMillis = Optional.ofNullable(timeoutMillis).orElse(DEFAULT_BROKER_CHANNEL_EXPIRED_TIME); + int realElectionPriority = Optional.ofNullable(electionPriority).orElse(Integer.MAX_VALUE); + BrokerLiveInfo liveInfo = new BrokerLiveInfo(brokerName, + brokerAddr, + realBrokerId, + System.currentTimeMillis(), + realTimeoutMillis, + null, + realEpoch, + realMaxOffset, + realElectionPriority, + realConfirmOffset); + log.info("broker {} heart beat", brokerIdentityInfo); + RaftBrokerHeartBeatEventRequest requestHeader = new RaftBrokerHeartBeatEventRequest(brokerIdentityInfo, liveInfo); + CompletableFuture future = controller.onBrokerHeartBeat(requestHeader); + try { + RemotingCommand remotingCommand = future.get(5, java.util.concurrent.TimeUnit.SECONDS); + if (remotingCommand.getCode() != ResponseCode.SUCCESS && remotingCommand.getCode() != ResponseCode.CONTROLLER_NOT_LEADER) { + throw new RuntimeException("on broker heartbeat return invalid code, code: " + remotingCommand.getCode()); + } + } catch (ExecutionException | InterruptedException | TimeoutException | RuntimeException e) { + log.error("on broker heartbeat through raft failed", e); + } + brokerChannelIdentityInfoMap.put(channel, brokerIdentityInfo); + } + + @Override + public void onBrokerChannelClose(Channel channel) { + BrokerIdentityInfo brokerIdentityInfo = brokerChannelIdentityInfoMap.get(channel); + log.info("Channel {} inactive, broker identity info: {}", channel, brokerIdentityInfo); + if (brokerIdentityInfo != null) { + BrokerCloseChannelRequest requestHeader = new BrokerCloseChannelRequest(brokerIdentityInfo); + CompletableFuture future = controller.onBrokerCloseChannel(requestHeader); + try { + RemotingCommand remotingCommand = future.get(5, java.util.concurrent.TimeUnit.SECONDS); + if (remotingCommand.getCode() != ResponseCode.SUCCESS) { + throw new RuntimeException("on broker close channel return invalid code, code: " + remotingCommand.getCode()); + } + this.executor.submit(() -> notifyBrokerInActive(brokerIdentityInfo.getClusterName(), brokerIdentityInfo.getBrokerName(), brokerIdentityInfo.getBrokerId())); + brokerChannelIdentityInfoMap.remove(channel); + } catch (ExecutionException | InterruptedException | TimeoutException | RuntimeException e) { + log.error("on broker close channel through raft failed", e); + } + } + } + + /** + * @param brokerIdentityInfo null means get broker live info of all brokers + */ + private Map getBrokerLiveInfo(BrokerIdentityInfo brokerIdentityInfo) { + GetBrokerLiveInfoRequest requestHeader; + if (brokerIdentityInfo == null) { + requestHeader = new GetBrokerLiveInfoRequest(); + } else { + requestHeader = new GetBrokerLiveInfoRequest(brokerIdentityInfo); + } + CompletableFuture future = controller.getBrokerLiveInfo(requestHeader); + try { + RemotingCommand remotingCommand = future.get(5, java.util.concurrent.TimeUnit.SECONDS); + if (remotingCommand.getCode() != ResponseCode.SUCCESS) { + throw new RuntimeException("get broker live info return invalid code, code: " + remotingCommand.getCode()); + } + GetBrokerLiveInfoResponse getBrokerLiveInfoResponse = (GetBrokerLiveInfoResponse) remotingCommand.decodeCommandCustomHeader(GetBrokerLiveInfoResponse.class); + return JSON.parseObject(remotingCommand.getBody(), new TypeReference>() { + }.getType()); + } catch (Throwable e) { + log.error("get broker live info through raft failed", e); + } + return new HashMap<>(); + } + + private void scanNotActiveBroker() { + if (!controller.isLeaderState()) { + log.info("current node is not leader, skip scan not active broker"); + return; + } + + // if has not received any heartbeat from broker, we do not need to scan + if (this.firstReceivedHeartbeatTime + controllerConfig.getJraftConfig().getjRaftScanWaitTimeoutMs() < System.currentTimeMillis()) { + log.info("has not received any heartbeat from broker, skip scan not active broker"); + return; + } + + log.info("start scan not active broker"); + CheckNotActiveBrokerRequest requestHeader = new CheckNotActiveBrokerRequest(); + CompletableFuture future = this.controller.checkNotActiveBroker(requestHeader); + try { + RemotingCommand remotingCommand = future.get(5, java.util.concurrent.TimeUnit.SECONDS); + if (remotingCommand.getCode() != ResponseCode.SUCCESS) { + throw new RuntimeException("check not active broker return invalid code, code: " + remotingCommand.getCode()); + } + List notActiveAndNeedReElectBrokerIdentityInfoList = JSON.parseObject(remotingCommand.getBody(), new TypeReference>() { + }.getType()); + if (notActiveAndNeedReElectBrokerIdentityInfoList != null && !notActiveAndNeedReElectBrokerIdentityInfoList.isEmpty()) { + notActiveAndNeedReElectBrokerIdentityInfoList.forEach(brokerIdentityInfo -> { + Iterator> iterator = brokerChannelIdentityInfoMap.entrySet().iterator(); + Channel channel = null; + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (entry.getValue().getBrokerId() == null) { + continue; + } + if (entry.getValue().equals(brokerIdentityInfo)) { + channel = entry.getKey(); + RemotingHelper.closeChannel(entry.getKey()); + iterator.remove(); + break; + } + } + this.executor.submit(() -> notifyBrokerInActive(brokerIdentityInfo.getClusterName(), brokerIdentityInfo.getBrokerName(), brokerIdentityInfo.getBrokerId())); + log.warn("The broker channel {} expired, brokerInfo {}", channel, brokerIdentityInfo); + }); + } + } catch (Throwable e) { + log.error("check not active broker through raft failed", e); + } + } + + @Override + public BrokerLiveInfo getBrokerLiveInfo(String clusterName, String brokerName, Long brokerId) { + log.info("get broker live info, clusterName: {}, brokerName: {}, brokerId: {}", clusterName, brokerName, brokerId); + BrokerIdentityInfo brokerIdentityInfo = new BrokerIdentityInfo(clusterName, brokerName, brokerId); + Map brokerLiveInfoMap = getBrokerLiveInfo(brokerIdentityInfo); + return brokerLiveInfoMap.get(brokerIdentityInfo); + } + + @Override + public boolean isBrokerActive(String clusterName, String brokerName, Long brokerId) { + BrokerLiveInfo info = null; + try { + info = getBrokerLiveInfo(clusterName, brokerName, brokerId); + } catch (RuntimeException e) { + log.error("get broker live info failed", e); + return false; + } + + if (info != null) { + long last = info.getLastUpdateTimestamp(); + long timeoutMillis = info.getHeartbeatTimeoutMillis(); + return (last + timeoutMillis) >= System.currentTimeMillis(); + } + return false; + } + + @Override + public Map> getActiveBrokersNum() { + Map> map = new HashMap<>(); + Map brokerLiveInfoMap = getBrokerLiveInfo(null); + brokerLiveInfoMap.keySet().stream() + .filter(brokerIdentity -> this.isBrokerActive(brokerIdentity.getClusterName(), brokerIdentity.getBrokerName(), brokerIdentity.getBrokerId())) + .forEach(id -> { + map.computeIfAbsent(id.getClusterName(), k -> new HashMap<>()); + map.get(id.getClusterName()).compute(id.getBrokerName(), (broker, num) -> + num == null ? 0 : num + 1 + ); + }); + return map; + } + + private void notifyBrokerInActive(String clusterName, String brokerName, Long brokerId) { + log.info("Broker {}-{}-{} inactive", clusterName, brokerName, brokerId); + for (BrokerLifecycleListener listener : this.brokerLifecycleListeners) { + listener.onBrokerInactive(clusterName, brokerName, brokerId); + } + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/manager/BrokerReplicaInfo.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/manager/BrokerReplicaInfo.java new file mode 100644 index 00000000000..1623a05908d --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/manager/BrokerReplicaInfo.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.manager; + +import java.io.Serializable; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.Pair; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Broker replicas info, mapping from brokerAddress to {brokerId, brokerHaAddress}. + */ +public class BrokerReplicaInfo implements Serializable { + private final String clusterName; + + private final String brokerName; + + // Start from 1 + private final AtomicLong nextAssignBrokerId; + + private final Map> brokerIdInfo; + + public BrokerReplicaInfo(String clusterName, String brokerName) { + this.clusterName = clusterName; + this.brokerName = brokerName; + this.nextAssignBrokerId = new AtomicLong(MixAll.FIRST_BROKER_CONTROLLER_ID); + this.brokerIdInfo = new ConcurrentHashMap<>(); + } + + public void removeBrokerId(final Long brokerId) { + this.brokerIdInfo.remove(brokerId); + } + + public Long getNextAssignBrokerId() { + return nextAssignBrokerId.get(); + } + + public String getClusterName() { + return clusterName; + } + + public String getBrokerName() { + return brokerName; + } + + public void addBroker(final Long brokerId, final String ipAddress, final String registerCheckCode) { + this.brokerIdInfo.put(brokerId, new Pair<>(ipAddress, registerCheckCode)); + this.nextAssignBrokerId.incrementAndGet(); + } + + public boolean isBrokerExist(final Long brokerId) { + return this.brokerIdInfo.containsKey(brokerId); + } + + public Set getAllBroker() { + return new HashSet<>(this.brokerIdInfo.keySet()); + } + + public Map getBrokerIdTable() { + Map map = new HashMap<>(this.brokerIdInfo.size()); + this.brokerIdInfo.forEach((id, pair) -> { + map.put(id, pair.getObject1()); + }); + return map; + } + + public String getBrokerAddress(final Long brokerId) { + if (brokerId == null) { + return null; + } + Pair pair = this.brokerIdInfo.get(brokerId); + if (pair != null) { + return pair.getObject1(); + } + return null; + } + + public String getBrokerRegisterCheckCode(final Long brokerId) { + if (brokerId == null) { + return null; + } + Pair pair = this.brokerIdInfo.get(brokerId); + if (pair != null) { + return pair.getObject2(); + } + return null; + } + + public void updateBrokerAddress(final Long brokerId, final String brokerAddress) { + if (brokerId == null) + return; + Pair oldPair = this.brokerIdInfo.get(brokerId); + if (oldPair != null) { + this.brokerIdInfo.put(brokerId, new Pair<>(brokerAddress, oldPair.getObject2())); + } + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/manager/RaftReplicasInfoManager.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/manager/RaftReplicasInfoManager.java new file mode 100644 index 00000000000..492043235b9 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/manager/RaftReplicasInfoManager.java @@ -0,0 +1,236 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.manager; + +import com.alibaba.fastjson.JSON; +import org.apache.rocketmq.common.ControllerConfig; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.controller.helper.BrokerValidPredicate; +import org.apache.rocketmq.controller.impl.event.ControllerResult; +import org.apache.rocketmq.controller.impl.heartbeat.BrokerIdentityInfo; +import org.apache.rocketmq.controller.impl.heartbeat.BrokerLiveInfo; +import org.apache.rocketmq.controller.impl.task.BrokerCloseChannelRequest; +import org.apache.rocketmq.controller.impl.task.BrokerCloseChannelResponse; +import org.apache.rocketmq.controller.impl.task.CheckNotActiveBrokerRequest; +import org.apache.rocketmq.controller.impl.task.CheckNotActiveBrokerResponse; +import org.apache.rocketmq.controller.impl.task.GetBrokerLiveInfoRequest; +import org.apache.rocketmq.controller.impl.task.GetBrokerLiveInfoResponse; +import org.apache.rocketmq.controller.impl.task.RaftBrokerHeartBeatEventRequest; +import org.apache.rocketmq.controller.impl.task.RaftBrokerHeartBeatEventResponse; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.ResponseCode; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public class RaftReplicasInfoManager extends ReplicasInfoManager { + private static final Logger log = LoggerFactory.getLogger(LoggerName.CONTROLLER_LOGGER_NAME); + private final Map brokerLiveTable = new ConcurrentHashMap<>(256); + + public RaftReplicasInfoManager(ControllerConfig controllerConfig) { + super(controllerConfig); + } + + public ControllerResult getBrokerLiveInfo(final GetBrokerLiveInfoRequest request) { + BrokerIdentityInfo brokerIdentityInfo = request.getBrokerIdentity(); + ControllerResult result = new ControllerResult<>(new GetBrokerLiveInfoResponse()); + Map resBrokerLiveTable = new HashMap<>(); + if (brokerIdentityInfo == null || brokerIdentityInfo.isEmpty()) { + resBrokerLiveTable.putAll(this.brokerLiveTable); + } else { + if (brokerLiveTable.containsKey(brokerIdentityInfo)) { + resBrokerLiveTable.put(brokerIdentityInfo, brokerLiveTable.get(brokerIdentityInfo)); + } else { + log.warn("GetBrokerLiveInfo failed, brokerIdentityInfo: {} not exist", brokerIdentityInfo); + result.setCodeAndRemark(ResponseCode.CONTROLLER_BROKER_LIVE_INFO_NOT_EXISTS, "brokerIdentityInfo not exist"); + } + } + try { + result.setBody(JSON.toJSONBytes(resBrokerLiveTable)); + } catch (Throwable e) { + log.error("json serialize resBrokerLiveTable {} error", resBrokerLiveTable, e); + result.setCodeAndRemark(ResponseCode.SYSTEM_ERROR, "serialize error"); + } + + return result; + } + + public ControllerResult onBrokerHeartBeat( + RaftBrokerHeartBeatEventRequest request) { + BrokerIdentityInfo brokerIdentityInfo = request.getBrokerIdentityInfo(); + BrokerLiveInfo brokerLiveInfo = request.getBrokerLiveInfo(); + ControllerResult result = new ControllerResult<>(new RaftBrokerHeartBeatEventResponse()); + BrokerLiveInfo prev = brokerLiveTable.computeIfAbsent(brokerIdentityInfo, identityInfo -> { + log.info("new broker registered, brokerIdentityInfo: {}", identityInfo); + return brokerLiveInfo; + }); + prev.setLastUpdateTimestamp(brokerLiveInfo.getLastUpdateTimestamp()); + prev.setHeartbeatTimeoutMillis(brokerLiveInfo.getHeartbeatTimeoutMillis()); + prev.setElectionPriority(brokerLiveInfo.getElectionPriority()); + if (brokerLiveInfo.getEpoch() > prev.getEpoch() || brokerLiveInfo.getEpoch() == prev.getEpoch() && brokerLiveInfo.getMaxOffset() > prev.getMaxOffset()) { + prev.setEpoch(brokerLiveInfo.getEpoch()); + prev.setMaxOffset(brokerLiveInfo.getMaxOffset()); + prev.setConfirmOffset(brokerLiveInfo.getConfirmOffset()); + } + return result; + } + + public ControllerResult onBrokerCloseChannel(BrokerCloseChannelRequest request) { + BrokerIdentityInfo brokerIdentityInfo = request.getBrokerIdentityInfo(); + ControllerResult result = new ControllerResult<>(new BrokerCloseChannelResponse()); + if (brokerIdentityInfo == null || brokerIdentityInfo.isEmpty()) { + log.warn("onBrokerCloseChannel failed, brokerIdentityInfo is null"); + } else { + brokerLiveTable.remove(brokerIdentityInfo); + log.info("onBrokerCloseChannel success, brokerIdentityInfo: {}", brokerIdentityInfo); + } + return result; + } + + public ControllerResult checkNotActiveBroker(CheckNotActiveBrokerRequest request) { + List notActiveBrokerIdentityInfoList = new ArrayList<>(); + long checkTime = request.getCheckTimeMillis(); + final Iterator> iterator = this.brokerLiveTable.entrySet().iterator(); + while (iterator.hasNext()) { + final Map.Entry next = iterator.next(); + long last = next.getValue().getLastUpdateTimestamp(); + long timeoutMillis = next.getValue().getHeartbeatTimeoutMillis(); + if (checkTime - last > timeoutMillis) { + notActiveBrokerIdentityInfoList.add(next.getKey()); + iterator.remove(); + log.warn("Broker expired, brokerInfo {}, expired {}ms", next.getKey(), timeoutMillis); + } + } + List needReElectBrokerNames = scanNeedReelectBrokerSets(new BrokerValidPredicate() { + @Override + public boolean check(String clusterName, String brokerName, Long brokerId) { + return !isBrokerActive(clusterName, brokerName, brokerId, checkTime); + } + }); + Set alreadyReportedBrokerName = notActiveBrokerIdentityInfoList.stream() + .map(BrokerIdentityInfo::getBrokerName) + .collect(Collectors.toSet()); + // avoid to duplicate report, filter by name, + // because BrokerIdentityInfo in needReElectBrokerNames does not have brokerId or clusterName + notActiveBrokerIdentityInfoList.addAll(needReElectBrokerNames.stream() + .filter(brokerName -> !alreadyReportedBrokerName.contains(brokerName)) + .map(brokerName -> new BrokerIdentityInfo(null, brokerName, null)) + .collect(Collectors.toList())); + ControllerResult result = new ControllerResult<>(new CheckNotActiveBrokerResponse()); + try { + result.setBody(JSON.toJSONBytes(notActiveBrokerIdentityInfoList)); + } catch (Throwable e) { + log.error("json serialize notActiveBrokerIdentityInfoList {} error", notActiveBrokerIdentityInfoList, e); + result.setCodeAndRemark(ResponseCode.SYSTEM_ERROR, "serialize error"); + } + return result; + } + + public boolean isBrokerActive(String clusterName, String brokerName, Long brokerId, long invokeTime) { + final BrokerLiveInfo info = this.brokerLiveTable.get(new BrokerIdentityInfo(clusterName, brokerName, brokerId)); + if (info != null) { + long last = info.getLastUpdateTimestamp(); + long timeoutMillis = info.getHeartbeatTimeoutMillis(); + return (last + timeoutMillis) >= invokeTime; + } + return false; + } + + public BrokerLiveInfo getBrokerLiveInfo(String clusterName, String brokerName, Long brokerId) { + return this.brokerLiveTable.get(new BrokerIdentityInfo(clusterName, brokerName, brokerId)); + } + + @Override + public byte[] serialize() throws Throwable { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + final byte[] superSerialize = super.serialize(); + putInt(outputStream, superSerialize.length); + outputStream.write(superSerialize); + putInt(outputStream, this.brokerLiveTable.size()); + for (Map.Entry entry : brokerLiveTable.entrySet()) { + final byte[] brokerIdentityInfo = hessianSerialize(entry.getKey()); + final byte[] brokerLiveInfo = hessianSerialize(entry.getValue()); + putInt(outputStream, brokerIdentityInfo.length); + outputStream.write(brokerIdentityInfo); + putInt(outputStream, brokerLiveInfo.length); + outputStream.write(brokerLiveInfo); + } + return outputStream.toByteArray(); + } catch (Throwable e) { + log.error("serialize replicaInfoTable or syncStateSetInfoTable error", e); + throw e; + } + } + + @Override + public void deserializeFrom(byte[] data) throws Throwable { + int index = 0; + this.brokerLiveTable.clear(); + + try { + int superTableSize = getInt(data, index); + index += 4; + byte[] superTableData = new byte[superTableSize]; + System.arraycopy(data, index, superTableData, 0, superTableSize); + super.deserializeFrom(superTableData); + index += superTableSize; + int brokerLiveTableSize = getInt(data, index); + index += 4; + for (int i = 0; i < brokerLiveTableSize; i++) { + int brokerIdentityInfoLength = getInt(data, index); + index += 4; + byte[] brokerIdentityInfoArray = new byte[brokerIdentityInfoLength]; + System.arraycopy(data, index, brokerIdentityInfoArray, 0, brokerIdentityInfoLength); + BrokerIdentityInfo brokerIdentityInfo = (BrokerIdentityInfo) hessianDeserialize(brokerIdentityInfoArray); + index += brokerIdentityInfoLength; + int brokerLiveInfoLength = getInt(data, index); + index += 4; + byte[] brokerLiveInfoArray = new byte[brokerLiveInfoLength]; + System.arraycopy(data, index, brokerLiveInfoArray, 0, brokerLiveInfoLength); + BrokerLiveInfo brokerLiveInfo = (BrokerLiveInfo) hessianDeserialize(brokerLiveInfoArray); + index += brokerLiveInfoLength; + this.brokerLiveTable.put(brokerIdentityInfo, brokerLiveInfo); + } + } catch (Throwable e) { + log.error("deserialize replicaInfoTable or syncStateSetInfoTable error", e); + throw e; + } + } + + public static class BrokerValidPredicateWithInvokeTime implements BrokerValidPredicate { + private final long invokeTime; + private final RaftReplicasInfoManager raftBrokerHeartBeatManager; + + public BrokerValidPredicateWithInvokeTime(long invokeTime, RaftReplicasInfoManager raftBrokerHeartBeatManager) { + this.invokeTime = invokeTime; + this.raftBrokerHeartBeatManager = raftBrokerHeartBeatManager; + } + + @Override + public boolean check(String clusterName, String brokerName, Long brokerId) { + return raftBrokerHeartBeatManager.isBrokerActive(clusterName, brokerName, brokerId, invokeTime); + } + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/manager/ReplicasInfoManager.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/manager/ReplicasInfoManager.java new file mode 100644 index 00000000000..086058d63a4 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/manager/ReplicasInfoManager.java @@ -0,0 +1,689 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.manager; + +import com.caucho.hessian.io.Hessian2Input; +import com.caucho.hessian.io.Hessian2Output; +import com.caucho.hessian.io.SerializerFactory; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.ControllerConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.controller.elect.ElectPolicy; +import org.apache.rocketmq.controller.helper.BrokerValidPredicate; +import org.apache.rocketmq.controller.impl.event.AlterSyncStateSetEvent; +import org.apache.rocketmq.controller.impl.event.ApplyBrokerIdEvent; +import org.apache.rocketmq.controller.impl.event.CleanBrokerDataEvent; +import org.apache.rocketmq.controller.impl.event.ControllerResult; +import org.apache.rocketmq.controller.impl.event.ElectMasterEvent; +import org.apache.rocketmq.controller.impl.event.EventMessage; +import org.apache.rocketmq.controller.impl.event.EventType; +import org.apache.rocketmq.controller.impl.event.UpdateBrokerAddressEvent; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.BrokerMemberGroup; +import org.apache.rocketmq.remoting.protocol.body.BrokerReplicasInfo; +import org.apache.rocketmq.remoting.protocol.body.ElectMasterResponseBody; +import org.apache.rocketmq.remoting.protocol.body.SyncStateSet; +import org.apache.rocketmq.remoting.protocol.header.controller.AlterSyncStateSetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.AlterSyncStateSetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetReplicaInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetReplicaInfoResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.admin.CleanControllerBrokerDataRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.ApplyBrokerIdRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.ApplyBrokerIdResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.GetNextBrokerIdRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.GetNextBrokerIdResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.RegisterBrokerToControllerRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.RegisterBrokerToControllerResponseHeader; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * The manager that manages the replicas info for all brokers. We can think of this class as the controller's memory + * state machine. If the upper layer want to update the statemachine, it must sequentially call its methods. + */ +public class ReplicasInfoManager { + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.CONTROLLER_LOGGER_NAME); + + protected static final SerializerFactory SERIALIZER_FACTORY = new SerializerFactory(); + protected final ControllerConfig controllerConfig; + private final Map replicaInfoTable; + private final Map syncStateSetInfoTable; + + protected static byte[] hessianSerialize(Object object) throws IOException { + try (ByteArrayOutputStream bout = new ByteArrayOutputStream()) { + Hessian2Output hessianOut = new Hessian2Output(bout); + hessianOut.setSerializerFactory(SERIALIZER_FACTORY); + hessianOut.writeObject(object); + hessianOut.close(); + return bout.toByteArray(); + } + } + + protected static Object hessianDeserialize(byte[] data) throws IOException { + try (ByteArrayInputStream bin = new ByteArrayInputStream(data, 0, data.length)) { + Hessian2Input hin = new Hessian2Input(bin); + hin.setSerializerFactory(new SerializerFactory()); + Object o = hin.readObject(); + hin.close(); + return o; + } + } + + public ReplicasInfoManager(final ControllerConfig config) { + this.controllerConfig = config; + this.replicaInfoTable = new ConcurrentHashMap(); + this.syncStateSetInfoTable = new ConcurrentHashMap(); + } + + public ControllerResult alterSyncStateSet( + final AlterSyncStateSetRequestHeader request, final SyncStateSet syncStateSet, + final BrokerValidPredicate brokerAlivePredicate) { + final String brokerName = request.getBrokerName(); + final ControllerResult result = new ControllerResult<>(new AlterSyncStateSetResponseHeader()); + final AlterSyncStateSetResponseHeader response = result.getResponse(); + + if (!isContainsBroker(brokerName)) { + result.setCodeAndRemark(ResponseCode.CONTROLLER_ALTER_SYNC_STATE_SET_FAILED, "Broker metadata is not existed"); + return result; + } + final Set newSyncStateSet = syncStateSet.getSyncStateSet(); + final SyncStateInfo syncStateInfo = this.syncStateSetInfoTable.get(brokerName); + final BrokerReplicaInfo brokerReplicaInfo = this.replicaInfoTable.get(brokerName); + + // Check whether the oldSyncStateSet is equal with newSyncStateSet + final Set oldSyncStateSet = syncStateInfo.getSyncStateSet(); + if (oldSyncStateSet.size() == newSyncStateSet.size() && oldSyncStateSet.containsAll(newSyncStateSet)) { + String err = "The newSyncStateSet is equal with oldSyncStateSet, no needed to update syncStateSet"; + LOGGER.warn("{}", err); + result.setCodeAndRemark(ResponseCode.CONTROLLER_ALTER_SYNC_STATE_SET_FAILED, err); + return result; + } + + // Check master + if (syncStateInfo.getMasterBrokerId() == null || !syncStateInfo.getMasterBrokerId().equals(request.getMasterBrokerId())) { + String err = String.format("Rejecting alter syncStateSet request because the current leader is:{%s}, not {%s}", + syncStateInfo.getMasterBrokerId(), request.getMasterBrokerId()); + LOGGER.error("{}", err); + result.setCodeAndRemark(ResponseCode.CONTROLLER_INVALID_MASTER, err); + return result; + } + + // Check master epoch + if (request.getMasterEpoch() != syncStateInfo.getMasterEpoch()) { + String err = String.format("Rejecting alter syncStateSet request because the current master epoch is:{%d}, not {%d}", + syncStateInfo.getMasterEpoch(), request.getMasterEpoch()); + LOGGER.error("{}", err); + result.setCodeAndRemark(ResponseCode.CONTROLLER_FENCED_MASTER_EPOCH, err); + return result; + } + + // Check syncStateSet epoch + if (syncStateSet.getSyncStateSetEpoch() != syncStateInfo.getSyncStateSetEpoch()) { + String err = String.format("Rejecting alter syncStateSet request because the current syncStateSet epoch is:{%d}, not {%d}", + syncStateInfo.getSyncStateSetEpoch(), syncStateSet.getSyncStateSetEpoch()); + LOGGER.error("{}", err); + result.setCodeAndRemark(ResponseCode.CONTROLLER_FENCED_SYNC_STATE_SET_EPOCH, err); + return result; + } + + // Check newSyncStateSet correctness + for (Long replica : newSyncStateSet) { + if (!brokerReplicaInfo.isBrokerExist(replica)) { + String err = String.format("Rejecting alter syncStateSet request because the replicas {%s} don't exist", replica); + LOGGER.error("{}", err); + result.setCodeAndRemark(ResponseCode.CONTROLLER_INVALID_REPLICAS, err); + return result; + } + if (!brokerAlivePredicate.check(brokerReplicaInfo.getClusterName(), brokerReplicaInfo.getBrokerName(), replica)) { + String err = String.format("Rejecting alter syncStateSet request because the replicas {%s} don't alive", replica); + LOGGER.error(err); + result.setCodeAndRemark(ResponseCode.CONTROLLER_BROKER_NOT_ALIVE, err); + return result; + } + } + + if (!newSyncStateSet.contains(syncStateInfo.getMasterBrokerId())) { + String err = String.format("Rejecting alter syncStateSet request because the newSyncStateSet don't contains origin leader {%s}", syncStateInfo.getMasterBrokerId()); + LOGGER.error(err); + result.setCodeAndRemark(ResponseCode.CONTROLLER_ALTER_SYNC_STATE_SET_FAILED, err); + return result; + } + + // Generate event + int epoch = syncStateInfo.getSyncStateSetEpoch() + 1; + response.setNewSyncStateSetEpoch(epoch); + result.setBody(new SyncStateSet(newSyncStateSet, epoch).encode()); + final AlterSyncStateSetEvent event = new AlterSyncStateSetEvent(brokerName, newSyncStateSet); + result.addEvent(event); + return result; + } + + public ControllerResult electMaster(final ElectMasterRequestHeader request, + final ElectPolicy electPolicy) { + final String brokerName = request.getBrokerName(); + final Long brokerId = request.getBrokerId(); + final ControllerResult result = new ControllerResult<>(new ElectMasterResponseHeader()); + final ElectMasterResponseHeader response = result.getResponse(); + if (!isContainsBroker(brokerName)) { + // this broker set hasn't been registered + result.setCodeAndRemark(ResponseCode.CONTROLLER_BROKER_NEED_TO_BE_REGISTERED, "Broker hasn't been registered"); + return result; + } + + final SyncStateInfo syncStateInfo = this.syncStateSetInfoTable.get(brokerName); + final BrokerReplicaInfo brokerReplicaInfo = this.replicaInfoTable.get(brokerName); + final Set syncStateSet = syncStateInfo.getSyncStateSet(); + final Long oldMaster = syncStateInfo.getMasterBrokerId(); + Set allReplicaBrokers = controllerConfig.isEnableElectUncleanMaster() ? brokerReplicaInfo.getAllBroker() : null; + Long newMaster = null; + + if (syncStateInfo.isFirstTimeForElect()) { + // If never have a master in this broker set, in other words, it is the first time to elect a master + // elect it as the first master + newMaster = brokerId; + } + + // elect by policy + if (newMaster == null || newMaster == -1) { + // we should assign this assignedBrokerId when the brokerAddress need to be elected by force + Long assignedBrokerId = request.getDesignateElect() ? brokerId : null; + newMaster = electPolicy.elect(brokerReplicaInfo.getClusterName(), brokerReplicaInfo.getBrokerName(), syncStateSet, allReplicaBrokers, oldMaster, assignedBrokerId); + } + + if (newMaster != null && newMaster.equals(oldMaster)) { + // old master still valid, change nothing + String err = String.format("The old master %s is still alive, not need to elect new master for broker %s", oldMaster, brokerReplicaInfo.getBrokerName()); + LOGGER.warn("{}", err); + // the master still exist + response.setMasterEpoch(syncStateInfo.getMasterEpoch()); + response.setSyncStateSetEpoch(syncStateInfo.getSyncStateSetEpoch()); + response.setMasterBrokerId(oldMaster); + response.setMasterAddress(brokerReplicaInfo.getBrokerAddress(oldMaster)); + + result.setBody(new ElectMasterResponseBody(syncStateSet).encode()); + result.setCodeAndRemark(ResponseCode.CONTROLLER_MASTER_STILL_EXIST, err); + return result; + } + + // a new master is elected + if (newMaster != null) { + final int masterEpoch = syncStateInfo.getMasterEpoch(); + final int syncStateSetEpoch = syncStateInfo.getSyncStateSetEpoch(); + final HashSet newSyncStateSet = new HashSet<>(); + newSyncStateSet.add(newMaster); + + response.setMasterBrokerId(newMaster); + response.setMasterAddress(brokerReplicaInfo.getBrokerAddress(newMaster)); + response.setMasterEpoch(masterEpoch + 1); + response.setSyncStateSetEpoch(syncStateSetEpoch + 1); + ElectMasterResponseBody responseBody = new ElectMasterResponseBody(newSyncStateSet); + + BrokerMemberGroup brokerMemberGroup = buildBrokerMemberGroup(brokerReplicaInfo); + if (null != brokerMemberGroup) { + responseBody.setBrokerMemberGroup(brokerMemberGroup); + } + + result.setBody(responseBody.encode()); + final ElectMasterEvent event = new ElectMasterEvent(brokerName, newMaster); + result.addEvent(event); + LOGGER.info("Elect new master {} for broker {}", newMaster, brokerName); + return result; + } + // If elect failed and the electMaster is triggered by controller (we can figure it out by brokerAddress), + // we still need to apply an ElectMasterEvent to tell the statemachine + // that the master was shutdown and no new master was elected. + if (request.getBrokerId() == null || request.getBrokerId() == -1) { + final ElectMasterEvent event = new ElectMasterEvent(false, brokerName); + result.addEvent(event); + result.setCodeAndRemark(ResponseCode.CONTROLLER_MASTER_NOT_AVAILABLE, "Old master has down and failed to elect a new broker master"); + } else { + result.setCodeAndRemark(ResponseCode.CONTROLLER_ELECT_MASTER_FAILED, "Failed to elect a new master"); + } + LOGGER.warn("Failed to elect a new master for broker {}", brokerName); + return result; + } + + private BrokerMemberGroup buildBrokerMemberGroup(final BrokerReplicaInfo brokerReplicaInfo) { + if (brokerReplicaInfo != null) { + final BrokerMemberGroup group = new BrokerMemberGroup(brokerReplicaInfo.getClusterName(), brokerReplicaInfo.getBrokerName()); + final Map brokerIdTable = brokerReplicaInfo.getBrokerIdTable(); + final Map memberGroup = new HashMap<>(); + brokerIdTable.forEach((id, addr) -> memberGroup.put(id, addr)); + group.setBrokerAddrs(memberGroup); + return group; + } + return null; + } + + public ControllerResult getNextBrokerId(final GetNextBrokerIdRequestHeader request) { + final String clusterName = request.getClusterName(); + final String brokerName = request.getBrokerName(); + BrokerReplicaInfo brokerReplicaInfo = this.replicaInfoTable.get(brokerName); + final ControllerResult result = new ControllerResult<>(new GetNextBrokerIdResponseHeader(clusterName, brokerName)); + final GetNextBrokerIdResponseHeader response = result.getResponse(); + if (brokerReplicaInfo == null) { + // means that none of brokers in this broker-set are registered + response.setNextBrokerId(MixAll.FIRST_BROKER_CONTROLLER_ID); + } else { + response.setNextBrokerId(brokerReplicaInfo.getNextAssignBrokerId()); + } + return result; + } + + public ControllerResult applyBrokerId(final ApplyBrokerIdRequestHeader request) { + final String clusterName = request.getClusterName(); + final String brokerName = request.getBrokerName(); + final Long brokerId = request.getAppliedBrokerId(); + final String registerCheckCode = request.getRegisterCheckCode(); + final String brokerAddress = registerCheckCode.split(";")[0]; + BrokerReplicaInfo brokerReplicaInfo = this.replicaInfoTable.get(brokerName); + final ControllerResult result = new ControllerResult<>(new ApplyBrokerIdResponseHeader(clusterName, brokerName)); + final ApplyBrokerIdEvent event = new ApplyBrokerIdEvent(clusterName, brokerName, brokerAddress, brokerId, registerCheckCode); + // broker-set unregistered + if (brokerReplicaInfo == null) { + // first brokerId + if (brokerId == MixAll.FIRST_BROKER_CONTROLLER_ID) { + result.addEvent(event); + } else { + result.setCodeAndRemark(ResponseCode.CONTROLLER_BROKER_ID_INVALID, String.format("Broker-set: %s hasn't been registered in controller, but broker try to apply brokerId: %d", brokerName, brokerId)); + } + return result; + } + // broker-set registered + if (!brokerReplicaInfo.isBrokerExist(brokerId) || registerCheckCode.equals(brokerReplicaInfo.getBrokerRegisterCheckCode(brokerId))) { + // if brokerId hasn't been assigned or brokerId was assigned to this broker + result.addEvent(event); + return result; + } + result.setCodeAndRemark(ResponseCode.CONTROLLER_BROKER_ID_INVALID, String.format("Fail to apply brokerId: %d in broker-set: %s", brokerId, brokerName)); + return result; + } + + public ControllerResult registerBroker( + final RegisterBrokerToControllerRequestHeader request, final BrokerValidPredicate alivePredicate) { + final String brokerAddress = request.getBrokerAddress(); + final String brokerName = request.getBrokerName(); + final String clusterName = request.getClusterName(); + final Long brokerId = request.getBrokerId(); + final ControllerResult result = new ControllerResult<>(new RegisterBrokerToControllerResponseHeader(clusterName, brokerName)); + final RegisterBrokerToControllerResponseHeader response = result.getResponse(); + if (!isContainsBroker(brokerName)) { + result.setCodeAndRemark(ResponseCode.CONTROLLER_BROKER_NEED_TO_BE_REGISTERED, String.format("Broker-set: %s hasn't been registered in controller", brokerName)); + return result; + } + final BrokerReplicaInfo brokerReplicaInfo = this.replicaInfoTable.get(brokerName); + final SyncStateInfo syncStateInfo = this.syncStateSetInfoTable.get(brokerName); + if (!brokerReplicaInfo.isBrokerExist(brokerId)) { + result.setCodeAndRemark(ResponseCode.CONTROLLER_BROKER_NEED_TO_BE_REGISTERED, String.format("BrokerId: %d hasn't been registered in broker-set: %s", brokerId, brokerName)); + return result; + } + if (syncStateInfo.isMasterExist() && alivePredicate.check(clusterName, brokerName, syncStateInfo.getMasterBrokerId())) { + // if master still exist + response.setMasterBrokerId(syncStateInfo.getMasterBrokerId()); + response.setMasterAddress(brokerReplicaInfo.getBrokerAddress(response.getMasterBrokerId())); + response.setMasterEpoch(syncStateInfo.getMasterEpoch()); + response.setSyncStateSetEpoch(syncStateInfo.getSyncStateSetEpoch()); + } + result.setBody(new SyncStateSet(syncStateInfo.getSyncStateSet(), syncStateInfo.getSyncStateSetEpoch()).encode()); + // if this broker's address has been changed, we need to update it + if (!brokerAddress.equals(brokerReplicaInfo.getBrokerAddress(brokerId))) { + final UpdateBrokerAddressEvent event = new UpdateBrokerAddressEvent(clusterName, brokerName, brokerAddress, brokerId); + result.addEvent(event); + } + return result; + } + + public ControllerResult getReplicaInfo(final GetReplicaInfoRequestHeader request) { + final String brokerName = request.getBrokerName(); + final ControllerResult result = new ControllerResult<>(new GetReplicaInfoResponseHeader()); + final GetReplicaInfoResponseHeader response = result.getResponse(); + if (isContainsBroker(brokerName)) { + // If exist broker metadata, just return metadata + final SyncStateInfo syncStateInfo = this.syncStateSetInfoTable.get(brokerName); + final BrokerReplicaInfo brokerReplicaInfo = this.replicaInfoTable.get(brokerName); + final Long masterBrokerId = syncStateInfo.getMasterBrokerId(); + response.setMasterBrokerId(masterBrokerId); + response.setMasterAddress(brokerReplicaInfo.getBrokerAddress(masterBrokerId)); + response.setMasterEpoch(syncStateInfo.getMasterEpoch()); + result.setBody(new SyncStateSet(syncStateInfo.getSyncStateSet(), syncStateInfo.getSyncStateSetEpoch()).encode()); + return result; + } + result.setCodeAndRemark(ResponseCode.CONTROLLER_BROKER_METADATA_NOT_EXIST, "Broker metadata is not existed"); + return result; + } + + public ControllerResult getSyncStateData(final List brokerNames, + final BrokerValidPredicate brokerAlivePredicate) { + final ControllerResult result = new ControllerResult<>(); + final BrokerReplicasInfo brokerReplicasInfo = new BrokerReplicasInfo(); + for (String brokerName : brokerNames) { + if (isContainsBroker(brokerName)) { + // If exist broker metadata, just return metadata + final SyncStateInfo syncStateInfo = this.syncStateSetInfoTable.get(brokerName); + final BrokerReplicaInfo brokerReplicaInfo = this.replicaInfoTable.get(brokerName); + final Set syncStateSet = syncStateInfo.getSyncStateSet(); + final Long masterBrokerId = syncStateInfo.getMasterBrokerId(); + final ArrayList inSyncReplicas = new ArrayList<>(); + final ArrayList notInSyncReplicas = new ArrayList<>(); + + if (brokerReplicaInfo == null) { + continue; + } + + brokerReplicaInfo.getBrokerIdTable().forEach((brokerId, brokerAddress) -> { + Boolean isAlive = brokerAlivePredicate.check(brokerReplicaInfo.getClusterName(), brokerName, brokerId); + BrokerReplicasInfo.ReplicaIdentity replica = new BrokerReplicasInfo.ReplicaIdentity(brokerName, brokerId, brokerAddress); + replica.setAlive(isAlive); + if (syncStateSet.contains(brokerId)) { + inSyncReplicas.add(replica); + } else { + notInSyncReplicas.add(replica); + } + }); + + final BrokerReplicasInfo.ReplicasInfo inSyncState = new BrokerReplicasInfo.ReplicasInfo(masterBrokerId, brokerReplicaInfo.getBrokerAddress(masterBrokerId), syncStateInfo.getMasterEpoch(), syncStateInfo.getSyncStateSetEpoch(), + inSyncReplicas, notInSyncReplicas); + brokerReplicasInfo.addReplicaInfo(brokerName, inSyncState); + } + } + result.setBody(brokerReplicasInfo.encode()); + return result; + } + + public ControllerResult cleanBrokerData(final CleanControllerBrokerDataRequestHeader requestHeader, + final BrokerValidPredicate validPredicate) { + final ControllerResult result = new ControllerResult<>(); + + final String clusterName = requestHeader.getClusterName(); + final String brokerName = requestHeader.getBrokerName(); + final String brokerControllerIdsToClean = requestHeader.getBrokerControllerIdsToClean(); + + Set brokerIdSet = null; + if (!requestHeader.isCleanLivingBroker()) { + //if SyncStateInfo.masterAddress is not empty, at least one broker with the same BrokerName is alive + SyncStateInfo syncStateInfo = this.syncStateSetInfoTable.get(brokerName); + if (StringUtils.isBlank(brokerControllerIdsToClean) && null != syncStateInfo && syncStateInfo.getMasterBrokerId() != null) { + String remark = String.format("Broker %s is still alive, clean up failure", requestHeader.getBrokerName()); + result.setCodeAndRemark(ResponseCode.CONTROLLER_INVALID_CLEAN_BROKER_METADATA, remark); + return result; + } + if (StringUtils.isNotBlank(brokerControllerIdsToClean)) { + try { + brokerIdSet = Stream.of(brokerControllerIdsToClean.split(";")).map(idStr -> Long.valueOf(idStr)).collect(Collectors.toSet()); + } catch (NumberFormatException numberFormatException) { + String remark = String.format("Please set the option according to the format, exception: %s", numberFormatException); + result.setCodeAndRemark(ResponseCode.CONTROLLER_INVALID_CLEAN_BROKER_METADATA, remark); + return result; + } + for (Long brokerId : brokerIdSet) { + if (validPredicate.check(clusterName, brokerName, brokerId)) { + String remark = String.format("Broker [%s, %s] is still alive, clean up failure", requestHeader.getBrokerName(), brokerId); + result.setCodeAndRemark(ResponseCode.CONTROLLER_INVALID_CLEAN_BROKER_METADATA, remark); + return result; + } + } + } + } + if (isContainsBroker(brokerName)) { + final CleanBrokerDataEvent event = new CleanBrokerDataEvent(brokerName, brokerIdSet); + result.addEvent(event); + return result; + } + result.setCodeAndRemark(ResponseCode.CONTROLLER_INVALID_CLEAN_BROKER_METADATA, String.format("Broker %s is not existed,clean broker data failure.", brokerName)); + return result; + } + + public List scanNeedReelectBrokerSets(final BrokerValidPredicate validPredicate) { + List needReelectBrokerSets = new LinkedList<>(); + this.syncStateSetInfoTable.forEach((brokerName, syncStateInfo) -> { + Long masterBrokerId = syncStateInfo.getMasterBrokerId(); + String clusterName = syncStateInfo.getClusterName(); + // Now master is inactive + if (masterBrokerId != null && !validPredicate.check(clusterName, brokerName, masterBrokerId)) { + // Still at least one broker alive + Set brokerIds = this.replicaInfoTable.get(brokerName).getBrokerIdTable().keySet(); + boolean alive = brokerIds.stream().anyMatch(id -> validPredicate.check(clusterName, brokerName, id)); + if (alive) { + needReelectBrokerSets.add(brokerName); + } + } + }); + return needReelectBrokerSets; + } + + /** + * Apply events to memory statemachine. + * + * @param event event message + */ + public void applyEvent(final EventMessage event) { + final EventType type = event.getEventType(); + switch (type) { + case ALTER_SYNC_STATE_SET_EVENT: + handleAlterSyncStateSet((AlterSyncStateSetEvent) event); + break; + case APPLY_BROKER_ID_EVENT: + handleApplyBrokerId((ApplyBrokerIdEvent) event); + break; + case ELECT_MASTER_EVENT: + handleElectMaster((ElectMasterEvent) event); + break; + case CLEAN_BROKER_DATA_EVENT: + handleCleanBrokerDataEvent((CleanBrokerDataEvent) event); + break; + case UPDATE_BROKER_ADDRESS: + handleUpdateBrokerAddress((UpdateBrokerAddressEvent) event); + break; + default: + break; + } + } + + private void handleAlterSyncStateSet(final AlterSyncStateSetEvent event) { + final String brokerName = event.getBrokerName(); + if (isContainsBroker(brokerName)) { + final SyncStateInfo syncStateInfo = this.syncStateSetInfoTable.get(brokerName); + syncStateInfo.updateSyncStateSetInfo(event.getNewSyncStateSet()); + } + } + + private void handleApplyBrokerId(final ApplyBrokerIdEvent event) { + final String brokerName = event.getBrokerName(); + if (isContainsBroker(brokerName)) { + final BrokerReplicaInfo brokerReplicaInfo = this.replicaInfoTable.get(brokerName); + if (!brokerReplicaInfo.isBrokerExist(event.getNewBrokerId())) { + brokerReplicaInfo.addBroker(event.getNewBrokerId(), event.getBrokerAddress(), event.getRegisterCheckCode()); + } + } else { + // First time to register in this broker set + // Initialize the replicaInfo about this broker set + final String clusterName = event.getClusterName(); + final BrokerReplicaInfo brokerReplicaInfo = new BrokerReplicaInfo(clusterName, brokerName); + brokerReplicaInfo.addBroker(event.getNewBrokerId(), event.getBrokerAddress(), event.getRegisterCheckCode()); + this.replicaInfoTable.put(brokerName, brokerReplicaInfo); + final SyncStateInfo syncStateInfo = new SyncStateInfo(clusterName, brokerName); + // Initialize an empty syncStateInfo for this broker set + this.syncStateSetInfoTable.put(brokerName, syncStateInfo); + } + } + + private void handleUpdateBrokerAddress(final UpdateBrokerAddressEvent event) { + final String brokerName = event.getBrokerName(); + final String brokerAddress = event.getBrokerAddress(); + final Long brokerId = event.getBrokerId(); + BrokerReplicaInfo brokerReplicaInfo = this.replicaInfoTable.get(brokerName); + brokerReplicaInfo.updateBrokerAddress(brokerId, brokerAddress); + } + + private void handleElectMaster(final ElectMasterEvent event) { + final String brokerName = event.getBrokerName(); + final Long newMaster = event.getNewMasterBrokerId(); + if (isContainsBroker(brokerName)) { + final SyncStateInfo syncStateInfo = this.syncStateSetInfoTable.get(brokerName); + + if (event.getNewMasterElected()) { + // Record new master + syncStateInfo.updateMasterInfo(newMaster); + + // Record new newSyncStateSet list + final HashSet newSyncStateSet = new HashSet<>(); + newSyncStateSet.add(newMaster); + syncStateInfo.updateSyncStateSetInfo(newSyncStateSet); + } else { + // If new master was not elected, which means old master was shutdown and the newSyncStateSet list had no more replicas + // So we should delete old master, but retain newSyncStateSet list. + syncStateInfo.updateMasterInfo(null); + } + return; + } + LOGGER.error("Receive an ElectMasterEvent which contains the un-registered broker, event = {}", event); + } + + private void handleCleanBrokerDataEvent(final CleanBrokerDataEvent event) { + + final String brokerName = event.getBrokerName(); + final Set brokerIdSetToClean = event.getBrokerIdSetToClean(); + + if (null == brokerIdSetToClean || brokerIdSetToClean.isEmpty()) { + this.replicaInfoTable.remove(brokerName); + this.syncStateSetInfoTable.remove(brokerName); + return; + } + if (!isContainsBroker(brokerName)) { + return; + } + final BrokerReplicaInfo brokerReplicaInfo = this.replicaInfoTable.get(brokerName); + final SyncStateInfo syncStateInfo = this.syncStateSetInfoTable.get(brokerName); + for (Long brokerId : brokerIdSetToClean) { + brokerReplicaInfo.removeBrokerId(brokerId); + syncStateInfo.removeFromSyncState(brokerId); + } + if (brokerReplicaInfo.getBrokerIdTable().isEmpty()) { + this.replicaInfoTable.remove(brokerName); + } + if (syncStateInfo.getSyncStateSet().isEmpty()) { + this.syncStateSetInfoTable.remove(brokerName); + } + } + + /** + * Is the broker existed in the memory metadata + * + * @return true if both existed in replicaInfoTable and inSyncReplicasInfoTable + */ + private boolean isContainsBroker(final String brokerName) { + return this.replicaInfoTable.containsKey(brokerName) && this.syncStateSetInfoTable.containsKey(brokerName); + } + + protected void putInt(ByteArrayOutputStream outputStream, int value) { + outputStream.write((byte) (value >>> 24)); + outputStream.write((byte) (value >>> 16)); + outputStream.write((byte) (value >>> 8)); + outputStream.write((byte) value); + } + + protected int getInt(byte[] memory, int index) { + return memory[index] << 24 | (memory[index + 1] & 0xFF) << 16 | (memory[index + 2] & 0xFF) << 8 | memory[index + 3] & 0xFF; + } + + public byte[] serialize() throws Throwable { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + putInt(outputStream, this.replicaInfoTable.size()); + for (Map.Entry entry : replicaInfoTable.entrySet()) { + final byte[] brokerName = entry.getKey().getBytes(StandardCharsets.UTF_8); + byte[] brokerReplicaInfo = hessianSerialize(entry.getValue()); + putInt(outputStream, brokerName.length); + outputStream.write(brokerName); + putInt(outputStream, brokerReplicaInfo.length); + outputStream.write(brokerReplicaInfo); + } + putInt(outputStream, this.syncStateSetInfoTable.size()); + for (Map.Entry entry : syncStateSetInfoTable.entrySet()) { + final byte[] brokerName = entry.getKey().getBytes(StandardCharsets.UTF_8); + byte[] syncStateInfo = hessianSerialize(entry.getValue()); + putInt(outputStream, brokerName.length); + outputStream.write(brokerName); + putInt(outputStream, syncStateInfo.length); + outputStream.write(syncStateInfo); + } + return outputStream.toByteArray(); + } catch (Throwable e) { + LOGGER.error("serialize replicaInfoTable or syncStateSetInfoTable error", e); + throw e; + } + } + + public void deserializeFrom(byte[] data) throws Throwable { + int index = 0; + this.replicaInfoTable.clear(); + this.syncStateSetInfoTable.clear(); + + try { + int replicaInfoTableSize = getInt(data, index); + index += 4; + for (int i = 0; i < replicaInfoTableSize; i++) { + int brokerNameLength = getInt(data, index); + index += 4; + String brokerName = new String(data, index, brokerNameLength, StandardCharsets.UTF_8); + index += brokerNameLength; + int brokerReplicaInfoLength = getInt(data, index); + index += 4; + byte[] brokerReplicaInfoArray = new byte[brokerReplicaInfoLength]; + System.arraycopy(data, index, brokerReplicaInfoArray, 0, brokerReplicaInfoLength); + BrokerReplicaInfo brokerReplicaInfo = (BrokerReplicaInfo) hessianDeserialize(brokerReplicaInfoArray); + index += brokerReplicaInfoLength; + this.replicaInfoTable.put(brokerName, brokerReplicaInfo); + } + int syncStateSetInfoTableSize = getInt(data, index); + index += 4; + for (int i = 0; i < syncStateSetInfoTableSize; i++) { + int brokerNameLength = getInt(data, index); + index += 4; + String brokerName = new String(data, index, brokerNameLength, StandardCharsets.UTF_8); + index += brokerNameLength; + int syncStateInfoLength = getInt(data, index); + index += 4; + byte[] syncStateInfoArray = new byte[syncStateInfoLength]; + System.arraycopy(data, index, syncStateInfoArray, 0, syncStateInfoLength); + SyncStateInfo syncStateInfo = (SyncStateInfo) hessianDeserialize(syncStateInfoArray); + index += syncStateInfoLength; + this.syncStateSetInfoTable.put(brokerName, syncStateInfo); + } + } catch (Throwable e) { + LOGGER.error("deserialize replicaInfoTable or syncStateSetInfoTable error", e); + throw e; + } + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/manager/SyncStateInfo.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/manager/SyncStateInfo.java new file mode 100644 index 00000000000..0b2bc1385b0 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/manager/SyncStateInfo.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.manager; + +import java.io.Serializable; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Manages the syncStateSet of broker replicas. + */ +public class SyncStateInfo implements Serializable { + private final String clusterName; + private final String brokerName; + private final AtomicInteger masterEpoch; + private final AtomicInteger syncStateSetEpoch; + + private Set syncStateSet; + + private Long masterBrokerId; + + public SyncStateInfo(String clusterName, String brokerName) { + this.clusterName = clusterName; + this.brokerName = brokerName; + this.masterEpoch = new AtomicInteger(0); + this.syncStateSetEpoch = new AtomicInteger(0); + this.syncStateSet = Collections.emptySet(); + } + + public void updateMasterInfo(Long masterBrokerId) { + this.masterBrokerId = masterBrokerId; + this.masterEpoch.incrementAndGet(); + } + + public void updateSyncStateSetInfo(Set newSyncStateSet) { + this.syncStateSet = new HashSet<>(newSyncStateSet); + this.syncStateSetEpoch.incrementAndGet(); + } + + public boolean isFirstTimeForElect() { + return this.masterEpoch.get() == 0; + } + + public boolean isMasterExist() { + return masterBrokerId != null; + } + + public String getClusterName() { + return clusterName; + } + + public String getBrokerName() { + return brokerName; + } + + public Set getSyncStateSet() { + return new HashSet<>(syncStateSet); + } + + public int getSyncStateSetEpoch() { + return syncStateSetEpoch.get(); + } + + public Long getMasterBrokerId() { + return masterBrokerId; + } + + public int getMasterEpoch() { + return masterEpoch.get(); + } + + public void removeFromSyncState(final Long brokerId) { + syncStateSet.remove(brokerId); + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/task/BrokerCloseChannelRequest.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/task/BrokerCloseChannelRequest.java new file mode 100644 index 00000000000..9f8e0d7363b --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/task/BrokerCloseChannelRequest.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.task; + +import org.apache.rocketmq.controller.impl.heartbeat.BrokerIdentityInfo; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNullable; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class BrokerCloseChannelRequest implements CommandCustomHeader { + @CFNullable + private String clusterName; + + @CFNullable + private String brokerName; + + @CFNullable + private Long brokerId; + + public BrokerCloseChannelRequest() { + this.clusterName = null; + this.brokerName = null; + this.brokerId = null; + } + + public BrokerCloseChannelRequest(BrokerIdentityInfo brokerIdentityInfo) { + this.clusterName = brokerIdentityInfo.getClusterName(); + this.brokerName = brokerIdentityInfo.getBrokerName(); + this.brokerId = brokerIdentityInfo.getBrokerId(); + } + + public BrokerIdentityInfo getBrokerIdentityInfo() { + return new BrokerIdentityInfo(this.clusterName, this.brokerName, this.brokerId); + } + + public void setBrokerIdentityInfo(BrokerIdentityInfo brokerIdentityInfo) { + this.clusterName = brokerIdentityInfo.getClusterName(); + this.brokerName = brokerIdentityInfo.getBrokerName(); + this.brokerId = brokerIdentityInfo.getBrokerId(); + } + + @Override + public void checkFields() throws RemotingCommandException { + + } + + @Override + public String toString() { + return "BrokerCloseChannelRequest{" + + "clusterName='" + clusterName + '\'' + + ", brokerName='" + brokerName + '\'' + + ", brokerId=" + brokerId + + '}'; + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/task/BrokerCloseChannelResponse.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/task/BrokerCloseChannelResponse.java new file mode 100644 index 00000000000..93462afffc2 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/task/BrokerCloseChannelResponse.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.task; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class BrokerCloseChannelResponse implements CommandCustomHeader { + public BrokerCloseChannelResponse() { + } + + @Override + public void checkFields() throws RemotingCommandException { + + } + + @Override + public String toString() { + return "BrokerCloseChannelResponse{}"; + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/task/CheckNotActiveBrokerRequest.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/task/CheckNotActiveBrokerRequest.java new file mode 100644 index 00000000000..489b51bd23c --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/task/CheckNotActiveBrokerRequest.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.task; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class CheckNotActiveBrokerRequest implements CommandCustomHeader { + private final Long checkTimeMillis = System.currentTimeMillis(); + + public CheckNotActiveBrokerRequest() { + } + + public Long getCheckTimeMillis() { + return checkTimeMillis; + } + + @Override + public void checkFields() throws RemotingCommandException { + + } + + @Override + public String toString() { + return "CheckNotActiveBrokerRequest{" + + "checkTimeMillis=" + checkTimeMillis + + '}'; + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/task/CheckNotActiveBrokerResponse.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/task/CheckNotActiveBrokerResponse.java new file mode 100644 index 00000000000..2424ee6d1ed --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/task/CheckNotActiveBrokerResponse.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.task; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class CheckNotActiveBrokerResponse implements CommandCustomHeader { + public CheckNotActiveBrokerResponse() { + } + + @Override + public void checkFields() throws RemotingCommandException { + + } + + @Override + public String toString() { + return "CheckNotActiveBrokerResponse{}"; + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/task/GetBrokerLiveInfoRequest.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/task/GetBrokerLiveInfoRequest.java new file mode 100644 index 00000000000..1798a411432 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/task/GetBrokerLiveInfoRequest.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.task; + +import org.apache.rocketmq.controller.impl.heartbeat.BrokerIdentityInfo; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class GetBrokerLiveInfoRequest implements CommandCustomHeader { + private String clusterName; + + private String brokerName; + + private Long brokerId; + + public GetBrokerLiveInfoRequest() { + this.clusterName = null; + this.brokerName = null; + this.brokerId = null; + } + + /** + * @param brokerIdentity The BrokerIdentityInfo that needs to be queried, if it is null, it means obtaining BrokerLiveInfo for all brokers + */ + public GetBrokerLiveInfoRequest(BrokerIdentityInfo brokerIdentity) { + this.clusterName = brokerIdentity.getClusterName(); + this.brokerName = brokerIdentity.getBrokerName(); + this.brokerId = brokerIdentity.getBrokerId(); + } + + public BrokerIdentityInfo getBrokerIdentity() { + return new BrokerIdentityInfo(this.clusterName, this.brokerName, this.brokerId); + } + + public void setBrokerIdentity(BrokerIdentityInfo brokerIdentity) { + this.clusterName = brokerIdentity.getClusterName(); + this.brokerName = brokerIdentity.getBrokerName(); + this.brokerId = brokerIdentity.getBrokerId(); + } + + @Override + public void checkFields() throws RemotingCommandException { + + } + + @Override + public String toString() { + return "GetBrokerLiveInfoRequest{" + + "brokerIdentity=" + getBrokerIdentity() + + '}'; + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/task/GetBrokerLiveInfoResponse.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/task/GetBrokerLiveInfoResponse.java new file mode 100644 index 00000000000..79a3c9cbe05 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/task/GetBrokerLiveInfoResponse.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.task; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class GetBrokerLiveInfoResponse implements CommandCustomHeader { + public GetBrokerLiveInfoResponse() { + } + + @Override + public void checkFields() throws RemotingCommandException { + + } + + @Override + public String toString() { + return "GetBrokerLiveInfoResponse{}"; + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/task/GetSyncStateDataRequest.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/task/GetSyncStateDataRequest.java new file mode 100644 index 00000000000..56d22420f3c --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/task/GetSyncStateDataRequest.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.task; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class GetSyncStateDataRequest implements CommandCustomHeader { + private final Long invokeTime = System.currentTimeMillis(); + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public GetSyncStateDataRequest() { + + } + + public Long getInvokeTime() { + return invokeTime; + } + + @Override + public String toString() { + return "GetSyncStateDataRequest{" + + "invokeTime=" + invokeTime + + '}'; + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/task/RaftBrokerHeartBeatEventRequest.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/task/RaftBrokerHeartBeatEventRequest.java new file mode 100644 index 00000000000..02c34963ec8 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/task/RaftBrokerHeartBeatEventRequest.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.task; + +import org.apache.rocketmq.controller.impl.heartbeat.BrokerIdentityInfo; +import org.apache.rocketmq.controller.impl.heartbeat.BrokerLiveInfo; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class RaftBrokerHeartBeatEventRequest implements CommandCustomHeader { + // brokerIdentityInfo + private String clusterNameIdentityInfo; + + private String brokerNameIdentityInfo; + + private Long brokerIdIdentityInfo; + + // brokerLiveInfo + private String brokerName; + private String brokerAddr; + private Long heartbeatTimeoutMillis; + private Long brokerId; + private Long lastUpdateTimestamp; + private Integer epoch; + private Long maxOffset; + private Long confirmOffset; + private Integer electionPriority; + + public RaftBrokerHeartBeatEventRequest() { + } + + public RaftBrokerHeartBeatEventRequest(BrokerIdentityInfo brokerIdentityInfo, BrokerLiveInfo brokerLiveInfo) { + this.clusterNameIdentityInfo = brokerIdentityInfo.getClusterName(); + this.brokerNameIdentityInfo = brokerIdentityInfo.getBrokerName(); + this.brokerIdIdentityInfo = brokerIdentityInfo.getBrokerId(); + + this.brokerName = brokerLiveInfo.getBrokerName(); + this.brokerAddr = brokerLiveInfo.getBrokerAddr(); + this.heartbeatTimeoutMillis = brokerLiveInfo.getHeartbeatTimeoutMillis(); + this.brokerId = brokerLiveInfo.getBrokerId(); + this.lastUpdateTimestamp = brokerLiveInfo.getLastUpdateTimestamp(); + this.epoch = brokerLiveInfo.getEpoch(); + this.maxOffset = brokerLiveInfo.getMaxOffset(); + this.confirmOffset = brokerLiveInfo.getConfirmOffset(); + this.electionPriority = brokerLiveInfo.getElectionPriority(); + } + + public BrokerIdentityInfo getBrokerIdentityInfo() { + return new BrokerIdentityInfo(clusterNameIdentityInfo, brokerNameIdentityInfo, brokerIdIdentityInfo); + } + + public void setBrokerIdentityInfo(BrokerIdentityInfo brokerIdentityInfo) { + this.clusterNameIdentityInfo = brokerIdentityInfo.getClusterName(); + this.brokerNameIdentityInfo = brokerIdentityInfo.getBrokerName(); + this.brokerIdIdentityInfo = brokerIdentityInfo.getBrokerId(); + } + + public BrokerLiveInfo getBrokerLiveInfo() { + return new BrokerLiveInfo(brokerName, brokerAddr, brokerId, lastUpdateTimestamp, heartbeatTimeoutMillis, null, epoch, maxOffset, electionPriority, confirmOffset); + } + + public void setBrokerLiveInfo(BrokerLiveInfo brokerLiveInfo) { + this.brokerName = brokerLiveInfo.getBrokerName(); + this.brokerAddr = brokerLiveInfo.getBrokerAddr(); + this.heartbeatTimeoutMillis = brokerLiveInfo.getHeartbeatTimeoutMillis(); + this.brokerId = brokerLiveInfo.getBrokerId(); + this.lastUpdateTimestamp = brokerLiveInfo.getLastUpdateTimestamp(); + this.epoch = brokerLiveInfo.getEpoch(); + this.maxOffset = brokerLiveInfo.getMaxOffset(); + this.confirmOffset = brokerLiveInfo.getConfirmOffset(); + this.electionPriority = brokerLiveInfo.getElectionPriority(); + } + + @Override + public void checkFields() throws RemotingCommandException { + + } + + @Override + public String toString() { + return "RaftBrokerHeartBeatEventRequest{" + + "brokerIdentityInfo=" + getBrokerIdentityInfo() + + ", brokerLiveInfo=" + getBrokerLiveInfo() + + "}"; + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/impl/task/RaftBrokerHeartBeatEventResponse.java b/controller/src/main/java/org/apache/rocketmq/controller/impl/task/RaftBrokerHeartBeatEventResponse.java new file mode 100644 index 00000000000..6eb87500632 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/impl/task/RaftBrokerHeartBeatEventResponse.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.task; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class RaftBrokerHeartBeatEventResponse implements CommandCustomHeader { + public RaftBrokerHeartBeatEventResponse() { + } + + @Override + public void checkFields() throws RemotingCommandException { + + } + + @Override + public String toString() { + return "RaftBrokerHeartBeatEventResponse{}"; + } +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/metrics/ControllerMetricsConstant.java b/controller/src/main/java/org/apache/rocketmq/controller/metrics/ControllerMetricsConstant.java new file mode 100644 index 00000000000..45b4006a961 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/metrics/ControllerMetricsConstant.java @@ -0,0 +1,148 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.controller.metrics; + +import org.apache.rocketmq.remoting.protocol.RequestCode; + +public class ControllerMetricsConstant { + + public static final String LABEL_ADDRESS = "address"; + public static final String LABEL_GROUP = "group"; + public static final String LABEL_PEER_ID = "peer_id"; + public static final String LABEL_AGGREGATION = "aggregation"; + public static final String AGGREGATION_DELTA = "delta"; + + public static final String OPEN_TELEMETRY_METER_NAME = "controller"; + + public static final String GAUGE_ROLE = "role"; + + // unit: B + public static final String GAUGE_DLEDGER_DISK_USAGE = "dledger_disk_usage"; + + public static final String GAUGE_ACTIVE_BROKER_NUM = "active_broker_num"; + + public static final String COUNTER_REQUEST_TOTAL = "request_total"; + + public static final String COUNTER_DLEDGER_OP_TOTAL = "dledger_op_total"; + + public static final String COUNTER_ELECTION_TOTAL = "election_total"; + + // unit: us + public static final String HISTOGRAM_REQUEST_LATENCY = "request_latency"; + + // unit: us + public static final String HISTOGRAM_DLEDGER_OP_LATENCY = "dledger_op_latency"; + + public static final String LABEL_CLUSTER_NAME = "cluster"; + + public static final String LABEL_BROKER_SET = "broker_set"; + + public static final String LABEL_REQUEST_TYPE = "request_type"; + + public static final String LABEL_REQUEST_HANDLE_STATUS = "request_handle_status"; + + public static final String LABEL_DLEDGER_OPERATION = "dledger_operation"; + + public static final String LABEL_DLEDGER_OPERATION_STATUS = "dLedger_operation_status"; + + public static final String LABEL_ELECTION_RESULT = "election_result"; + + public enum RequestType { + CONTROLLER_ALTER_SYNC_STATE_SET(RequestCode.CONTROLLER_ALTER_SYNC_STATE_SET), + + CONTROLLER_ELECT_MASTER(RequestCode.CONTROLLER_ELECT_MASTER), + + CONTROLLER_REGISTER_BROKER(RequestCode.CONTROLLER_REGISTER_BROKER), + + CONTROLLER_GET_REPLICA_INFO(RequestCode.CONTROLLER_GET_REPLICA_INFO), + + CONTROLLER_GET_METADATA_INFO(RequestCode.CONTROLLER_GET_METADATA_INFO), + + CONTROLLER_GET_SYNC_STATE_DATA(RequestCode.CONTROLLER_GET_SYNC_STATE_DATA), + + CONTROLLER_GET_BROKER_EPOCH_CACHE(RequestCode.GET_BROKER_EPOCH_CACHE), + + CONTROLLER_NOTIFY_BROKER_ROLE_CHANGED(RequestCode.NOTIFY_BROKER_ROLE_CHANGED), + + CONTROLLER_BROKER_HEARTBEAT(RequestCode.BROKER_HEARTBEAT), + + CONTROLLER_UPDATE_CONTROLLER_CONFIG(RequestCode.UPDATE_CONTROLLER_CONFIG), + + CONTROLLER_GET_CONTROLLER_CONFIG(RequestCode.GET_CONTROLLER_CONFIG), + + CONTROLLER_CLEAN_BROKER_DATA(RequestCode.CLEAN_BROKER_DATA), + + CONTROLLER_GET_NEXT_BROKER_ID(RequestCode.CONTROLLER_GET_NEXT_BROKER_ID), + + CONTROLLER_APPLY_BROKER_ID(RequestCode.CONTROLLER_APPLY_BROKER_ID); + + private final int code; + + RequestType(int code) { + this.code = code; + } + + public static String getLowerCaseNameByCode(int code) { + for (RequestType requestType : RequestType.values()) { + if (requestType.code == code) { + return requestType.name(); + } + } + return null; + } + } + + public enum RequestHandleStatus { + SUCCESS, + FAILED, + TIMEOUT; + + public String getLowerCaseName() { + return this.name().toLowerCase(); + } + } + + public enum DLedgerOperation { + APPEND; + + public String getLowerCaseName() { + return this.name().toLowerCase(); + } + } + + public enum DLedgerOperationStatus { + SUCCESS, + FAILED, + TIMEOUT; + + public String getLowerCaseName() { + return this.name().toLowerCase(); + } + } + + public enum ElectionResult { + NEW_MASTER_ELECTED, + KEEP_CURRENT_MASTER, + NO_MASTER_ELECTED; + + public String getLowerCaseName() { + return this.name().toLowerCase(); + } + } + +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/metrics/ControllerMetricsManager.java b/controller/src/main/java/org/apache/rocketmq/controller/metrics/ControllerMetricsManager.java new file mode 100644 index 00000000000..02008199ea9 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/metrics/ControllerMetricsManager.java @@ -0,0 +1,391 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.controller.metrics; + +import com.google.common.base.Splitter; +import io.openmessaging.storage.dledger.MemberState; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.LongUpDownCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.ObservableLongGauge; +import io.opentelemetry.exporter.logging.otlp.OtlpJsonLoggingMetricExporter; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporterBuilder; +import io.opentelemetry.exporter.prometheus.PrometheusHttpServer; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.metrics.Aggregation; +import io.opentelemetry.sdk.metrics.InstrumentSelector; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; +import io.opentelemetry.sdk.metrics.View; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; +import io.opentelemetry.sdk.resources.Resource; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.ControllerConfig; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.metrics.MetricsExporterType; +import org.apache.rocketmq.common.metrics.NopLongCounter; +import org.apache.rocketmq.common.metrics.NopLongHistogram; +import org.apache.rocketmq.common.metrics.NopLongUpDownCounter; +import org.apache.rocketmq.common.metrics.NopObservableLongGauge; +import org.apache.rocketmq.controller.ControllerManager; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.slf4j.bridge.SLF4JBridgeHandler; + +import java.io.File; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.AGGREGATION_DELTA; +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.COUNTER_DLEDGER_OP_TOTAL; +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.COUNTER_ELECTION_TOTAL; +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.COUNTER_REQUEST_TOTAL; +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.GAUGE_ACTIVE_BROKER_NUM; +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.GAUGE_DLEDGER_DISK_USAGE; +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.GAUGE_ROLE; +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.HISTOGRAM_DLEDGER_OP_LATENCY; +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.HISTOGRAM_REQUEST_LATENCY; +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.LABEL_ADDRESS; +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.LABEL_AGGREGATION; +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.LABEL_BROKER_SET; +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.LABEL_CLUSTER_NAME; +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.LABEL_GROUP; +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.LABEL_PEER_ID; +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.OPEN_TELEMETRY_METER_NAME; + +public class ControllerMetricsManager { + + private static final Logger logger = LoggerFactory.getLogger(LoggerName.CONTROLLER_LOGGER_NAME); + + private static volatile ControllerMetricsManager instance; + + private static final Map LABEL_MAP = new HashMap<>(); + + // metrics about node status + public static LongUpDownCounter role = new NopLongUpDownCounter(); + + public static ObservableLongGauge dLedgerDiskUsage = new NopObservableLongGauge(); + + public static ObservableLongGauge activeBrokerNum = new NopObservableLongGauge(); + + public static LongCounter requestTotal = new NopLongCounter(); + + public static LongCounter dLedgerOpTotal = new NopLongCounter(); + + public static LongCounter electionTotal = new NopLongCounter(); + + // metrics about latency + public static LongHistogram requestLatency = new NopLongHistogram(); + + public static LongHistogram dLedgerOpLatency = new NopLongHistogram(); + + private static double us = 1d; + + private static double ms = 1000 * us; + + private static double s = 1000 * ms; + + private final ControllerManager controllerManager; + + private final ControllerConfig config; + + private Meter controllerMeter; + + private OtlpGrpcMetricExporter metricExporter; + + private PeriodicMetricReader periodicMetricReader; + + private PrometheusHttpServer prometheusHttpServer; + + private MetricExporter loggingMetricExporter; + + public static ControllerMetricsManager getInstance(ControllerManager controllerManager) { + if (instance == null) { + synchronized (ControllerMetricsManager.class) { + if (instance == null) { + instance = new ControllerMetricsManager(controllerManager); + } + } + } + return instance; + } + + public static AttributesBuilder newAttributesBuilder() { + AttributesBuilder builder = Attributes.builder(); + LABEL_MAP.forEach(builder::put); + return builder; + } + + public static void recordRole(MemberState.Role newRole, MemberState.Role oldRole) { + role.add(getRoleValue(newRole) - getRoleValue(oldRole), + newAttributesBuilder().build()); + } + + private static int getRoleValue(MemberState.Role role) { + switch (role) { + case UNKNOWN: + return 0; + case CANDIDATE: + return 1; + case FOLLOWER: + return 2; + case LEADER: + return 3; + default: + logger.error("Unknown role {}", role); + return 0; + } + } + + private ControllerMetricsManager(ControllerManager controllerManager) { + this.controllerManager = controllerManager; + this.config = this.controllerManager.getControllerConfig(); + if (config.getControllerType().equals(ControllerConfig.JRAFT_CONTROLLER)) { + this.LABEL_MAP.put(LABEL_ADDRESS, this.config.getJraftConfig().getjRaftAddress()); + this.LABEL_MAP.put(LABEL_GROUP, this.config.getJraftConfig().getjRaftGroupId()); + this.LABEL_MAP.put(LABEL_PEER_ID, this.config.getJraftConfig().getjRaftServerId()); + } else { + this.LABEL_MAP.put(LABEL_ADDRESS, this.config.getDLedgerAddress()); + this.LABEL_MAP.put(LABEL_GROUP, this.config.getControllerDLegerGroup()); + this.LABEL_MAP.put(LABEL_PEER_ID, this.config.getControllerDLegerSelfId()); + } + this.init(); + } + + private boolean checkConfig() { + if (config == null) { + return false; + } + MetricsExporterType exporterType = config.getMetricsExporterType(); + if (!exporterType.isEnable()) { + return false; + } + + switch (exporterType) { + case OTLP_GRPC: + return StringUtils.isNotBlank(config.getMetricsGrpcExporterTarget()); + case PROM: + return true; + case LOG: + return true; + } + return false; + } + + private void registerMetricsView(SdkMeterProviderBuilder providerBuilder) { + // define latency bucket + List latencyBuckets = Arrays.asList( + 1 * us, 3 * us, 5 * us, + 10 * us, 30 * us, 50 * us, + 100 * us, 300 * us, 500 * us, + 1 * ms, 3 * ms, 5 * ms, + 10 * ms, 30 * ms, 50 * ms, + 100 * ms, 300 * ms, 500 * ms, + 1 * s, 3 * s, 5 * s, + 10 * s + ); + + View latencyView = View.builder() + .setAggregation(Aggregation.explicitBucketHistogram(latencyBuckets)) + .build(); + + InstrumentSelector requestLatencySelector = InstrumentSelector.builder() + .setType(InstrumentType.HISTOGRAM) + .setName(HISTOGRAM_REQUEST_LATENCY) + .build(); + + InstrumentSelector dLedgerOpLatencySelector = InstrumentSelector.builder() + .setType(InstrumentType.HISTOGRAM) + .setName(HISTOGRAM_DLEDGER_OP_LATENCY) + .build(); + + providerBuilder.registerView(requestLatencySelector, latencyView); + providerBuilder.registerView(dLedgerOpLatencySelector, latencyView); + } + + private void initMetric(Meter meter) { + role = meter.upDownCounterBuilder(GAUGE_ROLE) + .setDescription("role of current node") + .build(); + + dLedgerDiskUsage = meter.gaugeBuilder(GAUGE_DLEDGER_DISK_USAGE) + .setDescription("disk usage of dledger") + .setUnit("bytes") + .ofLongs() + .buildWithCallback(measurement -> { + String path = config.getControllerStorePath(); + if (!UtilAll.isPathExists(path)) { + return; + } + File file = new File(path); + Long diskUsage = UtilAll.calculateFileSizeInPath(file); + if (diskUsage == -1) { + logger.error("calculateFileSizeInPath error, path: {}", path); + return; + } + measurement.record(diskUsage, newAttributesBuilder().build()); + }); + + activeBrokerNum = meter.gaugeBuilder(GAUGE_ACTIVE_BROKER_NUM) + .setDescription("now active brokers num") + .ofLongs() + .buildWithCallback(measurement -> { + Map> activeBrokersNum = controllerManager.getHeartbeatManager().getActiveBrokersNum(); + activeBrokersNum.forEach((cluster, brokerSetAndNum) -> { + brokerSetAndNum.forEach((brokerSet, num) -> measurement.record(num, + newAttributesBuilder().put(LABEL_CLUSTER_NAME, cluster).put(LABEL_BROKER_SET, brokerSet).build())); + }); + }); + + requestTotal = meter.counterBuilder(COUNTER_REQUEST_TOTAL) + .setDescription("total request num") + .build(); + + dLedgerOpTotal = meter.counterBuilder(COUNTER_DLEDGER_OP_TOTAL) + .setDescription("total dledger operation num") + .build(); + + electionTotal = meter.counterBuilder(COUNTER_ELECTION_TOTAL) + .setDescription("total elect num") + .build(); + + requestLatency = meter.histogramBuilder(HISTOGRAM_REQUEST_LATENCY) + .setDescription("request latency") + .setUnit("us") + .ofLongs() + .build(); + + dLedgerOpLatency = meter.histogramBuilder(HISTOGRAM_DLEDGER_OP_LATENCY) + .setDescription("dledger operation latency") + .setUnit("us") + .ofLongs() + .build(); + + } + + public void init() { + MetricsExporterType type = this.config.getMetricsExporterType(); + if (type == MetricsExporterType.DISABLE) { + return; + } + if (!checkConfig()) { + logger.error("check metric config failed, will not export metrics"); + return; + } + + String labels = config.getMetricsLabel(); + if (StringUtils.isNotBlank(labels)) { + List labelList = Splitter.on(',').omitEmptyStrings().splitToList(labels); + for (String label : labelList) { + String[] pair = label.split(":"); + if (pair.length != 2) { + logger.warn("metrics label is not valid: {}", label); + continue; + } + LABEL_MAP.put(pair[0], pair[1]); + } + } + if (config.isMetricsInDelta()) { + LABEL_MAP.put(LABEL_AGGREGATION, AGGREGATION_DELTA); + } + + SdkMeterProviderBuilder providerBuilder = SdkMeterProvider.builder().setResource(Resource.empty()); + + if (type == MetricsExporterType.OTLP_GRPC) { + String endpoint = config.getMetricsGrpcExporterTarget(); + if (!endpoint.startsWith("http")) { + endpoint = "https://" + endpoint; + } + OtlpGrpcMetricExporterBuilder metricExporterBuilder = OtlpGrpcMetricExporter.builder() + .setEndpoint(endpoint) + .setTimeout(config.getMetricGrpcExporterTimeOutInMills(), TimeUnit.MILLISECONDS) + .setAggregationTemporalitySelector(x -> { + if (config.isMetricsInDelta() && + (x == InstrumentType.COUNTER || x == InstrumentType.OBSERVABLE_COUNTER || x == InstrumentType.HISTOGRAM)) { + return AggregationTemporality.DELTA; + } + return AggregationTemporality.CUMULATIVE; + }); + + String headers = config.getMetricsGrpcExporterHeader(); + if (StringUtils.isNotBlank(headers)) { + Map headerMap = new HashMap<>(); + List headerList = Splitter.on(',').omitEmptyStrings().splitToList(headers); + for (String header : headerList) { + String[] pair = header.split(":"); + if (pair.length != 2) { + logger.warn("metricsGrpcExporterHeader is not valid: {}", headers); + continue; + } + headerMap.put(pair[0], pair[1]); + } + headerMap.forEach(metricExporterBuilder::addHeader); + } + + metricExporter = metricExporterBuilder.build(); + + periodicMetricReader = PeriodicMetricReader.builder(metricExporter) + .setInterval(config.getMetricGrpcExporterIntervalInMills(), TimeUnit.MILLISECONDS) + .build(); + + providerBuilder.registerMetricReader(periodicMetricReader); + } + + if (type == MetricsExporterType.PROM) { + String promExporterHost = config.getMetricsPromExporterHost(); + if (StringUtils.isBlank(promExporterHost)) { + promExporterHost = "0.0.0.0"; + } + prometheusHttpServer = PrometheusHttpServer.builder() + .setHost(promExporterHost) + .setPort(config.getMetricsPromExporterPort()) + .build(); + providerBuilder.registerMetricReader(prometheusHttpServer); + } + + if (type == MetricsExporterType.LOG) { + SLF4JBridgeHandler.removeHandlersForRootLogger(); + SLF4JBridgeHandler.install(); + loggingMetricExporter = OtlpJsonLoggingMetricExporter.create(config.isMetricsInDelta() ? AggregationTemporality.DELTA : AggregationTemporality.CUMULATIVE); + java.util.logging.Logger.getLogger(OtlpJsonLoggingMetricExporter.class.getName()).setLevel(java.util.logging.Level.FINEST); + periodicMetricReader = PeriodicMetricReader.builder(loggingMetricExporter) + .setInterval(config.getMetricLoggingExporterIntervalInMills(), TimeUnit.MILLISECONDS) + .build(); + providerBuilder.registerMetricReader(periodicMetricReader); + } + + registerMetricsView(providerBuilder); + + controllerMeter = OpenTelemetrySdk.builder().setMeterProvider(providerBuilder.build()) + .build().getMeter(OPEN_TELEMETRY_METER_NAME); + + initMetric(controllerMeter); + } + +} diff --git a/controller/src/main/java/org/apache/rocketmq/controller/processor/ControllerRequestProcessor.java b/controller/src/main/java/org/apache/rocketmq/controller/processor/ControllerRequestProcessor.java new file mode 100644 index 00000000000..2713cf3dea2 --- /dev/null +++ b/controller/src/main/java/org/apache/rocketmq/controller/processor/ControllerRequestProcessor.java @@ -0,0 +1,340 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.processor; + +import com.google.common.base.Stopwatch; +import io.netty.channel.ChannelHandlerContext; +import io.opentelemetry.api.common.Attributes; +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import java.util.concurrent.TimeoutException; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.controller.BrokerHeartbeatManager; +import org.apache.rocketmq.controller.ControllerManager; +import org.apache.rocketmq.controller.metrics.ControllerMetricsConstant; +import org.apache.rocketmq.controller.metrics.ControllerMetricsManager; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.RoleChangeNotifyEntry; +import org.apache.rocketmq.remoting.protocol.body.SyncStateSet; +import org.apache.rocketmq.remoting.protocol.header.controller.AlterSyncStateSetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetReplicaInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.admin.CleanControllerBrokerDataRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.ApplyBrokerIdRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.GetNextBrokerIdRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.RegisterBrokerToControllerRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.BrokerHeartbeatRequestHeader; + +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.LABEL_REQUEST_HANDLE_STATUS; +import static org.apache.rocketmq.controller.metrics.ControllerMetricsConstant.LABEL_REQUEST_TYPE; +import static org.apache.rocketmq.remoting.protocol.RequestCode.CONTROLLER_APPLY_BROKER_ID; +import static org.apache.rocketmq.remoting.protocol.RequestCode.BROKER_HEARTBEAT; +import static org.apache.rocketmq.remoting.protocol.RequestCode.CLEAN_BROKER_DATA; +import static org.apache.rocketmq.remoting.protocol.RequestCode.CONTROLLER_ALTER_SYNC_STATE_SET; +import static org.apache.rocketmq.remoting.protocol.RequestCode.CONTROLLER_ELECT_MASTER; +import static org.apache.rocketmq.remoting.protocol.RequestCode.CONTROLLER_GET_METADATA_INFO; +import static org.apache.rocketmq.remoting.protocol.RequestCode.CONTROLLER_GET_REPLICA_INFO; +import static org.apache.rocketmq.remoting.protocol.RequestCode.CONTROLLER_GET_SYNC_STATE_DATA; +import static org.apache.rocketmq.remoting.protocol.RequestCode.CONTROLLER_REGISTER_BROKER; +import static org.apache.rocketmq.remoting.protocol.RequestCode.GET_CONTROLLER_CONFIG; +import static org.apache.rocketmq.remoting.protocol.RequestCode.CONTROLLER_GET_NEXT_BROKER_ID; +import static org.apache.rocketmq.remoting.protocol.RequestCode.UPDATE_CONTROLLER_CONFIG; + +/** + * Processor for controller request + */ +public class ControllerRequestProcessor implements NettyRequestProcessor { + private static final Logger log = LoggerFactory.getLogger(LoggerName.CONTROLLER_LOGGER_NAME); + private static final int WAIT_TIMEOUT_OUT = 5; + private final ControllerManager controllerManager; + private final BrokerHeartbeatManager heartbeatManager; + protected Set configBlackList = new HashSet<>(); + + public ControllerRequestProcessor(final ControllerManager controllerManager) { + this.controllerManager = controllerManager; + this.heartbeatManager = controllerManager.getHeartbeatManager(); + initConfigBlackList(); + } + private void initConfigBlackList() { + configBlackList.add("configBlackList"); + configBlackList.add("configStorePath"); + configBlackList.add("rocketmqHome"); + String[] configArray = controllerManager.getControllerConfig().getConfigBlackList().split(";"); + configBlackList.addAll(Arrays.asList(configArray)); + } + @Override + public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) throws Exception { + if (ctx != null) { + log.debug("Receive request, {} {} {}", + request.getCode(), + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), + request); + } + Stopwatch stopwatch = Stopwatch.createStarted(); + try { + RemotingCommand resp = handleRequest(ctx, request); + Attributes attributes = ControllerMetricsManager.newAttributesBuilder() + .put(LABEL_REQUEST_TYPE, ControllerMetricsConstant.RequestType.getLowerCaseNameByCode(request.getCode())) + .put(LABEL_REQUEST_HANDLE_STATUS, ControllerMetricsConstant.RequestHandleStatus.SUCCESS.getLowerCaseName()) + .build(); + ControllerMetricsManager.requestTotal.add(1, attributes); + attributes = ControllerMetricsManager.newAttributesBuilder() + .put(LABEL_REQUEST_TYPE, ControllerMetricsConstant.RequestType.getLowerCaseNameByCode(request.getCode())) + .build(); + ControllerMetricsManager.requestLatency.record(stopwatch.elapsed(TimeUnit.MICROSECONDS), attributes); + return resp; + } catch (Exception e) { + log.error("process request: {} error, ", request, e); + Attributes attributes; + if (e instanceof TimeoutException) { + attributes = ControllerMetricsManager.newAttributesBuilder() + .put(LABEL_REQUEST_TYPE, ControllerMetricsConstant.RequestType.getLowerCaseNameByCode(request.getCode())) + .put(LABEL_REQUEST_HANDLE_STATUS, ControllerMetricsConstant.RequestHandleStatus.TIMEOUT.getLowerCaseName()) + .build(); + } else { + attributes = ControllerMetricsManager.newAttributesBuilder() + .put(LABEL_REQUEST_TYPE, ControllerMetricsConstant.RequestType.getLowerCaseNameByCode(request.getCode())) + .put(LABEL_REQUEST_HANDLE_STATUS, ControllerMetricsConstant.RequestHandleStatus.FAILED.getLowerCaseName()) + .build(); + } + ControllerMetricsManager.requestTotal.add(1, attributes); + throw e; + } + } + + private RemotingCommand handleRequest(ChannelHandlerContext ctx, RemotingCommand request) throws Exception { + switch (request.getCode()) { + case CONTROLLER_ALTER_SYNC_STATE_SET: + return this.handleAlterSyncStateSet(ctx, request); + case CONTROLLER_ELECT_MASTER: + return this.handleControllerElectMaster(ctx, request); + case CONTROLLER_GET_REPLICA_INFO: + return this.handleControllerGetReplicaInfo(ctx, request); + case CONTROLLER_GET_METADATA_INFO: + return this.handleControllerGetMetadataInfo(ctx, request); + case BROKER_HEARTBEAT: + return this.handleBrokerHeartbeat(ctx, request); + case CONTROLLER_GET_SYNC_STATE_DATA: + return this.handleControllerGetSyncStateData(ctx, request); + case UPDATE_CONTROLLER_CONFIG: + return this.handleUpdateControllerConfig(ctx, request); + case GET_CONTROLLER_CONFIG: + return this.handleGetControllerConfig(ctx, request); + case CLEAN_BROKER_DATA: + return this.handleCleanBrokerData(ctx, request); + case CONTROLLER_GET_NEXT_BROKER_ID: + return this.handleGetNextBrokerId(ctx, request); + case CONTROLLER_APPLY_BROKER_ID: + return this.handleApplyBrokerId(ctx, request); + case CONTROLLER_REGISTER_BROKER: + return this.handleRegisterBroker(ctx, request); + default: { + final String error = " request type " + request.getCode() + " not supported"; + return RemotingCommand.createResponseCommand(ResponseCode.REQUEST_CODE_NOT_SUPPORTED, error); + } + } + } + + private RemotingCommand handleAlterSyncStateSet(ChannelHandlerContext ctx, + RemotingCommand request) throws Exception { + final AlterSyncStateSetRequestHeader controllerRequest = (AlterSyncStateSetRequestHeader) request.decodeCommandCustomHeader(AlterSyncStateSetRequestHeader.class); + final SyncStateSet syncStateSet = RemotingSerializable.decode(request.getBody(), SyncStateSet.class); + final CompletableFuture future = this.controllerManager.getController().alterSyncStateSet(controllerRequest, syncStateSet); + if (future != null) { + return future.get(WAIT_TIMEOUT_OUT, TimeUnit.SECONDS); + } + return RemotingCommand.createResponseCommand(null); + } + + private RemotingCommand handleControllerElectMaster(ChannelHandlerContext ctx, + RemotingCommand request) throws Exception { + final ElectMasterRequestHeader electMasterRequest = (ElectMasterRequestHeader) request.decodeCommandCustomHeader(ElectMasterRequestHeader.class); + final CompletableFuture future = this.controllerManager.getController().electMaster(electMasterRequest); + if (future != null) { + final RemotingCommand response = future.get(WAIT_TIMEOUT_OUT, TimeUnit.SECONDS); + + if (response.getCode() == ResponseCode.SUCCESS) { + if (this.controllerManager.getControllerConfig().isNotifyBrokerRoleChanged()) { + this.controllerManager.notifyBrokerRoleChanged(RoleChangeNotifyEntry.convert(response)); + } + } + return response; + } + return RemotingCommand.createResponseCommand(null); + } + + private RemotingCommand handleControllerGetReplicaInfo(ChannelHandlerContext ctx, + RemotingCommand request) throws Exception { + final GetReplicaInfoRequestHeader controllerRequest = (GetReplicaInfoRequestHeader) request.decodeCommandCustomHeader(GetReplicaInfoRequestHeader.class); + final CompletableFuture future = this.controllerManager.getController().getReplicaInfo(controllerRequest); + if (future != null) { + return future.get(WAIT_TIMEOUT_OUT, TimeUnit.SECONDS); + } + return RemotingCommand.createResponseCommand(null); + } + + private RemotingCommand handleControllerGetMetadataInfo(ChannelHandlerContext ctx, RemotingCommand request) { + return this.controllerManager.getController().getControllerMetadata(); + } + + private RemotingCommand handleBrokerHeartbeat(ChannelHandlerContext ctx, RemotingCommand request) throws Exception { + final BrokerHeartbeatRequestHeader requestHeader = (BrokerHeartbeatRequestHeader) request.decodeCommandCustomHeader(BrokerHeartbeatRequestHeader.class); + if (requestHeader.getBrokerId() == null) { + return RemotingCommand.createResponseCommand(ResponseCode.CONTROLLER_INVALID_REQUEST, "Heart beat with empty brokerId"); + } + this.heartbeatManager.onBrokerHeartbeat(requestHeader.getClusterName(), requestHeader.getBrokerName(), requestHeader.getBrokerAddr(), requestHeader.getBrokerId(), + requestHeader.getHeartbeatTimeoutMills(), ctx.channel(), requestHeader.getEpoch(), requestHeader.getMaxOffset(), requestHeader.getConfirmOffset(), requestHeader.getElectionPriority()); + return RemotingCommand.createResponseCommand(ResponseCode.SUCCESS, "Heart beat success"); + } + + private RemotingCommand handleControllerGetSyncStateData(ChannelHandlerContext ctx, + RemotingCommand request) throws Exception { + if (request.getBody() != null) { + final List brokerNames = RemotingSerializable.decode(request.getBody(), List.class); + if (brokerNames != null && brokerNames.size() > 0) { + final CompletableFuture future = this.controllerManager.getController().getSyncStateData(brokerNames); + if (future != null) { + return future.get(WAIT_TIMEOUT_OUT, TimeUnit.SECONDS); + } + } + } + return RemotingCommand.createResponseCommand(null); + } + + private RemotingCommand handleCleanBrokerData(ChannelHandlerContext ctx, RemotingCommand request) throws Exception { + final CleanControllerBrokerDataRequestHeader requestHeader = (CleanControllerBrokerDataRequestHeader) request.decodeCommandCustomHeader(CleanControllerBrokerDataRequestHeader.class); + final CompletableFuture future = this.controllerManager.getController().cleanBrokerData(requestHeader); + if (null != future) { + return future.get(WAIT_TIMEOUT_OUT, TimeUnit.SECONDS); + } + return RemotingCommand.createResponseCommand(null); + } + + private RemotingCommand handleGetNextBrokerId(ChannelHandlerContext ctx, RemotingCommand request) throws Exception { + final GetNextBrokerIdRequestHeader requestHeader = (GetNextBrokerIdRequestHeader) request.decodeCommandCustomHeader(GetNextBrokerIdRequestHeader.class); + CompletableFuture future = this.controllerManager.getController().getNextBrokerId(requestHeader); + if (future != null) { + return future.get(WAIT_TIMEOUT_OUT, TimeUnit.SECONDS); + } + return RemotingCommand.createResponseCommand(null); + } + + private RemotingCommand handleApplyBrokerId(ChannelHandlerContext ctx, RemotingCommand request) throws Exception { + final ApplyBrokerIdRequestHeader requestHeader = (ApplyBrokerIdRequestHeader) request.decodeCommandCustomHeader(ApplyBrokerIdRequestHeader.class); + CompletableFuture future = this.controllerManager.getController().applyBrokerId(requestHeader); + if (future != null) { + return future.get(WAIT_TIMEOUT_OUT, TimeUnit.SECONDS); + } + return RemotingCommand.createResponseCommand(null); + } + + private RemotingCommand handleRegisterBroker(ChannelHandlerContext ctx, RemotingCommand request) throws Exception { + RegisterBrokerToControllerRequestHeader requestHeader = (RegisterBrokerToControllerRequestHeader) request.decodeCommandCustomHeader(RegisterBrokerToControllerRequestHeader.class); + CompletableFuture future = this.controllerManager.getController().registerBroker(requestHeader); + if (future != null) { + return future.get(WAIT_TIMEOUT_OUT, TimeUnit.SECONDS); + } + return RemotingCommand.createResponseCommand(null); + } + + private RemotingCommand handleUpdateControllerConfig(ChannelHandlerContext ctx, RemotingCommand request) { + if (ctx != null) { + log.info("updateConfig called by {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + } + + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + byte[] body = request.getBody(); + if (body != null) { + String bodyStr; + try { + bodyStr = new String(body, MixAll.DEFAULT_CHARSET); + } catch (UnsupportedEncodingException e) { + log.error("updateConfig byte array to string error: ", e); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("UnsupportedEncodingException " + e); + return response; + } + + Properties properties = MixAll.string2Properties(bodyStr); + if (properties == null) { + log.error("updateConfig MixAll.string2Properties error {}", bodyStr); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("string2Properties error"); + return response; + } + if (validateBlackListConfigExist(properties)) { + response.setCode(ResponseCode.NO_PERMISSION); + response.setRemark("Can not update config in black list."); + return response; + } + + this.controllerManager.getConfiguration().update(properties); + } + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + private RemotingCommand handleGetControllerConfig(ChannelHandlerContext ctx, RemotingCommand request) { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + String content = this.controllerManager.getConfiguration().getAllConfigsFormatString(); + if (content != null && content.length() > 0) { + try { + response.setBody(content.getBytes(MixAll.DEFAULT_CHARSET)); + } catch (UnsupportedEncodingException e) { + log.error("getConfig error, ", e); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("UnsupportedEncodingException " + e); + return response; + } + } + + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + @Override + public boolean rejectRequest() { + return false; + } + private boolean validateBlackListConfigExist(Properties properties) { + for (String blackConfig : configBlackList) { + if (properties.containsKey(blackConfig)) { + return true; + } + } + return false; + } +} diff --git a/controller/src/main/resources/rmq.controller.logback.xml b/controller/src/main/resources/rmq.controller.logback.xml new file mode 100644 index 00000000000..5629da91d0a --- /dev/null +++ b/controller/src/main/resources/rmq.controller.logback.xml @@ -0,0 +1,186 @@ + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}controller_default.log + + true + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}controller_default.%i.log.gz + + 1 + 5 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}dledger.log + true + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}dledger.%i.log.gz + + 1 + 5 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + 0 + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}jraft.log + true + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}jraft.%i.log.gz + + 1 + 5 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + 0 + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}controller_traffic.log + + true + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}controller_traffic.%i.log.gz + + 1 + 10 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}controller.log + true + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}controller.%i.log.gz + + 1 + 5 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + 0 + + + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/controller/src/test/java/org/apache/rocketmq/controller/ControllerManagerTest.java b/controller/src/test/java/org/apache/rocketmq/controller/ControllerManagerTest.java new file mode 100644 index 00000000000..3cf387a39f4 --- /dev/null +++ b/controller/src/test/java/org/apache/rocketmq/controller/ControllerManagerTest.java @@ -0,0 +1,256 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller; + +import org.apache.rocketmq.common.ControllerConfig; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.controller.impl.DLedgerController; +import org.apache.rocketmq.remoting.RemotingClient; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyRemotingClient; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetReplicaInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetReplicaInfoResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.ApplyBrokerIdRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.ApplyBrokerIdResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.GetNextBrokerIdRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.GetNextBrokerIdResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.RegisterBrokerToControllerRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.RegisterBrokerToControllerResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.BrokerHeartbeatRequestHeader; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class ControllerManagerTest { + + public static final String STORE_BASE_PATH = System.getProperty("java.io.tmpdir") + File.separator + "ControllerManagerTest"; + + public static final String STORE_PATH = STORE_BASE_PATH + File.separator + UUID.randomUUID(); + + private List controllers; + private NettyRemotingClient remotingClient; + private NettyRemotingClient remotingClient1; + + public ControllerManager launchManager(final String group, final String peers, final String selfId) { + final String path = STORE_PATH + File.separator + group + File.separator + selfId; + final ControllerConfig config = new ControllerConfig(); + config.setControllerDLegerGroup(group); + config.setControllerDLegerPeers(peers); + config.setControllerDLegerSelfId(selfId); + config.setControllerStorePath(path); + config.setMappedFileSize(10 * 1024 * 1024); + config.setEnableElectUncleanMaster(true); + config.setScanNotActiveBrokerInterval(1000L); + config.setNotifyBrokerRoleChanged(false); + + final NettyServerConfig serverConfig = new NettyServerConfig(); + + final ControllerManager manager = new ControllerManager(config, serverConfig, new NettyClientConfig()); + manager.initialize(); + manager.start(); + this.controllers.add(manager); + return manager; + } + + @Before + public void startup() { + UtilAll.deleteFile(new File(STORE_BASE_PATH)); + this.controllers = new ArrayList<>(); + this.remotingClient = new NettyRemotingClient(new NettyClientConfig()); + this.remotingClient.start(); + this.remotingClient1 = new NettyRemotingClient(new NettyClientConfig()); + this.remotingClient1.start(); + } + + public ControllerManager waitLeader(final List controllers) throws Exception { + if (controllers.isEmpty()) { + return null; + } + DLedgerController c1 = (DLedgerController) controllers.get(0).getController(); + + ControllerManager manager = await().atMost(Duration.ofSeconds(10)).until(() -> { + String leaderId = c1.getMemberState().getLeaderId(); + if (null == leaderId) { + return null; + } + for (ControllerManager controllerManager : controllers) { + final DLedgerController controller = (DLedgerController) controllerManager.getController(); + if (controller.getMemberState().getSelfId().equals(leaderId) && controller.isLeaderState()) { + return controllerManager; + } + } + return null; + }, item -> item != null); + return manager; + } + + public void mockData() { + String group = UUID.randomUUID().toString(); + String peers = String.format("n0-localhost:%d;n1-localhost:%d;n2-localhost:%d", 30000, 30001, 30002); + launchManager(group, peers, "n0"); + launchManager(group, peers, "n1"); + launchManager(group, peers, "n2"); + } + + /** + * Register broker to controller + */ + public void registerBroker( + final String controllerAddress, final String clusterName, + final String brokerName, final Long brokerId, final String brokerAddress, final Long expectMasterBrokerId, + final RemotingClient client) throws Exception { + // Get next brokerId; + final GetNextBrokerIdRequestHeader getNextBrokerIdRequestHeader = new GetNextBrokerIdRequestHeader(clusterName, brokerName); + final RemotingCommand getNextBrokerIdRequest = RemotingCommand.createRequestCommand(RequestCode.CONTROLLER_GET_NEXT_BROKER_ID, getNextBrokerIdRequestHeader); + final RemotingCommand getNextBrokerIdResponse = client.invokeSync(controllerAddress, getNextBrokerIdRequest, 3000); + final GetNextBrokerIdResponseHeader getNextBrokerIdResponseHeader = (GetNextBrokerIdResponseHeader) getNextBrokerIdResponse.decodeCommandCustomHeader(GetNextBrokerIdResponseHeader.class); + String registerCheckCode = brokerAddress + ";" + System.currentTimeMillis(); + assertEquals(ResponseCode.SUCCESS, getNextBrokerIdResponse.getCode()); + assertEquals(brokerId, getNextBrokerIdResponseHeader.getNextBrokerId()); + + // Apply brokerId + final ApplyBrokerIdRequestHeader applyBrokerIdRequestHeader = new ApplyBrokerIdRequestHeader(clusterName, brokerName, brokerId, registerCheckCode); + final RemotingCommand applyBrokerIdRequest = RemotingCommand.createRequestCommand(RequestCode.CONTROLLER_APPLY_BROKER_ID, applyBrokerIdRequestHeader); + final RemotingCommand applyBrokerIdResponse = client.invokeSync(controllerAddress, applyBrokerIdRequest, 3000); + final ApplyBrokerIdResponseHeader applyBrokerIdResponseHeader = (ApplyBrokerIdResponseHeader) applyBrokerIdResponse.decodeCommandCustomHeader(ApplyBrokerIdResponseHeader.class); + assertEquals(ResponseCode.SUCCESS, applyBrokerIdResponse.getCode()); + + // Register success + final RegisterBrokerToControllerRequestHeader registerBrokerToControllerRequestHeader = new RegisterBrokerToControllerRequestHeader(clusterName, brokerName, brokerId, brokerAddress); + final RemotingCommand registerSuccessRequest = RemotingCommand.createRequestCommand(RequestCode.CONTROLLER_REGISTER_BROKER, registerBrokerToControllerRequestHeader); + final RemotingCommand registerSuccessResponse = client.invokeSync(controllerAddress, registerSuccessRequest, 3000); + final RegisterBrokerToControllerResponseHeader registerBrokerToControllerResponseHeader = (RegisterBrokerToControllerResponseHeader) registerSuccessResponse.decodeCommandCustomHeader(RegisterBrokerToControllerResponseHeader.class); + assertEquals(ResponseCode.SUCCESS, registerSuccessResponse.getCode()); + assertEquals(expectMasterBrokerId, registerBrokerToControllerResponseHeader.getMasterBrokerId()); + } + + public RemotingCommand brokerTryElect(final String controllerAddress, final String clusterName, + final String brokerName, final Long brokerId, final RemotingClient client) throws Exception { + final ElectMasterRequestHeader requestHeader = ElectMasterRequestHeader.ofBrokerTrigger(clusterName, brokerName, brokerId); + final RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CONTROLLER_ELECT_MASTER, requestHeader); + RemotingCommand response = client.invokeSync(controllerAddress, request, 10000); + assertNotNull(response); + return response; + } + + public void sendHeartbeat(final String controllerAddress, final String clusterName, final String brokerName, + final Long brokerId, + final String brokerAddress, final Long timeout, final RemotingClient client) throws Exception { + final BrokerHeartbeatRequestHeader heartbeatRequestHeader0 = new BrokerHeartbeatRequestHeader(); + heartbeatRequestHeader0.setBrokerId(brokerId); + heartbeatRequestHeader0.setClusterName(clusterName); + heartbeatRequestHeader0.setBrokerName(brokerName); + heartbeatRequestHeader0.setBrokerAddr(brokerAddress); + heartbeatRequestHeader0.setHeartbeatTimeoutMills(timeout); + final RemotingCommand heartbeatRequest = RemotingCommand.createRequestCommand(RequestCode.BROKER_HEARTBEAT, heartbeatRequestHeader0); + RemotingCommand remotingCommand = client.invokeSync(controllerAddress, heartbeatRequest, 3000); + assertEquals(ResponseCode.SUCCESS, remotingCommand.getCode()); + } + + @Test + public void testSomeApi() throws Exception { + mockData(); + final ControllerManager leader = waitLeader(this.controllers); + String leaderAddr = "localhost" + ":" + leader.getController().getRemotingServer().localListenPort(); + + // Register two broker + registerBroker(leaderAddr, "cluster1", "broker1", 1L, "127.0.0.1:8000", null, this.remotingClient); + + registerBroker(leaderAddr, "cluster1", "broker1", 2L, "127.0.0.1:8001", null, this.remotingClient1); + + // Send heartbeat + sendHeartbeat(leaderAddr, "cluster1", "broker1", 1L, "127.0.0.1:8000", 3000L, remotingClient); + sendHeartbeat(leaderAddr, "cluster1", "broker1", 2L, "127.0.0.1:8001", 3000L, remotingClient1); + + // Two all try elect itself as master, but only the first can be the master + RemotingCommand tryElectCommand1 = brokerTryElect(leaderAddr, "cluster1", "broker1", 1L, this.remotingClient); + ElectMasterResponseHeader brokerTryElectResponseHeader1 = (ElectMasterResponseHeader) tryElectCommand1.decodeCommandCustomHeader(ElectMasterResponseHeader.class); + RemotingCommand tryElectCommand2 = brokerTryElect(leaderAddr, "cluster1", "broker1", 2L, this.remotingClient1); + ElectMasterResponseHeader brokerTryElectResponseHeader2 = (ElectMasterResponseHeader) tryElectCommand2.decodeCommandCustomHeader(ElectMasterResponseHeader.class); + + assertEquals(ResponseCode.SUCCESS, tryElectCommand1.getCode()); + assertEquals(1L, brokerTryElectResponseHeader1.getMasterBrokerId().longValue()); + assertEquals("127.0.0.1:8000", brokerTryElectResponseHeader1.getMasterAddress()); + assertEquals(1, brokerTryElectResponseHeader1.getMasterEpoch().intValue()); + assertEquals(1, brokerTryElectResponseHeader1.getSyncStateSetEpoch().intValue()); + + assertEquals(ResponseCode.CONTROLLER_MASTER_STILL_EXIST, tryElectCommand2.getCode()); + assertEquals(1L, brokerTryElectResponseHeader2.getMasterBrokerId().longValue()); + assertEquals("127.0.0.1:8000", brokerTryElectResponseHeader2.getMasterAddress()); + assertEquals(1, brokerTryElectResponseHeader2.getMasterEpoch().intValue()); + assertEquals(1, brokerTryElectResponseHeader2.getSyncStateSetEpoch().intValue()); + + // Send heartbeat for broker2 every one second + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + executor.scheduleAtFixedRate(() -> { + final BrokerHeartbeatRequestHeader heartbeatRequestHeader = new BrokerHeartbeatRequestHeader(); + heartbeatRequestHeader.setClusterName("cluster1"); + heartbeatRequestHeader.setBrokerName("broker1"); + heartbeatRequestHeader.setBrokerAddr("127.0.0.1:8001"); + heartbeatRequestHeader.setBrokerId(2L); + heartbeatRequestHeader.setHeartbeatTimeoutMills(3000L); + final RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.BROKER_HEARTBEAT, heartbeatRequestHeader); + try { + final RemotingCommand remotingCommand = this.remotingClient1.invokeSync(leaderAddr, request, 3000); + } catch (Exception e) { + e.printStackTrace(); + } + }, 0, 1000L, TimeUnit.MILLISECONDS); + Boolean flag = await().atMost(Duration.ofSeconds(10)).until(() -> { + final GetReplicaInfoRequestHeader requestHeader = new GetReplicaInfoRequestHeader("broker1"); + final RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CONTROLLER_GET_REPLICA_INFO, requestHeader); + final RemotingCommand response = this.remotingClient1.invokeSync(leaderAddr, request, 3000); + final GetReplicaInfoResponseHeader responseHeader = (GetReplicaInfoResponseHeader) response.decodeCommandCustomHeader(GetReplicaInfoResponseHeader.class); + return responseHeader.getMasterBrokerId().equals(2L); + }, item -> item); + + // The new master should be broker2. + assertTrue(flag); + + executor.shutdown(); + } + + @After + public void tearDown() { + for (ControllerManager controller : this.controllers) { + controller.shutdown(); + } + UtilAll.deleteFile(new File(STORE_BASE_PATH)); + this.remotingClient.shutdown(); + this.remotingClient1.shutdown(); + } +} diff --git a/controller/src/test/java/org/apache/rocketmq/controller/ControllerRequestProcessorTest.java b/controller/src/test/java/org/apache/rocketmq/controller/ControllerRequestProcessorTest.java new file mode 100644 index 00000000000..5256522570a --- /dev/null +++ b/controller/src/test/java/org/apache/rocketmq/controller/ControllerRequestProcessorTest.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.controller; + +import org.apache.rocketmq.common.ControllerConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.controller.processor.ControllerRequestProcessor; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.junit.Before; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; +import java.util.Properties; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ControllerRequestProcessorTest { + + private ControllerRequestProcessor controllerRequestProcessor; + + @Before + public void init() throws Exception { + controllerRequestProcessor = new ControllerRequestProcessor(new ControllerManager(new ControllerConfig(), new NettyServerConfig(), new NettyClientConfig())); + } + + @Test + public void testProcessRequest_UpdateConfigPath() throws Exception { + final RemotingCommand updateConfigRequest = RemotingCommand.createRequestCommand(RequestCode.UPDATE_CONTROLLER_CONFIG, null); + Properties properties = new Properties(); + + // Update allowed value + properties.setProperty("notifyBrokerRoleChanged", "true"); + updateConfigRequest.setBody(MixAll.properties2String(properties).getBytes(StandardCharsets.UTF_8)); + + RemotingCommand response = controllerRequestProcessor.processRequest(null, updateConfigRequest); + + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + // Update disallowed value + properties.clear(); + properties.setProperty("configStorePath", "test/path"); + updateConfigRequest.setBody(MixAll.properties2String(properties).getBytes(StandardCharsets.UTF_8)); + + response = controllerRequestProcessor.processRequest(null, updateConfigRequest); + + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.NO_PERMISSION); + assertThat(response.getRemark()).contains("Can not update config in black list."); + + // Update disallowed value + properties.clear(); + properties.setProperty("rocketmqHome", "test/path"); + updateConfigRequest.setBody(MixAll.properties2String(properties).getBytes(StandardCharsets.UTF_8)); + + response = controllerRequestProcessor.processRequest(null, updateConfigRequest); + + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.NO_PERMISSION); + assertThat(response.getRemark()).contains("Can not update config in black list."); + + // Update disallowed value + properties.clear(); + properties.setProperty("configBlackList", "test;path"); + updateConfigRequest.setBody(MixAll.properties2String(properties).getBytes(StandardCharsets.UTF_8)); + + response = controllerRequestProcessor.processRequest(null, updateConfigRequest); + + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.NO_PERMISSION); + assertThat(response.getRemark()).contains("Can not update config in black list."); + } +} diff --git a/controller/src/test/java/org/apache/rocketmq/controller/ControllerTestBase.java b/controller/src/test/java/org/apache/rocketmq/controller/ControllerTestBase.java new file mode 100644 index 00000000000..f77f49dcf25 --- /dev/null +++ b/controller/src/test/java/org/apache/rocketmq/controller/ControllerTestBase.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.controller; + +public class ControllerTestBase { + + public final static String DEFAULT_CLUSTER_NAME = "cluster-a"; + + public final static String DEFAULT_BROKER_NAME = "broker-set-a"; + + public final static String[] DEFAULT_IP = {"127.0.0.1:9000", "127.0.0.1:9001", "127.0.0.1:9002"}; +} diff --git a/controller/src/test/java/org/apache/rocketmq/controller/impl/DLedgerControllerTest.java b/controller/src/test/java/org/apache/rocketmq/controller/impl/DLedgerControllerTest.java new file mode 100644 index 00000000000..32e7859a58a --- /dev/null +++ b/controller/src/test/java/org/apache/rocketmq/controller/impl/DLedgerControllerTest.java @@ -0,0 +1,394 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl; + +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.ControllerConfig; +import org.apache.rocketmq.controller.Controller; +import org.apache.rocketmq.controller.elect.impl.DefaultElectPolicy; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.SyncStateSet; +import org.apache.rocketmq.remoting.protocol.header.controller.AlterSyncStateSetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetReplicaInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetReplicaInfoResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.ApplyBrokerIdRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.GetNextBrokerIdRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.GetNextBrokerIdResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.RegisterBrokerToControllerRequestHeader; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +import static org.apache.rocketmq.controller.ControllerTestBase.DEFAULT_BROKER_NAME; +import static org.apache.rocketmq.controller.ControllerTestBase.DEFAULT_CLUSTER_NAME; +import static org.apache.rocketmq.controller.ControllerTestBase.DEFAULT_IP; +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class DLedgerControllerTest { + private List baseDirs; + private List controllers; + + public DLedgerController launchController(final String group, final String peers, final String selfId, + final boolean isEnableElectUncleanMaster) { + String tmpdir = System.getProperty("java.io.tmpdir"); + final String path = (StringUtils.endsWith(tmpdir, File.separator) ? tmpdir : tmpdir + File.separator) + group + File.separator + selfId; + baseDirs.add(path); + + final ControllerConfig config = new ControllerConfig(); + config.setControllerDLegerGroup(group); + config.setControllerDLegerPeers(peers); + config.setControllerDLegerSelfId(selfId); + config.setControllerStorePath(path); + config.setMappedFileSize(10 * 1024 * 1024); + config.setEnableElectUncleanMaster(isEnableElectUncleanMaster); + config.setScanInactiveMasterInterval(1000); + final DLedgerController controller = new DLedgerController(config, (str1, str2, str3) -> true); + + controller.startup(); + return controller; + } + + @Before + public void startup() { + this.baseDirs = new ArrayList<>(); + this.controllers = new ArrayList<>(); + } + + @After + public void tearDown() { + for (Controller controller : this.controllers) { + controller.shutdown(); + } + for (String dir : this.baseDirs) { + new File(dir).delete(); + } + } + + public void registerNewBroker(Controller leader, String clusterName, String brokerName, String brokerAddress, + Long expectBrokerId) throws Exception { + // Get next brokerId + final GetNextBrokerIdRequestHeader getNextBrokerIdRequest = new GetNextBrokerIdRequestHeader(clusterName, brokerName); + RemotingCommand remotingCommand = leader.getNextBrokerId(getNextBrokerIdRequest).get(2, TimeUnit.SECONDS); + GetNextBrokerIdResponseHeader getNextBrokerIdResp = (GetNextBrokerIdResponseHeader) remotingCommand.readCustomHeader(); + Long nextBrokerId = getNextBrokerIdResp.getNextBrokerId(); + String registerCheckCode = brokerAddress + ";" + System.currentTimeMillis(); + + // Check response + assertEquals(expectBrokerId, nextBrokerId); + + // Apply brokerId + final ApplyBrokerIdRequestHeader applyBrokerIdRequestHeader = new ApplyBrokerIdRequestHeader(clusterName, brokerName, nextBrokerId, registerCheckCode); + RemotingCommand remotingCommand1 = leader.applyBrokerId(applyBrokerIdRequestHeader).get(2, TimeUnit.SECONDS); + + // Check response + assertEquals(ResponseCode.SUCCESS, remotingCommand1.getCode()); + + // Register success + final RegisterBrokerToControllerRequestHeader registerBrokerToControllerRequestHeader = new RegisterBrokerToControllerRequestHeader(clusterName, brokerName, nextBrokerId, brokerAddress); + RemotingCommand remotingCommand2 = leader.registerBroker(registerBrokerToControllerRequestHeader).get(2, TimeUnit.SECONDS); + + assertEquals(ResponseCode.SUCCESS, remotingCommand2.getCode()); + } + + public void brokerTryElectMaster(Controller leader, String clusterName, String brokerName, String brokerAddress, + Long brokerId, + boolean exceptSuccess) throws Exception { + final ElectMasterRequestHeader electMasterRequestHeader = ElectMasterRequestHeader.ofBrokerTrigger(clusterName, brokerName, brokerId); + RemotingCommand command = leader.electMaster(electMasterRequestHeader).get(2, TimeUnit.SECONDS); + ElectMasterResponseHeader header = (ElectMasterResponseHeader) command.readCustomHeader(); + assertEquals(exceptSuccess, ResponseCode.SUCCESS == command.getCode()); + } + + private boolean alterNewInSyncSet(Controller leader, String brokerName, Long masterBrokerId, Integer masterEpoch, + Set newSyncStateSet, Integer syncStateSetEpoch) throws Exception { + final AlterSyncStateSetRequestHeader alterRequest = + new AlterSyncStateSetRequestHeader(brokerName, masterBrokerId, masterEpoch); + final RemotingCommand response = leader.alterSyncStateSet(alterRequest, new SyncStateSet(newSyncStateSet, syncStateSetEpoch)).get(10, TimeUnit.SECONDS); + if (null == response || response.getCode() != ResponseCode.SUCCESS) { + return false; + } + final RemotingCommand getInfoResponse = leader.getReplicaInfo(new GetReplicaInfoRequestHeader(brokerName)).get(10, TimeUnit.SECONDS); + final GetReplicaInfoResponseHeader replicaInfo = (GetReplicaInfoResponseHeader) getInfoResponse.readCustomHeader(); + final SyncStateSet syncStateSet = RemotingSerializable.decode(getInfoResponse.getBody(), SyncStateSet.class); + assertArrayEquals(syncStateSet.getSyncStateSet().toArray(), newSyncStateSet.toArray()); + assertEquals(syncStateSet.getSyncStateSetEpoch(), syncStateSetEpoch + 1); + return true; + } + + public DLedgerController waitLeader(final List controllers) throws Exception { + if (controllers.isEmpty()) { + return null; + } + DLedgerController c1 = controllers.get(0); + DLedgerController dLedgerController = await().atMost(Duration.ofSeconds(10)).until(() -> { + String leaderId = c1.getMemberState().getLeaderId(); + if (null == leaderId) { + return null; + } + for (DLedgerController controller : controllers) { + if (controller.getMemberState().getSelfId().equals(leaderId) && controller.isLeaderState()) { + return controller; + } + } + return null; + }, item -> item != null); + return dLedgerController; + } + + public DLedgerController mockMetaData(boolean enableElectUncleanMaster) throws Exception { + String group = UUID.randomUUID().toString(); + String peers = String.format("n0-localhost:%d;n1-localhost:%d;n2-localhost:%d", 30000, 30001, 30002); + DLedgerController c0 = launchController(group, peers, "n0", enableElectUncleanMaster); + DLedgerController c1 = launchController(group, peers, "n1", enableElectUncleanMaster); + DLedgerController c2 = launchController(group, peers, "n2", enableElectUncleanMaster); + controllers.add(c0); + controllers.add(c1); + controllers.add(c2); + + DLedgerController leader = waitLeader(controllers); + + // register + registerNewBroker(leader, DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, DEFAULT_IP[0], 1L); + registerNewBroker(leader, DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, DEFAULT_IP[1], 2L); + registerNewBroker(leader, DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, DEFAULT_IP[2], 3L); + // try elect + brokerTryElectMaster(leader, DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, DEFAULT_IP[0], 1L, true); + brokerTryElectMaster(leader, DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, DEFAULT_IP[1], 2L, false); + brokerTryElectMaster(leader, DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, DEFAULT_IP[2], 3L, false); + final RemotingCommand getInfoResponse = leader.getReplicaInfo(new GetReplicaInfoRequestHeader(DEFAULT_BROKER_NAME)).get(10, TimeUnit.SECONDS); + final GetReplicaInfoResponseHeader replicaInfo = (GetReplicaInfoResponseHeader) getInfoResponse.readCustomHeader(); + assertEquals(1, replicaInfo.getMasterEpoch().intValue()); + assertEquals(DEFAULT_IP[0], replicaInfo.getMasterAddress()); + // Try alter SyncStateSet + final HashSet newSyncStateSet = new HashSet<>(); + newSyncStateSet.add(1L); + newSyncStateSet.add(2L); + newSyncStateSet.add(3L); + assertTrue(alterNewInSyncSet(leader, DEFAULT_BROKER_NAME, 1L, 1, newSyncStateSet, 1)); + return leader; + } + + public void setBrokerAlivePredicate(DLedgerController controller, Long... deathBroker) { + controller.setBrokerAlivePredicate((clusterName, brokerName, brokerId) -> { + for (Long broker : deathBroker) { + if (broker.equals(brokerId)) { + return false; + } + } + return true; + }); + } + + public void setBrokerElectPolicy(DLedgerController controller, Long... deathBroker) { + controller.setElectPolicy(new DefaultElectPolicy((clusterName, brokerName, brokerId) -> { + for (Long broker : deathBroker) { + if (broker.equals(brokerId)) { + return false; + } + } + return true; + }, null)); + } + + @Test + public void testElectMaster() throws Exception { + final DLedgerController leader = mockMetaData(false); + final ElectMasterRequestHeader request = ElectMasterRequestHeader.ofControllerTrigger(DEFAULT_BROKER_NAME); + setBrokerElectPolicy(leader, 1L); + final RemotingCommand resp = leader.electMaster(request).get(10, TimeUnit.SECONDS); + final ElectMasterResponseHeader response = (ElectMasterResponseHeader) resp.readCustomHeader(); + assertEquals(2, response.getMasterEpoch().intValue()); + assertNotEquals(1L, response.getMasterBrokerId().longValue()); + assertNotEquals(DEFAULT_IP[0], response.getMasterAddress()); + } + + @Test + public void testBrokerLifecycleListener() throws Exception { + final DLedgerController leader = mockMetaData(false); + + assertTrue(leader.isLeaderState()); + // Mock that master broker has been inactive, and try to elect a new master from sync-state-set + // But we shut down two controller, so the ElectMasterEvent will be appended to DLedger failed. + // So the statemachine still keep the stale master's information + List removed = controllers.stream().filter(controller -> controller != leader).collect(Collectors.toList()); + for (DLedgerController dLedgerController : removed) { + dLedgerController.shutdown(); + controllers.remove(dLedgerController); + } + + final ElectMasterRequestHeader request = ElectMasterRequestHeader.ofControllerTrigger(DEFAULT_BROKER_NAME); + setBrokerElectPolicy(leader, 1L); + Exception exception = null; + RemotingCommand remotingCommand = null; + try { + remotingCommand = leader.electMaster(request).get(5, TimeUnit.SECONDS); + } catch (Exception e) { + exception = e; + } + + assertTrue(exception != null || + remotingCommand != null && remotingCommand.getCode() == ResponseCode.CONTROLLER_NOT_LEADER); + + // Shut down leader controller + leader.shutdown(); + controllers.remove(leader); + // Restart two controller + for (DLedgerController controller : removed) { + if (controller != leader) { + ControllerConfig config = controller.getControllerConfig(); + DLedgerController newController = launchController(config.getControllerDLegerGroup(), config.getControllerDLegerPeers(), config.getControllerDLegerSelfId(), false); + controllers.add(newController); + newController.startup(); + } + } + DLedgerController newLeader = waitLeader(controllers); + setBrokerAlivePredicate(newLeader, 1L); + // Check if the statemachine is stale + final RemotingCommand resp = newLeader.getReplicaInfo(new GetReplicaInfoRequestHeader(DEFAULT_BROKER_NAME)). + get(10, TimeUnit.SECONDS); + final GetReplicaInfoResponseHeader replicaInfo = (GetReplicaInfoResponseHeader) resp.readCustomHeader(); + assertEquals(1, replicaInfo.getMasterBrokerId().longValue()); + assertEquals(1, replicaInfo.getMasterEpoch().intValue()); + + // Register broker's lifecycle listener + AtomicBoolean atomicBoolean = new AtomicBoolean(false); + newLeader.registerBrokerLifecycleListener((clusterName, brokerName, brokerId) -> { + assertEquals(DEFAULT_BROKER_NAME, brokerName); + atomicBoolean.set(true); + }); + Thread.sleep(2000); + assertTrue(atomicBoolean.get()); + } + + @Test + public void testAllReplicasShutdownAndRestartWithUnEnableElectUnCleanMaster() throws Exception { + final DLedgerController leader = mockMetaData(false); + final HashSet newSyncStateSet = new HashSet<>(); + newSyncStateSet.add(1L); + + assertTrue(alterNewInSyncSet(leader, DEFAULT_BROKER_NAME, 1L, 1, newSyncStateSet, 2)); + + // Now we trigger electMaster api, which means the old master is shutdown and want to elect a new master. + // However, the syncStateSet in statemachine is {1}, not more replicas can be elected as master, it will be failed. + final ElectMasterRequestHeader electRequest = ElectMasterRequestHeader.ofControllerTrigger(DEFAULT_BROKER_NAME); + setBrokerElectPolicy(leader, 1L); + leader.electMaster(electRequest).get(10, TimeUnit.SECONDS); + + final RemotingCommand resp = leader.getReplicaInfo(new GetReplicaInfoRequestHeader(DEFAULT_BROKER_NAME)). + get(10, TimeUnit.SECONDS); + final GetReplicaInfoResponseHeader replicaInfo = (GetReplicaInfoResponseHeader) resp.readCustomHeader(); + final SyncStateSet syncStateSet = RemotingSerializable.decode(resp.getBody(), SyncStateSet.class); + assertEquals(syncStateSet.getSyncStateSet(), newSyncStateSet); + assertEquals(null, replicaInfo.getMasterAddress()); + assertEquals(2, replicaInfo.getMasterEpoch().intValue()); + + // Now, we start broker - id[2]address[127.0.0.1:9001] to try elect, but it was not in syncStateSet, so it will not be elected as master. + final ElectMasterRequestHeader request1 = + ElectMasterRequestHeader.ofBrokerTrigger(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, 2L); + final ElectMasterResponseHeader r1 = (ElectMasterResponseHeader) leader.electMaster(request1).get(10, TimeUnit.SECONDS).readCustomHeader(); + assertEquals(null, r1.getMasterBrokerId()); + assertEquals(null, r1.getMasterAddress()); + + // Now, we start broker - id[1]address[127.0.0.1:9000] to try elect, it will be elected as master + setBrokerElectPolicy(leader); + final ElectMasterRequestHeader request2 = + ElectMasterRequestHeader.ofBrokerTrigger(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, 1L); + final ElectMasterResponseHeader r2 = (ElectMasterResponseHeader) leader.electMaster(request2).get(10, TimeUnit.SECONDS).readCustomHeader(); + assertEquals(1L, r2.getMasterBrokerId().longValue()); + assertEquals(DEFAULT_IP[0], r2.getMasterAddress()); + assertEquals(3, r2.getMasterEpoch().intValue()); + } + + @Test + public void testEnableElectUnCleanMaster() throws Exception { + final DLedgerController leader = mockMetaData(true); + final HashSet newSyncStateSet = new HashSet<>(); + newSyncStateSet.add(1L); + + assertTrue(alterNewInSyncSet(leader, DEFAULT_BROKER_NAME, 1L, 1, newSyncStateSet, 2)); + + // Now we trigger electMaster api, which means the old master is shutdown and want to elect a new master. + // However, event if the syncStateSet in statemachine is {DEFAULT_IP[0]} + // the option {enableElectUncleanMaster = true}, so the controller sill can elect a new master + final ElectMasterRequestHeader electRequest = ElectMasterRequestHeader.ofControllerTrigger(DEFAULT_BROKER_NAME); + setBrokerElectPolicy(leader, 1L); + final CompletableFuture future = leader.electMaster(electRequest); + future.get(10, TimeUnit.SECONDS); + + final RemotingCommand resp = leader.getReplicaInfo(new GetReplicaInfoRequestHeader(DEFAULT_BROKER_NAME)).get(10, TimeUnit.SECONDS); + final GetReplicaInfoResponseHeader replicaInfo = (GetReplicaInfoResponseHeader) resp.readCustomHeader(); + final SyncStateSet syncStateSet = RemotingSerializable.decode(resp.getBody(), SyncStateSet.class); + + final HashSet newSyncStateSet2 = new HashSet<>(); + newSyncStateSet2.add(replicaInfo.getMasterBrokerId()); + assertEquals(syncStateSet.getSyncStateSet(), newSyncStateSet2); + assertNotEquals(1L, replicaInfo.getMasterBrokerId().longValue()); + assertNotEquals(DEFAULT_IP[0], replicaInfo.getMasterAddress()); + assertEquals(2, replicaInfo.getMasterEpoch().intValue()); + } + + @Test + public void testChangeControllerLeader() throws Exception { + final DLedgerController leader = mockMetaData(false); + leader.shutdown(); + this.controllers.remove(leader); + // Wait leader again + final DLedgerController newLeader = waitLeader(this.controllers); + assertNotNull(newLeader); + + RemotingCommand response = await().atMost(Duration.ofSeconds(10)).until(() -> { + final RemotingCommand resp = newLeader.getReplicaInfo(new GetReplicaInfoRequestHeader(DEFAULT_BROKER_NAME)).get(10, TimeUnit.SECONDS); + if (resp.getCode() == ResponseCode.SUCCESS) { + + return resp; + } + return null; + + }, item -> item != null); + final GetReplicaInfoResponseHeader replicaInfo = (GetReplicaInfoResponseHeader) response.readCustomHeader(); + final SyncStateSet syncStateSetResult = RemotingSerializable.decode(response.getBody(), SyncStateSet.class); + assertEquals(replicaInfo.getMasterAddress(), DEFAULT_IP[0]); + assertEquals(1, replicaInfo.getMasterEpoch().intValue()); + + final HashSet syncStateSet = new HashSet<>(); + syncStateSet.add(1L); + syncStateSet.add(2L); + syncStateSet.add(3L); + assertEquals(syncStateSetResult.getSyncStateSet(), syncStateSet); + } +} diff --git a/controller/src/test/java/org/apache/rocketmq/controller/impl/DefaultBrokerHeartbeatManagerTest.java b/controller/src/test/java/org/apache/rocketmq/controller/impl/DefaultBrokerHeartbeatManagerTest.java new file mode 100644 index 00000000000..9d693e47320 --- /dev/null +++ b/controller/src/test/java/org/apache/rocketmq/controller/impl/DefaultBrokerHeartbeatManagerTest.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl; + +import org.apache.rocketmq.common.ControllerConfig; +import org.apache.rocketmq.controller.BrokerHeartbeatManager; +import org.apache.rocketmq.controller.impl.heartbeat.DefaultBrokerHeartbeatManager; +import org.junit.Before; +import org.junit.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertTrue; + +public class DefaultBrokerHeartbeatManagerTest { + private BrokerHeartbeatManager heartbeatManager; + + @Before + public void init() { + final ControllerConfig config = new ControllerConfig(); + config.setScanNotActiveBrokerInterval(2000); + this.heartbeatManager = new DefaultBrokerHeartbeatManager(config); + this.heartbeatManager.initialize(); + this.heartbeatManager.start(); + } + + @Test + public void testDetectBrokerAlive() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + this.heartbeatManager.registerBrokerLifecycleListener((clusterName, brokerName, brokerId) -> { + latch.countDown(); + }); + this.heartbeatManager.onBrokerHeartbeat("cluster1", "broker1", "127.0.0.1:7000", 1L, 3000L, null, + 1, 1L, -1L, 0); + assertTrue(latch.await(5000, TimeUnit.MILLISECONDS)); + this.heartbeatManager.shutdown(); + } + +} \ No newline at end of file diff --git a/controller/src/test/java/org/apache/rocketmq/controller/impl/manager/ReplicasInfoManagerTest.java b/controller/src/test/java/org/apache/rocketmq/controller/impl/manager/ReplicasInfoManagerTest.java new file mode 100644 index 00000000000..bc9c50cecb8 --- /dev/null +++ b/controller/src/test/java/org/apache/rocketmq/controller/impl/manager/ReplicasInfoManagerTest.java @@ -0,0 +1,511 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.controller.impl.manager; + +import org.apache.rocketmq.common.ControllerConfig; +import org.apache.rocketmq.controller.elect.ElectPolicy; +import org.apache.rocketmq.controller.elect.impl.DefaultElectPolicy; +import org.apache.rocketmq.controller.helper.BrokerValidPredicate; +import org.apache.rocketmq.controller.impl.event.ControllerResult; +import org.apache.rocketmq.controller.impl.event.ElectMasterEvent; +import org.apache.rocketmq.controller.impl.event.EventMessage; +import org.apache.rocketmq.controller.impl.heartbeat.DefaultBrokerHeartbeatManager; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.BrokerReplicasInfo; +import org.apache.rocketmq.remoting.protocol.body.SyncStateSet; +import org.apache.rocketmq.remoting.protocol.header.controller.AlterSyncStateSetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.AlterSyncStateSetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetReplicaInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.GetReplicaInfoResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.admin.CleanControllerBrokerDataRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.ApplyBrokerIdRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.ApplyBrokerIdResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.GetNextBrokerIdRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.GetNextBrokerIdResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.RegisterBrokerToControllerRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.controller.register.RegisterBrokerToControllerResponseHeader; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import static org.apache.rocketmq.controller.ControllerTestBase.DEFAULT_BROKER_NAME; +import static org.apache.rocketmq.controller.ControllerTestBase.DEFAULT_CLUSTER_NAME; +import static org.apache.rocketmq.controller.ControllerTestBase.DEFAULT_IP; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class ReplicasInfoManagerTest { + private ReplicasInfoManager replicasInfoManager; + + private DefaultBrokerHeartbeatManager heartbeatManager; + + private ControllerConfig config; + + @Before + public void init() { + this.config = new ControllerConfig(); + this.config.setEnableElectUncleanMaster(false); + this.config.setScanNotActiveBrokerInterval(300000000); + this.replicasInfoManager = new ReplicasInfoManager(config); + this.heartbeatManager = new DefaultBrokerHeartbeatManager(config); + this.heartbeatManager.initialize(); + this.heartbeatManager.start(); + } + + @After + public void destroy() { + this.replicasInfoManager = null; + this.heartbeatManager.shutdown(); + this.heartbeatManager = null; + } + + private BrokerReplicasInfo.ReplicasInfo getReplicasInfo(String brokerName) { + ControllerResult syncStateData = this.replicasInfoManager.getSyncStateData(Arrays.asList(brokerName), (a, b, c) -> true); + BrokerReplicasInfo replicasInfo = RemotingSerializable.decode(syncStateData.getBody(), BrokerReplicasInfo.class); + return replicasInfo.getReplicasInfoTable().get(brokerName); + } + + public void registerNewBroker(String clusterName, String brokerName, String brokerAddress, + Long exceptBrokerId, Long exceptMasterBrokerId) { + + // Get next brokerId + final GetNextBrokerIdRequestHeader getNextBrokerIdRequestHeader = new GetNextBrokerIdRequestHeader(clusterName, brokerName); + final ControllerResult nextBrokerIdResult = this.replicasInfoManager.getNextBrokerId(getNextBrokerIdRequestHeader); + Long nextBrokerId = nextBrokerIdResult.getResponse().getNextBrokerId(); + String registerCheckCode = brokerAddress + ";" + System.currentTimeMillis(); + + // check response + assertEquals(ResponseCode.SUCCESS, nextBrokerIdResult.getResponseCode()); + assertEquals(exceptBrokerId, nextBrokerId); + + // Apply brokerId + final ApplyBrokerIdRequestHeader applyBrokerIdRequestHeader = new ApplyBrokerIdRequestHeader(clusterName, brokerName, nextBrokerId, registerCheckCode); + final ControllerResult applyBrokerIdResult = this.replicasInfoManager.applyBrokerId(applyBrokerIdRequestHeader); + apply(applyBrokerIdResult.getEvents()); + + // check response + assertEquals(ResponseCode.SUCCESS, applyBrokerIdResult.getResponseCode()); + + // check it in state machine + BrokerReplicasInfo.ReplicasInfo replicasInfo = getReplicasInfo(brokerName); + BrokerReplicasInfo.ReplicaIdentity replicaIdentity = replicasInfo.getNotInSyncReplicas().stream().filter(x -> x.getBrokerId().equals(nextBrokerId)).findFirst().get(); + assertNotNull(replicaIdentity); + assertEquals(brokerName, replicaIdentity.getBrokerName()); + assertEquals(exceptBrokerId, replicaIdentity.getBrokerId()); + assertEquals(brokerAddress, replicaIdentity.getBrokerAddress()); + + // register success + final RegisterBrokerToControllerRequestHeader registerBrokerToControllerRequestHeader = new RegisterBrokerToControllerRequestHeader(clusterName, brokerName, exceptBrokerId, brokerAddress); + ControllerResult registerSuccessResult = this.replicasInfoManager.registerBroker(registerBrokerToControllerRequestHeader, (a, b, c) -> true); + apply(registerSuccessResult.getEvents()); + + // check response + assertEquals(ResponseCode.SUCCESS, registerSuccessResult.getResponseCode()); + assertEquals(exceptMasterBrokerId, registerSuccessResult.getResponse().getMasterBrokerId()); + + } + + public void brokerElectMaster(String clusterName, Long brokerId, String brokerName, String brokerAddress, + boolean isFirstTryElect, boolean expectToBeElected) { + this.brokerElectMaster(clusterName, brokerId, brokerName, brokerAddress, isFirstTryElect, expectToBeElected, (a, b, c) -> true); + } + + public void brokerElectMaster(String clusterName, Long brokerId, String brokerName, String brokerAddress, + boolean isFirstTryElect, boolean expectToBeElected, BrokerValidPredicate validPredicate) { + + final GetReplicaInfoResponseHeader replicaInfoBefore = this.replicasInfoManager.getReplicaInfo(new GetReplicaInfoRequestHeader(brokerName)).getResponse(); + BrokerReplicasInfo.ReplicasInfo syncStateSetInfo = getReplicasInfo(brokerName); + // Try elect itself as a master + ElectMasterRequestHeader requestHeader = ElectMasterRequestHeader.ofBrokerTrigger(clusterName, brokerName, brokerId); + final ControllerResult result = this.replicasInfoManager.electMaster(requestHeader, new DefaultElectPolicy(validPredicate, null)); + apply(result.getEvents()); + + final GetReplicaInfoResponseHeader replicaInfoAfter = this.replicasInfoManager.getReplicaInfo(new GetReplicaInfoRequestHeader(brokerName)).getResponse(); + final ElectMasterResponseHeader response = result.getResponse(); + + if (isFirstTryElect) { + // it should be elected + // check response + assertEquals(ResponseCode.SUCCESS, result.getResponseCode()); + assertEquals(1, response.getMasterEpoch().intValue()); + assertEquals(1, response.getSyncStateSetEpoch().intValue()); + assertEquals(brokerAddress, response.getMasterAddress()); + assertEquals(brokerId, response.getMasterBrokerId()); + // check it in state machine + assertEquals(brokerAddress, replicaInfoAfter.getMasterAddress()); + assertEquals(1, replicaInfoAfter.getMasterEpoch().intValue()); + assertEquals(brokerId, replicaInfoAfter.getMasterBrokerId()); + } else { + + // failed because now master still exist + if (replicaInfoBefore.getMasterBrokerId() != null && validPredicate.check(clusterName, brokerName, replicaInfoBefore.getMasterBrokerId())) { + assertEquals(ResponseCode.CONTROLLER_MASTER_STILL_EXIST, result.getResponseCode()); + assertEquals(replicaInfoBefore.getMasterAddress(), response.getMasterAddress()); + assertEquals(replicaInfoBefore.getMasterEpoch(), response.getMasterEpoch()); + assertEquals(replicaInfoBefore.getMasterBrokerId(), response.getMasterBrokerId()); + assertEquals(replicaInfoBefore.getMasterBrokerId(), replicaInfoAfter.getMasterBrokerId()); + return; + } + if (syncStateSetInfo.isExistInSync(brokerName, brokerId, brokerAddress) || this.config.isEnableElectUncleanMaster()) { + // a new master can be elected successfully + assertEquals(ResponseCode.SUCCESS, result.getResponseCode()); + assertEquals(replicaInfoBefore.getMasterEpoch() + 1, replicaInfoAfter.getMasterEpoch().intValue()); + + if (expectToBeElected) { + assertEquals(brokerAddress, response.getMasterAddress()); + assertEquals(brokerId, response.getMasterBrokerId()); + assertEquals(brokerAddress, replicaInfoAfter.getMasterAddress()); + assertEquals(brokerId, replicaInfoAfter.getMasterBrokerId()); + } + + } else { + // failed because elect nothing + assertEquals(ResponseCode.CONTROLLER_ELECT_MASTER_FAILED, result.getResponseCode()); + } + } + } + + private boolean alterNewInSyncSet(String brokerName, Long brokerId, Integer masterEpoch, + Set newSyncStateSet, Integer syncStateSetEpoch) { + final AlterSyncStateSetRequestHeader alterRequest = + new AlterSyncStateSetRequestHeader(brokerName, brokerId, masterEpoch); + final ControllerResult result = this.replicasInfoManager.alterSyncStateSet(alterRequest, + new SyncStateSet(newSyncStateSet, syncStateSetEpoch), (cluster, brokerName1, brokerId1) -> true); + apply(result.getEvents()); + + final ControllerResult resp = this.replicasInfoManager.getReplicaInfo(new GetReplicaInfoRequestHeader(brokerName)); + final GetReplicaInfoResponseHeader replicaInfo = resp.getResponse(); + final SyncStateSet syncStateSet = RemotingSerializable.decode(resp.getBody(), SyncStateSet.class); + + assertArrayEquals(syncStateSet.getSyncStateSet().toArray(), newSyncStateSet.toArray()); + assertEquals(syncStateSet.getSyncStateSetEpoch(), syncStateSetEpoch + 1); + return true; + } + + private void apply(final List events) { + for (EventMessage event : events) { + this.replicasInfoManager.applyEvent(event); + } + } + + public void mockMetaData() { + registerNewBroker(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, DEFAULT_IP[0], 1L, null); + registerNewBroker(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, DEFAULT_IP[1], 2L, null); + registerNewBroker(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, DEFAULT_IP[2], 3L, null); + brokerElectMaster(DEFAULT_CLUSTER_NAME, 1L, DEFAULT_BROKER_NAME, DEFAULT_IP[0], true, true); + brokerElectMaster(DEFAULT_CLUSTER_NAME, 2L, DEFAULT_BROKER_NAME, DEFAULT_IP[1], false, false); + brokerElectMaster(DEFAULT_CLUSTER_NAME, 3L, DEFAULT_BROKER_NAME, DEFAULT_IP[2], false, false); + final HashSet newSyncStateSet = new HashSet<>(); + newSyncStateSet.add(1L); + newSyncStateSet.add(2L); + newSyncStateSet.add(3L); + assertTrue(alterNewInSyncSet(DEFAULT_BROKER_NAME, 1L, 1, newSyncStateSet, 1)); + } + + public void mockHeartbeatDataMasterStillAlive() { + this.heartbeatManager.onBrokerHeartbeat(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, DEFAULT_IP[0], 1L, 10000000000L, null, + 1, 1L, -1L, 0); + this.heartbeatManager.onBrokerHeartbeat(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, DEFAULT_IP[1], 2L, 10000000000L, null, + 1, 2L, -1L, 0); + this.heartbeatManager.onBrokerHeartbeat(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, DEFAULT_IP[2], 3L, 10000000000L, null, + 1, 3L, -1L, 0); + } + + public void mockHeartbeatDataHigherEpoch() { + this.heartbeatManager.onBrokerHeartbeat(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, DEFAULT_IP[0], 1L, -10000L, null, + 1, 3L, -1L, 0); + this.heartbeatManager.onBrokerHeartbeat(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, DEFAULT_IP[1], 2L, 10000000000L, null, + 1, 2L, -1L, 0); + this.heartbeatManager.onBrokerHeartbeat(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, DEFAULT_IP[2], 3L, 10000000000L, null, + 0, 3L, -1L, 0); + } + + public void mockHeartbeatDataHigherOffset() { + this.heartbeatManager.onBrokerHeartbeat(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, DEFAULT_IP[0], 1L, -10000L, null, + 1, 3L, -1L, 0); + this.heartbeatManager.onBrokerHeartbeat(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, DEFAULT_IP[1], 2L, 10000000000L, null, + 1, 2L, -1L, 0); + this.heartbeatManager.onBrokerHeartbeat(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, DEFAULT_IP[2], 3L, 10000000000L, null, + 1, 3L, -1L, 0); + } + + public void mockHeartbeatDataHigherPriority() { + this.heartbeatManager.onBrokerHeartbeat(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, DEFAULT_IP[0], 1L, -10000L, null, + 1, 3L, -1L, 3); + this.heartbeatManager.onBrokerHeartbeat(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, DEFAULT_IP[1], 2L, 10000000000L, null, + 1, 3L, -1L, 2); + this.heartbeatManager.onBrokerHeartbeat(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, DEFAULT_IP[2], 3L, 10000000000L, null, + 1, 3L, -1L, 1); + } + + @Test + public void testRegisterBrokerSuccess() { + mockMetaData(); + + BrokerReplicasInfo.ReplicasInfo replicasInfo = getReplicasInfo(DEFAULT_BROKER_NAME); + assertEquals(1L, replicasInfo.getMasterBrokerId().longValue()); + assertEquals(DEFAULT_IP[0], replicasInfo.getMasterAddress()); + assertEquals(1, replicasInfo.getMasterEpoch()); + assertEquals(2, replicasInfo.getSyncStateSetEpoch()); + assertEquals(3, replicasInfo.getInSyncReplicas().size()); + assertEquals(0, replicasInfo.getNotInSyncReplicas().size()); + } + + @Test + public void testRegisterWithMasterExistResp() { + registerNewBroker(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, DEFAULT_IP[0], 1L, null); + registerNewBroker(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, DEFAULT_IP[1], 2L, null); + brokerElectMaster(DEFAULT_CLUSTER_NAME, 1L, DEFAULT_BROKER_NAME, DEFAULT_IP[0], true, true); + brokerElectMaster(DEFAULT_CLUSTER_NAME, 2L, DEFAULT_BROKER_NAME, DEFAULT_IP[1], false, false); + registerNewBroker(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, DEFAULT_IP[2], 3L, 1L); + brokerElectMaster(DEFAULT_CLUSTER_NAME, 3L, DEFAULT_BROKER_NAME, DEFAULT_IP[2], false, false); + + BrokerReplicasInfo.ReplicasInfo replicasInfo = getReplicasInfo(DEFAULT_BROKER_NAME); + assertEquals(1L, replicasInfo.getMasterBrokerId().longValue()); + assertEquals(DEFAULT_IP[0], replicasInfo.getMasterAddress()); + assertEquals(1, replicasInfo.getMasterEpoch()); + assertEquals(1, replicasInfo.getSyncStateSetEpoch()); + assertEquals(1, replicasInfo.getInSyncReplicas().size()); + assertEquals(2, replicasInfo.getNotInSyncReplicas().size()); + } + + @Test + public void testRegisterWithOldMasterInactive() { + mockMetaData(); + // If now only broker-3 alive, it will be elected to be a new master + brokerElectMaster(DEFAULT_CLUSTER_NAME, 3L, DEFAULT_BROKER_NAME, DEFAULT_IP[2], false, true, (a, b, c) -> c.equals(3L)); + + // Check in statemachine + BrokerReplicasInfo.ReplicasInfo replicasInfo = getReplicasInfo(DEFAULT_BROKER_NAME); + assertEquals(3L, replicasInfo.getMasterBrokerId().longValue()); + assertEquals(DEFAULT_IP[2], replicasInfo.getMasterAddress()); + assertEquals(2, replicasInfo.getMasterEpoch()); + assertEquals(3, replicasInfo.getSyncStateSetEpoch()); + assertEquals(1, replicasInfo.getInSyncReplicas().size()); + assertEquals(2, replicasInfo.getNotInSyncReplicas().size()); + } + + @Test + public void testElectMasterOldMasterStillAlive() { + mockMetaData(); + final ElectMasterRequestHeader request = ElectMasterRequestHeader.ofControllerTrigger(DEFAULT_BROKER_NAME); + ElectPolicy electPolicy = new DefaultElectPolicy(this.heartbeatManager::isBrokerActive, this.heartbeatManager::getBrokerLiveInfo); + mockHeartbeatDataMasterStillAlive(); + final ControllerResult cResult = this.replicasInfoManager.electMaster(request, + electPolicy); + assertEquals(ResponseCode.CONTROLLER_MASTER_STILL_EXIST, cResult.getResponseCode()); + } + + @Test + public void testElectMasterPreferHigherEpoch() { + mockMetaData(); + final ElectMasterRequestHeader request = ElectMasterRequestHeader.ofControllerTrigger(DEFAULT_BROKER_NAME); + ElectPolicy electPolicy = new DefaultElectPolicy(this.heartbeatManager::isBrokerActive, this.heartbeatManager::getBrokerLiveInfo); + mockHeartbeatDataHigherEpoch(); + final ControllerResult cResult = this.replicasInfoManager.electMaster(request, + electPolicy); + final ElectMasterResponseHeader response = cResult.getResponse(); + assertEquals(DEFAULT_IP[1], response.getMasterAddress()); + assertEquals(2L, response.getMasterBrokerId().longValue()); + assertEquals(2, response.getMasterEpoch().intValue()); + } + + @Test + public void testElectMasterPreferHigherOffsetWhenEpochEquals() { + mockMetaData(); + final ElectMasterRequestHeader request = ElectMasterRequestHeader.ofControllerTrigger(DEFAULT_BROKER_NAME); + ElectPolicy electPolicy = new DefaultElectPolicy(this.heartbeatManager::isBrokerActive, this.heartbeatManager::getBrokerLiveInfo); + mockHeartbeatDataHigherOffset(); + final ControllerResult cResult = this.replicasInfoManager.electMaster(request, + electPolicy); + final ElectMasterResponseHeader response = cResult.getResponse(); + assertEquals(DEFAULT_IP[2], response.getMasterAddress()); + assertEquals(3L, response.getMasterBrokerId().longValue()); + assertEquals(2, response.getMasterEpoch().intValue()); + } + + @Test + public void testElectMasterPreferHigherPriorityWhenEpochAndOffsetEquals() { + mockMetaData(); + final ElectMasterRequestHeader request = new ElectMasterRequestHeader(DEFAULT_BROKER_NAME); + ElectPolicy electPolicy = new DefaultElectPolicy(this.heartbeatManager::isBrokerActive, this.heartbeatManager::getBrokerLiveInfo); + mockHeartbeatDataHigherPriority(); + final ControllerResult cResult = this.replicasInfoManager.electMaster(request, + electPolicy); + final ElectMasterResponseHeader response = cResult.getResponse(); + assertEquals(DEFAULT_IP[2], response.getMasterAddress()); + assertEquals(3L, response.getMasterBrokerId().longValue()); + assertEquals(2, response.getMasterEpoch().intValue()); + } + + @Test + public void testElectMaster() { + mockMetaData(); + final ElectMasterRequestHeader request = ElectMasterRequestHeader.ofControllerTrigger(DEFAULT_BROKER_NAME); + final ControllerResult cResult = this.replicasInfoManager.electMaster(request, + new DefaultElectPolicy((cluster, brokerName, brokerId) -> !brokerId.equals(1L), null)); + final ElectMasterResponseHeader response = cResult.getResponse(); + assertEquals(2, response.getMasterEpoch().intValue()); + assertNotEquals(1L, response.getMasterBrokerId().longValue()); + assertNotEquals(DEFAULT_IP[0], response.getMasterAddress()); + apply(cResult.getEvents()); + + final Set brokerSet = new HashSet<>(); + brokerSet.add(1L); + brokerSet.add(2L); + brokerSet.add(3L); + assertTrue(alterNewInSyncSet(DEFAULT_BROKER_NAME, response.getMasterBrokerId(), response.getMasterEpoch(), brokerSet, response.getSyncStateSetEpoch())); + + // test admin try to elect a assignedMaster, but it isn't alive + final ElectMasterRequestHeader assignRequest = ElectMasterRequestHeader.ofAdminTrigger(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, 1L); + final ControllerResult cResult1 = this.replicasInfoManager.electMaster(assignRequest, + new DefaultElectPolicy((cluster, brokerName, brokerId) -> !brokerId.equals(1L), null)); + + assertEquals(cResult1.getResponseCode(), ResponseCode.CONTROLLER_ELECT_MASTER_FAILED); + + // test admin try to elect a assignedMaster but old master still alive, and the old master is equals to assignedMaster + final ElectMasterRequestHeader assignRequest1 = ElectMasterRequestHeader.ofAdminTrigger(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, response.getMasterBrokerId()); + final ControllerResult cResult2 = this.replicasInfoManager.electMaster(assignRequest1, + new DefaultElectPolicy((cluster, brokerName, brokerId) -> true, null)); + assertEquals(cResult2.getResponseCode(), ResponseCode.CONTROLLER_MASTER_STILL_EXIST); + + // admin successful elect a assignedMaster. + final ElectMasterRequestHeader assignRequest2 = ElectMasterRequestHeader.ofAdminTrigger(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, 1L); + final ControllerResult cResult3 = this.replicasInfoManager.electMaster(assignRequest2, + new DefaultElectPolicy((cluster, brokerName, brokerId) -> !brokerId.equals(response.getMasterBrokerId()), null)); + assertEquals(cResult3.getResponseCode(), ResponseCode.SUCCESS); + + final ElectMasterResponseHeader response3 = cResult3.getResponse(); + assertEquals(1L, response3.getMasterBrokerId().longValue()); + assertEquals(DEFAULT_IP[0], response3.getMasterAddress()); + assertEquals(3, response3.getMasterEpoch().intValue()); + } + + @Test + public void testAllReplicasShutdownAndRestart() { + mockMetaData(); + final HashSet newSyncStateSet = new HashSet<>(); + newSyncStateSet.add(1L); + assertTrue(alterNewInSyncSet(DEFAULT_BROKER_NAME, 1L, 1, newSyncStateSet, 2)); + + // Now we trigger electMaster api, which means the old master is shutdown and want to elect a new master. + // However, the syncStateSet in statemachine is {DEFAULT_IP[0]}, not more replicas can be elected as master, it will be failed. + final ElectMasterRequestHeader electRequest = ElectMasterRequestHeader.ofControllerTrigger(DEFAULT_BROKER_NAME); + final ControllerResult cResult = this.replicasInfoManager.electMaster(electRequest, + new DefaultElectPolicy((cluster, brokerName, brokerId) -> !brokerId.equals(1L), null)); + final List events = cResult.getEvents(); + assertEquals(events.size(), 1); + final ElectMasterEvent event = (ElectMasterEvent) events.get(0); + assertFalse(event.getNewMasterElected()); + + apply(cResult.getEvents()); + + final GetReplicaInfoResponseHeader replicaInfo = this.replicasInfoManager.getReplicaInfo(new GetReplicaInfoRequestHeader(DEFAULT_BROKER_NAME)).getResponse(); + assertEquals(replicaInfo.getMasterAddress(), null); + assertEquals(2, replicaInfo.getMasterEpoch().intValue()); + } + + @Test + public void testCleanBrokerData() { + mockMetaData(); + CleanControllerBrokerDataRequestHeader header1 = new CleanControllerBrokerDataRequestHeader(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, "1"); + ControllerResult result1 = this.replicasInfoManager.cleanBrokerData(header1, (cluster, brokerName, brokerId) -> true); + assertEquals(ResponseCode.CONTROLLER_INVALID_CLEAN_BROKER_METADATA, result1.getResponseCode()); + + CleanControllerBrokerDataRequestHeader header2 = new CleanControllerBrokerDataRequestHeader(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, null); + ControllerResult result2 = this.replicasInfoManager.cleanBrokerData(header2, (cluster, brokerName, brokerId) -> true); + assertEquals(ResponseCode.CONTROLLER_INVALID_CLEAN_BROKER_METADATA, result2.getResponseCode()); + assertEquals("Broker broker-set-a is still alive, clean up failure", result2.getRemark()); + + CleanControllerBrokerDataRequestHeader header3 = new CleanControllerBrokerDataRequestHeader(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, "1"); + ControllerResult result3 = this.replicasInfoManager.cleanBrokerData(header3, (cluster, brokerName, brokerId) -> false); + assertEquals(ResponseCode.SUCCESS, result3.getResponseCode()); + + CleanControllerBrokerDataRequestHeader header4 = new CleanControllerBrokerDataRequestHeader(DEFAULT_CLUSTER_NAME, DEFAULT_BROKER_NAME, "1;2;3"); + ControllerResult result4 = this.replicasInfoManager.cleanBrokerData(header4, (cluster, brokerName, brokerId) -> false); + assertEquals(ResponseCode.SUCCESS, result4.getResponseCode()); + + CleanControllerBrokerDataRequestHeader header5 = new CleanControllerBrokerDataRequestHeader(DEFAULT_CLUSTER_NAME, "broker12", "1;2;3", true); + ControllerResult result5 = this.replicasInfoManager.cleanBrokerData(header5, (cluster, brokerName, brokerId) -> false); + assertEquals(ResponseCode.CONTROLLER_INVALID_CLEAN_BROKER_METADATA, result5.getResponseCode()); + assertEquals("Broker broker12 is not existed,clean broker data failure.", result5.getRemark()); + + CleanControllerBrokerDataRequestHeader header6 = new CleanControllerBrokerDataRequestHeader(null, "broker12", "1;2;3", true); + ControllerResult result6 = this.replicasInfoManager.cleanBrokerData(header6, (cluster, brokerName, brokerId) -> cluster != null); + assertEquals(ResponseCode.CONTROLLER_INVALID_CLEAN_BROKER_METADATA, result6.getResponseCode()); + + CleanControllerBrokerDataRequestHeader header7 = new CleanControllerBrokerDataRequestHeader(null, DEFAULT_BROKER_NAME, "1;2;3", true); + ControllerResult result7 = this.replicasInfoManager.cleanBrokerData(header7, (cluster, brokerName, brokerId) -> false); + assertEquals(ResponseCode.SUCCESS, result7.getResponseCode()); + + } + + @Test + public void testSerialize() { + mockMetaData(); + byte[] data; + try { + data = this.replicasInfoManager.serialize(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + final ReplicasInfoManager newReplicasInfoManager = new ReplicasInfoManager(config); + try { + newReplicasInfoManager.deserializeFrom(data); + } catch (Throwable e) { + throw new RuntimeException(e); + } + Map oldReplicaInfoTable = new TreeMap<>(); + Map newReplicaInfoTable = new TreeMap<>(); + Map oldSyncStateTable = new TreeMap<>(); + Map newSyncStateTable = new TreeMap<>(); + try { + Field field = ReplicasInfoManager.class.getDeclaredField("replicaInfoTable"); + field.setAccessible(true); + oldReplicaInfoTable.putAll((Map) field.get(this.replicasInfoManager)); + newReplicaInfoTable.putAll((Map) field.get(newReplicasInfoManager)); + field = ReplicasInfoManager.class.getDeclaredField("syncStateSetInfoTable"); + field.setAccessible(true); + oldSyncStateTable.putAll((Map) field.get(this.replicasInfoManager)); + newSyncStateTable.putAll((Map) field.get(newReplicasInfoManager)); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + assertArrayEquals(oldReplicaInfoTable.keySet().toArray(), newReplicaInfoTable.keySet().toArray()); + assertArrayEquals(oldSyncStateTable.keySet().toArray(), newSyncStateTable.keySet().toArray()); + for (String brokerName : oldReplicaInfoTable.keySet()) { + BrokerReplicaInfo oldReplicaInfo = oldReplicaInfoTable.get(brokerName); + BrokerReplicaInfo newReplicaInfo = newReplicaInfoTable.get(brokerName); + Field[] fields = oldReplicaInfo.getClass().getFields(); + } + } +} diff --git a/controller/src/test/resources/rmq.logback-test.xml b/controller/src/test/resources/rmq.logback-test.xml new file mode 100644 index 00000000000..8695d52d57c --- /dev/null +++ b/controller/src/test/resources/rmq.logback-test.xml @@ -0,0 +1,36 @@ + + + + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + \ No newline at end of file diff --git a/dev/merge_rocketmq_pr.py b/dev/merge_rocketmq_pr.py index 849b0da2afe..981b2c22313 100644 --- a/dev/merge_rocketmq_pr.py +++ b/dev/merge_rocketmq_pr.py @@ -17,8 +17,8 @@ # limitations under the License. # -# This script is a modified version of the one created by the Spark -# project (https://github.com/apache/spark/blob/master/dev/merge_spark_pr.py). +# This script is a modified version of the one created by the RocketMQ +# project (https://github.com/apache/rocketmq/blob/master/dev/merge_rocketmq_pr.py). # Utility for creating well-formed pull request merges and pushing them to Apache. # usage: ./merge_rocketmq_pr.py (see config env vars below) @@ -448,4 +448,4 @@ def main(): main() except: clean_up() - raise \ No newline at end of file + raise diff --git a/distribution/LICENSE-BIN b/distribution/LICENSE-BIN index 372617233b3..bd431bfdea9 100644 --- a/distribution/LICENSE-BIN +++ b/distribution/LICENSE-BIN @@ -15,7 +15,7 @@ "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, - "control" means (properties) the power, direct or indirect, to cause the + "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. diff --git a/distribution/NOTICE-BIN b/distribution/NOTICE-BIN index c91dc225fb2..145520b5449 100644 --- a/distribution/NOTICE-BIN +++ b/distribution/NOTICE-BIN @@ -1,5 +1,5 @@ Apache RocketMQ -Copyright 2016-2017 The Apache Software Foundation +Copyright 2016-2022 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). @@ -33,4 +33,4 @@ components that this product depends on. ------ This product has a bundle commons-lang, which includes software from the Spring Framework, -under the Apache License 2.0 (see: StringUtils.containsWhitespace()) \ No newline at end of file +under the Apache License 2.0 (see: StringUtils.containsWhitespace()) diff --git a/distribution/benchmark/batchproducer.sh b/distribution/benchmark/batchproducer.sh new file mode 100644 index 00000000000..07297004e62 --- /dev/null +++ b/distribution/benchmark/batchproducer.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +sh ./runclass.sh org.apache.rocketmq.example.benchmark.BatchProducer $@ & diff --git a/distribution/benchmark/runclass.sh b/distribution/benchmark/runclass.sh index 13c58d198c1..885e2227513 100644 --- a/distribution/benchmark/runclass.sh +++ b/distribution/benchmark/runclass.sh @@ -22,17 +22,46 @@ then fi BASE_DIR=$(dirname $0)/.. -CLASSPATH=.:${BASE_DIR}/conf:${CLASSPATH} +CLASSPATH=.:${BASE_DIR}/conf:${BASE_DIR}/lib/*:${CLASSPATH} + +# The RAMDisk initializing size in MB on Darwin OS for gc-log +DIR_SIZE_IN_MB=600 + +choose_gc_log_directory() +{ + case "`uname`" in + Darwin) + if [ ! -d "/Volumes/RAMDisk" ]; then + # create ram disk on Darwin systems as gc-log directory + DEV=`hdiutil attach -nomount ram://$((2 * 1024 * DIR_SIZE_IN_MB))` > /dev/null + diskutil eraseVolume HFS+ RAMDisk ${DEV} > /dev/null + echo "Create RAMDisk /Volumes/RAMDisk for gc logging on Darwin OS." + fi + GC_LOG_DIR="/Volumes/RAMDisk" + ;; + *) + # check if /dev/shm exists on other systems + if [ -d "/dev/shm" ]; then + GC_LOG_DIR="/dev/shm" + else + GC_LOG_DIR=${BASE_DIR} + fi + ;; + esac +} + +choose_gc_log_directory JAVA_OPT="${JAVA_OPT} -server -Xms1g -Xmx1g -Xmn256m -XX:PermSize=128m -XX:MaxPermSize=320m" JAVA_OPT="${JAVA_OPT} -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSClassUnloadingEnabled -XX:SurvivorRatio=8 -XX:+DisableExplicitGC" -JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:/dev/shm/rmq_srv_gc.log -XX:+PrintGCDetails" +JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:${GC_LOG_DIR}/rmq_run_class_gc_%p_%t.log -XX:+PrintGCDetails" JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow" -JAVA_OPT="${JAVA_OPT} -Djava.ext.dirs=${BASE_DIR}/lib" -JAVA_OPT="${JAVA_OPT} -XX:-UseLargePages" -JAVA_OPT="${JAVA_OPT} -XX:+PerfDisableSharedMem" +JAVA_OPT="${JAVA_OPT} -XX:-UseLargePages" +JAVA_OPT="${JAVA_OPT} -XX:+PerfDisableSharedMem" #JAVA_OPT="${JAVA_OPT} -Xdebug -Xrunjdwp:transport=dt_socket,address=9555,server=y,suspend=n" JAVA_OPT="${JAVA_OPT} -cp ${CLASSPATH}" +JAVA_OPT="${JAVA_OPT} -Djava.ext.dirs=${JAVA_HOME}/jre/lib/ext:${BASE_DIR}/lib:${JAVA_HOME}/lib/ext" +JAVA_OPT="${JAVA_OPT} -Drmq.logback.configurationFile=${BASE_DIR}/conf/rmq.client.logback.xml" if [ -z "$JAVA_HOME" ]; then JAVA_HOME=/usr/java diff --git a/distribution/benchmark/shutdown.sh b/distribution/benchmark/shutdown.sh new file mode 100644 index 00000000000..2af7ef741d9 --- /dev/null +++ b/distribution/benchmark/shutdown.sh @@ -0,0 +1,77 @@ +#!/bin/sh + +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +case $1 in + producer) + + pid=`ps ax | grep -i 'org.apache.rocketmq.example.benchmark.Producer' |grep java | grep -v grep | awk '{print $1}'` + if [ -z "$pid" ] ; then + echo "No benchmark producer running." + exit -1; + fi + + echo "The benchmark producer(${pid}) is running..." + + kill ${pid} + + echo "Send shutdown request to benchmark producer(${pid}) OK" + ;; + consumer) + + pid=`ps ax | grep -i 'org.apache.rocketmq.example.benchmark.Consumer' |grep java | grep -v grep | awk '{print $1}'` + if [ -z "$pid" ] ; then + echo "No benchmark consumer running." + exit -1; + fi + + echo "The benchmark consumer(${pid}) is running..." + + kill ${pid} + + echo "Send shutdown request to benchmark consumer(${pid}) OK" + ;; + tproducer) + + pid=`ps ax | grep -i 'org.apache.rocketmq.example.benchmark.TransactionProducer' |grep java | grep -v grep | awk '{print $1}'` + if [ -z "$pid" ] ; then + echo "No benchmark transaction producer running." + exit -1; + fi + + echo "The benchmark transaction producer(${pid}) is running..." + + kill ${pid} + + echo "Send shutdown request to benchmark transaction producer(${pid}) OK" + ;; + bproducer) + + pid=`ps ax | grep -i 'org.apache.rocketmq.example.benchmark.BatchProducer' |grep java | grep -v grep | awk '{print $1}'` + if [ -z "$pid" ] ; then + echo "No benchmark batch producer running." + exit -1; + fi + + echo "The benchmark batch producer(${pid}) is running..." + + kill ${pid} + + echo "Send shutdown request to benchmark batch producer(${pid}) OK" + ;; + *) + echo "Usage: shutdown producer | consumer | tproducer | bproducer" +esac diff --git a/distribution/benchmark/tproducer.sh b/distribution/benchmark/tproducer.sh index ac4bbf3ee6d..9ced41f8324 100644 --- a/distribution/benchmark/tproducer.sh +++ b/distribution/benchmark/tproducer.sh @@ -15,4 +15,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -sh ./runclass.sh org.apache.rocketmq.example.benchmark.TransactionProducer $@ +sh ./runclass.sh org.apache.rocketmq.example.benchmark.TransactionProducer $@ & diff --git a/distribution/bin/controller/fast-try-independent-deployment.cmd b/distribution/bin/controller/fast-try-independent-deployment.cmd new file mode 100644 index 00000000000..debddef767a --- /dev/null +++ b/distribution/bin/controller/fast-try-independent-deployment.cmd @@ -0,0 +1,36 @@ +@echo off +rem Licensed to the Apache Software Foundation (ASF) under one or more +rem contributor license agreements. See the NOTICE file distributed with +rem this work for additional information regarding copyright ownership. +rem The ASF licenses this file to You under the Apache License, Version 2.0 +rem (the "License"); you may not use this file except in compliance with +rem the License. You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. + +set ROCKETMQ_HOME=%cd% +if not exist "%ROCKETMQ_HOME%\conf\controller\cluster-3n-independent\controller-n0.conf" echo Make sure the %ROCKETMQ_HOME%\conf\controller\cluster-3n-independent\controller-n0.conf exists & EXIT /B 1 +if not exist "%ROCKETMQ_HOME%\conf\controller\cluster-3n-independent\controller-n1.conf" echo Make sure the %ROCKETMQ_HOME%\conf\controller\cluster-3n-independent\controller-n1.conf exists & EXIT /B 1 +if not exist "%ROCKETMQ_HOME%\conf\controller\cluster-3n-independent\controller-n2.conf" echo Make sure the %ROCKETMQ_HOME%\conf\controller\cluster-3n-independent\controller-n2.conf exists & EXIT /B 1 + +set "JAVA_OPT_EXT= -server -Xms512m -Xmx512m" +start call "%ROCKETMQ_HOME%\bin\runserver.cmd" org.apache.rocketmq.controller.ControllerStartup -c %ROCKETMQ_HOME%\conf\controller\cluster-3n-independent\controller-n0.conf +IF %ERRORLEVEL% EQU 0 ( + ECHO "Controller start OK" +) +timeout /T 3 /NOBREAK +start call "%ROCKETMQ_HOME%\bin\runserver.cmd" org.apache.rocketmq.controller.ControllerStartup -c %ROCKETMQ_HOME%\conf\controller\cluster-3n-independent\controller-n1.conf +IF %ERRORLEVEL% EQU 0 ( + ECHO "Controller start OK" +) +timeout /T 3 /NOBREAK +start call "%ROCKETMQ_HOME%\bin\runserver.cmd" org.apache.rocketmq.controller.ControllerStartup -c %ROCKETMQ_HOME%\conf\controller\cluster-3n-independent\controller-n2.conf +IF %ERRORLEVEL% EQU 0 ( + ECHO "Controller start OK" +) diff --git a/distribution/bin/controller/fast-try-independent-deployment.sh b/distribution/bin/controller/fast-try-independent-deployment.sh new file mode 100644 index 00000000000..7aa52d51900 --- /dev/null +++ b/distribution/bin/controller/fast-try-independent-deployment.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash + +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## Revise the base dir +CURRENT_DIR="$(cd "$(dirname "$0")"; pwd)" +RMQ_DIR=$CURRENT_DIR/../.. +cd $RMQ_DIR + +startController() { + export JAVA_OPT_EXT=" -Xms512m -Xmx512m " + conf_name=$1 + nohup bin/mqcontroller -c $conf_name & +} + +stopController() { + PIDS=$(ps -ef|grep java|grep ControllerStartup|grep -v grep|awk '{print $2}') + if [ ! -z "$PIDS" ]; then + kill -s TERM $PIDS + fi +} + +stopAll() { + stopController +} + +startAll() { + startController ./conf/controller/cluster-3n-independent/controller-n0.conf + startController ./conf/controller/cluster-3n-independent/controller-n1.conf + startController ./conf/controller/cluster-3n-independent/controller-n2.conf +} + +checkConf() { + if [ ! -f ./conf/controller/cluster-3n-independent/controller-n0.conf -o ! -f ./conf/controller/cluster-3n-independent/controller-n1.conf -o ! -f ./conf/controller/cluster-3n-independent/controller-n2.conf ]; then + echo "Make sure the ./conf/controller/cluster-3n-independent/controller-n0.conf, ./conf/controller/cluster-3n-independent/controller-n1.conf, ./conf/controller/cluster-3n-independent/controller-n2.conf exists" + exit 1 + fi +} + + + +## Main +if [ $# -lt 1 ]; then + echo "Usage: sh $0 start|stop" + exit 1 +fi +action=$1 +checkConf +case $action in + "start") + startAll + exit + ;; + "stop") + stopAll + exit + ;; + *) + echo "Usage: sh $0 start|stop" + exit + ;; +esac + diff --git a/distribution/bin/controller/fast-try-namesrv-plugin.cmd b/distribution/bin/controller/fast-try-namesrv-plugin.cmd new file mode 100644 index 00000000000..6633d3ac4b0 --- /dev/null +++ b/distribution/bin/controller/fast-try-namesrv-plugin.cmd @@ -0,0 +1,36 @@ +@echo off +rem Licensed to the Apache Software Foundation (ASF) under one or more +rem contributor license agreements. See the NOTICE file distributed with +rem this work for additional information regarding copyright ownership. +rem The ASF licenses this file to You under the Apache License, Version 2.0 +rem (the "License"); you may not use this file except in compliance with +rem the License. You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. + +set ROCKETMQ_HOME=%cd% +if not exist "%ROCKETMQ_HOME%\conf\controller\cluster-3n-namesrv-plugin\namesrv-n0.conf" echo Make sure the %ROCKETMQ_HOME%\conf\controller\cluster-3n-independent\controller-n0.conf exists & EXIT /B 1 +if not exist "%ROCKETMQ_HOME%\conf\controller\cluster-3n-namesrv-plugin\namesrv-n1.conf" echo Make sure the %ROCKETMQ_HOME%\conf\controller\cluster-3n-independent\controller-n1.conf exists & EXIT /B 1 +if not exist "%ROCKETMQ_HOME%\conf\controller\cluster-3n-namesrv-plugin\namesrv-n2.conf" echo Make sure the %ROCKETMQ_HOME%\conf\controller\cluster-3n-independent\controller-n2.conf exists & EXIT /B 1 + +set "JAVA_OPT_EXT= -server -Xms512m -Xmx512m" +start call "%ROCKETMQ_HOME%\bin\runserver.cmd" org.apache.rocketmq.namesrv.NamesrvStartup -c %ROCKETMQ_HOME%\conf\controller\cluster-3n-namesrv-plugin\namesrv-n0.conf +IF %ERRORLEVEL% EQU 0 ( + ECHO "Controller start OK" +) +timeout /T 3 /NOBREAK +start call "%ROCKETMQ_HOME%\bin\runserver.cmd" org.apache.rocketmq.namesrv.NamesrvStartup -c %ROCKETMQ_HOME%\conf\controller\cluster-3n-namesrv-plugin\namesrv-n1.conf +IF %ERRORLEVEL% EQU 0 ( + ECHO "Controller start OK" +) +timeout /T 3 /NOBREAK +start call "%ROCKETMQ_HOME%\bin\runserver.cmd" org.apache.rocketmq.namesrv.NamesrvStartup -c %ROCKETMQ_HOME%\conf\controller\cluster-3n-namesrv-plugin\namesrv-n2.conf +IF %ERRORLEVEL% EQU 0 ( + ECHO "Controller start OK" +) \ No newline at end of file diff --git a/distribution/bin/controller/fast-try-namesrv-plugin.sh b/distribution/bin/controller/fast-try-namesrv-plugin.sh new file mode 100644 index 00000000000..a349153cc11 --- /dev/null +++ b/distribution/bin/controller/fast-try-namesrv-plugin.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash + +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## Revise the base dir +CURRENT_DIR="$(cd "$(dirname "$0")"; pwd)" +RMQ_DIR=$CURRENT_DIR/../.. +cd $RMQ_DIR + +startNameserver() { + export JAVA_OPT_EXT=" -Xms512m -Xmx512m " + conf_name=$1 + nohup bin/mqnamesrv -c $conf_name & +} + +stopNameserver() { + PIDS=$(ps -ef|grep java|grep NamesrvStartup|grep -v grep|awk '{print $2}') + if [ ! -z "$PIDS" ]; then + kill -s TERM $PIDS + fi +} + +stopAll() { + stopNameserver +} + +startAll() { + startNameserver ./conf/controller/cluster-3n-namesrv-plugin/namesrv-n0.conf + startNameserver ./conf/controller/cluster-3n-namesrv-plugin/namesrv-n1.conf + startNameserver ./conf/controller/cluster-3n-namesrv-plugin/namesrv-n2.conf +} + +checkConf() { + if [ ! -f ./conf/controller/cluster-3n-namesrv-plugin/namesrv-n0.conf -o ! -f ./conf/controller/cluster-3n-namesrv-plugin/namesrv-n1.conf -o ! -f ./conf/controller/cluster-3n-namesrv-plugin/namesrv-n2.conf ]; then + echo "Make sure the ./conf/controller/cluster-3n-namesrv-plugin/namesrv-n0.conf, ./conf/controller/cluster-3n-namesrv-plugin/namesrv-n1.conf, ./conf/controller/cluster-3n-namesrv-plugin/namesrv-n2.conf exists" + exit 1 + fi +} + + + +## Main +if [ $# -lt 1 ]; then + echo "Usage: sh $0 start|stop" + exit 1 +fi +action=$1 +checkConf +case $action in + "start") + startAll + exit + ;; + "stop") + stopAll + exit + ;; + *) + echo "Usage: sh $0 start|stop" + exit + ;; +esac + diff --git a/distribution/bin/controller/fast-try.cmd b/distribution/bin/controller/fast-try.cmd new file mode 100644 index 00000000000..a32ed61ad3c --- /dev/null +++ b/distribution/bin/controller/fast-try.cmd @@ -0,0 +1,37 @@ +@echo off +rem Licensed to the Apache Software Foundation (ASF) under one or more +rem contributor license agreements. See the NOTICE file distributed with +rem this work for additional information regarding copyright ownership. +rem The ASF licenses this file to You under the Apache License, Version 2.0 +rem (the "License"); you may not use this file except in compliance with +rem the License. You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. + +set ROCKETMQ_HOME=%cd% +if not exist "%ROCKETMQ_HOME%\conf\controller\quick-start\namesrv.conf" echo Make sure the %ROCKETMQ_HOME%\conf\controller\quick-start\namesrv.conf exists & EXIT /B 1 +if not exist "%ROCKETMQ_HOME%\conf\controller\quick-start\broker-n0.conf" echo Make sure the %ROCKETMQ_HOME%\conf\controller\quick-start\broker-n0.conf exists & EXIT /B 1 +if not exist "%ROCKETMQ_HOME%\conf\controller\quick-start\broker-n1.conf" echo Make sure the %ROCKETMQ_HOME%\conf\controller\quick-start\broker-n1.conf exists & EXIT /B 1 + +set "JAVA_OPT_EXT= -server -Xms512m -Xmx512m" +start call "%ROCKETMQ_HOME%\bin\runserver.cmd" org.apache.rocketmq.namesrv.NamesrvStartup -c %ROCKETMQ_HOME%\conf\controller\quick-start\namesrv.conf +IF %ERRORLEVEL% EQU 0 ( + ECHO "Namesrv start OK" +) +timeout /T 3 /NOBREAK + +set "JAVA_OPT_EXT= -server -Xms1g -Xmx1g" +start call "%ROCKETMQ_HOME%\bin\runbroker.cmd" org.apache.rocketmq.broker.BrokerStartup -c %ROCKETMQ_HOME%\conf\controller\quick-start\broker-n0.conf +timeout /T 1 /NOBREAK +start call "%ROCKETMQ_HOME%\bin\runbroker.cmd" org.apache.rocketmq.broker.BrokerStartup -c %ROCKETMQ_HOME%\conf\controller\quick-start\broker-n1.conf +timeout /T 1 /NOBREAK + +IF %ERRORLEVEL% EQU 0 ( + ECHO "Broker starts OK" +) \ No newline at end of file diff --git a/distribution/bin/controller/fast-try.sh b/distribution/bin/controller/fast-try.sh new file mode 100644 index 00000000000..211a4ff8a47 --- /dev/null +++ b/distribution/bin/controller/fast-try.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash + +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## Revise the base dir +CURRENT_DIR="$(cd "$(dirname "$0")"; pwd)" +RMQ_DIR=$CURRENT_DIR/../.. +cd $RMQ_DIR + +startNameserver() { + export JAVA_OPT_EXT=" -Xms512m -Xmx512m " + conf_name=$1 + nohup bin/mqnamesrv -c $conf_name & +} + +startBroker() { + export JAVA_OPT_EXT=" -Xms1g -Xmx1g " + conf_name=$1 + nohup bin/mqbroker -c $conf_name & +} + +stopNameserver() { + PIDS=$(ps -ef|grep java|grep NamesrvStartup|grep -v grep|awk '{print $2}') + if [ ! -z "$PIDS" ]; then + kill -s TERM $PIDS + fi +} + +stopBroker() { + conf_name=$1 + PIDS=$(ps -ef|grep java|grep BrokerStartup|grep $conf_name|grep -v grep|awk '{print $2}') + i=1 + while [ ! -z "$PIDS" -a $i -lt 5 ] + do + echo "Waiting to kill ..." + kill -s TERM $PIDS + i=`expr $i + 1` + sleep 2 + PIDS=$(ps -ef|grep java|grep BrokerStartup|grep $conf_name|grep -v grep|awk '{print $2}') + done + PIDS=$(ps -ef|grep java|grep BrokerStartup|grep $conf_name|grep -v grep|awk '{print $2}') + if [ ! -z "$PIDS" ]; then + kill -9 $PIDS + fi +} + +stopAll() { + ps -ef|grep java|grep BrokerStartup|grep -v grep|awk '{print $2}'|xargs kill + stopNameserver + stopBroker ./conf/controller/quick-start/broker-n0.conf + stopBroker ./conf/controller/quick-start/broker-n1.conf +} + +startAll() { + startNameserver ./conf/controller/quick-start/namesrv.conf + startBroker ./conf/controller/quick-start/broker-n0.conf + startBroker ./conf/controller/quick-start/broker-n1.conf +} + +checkConf() { + if [ ! -f ./conf/controller/quick-start/broker-n0.conf -o ! -f ./conf/controller/quick-start/broker-n1.conf -o ! -f ./conf/controller/quick-start/namesrv.conf ]; then + echo "Make sure the ./conf/controller/quick-start/broker-n0.conf, ./conf/controller/quick-start/broker-n1.conf, ./conf/controller/quick-start/namesrv.conf exists" + exit 1 + fi +} + + + +## Main +if [ $# -lt 1 ]; then + echo "Usage: sh $0 start|stop" + exit 1 +fi +action=$1 +checkConf +case $action in + "start") + startAll + exit + ;; + "stop") + stopAll + ;; + *) + echo "Usage: sh $0 start|stop" + ;; +esac + diff --git a/distribution/bin/dledger/fast-try.sh b/distribution/bin/dledger/fast-try.sh new file mode 100644 index 00000000000..acdde71b8df --- /dev/null +++ b/distribution/bin/dledger/fast-try.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash + +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## Revise the base dir +CURRENT_DIR="$(cd "$(dirname "$0")"; pwd)" +RMQ_DIR=$CURRENT_DIR/../.. +cd $RMQ_DIR + +startNameserver() { + export JAVA_OPT_EXT=" -Xms512m -Xmx512m " + nohup bin/mqnamesrv & +} + +startBroker() { + export JAVA_OPT_EXT=" -Xms1g -Xmx1g " + conf_name=$1 + nohup bin/mqbroker -c $conf_name & +} + +stopNameserver() { + PIDS=$(ps -ef|grep java|grep NamesrvStartup|grep -v grep|awk '{print $2}') + if [ ! -z "$PIDS" ]; then + kill -s TERM $PIDS + fi +} + +stopBroker() { + conf_name=$1 + PIDS=$(ps -ef|grep java|grep BrokerStartup|grep $conf_name|grep -v grep|awk '{print $2}') + i=1 + while [ ! -z "$PIDS" -a $i -lt 5 ] + do + echo "Waiting to kill ..." + kill -s TERM $PIDS + i=`expr $i + 1` + sleep 2 + PIDS=$(ps -ef|grep java|grep BrokerStartup|grep $conf_name|grep -v grep|awk '{print $2}') + done + PIDS=$(ps -ef|grep java|grep BrokerStartup|grep $conf_name|grep -v grep|awk '{print $2}') + if [ ! -z "$PIDS" ]; then + kill -9 $PIDS + fi +} + +stopAll() { + ps -ef|grep java|grep BrokerStartup|grep -v grep|awk '{print $2}'|xargs kill + stopNameserver + stopBroker ./conf/dledger/broker-n0.conf + stopBroker ./conf/dledger/broker-n1.conf + stopBroker ./conf/dledger/broker-n2.conf +} + +startAll() { + startNameserver + startBroker ./conf/dledger/broker-n0.conf + startBroker ./conf/dledger/broker-n1.conf + startBroker ./conf/dledger/broker-n2.conf +} + +checkConf() { + if [ ! -f ./conf/dledger/broker-n0.conf -o ! -f ./conf/dledger/broker-n1.conf -o ! -f ./conf/dledger/broker-n2.conf ]; then + echo "Make sure the ./conf/dledger/broker-n0.conf, ./conf/dledger/broker-n1.conf, ./conf/dledger/broker-n2.conf exists" + exit 1 + fi +} + + + +## Main +if [ $# -lt 1 ]; then + echo "Usage: sh $0 start|stop" + exit 1 +fi +action=$1 +checkConf +case $action in + "start") + startAll + exit + ;; + "stop") + stopAll + ;; + *) + echo "Usage: sh $0 start|stop" + ;; +esac + diff --git a/distribution/bin/export.sh b/distribution/bin/export.sh new file mode 100644 index 00000000000..2b323e8b21d --- /dev/null +++ b/distribution/bin/export.sh @@ -0,0 +1,89 @@ +#!/bin/bash + +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +if [ -z "$ROCKETMQ_HOME" ]; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ]; do + ls=$(ls -ld "$PRG") + link=$(expr "$ls" : '.*-> \(.*\)$') + if expr "$link" : '/.*' >/dev/null; then + PRG="$link" + else + PRG="$(dirname "$PRG")/$link" + fi + done + + saveddir=$(pwd) + + ROCKETMQ_HOME=$(dirname "$PRG")/.. + + # make it fully qualified + ROCKETMQ_HOME=$(cd "$ROCKETMQ_HOME" && pwd) + + cd "$saveddir" +fi + +export ROCKETMQ_HOME + +namesrvAddr= +while [ -z "${namesrvAddr}" ]; do + read -p "Enter name server address list:" namesrvAddr +done + +clusterName= +while [ -z "${clusterName}" ]; do + read -p "Choose a cluster to export:" clusterName +done + +read -p "Enter file path to export [default /tmp/rocketmq/export]:" filePath +if [ -z "${filePath}" ]; then + filePath="/tmp/rocketmq/config" +fi + +if [[ -e ${filePath} ]]; then + rm -rf ${filePath} +fi + +sh ${ROCKETMQ_HOME}/bin/mqadmin exportMetrics -c ${clusterName} -n ${namesrvAddr} -f ${filePath} +sh ${ROCKETMQ_HOME}/bin/mqadmin exportConfigs -c ${clusterName} -n ${namesrvAddr} -f ${filePath} +sh ${ROCKETMQ_HOME}/bin/mqadmin exportMetadata -c ${clusterName} -n ${namesrvAddr} -f ${filePath} + +cd ${filePath} || exit + +configs=$(cat ./configs.json) +if [ -z "$configs" ]; then + configs="{}" +fi +metadata=$(cat ./metadata.json) +if [ -z "$metadata" ]; then + metadata="{}" +fi +metrics=$(cat ./metrics.json) +if [ -z "$metrics" ]; then + metrics="{}" +fi + +echo "{ + \"configs\": ${configs}, + \"metadata\": ${metadata}, + \"metrics\": ${metrics} + }" >rocketmq-metadata-export.json + +echo -e "[INFO] The RocketMQ metadata has been exported to the file:${filePath}/rocketmq-metadata-export.json" diff --git a/distribution/bin/mqadmin b/distribution/bin/mqadmin index cd0253bc87d..489da8b7659 100644 --- a/distribution/bin/mqadmin +++ b/distribution/bin/mqadmin @@ -42,4 +42,4 @@ fi export ROCKETMQ_HOME -sh ${ROCKETMQ_HOME}/bin/tools.sh org.apache.rocketmq.tools.command.MQAdminStartup $@ +sh ${ROCKETMQ_HOME}/bin/tools.sh -Drmq.logback.configurationFile=$ROCKETMQ_HOME/conf/rmq.tools.logback.xml org.apache.rocketmq.tools.command.MQAdminStartup "$@" diff --git a/distribution/bin/mqadmin.cmd b/distribution/bin/mqadmin.cmd index 4e061f0ef93..a28facb1ff5 100644 --- a/distribution/bin/mqadmin.cmd +++ b/distribution/bin/mqadmin.cmd @@ -15,4 +15,4 @@ rem See the License for the specific language governing permissions and rem limitations under the License. if not exist "%ROCKETMQ_HOME%\bin\tools.cmd" echo Please set the ROCKETMQ_HOME variable in your environment! & EXIT /B 1 -call "%ROCKETMQ_HOME%\bin\tools.cmd" org.apache.rocketmq.tools.command.MQAdminStartup %* \ No newline at end of file +call "%ROCKETMQ_HOME%\bin\tools.cmd" -Drmq.logback.configurationFile=%ROCKETMQ_HOME%\conf\rmq.tools.logback.xml org.apache.rocketmq.tools.command.MQAdminStartup %* \ No newline at end of file diff --git a/distribution/bin/mqadmin.xml b/distribution/bin/mqadmin.xml deleted file mode 100644 index 0f07da414e2..00000000000 --- a/distribution/bin/mqadmin.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - false - - ${JAVA_HOME} - - server - - org.apache.rocketmq.tools.command.MQAdminStartup - - - ${cpd}/../lib - ${cpd}/.. - - - - - - - <-Xms512m> - <-Xmx1g> - <-XX:NewSize>256M - <-XX:MaxNewSize>512M - <-XX:PermSize>128M - <-XX:MaxPermSize>128M - - diff --git a/distribution/bin/mqbroker b/distribution/bin/mqbroker index 6a79c392e8d..35eb93c4461 100644 --- a/distribution/bin/mqbroker +++ b/distribution/bin/mqbroker @@ -42,4 +42,37 @@ fi export ROCKETMQ_HOME -sh ${ROCKETMQ_HOME}/bin/runbroker.sh org.apache.rocketmq.broker.BrokerStartup $@ +other_args=" " +enable_proxy=false + +while [ $# -gt 0 ]; do + case $1 in + --enable-proxy) + enable_proxy=true + shift + ;; + -c|--configFile) + broker_config="$2" + shift + shift + ;; + *) + other_args=${other_args}" "${1} + shift + ;; + esac +done + +if [ "$enable_proxy" = true ]; then + args_for_proxy=$other_args" -pm local" + if [ "$broker_config" != "" ]; then + args_for_proxy=${args_for_proxy}" -bc "${broker_config} + fi + sh ${ROCKETMQ_HOME}/bin/runbroker.sh -Drmq.logback.configurationFile=$ROCKETMQ_HOME/conf/rmq.proxy.logback.xml org.apache.rocketmq.proxy.ProxyStartup ${args_for_proxy} +else + args_for_broker=$other_args + if [ "$broker_config" != "" ]; then + args_for_broker=${args_for_broker}" -c "${broker_config} + fi + sh ${ROCKETMQ_HOME}/bin/runbroker.sh -Drmq.logback.configurationFile=$ROCKETMQ_HOME/conf/rmq.broker.logback.xml org.apache.rocketmq.broker.BrokerStartup ${args_for_broker} +fi diff --git a/distribution/bin/mqbroker.cmd b/distribution/bin/mqbroker.cmd index 3efb47577cc..644e217a2f0 100644 --- a/distribution/bin/mqbroker.cmd +++ b/distribution/bin/mqbroker.cmd @@ -16,7 +16,7 @@ rem limitations under the License. if not exist "%ROCKETMQ_HOME%\bin\runbroker.cmd" echo Please set the ROCKETMQ_HOME variable in your environment! & EXIT /B 1 -call "%ROCKETMQ_HOME%\bin\runbroker.cmd" org.apache.rocketmq.broker.BrokerStartup %* +call "%ROCKETMQ_HOME%\bin\runbroker.cmd" -Drmq.logback.configurationFile=%ROCKETMQ_HOME%\conf\rmq.broker.logback.xml org.apache.rocketmq.broker.BrokerStartup %* IF %ERRORLEVEL% EQU 0 ( ECHO "Broker starts OK" diff --git a/distribution/bin/mqbroker.xml b/distribution/bin/mqbroker.xml deleted file mode 100644 index 3043cc0f8d0..00000000000 --- a/distribution/bin/mqbroker.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - false - - ${JAVA_HOME} - - server - - org.apache.rocketmq.broker.BrokerStartup - - - ${cpd}/../lib - ${cpd}/.. - - - - - - - <-Xms512m> - - <-Xmx1g> - -<-XX:NewSize>256M -<-XX:MaxNewSize>512M -<-XX:PermSize>128M -<-XX:MaxPermSize>128M - - diff --git a/distribution/bin/mqbrokercontainer b/distribution/bin/mqbrokercontainer new file mode 100644 index 00000000000..0ce383f0219 --- /dev/null +++ b/distribution/bin/mqbrokercontainer @@ -0,0 +1,45 @@ +#!/bin/sh + +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +if [ -z "$ROCKETMQ_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + ROCKETMQ_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + ROCKETMQ_HOME=`cd "$ROCKETMQ_HOME" && pwd` + + cd "$saveddir" +fi + +export ROCKETMQ_HOME + +sh ${ROCKETMQ_HOME}/bin/runbroker.sh org.apache.rocketmq.container.BrokerContainerStartup $@ diff --git a/distribution/bin/mqcontroller b/distribution/bin/mqcontroller new file mode 100644 index 00000000000..5ac064d43e3 --- /dev/null +++ b/distribution/bin/mqcontroller @@ -0,0 +1,45 @@ +#!/bin/sh + +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +if [ -z "$ROCKETMQ_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + ROCKETMQ_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + ROCKETMQ_HOME=`cd "$ROCKETMQ_HOME" && pwd` + + cd "$saveddir" +fi + +export ROCKETMQ_HOME + +sh ${ROCKETMQ_HOME}/bin/runserver.sh -Drmq.logback.configurationFile=$ROCKETMQ_HOME/conf/rmq.controller.logback.xml org.apache.rocketmq.controller.ControllerStartup $@ diff --git a/distribution/bin/mqcontroller.cmd b/distribution/bin/mqcontroller.cmd new file mode 100644 index 00000000000..95fae9724dd --- /dev/null +++ b/distribution/bin/mqcontroller.cmd @@ -0,0 +1,23 @@ +@echo off +rem Licensed to the Apache Software Foundation (ASF) under one or more +rem contributor license agreements. See the NOTICE file distributed with +rem this work for additional information regarding copyright ownership. +rem The ASF licenses this file to You under the Apache License, Version 2.0 +rem (the "License"); you may not use this file except in compliance with +rem the License. You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. + +if not exist "%ROCKETMQ_HOME%\bin\runserver.cmd" echo Please set the ROCKETMQ_HOME variable in your environment! & EXIT /B 1 + +call "%ROCKETMQ_HOME%\bin\runserver.cmd" -Drmq.logback.configurationFile=%ROCKETMQ_HOME%\conf\rmq.controller.logback.xml org.apache.rocketmq.controller.ControllerStartup %* + +IF %ERRORLEVEL% EQU 0 ( + ECHO "Controller starts OK" +) \ No newline at end of file diff --git a/distribution/bin/mqfiltersrv b/distribution/bin/mqfiltersrv deleted file mode 100644 index 2fd0cbea8c5..00000000000 --- a/distribution/bin/mqfiltersrv +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/sh - -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -if [ -z "$ROCKETMQ_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - ROCKETMQ_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - ROCKETMQ_HOME=`cd "$ROCKETMQ_HOME" && pwd` - - cd "$saveddir" -fi - -export ROCKETMQ_HOME - -sh ${ROCKETMQ_HOME}/bin/runserver.sh org.apache.rocketmq.filtersrv.FiltersrvStartup $@ diff --git a/distribution/bin/mqfiltersrv.cmd b/distribution/bin/mqfiltersrv.cmd deleted file mode 100644 index 0503026a397..00000000000 --- a/distribution/bin/mqfiltersrv.cmd +++ /dev/null @@ -1,23 +0,0 @@ -@echo off -rem Licensed to the Apache Software Foundation (ASF) under one or more -rem contributor license agreements. See the NOTICE file distributed with -rem this work for additional information regarding copyright ownership. -rem The ASF licenses this file to You under the Apache License, Version 2.0 -rem (the "License"); you may not use this file except in compliance with -rem the License. You may obtain a copy of the License at -rem -rem http://www.apache.org/licenses/LICENSE-2.0 -rem -rem Unless required by applicable law or agreed to in writing, software -rem distributed under the License is distributed on an "AS IS" BASIS, -rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -rem See the License for the specific language governing permissions and -rem limitations under the License. - -if not exist "%ROCKETMQ_HOME%\bin\runbroker.cmd" echo Please set the ROCKETMQ_HOME variable in your environment! & EXIT /B 1 - -call "%ROCKETMQ_HOME%\bin\runserver.cmd" org.apache.rocketmq.filtersrv.FiltersrvStartup %* - -IF %ERRORLEVEL% EQU 0 ( - ECHO "Filtersrv starts OK" -) \ No newline at end of file diff --git a/distribution/bin/mqfiltersrv.xml b/distribution/bin/mqfiltersrv.xml deleted file mode 100644 index dc36a8d8c1e..00000000000 --- a/distribution/bin/mqfiltersrv.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - false - - ${JAVA_HOME} - - server - - org.apache.rocketmq.filtersrv.FiltersrvStartup - - - ${cpd}/../lib - ${cpd}/.. - - - - - - - <-Xms512m> - - <-Xmx1g> - -<-XX:NewSize>256M -<-XX:MaxNewSize>512M -<-XX:PermSize>128M -<-XX:MaxPermSize>128M - - diff --git a/distribution/bin/mqnamesrv b/distribution/bin/mqnamesrv index c1e70bde8df..6741c7f00b8 100644 --- a/distribution/bin/mqnamesrv +++ b/distribution/bin/mqnamesrv @@ -42,4 +42,4 @@ fi export ROCKETMQ_HOME -sh ${ROCKETMQ_HOME}/bin/runserver.sh org.apache.rocketmq.namesrv.NamesrvStartup $@ +sh ${ROCKETMQ_HOME}/bin/runserver.sh -Drmq.logback.configurationFile=$ROCKETMQ_HOME/conf/rmq.namesrv.logback.xml org.apache.rocketmq.namesrv.NamesrvStartup $@ diff --git a/distribution/bin/mqnamesrv.cmd b/distribution/bin/mqnamesrv.cmd index 2828bdc28d0..97219d86e3b 100644 --- a/distribution/bin/mqnamesrv.cmd +++ b/distribution/bin/mqnamesrv.cmd @@ -16,7 +16,7 @@ rem limitations under the License. if not exist "%ROCKETMQ_HOME%\bin\runserver.cmd" echo Please set the ROCKETMQ_HOME variable in your environment! & EXIT /B 1 -call "%ROCKETMQ_HOME%\bin\runserver.cmd" org.apache.rocketmq.namesrv.NamesrvStartup %* +call "%ROCKETMQ_HOME%\bin\runserver.cmd" -Drmq.logback.configurationFile=%ROCKETMQ_HOME%\conf\rmq.namesrv.logback.xml org.apache.rocketmq.namesrv.NamesrvStartup %* IF %ERRORLEVEL% EQU 0 ( ECHO "Namesrv starts OK" diff --git a/distribution/bin/mqnamesrv.xml b/distribution/bin/mqnamesrv.xml deleted file mode 100644 index 1f050d145d7..00000000000 --- a/distribution/bin/mqnamesrv.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - false - - ${JAVA_HOME} - - server - - org.apache.rocketmq.namesrv.NamesrvStartup - - - ${cpd}/../lib - ${cpd}/.. - - - - - - - <-Xms512m> - - <-Xmx1g> - -<-XX:NewSize>256M -<-XX:MaxNewSize>512M -<-XX:PermSize>128M -<-XX:MaxPermSize>128M - - diff --git a/distribution/bin/mqproxy b/distribution/bin/mqproxy new file mode 100644 index 00000000000..d6a8f3f268e --- /dev/null +++ b/distribution/bin/mqproxy @@ -0,0 +1,45 @@ +#!/bin/sh + +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +if [ -z "$ROCKETMQ_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + ROCKETMQ_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + ROCKETMQ_HOME=`cd "$ROCKETMQ_HOME" && pwd` + + cd "$saveddir" +fi + +export ROCKETMQ_HOME + +sh ${ROCKETMQ_HOME}/bin/runserver.sh -Drmq.logback.configurationFile=$ROCKETMQ_HOME/conf/rmq.proxy.logback.xml org.apache.rocketmq.proxy.ProxyStartup $@ diff --git a/distribution/bin/mqproxy.cmd b/distribution/bin/mqproxy.cmd new file mode 100644 index 00000000000..51f7b2118a3 --- /dev/null +++ b/distribution/bin/mqproxy.cmd @@ -0,0 +1,23 @@ +@echo off +rem Licensed to the Apache Software Foundation (ASF) under one or more +rem contributor license agreements. See the NOTICE file distributed with +rem this work for additional information regarding copyright ownership. +rem The ASF licenses this file to You under the Apache License, Version 2.0 +rem (the "License"); you may not use this file except in compliance with +rem the License. You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. + +if not exist "%ROCKETMQ_HOME%\bin\runserver.cmd" echo Please set the ROCKETMQ_HOME variable in your environment! & EXIT /B 1 + +call "%ROCKETMQ_HOME%\bin\runserver.cmd" -Drmq.logback.configurationFile=%ROCKETMQ_HOME%\conf\rmq.proxy.logback.xml org.apache.rocketmq.proxy.ProxyStartup %* + +IF %ERRORLEVEL% EQU 0 ( + ECHO "Proxy starts OK" +) \ No newline at end of file diff --git a/distribution/bin/mqshutdown b/distribution/bin/mqshutdown index d2d51fc68ec..f4b58e2b79e 100644 --- a/distribution/bin/mqshutdown +++ b/distribution/bin/mqshutdown @@ -17,11 +17,16 @@ case $1 in broker) - + pid=`ps ax | grep -i 'org.apache.rocketmq.proxy.ProxyStartup' | grep '\-pm local' |grep java | grep -v grep | awk '{print $1}'` + if [ "$pid" != "" ] ; then + echo "The mqbroker with proxy enable is running(${pid})..." + kill ${pid} + echo "Send shutdown request to mqbroker with proxy enable OK(${pid})" + fi pid=`ps ax | grep -i 'org.apache.rocketmq.broker.BrokerStartup' |grep java | grep -v grep | awk '{print $1}'` if [ -z "$pid" ] ; then echo "No mqbroker running." - exit -1; + exit 1; fi echo "The mqbroker(${pid}) is running..." @@ -30,12 +35,26 @@ case $1 in echo "Send shutdown request to mqbroker(${pid}) OK" ;; + brokerContainer) + + pid=`ps ax | grep -i 'org.apache.rocketmq.container.BrokerContainerStartup' |grep java | grep -v grep | awk '{print $1}'` + if [ -z "$pid" ] ; then + echo "No broker container running." + exit 1; + fi + + echo "The broker container(${pid}) is running..." + + kill ${pid} + + echo "Send shutdown request to broker container(${pid}) OK" + ;; namesrv) pid=`ps ax | grep -i 'org.apache.rocketmq.namesrv.NamesrvStartup' |grep java | grep -v grep | awk '{print $1}'` if [ -z "$pid" ] ; then echo "No mqnamesrv running." - exit -1; + exit 1; fi echo "The mqnamesrv(${pid}) is running..." @@ -44,6 +63,34 @@ case $1 in echo "Send shutdown request to mqnamesrv(${pid}) OK" ;; + controller) + + pid=`ps ax | grep -i 'org.apache.rocketmq.controller.ControllerStartup' |grep java | grep -v grep | awk '{print $1}'` + if [ -z "$pid" ] ; then + echo "No mqcontroller running." + exit 1; + fi + + echo "The mqcontroller(${pid}) is running..." + + kill ${pid} + + echo "Send shutdown request to mqcontroller(${pid}) OK" + ;; + proxy) + + pid=`ps ax | grep -i 'org.apache.rocketmq.proxy.ProxyStartup' |grep java | grep -v grep | awk '{print $1}'` + if [ -z "$pid" ] ; then + echo "No mqproxy running." + exit 1; + fi + + echo "The mqproxy(${pid}) is running..." + + kill ${pid} + + echo "Send shutdown request to mqproxy(${pid}) OK" + ;; *) - echo "Useage: mqshutdown broker | namesrv" + echo "Usage: mqshutdown broker | namesrv | controller | proxy" esac diff --git a/distribution/bin/mqshutdown.cmd b/distribution/bin/mqshutdown.cmd index 50af026f7c0..32fcc997eb4 100644 --- a/distribution/bin/mqshutdown.cmd +++ b/distribution/bin/mqshutdown.cmd @@ -29,7 +29,13 @@ if /I "%1" == "broker" ( for /f "tokens=1" %%i in ('jps -m ^| find "NamesrvStartup"') do ( taskkill /F /PID %%i ) + echo Done! +) else if /I "%1" == "controller" ( + echo killing controller server + + for /f "tokens=1" %%i in ('jps -m ^| find "ControllerStartup"') do ( taskkill /F /PID %%i ) + echo Done! ) else ( - echo Unknown role to kill, please specify broker or namesrv + echo Unknown role to kill, please specify broker or namesrv or controller ) \ No newline at end of file diff --git a/distribution/bin/runbroker.cmd b/distribution/bin/runbroker.cmd index eab7e30f9d4..0ea87f876db 100644 --- a/distribution/bin/runbroker.cmd +++ b/distribution/bin/runbroker.cmd @@ -23,20 +23,38 @@ set BASE_DIR=%~dp0 set BASE_DIR=%BASE_DIR:~0,-1% for %%d in (%BASE_DIR%) do set BASE_DIR=%%~dpd -set CLASSPATH=.;%BASE_DIR%conf;%CLASSPATH% +set CLASSPATH=.;%BASE_DIR%conf;%BASE_DIR%lib\*;%CLASSPATH% rem =========================================================================================== rem JVM Configuration rem =========================================================================================== -set "JAVA_OPT=%JAVA_OPT% -server -Xms2g -Xmx2g -Xmn1g" -set "JAVA_OPT=%JAVA_OPT% -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:SurvivorRatio=8" -set "JAVA_OPT=%JAVA_OPT% -verbose:gc -Xloggc:%USERPROFILE%\mq_gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintAdaptiveSizePolicy" -set "JAVA_OPT=%JAVA_OPT% -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m" -set "JAVA_OPT=%JAVA_OPT% -XX:-OmitStackTraceInFastThrow" -set "JAVA_OPT=%JAVA_OPT% -XX:+AlwaysPreTouch" -set "JAVA_OPT=%JAVA_OPT% -XX:MaxDirectMemorySize=15g" -set "JAVA_OPT=%JAVA_OPT% -XX:-UseLargePages -XX:-UseBiasedLocking" -set "JAVA_OPT=%JAVA_OPT% -Djava.ext.dirs=%BASE_DIR%lib" -set "JAVA_OPT=%JAVA_OPT% -cp %CLASSPATH%" +for /f "tokens=2 delims=" %%v in ('java -version 2^>^&1 ^| findstr /i "version"') do ( + for /f "tokens=1 delims=." %%m in ("%%v") do set "JAVA_MAJOR_VERSION=%%m" +) + +if "%JAVA_MAJOR_VERSION%"=="" ( + set "JAVA_MAJOR_VERSION=0" +) +if %JAVA_MAJOR_VERSION% lss 17 ( + set "JAVA_OPT=%JAVA_OPT% -server -Xms2g -Xmx2g" + set "JAVA_OPT=%JAVA_OPT% -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:SurvivorRatio=8" + set "JAVA_OPT=%JAVA_OPT% -verbose:gc -Xloggc:%USERPROFILE%\mq_gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintAdaptiveSizePolicy" + set "JAVA_OPT=%JAVA_OPT% -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m" + set "JAVA_OPT=%JAVA_OPT% -XX:-OmitStackTraceInFastThrow" + set "JAVA_OPT=%JAVA_OPT% -XX:+AlwaysPreTouch" + set "JAVA_OPT=%JAVA_OPT% -XX:MaxDirectMemorySize=15g" + set "JAVA_OPT=%JAVA_OPT% -XX:-UseLargePages -XX:-UseBiasedLocking" + set "JAVA_OPT=%JAVA_OPT% %JAVA_OPT_EXT% -cp %CLASSPATH%" +) else ( + set "JAVA_OPT=%JAVA_OPT% -server -Xms2g -Xmx2g" + set "JAVA_OPT=%JAVA_OPT% -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:SurvivorRatio=8" + rem set "JAVA_OPT=%JAVA_OPT% -verbose:gc -Xloggc:%USERPROFILE%\mq_gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintAdaptiveSizePolicy" + rem set "JAVA_OPT=%JAVA_OPT% -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m" + set "JAVA_OPT=%JAVA_OPT% -XX:-OmitStackTraceInFastThrow" + set "JAVA_OPT=%JAVA_OPT% -XX:+AlwaysPreTouch" + set "JAVA_OPT=%JAVA_OPT% -XX:MaxDirectMemorySize=15g" + set "JAVA_OPT=%JAVA_OPT% -XX:-UseLargePages -XX:-UseBiasedLocking" + set "JAVA_OPT=%JAVA_OPT% %JAVA_OPT_EXT% -cp "%CLASSPATH%"" +) "%JAVA%" %JAVA_OPT% %* \ No newline at end of file diff --git a/distribution/bin/runbroker.sh b/distribution/bin/runbroker.sh index 3bd00bb3014..e6e2132aba3 100644 --- a/distribution/bin/runbroker.sh +++ b/distribution/bin/runbroker.sh @@ -24,6 +24,24 @@ error_exit () exit 1 } +find_java_home() +{ + case "`uname`" in + Darwin) + if [ -n "$JAVA_HOME" ]; then + JAVA_HOME=$JAVA_HOME + return + fi + JAVA_HOME=$(/usr/libexec/java_home) + ;; + *) + JAVA_HOME=$(dirname $(dirname $(readlink -f $(which javac)))) + ;; + esac +} + +find_java_home + [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=$HOME/jdk/java [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=/usr/java [ ! -e "$JAVA_HOME/bin/java" ] && error_exit "Please set the JAVA_HOME variable in your environment, We need java(x64)!" @@ -31,20 +49,63 @@ error_exit () export JAVA_HOME export JAVA="$JAVA_HOME/bin/java" export BASE_DIR=$(dirname $0)/.. -export CLASSPATH=.:${BASE_DIR}/conf:${CLASSPATH} +export CLASSPATH=.:${BASE_DIR}/conf:${BASE_DIR}/lib/*:${CLASSPATH} #=========================================================================================== # JVM Configuration #=========================================================================================== -JAVA_OPT="${JAVA_OPT} -server -Xms8g -Xmx8g -Xmn4g" -JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:SurvivorRatio=8" -JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:/dev/shm/mq_gc_%p.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintAdaptiveSizePolicy" -JAVA_OPT="${JAVA_OPT} -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m" +# The RAMDisk initializing size in MB on Darwin OS for gc-log +DIR_SIZE_IN_MB=600 + +choose_gc_log_directory() +{ + case "`uname`" in + Darwin) + if [ ! -d "/Volumes/RAMDisk" ]; then + # create ram disk on Darwin systems as gc-log directory + DEV=`hdiutil attach -nomount ram://$((2 * 1024 * DIR_SIZE_IN_MB))` > /dev/null + diskutil eraseVolume HFS+ RAMDisk ${DEV} > /dev/null + echo "Create RAMDisk /Volumes/RAMDisk for gc logging on Darwin OS." + fi + GC_LOG_DIR="/Volumes/RAMDisk" + ;; + *) + # check if /dev/shm exists on other systems + if [ -d "/dev/shm" ]; then + GC_LOG_DIR="/dev/shm" + else + GC_LOG_DIR=${BASE_DIR} + fi + ;; + esac +} + +choose_gc_options() +{ + JAVA_MAJOR_VERSION=$("$JAVA" -version 2>&1 | head -1 | cut -d'"' -f2 | sed 's/^1\.//' | cut -d'.' -f1) + if [ -z "$JAVA_MAJOR_VERSION" ] || [ "$JAVA_MAJOR_VERSION" -lt "8" ] ; then + JAVA_OPT="${JAVA_OPT} -Xmn4g -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSClassUnloadingEnabled -XX:SurvivorRatio=8 -XX:-UseParNewGC" + else + JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0" + fi + + if [ -z "$JAVA_MAJOR_VERSION" ] || [ "$JAVA_MAJOR_VERSION" -lt "9" ] ; then + JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintAdaptiveSizePolicy" + JAVA_OPT="${JAVA_OPT} -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m" + else + JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0" + JAVA_OPT="${JAVA_OPT} -Xlog:gc*:file=${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log:time,tags:filecount=5,filesize=30M" + fi +} + +choose_gc_log_directory + +JAVA_OPT="${JAVA_OPT} -server -Xms8g -Xmx8g" +choose_gc_options JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow" JAVA_OPT="${JAVA_OPT} -XX:+AlwaysPreTouch" JAVA_OPT="${JAVA_OPT} -XX:MaxDirectMemorySize=15g" JAVA_OPT="${JAVA_OPT} -XX:-UseLargePages -XX:-UseBiasedLocking" -JAVA_OPT="${JAVA_OPT} -Djava.ext.dirs=${JAVA_HOME}/jre/lib/ext:${BASE_DIR}/lib" #JAVA_OPT="${JAVA_OPT} -Xdebug -Xrunjdwp:transport=dt_socket,address=9555,server=y,suspend=n" JAVA_OPT="${JAVA_OPT} ${JAVA_OPT_EXT}" JAVA_OPT="${JAVA_OPT} -cp ${CLASSPATH}" @@ -58,5 +119,5 @@ then numactl --cpunodebind=$RMQ_NUMA_NODE --membind=$RMQ_NUMA_NODE $JAVA ${JAVA_OPT} $@ fi else - $JAVA ${JAVA_OPT} $@ + "$JAVA" ${JAVA_OPT} $@ fi diff --git a/distribution/bin/runserver.cmd b/distribution/bin/runserver.cmd index 48e32bf2d7c..103a5a6e983 100644 --- a/distribution/bin/runserver.cmd +++ b/distribution/bin/runserver.cmd @@ -24,14 +24,33 @@ set BASE_DIR=%~dp0 set BASE_DIR=%BASE_DIR:~0,-1% for %%d in (%BASE_DIR%) do set BASE_DIR=%%~dpd -set CLASSPATH=.;%BASE_DIR%conf;%CLASSPATH% - -set "JAVA_OPT=%JAVA_OPT% -server -Xms2g -Xmx2g -Xmn1g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m" -set "JAVA_OPT=%JAVA_OPT% -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSClassUnloadingEnabled -XX:SurvivorRatio=8 -XX:-UseParNewGC" -set "JAVA_OPT=%JAVA_OPT% -verbose:gc -Xloggc:"%USERPROFILE%\rmq_srv_gc.log" -XX:+PrintGCDetails" -set "JAVA_OPT=%JAVA_OPT% -XX:-OmitStackTraceInFastThrow" -set "JAVA_OPT=%JAVA_OPT% -XX:-UseLargePages" -set "JAVA_OPT=%JAVA_OPT% -Djava.ext.dirs=%BASE_DIR%lib" -set "JAVA_OPT=%JAVA_OPT% -cp "%CLASSPATH%"" +set CLASSPATH=.;%BASE_DIR%conf;%BASE_DIR%lib\*;%CLASSPATH% + +REM Example of JAVA_MAJOR_VERSION value: '1', '9', '10', '11', ... +REM '1' means releases before Java 9 + +for /f "tokens=2 delims=" %%v in ('java -version 2^>^&1 ^| findstr /i "version"') do ( + for /f "tokens=1 delims=." %%m in ("%%v") do set "JAVA_MAJOR_VERSION=%%m" +) + +if "%JAVA_MAJOR_VERSION%"=="" ( + set "JAVA_MAJOR_VERSION=0" +) + +if %JAVA_MAJOR_VERSION% lss 17 ( + set "JAVA_OPT=%JAVA_OPT% -server -Xms2g -Xmx2g -Xmn1g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m" + set "JAVA_OPT=%JAVA_OPT% -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSClassUnloadingEnabled -XX:SurvivorRatio=8 -XX:-UseParNewGC" + set "JAVA_OPT=%JAVA_OPT% -verbose:gc -Xloggc:"%USERPROFILE%\rmq_srv_gc.log" -XX:+PrintGCDetails -XX:+PrintGCDateStamps" + set "JAVA_OPT=%JAVA_OPT% -XX:-OmitStackTraceInFastThrow" + set "JAVA_OPT=%JAVA_OPT% -XX:-UseLargePages" + set "JAVA_OPT=%JAVA_OPT% %JAVA_OPT_EXT% -cp "%CLASSPATH%"" +) else ( + set "JAVA_OPT=%JAVA_OPT% -server -Xms2g -Xmx2g -Xmn1g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m" + rem set "JAVA_OPT=%JAVA_OPT% -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSClassUnloadingEnabled -XX:SurvivorRatio=8 -XX:-UseParNewGC" + rem set "JAVA_OPT=%JAVA_OPT% -verbose:gc -Xloggc:"%USERPROFILE%\rmq_srv_gc.log" -XX:+PrintGCDetails -XX:+PrintGCDateStamps" + set "JAVA_OPT=%JAVA_OPT% -XX:-OmitStackTraceInFastThrow" + set "JAVA_OPT=%JAVA_OPT% -XX:-UseLargePages" + set "JAVA_OPT=%JAVA_OPT% %JAVA_OPT_EXT% -cp "%CLASSPATH%"" +) "%JAVA%" %JAVA_OPT% %* \ No newline at end of file diff --git a/distribution/bin/runserver.sh b/distribution/bin/runserver.sh index 908c79df946..2a5184d9f8f 100644 --- a/distribution/bin/runserver.sh +++ b/distribution/bin/runserver.sh @@ -24,6 +24,24 @@ error_exit () exit 1 } +find_java_home() +{ + case "`uname`" in + Darwin) + if [ -n "$JAVA_HOME" ]; then + JAVA_HOME=$JAVA_HOME + return + fi + JAVA_HOME=$(/usr/libexec/java_home) + ;; + *) + JAVA_HOME=$(dirname $(dirname $(readlink -f $(which javac)))) + ;; + esac +} + +find_java_home + [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=$HOME/jdk/java [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=/usr/java [ ! -e "$JAVA_HOME/bin/java" ] && error_exit "Please set the JAVA_HOME variable in your environment, We need java(x64)!" @@ -31,19 +49,60 @@ error_exit () export JAVA_HOME export JAVA="$JAVA_HOME/bin/java" export BASE_DIR=$(dirname $0)/.. -export CLASSPATH=.:${BASE_DIR}/conf:${CLASSPATH} +export CLASSPATH=.:${BASE_DIR}/conf:${BASE_DIR}/lib/*:${CLASSPATH} #=========================================================================================== # JVM Configuration #=========================================================================================== -JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -Xmn2g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m" -JAVA_OPT="${JAVA_OPT} -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSClassUnloadingEnabled -XX:SurvivorRatio=8 -XX:-UseParNewGC" -JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:/dev/shm/rmq_srv_gc.log -XX:+PrintGCDetails" +# The RAMDisk initializing size in MB on Darwin OS for gc-log +DIR_SIZE_IN_MB=600 + +choose_gc_log_directory() +{ + case "`uname`" in + Darwin) + if [ ! -d "/Volumes/RAMDisk" ]; then + # create ram disk on Darwin systems as gc-log directory + DEV=`hdiutil attach -nomount ram://$((2 * 1024 * DIR_SIZE_IN_MB))` > /dev/null + diskutil eraseVolume HFS+ RAMDisk ${DEV} > /dev/null + echo "Create RAMDisk /Volumes/RAMDisk for gc logging on Darwin OS." + fi + GC_LOG_DIR="/Volumes/RAMDisk" + ;; + *) + # check if /dev/shm exists on other systems + if [ -d "/dev/shm" ]; then + GC_LOG_DIR="/dev/shm" + else + GC_LOG_DIR=${BASE_DIR} + fi + ;; + esac +} + +choose_gc_options() +{ + # Example of JAVA_MAJOR_VERSION value : '1', '9', '10', '11', ... + # '1' means releases before Java 9 + JAVA_MAJOR_VERSION=$("$JAVA" -version 2>&1 | awk -F '"' '/version/ {print $2}' | awk -F '.' '{print $1}') + if [ -z "$JAVA_MAJOR_VERSION" ] || [ "$JAVA_MAJOR_VERSION" -lt "9" ] ; then + JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -Xmn2g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m" + JAVA_OPT="${JAVA_OPT} -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSClassUnloadingEnabled -XX:SurvivorRatio=8 -XX:-UseParNewGC" + JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps" + JAVA_OPT="${JAVA_OPT} -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m" + else + JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m" + JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0" + JAVA_OPT="${JAVA_OPT} -Xlog:gc*:file=${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log:time,tags:filecount=5,filesize=30M" + fi +} + +choose_gc_log_directory +choose_gc_options JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow" -JAVA_OPT="${JAVA_OPT} -XX:-UseLargePages" -JAVA_OPT="${JAVA_OPT} -Djava.ext.dirs=${JAVA_HOME}/jre/lib/ext:${BASE_DIR}/lib" +JAVA_OPT="${JAVA_OPT} -XX:-UseLargePages" #JAVA_OPT="${JAVA_OPT} -Xdebug -Xrunjdwp:transport=dt_socket,address=9555,server=y,suspend=n" JAVA_OPT="${JAVA_OPT} ${JAVA_OPT_EXT}" JAVA_OPT="${JAVA_OPT} -cp ${CLASSPATH}" -$JAVA ${JAVA_OPT} $@ +"$JAVA" ${JAVA_OPT} $@ diff --git a/distribution/bin/tools.cmd b/distribution/bin/tools.cmd index 28ce7653267..263700dc3e8 100644 --- a/distribution/bin/tools.cmd +++ b/distribution/bin/tools.cmd @@ -23,13 +23,12 @@ set BASE_DIR=%~dp0 set BASE_DIR=%BASE_DIR:~0,-1% for %%d in (%BASE_DIR%) do set BASE_DIR=%%~dpd -set CLASSPATH=.;%BASE_DIR%conf;%CLASSPATH% +set CLASSPATH=.;%BASE_DIR%conf;%BASE_DIR%lib\*;%CLASSPATH% rem =========================================================================================== rem JVM Configuration rem =========================================================================================== -set "JAVA_OPT=%JAVA_OPT% -server -Xms1g -Xmx1g -Xmn256m -XX:PermSize=128m -XX:MaxPermSize=128m" -set "JAVA_OPT=%JAVA_OPT% -Djava.ext.dirs="%BASE_DIR%\lib";"%JAVA_HOME%\jre\lib\ext"" +set "JAVA_OPT=%JAVA_OPT% -server -Xms1g -Xmx1g -Xmn256m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m" set "JAVA_OPT=%JAVA_OPT% -cp "%CLASSPATH%"" -"%JAVA%" %JAVA_OPT% %* \ No newline at end of file +"%JAVA%" %JAVA_OPT% %* diff --git a/distribution/bin/tools.sh b/distribution/bin/tools.sh index 66862cac560..9b1e1d804df 100644 --- a/distribution/bin/tools.sh +++ b/distribution/bin/tools.sh @@ -24,6 +24,24 @@ error_exit () exit 1 } +find_java_home() +{ + if [ -n "$JAVA_HOME" ]; then + JAVA_HOME=$JAVA_HOME + return + fi + case "`uname`" in + Darwin) + JAVA_HOME=$(/usr/libexec/java_home) + ;; + *) + JAVA_HOME=$(dirname $(dirname $(readlink -f $(which javac)))) + ;; + esac +} + +find_java_home + [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=$HOME/jdk/java [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=/usr/java [ ! -e "$JAVA_HOME/bin/java" ] && error_exit "Please set the JAVA_HOME variable in your environment, We need java(x64)!" @@ -31,13 +49,12 @@ error_exit () export JAVA_HOME export JAVA="$JAVA_HOME/bin/java" export BASE_DIR=$(dirname $0)/.. -export CLASSPATH=.:${BASE_DIR}/conf:${CLASSPATH} +export CLASSPATH=.:${BASE_DIR}/conf:${BASE_DIR}/lib/*:${CLASSPATH} #=========================================================================================== # JVM Configuration #=========================================================================================== -JAVA_OPT="${JAVA_OPT} -server -Xms1g -Xmx1g -Xmn256m -XX:PermSize=128m -XX:MaxPermSize=128m" -JAVA_OPT="${JAVA_OPT} -Djava.ext.dirs=${BASE_DIR}/lib:${JAVA_HOME}/jre/lib/ext" +JAVA_OPT="${JAVA_OPT} -server -Xms1g -Xmx1g -Xmn256m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m" JAVA_OPT="${JAVA_OPT} -cp ${CLASSPATH}" -$JAVA ${JAVA_OPT} $@ +"$JAVA" ${JAVA_OPT} "$@" diff --git a/distribution/conf/2m-noslave/broker-trace.properties b/distribution/conf/2m-noslave/broker-trace.properties new file mode 100644 index 00000000000..9dd57a73def --- /dev/null +++ b/distribution/conf/2m-noslave/broker-trace.properties @@ -0,0 +1,23 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +brokerClusterName=DefaultCluster +brokerName=broker-trace +brokerId=0 +deleteWhen=04 +fileReservedTime=48 +brokerRole=ASYNC_MASTER +flushDiskType=ASYNC_FLUSH diff --git a/distribution/conf/container/2container-2m-2s/broker-a-in-container1.conf b/distribution/conf/container/2container-2m-2s/broker-a-in-container1.conf new file mode 100644 index 00000000000..8da6011a5a0 --- /dev/null +++ b/distribution/conf/container/2container-2m-2s/broker-a-in-container1.conf @@ -0,0 +1,34 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#Master配置 +brokerClusterName=DefaultCluster +brokerName=broker-a +brokerId=0 +brokerRole=SYNC_MASTER +flushDiskType=ASYNC_FLUSH +storePathRootDir=/root/broker-a/store +storePathCommitLog=/root/broker-a/store/commitlog +listenPort=10911 +haListenPort=10912 +totalReplicas=2 +inSyncReplicas=2 +minInSyncReplicas=1 +enableAutoInSyncReplicas=true +slaveReadEnable=true +brokerHeartbeatInterval=1000 +brokerNotActiveTimeoutMillis=5000 +sendHeartbeatTimeoutMillis=1000 +enableSlaveActingMaster=true \ No newline at end of file diff --git a/distribution/conf/container/2container-2m-2s/broker-a-in-container2.conf b/distribution/conf/container/2container-2m-2s/broker-a-in-container2.conf new file mode 100644 index 00000000000..c654bf8c688 --- /dev/null +++ b/distribution/conf/container/2container-2m-2s/broker-a-in-container2.conf @@ -0,0 +1,34 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#Master配置 +brokerClusterName=DefaultCluster +brokerName=broker-a +brokerId=1 +brokerRole=SLAVE +flushDiskType=ASYNC_FLUSH +storePathRootDir=/root/broker-a/store +storePathCommitLog=/root/broker-a/store/commitlog +listenPort=10911 +haListenPort=10912 +totalReplicas=2 +inSyncReplicas=2 +minInSyncReplicas=1 +enableAutoInSyncReplicas=true +slaveReadEnable=true +brokerHeartbeatInterval=1000 +brokerNotActiveTimeoutMillis=5000 +sendHeartbeatTimeoutMillis=1000 +enableSlaveActingMaster=true \ No newline at end of file diff --git a/distribution/conf/container/2container-2m-2s/broker-b-in-container1.conf b/distribution/conf/container/2container-2m-2s/broker-b-in-container1.conf new file mode 100644 index 00000000000..6e8f896d174 --- /dev/null +++ b/distribution/conf/container/2container-2m-2s/broker-b-in-container1.conf @@ -0,0 +1,34 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#Slave配置 +brokerClusterName=DefaultCluster +brokerName=broker-b +brokerId=1 +brokerRole=SLAVE +flushDiskType=ASYNC_FLUSH +storePathRootDir=/root/broker-b/store +storePathCommitLog=/root/broker-b/store/commitlog +listenPort=20911 +haListenPort=20912 +totalReplicas=2 +inSyncReplicas=2 +minInSyncReplicas=1 +enableAutoInSyncReplicas=true +slaveReadEnable=true +brokerHeartbeatInterval=1000 +brokerNotActiveTimeoutMillis=5000 +sendHeartbeatTimeoutMillis=1000 +enableSlaveActingMaster=true \ No newline at end of file diff --git a/distribution/conf/container/2container-2m-2s/broker-b-in-container2.conf b/distribution/conf/container/2container-2m-2s/broker-b-in-container2.conf new file mode 100644 index 00000000000..cf9f803ea6d --- /dev/null +++ b/distribution/conf/container/2container-2m-2s/broker-b-in-container2.conf @@ -0,0 +1,34 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#Slave配置 +brokerClusterName=DefaultCluster +brokerName=broker-b +brokerId=0 +brokerRole=SYNC_MASTER +flushDiskType=ASYNC_FLUSH +storePathRootDir=/root/broker-b/store +storePathCommitLog=/root/broker-b/store/commitlog +listenPort=20911 +haListenPort=20912 +totalReplicas=2 +inSyncReplicas=2 +minInSyncReplicas=1 +enableAutoInSyncReplicas=true +slaveReadEnable=true +brokerHeartbeatInterval=1000 +brokerNotActiveTimeoutMillis=5000 +sendHeartbeatTimeoutMillis=1000 +enableSlaveActingMaster=true \ No newline at end of file diff --git a/distribution/conf/container/2container-2m-2s/broker-container1.conf b/distribution/conf/container/2container-2m-2s/broker-container1.conf new file mode 100644 index 00000000000..f50165d57c9 --- /dev/null +++ b/distribution/conf/container/2container-2m-2s/broker-container1.conf @@ -0,0 +1,24 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#配置端口,用于接收mqadmin命令 +listenPort=10811 +#指定namesrv +namesrvAddr=172.22.144.49:9876 +#或指定自动获取namesrv +fetchNamesrvAddrByAddressServer=false +#指定要向BrokerContainer内添加的brokerConfig路径,多个config间用“:”分隔; +#不指定则只启动BrokerConainer,具体broker可通过mqadmin工具添加 +brokerConfigPaths=/root/2container-2m-2s/broker-a-in-container1.conf:/root/2container-2m-2s/broker-b-in-container1.conf \ No newline at end of file diff --git a/distribution/conf/container/2container-2m-2s/broker-container2.conf b/distribution/conf/container/2container-2m-2s/broker-container2.conf new file mode 100644 index 00000000000..0870bfdf775 --- /dev/null +++ b/distribution/conf/container/2container-2m-2s/broker-container2.conf @@ -0,0 +1,24 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#配置端口,用于接收mqadmin命令 +listenPort=10811 +#指定namesrv +namesrvAddr=172.22.144.49:9876 +#或指定自动获取namesrv +fetchNamesrvAddrByAddressServer=false +#指定要向BrokerContainer内添加的brokerConfig路径,多个config间用“:”分隔; +#不指定则只启动BrokerConainer,具体broker可通过mqadmin工具添加 +brokerConfigPaths=/root/2container-2m-2s/broker-a-in-container2.conf:/root/2container-2m-2s/broker-b-in-container2.conf \ No newline at end of file diff --git a/distribution/conf/container/2container-2m-2s/nameserver.conf b/distribution/conf/container/2container-2m-2s/nameserver.conf new file mode 100644 index 00000000000..fd700c18748 --- /dev/null +++ b/distribution/conf/container/2container-2m-2s/nameserver.conf @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +supportActingMaster=true \ No newline at end of file diff --git a/distribution/conf/controller/cluster-3n-independent/controller-n0.conf b/distribution/conf/controller/cluster-3n-independent/controller-n0.conf new file mode 100644 index 00000000000..d5741379205 --- /dev/null +++ b/distribution/conf/controller/cluster-3n-independent/controller-n0.conf @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +controllerDLegerGroup = group1 +controllerDLegerPeers = n0-127.0.0.1:9878;n1-127.0.0.1:9868;n2-127.0.0.1:9858 +controllerDLegerSelfId = n0 + diff --git a/distribution/conf/controller/cluster-3n-independent/controller-n1.conf b/distribution/conf/controller/cluster-3n-independent/controller-n1.conf new file mode 100644 index 00000000000..f6dec223551 --- /dev/null +++ b/distribution/conf/controller/cluster-3n-independent/controller-n1.conf @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +controllerDLegerGroup = group1 +controllerDLegerPeers = n0-127.0.0.1:9878;n1-127.0.0.1:9868;n2-127.0.0.1:9858 +controllerDLegerSelfId = n1 + diff --git a/distribution/conf/controller/cluster-3n-independent/controller-n2.conf b/distribution/conf/controller/cluster-3n-independent/controller-n2.conf new file mode 100644 index 00000000000..aa45fa53cc3 --- /dev/null +++ b/distribution/conf/controller/cluster-3n-independent/controller-n2.conf @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +controllerDLegerGroup = group1 +controllerDLegerPeers = n0-127.0.0.1:9878;n1-127.0.0.1:9868;n2-127.0.0.1:9858 +controllerDLegerSelfId = n2 + diff --git a/distribution/conf/controller/cluster-3n-namesrv-plugin/namesrv-n0.conf b/distribution/conf/controller/cluster-3n-namesrv-plugin/namesrv-n0.conf new file mode 100644 index 00000000000..ee2f5dfc583 --- /dev/null +++ b/distribution/conf/controller/cluster-3n-namesrv-plugin/namesrv-n0.conf @@ -0,0 +1,25 @@ + + +#Namesrv config +listenPort = 9876 +enableControllerInNamesrv = true + +#controller config +controllerDLegerGroup = group1 +controllerDLegerPeers = n0-127.0.0.1:9878;n1-127.0.0.1:9868;n2-127.0.0.1:9858 +controllerDLegerSelfId = n0 \ No newline at end of file diff --git a/distribution/conf/controller/cluster-3n-namesrv-plugin/namesrv-n1.conf b/distribution/conf/controller/cluster-3n-namesrv-plugin/namesrv-n1.conf new file mode 100644 index 00000000000..9461321a291 --- /dev/null +++ b/distribution/conf/controller/cluster-3n-namesrv-plugin/namesrv-n1.conf @@ -0,0 +1,25 @@ + + +#Namesrv config +listenPort = 9886 +enableControllerInNamesrv = true + +#controller config +controllerDLegerGroup = group1 +controllerDLegerPeers = n0-127.0.0.1:9878;n1-127.0.0.1:9868;n2-127.0.0.1:9858 +controllerDLegerSelfId = n1 \ No newline at end of file diff --git a/distribution/conf/controller/cluster-3n-namesrv-plugin/namesrv-n2.conf b/distribution/conf/controller/cluster-3n-namesrv-plugin/namesrv-n2.conf new file mode 100644 index 00000000000..aee7e9947d8 --- /dev/null +++ b/distribution/conf/controller/cluster-3n-namesrv-plugin/namesrv-n2.conf @@ -0,0 +1,25 @@ + + +#Namesrv config +listenPort = 9896 +enableControllerInNamesrv = true + +#controller config +controllerDLegerGroup = group1 +controllerDLegerPeers = n0-127.0.0.1:9878;n1-127.0.0.1:9868;n2-127.0.0.1:9858 +controllerDLegerSelfId = n2 \ No newline at end of file diff --git a/distribution/conf/controller/controller-standalone.conf b/distribution/conf/controller/controller-standalone.conf new file mode 100644 index 00000000000..700908f3446 --- /dev/null +++ b/distribution/conf/controller/controller-standalone.conf @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +controllerDLegerGroup = group1 +controllerDLegerPeers = n0-127.0.0.1:9878 +controllerDLegerSelfId = n0 + diff --git a/distribution/conf/controller/quick-start/broker-n0.conf b/distribution/conf/controller/quick-start/broker-n0.conf new file mode 100644 index 00000000000..c397689abde --- /dev/null +++ b/distribution/conf/controller/quick-start/broker-n0.conf @@ -0,0 +1,28 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +brokerClusterName = DefaultCluster +brokerName = broker-a +brokerId = -1 +brokerRole = SLAVE +deleteWhen = 04 +fileReservedTime = 48 +enableControllerMode = true +controllerAddr = 127.0.0.1:9878 +namesrvAddr = 127.0.0.1:9876 +allAckInSyncStateSet=true +listenPort=30911 +storePathRootDir=/tmp/rmqstore/node00 +storePathCommitLog=/tmp/rmqstore/node00/commitlog \ No newline at end of file diff --git a/distribution/conf/controller/quick-start/broker-n1.conf b/distribution/conf/controller/quick-start/broker-n1.conf new file mode 100644 index 00000000000..33bab6b387a --- /dev/null +++ b/distribution/conf/controller/quick-start/broker-n1.conf @@ -0,0 +1,28 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +brokerClusterName = DefaultCluster +brokerName = broker-a +brokerId = -1 +brokerRole = SLAVE +deleteWhen = 04 +fileReservedTime = 48 +enableControllerMode = true +controllerAddr = 127.0.0.1:9878 +namesrvAddr = 127.0.0.1:9876 +allAckInSyncStateSet=true +listenPort=30921 +storePathRootDir=/tmp/rmqstore/node01 +storePathCommitLog=/tmp/rmqstore/node01/commitlog \ No newline at end of file diff --git a/distribution/conf/controller/quick-start/namesrv.conf b/distribution/conf/controller/quick-start/namesrv.conf new file mode 100644 index 00000000000..a7d81b0d8c9 --- /dev/null +++ b/distribution/conf/controller/quick-start/namesrv.conf @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +enableControllerInNamesrv = true +controllerDLegerGroup = group1 +controllerDLegerPeers = n0-127.0.0.1:9878 +controllerDLegerSelfId = n0 \ No newline at end of file diff --git a/distribution/conf/dledger/broker-n0.conf b/distribution/conf/dledger/broker-n0.conf new file mode 100644 index 00000000000..5351e497dc8 --- /dev/null +++ b/distribution/conf/dledger/broker-n0.conf @@ -0,0 +1,27 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +brokerClusterName = RaftCluster +brokerName=RaftNode00 +listenPort=30911 +namesrvAddr=127.0.0.1:9876 +storePathRootDir=/tmp/rmqstore/node00 +storePathCommitLog=/tmp/rmqstore/node00/commitlog +enableDLegerCommitLog=true +dLegerGroup=RaftNode00 +dLegerPeers=n0-127.0.0.1:40911;n1-127.0.0.1:40912;n2-127.0.0.1:40913 +## must be unique +dLegerSelfId=n0 +sendMessageThreadPoolNums=16 diff --git a/distribution/conf/dledger/broker-n1.conf b/distribution/conf/dledger/broker-n1.conf new file mode 100644 index 00000000000..6aaf8f9309c --- /dev/null +++ b/distribution/conf/dledger/broker-n1.conf @@ -0,0 +1,27 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +brokerClusterName = RaftCluster +brokerName=RaftNode00 +listenPort=30921 +namesrvAddr=127.0.0.1:9876 +storePathRootDir=/tmp/rmqstore/node01 +storePathCommitLog=/tmp/rmqstore/node01/commitlog +enableDLegerCommitLog=true +dLegerGroup=RaftNode00 +dLegerPeers=n0-127.0.0.1:40911;n1-127.0.0.1:40912;n2-127.0.0.1:40913 +## must be unique +dLegerSelfId=n1 +sendMessageThreadPoolNums=16 diff --git a/distribution/conf/dledger/broker-n2.conf b/distribution/conf/dledger/broker-n2.conf new file mode 100644 index 00000000000..c863d89ee82 --- /dev/null +++ b/distribution/conf/dledger/broker-n2.conf @@ -0,0 +1,27 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +brokerClusterName = RaftCluster +brokerName=RaftNode00 +listenPort=30931 +namesrvAddr=127.0.0.1:9876 +storePathRootDir=/tmp/rmqstore/node02 +storePathCommitLog=/tmp/rmqstore/node02/commitlog +enableDLegerCommitLog=true +dLegerGroup=RaftNode00 +dLegerPeers=n0-127.0.0.1:40911;n1-127.0.0.1:40912;n2-127.0.0.1:40913 +## must be unique +dLegerSelfId=n2 +sendMessageThreadPoolNums=16 diff --git a/distribution/conf/logback_broker.xml b/distribution/conf/logback_broker.xml deleted file mode 100644 index 9d1a6b17618..00000000000 --- a/distribution/conf/logback_broker.xml +++ /dev/null @@ -1,337 +0,0 @@ - - - - - - ${user.home}/logs/rocketmqlogs/broker_default.log - true - - ${user.home}/logs/rocketmqlogs/otherdays/broker_default.%i.log.gz - 1 - 10 - - - 100MB - - - %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n - UTF-8 - - - - - ${user.home}/logs/rocketmqlogs/broker.log - true - - ${user.home}/logs/rocketmqlogs/otherdays/broker.%i.log.gz - 1 - 20 - - - 128MB - - - %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n - UTF-8 - - - - - - - - ${user.home}/logs/rocketmqlogs/protection.log - true - - ${user.home}/logs/rocketmqlogs/otherdays/protection.%i.log.gz - 1 - 10 - - - 100MB - - - %d{yyy-MM-dd HH:mm:ss,GMT+8} - %m%n - UTF-8 - - - - - - - - ${user.home}/logs/rocketmqlogs/watermark.log - true - - ${user.home}/logs/rocketmqlogs/otherdays/watermark.%i.log.gz - 1 - 10 - - - 100MB - - - %d{yyy-MM-dd HH:mm:ss,GMT+8} - %m%n - UTF-8 - - - - - - - - ${user.home}/logs/rocketmqlogs/store.log - true - - ${user.home}/logs/rocketmqlogs/otherdays/store.%i.log.gz - 1 - 10 - - - 128MB - - - %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n - UTF-8 - - - - - - - - ${user.home}/logs/rocketmqlogs/remoting.log - true - - ${user.home}/logs/rocketmqlogs/otherdays/remoting.%i.log.gz - 1 - 10 - - - 100MB - - - %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n - UTF-8 - - - - - - - - ${user.home}/logs/rocketmqlogs/storeerror.log - true - - ${user.home}/logs/rocketmqlogs/otherdays/storeerror.%i.log.gz - 1 - 10 - - - 100MB - - - %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n - UTF-8 - - - - - - - - - ${user.home}/logs/rocketmqlogs/transaction.log - true - - ${user.home}/logs/rocketmqlogs/otherdays/transaction.%i.log.gz - 1 - 10 - - - 100MB - - - %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n - UTF-8 - - - - - - - - ${user.home}/logs/rocketmqlogs/lock.log - true - - ${user.home}/logs/rocketmqlogs/otherdays/lock.%i.log.gz - 1 - 5 - - - 100MB - - - %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n - UTF-8 - - - - - - - - ${user.home}/logs/rocketmqlogs/filter.log - true - - ${user.home}/logs/rocketmqlogs/otherdays/filter.%i.log.gz - 1 - 10 - - - 100MB - - - %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n - UTF-8 - - - - - - - - ${user.home}/logs/rocketmqlogs/stats.log - true - - ${user.home}/logs/rocketmqlogs/otherdays/stats.%i.log.gz - 1 - 5 - - - 100MB - - - %d{yyy-MM-dd HH:mm:ss,GMT+8} %p - %m%n - UTF-8 - - - - - ${user.home}/logs/rocketmqlogs/commercial.log - true - - ${user.home}/logs/rocketmqlogs/otherdays/commercial.%i.log.gz - 1 - 10 - - - 500MB - - - - - true - - %d{yyy-MM-dd HH\:mm\:ss,GMT+8} %p %t - %m%n - UTF-8 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/conf/logback_filtersrv.xml b/distribution/conf/logback_filtersrv.xml deleted file mode 100644 index 71b9a939130..00000000000 --- a/distribution/conf/logback_filtersrv.xml +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - ${user.home}/logs/rocketmqlogs/filtersrv_default.log - true - - ${user.home}/logs/rocketmqlogs/otherdays/filtersrv_default.%i.log.gz - 1 - 5 - - - 100MB - - - %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n - UTF-8 - - - - - ${user.home}/logs/rocketmqlogs/filtersrv.log - true - - ${user.home}/logs/rocketmqlogs/otherdays/filtersrv.%i.log.gz - 1 - 5 - - - 100MB - - - %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n - UTF-8 - - - - - 0 - - - - true - - %d{yyy-MM-dd HH\:mm\:ss,SSS} %p %t - %m%n - UTF-8 - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/conf/logback_namesrv.xml b/distribution/conf/logback_namesrv.xml deleted file mode 100644 index 53288945be8..00000000000 --- a/distribution/conf/logback_namesrv.xml +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - ${user.home}/logs/rocketmqlogs/namesrv_default.log - true - - ${user.home}/logs/rocketmqlogs/otherdays/namesrv_default.%i.log.gz - 1 - 5 - - - 100MB - - - %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n - UTF-8 - - - - - ${user.home}/logs/rocketmqlogs/namesrv.log - true - - ${user.home}/logs/rocketmqlogs/otherdays/namesrv.%i.log.gz - 1 - 5 - - - 100MB - - - %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n - UTF-8 - - - - - 0 - - - - true - - %d{yyy-MM-dd HH\:mm\:ss,SSS} %p %t - %m%n - UTF-8 - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/conf/logback_tools.xml b/distribution/conf/logback_tools.xml deleted file mode 100644 index 28283ad1d1d..00000000000 --- a/distribution/conf/logback_tools.xml +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - ${user.home}/logs/rocketmqlogs/tools_default.log - true - - ${user.home}/logs/rocketmqlogs/otherdays/tools_default.%i.log.gz - 1 - 5 - - - 100MB - - - %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n - UTF-8 - - - - - ${user.home}/logs/rocketmqlogs/tools.log - true - - ${user.home}/logs/rocketmqlogs/otherdays/tools.%i.log.gz - 1 - 5 - - - 100MB - - - %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n - UTF-8 - - - - - - - - - true - - %d{yyy-MM-dd HH\:mm\:ss,GMT+8} %p %t - %m%n - UTF-8 - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/distribution/conf/plain_acl.yml b/distribution/conf/plain_acl.yml new file mode 100644 index 00000000000..2435380d856 --- /dev/null +++ b/distribution/conf/plain_acl.yml @@ -0,0 +1,42 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +globalWhiteRemoteAddresses: + - 10.10.103.* + - 192.168.0.* + +accounts: + - accessKey: RocketMQ + secretKey: 12345678 + whiteRemoteAddress: + admin: false + defaultTopicPerm: DENY + defaultGroupPerm: SUB + topicPerms: + - topicA=DENY + - topicB=PUB|SUB + - topicC=SUB + groupPerms: + # the group should convert to retry topic + - groupA=DENY + - groupB=PUB|SUB + - groupC=SUB + + - accessKey: rocketmq2 + secretKey: 12345678 + whiteRemoteAddress: 192.168.1.* + # if it is admin, it could access all resources + admin: true + diff --git a/distribution/conf/rmq-proxy.json b/distribution/conf/rmq-proxy.json new file mode 100644 index 00000000000..8e92bb18e52 --- /dev/null +++ b/distribution/conf/rmq-proxy.json @@ -0,0 +1,3 @@ +{ + "rocketMQClusterName": "DefaultCluster" +} \ No newline at end of file diff --git a/distribution/conf/tools.yml b/distribution/conf/tools.yml new file mode 100644 index 00000000000..9a372593709 --- /dev/null +++ b/distribution/conf/tools.yml @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +accessKey: rocketmq2 +secretKey: 12345678 + diff --git a/distribution/pom.xml b/distribution/pom.xml index fccb91e5c7a..427971c6e14 100644 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -20,38 +20,44 @@ org.apache.rocketmq rocketmq-all - 4.2.0 + 5.2.0 rocketmq-distribution rocketmq-distribution ${project.version} pom - + + ${basedir}/.. + + release-all - + + org.apache.rocketmq + rocketmq-container + + + org.apache.rocketmq + rocketmq-controller + org.apache.rocketmq rocketmq-broker - org.apache.rocketmq - rocketmq-client + rocketmq-proxy - org.apache.rocketmq - rocketmq-filtersrv + rocketmq-client - org.apache.rocketmq rocketmq-tools - org.apache.rocketmq rocketmq-example @@ -79,18 +85,16 @@ - apache-rocketmq + rocketmq-${project.version} release-client - org.apache.rocketmq rocketmq-client - ${project.version} @@ -115,7 +119,7 @@ - apache-rocketmq + rocketmq-client-${project.version} diff --git a/distribution/release-client.xml b/distribution/release-client.xml index 9f5da2552ac..f787c333897 100644 --- a/distribution/release-client.xml +++ b/distribution/release-client.xml @@ -17,18 +17,21 @@ --> client - false + true dir tar.gz + zip - - ../ - - README.md - - + + + ../ + + README.md + + + diff --git a/distribution/release.xml b/distribution/release.xml index 1ec535e7c3e..b7710425d06 100644 --- a/distribution/release.xml +++ b/distribution/release.xml @@ -17,7 +17,7 @@ --> all - false + true dir tar.gz @@ -40,7 +40,7 @@ - bin/* + bin/** 0755 @@ -55,20 +55,44 @@ NOTICE-BIN NOTICE + + ../broker/src/main/resources/rmq.broker.logback.xml + conf/rmq.broker.logback.xml + + + ../client/src/main/resources/rmq.client.logback.xml + conf/rmq.client.logback.xml + + + ../controller/src/main/resources/rmq.controller.logback.xml + conf/rmq.controller.logback.xml + + + ../namesrv/src/main/resources/rmq.namesrv.logback.xml + conf/rmq.namesrv.logback.xml + + + ../tools/src/main/resources/rmq.tools.logback.xml + conf/rmq.tools.logback.xml + + + ../proxy/src/main/resources/rmq.proxy.logback.xml + conf/rmq.proxy.logback.xml + true + org.apache.rocketmq:rocketmq-container org.apache.rocketmq:rocketmq-broker org.apache.rocketmq:rocketmq-tools org.apache.rocketmq:rocketmq-client org.apache.rocketmq:rocketmq-namesrv - org.apache.rocketmq:rocketmq-filtersrv org.apache.rocketmq:rocketmq-example - org.apache.rocketmq:rocketmq-filter org.apache.rocketmq:rocketmq-openmessaging + org.apache.rocketmq:rocketmq-controller lib/ @@ -76,6 +100,10 @@ lib/ + + io.jaegertracing:jaeger-core + io.jaegertracing:jaeger-client + diff --git a/docs/cn/BrokerContainer.md b/docs/cn/BrokerContainer.md new file mode 100644 index 00000000000..236439284be --- /dev/null +++ b/docs/cn/BrokerContainer.md @@ -0,0 +1,128 @@ +# BrokerContainer + +## 背景 + +在RocketMQ 4.x 版本中,一个进程只有一个broker,通常会以主备或者DLedger(Raft)的形式部署,但是一个进程中只有一个broker,而slave一般只承担冷备或热备的作用,节点之间角色的不对等导致slave节点资源没有充分被利用。 +因此在RocketMQ 5.x 版本中,提供一种新的模式BrokerContainer,在一个BrokerContainer进程中可以加入多个Broker(Master Broker、Slave Broker、DLedger Broker),来提高单个节点的资源利用率,并且可以通过各种形式的交叉部署来实现节点之间的对等部署。 +该特性的优点包括: + +1. 一个BrokerContainer进程中可以加入多个broker,通过进程内混部来提高单个节点的资源利用率 +2. 通过各种形式的交叉部署来实现节点之间的对等部署,增强单节点的高可用能力 +3. 利用BrokerContainer可以实现单进程内多CommitLog写入,也可以实现单机的多磁盘写入 +4. BrokerContainer中的CommitLog天然隔离的,不同的CommitLog(broker)可以采取不同作用,比如可以用来比如创建单独的broker做不同TTL的CommitLog。 + +## 架构 + +### 单进程视图 + +![](https://s4.ax1x.com/2022/01/26/7LMZHP.png) + +相比于原来一个Broker一个进程,RocketMQ 5.0将增加BrokerContainer概念,一个BrokerContainer可以存放多个Broker,每个Broker拥有不同的端口,但它们共享同一个传输层(remoting层),而每一个broker在功能上是完全独立的。BrokerContainer也拥有自己端口,在运行时可以通过admin命令来增加或减少Broker。 + +### 对等部署形态 + +在BrokerContainer模式下,可以通过各种形式的交叉部署完成节点的对等部署 + +- 二副本对等部署 + +![](https://s4.ax1x.com/2022/01/26/7LQi5T.png) + +二副本对等部署情况下,每个节点都会有一主一备,资源利用率均等。另外假设图中Node1宕机,由于Node2的broker_2可读可写,broker_1可以备读,因此普通消息的收发不会收到影响,单节点的高可用能力得到了增强。 + +- 三副本对等部署 + +![](https://s4.ax1x.com/2022/01/26/7LQMa6.png) + +三副本对等部署情况下,每个节点都会有一主两备,资源利用率均等。此外,和二副本一样,任意一个节点的宕机也不会影响到普通消息的收发。 + +### 传输层共享 + +![](https://s4.ax1x.com/2022/02/07/HMNIVs.png) + +BrokerContainer中的所有broker共享同一个传输层,就像RocketMQ客户端中同进程的Consumer和Producer共享同一个传输层一样。 + +这里为NettyRemotingServer提供SubRemotingServer支持,通过为一个RemotingServer绑定另一个端口即可生成SubRemotingServer,其共享NettyRemotingServer的Netty实例、计算资源、以及协议栈等,但拥有不同的端口以及ProcessorTable。另外同一个BrokerContainer中的所有的broker也会共享同一个BrokerOutAPI(RemotingClient)。 + +## 启动方式和配置 + +![](https://s4.ax1x.com/2022/01/26/7LQ1PO.png) + +像Broker启动利用BrokerStartup一样,使用BrokerContainerStartup来启动BrokerContainer。我们可以通过两种方式向BrokerContainer中增加broker,一种是通过启动时通过在配置文件中指定 + +BrokerContainer配置文件内容主要是Netty网络层参数(由于传输层共享),BrokerContainer的监听端口、namesrv配置,以及最重要的brokerConfigPaths参数,brokerConfigPaths是指需要向BrokerContainer内添加的brokerConfig路径,多个config间用“:”分隔,不指定则只启动BrokerConainer,具体broker可通过mqadmin工具添加 + +broker-container.conf(distribution/conf/container/broker-container.conf): + +``` +#配置端口,用于接收mqadmin命令 +listenPort=10811 +#指定namesrv +namesrvAddr=127.0.0.1:9876 +#或指定自动获取namesrv +fetchNamesrvAddrByAddressServer=true +#指定要向BrokerContainer内添加的brokerConfig路径,多个config间用“:”分隔; +#不指定则只启动BrokerConainer,具体broker可通过mqadmin工具添加 +brokerConfigPaths=/home/admin/broker-a.conf:/home/admin/broker-b.conf +``` +broker的配置和以前一样,但在BrokerContainer模式下broker配置文件中下Netty网络层参数和nameserver参数不生效,均使用BrokerContainer的配置参数。 + +完成配置文件后,可以以如下命令启动 +``` +sh mqbrokercontainer -c broker-container.conf +``` +mqbrokercontainer脚本路径为distribution/bin/mqbrokercontainer。 + +## 运行时增加或较少Broker + +当BrokerContainer进程启动后,也可以通过Admin命令来增加或减少Broker。 + +AddBrokerCommand +``` +usage: mqadmin addBroker -b -c [-h] [-n ] + -b,--brokerConfigPath Broker config path + -c,--brokerContainerAddr Broker container address + -h,--help Print help + -n,--namesrvAddr Name server address list, eg: 192.168.0.1:9876;192.168.0.2:9876 +``` + +RemoveBroker Command +``` +usage: mqadmin removeBroker -b -c [-h] [-n ] + -b,--brokerIdentity Information to identify a broker: clusterName:brokerName:brokerId + -c,--brokerContainerAddr Broker container address + -h,--help Print help + -n,--namesrvAddr Name server address list, eg: 192.168.0.1:9876;192.168.0.2:9876 +``` + +## 存储变化 + +storePathRootDir,storePathCommitLog路径依然为MessageStoreConfig中配置值,需要注意的是同一个brokerContainer中的broker不能使用相同的storePathRootDir,storePathCommitLog,否则不同的broker占用同一个存储目录,发生数据混乱。 + +在文件删除策略上,仍然单个Broker的视角来进行删除,但MessageStoreConfig新增replicasPerDiskPartition参数和logicalDiskSpaceCleanForciblyThreshold。 + +replicasPerDiskPartition表示同一磁盘分区上有多少个副本,即该broker的存储目录所在的磁盘分区被几个broker共享,默认值为1。该配置用于计算当同一节点上的多个broker共享同一磁盘分区时,各broker的磁盘配额 + +e.g. replicasPerDiskPartition==2且broker所在磁盘空间为1T时,则该broker磁盘配额为512G,该broker的逻辑磁盘空间利用率基于512G的空间进行计算。 + +logicalDiskSpaceCleanForciblyThreshold,该值只在replicasPerDiskPartition大于1时生效,表示逻辑磁盘空间强制清理阈值,默认为0.80(80%), 逻辑磁盘空间利用率为该broker在自身磁盘配额内的空间利用率,物理磁盘空间利用率为该磁盘分区总空间利用率。由于在BrokerContainer实现中,考虑计算效率的情况下,仅统计了commitLog+consumeQueue(+ BCQ)+indexFile作为broker的存储空间占用,其余文件如元数据、消费进度、磁盘脏数据等未统计在内,故在多个broker存储空间达到动态平衡时,各broker所占空间可能有相差,以一个BrokerContainer中有两个broker为例,两broker存储空间差异可表示为: +![](https://s4.ax1x.com/2022/01/26/7L14v4.png) +其中,R_logical为logicalDiskSpaceCleanForciblyThreshold,R_phy为diskSpaceCleanForciblyRatio,T为磁盘分区总空间,x为除上述计算的broker存储空间外的其他文件所占磁盘总空间比例,可见,当 +![](https://s4.ax1x.com/2022/01/26/7L1TbR.png) +时,可保证BrokerContainer各Broker存储空间在达到动态平衡时相差无几。 + +eg.假设broker获取到的配额是500g(根据replicasPerDiskPartition计算获得),logicalDiskSpaceCleanForciblyThreshold为默认值0.8,则默认commitLog+consumeQueue(+ BCQ)+indexFile总量超过400g就会强制清理文件。 + +其他清理阈值(diskSpaceCleanForciblyRatio、diskSpaceWarningLevelRatio),文件保存时间(fileReservedTime)等逻辑与之前保持一致。 + +注意:当以普通broker方式启动而非brokerContainer启动时,且replicasPerDiskPartition=1(默认值)时,清理逻辑与之前完全一致。replicasPerDiskPartition>1时,逻辑磁盘空间强制清理阈值logicalDiskSpaceCleanForciblyThreshold将会生效。 + + +## 日志变化 + +在BrokerContainer模式下日志的默认输出路径将发生变化,具体为: + +``` +{user.home}/logs/rocketmqlogs/${brokerCanonicalName}/ +``` + +其中 `brokerCanonicalName` 为 `{BrokerClusterName_BrokerName_BrokerId}`。 \ No newline at end of file diff --git a/docs/cn/Configuration_System.md b/docs/cn/Configuration_System.md new file mode 100644 index 00000000000..b85d365bd65 --- /dev/null +++ b/docs/cn/Configuration_System.md @@ -0,0 +1,70 @@ +# 系统配置 + +本节重点介绍系统(JVM/OS)的配置 +--- + +## **1 JVM 选项** ## + +建议使用最新发布的 JDK 1.8 版本。设置相同的 Xms 和 Xmx 值以防止 JVM 调整堆大小,并获得更好的性能。一种通用的JVM配置如下: + + -server -Xms8g -Xmx8g -Xmn4g + +设置 Direct ByteBuffer 内存大小。当 Direct ByteBuffer 达到指定大小时,将触发 Full GC: + + -XXMaxDirectMemorySize=15g + +如果你不在乎 RocketMQ broker 的启动时间,建议启用预分配 Java 堆以确保在 JVM 初始化期间为每个页面分配内存。你可以通过以下方式启用它: + + -XX+AlwaysPreTouch + +禁用偏向锁定可以减少 JVM 停顿: + + -XX-UseBiasedLocking + +关于垃圾收集器,推荐使用 JDK 1.8 的 G1 收集器: + + -XX+UseG1GC -XXG1HeapRegionSize=16m + -XXG1ReservePercent=25 + -XXInitiatingHeapOccupancyPercent=30 + +这些 GC 选项看起来有点激进,但事实证明它在生产环境中具有良好的性能 + +不要把-XXMaxGCPauseMillis 的值设置太小,否则JVM会使用一个小的新生代来实现这个目标,从而导致频繁发生minor GC。因此,建议使用滚动 GC 日志文件: + + -XX+UseGCLogFileRotation + -XXNumberOfGCLogFiles=5 + -XXGCLogFileSize=30m + +写 GC 文件会增加 broker 的延迟,因此可以考虑将 GC 日志文件重定向到内存文件系统: + + -Xloggcdevshmmq_gc_%p.log123 + +## 2 Linux 内核参数 ## + +在 bin 文件夹里,有一个 os.sh 脚本,里面列出了许多的内核参数,只需稍作更改即可用于生产用途。需特别关注以下参数,如想了解更多细节,请参考文档/proc/sys/vm/*。 + + + +- **vm.extra_free_kbytes**, 控制VM在后台回收(kswapd)开始的阈值和直接回收(通过分配进程)开始的阈值之间保留额外的空闲内存。通过使用这个参数,RocketMQ 可以避免在内存分配过程中出现高延迟。(与内核版本有关) + + + +- **vm.min_free_kbytes**, 该值不应设置低于1024KB,否则系统将遭到破坏,并且在高负载环境下容易出现死锁。 + + + + + +- **vm.max_map_count**, 规定进程可以拥有的最大内存映射区域数。 RocketMQ 使用 mmap 来加载 CommitLog 和 ConsumeQueue,因此建议将此参数设置为较大的值。 + + + +- **vm.swappiness**, 定义内核交换内存页的频率。该值若较大,则会导致频繁交换,较小则会减少交换量。为了避免交换延迟,建议将此值设为 10。 + + + +- **File descriptor limits**, RocketMQ 需要给文件(CommitLog 和 ConsumeQueue)和网络连接分配文件描述符。因此建议将该值设置为 655350。 + + + +- **Disk scheduler**, 推荐使用deadline IO 调度器,它可以为请求提供有保证的延迟。 \ No newline at end of file diff --git a/docs/cn/Configuration_TLS.md b/docs/cn/Configuration_TLS.md new file mode 100644 index 00000000000..9ff03e53a2e --- /dev/null +++ b/docs/cn/Configuration_TLS.md @@ -0,0 +1,119 @@ +# TLS配置 +本节介绍TLS相关配置 + +## 1 生成证书 +开发、测试的证书可以自行安装OpenSSL进行生成.建议在Linux环境下安装Open SSL并进行证书生成。 + +### 1.1 生成ca.pem +```shell +openssl req -newkey rsa:2048 -keyout ca_rsa_private.pem -x509 -days 365 -out ca.pem +``` +### 1.2 生成server.csr +```shell +openssl req -newkey rsa:2048 -keyout server_rsa.key -out server.csr +``` +### 1.3 生成server.pem +```shell +openssl x509 -req -days 365 -in server.csr -CA ca.pem -CAkey ca_rsa_private.pem -CAcreateserial -out server.pem +``` +### 1.4 生成client.csr +```shell +openssl req -newkey rsa:2048 -keyout client_rsa.key -out client.csr +``` +### 1.5 生成client.pem +```shell +openssl x509 -req -days 365 -in client.csr -CA ca.pem -CAkey ca_rsa_private.pem -CAcreateserial -out client.pem +``` +### 1.6 生成server.key +```shell +openssl pkcs8 -topk8 -v1 PBE-SHA1-RC4-128 -in server_rsa.key -out server.key +``` +### 1.7 生成client.key +```shell +openssl pkcs8 -topk8 -v1 PBE-SHA1-RC4-128 -in client_rsa.key -out client.key +``` + +## 2 创建tls.properties +创建tls.properties文件,并将生成证书的路径和密码进行正确的配置. + + +```properties +# The flag to determine whether use test mode when initialize TLS context. default is true +tls.test.mode.enable=false +# Indicates how SSL engine respect to client authentication, default is none +tls.server.need.client.auth=require +# The store path of server-side private key +tls.server.keyPath=/opt/certFiles/server.key +# The password of the server-side private key +tls.server.keyPassword=123456 +# The store path of server-side X.509 certificate chain in PEM format +tls.server.certPath=/opt/certFiles/server.pem +# To determine whether verify the client endpoint's certificate strictly. default is false +tls.server.authClient=false +# The store path of trusted certificates for verifying the client endpoint's certificate +tls.server.trustCertPath=/opt/certFiles/ca.pem +``` + +如果需要客户端连接时也进行认证,则还需要在该文件中增加以下内容 +```properties +# The store path of client-side private key +tls.client.keyPath=/opt/certFiles/client.key +# The password of the client-side private key +tls.client.keyPassword=123456 +# The store path of client-side X.509 certificate chain in PEM format +tls.client.certPath=/opt/certFiles/client.pem +# To determine whether verify the server endpoint's certificate strictly +tls.client.authServer=false +# The store path of trusted certificates for verifying the server endpoint's certificate +tls.client.trustCertPath=/opt/certFiles/ca.pem +``` + + +## 3 配置Rocketmq启动参数 + +编辑rocketmq/bin路径下的配置文件,使tls.properties配置生效.-Dtls.config.file的值需要替换为步骤2中创建的tls.peoperties文件的路径 + +### 3.1 编辑runserver.sh,在JAVA_OPT中增加以下内容: +```shell +JAVA_OPT="${JAVA_OPT} -Dtls.server.mode=enforcing -Dtls.config.file=/opt/rocketmq-4.9.3/conf/tls.properties" +``` + +### 3.2 编辑runbroker.sh,在JAVA_OPT中增加以下内容: + +```shell +JAVA_OPT="${JAVA_OPT} -Dorg.apache.rocketmq.remoting.ssl.mode=enforcing -Dtls.config.file=/opt/rocketmq-4.9.3/conf/tls.properties -Dtls.enable=true" +``` + +# 4 客户端连接 + +创建客户端使用的tlsclient.properties,并加入以下内容: +```properties +# The store path of client-side private key +tls.client.keyPath=/opt/certFiles/client.key +# The password of the client-side private key +tls.client.keyPassword=123456 +# The store path of client-side X.509 certificate chain in PEM format +tls.client.certPath=/opt/certFiles/client.pem +# The store path of trusted certificates for verifying the server endpoint's certificate +tls.client.trustCertPath=/opt/certFiles/ca.pem +``` + +JVM中需要加以下参数.tls.config.file的值需要使用之前创建的文件: +```shell +-Dtls.client.authServer=true -Dtls.enable=true -Dtls.test.mode.enable=false -Dtls.config.file=/opt/certs/tlsclient.properties +``` + +在客户端连接的代码中,需要将setUseTLS设置为true: +```java +public class ExampleProducer { + public static void main(String[] args) throws Exception { + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + //setUseTLS should be true + producer.setUseTLS(true); + producer.start(); + + // Send messages as usual. + producer.shutdown(); + } +} +``` \ No newline at end of file diff --git a/docs/cn/Debug_In_Idea.md b/docs/cn/Debug_In_Idea.md new file mode 100644 index 00000000000..fd01751ee99 --- /dev/null +++ b/docs/cn/Debug_In_Idea.md @@ -0,0 +1,55 @@ +## 本地调试RocketMQ + +### Step0: 解决依赖问题 +1. 运行前下载RocketMQ需要的maven依赖,可以使用`mvn clean install -Dmaven.test.skip=true` +2. 确保本地能够编译通过 + +### Step1: 启动NameServer +1. NamerServer的启动类在`org.apache.rocketmq.namesrv.NamesrvStartup` +2. `Idea-Edit Configurations`中添加运行参数 `ROCKETMQ_HOME=` +![Idea_config_nameserver.png](image/Idea_config_nameserver.png) +3. 运行NameServer,观察到如下日志输出则启动成功 +```shell +The Name Server boot success. serializeType=JSON, address 0.0.0.0:9876 +``` + +### Step2: 启动Broker +1. Broker的启动类在`org.apache.rocketmq.broker.BrokerStartup` +2. 创建`/rocketmq/conf/broker.conf`文件或直接在官方release发布包中拷贝即可 +```shell +# broker.conf + +brokerClusterName = DefaultCluster +brokerName = broker-a +brokerId = 0 +deleteWhen = 04 +fileReservedTime = 48 +brokerRole = ASYNC_MASTER +flushDiskType = ASYNC_FLUSH +namesrvAddr = 127.0.0.1:9876 # name server地址 +``` +3. `Idea-Edit Configurations`中添加运行参数 `ROCKETMQ_HOME=` 以及环境变量`-c /Users/xxx/rocketmq/conf/broker.conf` +![Idea_config_broker.png](image/Idea_config_broker.png) +4. 运行Broker,观察到如下日志则启动成功 +```shell +The broker[broker-a,192.169.1.2:10911] boot success... +``` + +### Step3: 发送或消费消息 +至此已经完成了RocketMQ的启动,可以使用`/example`里的示例进行收发消息 + +### 补充:本地启动Proxy +1. RocketMQ5.x支持了Proxy模式,使用`LOCAL`模式可以免去`Step2`,启动类在`org.apache.rocketmq.proxy.ProxyStartup` +2. `Idea-Edit Configurations`中添加运行参数 `ROCKETMQ_HOME=` +3. 在`/conf/`下新建配置文件`rmq-proxy.json` +```json +{ + "rocketMQClusterName": "DefaultCluster", + "nameSrvAddr": "127.0.0.1:9876", + "proxyMode": "local" +} +``` +4. 运行Proxy,观察到如下日志则启动成功 +```shell +Sat Aug 26 15:29:33 CST 2023 rocketmq-proxy startup successfully +``` \ No newline at end of file diff --git a/docs/cn/Deployment.md b/docs/cn/Deployment.md new file mode 100644 index 00000000000..14529d111b0 --- /dev/null +++ b/docs/cn/Deployment.md @@ -0,0 +1,171 @@ +# 部署架构和设置步骤 + +## 集群的设置 + +### 1 单master模式 + +这是最简单但也是最危险的模式,一旦broker服务器重启或宕机,整个服务将不可用。 建议在生产环境中不要使用这种部署方式,在本地测试和开发可以选择这种模式。 以下是构建的步骤。 + +**1)启动NameServer** + +```shell +### 第一步启动namesrv +$ nohup sh mqnamesrv & + +### 验证namesrv是否启动成功 +$ tail -f ~/logs/rocketmqlogs/namesrv.log +The Name Server boot success... +``` + +我们可以在namesrv.log 中看到'The Name Server boot success..',表示NameServer 已成功启动。 + +**2)启动Broker** + +```shell +### 第一步先启动broker +$ nohup sh bin/mqbroker -n localhost:9876 & + +### 验证broker是否启动成功,比如,broker的ip是192.168.1.2 然后名字是broker-a +$ tail -f ~/logs/rocketmqlogs/Broker.log +The broker[broker-a,192.169.1.2:10911] boot success... +``` + +我们可以在 Broker.log 中看到“The broker[brokerName,ip:port] boot success..”,这表明 broker 已成功启动。 + +### 2 多Master模式 + +该模式是指所有节点都是master主节点(比如2个或3个主节点),没有slave从节点的模式。 这种模式的优缺点如下: + +- 优点: + 1. 配置简单。 + 2. 一个master节点的宕机或者重启(维护)对应用程序没有影响。 + 3. 当磁盘配置为RAID10时,消息不会丢失,因为RAID10磁盘非常可靠,即使机器不可恢复(消息异步刷盘模式的情况下,会丢失少量消息;如果消息是同步刷盘模式,不会丢失任何消息)。 + 4. 在这种模式下,性能是最高的。 +- 缺点: + 1. 单台机器宕机时,本机未消费的消息,直到机器恢复后才会订阅,影响消息实时性。 + +多Master模式的启动步骤如下: + +**1)启动 NameServer** + +```shell +### 第一步先启动namesrv +$ nohup sh mqnamesrv & + +### 验证namesrv是否启动成功 +$ tail -f ~/logs/rocketmqlogs/namesrv.log +The Name Server boot success... +``` + +**2)启动 Broker 集群** + +```shell +### 比如在A机器上启动第一个Master,假设配置的NameServer IP为:192.168.1.1 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-noslave/broker-a.properties & + +### 然后在机器B上启动第二个Master,假设配置的NameServer IP是:192.168.1.1 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-noslave/broker-b.properties & + +... +``` + +上面显示的启动命令用于单个NameServer的情况。对于多个NameServer的集群,broker 启动命令中-n参数后面的地址列表用分号隔开,例如 192.168.1.1:9876;192.161.2:9876 + +### 3 多Master多Slave模式-异步复制 + +每个主节点配置多个从节点,多对主从。HA采用异步复制,主节点和从节点之间有短消息延迟(毫秒)。这种模式的优缺点如下: + +- 优点: + 1. 即使磁盘损坏,也只会丢失极少的消息,不影响消息的实时性能。 + 2. 同时,当主节点宕机时,消费者仍然可以消费从节点的消息,这个过程对应用本身是透明的,不需要人为干预。 + 3. 性能几乎与多Master模式一样高。 +- 缺点: + 1. 主节点宕机、磁盘损坏时,会丢失少量消息。 + +多主多从模式的启动步骤如下: + +**1)启动 NameServer** + +```shell +### 第一步先启动namesrv +$ nohup sh mqnamesrv & + +### 验证namesrv是否启动成功 +$ tail -f ~/logs/rocketmqlogs/namesrv.log +The Name Server boot success... +``` + +**2)启动 Broker 集群** + +```shell +### 例如在A机器上启动第一个Master,假设配置的NameServer IP为:192.168.1.1,端口为9876。 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-async/broker-a.properties & + +### 然后在机器B上启动第二个Master,假设配置的NameServer IP为:192.168.1.1,端口为9876。 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-async/broker-b.properties & + +### 然后在C机器上启动第一个Slave,假设配置的NameServer IP为:192.168.1.1,端口为9876。 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-async/broker-a-s.properties & + +### 最后在D机启动第二个Slave,假设配置的NameServer IP为:192.168.1.1,端口为9876。 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-async/broker-b-s.properties & +``` + +上图显示了 2M-2S-Async 模式的启动命令,类似于其他 nM-nS-Async 模式。 + +### 4 多Master多Slave模式-同步双写 + +这种模式下,每个master节点配置多个slave节点,有多对Master-Slave。HA采用同步双写,即只有消息成功写入到主节点并复制到多个从节点,才会返回成功响应给应用程序。 + +这种模式的优缺点如下: + +- 优点: + 1. 数据和服务都没有单点故障。 + 2. 在master节点关闭的情况下,消息也没有延迟。 + 3. 服务可用性和数据可用性非常高; +- 缺点: + 1. 这种模式下的性能略低于异步复制模式(大约低 10%)。 + 2. 发送单条消息的RT略高,目前版本,master节点宕机后,slave节点无法自动切换到master。 + +启动步骤如下: + +**1)启动NameServer** + +```shell +### 第一步启动namesrv +$ nohup sh mqnamesrv & + +### 验证namesrv是否启动成功 +$ tail -f ~/logs/rocketmqlogs/namesrv.log +The Name Server boot success... +``` + +**2)启动 Broker 集群** + +```shell +### 例如在A机器上启动第一个Master,假设配置的NameServer IP为:192.168.1.1,端口为9876。 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-sync/broker-a.properties & + +### 然后在B机器上启动第二个Master,假设配置的NameServer IP为:192.168.1.1,端口为9876。 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-sync/broker-b.properties & + +### 然后在C机器上启动第一个Slave,假设配置的NameServer IP为:192.168.1.1,端口为9876。 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-sync/broker-a-s.properties & + +### 最后在D机启动第二个Slave,假设配置的NameServer IP为:192.168.1.1,端口为9876。 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-sync/broker-b-s.properties & +``` + +上述Master和Slave是通过指定相同的config命名为“brokerName”来配对的,master节点的brokerId必须为0,slave节点的brokerId必须大于0。 + +### 5 RocketMQ 5.0 自动主从切换 + +RocketMQ 5.0 开始支持自动主从切换的模式,可参考以下文档 + +[快速开始](controller/quick_start.md) + +[部署文档](controller/deploy.md) + +[设计思想](controller/design.md) + + diff --git a/docs/cn/Example_Batch.md b/docs/cn/Example_Batch.md new file mode 100644 index 00000000000..4edac9a326e --- /dev/null +++ b/docs/cn/Example_Batch.md @@ -0,0 +1,84 @@ +# 批量消息发送 +批量消息发送能够提高发送效率,提升系统吞吐量。同一批批量消息的topic、waitStoreMsgOK属性必须保持一致,批量消息不支持延迟消息。批量消息发送一次最多可以发送 4MiB 的消息,但是如果需要发送更大的消息,建议将较大的消息分成多个不超过 1MiB 的小消息。 + +### 1 发送批量消息 +如果你一次只发送不超过 4MiB 的消息,使用批处理很容易: +```java +String topic = "BatchTest"; +List messages = new ArrayList<>(); +messages.add(new Message(topic, "TagA", "OrderID001", "Hello world 0".getBytes())); +messages.add(new Message(topic, "TagA", "OrderID002", "Hello world 1".getBytes())); +messages.add(new Message(topic, "TagA", "OrderID003", "Hello world 2".getBytes())); +try { + producer.send(messages); +} catch (Exception e) { + e.printStackTrace(); + //handle the error +} +``` +### 2 拆分 +当您发送较大的消息时,复杂性会增加,如果您不确定它是否超过 4MiB的限制。 这时候,您最好将较大的消息分成多个不超过 1MiB 的小消息: + +```java +public class ListSplitter implements Iterator> { + private final int SIZE_LIMIT = 1024 * 1024 * 4; + private final List messages; + private int currIndex; + public ListSplitter(List messages) { + this.messages = messages; + } + @Override + public boolean hasNext() { + return currIndex < messages.size(); + } + @Override + public List next() { + int startIndex = getStartIndex(); + int nextIndex = startIndex; + int totalSize = 0; + for (; nextIndex < messages.size(); nextIndex++) { + Message message = messages.get(nextIndex); + int tmpSize = calcMessageSize(message); + if (tmpSize + totalSize > SIZE_LIMIT) { + break; + } else { + totalSize += tmpSize; + } + } + List subList = messages.subList(startIndex, nextIndex); + currIndex = nextIndex; + return subList; + } + private int getStartIndex() { + Message currMessage = messages.get(currIndex); + int tmpSize = calcMessageSize(currMessage); + while(tmpSize > SIZE_LIMIT) { + currIndex += 1; + Message message = messages.get(curIndex); + tmpSize = calcMessageSize(message); + } + return currIndex; + } + private int calcMessageSize(Message message) { + int tmpSize = message.getTopic().length() + message.getBody().length(); + Map properties = message.getProperties(); + for (Map.Entry entry : properties.entrySet()) { + tmpSize += entry.getKey().length() + entry.getValue().length(); + } + tmpSize = tmpSize + 20; // Increase the log overhead by 20 bytes + return tmpSize; + } +} + +// then you could split the large list into small ones: +ListSplitter splitter = new ListSplitter(messages); +while (splitter.hasNext()) { + try { + List listItem = splitter.next(); + producer.send(listItem); + } catch (Exception e) { + e.printStackTrace(); + // handle the error + } +} +``` \ No newline at end of file diff --git a/docs/cn/Example_Compaction_Topic_cn.md b/docs/cn/Example_Compaction_Topic_cn.md new file mode 100644 index 00000000000..6ebb5a986d2 --- /dev/null +++ b/docs/cn/Example_Compaction_Topic_cn.md @@ -0,0 +1,73 @@ +# Compaction Topic + +## 使用方式 + +### 打开namesrv上支持顺序消息的开关 +CompactionTopic依赖顺序消息来保障一致性 +```shell +$ bin/mqadmin updateNamesrvConfig -k orderMessageEnable -v true +``` + +### 创建compaction topic + +```shell +$ bin/mqadmin updateTopic -w 8 -r 8 -a +cleanup.policy=COMPACTION -n localhost:9876 -t ctopic -o true -c DefaultCluster +create topic to 127.0.0.1:10911 success. +TopicConfig [topicName=ctopic, readQueueNums=8, writeQueueNums=8, perm=RW-, topicFilterType=SINGLE_TAG, topicSysFlag=0, order=false, attributes={+cleanup.policy=COMPACTION}] +``` + +### 生产数据 + +与普通消息一样 + +```java +DefaultMQProducer producer = new DefaultMQProducer("CompactionTestGroup"); +producer.setNamesrvAddr("localhost:9876"); +producer.start(); + +String topic = "ctopic"; +String tag = "tag1"; +String key = "key1"; +Message msg = new Message(topic, tag, key, "bodys".getBytes(StandardCharsets.UTF_8)); +SendResult sendResult = producer.send(msg, (mqs, message, shardingKey) -> { + int select = Math.abs(shardingKey.hashCode()); + if (select < 0) { + select = 0; + } + return mqs.get(select % mqs.size()); +}, key); + +System.out.printf("%s%n", sendResult); +``` + +### 消费数据 + +消费offset与compaction之前保持不变,如果指定offset消费,当指定的offset不存在时,返回后面最近的一条数据 +在compaction场景下,大部分消费都是从0开始消费完整的数据 + +```java +DefaultLitePullConsumer consumer = new DefaultLitePullConsumer("compactionTestGroup"); +consumer.setNamesrvAddr("localhost:9876"); +consumer.setPullThreadNums(4); +consumer.start(); + +Collection messageQueueList = consumer.fetchMessageQueues("ctopic"); +consumer.assign(messageQueueList); +messageQueueList.forEach(mq -> { + try { + consumer.seekToBegin(mq); + } catch (MQClientException e) { + e.printStackTrace(); + } +}); + +Map kvStore = Maps.newHashMap(); +while (true) { + List msgList = consumer.poll(1000); + if (CollectionUtils.isNotEmpty(msgList)) { + msgList.forEach(msg -> kvStore.put(msg.getKeys(), msg.getBody())); + } +} + +//use the kvStore +``` \ No newline at end of file diff --git a/docs/cn/Example_CreateTopic.md b/docs/cn/Example_CreateTopic.md new file mode 100644 index 00000000000..ee975290edf --- /dev/null +++ b/docs/cn/Example_CreateTopic.md @@ -0,0 +1,26 @@ +# 创建主题 + +## 背景 + +RocketMQ 5.0 引入了 `TopicMessageType` 的概念,并且使用了现有的主题属性功能来实现它。 + +主题的创建是通过 `mqadmin` 工具来申明 `message.type` 属性。 + +## 使用案例 + +```shell +# default +sh ./mqadmin updateTopic -n -t -c DefaultCluster + +# normal topic +sh ./mqadmin updateTopic -n -t -c DefaultCluster -a +message.type=NORMAL + +# fifo topic +sh ./mqadmin updateTopic -n -t -c DefaultCluster -a +message.type=FIFO + +# delay topic +sh ./mqadmin updateTopic -n -t -c DefaultCluster -a +message.type=DELAY + +# transaction topic +sh ./mqadmin updateTopic -n -t -c DefaultCluster -a +message.type=TRANSACTION +``` diff --git a/docs/cn/Example_Delay.md b/docs/cn/Example_Delay.md new file mode 100644 index 00000000000..31df40f4485 --- /dev/null +++ b/docs/cn/Example_Delay.md @@ -0,0 +1,85 @@ +# Schedule example + +### 1 启动消费者等待传入的订阅消息 + +```java +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.common.message.MessageExt; +import java.util.List; + +public class ScheduledMessageConsumer { + + public static void main(String[] args) throws Exception { + // Instantiate message consumer + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ExampleConsumer"); + // Subscribe topics + consumer.subscribe("TestTopic", "*"); + // Register message listener + consumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List messages, ConsumeConcurrentlyContext context) { + for (MessageExt message : messages) { + // Print approximate delay time period + System.out.println("Receive message[msgId=" + message.getMsgId() + "] " + + (System.currentTimeMillis() - message.getStoreTimestamp()) + "ms later"); + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + // Launch consumer + consumer.start(); + } +} +``` + +### 2 发送延迟消息 + +```java +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.common.message.Message; + +public class ScheduledMessageProducer { + + public static void main(String[] args) throws Exception { + // Instantiate a producer to send scheduled messages + DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup"); + // Launch producer + producer.start(); + int totalMessagesToSend = 100; + for (int i = 0; i < totalMessagesToSend; i++) { + Message message = new Message("TestTopic", ("Hello scheduled message " + i).getBytes()); + // This message will be delivered to consumer 10 seconds later. + message.setDelayTimeLevel(3); + // Send the message + producer.send(message); + } + + // Shutdown producer after use. + producer.shutdown(); + } + +} +``` + +### 3 确认 + +您应该会看到消息在其存储时间后大约 10 秒被消耗。 + +### 4 延迟消息的使用场景 + +例如在电子商务中,如果提交订单,可以发送延迟消息,1小时后可以查看订单状态。 如果订单仍未付款,则可以取消订单并释放库存。 + +### 5 使用延迟消息的限制 + +```java +// org/apache/rocketmq/store/config/MessageStoreConfig.java + +private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"; +``` + +当前 RocketMQ 不支持任意时间的延迟。 生产者发送延迟消息前需要设置几个固定的延迟级别,分别对应1s到2h的1到18个延迟级,消息消费失败会进入延迟消息队列,消息发送时间与设置的延迟级别和重试次数有关。 + + See `SendMessageProcessor.java` diff --git a/docs/cn/Example_LMQ.md b/docs/cn/Example_LMQ.md new file mode 100644 index 00000000000..85a3db5005c --- /dev/null +++ b/docs/cn/Example_LMQ.md @@ -0,0 +1,85 @@ +# Light message queue (LMQ) +LMQ采用的读放大的策略,写一份数据,多个LMQ队列分发, +因为存储的成本和效率对用户的体感最明显。写多份不仅加大了存储成本,同时也对性能和数据准确一致性提出了挑战。 + +![](image/LMQ_1.png) + +上图描述的是LMQ的队列存储模型,消息可以来自各个接入场景 +(如服务端的MQ/AMQP,客户端的MQTT),但只会写一份存到commitlog里面,然后分发出多个需求场景的队列索引(ConsumerQueue),如服务端场景(MQ/AMQP)可以按照一级Topic队列进行传统的服务端消费,客户端MQTT场景可以按照MQTT多级Topic(也即 LMQ)进行消费消息。 + +## 一、broker启动配置 + + +broker.conf文件需要增加以下的配置项,开启LMQ开关,这样就可以识别LMQ相关属性的消息,进行原子分发消息到LMQ队列 +```properties +enableLmq = true +enableMultiDispatch = true +``` +## 二、发送消息 +发送消息的时候通过设置 INNER_MULTI_DISPATCH 属性,LMQ queue使用逗号分割,queue前缀必须是 %LMQ%,这样broker就可以识别LMQ queue. +以下代码只是demo伪代码 具体逻辑参照执行即可 +```java +DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); +producer.setNamesrvAddr("name-server1-ip:9876;name-server2-ip:9876"); +producer.start(); + + +/* +* Create a message instance, specifying topic, tag and message body. +*/ +Message msg = new Message("TopicTest" /* Topic */, + "TagA" /* Tag */, + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */ + ); +/* +* INNER_MULTI_DISPATCH property and PREFIX must start as "%LMQ%", +* If it is multiple LMQ, need to use “,” split +*/ +message.putUserProperty("INNER_MULTI_DISPATCH", "%LMQ%123,%LMQ%456"); +/* +* Call send message to deliver message to one of brokers. +*/ +SendResult sendResult = producer.send(msg); +``` +## 三、拉取消息 +LMQ queue在每个broker上只有一个queue,也即queueId为0, 指明轻量级的MessageQueue,就可以拉取消息进行消费。 +以下代码只是demo伪代码 具体逻辑参照执行即可 +```java +DefaultMQPullConsumer defaultMQPullConsumer = new DefaultMQPullConsumer(); +defaultMQPullConsumer.setNamesrvAddr("name-server1-ip:9876;name-server2-ip:9876"); +defaultMQPullConsumer.setVipChannelEnabled(false); +defaultMQPullConsumer.setConsumerGroup("CID_RMQ_SYS_LMQ_TEST"); +defaultMQPullConsumer.setInstanceName("CID_RMQ_SYS_LMQ_TEST"); +defaultMQPullConsumer.setRegisterTopics(new HashSet<>(Arrays.asList("TopicTest"))); +defaultMQPullConsumer.setBrokerSuspendMaxTimeMillis(2000); +defaultMQPullConsumer.setConsumerTimeoutMillisWhenSuspend(3000); +defaultMQPullConsumer.start(); + +String brokerName = "set broker Name"; +MessageQueue mq = new MessageQueue("%LMQ%123", brokerName, 0); +defaultMQPullConsumer.getDefaultMQPullConsumerImpl().getRebalanceImpl().getmQClientFactory().updateTopicRouteInfoFromNameServer("TopicTest"); + +Thread.sleep(30000); +Long offset = defaultMQPullConsumer.maxOffset(mq); + +defaultMQPullConsumer.pullBlockIfNotFound( + mq, "*", offset, 32, + new PullCallback() { + @Override + public void onSuccess(PullResult pullResult) { + List list = pullResult.getMsgFoundList(); + if (list == null || list.isEmpty()) { + return; + } + for (MessageExt messageExt : list) { + System.out.println(messageExt); + } + } + @Override + public void onException(Throwable e) { + + } +}); +``` +​ + diff --git a/docs/cn/Example_Simple_cn.md b/docs/cn/Example_Simple_cn.md new file mode 100644 index 00000000000..f0a2b6a1dea --- /dev/null +++ b/docs/cn/Example_Simple_cn.md @@ -0,0 +1,136 @@ +# Basic Sample +------ +基本示例中提供了以下两个功能 +* RocketMQ可用于以三种方式发送消息:可靠的同步、可靠的异步和单向传输。前两种消息类型是可靠的,因为无论它们是否成功发送都有响应。 +* RocketMQ可以用来消费消息。 +### 1 添加依赖 +maven: +``` java + + org.apache.rocketmq + rocketmq-client + 4.3.0 + +``` +gradle: +``` java +compile 'org.apache.rocketmq:rocketmq-client:4.3.0' +``` +### 2 发送消息 +##### 2.1 使用Producer发送同步消息 +可靠的同步传输被广泛应用于各种场景,如重要的通知消息、短消息通知等。 +``` java +public class SyncProducer { + public static void main(String[] args) throws Exception { + // Instantiate with a producer group name + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + // Specify name server addresses + producer.setNamesrvAddr("localhost:9876"); + // Launch the producer instance + producer.start(); + for (int i = 0; i < 100; i++) { + // Create a message instance with specifying topic, tag and message body + Message msg = new Message("TopicTest" /* Topic */, + "TagA" /* Tag */, + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */ + ); + // Send message to one of brokers + SendResult sendResult = producer.send(msg); + // Check whether the message has been delivered by the callback of sendResult + System.out.printf("%s%n", sendResult); + } + // Shut down once the producer instance is not longer in use + producer.shutdown(); + } +} +``` +##### 2.2 发送异步消息 +异步传输通常用于响应时间敏感的业务场景。这意味着发送方无法等待代理的响应太长时间。 +``` java +public class AsyncProducer { + public static void main(String[] args) throws Exception { + // Instantiate with a producer group name + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + // Specify name server addresses + producer.setNamesrvAddr("localhost:9876"); + // Launch the producer instance + producer.start(); + producer.setRetryTimesWhenSendAsyncFailed(0); + for (int i = 0; i < 100; i++) { + final int index = i; + // Create a message instance with specifying topic, tag and message body + Message msg = new Message("TopicTest", + "TagA", + "OrderID188", + "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); + // SendCallback: receive the callback of the asynchronous return result. + producer.send(msg, new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + System.out.printf("%-10d OK %s %n", index, + sendResult.getMsgId()); + } + @Override + public void onException(Throwable e) { + System.out.printf("%-10d Exception %s %n", index, e); + e.printStackTrace(); + } + }); + } + // Shut down once the producer instance is not longer in use + producer.shutdown(); + } +} +``` +##### 2.3 以单向模式发送消息 +单向传输用于需要中等可靠性的情况,如日志收集。 +``` java +public class OnewayProducer { + public static void main(String[] args) throws Exception{ + // Instantiate with a producer group name + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + // Specify name server addresses + producer.setNamesrvAddr("localhost:9876"); + // Launch the producer instance + producer.start(); + for (int i = 0; i < 100; i++) { + // Create a message instance with specifying topic, tag and message body + Message msg = new Message("TopicTest" /* Topic */, + "TagA" /* Tag */, + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */ + ); + // Send in one-way mode, no return result + producer.sendOneway(msg); + } + // Shut down once the producer instance is not longer in use + producer.shutdown(); + } +} +``` +### 3 消费消息 +``` java +public class Consumer { + public static void main(String[] args) throws InterruptedException, MQClientException { + // Instantiate with specified consumer group name + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name"); + + // Specify name server addresses + consumer.setNamesrvAddr("localhost:9876"); + + // Subscribe one or more topics and tags for finding those messages need to be consumed + consumer.subscribe("TopicTest", "*"); + // Register callback to execute on arrival of messages fetched from brokers + consumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); + // Mark the message that have been consumed successfully + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + // Launch the consumer instance + consumer.start(); + System.out.printf("Consumer Started.%n"); + } +} +``` \ No newline at end of file diff --git a/docs/cn/FAQ.md b/docs/cn/FAQ.md new file mode 100644 index 00000000000..b58807926a0 --- /dev/null +++ b/docs/cn/FAQ.md @@ -0,0 +1,110 @@ +# 经常被问到的问题 + +以下是关于RocketMQ项目的常见问题 + +## 1 基本 + +1. **为什么我们要使用RocketMQ而不是选择其他的产品?** + + 请参考[为什么要选择RocketMQ](http://rocketmq.apache.org/docs/motivation/) + +2. **我是否需要安装其他的软件才能使用RocketMQ,例如zookeeper?** + + 不需要,RocketMQ可以独立的运行。 + +## 2 使用 + +1. **新创建的Consumer ID从哪里开始消费消息?** + + 1)如果发送的消息在三天之内,那么消费者会从服务器中保存的第一条消息开始消费。 + + 2)如果发送的消息已经超过三天,则消费者会从服务器中的最新消息开始消费,也就是从队列的尾部开始消费。 + + 3)如果消费者重新启动,那么它会从最后一个消费位置开始消费消息。 + +2. **当消费失败的时候如何重新消费消息?** + + 1)在集群模式下,消费的业务逻辑代码会返回Action.ReconsumerLater,NULL,或者抛出异常,如果一条消息消费失败,最多会重试16次,之后该消息会被丢弃。 + + 2)在广播消费模式下,广播消费仍然保证消息至少被消费一次,但不提供重发的选项。 + +3. **当消费失败的时候如何找到失败的消息?** + + 1)使用按时间的主题查询,可以查询到一段时间内的消息。 + + 2)使用主题和消息ID来准确查询消息。 + + 3)使用主题和消息的Key来准确查询所有消息Key相同的消息。 + +4. **消息只会被传递一次吗?** + + RocketMQ 确保所有消息至少传递一次。 在大多数情况下,消息不会重复。 + +5. **如何增加一个新的Broker?** + + 1)启动一个新的Broker并将其注册到name server中的Broker列表里。 + + 2)默认只自动创建内部系统topic和consumer group。 如果您希望在新节点上拥有您的业务主题和消费者组,请从现有的Broker中复制它们。 我们提供了管理工具和命令行来处理此问题。 + +## 3 配置相关 + +以下回答均为默认值,可通过配置修改。 + +1. **消息在服务器上可以保存多长时间?** + + 存储的消息将最多保存 3 天,超过 3 天未使用的消息将被删除。 + +2. **消息体的大小限制是多少?** + + 通常是256KB + +3. **怎么设置消费者线程数?** + + 当你启动消费者的时候,可以设置 ConsumeThreadNums属性的值,举例如下: + + ```java + consumer.setConsumeThreadMin(20); + consumer.setConsumeThreadMax(20); + ``` + +## 4 错误 + +1. **当你启动一个生产者或消费者的过程失败了并且错误信息是生产者组或消费者重复** + + 原因:使用同一个Producer/Consumer Group在同一个JVM中启动多个Producer/Consumer实例可能会导致客户端无法启动。 + + 解决方案:确保一个 Producer/Consumer Group 对应的 JVM 只启动一个 Producer/Consumer 实例。 + +2. **消费者无法在广播模式下开始加载 json 文件** + + 原因:fastjson 版本太低,无法让广播消费者加载本地 offsets.json,导致消费者启动失败。 损坏的 fastjson 文件也会导致同样的问题。 + + 解决方案:Fastjson 版本必须升级到 RocketMQ 客户端依赖版本,以确保可以加载本地 offsets.json。 默认情况下,offsets.json 文件在 /home/{user}/.rocketmq_offsets 中。 或者检查fastjson的完整性。 + +3. **Broker崩溃以后有什么影响?** + + 1)Master节点崩溃 + + 消息不能再发送到该Broker集群,但是如果您有另一个可用的Broker集群,那么在主题存在的条件下仍然可以发送消息。消息仍然可以从Slave节点消费。 + + 2)一些Slave节点崩溃 + + 只要有另一个工作的slave,就不会影响发送消息。 对消费消息也不会产生影响,除非消费者组设置为优先从该Slave消费。 默认情况下,消费者组从 master 消费。 + + 3)所有Slave节点崩溃 + + 向master发送消息不会有任何影响,但是,如果master是SYNC_MASTER,producer会得到一个SLAVE_NOT_AVAILABLE,表示消息没有发送给任何slave。 对消费消息也没有影响,除非消费者组设置为优先从slave消费。 默认情况下,消费者组从master消费。 + +4. **Producer提示“No Topic Route Info”,如何诊断?** + + 当您尝试将消息发送到一个路由信息对生产者不可用的主题时,就会发生这种情况。 + + 1)确保生产者可以连接到名称服务器并且能够从中获取路由元信息。 + + 2)确保名称服务器确实包含主题的路由元信息。 您可以使用管理工具或 Web 控制台通过 topicRoute 从名称服务器查询路由元信息。 + + 3)确保您的Broker将心跳发送到您的生产者正在连接的同一name server列表。 + + 4)确保主题的权限为6(rw-),或至少为2(-w-)。 + + 如果找不到此主题,请通过管理工具命令updateTopic或Web控制台在Broker上创建它。 \ No newline at end of file diff --git a/docs/cn/QuorumACK.md b/docs/cn/QuorumACK.md new file mode 100644 index 00000000000..bbeb94189d6 --- /dev/null +++ b/docs/cn/QuorumACK.md @@ -0,0 +1,73 @@ +# Quorum Write和自动降级 + +## 背景 + +![](https://s4.ax1x.com/2022/02/05/HnWo2d.png) + +在RocketMQ中,主备之间的复制模式主要有同步复制和异步复制,如上图所示,Slave1的复制是同步的,在向Producer报告成功写入之前,Master需要等待Slave1成功复制该消息并确认,Slave2的复制是异步的,Master不需要等待Slave2的响应。在RocketMQ中,发送一条消息,如果一切都顺利,那最后会返回给Producer客户端一个PUT_OK的状态,如果是Slave同步超时则返回FLUSH_SLAVE_TIMEOUT状态,如果是Slave不可用或者Slave与Master之间CommitLog差距超过一定的值(默认是256MB),则返回SLAVE_NOT_AVAILABLE,后面两个状态并不会导致系统异常而无法写入下一条消息。 + +同步复制可以保证Master失效后,数据仍然能在Slave中找到,适合可靠性要求较高的场景。异步复制虽然消息可能会丢失,但是由于无需等待Slave的确认,效率上要高于同步复制,适合对效率有一定要求的场景。但是只有两种模式仍然不够灵活,比如在三副本甚至五副本且对可靠性要求高场景中,采用异步复制无法满足需求,但采用同步复制则需要每一个副本确认后才会返回,在副本数多的情况下严重影响效率。另一方面,在同步复制的模式下,如果副本组中的某一个Slave出现假死,整个发送将一直失败直到进行手动处理。 + +因此,RocketMQ 5 提出了副本组的quorum write,在同步复制的模式下,用户可以在broker端指定发送后至少需要写入多少副本数后才能返回,并且提供自适应降级的方式,可以根据存活的副本数以及CommitLog差距自动完成降级。 + +## 架构和参数 + +### Quorum Write + +通过增加两个参数来支持quorum write。 + +- **totalReplicas**:副本组broker总数。默认为1。 +- **inSyncReplicas**:正常情况需保持同步的副本组数量。默认为1。 + +通过这两个参数,可以在同步复制的模式下,灵活指定需要ACK的副本数。 + +![](https://s4.ax1x.com/2022/02/05/HnWHKI.png) + +如上图所示,在两副本情况下,如果inSyncReplicas为2,则该条消息需要在Master和Slave中均复制完成后才会返回给客户端;在三副本情况下,如果inSyncReplicas为2,则该条消息除了需要复制在Master上,还需要复制到任意一个slave上,才会返回给客户端。在四副本情况下,如果inSyncReplicas为3,则条消息除了需要复制在Master上,还需要复制到任意两个slave上,才会返回给客户端。通过灵活设置totalReplicas和inSyncReplicas,可以满足用户各类场景的需求。 + +### 自动降级 + +自动降级的标准是 + +- 当前副本组的存活副本数 +- Master Commitlog和Slave CommitLog的高度差 + +> **注意:自动降级只在slaveActingMaster模式开启后才生效** + +通过Nameserver的反向通知以及GetBrokerMemberGroup请求可以获取当前副本组的存活信息,而Master与Slave的Commitlog高度差也可以通过HA服务中的位点记录计算出来。将增加以下参数完成自动降级: + +- **minInSyncReplicas**:最小需保持同步的副本组数量,仅在enableAutoInSyncReplicas为true时生效,默认为1 +- **enableAutoInSyncReplicas**:自动同步降级开关,开启后,若当前副本组处于同步状态的broker数量(包括master自身)不满足inSyncReplicas指定的数量,则按照minInSyncReplicas进行同步。同步状态判断条件为:slave commitLog落后master长度不超过haSlaveFallBehindMax。默认为false。 +- **haMaxGapNotInSync**:slave是否与master处于in-sync状态的判断值,slave commitLog落后master长度超过该值则认为slave已处于非同步状态。当enableAutoInSyncReplicas打开时,该值越小,越容易触发master的自动降级,当enableAutoInSyncReplicas关闭,且totalReplicas==inSyncReplicas时,该值越小,越容易导致在大流量时发送请求失败,故在该情况下可适当调大haMaxGapNotInSync。默认为256K。 + +注意:在RocketMQ 4.x中存在haSlaveFallbehindMax参数,默认256MB,表明Slave与Master的CommitLog高度差多少后判定其为不可用,在[RIP-34](https://github.com/apache/rocketmq/wiki/RIP-34-Support-quorum-write-and-adaptive-degradation-in-master-slave-architecture)中该参数被取消。 + +```java +//计算needAckNums +int inSyncReplicas = Math.min(this.defaultMessageStore.getAliveReplicaNumInGroup(), + this.defaultMessageStore.getHaService().inSyncSlaveNums(currOffset) + 1); +needAckNums = calcNeedAckNums(inSyncReplicas); +if (needAckNums > inSyncReplicas) { + // Tell the producer, don't have enough slaves to handle the send request + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.IN_SYNC_REPLICAS_NOT_ENOUGH, null)); +} + +private int calcNeedAckNums(int inSyncReplicas) { + int needAckNums = this.defaultMessageStore.getMessageStoreConfig().getInSyncReplicas(); + if (this.defaultMessageStore.getMessageStoreConfig().isEnableAutoInSyncReplicas()) { + needAckNums = Math.min(needAckNums, inSyncReplicas); + needAckNums = Math.max(needAckNums, this.defaultMessageStore.getMessageStoreConfig().getMinInSyncReplicas()); + } + return needAckNums; +} +``` + +当enableAutoInSyncReplicas=true是开启自适应降级模式,当副本组中存活的副本数减少或Master和Slave Commitlog高度差过大时,都会进行自动降级,最小降级到minInSyncReplicas副本数。比如在两副本中,如果设置totalReplicas=2,InSyncReplicas=2,minInSyncReplicas=1,enableAutoInSyncReplicas=true,正常情况下,两个副本均会处于同步复制,当Slave下线或假死时,会进行自适应降级,producer只需要发送到master即成功。 + +## 兼容性 + +用户需要设置正确的参数才能完成正确的向后兼容。举个例子,假设用户原集群为两副本同步复制,在没有修改任何参数的情况下,升级到RocketMQ 5的版本,由于totalReplicas、inSyncReplicas默认都为1,将降级为异步复制,如果需要和以前行为保持一致,则需要将totalReplicas和inSyncReplicas均设置为2。 + +参考文档: + +- [RIP-34](https://github.com/apache/rocketmq/wiki/RIP-34-Support-quorum-write-and-adaptive-degradation-in-master-slave-architecture) \ No newline at end of file diff --git a/docs/cn/README.md b/docs/cn/README.md new file mode 100644 index 00000000000..acfbd2f69ff --- /dev/null +++ b/docs/cn/README.md @@ -0,0 +1,54 @@ +Apache RocketMQ开发者指南 +-------- + +##### 这个开发者指南旨在帮助您快速了解并使用 Apache RocketMQ + +### 1. 概念和特性 + +- [概念(Concept)](concept.md):介绍RocketMQ的基本概念模型。 + +- [特性(Features)](features.md):介绍RocketMQ实现的功能特性。 + + +### 2. 架构设计 + +- [架构(Architecture)](architecture.md):介绍RocketMQ部署架构和技术架构。 + +- [设计(Design)](design.md):介绍RocketMQ关键机制的设计原理,主要包括消息存储、通信机制、消息过滤、负载均衡、事务消息等。 + + +### 3. 样例 + +- [样例(Example)](RocketMQ_Example.md) :介绍RocketMQ的常见用法,包括基本样例、顺序消息样例、延时消息样例、批量消息样例、过滤消息样例、事务消息样例等。 + +### 4. 最佳实践 +- [最佳实践(Best Practice)](best_practice.md):介绍RocketMQ的最佳实践,包括生产者、消费者、Broker以及NameServer的最佳实践,客户端的配置方式以及JVM和linux的最佳参数配置。 +- [消息轨迹指南(Message Trace)](msg_trace/user_guide.md):介绍RocketMQ消息轨迹的使用方法。 +- [权限管理(Auth Management)](acl/user_guide.md):介绍如何快速部署和使用支持权限控制特性的RocketMQ集群。 +- [自动主从切换快速开始](controller/quick_start.md):RocketMQ 5.0 自动主从切换快速开始。 +- [自动主从切换部署升级指南](controller/deploy.md):RocketMQ 5.0 自动主从切换部署升级指南。 +- [Proxy 部署指南](proxy/deploy_guide.md):介绍如何部署Proxy (包括 `Local` 模式和 `Cluster` 模式). + +### 5. 运维管理 +- [集群部署(Operation)](operation.md):介绍单Master模式、多Master模式、多Master多slave模式等RocketMQ集群各种形式的部署方法以及运维工具mqadmin的使用方式。 + +### 6. RocketMQ 5.0 新特性 + +- [POP消费](https://github.com/apache/rocketmq/wiki/%5BRIP-19%5D-Server-side-rebalance,--lightweight-consumer-client-support) +- [StaticTopic](statictopic/RocketMQ_Static_Topic_Logic_Queue_设计.md) +- [BatchConsumeQueue](https://github.com/apache/rocketmq/wiki/RIP-26-Improve-Batch-Message-Processing-Throughput) +- [自动主从切换](controller/design.md) +- [BrokerContainer](BrokerContainer.md) +- [SlaveActingMaster模式](SlaveActingMasterMode.md) +- [Grpc Proxy](../../proxy/README.md) + +### 7. API Reference(待补充) + +- [DefaultMQProducer API Reference](client/java/API_Reference_DefaultMQProducer.md) + + + + + + + diff --git a/docs/cn/RocketMQ_Example.md b/docs/cn/RocketMQ_Example.md new file mode 100644 index 00000000000..ece3a56f229 --- /dev/null +++ b/docs/cn/RocketMQ_Example.md @@ -0,0 +1,1010 @@ +# 样例 +----- + * [目录](#样例) + * [1 基本样例](#1-基本样例) + * [1.1 加入依赖:](#11-加入依赖) + * [1.2 消息发送](#12-消息发送) + * [1、Producer端发送同步消息](#1producer端发送同步消息) + * [2、发送异步消息](#2发送异步消息) + * [3、单向发送消息](#3单向发送消息) + * [1.3 消费消息](#13-消费消息) + * [2 顺序消息样例](#2-顺序消息样例) + * [2.1 顺序消息生产](#21-顺序消息生产) + * [2.2 顺序消费消息](#22-顺序消费消息) + * [3 延时消息样例](#3-延时消息样例) + * [3.1 启动消费者等待传入订阅消息](#31-启动消费者等待传入订阅消息) + * [3.2 发送延时消息](#32-发送延时消息) + * [3.3 验证](#33-验证) + * [3.4 延时消息的使用场景](#34-延时消息的使用场景) + * [3.5 延时消息的使用限制](#35-延时消息的使用限制) + * [4 批量消息样例](#4-批量消息样例) + * [4.1 发送批量消息](#41-发送批量消息) + * [4.2 消息列表分割](#42-消息列表分割) + * [5 过滤消息样例](#5-过滤消息样例) + * [5.1 基本语法](#51-基本语法) + * [5.2 使用样例](#52-使用样例) + * [1、生产者样例](#1生产者样例) + * [2、消费者样例](#2消费者样例) + * [6 消息事务样例](#6-消息事务样例) + * [6.1 发送事务消息样例](#61-发送事务消息样例) + * [1、创建事务性生产者](#1创建事务性生产者) + * [2、实现事务的监听接口](#2实现事务的监听接口) + * [6.2 事务消息使用上的限制](#62-事务消息使用上的限制) + * [7 Logappender样例](#7-logappender样例) + * [7.1 log4j样例](#71-log4j样例) + * [7.2 log4j2样例](#72-log4j2样例) + * [7.3 logback样例](#73-logback样例) + * [8 OpenMessaging样例](#8-openmessaging样例) + * [8.1 OMSProducer样例](#81-omsproducer样例) + * [8.2 OMSPullConsumer](#82-omspullconsumer) + * [8.3 OMSPushConsumer](#83-omspushconsumer) +----- +## 1 基本样例 + + +在基本样例中我们提供如下的功能场景: + +* 使用RocketMQ发送三种类型的消息:同步消息、异步消息和单向消息。其中前两种消息是可靠的,因为会有发送是否成功的应答。 +* 使用RocketMQ来消费接收到的消息。 + +### 1.1 加入依赖: + +`maven:` +``` + + org.apache.rocketmq + rocketmq-client + 4.9.1 + +``` +`gradle` +``` +compile 'org.apache.rocketmq:rocketmq-client:4.3.0' +``` +### 1.2 消息发送 + +#### 1、Producer端发送同步消息 + +这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知。 +```java +public class SyncProducer { + public static void main(String[] args) throws Exception { + // 实例化消息生产者Producer + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + // 设置NameServer的地址 + producer.setNamesrvAddr("localhost:9876"); + // 启动Producer实例 + producer.start(); + for (int i = 0; i < 100; i++) { + // 创建消息,并指定Topic,Tag和消息体 + Message msg = new Message("TopicTest" /* Topic */, + "TagA" /* Tag */, + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */ + ); + // 发送消息到一个Broker + SendResult sendResult = producer.send(msg); + // 通过sendResult返回消息是否成功送达 + System.out.printf("%s%n", sendResult); + } + // 如果不再发送消息,关闭Producer实例。 + producer.shutdown(); + } +} +``` +#### 2、发送异步消息 + +异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。 + +```java +public class AsyncProducer { + public static void main(String[] args) throws Exception { + // 实例化消息生产者Producer + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + // 设置NameServer的地址 + producer.setNamesrvAddr("localhost:9876"); + // 启动Producer实例 + producer.start(); + producer.setRetryTimesWhenSendAsyncFailed(0); + + int messageCount = 100; + // 根据消息数量实例化倒计时计算器 + final CountDownLatch2 countDownLatch = new CountDownLatch2(messageCount); + for (int i = 0; i < messageCount; i++) { + final int index = i; + // 创建消息,并指定Topic,Tag和消息体 + Message msg = new Message("TopicTest", + "TagA", + "OrderID188", + "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); + // SendCallback接收异步返回结果的回调 + producer.send(msg, new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + countDownLatch.countDown(); + System.out.printf("%-10d OK %s %n", index, + sendResult.getMsgId()); + } + @Override + public void onException(Throwable e) { + countDownLatch.countDown(); + System.out.printf("%-10d Exception %s %n", index, e); + e.printStackTrace(); + } + }); + } + // 等待5s + countDownLatch.await(5, TimeUnit.SECONDS); + // 如果不再发送消息,关闭Producer实例。 + producer.shutdown(); + } +} +``` + +#### 3、单向发送消息 + +这种方式主要用在不特别关心发送结果的场景,例如日志发送。 + +```java +public class OnewayProducer { + public static void main(String[] args) throws Exception{ + // 实例化消息生产者Producer + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + // 设置NameServer的地址 + producer.setNamesrvAddr("localhost:9876"); + // 启动Producer实例 + producer.start(); + for (int i = 0; i < 100; i++) { + // 创建消息,并指定Topic,Tag和消息体 + Message msg = new Message("TopicTest" /* Topic */, + "TagA" /* Tag */, + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */ + ); + // 发送单向消息,没有任何返回结果 + producer.sendOneway(msg); + + } + // 如果不再发送消息,关闭Producer实例。 + producer.shutdown(); + } +} +``` + +### 1.3 消费消息 + +```java +public class Consumer { + + public static void main(String[] args) throws InterruptedException, MQClientException { + + // 实例化消费者 + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name"); + + // 设置NameServer的地址 + consumer.setNamesrvAddr("localhost:9876"); + + // 订阅一个或者多个Topic,以及Tag来过滤需要消费的消息 + consumer.subscribe("TopicTest", "*"); + // 注册回调实现类来处理从broker拉取回来的消息 + consumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); + // 标记该消息已经被成功消费 + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + // 启动消费者实例 + consumer.start(); + System.out.printf("Consumer Started.%n"); + } +} +``` + +2 顺序消息样例 +---------- + +消息有序指的是可以按照消息的发送顺序来消费(FIFO)。RocketMQ可以严格的保证消息有序,可以分为分区有序或者全局有序。 + +顺序消费的原理解析,在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列);而消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的。 + +下面用订单进行分区有序的示例。一个订单的顺序流程是:创建、付款、推送、完成。订单号相同的消息会被先后发送到同一个队列中,消费时,同一个OrderId获取到的肯定是同一个队列。 + +### 2.1 顺序消息生产 + +```java +package org.apache.rocketmq.example.order2; + +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.MessageQueueSelector; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageQueue; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** +* Producer,发送顺序消息 +*/ +public class Producer { + + public static void main(String[] args) throws Exception { + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + + producer.setNamesrvAddr("127.0.0.1:9876"); + + producer.start(); + + String[] tags = new String[]{"TagA", "TagC", "TagD"}; + + // 订单列表 + List orderList = new Producer().buildOrders(); + + Date date = new Date(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String dateStr = sdf.format(date); + for (int i = 0; i < 10; i++) { + // 加个时间前缀 + String body = dateStr + " Hello RocketMQ " + orderList.get(i); + Message msg = new Message("TopicTest", tags[i % tags.length], "KEY" + i, body.getBytes()); + + SendResult sendResult = producer.send(msg, new MessageQueueSelector() { + @Override + public MessageQueue select(List mqs, Message msg, Object arg) { + Long id = (Long) arg; //根据订单id选择发送queue + long index = id % mqs.size(); + return mqs.get((int) index); + } + }, orderList.get(i).getOrderId());//订单id + + System.out.println(String.format("SendResult status:%s, queueId:%d, body:%s", + sendResult.getSendStatus(), + sendResult.getMessageQueue().getQueueId(), + body)); + } + + producer.shutdown(); + } + + /** + * 订单的步骤 + */ + private static class OrderStep { + private long orderId; + private String desc; + + public long getOrderId() { + return orderId; + } + + public void setOrderId(long orderId) { + this.orderId = orderId; + } + + public String getDesc() { + return desc; + } + + public void setDesc(String desc) { + this.desc = desc; + } + + @Override + public String toString() { + return "OrderStep{" + + "orderId=" + orderId + + ", desc='" + desc + '\'' + + '}'; + } + } + + /** + * 生成模拟订单数据 + */ + private List buildOrders() { + List orderList = new ArrayList(); + + OrderStep orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111039L); + orderDemo.setDesc("创建"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111065L); + orderDemo.setDesc("创建"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111039L); + orderDemo.setDesc("付款"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103117235L); + orderDemo.setDesc("创建"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111065L); + orderDemo.setDesc("付款"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103117235L); + orderDemo.setDesc("付款"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111065L); + orderDemo.setDesc("完成"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111039L); + orderDemo.setDesc("推送"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103117235L); + orderDemo.setDesc("完成"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111039L); + orderDemo.setDesc("完成"); + orderList.add(orderDemo); + + return orderList; + } +} +``` + +### 2.2 顺序消费消息 + +```java +package org.apache.rocketmq.example.order2; + +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.MessageExt; + +import java.util.List; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +/** +* 顺序消息消费,带事务方式(应用可控制Offset什么时候提交) +*/ +public class ConsumerInOrder { + + public static void main(String[] args) throws Exception { + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3"); + consumer.setNamesrvAddr("127.0.0.1:9876"); + /** + * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费
    + * 如果非第一次启动,那么按照上次消费的位置继续消费 + */ + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + + consumer.subscribe("TopicTest", "TagA || TagC || TagD"); + + consumer.registerMessageListener(new MessageListenerOrderly() { + + Random random = new Random(); + + @Override + public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) { + context.setAutoCommit(true); + for (MessageExt msg : msgs) { + // 可以看到每个queue有唯一的consume线程来消费, 订单对每个queue(分区)有序 + System.out.println("consumeThread=" + Thread.currentThread().getName() + "queueId=" + msg.getQueueId() + ", content:" + new String(msg.getBody())); + } + + try { + //模拟业务逻辑处理中... + TimeUnit.SECONDS.sleep(random.nextInt(10)); + } catch (Exception e) { + e.printStackTrace(); + } + return ConsumeOrderlyStatus.SUCCESS; + } + }); + + consumer.start(); + + System.out.println("Consumer Started."); + } +} +``` + +3 延时消息样例 +---------- + +### 3.1 启动消费者等待传入订阅消息 + +```java + +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.common.message.MessageExt; +import java.util.List; + +public class ScheduledMessageConsumer { + public static void main(String[] args) throws Exception { + // 实例化消费者 + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ExampleConsumer"); + // 设置NameServer的地址 + consumer.setNamesrvAddr("localhost:9876"); + // 订阅Topics + consumer.subscribe("TestTopic", "*"); + // 注册消息监听者 + consumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List messages, ConsumeConcurrentlyContext context) { + for (MessageExt message : messages) { + // Print approximate delay time period + System.out.println("Receive message[msgId=" + message.getMsgId() + "] " + (System.currentTimeMillis() - message.getBornTimestamp()) + "ms later"); + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + // 启动消费者 + consumer.start(); + } +} + +``` + +### 3.2 发送延时消息 + +```java + +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.common.message.Message; + +public class ScheduledMessageProducer { + public static void main(String[] args) throws Exception { + // 实例化一个生产者来产生延时消息 + DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup"); + // 设置NameServer的地址 + producer.setNamesrvAddr("localhost:9876"); + // 启动生产者 + producer.start(); + int totalMessagesToSend = 100; + for (int i = 0; i < totalMessagesToSend; i++) { + Message message = new Message("TestTopic", ("Hello scheduled message " + i).getBytes()); + // 设置延时等级3,这个消息将在10s之后发送(现在只支持固定的几个时间,详看delayTimeLevel) + message.setDelayTimeLevel(3); + // 发送消息 + producer.send(message); + } + // 关闭生产者 + producer.shutdown(); + } +} +``` + +### 3.3 验证 + +您将会看到消息的消费比存储时间晚10秒。 + +### 3.4 延时消息的使用场景 +比如电商里,提交了一个订单就可以发送一个延时消息,1h后去检查这个订单的状态,如果还是未付款就取消订单释放库存。 + +### 3.5 延时消息的使用限制 + +```java +// org/apache/rocketmq/store/config/MessageStoreConfig.java + +private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"; +``` +现在RocketMq并不支持任意时间的延时,需要设置几个固定的延时等级,从1s到2h分别对应着等级1到18 +消息消费失败会进入延时消息队列,消息发送时间与设置的延时等级和重试次数有关,详见代码`SendMessageProcessor.java` + + +4 批量消息样例 +---------- + +批量发送消息能显著提高传递小消息的性能。限制是这些批量消息应该有相同的topic,相同的waitStoreMsgOK,而且不能是延时消息。此外,这一批消息的总大小不应超过4MB。 + +### 4.1 发送批量消息 + +如果您每次只发送不超过4MB的消息,则很容易使用批处理,样例如下: + +```java +String topic = "BatchTest"; +List messages = new ArrayList<>(); +messages.add(new Message(topic, "TagA", "OrderID001", "Hello world 0".getBytes())); +messages.add(new Message(topic, "TagA", "OrderID002", "Hello world 1".getBytes())); +messages.add(new Message(topic, "TagA", "OrderID003", "Hello world 2".getBytes())); +try { + producer.send(messages); +} catch (Exception e) { + e.printStackTrace(); + //处理error +} + +``` + +### 4.2 消息列表分割 + +复杂度只有当你发送大批量时才会增长,你可能不确定它是否超过了大小限制(4MB)。这时候你最好把你的消息列表分割一下: + +```java +public class ListSplitter implements Iterator> { + private final int SIZE_LIMIT = 1024 * 1024 * 4; + private final List messages; + private int currIndex; + public ListSplitter(List messages) { + this.messages = messages; + } + @Override + public boolean hasNext() { + return currIndex < messages.size(); + } + @Override + public List next() { + int startIndex = getStartIndex(); + int nextIndex = startIndex; + int totalSize = 0; + for (; nextIndex < messages.size(); nextIndex++) { + Message message = messages.get(nextIndex); + int tmpSize = calcMessageSize(message); + if (tmpSize + totalSize > SIZE_LIMIT) { + break; + } else { + totalSize += tmpSize; + } + } + List subList = messages.subList(startIndex, nextIndex); + currIndex = nextIndex; + return subList; + } + private int getStartIndex() { + Message currMessage = messages.get(currIndex); + int tmpSize = calcMessageSize(currMessage); + while(tmpSize > SIZE_LIMIT) { + currIndex += 1; + Message message = messages.get(currIndex); + tmpSize = calcMessageSize(message); + } + return currIndex; + } + private int calcMessageSize(Message message) { + int tmpSize = message.getTopic().length() + message.getBody().length; + Map properties = message.getProperties(); + for (Map.Entry entry : properties.entrySet()) { + tmpSize += entry.getKey().length() + entry.getValue().length(); + } + tmpSize = tmpSize + 20; // 增加⽇日志的开销20字节 + return tmpSize; + } +} +//把大的消息分裂成若干个小的消息 +ListSplitter splitter = new ListSplitter(messages); +while (splitter.hasNext()) { + try { + List listItem = splitter.next(); + producer.send(listItem); + } catch (Exception e) { + e.printStackTrace(); + //处理error + } +} +``` + +5 过滤消息样例 +---------- + +在大多数情况下,TAG是一个简单而有用的设计,其可以来选择您想要的消息。例如: + +```java +DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_EXAMPLE"); +consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC"); +``` + +消费者将接收包含TAGA或TAGB或TAGC的消息。但是限制是一个消息只能有一个标签,这对于复杂的场景可能不起作用。在这种情况下,可以使用SQL表达式筛选消息。SQL特性可以通过发送消息时的属性来进行计算。在RocketMQ定义的语法下,可以实现一些简单的逻辑。下面是一个例子: +``` +------------ +| message | +|----------| a > 5 AND b = 'abc' +| a = 10 | --------------------> Gotten +| b = 'abc'| +| c = true | +------------ +------------ +| message | +|----------| a > 5 AND b = 'abc' +| a = 1 | --------------------> Missed +| b = 'abc'| +| c = true | +------------ +``` +### 5.1 基本语法 + +RocketMQ只定义了一些基本语法来支持这个特性。你也可以很容易地扩展它。 + +- 数值比较,比如:**>,>=,<,<=,BETWEEN,=;** +- 字符比较,比如:**=,<>,IN;** +- **IS NULL** 或者 **IS NOT NULL;** +- 逻辑符号 **AND,OR,NOT;** + +常量支持类型为: + +- 数值,比如:**123,3.1415;** +- 字符,比如:**'abc',必须用单引号包裹起来;** +- **NULL**,特殊的常量 +- 布尔值,**TRUE** 或 **FALSE** + +只有使用push模式的消费者才能用使用SQL92标准的sql语句,接口如下: +``` +public void subscribe(finalString topic, final MessageSelector messageSelector) +``` + +### 5.2 使用样例 + +#### 1、生产者样例 + +发送消息时,你能通过`putUserProperty`来设置消息的属性 + +```java +DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); +producer.start(); +Message msg = new Message("TopicTest", + tag, + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) +); +// 设置一些属性 +msg.putUserProperty("a", String.valueOf(i)); +SendResult sendResult = producer.send(msg); + +producer.shutdown(); +``` + +#### 2、消费者样例 + +用MessageSelector.bySql来使用sql筛选消息 + +```java +DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4"); +// 只有订阅的消息有这个属性a, a >=0 and a <= 3 +consumer.subscribe("TopicTest", MessageSelector.bySql("a between 0 and 3"); +consumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } +}); +consumer.start(); + +``` + +6 消息事务样例 +---------- + +事务消息共有三种状态,提交状态、回滚状态、中间状态: + +- TransactionStatus.CommitTransaction: 提交事务,它允许消费者消费此消息。 +- TransactionStatus.RollbackTransaction: 回滚事务,它代表该消息将被删除,不允许被消费。 +- TransactionStatus.Unknown: 中间状态,它代表需要检查消息队列来确定状态。 + +### 6.1 发送事务消息样例 + +#### 1、创建事务性生产者 + +使用 `TransactionMQProducer`类创建生产者,并指定唯一的 `ProducerGroup`,就可以设置自定义线程池来处理这些检查请求。执行本地事务后、需要根据执行结果对消息队列进行回复。回传的事务状态在请参考前一节。 + +```java +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.common.message.MessageExt; +import java.util.List; +public class TransactionProducer { + public static void main(String[] args) throws MQClientException, InterruptedException { + TransactionListener transactionListener = new TransactionListenerImpl(); + TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name"); + ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue(2000), new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r); + thread.setName("client-transaction-msg-check-thread"); + return thread; + } + }); + producer.setExecutorService(executorService); + producer.setTransactionListener(transactionListener); + producer.start(); + String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"}; + for (int i = 0; i < 10; i++) { + try { + Message msg = + new Message("TopicTest1234", tags[i % tags.length], "KEY" + i, + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); + SendResult sendResult = producer.sendMessageInTransaction(msg, null); + System.out.printf("%s%n", sendResult); + Thread.sleep(10); + } catch (MQClientException | UnsupportedEncodingException e) { + e.printStackTrace(); + } + } + for (int i = 0; i < 100000; i++) { + Thread.sleep(1000); + } + producer.shutdown(); + } +} + +``` +#### 2、实现事务的监听接口 + +当发送半消息成功时,我们使用 `executeLocalTransaction` 方法来执行本地事务。它返回前一节中提到的三个事务状态之一。`checkLocalTransaction` 方法用于检查本地事务状态,并回应消息队列的检查请求。它也是返回前一节中提到的三个事务状态之一。 + +```java +public class TransactionListenerImpl implements TransactionListener { + private AtomicInteger transactionIndex = new AtomicInteger(0); + private ConcurrentHashMap localTrans = new ConcurrentHashMap<>(); + @Override + public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { + int value = transactionIndex.getAndIncrement(); + int status = value % 3; + localTrans.put(msg.getTransactionId(), status); + return LocalTransactionState.UNKNOW; + } + @Override + public LocalTransactionState checkLocalTransaction(MessageExt msg) { + Integer status = localTrans.get(msg.getTransactionId()); + if (null != status) { + switch (status) { + case 0: + return LocalTransactionState.UNKNOW; + case 1: + return LocalTransactionState.COMMIT_MESSAGE; + case 2: + return LocalTransactionState.ROLLBACK_MESSAGE; + } + } + return LocalTransactionState.COMMIT_MESSAGE; + } +} + +``` + +### 6.2 事务消息使用上的限制 + +1. 事务消息不支持延时消息和批量消息。 +2. 为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为 15 次,但是用户可以通过 Broker 配置文件的 `transactionCheckMax`参数来修改此限制。如果已经检查某条消息超过 N 次的话( N = `transactionCheckMax` ) 则 Broker 将丢弃此消息,并在默认情况下同时打印错误日志。用户可以通过重写 `AbstractTransactionalMessageCheckListener` 类来修改这个行为。 +3. 事务消息将在 Broker 配置文件中的参数 transactionTimeout 这样的特定时间长度之后被检查。当发送事务消息时,用户还可以通过设置用户属性 CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于 `transactionTimeout` 参数。 +4. 事务性消息可能不止一次被检查或消费。 +5. 提交给用户的目标主题消息可能会失败,目前这依日志的记录而定。它的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望确保事务消息不丢失、并且事务完整性得到保证,建议使用同步的双重写入机制。 +6. 事务消息的生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不同,事务消息允许反向查询、MQ服务器能通过它们的生产者 ID 查询到消费者。 + +7 Logappender样例 +----------------- + +RocketMQ日志提供log4j、log4j2和logback日志框架作为业务应用,下面是配置样例 + +### 7.1 log4j样例 + +按下面样例使用log4j属性配置 +``` +log4j.appender.mq=org.apache.rocketmq.logappender.log4j.RocketmqLog4jAppender +log4j.appender.mq.Tag=yourTag +log4j.appender.mq.Topic=yourLogTopic +log4j.appender.mq.ProducerGroup=yourLogGroup +log4j.appender.mq.NameServerAddress=yourRocketmqNameserverAddress +log4j.appender.mq.layout=org.apache.log4j.PatternLayout +log4j.appender.mq.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-4r [%t] (%F:%L) %-5p - %m%n +``` +按下面样例使用log4j xml配置来使用异步添加日志 +``` + +      + + + +``` +### 7.2 log4j2样例 + +用log4j2时,配置如下,如果想要非阻塞,只需要使用异步添加引用即可 +``` + + +``` +### 7.3 logback样例 +``` +yourTagyourLogTopicyourLogGroupyourRocketmqNameserverAddress +      %date %p %t - %m%n + +1024802000true + +``` + +8 OpenMessaging样例 +--------------- + + [OpenMessaging](https://www.google.com/url?q=http://openmessaging.cloud/&sa=D&ust=1546524111089000)旨在建立消息和流处理规范,以为金融、电子商务、物联网和大数据领域提供通用框架及工业级指导方案。在分布式异构环境中,设计原则是面向云、简单、灵活和独立于语言。符合这些规范将帮助企业方便的开发跨平台和操作系统的异构消息传递应用程序。提供了openmessaging-api 0.3.0-alpha的部分实现,下面的示例演示如何基于OpenMessaging访问RocketMQ。 + +### 8.1 OMSProducer样例 + +下面的示例演示如何在同步、异步或单向传输中向RocketMQ代理发送消息。 + +```java +import io.openmessaging.Future; +import io.openmessaging.FutureListener; +import io.openmessaging.Message; +import io.openmessaging.MessagingAccessPoint; +import io.openmessaging.OMS; +import io.openmessaging.producer.Producer; +import io.openmessaging.producer.SendResult; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CountDownLatch; + +public class SimpleProducer { + public static void main(String[] args) { + final MessagingAccessPoint messagingAccessPoint = + OMS.getMessagingAccessPoint("oms:rocketmq://localhost:9876/default:default"); + final Producer producer = messagingAccessPoint.createProducer(); + messagingAccessPoint.startup(); + System.out.printf("MessagingAccessPoint startup OK%n"); + producer.startup(); + System.out.printf("Producer startup OK%n"); + { + Message message = producer.createBytesMessage("OMS_HELLO_TOPIC", "OMS_HELLO_BODY".getBytes(StandardCharsets.UTF_8)); + SendResult sendResult = producer.send(message); + //final Void aVoid = result.get(3000L); + System.out.printf("Send async message OK, msgId: %s%n", sendResult.messageId()); + } + final CountDownLatch countDownLatch = new CountDownLatch(1); + { + final Future result = producer.sendAsync(producer.createBytesMessage("OMS_HELLO_TOPIC", "OMS_HELLO_BODY".getBytes(StandardCharsets.UTF_8))); + result.addListener(new FutureListener() { + @Override + public void operationComplete(Future future) { + if (future.getThrowable() != null) { + System.out.printf("Send async message Failed, error: %s%n", future.getThrowable().getMessage()); + } else { + System.out.printf("Send async message OK, msgId: %s%n", future.get().messageId()); + } + countDownLatch.countDown(); + } + }); + } + { + producer.sendOneway(producer.createBytesMessage("OMS_HELLO_TOPIC", "OMS_HELLO_BODY".getBytes(StandardCharsets.UTF_8))); + System.out.printf("Send oneway message OK%n"); + } + try { + countDownLatch.await(); + Thread.sleep(500); // 等一些时间来发送消息 + } catch (InterruptedException ignore) { + } + producer.shutdown(); + } +} +``` + +### 8.2 OMSPullConsumer + +用OMS PullConsumer 来从指定的队列中拉取消息 + +```java +import io.openmessaging.Message; +import io.openmessaging.MessagingAccessPoint; +import io.openmessaging.OMS; +import io.openmessaging.OMSBuiltinKeys; +import io.openmessaging.consumer.PullConsumer; +import io.openmessaging.producer.Producer; +import io.openmessaging.producer.SendResult; + +public class SimplePullConsumer { + public static void main(String[] args) { + final MessagingAccessPoint messagingAccessPoint = + OMS.getMessagingAccessPoint("oms:rocketmq://localhost:9876/default:default"); + messagingAccessPoint.startup(); + final Producer producer = messagingAccessPoint.createProducer(); + final PullConsumer consumer = messagingAccessPoint.createPullConsumer( + OMS.newKeyValue().put(OMSBuiltinKeys.CONSUMER_ID, "OMS_CONSUMER")); + messagingAccessPoint.startup(); + System.out.printf("MessagingAccessPoint startup OK%n"); + final String queueName = "TopicTest"; + producer.startup(); + Message msg = producer.createBytesMessage(queueName, "Hello Open Messaging".getBytes()); + SendResult sendResult = producer.send(msg); + System.out.printf("Send Message OK. MsgId: %s%n", sendResult.messageId()); + producer.shutdown(); + consumer.attachQueue(queueName); + consumer.startup(); + System.out.printf("Consumer startup OK%n"); + // 运行直到发现一个消息被发送了 + boolean stop = false; + while (!stop) { + Message message = consumer.receive(); + if (message != null) { + String msgId = message.sysHeaders().getString(Message.BuiltinKeys.MESSAGE_ID); + System.out.printf("Received one message: %s%n", msgId); + consumer.ack(msgId); + if (!stop) { + stop = msgId.equalsIgnoreCase(sendResult.messageId()); + } + } else { + System.out.printf("Return without any message%n"); + } + } + consumer.shutdown(); + messagingAccessPoint.shutdown(); + } +} +``` + +### 8.3 OMSPushConsumer + +以下示范如何将 OMS PushConsumer 添加到指定的队列,并通过 MessageListener 消费这些消息。 + +```java +import io.openmessaging.Message; +import io.openmessaging.MessagingAccessPoint; +import io.openmessaging.OMS; +import io.openmessaging.OMSBuiltinKeys; +import io.openmessaging.consumer.MessageListener; +import io.openmessaging.consumer.PushConsumer; + +public class SimplePushConsumer { + public static void main(String[] args) { + final MessagingAccessPoint messagingAccessPoint = OMS + .getMessagingAccessPoint("oms:rocketmq://localhost:9876/default:default"); + final PushConsumer consumer = messagingAccessPoint. + createPushConsumer(OMS.newKeyValue().put(OMSBuiltinKeys.CONSUMER_ID, "OMS_CONSUMER")); + messagingAccessPoint.startup(); + System.out.printf("MessagingAccessPoint startup OK%n"); + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + @Override + public void run() { + consumer.shutdown(); + messagingAccessPoint.shutdown(); + } + })); + consumer.attachQueue("OMS_HELLO_TOPIC", new MessageListener() { + @Override + public void onReceived(Message message, Context context) { + System.out.printf("Received one message: %s%n", message.sysHeaders().getString(Message.BuiltinKeys.MESSAGE_ID)); + context.ack(); + } + }); + consumer.startup(); + System.out.printf("Consumer startup OK%n"); + } +} +``` diff --git a/docs/cn/SlaveActingMasterMode.md b/docs/cn/SlaveActingMasterMode.md new file mode 100644 index 00000000000..b1e266f2b42 --- /dev/null +++ b/docs/cn/SlaveActingMasterMode.md @@ -0,0 +1,164 @@ +# Slave Acting Master模式 + +## 背景 + +![](https://s4.ax1x.com/2022/02/05/HnW3CQ.png) + +上图为当前RocketMQ Master-Slave冷备部署,在该部署方式下,即使一个Master掉线,发送端仍然可以向其他Master发送消息,对于消费端而言,若开启备读,Consumer会自动重连到对应的Slave机器,不会出现消费停滞的情况。但也存在以下问题: + +1. 一些仅限于在Master上进行的操作将无法进行,包括且不限于: + +- searchOffset +- maxOffset +- minOffset +- earliestMsgStoreTime +- endTransaction + +所有锁MQ相关操作,包括lock,unlock,lockBatch,unlockAll + +具体影响为: +- 客户端无法获取位于该副本组的mq的锁,故当本地锁过期后,将无法消费该组的顺序消息 +- 客户端无法主动结束处于半状态的事务消息,只能等待broker回查事务状态 +- Admin tools或控制中依赖查询offset及earliestMsgStoreTime等操作在该组上无法生效 + +2. 故障Broker组上的二级消息消费将会中断,该类消息特点依赖Master Broker上的线程扫描CommitLog上的特殊Topic,并将满足要求的消息投放回CommitLog,如果Master Broker下线,会出现二级消息的消费延迟或丢失。具体会影响到当前版本的延迟消息消费、事务消息消费、Pop消费。 + +3. 没有元数据的反向同步。Master重新被人工拉起后,容易造成元数据的回退,如Master上线后将落后的消费位点同步给备,该组broker的消费位点回退,造成大量消费重复。 + +![](https://s4.ax1x.com/2022/02/05/HnWwUU.png) + +上图为DLedger(Raft)架构,其可以通过选主一定程度上规避上述存在的问题,但可以看到DLedger模式下当前需要强制三副本及以上。 + +提出一个新的方案,Slave代理Master模式,作为Master-Slave部署模式的升级。在原先Master-Slave部署模式下,通过备代理主、轻量级心跳、副本组信息获取、broker预上线机制、二级消息逃逸等方式,当同组Master发生故障时,Slave将承担更加重要的作用,包括: + +- 当Master下线后,该组中brokerId最小的Slave会承担备读 以及 一些 客户端和管控会访问 但却只能在Master节点上完成的任务。包括且不限于searchOffset、maxOffset、minOffset、earliestMsgStoreTime、endTransaction以及所有锁MQ相关操作lock,unlock,lockBatch,unlockAll。 +- 当Master下线后,故障Broker组上的二级消息消费将不会中断,由该组中该组中brokerId最小的Slave承担起该任务,定时消息、Pop消息、事务消息等仍然可以正常运行。 +- 当Master下线后,在Slave代理Master一段时间主后,然后当Master再次上线后,通过预上线机制,Master会自动完成元数据的反向同步后再上线,不会出现元数据回退,造成消息大量重复消费或二级消息大量重放。 + +## 架构 + +### 备代理主 + +Master下线后Slave能正常消费,且在不修改客户端代码情况下完成只能在Master完成的操作源自于Namesrv对“代理”Master的支持。此处“代理”Master指的是,当副本组处于无主状态时,Namesrv将把brokerId最小的存活Slave视为“代理”Master,具体表现为在构建TopicRouteData时,将该Slave的brokerId替换为0,并将brokerPermission修改为4(Read-Only),从而使得该Slave在客户端视图中充当只读模式的Master的角色。 + +此外,当Master下线后,brokerId最小的Slave会承担起二级消息的扫描和重新投递功能,这也是“代理”的一部分。 + +```java +//改变二级消息扫描状态 +public void changeSpecialServiceStatus(boolean shouldStart) { + …… + + //改变延迟消息服务的状态 + changeScheduleServiceStatus(shouldStart); + + //改变事务消息服务的状态 + changeTransactionCheckServiceStatus(shouldStart); + + //改变Pop消息服务状态 + if (this.ackMessageProcessor != null) { + LOG.info("Set PopReviveService Status to {}", shouldStart); + this.ackMessageProcessor.setPopReviveServiceStatus(shouldStart); + } +} +``` + +### 轻量级心跳 + +如上文所述,brokerId最小的存活Slave在Master故障后开启自动代理Master模式,因此需要一种机制,这个机制需要保证: + +1. Nameserver能及时发现broker上下线并完成路由替换以及下线broker的路由剔除。 + +2. Broker能及时感知到同组Broker的上下线情况。 + +针对1,Nameserver原本就存在判活机制,定时会扫描不活跃的broker使其下线,而原本broker与nameserver的“心跳”则依赖于registerBroker操作,而这个操作涉及到topic信息上报,过于“重”,而且注册间隔过于长,因此需要一个轻量级的心跳机制,RoccketMQ 5.0在nameserver和broker间新增BrokerHeartbeat请求,broker会定时向nameserver发送心跳,若nameserver定时任务扫描发现超过心跳超时时间仍未收到该broker的心跳,将unregister该broker。registerBroker时会完成心跳超时时间的设置,并且注册时如果发现broker组内最小brokerId发生变化,将反向通知该组所有broker,并在路由获取时将最小brokerId的Slave路由替换使其充当只读模式的Master的角色 + +针对2,通过两个机制来及时感知同组broker上下线情况,1是上文中介绍的当nameserver发现该broker组内最小brokerId发生变化,反向通知该组所有broker。2是broker自身会有定时任务,向nameserver同步本broker组存活broker的信息,RoccketMQ 5.0会新增GetBrokerMemberGroup请求来完成该工作。 + +Slave Broker发现自己是该组中最小的brokerId,将会开启代理模式,而一旦Master Broker重新上线,Slave Broker同样会通过Nameserver反向通知或自身定时任务同步同组broker的信息感知到,并自动结束代理模式。 + +### 二级消息逃逸 + +代理模式开启后,brokerId最小的Slave会承担起二级消息的扫描和重新投递功能。 + +二级消息一般分为两个阶段,发送或者消费时会发送到一个特殊topic中,后台会有线程会扫描,最终的满足要求的消息会被重新投递到Commitlog中。我们可以让brokerId最小的Slave进行扫描,但如果扫描之后的消息重新投递到本Commitlog,那将会破坏Slave不可写的语义,造成Commitlog分叉。因此RoccketMQ 5.0提出一种逃逸机制,将重放的二级消息远程或本地投放到其他Master的Commitlog中。 + +- 远程逃逸 + +![](https://s4.ax1x.com/2022/02/05/HnWWVK.png) + +如上图所示,假设Region A发生故障,Region B中的节点2将会承担二级消息的扫描任务,同时将最终的满足要求的消息通过EscapeBridge远程发送到当前Broker集群中仍然存活的Master上。 + +- 本地逃逸 + +![](https://s4.ax1x.com/2022/02/05/HnWfUO.png) + +本地逃逸需要在BrokerContainer下进行,如果BrokerContainer中存在存活的Master,会优先向同进程的Master Commitlog中逃逸,避免远程RPC。 + +#### 各类二级消息变化 + +**延迟消息** + +Slave代理Master时,ScheduleMessageService将启动,时间到期的延迟消息将通过EscapeBridge优先往本地Master逃逸,若没有则向远程的Master逃逸。该broker上存量的时间未到期的消息将会被逃逸到存活的其他Master上,数据量上如果该broker上有大量的延迟消息未到期,远程逃逸会造成集群内部会有较大数据流转,但基本可控。 + + +**POP消息** + +1. CK/ACK拼key的时候增加brokerName属性。这样每个broker能在扫描自身commitlog的revive topic时抵消其他broker的CK/ACK消息。 + +2. Slave上的CK/ACK消息将被逃逸到其他指定的Master A上(需要同一个Master,否则CK/ACK无法抵消,造成消息重复),Master A扫描自身Commitlog revive消息并进行抵消,若超时,则将根据CK消息中的信息向Slave拉取消息(若本地有则拉取本地,否则远程拉取),然后投放到本地的retry topic中。 + +数据量上,如果是远程投递或拉取,且有消费者大量通过Pop消费存量的Slave消息,并且长时间不ACK,则在集群内部会有较大数据流转。 + +### 预上线机制 + +![](https://s4.ax1x.com/2022/02/05/HnW5Pe.png) + +当Master Broker下线后,Slave Broker将承担备读的作用,并对二级消息进行代理,因此Slave Broker中的部分元数据包括消费位点、定时消息进度等会比下线的Master Broker更加超前。如果Master Broker重新上线,Slave Broker元数据将被Master Broker覆盖,该组Broker元数据将发生回退,可能造成大量消息重复。因此,需要一套预上线机制来完成元数据的反向同步。 + +需要为consumerOffset和delayOffset等元数据增加版本号(DataVersion)的概念,并且为了防止版本号更新太频繁,增加更新步长的概念,比如对于消费位点来说,默认每更新位点超过500次,版本号增加到下一个版本。 + +如上图所示,Master Broker启动前会进行预上线,再预上线之前,对外不可见(Broker会有isIsolated标记自己的状态,当其为true时,不会像nameserver注册和发送心跳),因此也不会对外提供服务,二级消息的扫描流程也不会进行启动,具体预上线机制如下: + +1. Master Broker向NameServer获取Slave Broker地址(GetBrokerMemberGroup请求),但不注册 +2. Master Broker向Slave Broker发送自己的状态信息和地址 +3. Slave Broker得到Master Broker地址后和状态信息后,建立HA连接,并完成握手,进入Transfer状态 +4. Master Broker再完成握手后,反向获取备的元数据,包括消费位点、定时消息进度等,根据版本号决定是否更新。 +5. Master Broker对broker组内所有Slave Broker都完成1-4步操作后,正式上线,向NameServer注册,正式对外提供服务。 + +### 锁Quorum + +当Slave代理Master时,外部看到的是“只读”的Master,因此顺序消息仍然可以对队列上锁,消费不会中断。但当真的Master重新上线后,在一定的时间差内可能会造成多个consumer锁定同一个队列,比如一个consumer仍然锁着代理的备某一个队列,一个consumer锁刚上线的主的同一队列,造成顺序消息的乱序和重复。 + +因此在lock操作时要求,需锁broker副本组的大多数成员(quorum原则)均成功才算锁成功。但两副本下达不到quorum的原则,所以提供了lockInStrictMode参数,表示消费端消费顺序消息锁队列时是否使用严格模式。严格模式即对单个队列而言,需锁副本组的大多数成员(quorum原则)均成功才算锁成功,非严格模式即锁任意一副本成功就算锁成功,该参数默认为false。当对消息顺序性高于可用性时,需将该参数设置为false。 + +## 配置更新 + +Nameserver + +- scanNotActiveBrokerInterval:扫描不活跃broker间隔,每次扫描将判断broker心跳是否超时,默认5s。 +- supportActingMaster:nameserver端是否支持Slave代理Master模式,开启后,副本组在无master状态下,brokerId==1的slave将在TopicRoute中被替换成master(即brokerId=0),并以只读模式对客户端提供服务,默认为false。 + +Broker + +- enableSlaveActingMaster:broker端开启slave代理master模式总开关,默认为false。 +- enableRemoteEscape:是否允许远程逃逸,默认为false。 +- brokerHeartbeatInterval:broker向nameserver发送心跳间隔(不同于注册间隔),默认1s。 +- brokerNotActiveTimeoutMillis:broker不活跃超时时间,超过此时间nameserver仍未收到broker心跳,则判定broker下线,默认10s。 +- sendHeartbeatTimeoutMillis:broker发送心跳请求超时时间,默认1s。 +- lockInStrictMode:消费端消费顺序消息锁队列时是否使用严格模式,默认为false,上文已介绍。 +- skipPreOnline:broker跳过预上线流程,默认为false。 +- compatibleWithOldNameSrv:是否以兼容模式访问旧nameserver,默认为true。 + +## 兼容性方案 + +新版nameserver和旧版broker:新版nameserver可以完全兼容旧版broker,无兼容问题。 + +旧版nameserver和新版Broker:新版Broker开启Slave代理Master,会向Nameserver发送 BROKER_HEARTBEAT以及GET_BROKER_MEMBER_GROUP请求,但由于旧版本nameserver无法处理这些请求。因此需要在brokerConfig中配置compatibleWithOldNameSrv=true,开启对旧版nameserver的兼容模式,在该模式下,broker的一些新增RPC将通过复用原有RequestCode实现,具体为: +新增轻量级心跳将通过复用QUERY_DATA_VERSION实现 +新增获取BrokerMemberGroup数据将通过复用GET_ROUTEINFO_BY_TOPIC实现,具体实现方式是每个broker都会新增rmq_sys_{brokerName}的系统topic,通过获取该系统topic的路由来获取该副本组的存活信息。 +但旧版nameserver无法提供代理功能,Slave代理Master的功能将无法生效,但不影响其他功能。 + +客户端对新旧版本的nameserver和broker均无兼容性问题。 + + +参考文档:[原RIP](https://github.com/apache/rocketmq/wiki/RIP-32-Slave-Acting-Master-Mode) \ No newline at end of file diff --git "a/docs/cn/acl/RocketMQ_Multiple_ACL_Files_\350\256\276\350\256\241.md" "b/docs/cn/acl/RocketMQ_Multiple_ACL_Files_\350\256\276\350\256\241.md" new file mode 100644 index 00000000000..d457b67566d --- /dev/null +++ "b/docs/cn/acl/RocketMQ_Multiple_ACL_Files_\350\256\276\350\256\241.md" @@ -0,0 +1,137 @@ +# Version记录 +| 时间 | 主要内容 | 作者 | +| --- | --- | --- | +| 2022-01-27 | 初版,包括需求背景、兼容性影响、重要业务逻辑和后续扩展性考虑 | sunxi92 | + +中文文档在描述特定专业术语时,仍然使用英文。 +# 需求背景 +RocketMQ ACL特性目前只支持单个ACL配置文件,当存在很多用户时该配置文件会非常大,因此提出支持多ACL配置文件的想法。 +如果支持该特性那么也方便对RocketMQ用户进行分类。 + +# 兼容性影响 +当前在支持多ACL配置文件特性的设计上是向前兼容的。 + +# 重要业务逻辑 +## 1. ACL配置文件存储路径 +ACL配置文件夹是在RocketMQ安装目录下的conf/acl目录中,也可以在该路径新建子目录并在子目录中新建ACL配置文件,同时也保留了之前默认的配置文件conf/plain_acl.yml。 +注意:目前用户还不能自定义配置文件目录。 +## 2. ACL配置文件更新 +热感知:当检测到ACL配置文件改动会自动刷新数据,判断ACL配置文件是否发生变化的依据是文件的修改时间是否发生变化 +## 3. RocketMQ Broker缓存ACL配置信息数据结构设计 +- aclPlainAccessResourceMap + +aclPlainAccessResourceMap是个Map类型,用来缓存所有ACL配置文件的权限数据,其中key表示ACL配置文件的绝对路径, +value表示相应配置文件中的权限数据,需要注意的是value也是一个Map类型,其中key是String类型表示AccessKey,value是PlainAccessResource类型。 +- accessKeyTable + +accessKeyTable是个Map类型,用来缓存AccessKey和ACL配置文件的映射关系,其中key表示AccessKey,value表示ACL配置文件的绝对路径。 +- globalWhiteRemoteAddressStrategy + +globalWhiteRemoteAddressStrategy用来缓存所有ACL配置文件的全局白名单。 +- globalWhiteRemoteAddressStrategyMap + +globalWhiteRemoteAddressStrategyMap是个Map类型,用来缓存ACL配置文件和全局白名单的映射关系 +- dataVersionMap + +dataVersionMap是个Map类型,用来缓存所有ACL配置文件的DataVersion,其中key表示ACL配置文件的绝对路径,value表示该配置文件对应的DataVersion。 +## 4.加载和监控ACL配置文件 +### 4.1 加载ACL配置文件 +- load() + +load()方法会获取"RocketMQ安装目录/conf/acl"目录(包括该目录的子目录)和"rocketmq.acl.plain.file"下所有ACL配置文件,然后遍历这些文件读取权限数据和全局白名单。 +- load(String aclFilePath) + +load(String aclFilePath)方法完成加载指定ACL配置文件内容的功能,将配置文件中的全局白名单globalWhiteRemoteAddresses和用户权限accounts加载到缓存中, +这里需要注意以下几点: + +(1)判断缓存中该配置文件的全局白名单globalWhiteRemoteAddresses和用户权限accounts数据是否为空,如果不为空则需要注意删除文件原有数据 + +(2)相同的accessKey只允许存在在一个ACL配置文件中 +### 4.2 监控ACL配置文件 +watch()方法用来监控"RocketMQ安装目录/conf/acl"目录下所有ACL配置文件和"rocketmq.acl.plain.file"是否发生变化,变化考虑两种情况:一种是ACL配置文件的数量发生变化, +此时会调用load()方法重新加载所有配置文件的数据;一种是配置文件的内容发生变化;具体完成监控ACL配置文件变化的是AclFileWatchService服务, +该服务是一个线程,当启动该服务后它会以WATCH_INTERVAL(该参数目前设置为5秒,目前还不能在Broker配置文件中设置)的时间间隔来执行其核心逻辑。在该服务中会记录其监控的ACL配置文件目录aclPath、 +ACL配置文件的数量aclFilesNum、所有ACL配置文件绝对路径fileList以及每个ACL配置文件最近一次修改的时间fileLastModifiedTime +(Map类型,key为ACL配置文件的绝对路径,value为其最近一次修改时间)。 +该服务的核心逻辑如下: +获取ACL配置文件数量并和aclFilesNum进行比较是否相等,如果不相等则更新aclFilesNum和fileList并调用load()方法重新加载所有配置文件; +如果相等则遍历每个ACL配置文件,获取其最近一次修改的时间,并将该时间与fileLastModifiedTime中记录的时间进行比较,如果不相等则表示该文件发生过修改, +此时调用load(String aclFilePath)方法重新加载该配置文件。 + +## 5. 权限数据相关操作修改 +(1) updateAclConfigFileVersion(Map updateAclConfigMap) + +添加对缓存dataVersionMap的修改 + +(2)updateAccessConfig(PlainAccessConfig plainAccessConfig) + +将该方法原有的逻辑修改为:首先判断accessKeyTable中是否包含待修改的accessKey,如果包含则根据accessKey来获取其对应的ACL配置文件绝对路径, +再根据该路径更新aclPlainAccessResourceMap中缓存的数据,最后将该ACL配置文件中的数据写回原文件;如果不包含则会将数据写到"rocketmq.acl.plain.file"配置文件中, +然后更新accessKeyTable和aclPlainAccessResourceMap,最后最后将该ACL配置文件中的数据写回原文件。 + +(3)deleteAccessConfig(String accessKey) + +将该方法原有的逻辑修改为:判断accessKeyTable中是否存在accessKey,如果不存在则返回false,否则将其删除并将修改后的数据写回原文件。 + +(4)getAllAclConfig() + +fileList中存储了所有ACL配置文件的绝对路径,遍历fileList分别从各ACL配置文件中读取数据并组装返回 + +(5)updateGlobalWhiteAddrsConfig(List globalWhiteAddrsList, String fileName) + +该方法是新增的,完成功能是修改指定ACL配置文件的全局白名单,为后续添加相关运维命令做准备 +## 6. ACL相关运维命令修改 +(1)ClusterAclConfigVersionListSubCommand + +将printClusterBaseInfo(final DefaultMQAdminExt defaultMQAdminExt, final String addr)方法原有的逻辑修改为: +获取全部的ACL配置文件的DataVersion并输出。注意:获取的全部ACL配置文件的DataVersion集合可能为空,这里需要添加判断 + +(2)GetBrokerAclConfigResponseHeader + +在GetBrokerAclConfigResponseHeader中新增allAclFileVersion字段,它是个Map类型,其key表示ACL配置文件的绝对路径,value表示对应ACL配置文件的DataVersion + +(3)ClusterAclVersionInfo + +在ClusterAclVersionInfo中废弃了aclConfigDataVersion属性,增加了allAclConfigDataVersion属性,该属性是个Map类型,用来存储所有ACL配置文件的版本数据, +其中key表示ACL配置文件的绝对路径,value表示对应ACL配置文件的DataVersion + +## 7. 关于ACL配置文件DataVersion存储修改 + +在原来版本中ACL权限数据存储在一个配置文件中,所以只记录了该配置文件的DataVersion,而现在需要支持多个配置文件特性,每个配置文件都有自己的DataVersion, +为了能够准确记录所有配置文件的DataVersion,需要调整相关类型的属性、接口及方法。 + +(1)PlainPermissionManager + +对PlainPermissionManager属性的修改具体如下: + +- 废弃dataVersion属性,该属性在历史版本中是用来存来存储默认ACL配置文件的DataVersion + +- 新增dataVersionMap属性用来缓存所有ACL配置文件的DataVersion,它是一个Map类型,其key表示ACL配置文件的绝对路径,value表示对应配置文件的DataVersion + +(2)AccessValidator + +对AccessValidator的修改如下: + +- 废弃String getAclConfigVersion();,该接口原来是获取ACL配置文件文件的版本数据 + +- 新增Map getAllAclConfigVersion();该接口是用来获取所有ACL配置文件的版本数据,接口会返回一个Map类型数据, +key表示各ACL配置文件的绝对路径,value表示对应配置文件的版本数据 + +(3)PlainAccessValidator + +由于PlainAccessValidator实现了AccessValidator接口,所以相应地增加了getAllAclConfigVersion()方法 + +# 后续扩展性考虑 +1.目前的修改只支持ACL配置文件存储在"RocketMQ安装目录/conf/acl"目录下,后续可以考虑支持多目录; + +2.目前ACL配置文件路径是不支持让用户指定,后续可以考虑让用户指定指定ACL配置文件的存储路径 + +3.当前updateGlobalWhiteAddrsConfig命令只支持修改"rocketmq.acl.plain.file"文件中全局白名单, +后续可以扩展为修改指定ACL配置文件的全局白名单(如果参数中没有传ACL配置文件则会修改"rocketmq.acl.plain.file"文件) + +4.目前ACL数据中的secretKey是以明文形式存储在文件中,在一些对此类信息敏感的行业是不允许以明文落地,后续可以考虑安全性问题 + +5.目前ACL数据存储只支持文件形式存储,后续可以考虑增加数据库存储 + + + diff --git a/docs/cn/acl/user_guide.md b/docs/cn/acl/user_guide.md new file mode 100644 index 00000000000..463a28d8ce4 --- /dev/null +++ b/docs/cn/acl/user_guide.md @@ -0,0 +1,169 @@ +# 权限控制 +---- + + +## 1.权限控制特性介绍 +权限控制(ACL)主要为RocketMQ提供Topic资源级别的用户访问控制。用户在使用RocketMQ权限控制时,可以在Client客户端通过 RPCHook注入AccessKey和SecretKey签名;同时,将对应的权限控制属性(包括Topic访问权限、IP白名单和AccessKey和SecretKey签名等)设置在distribution/conf/plain_acl.yml的配置文件中。Broker端对AccessKey所拥有的权限进行校验,校验不过,抛出异常; +ACL客户端可以参考:**org.apache.rocketmq.example.simple**包下面的**AclClient**代码。 + +## 2. 权限控制的定义与属性值 +### 2.1权限定义 +对RocketMQ的Topic资源访问权限控制定义主要如下表所示,分为以下四种 + + +| 权限 | 含义 | +| --- | --- | +| DENY | 拒绝 | +| ANY | PUB 或者 SUB 权限 | +| PUB | 发送权限 | +| SUB | 订阅权限 | + +### 2.2 权限定义的关键属性 +| 字段 | 取值 | 含义 | +| --- | --- | --- | +| globalWhiteRemoteAddresses | \*;192.168.\*.\*;192.168.0.1 | 全局IP白名单 | +| accessKey | 字符串 | Access Key | +| secretKey | 字符串 | Secret Key | +| whiteRemoteAddress | \*;192.168.\*.\*;192.168.0.1 | 用户IP白名单 | +| admin | true;false | 是否管理员账户 | +| defaultTopicPerm | DENY;PUB;SUB;PUB\|SUB | 默认的Topic权限 | +| defaultGroupPerm | DENY;PUB;SUB;PUB\|SUB | 默认的ConsumerGroup权限 | +| topicPerms | topic=权限 | 各个Topic的权限 | +| groupPerms | group=权限 | 各个ConsumerGroup的权限 | + +具体可以参考**distribution/conf/plain_acl.yml**配置文件 + +## 3. 支持权限控制的集群部署 +在**distribution/conf/plain_acl.yml**配置文件中按照上述说明定义好权限属性后,打开**aclEnable**开关变量即可开启RocketMQ集群的ACL特性。这里贴出Broker端开启ACL特性的properties配置文件内容: +``` +brokerClusterName=DefaultCluster +brokerName=broker-a +brokerId=0 +deleteWhen=04 +fileReservedTime=48 +brokerRole=ASYNC_MASTER +flushDiskType=ASYNC_FLUSH +storePathRootDir=/data/rocketmq/rootdir-a-m +storePathCommitLog=/data/rocketmq/commitlog-a-m +autoCreateSubscriptionGroup=true +## if acl is open,the flag will be true +aclEnable=true +listenPort=10911 +brokerIP1=XX.XX.XX.XX1 +namesrvAddr=XX.XX.XX.XX:9876 +``` + +## 4. 权限控制主要流程 +ACL主要流程分为两部分,主要包括权限解析和权限校验。 + +### 4.1 权限解析 +Broker端对客户端的RequestCommand请求进行解析,拿到需要鉴权的属性字段。 +主要包括: +(1)AccessKey:类似于用户名,代指用户主体,权限数据与之对应; +(2)Signature:客户根据 SecretKey 签名得到的串,服务端再用SecretKey进行签名验证; + +### 4.2 权限校验 +Broker端对权限的校验逻辑主要分为以下几步: +(1)检查是否命中全局 IP 白名单;如果是,则认为校验通过;否则走 2; +(2)检查是否命中用户 IP 白名单;如果是,则认为校验通过;否则走 3; +(3)校验签名,校验不通过,抛出异常;校验通过,则走 4; +(4)对用户请求所需的权限 和 用户所拥有的权限进行校验;不通过,抛出异常; +用户所需权限的校验需要注意已下内容: +(1)特殊的请求例如 UPDATE_AND_CREATE_TOPIC 等,只能由 admin 账户进行操作; +(2)对于某个资源,如果有显性配置权限,则采用配置的权限;如果没有显性配置权限,则采用默认的权限; + +## 5. 热加载修改后权限控制定义 +RocketMQ的权限控制存储的默认实现是基于yml配置文件。用户可以动态修改权限控制定义的属性,而不需重新启动Broker服务节点。 + +## 6. 权限控制的使用限制 +(1)如果ACL与高可用部署(Master/Slave架构)同时启用,那么需要在Broker Master节点的distribution/conf/plain_acl.yml配置文件中 +设置全局白名单信息,即为将Slave节点的ip地址设置至Master节点plain_acl.yml配置文件的全局白名单中。 + +(2)如果ACL与高可用部署(多副本Dledger架构)同时启用,由于出现节点宕机时,Dledger Group组内会自动选主,那么就需要将Dledger Group组 +内所有Broker节点的plain_acl.yml配置文件的白名单设置所有Broker节点的ip地址。 + +## 7. ACL mqadmin配置管理命令 + +### 7.1 更新ACL配置文件中“account”的属性值 + +该命令的示例如下: + +sh mqadmin updateAclConfig -n 192.168.1.2:9876 -b 192.168.12.134:10911 -a RocketMQ -s 1234567809123 +-t topicA=DENY,topicD=SUB -g groupD=DENY,groupB=SUB + +说明:如果不存在则会在ACL Config YAML配置文件中创建;若存在,则会更新对应的“accounts”的属性值; +如果指定的是集群名称,则会在集群中各个broker节点执行该命令;否则会在单个broker节点执行该命令。 + +| 参数 | 取值 | 含义 | +| --- | --- | --- | +| n | eg:192.168.1.2:9876 | namesrv地址(必填) | +| c | eg:DefaultCluster | 指定集群名称(与broker地址二选一) | +| b | eg:192.168.12.134:10911 | 指定broker地址(与集群名称二选一) | +| a | eg:RocketMQ | Access Key值(必填) | +| s | eg:1234567809123 | Secret Key值(可选) | +| m | eg:true | 是否管理员账户(可选) | +| w | eg:192.168.0.* | whiteRemoteAddress,用户IP白名单(可选) | +| i | eg:DENY;PUB;SUB;PUB\|SUB | defaultTopicPerm,默认Topic权限(可选) | +| u | eg:DENY;PUB;SUB;PUB\|SUB | defaultGroupPerm,默认ConsumerGroup权限(可选) | +| t | eg:topicA=DENY,topicD=SUB | topicPerms,各个Topic的权限(可选) | +| g | eg:groupD=DENY,groupB=SUB | groupPerms,各个ConsumerGroup的权限(可选) | + +### 7.2 删除ACL配置文件里面的对应“account” +该命令的示例如下: + +sh mqadmin deleteAccessConfig -n 192.168.1.2:9876 -c DefaultCluster -a RocketMQ + +说明:如果指定的是集群名称,则会在集群中各个broker节点执行该命令;否则会在单个broker节点执行该命令。 +其中,参数"a"为Access Key的值,用以标识唯一账户id,因此该命令的参数中指定账户id即可。 + +| 参数 | 取值 | 含义 | +| --- | --- | --- | +| n | eg:192.168.1.2:9876 | namesrv地址(必填) | +| c | eg:DefaultCluster | 指定集群名称(与broker地址二选一) | +| b | eg:192.168.12.134:10911 | 指定broker地址(与集群名称二选一) | +| a | eg:RocketMQ | Access Key的值(必填) | + + +### 7.3 更新ACL配置文件里面中的全局白名单 +该命令的示例如下: + +sh mqadmin updateGlobalWhiteAddr -n 192.168.1.2:9876 -b 192.168.12.134:10911 -g 10.10.154.1,10.10.154.2 + +说明:如果指定的是集群名称,则会在集群中各个broker节点执行该命令;否则会在单个broker节点执行该命令。 +其中,参数"g"为全局IP白名的值,用以更新ACL配置文件中的“globalWhiteRemoteAddresses”字段的属性值。 + +| 参数 | 取值 | 含义 | +| --- | --- | --- | +| n | eg:192.168.1.2:9876 | namesrv地址(必填) | +| c | eg:DefaultCluster | 指定集群名称(与broker地址二选一) | +| b | eg:192.168.12.134:10911 | 指定broker地址(与集群名称二选一) | +| g | eg:10.10.154.1,10.10.154.2 | 全局IP白名单(必填) | + +### 7.4 查询集群/Broker的ACL配置文件版本信息 +该命令的示例如下: + +sh mqadmin clusterAclConfigVersion -n 192.168.1.2:9876 -c DefaultCluster + +说明:如果指定的是集群名称,则会在集群中各个broker节点执行该命令;否则会在单个broker节点执行该命令。 + +| 参数 | 取值 | 含义 | +| --- | --- | --- | +| n | eg:192.168.1.2:9876 | namesrv地址(必填) | +| c | eg:DefaultCluster | 指定集群名称(与broker地址二选一) | +| b | eg:192.168.12.134:10911 | 指定broker地址(与集群名称二选一) | + +### 7.5 查询集群/Broker的ACL配置文件全部内容 +该命令的示例如下: + +sh mqadmin getAclConfig -n 192.168.1.2:9876 -c DefaultCluster + +说明:如果指定的是集群名称,则会在集群中各个broker节点执行该命令;否则会在单个broker节点执行该命令。 + +| 参数 | 取值 | 含义 | +| --- | --- | --- | +| n | eg:192.168.1.2:9876 | namesrv地址(必填) | +| c | eg:DefaultCluster | 指定集群名称(与broker地址二选一) | +| b | eg:192.168.12.134:10911 | 指定broker地址(与集群名称二选一) | + +**特别注意**开启Acl鉴权认证后导致Master/Slave和Dledger模式下Broker同步数据异常的问题, +在社区[4.5.1]版本中已经修复,具体的PR链接为:#1149。 diff --git a/docs/cn/architecture.md b/docs/cn/architecture.md new file mode 100644 index 00000000000..87e93d18329 --- /dev/null +++ b/docs/cn/architecture.md @@ -0,0 +1,45 @@ +# 架构设计 +--- +## 1 技术架构 +![](image/rocketmq_architecture_1.png) + +RocketMQ架构上主要分为四部分,如上图所示: + +- Producer:消息发布的角色,支持分布式集群方式部署。Producer通过MQ的负载均衡模块选择相应的Broker集群队列进行消息投递,投递的过程支持快速失败并且低延迟。 + +- Consumer:消息消费的角色,支持分布式集群方式部署。支持以push推,pull拉两种模式对消息进行消费。同时也支持集群方式和广播方式的消费,它提供实时消息订阅机制,可以满足大多数用户的需求。 + +- NameServer:NameServer是一个非常简单的Topic路由注册中心,其角色类似Dubbo中的zookeeper,支持Broker的动态注册与发现。主要包括两个功能:Broker管理,NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活;路由信息管理,每个NameServer将保存关于Broker集群的整个路由信息和用于客户端查询的队列信息。然后Producer和Consumer通过NameServer就可以知道整个Broker集群的路由信息,从而进行消息的投递和消费。NameServer通常也是集群的方式部署,各实例间相互不进行信息通讯。Broker是向每一台NameServer注册自己的路由信息,所以每一个NameServer实例上面都保存一份完整的路由信息。当某个NameServer因某种原因下线了,Broker仍然可以向其它NameServer同步其路由信息,Producer和Consumer仍然可以动态感知Broker的路由的信息。 + +- BrokerServer:Broker主要负责消息的存储、投递和查询以及服务高可用保证,为了实现这些功能,Broker包含了以下几个重要子模块。 + 1. Remoting Module:整个Broker的实体,负责处理来自Client端的请求。 + 2. Client Manager:负责管理客户端(Producer/Consumer)和维护Consumer的Topic订阅信息。 + 3. Store Service:提供方便简单的API接口处理消息存储到物理硬盘和查询功能。 + 4. HA Service:高可用服务,提供Master Broker 和 Slave Broker之间的数据同步功能。 + 5. Index Service:根据特定的Message key对投递到Broker的消息进行索引服务,以提供消息的快速查询。 + +![](image/rocketmq_architecture_2.png) + +## 2 部署架构 + + +![](image/rocketmq_architecture_3.png) + + +### RocketMQ 网络部署特点 + +- NameServer是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。 + +- Broker部署相对复杂,Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave 的对应关系通过指定相同的BrokerName,不同的BrokerId 来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。每个Broker与NameServer集群中的所有节点建立长连接,定时注册Topic信息到所有NameServer。 注意:当前RocketMQ版本在部署架构上支持一Master多Slave,但只有BrokerId=1的从服务器才会参与消息的读负载。 + +- Producer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic 服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。 + +- Consumer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,消费者在向Master拉取消息时,Master服务器会根据拉取偏移量与最大偏移量的距离(判断是否读老消息,产生读I/O),以及从服务器是否可读等因素建议下一次是从Master还是Slave拉取。 + +结合部署架构图,描述集群工作流程: + +- 启动NameServer,NameServer起来后监听端口,等待Broker、Producer、Consumer连上来,相当于一个路由控制中心。 +- Broker启动,跟所有的NameServer保持长连接,定时发送心跳包。心跳包中包含当前Broker信息(IP+端口等)以及存储所有Topic信息。注册成功后,NameServer集群中就有Topic跟Broker的映射关系。 +- 收发消息前,先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,也可以在发送消息时自动创建Topic。 +- Producer发送消息,启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取当前发送的Topic存在哪些Broker上,轮询从队列列表中选择一个队列,然后与队列所在的Broker建立长连接从而向Broker发消息。 +- Consumer跟Producer类似,跟其中一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,开始消费消息。 diff --git a/docs/cn/best_practice.md b/docs/cn/best_practice.md new file mode 100755 index 00000000000..5cc5b37643f --- /dev/null +++ b/docs/cn/best_practice.md @@ -0,0 +1,373 @@ +# 最佳实践 + +--- +## 1 生产者 + +### 1.1 发送消息注意事项 + +#### 1 Tags的使用 + +一个应用尽可能用一个Topic,而消息子类型则可以用tags来标识。tags可以由应用自由设置,只有生产者在发送消息设置了tags,消费方在订阅消息时才可以利用tags通过broker做消息过滤:message.setTags("TagA")。 + +#### 2 Keys的使用 + +每个消息在业务层面的唯一标识码要设置到keys字段,方便将来定位消息丢失问题。服务器会为每个消息创建索引(哈希索引),应用可以通过topic、key来查询这条消息内容,以及消息被谁消费。由于是哈希索引,请务必保证key尽可能唯一,这样可以避免潜在的哈希冲突。 + + +```java + // 订单Id + String orderId = "20034568923546"; + message.setKeys(orderId); +``` +#### 3 日志的打印 + +​消息发送成功或者失败要打印消息日志,务必要打印SendResult和key字段。send消息方法只要不抛异常,就代表发送成功。发送成功会有多个状态,在sendResult里定义。以下对每个状态进行说明: + +- **SEND_OK** + +消息发送成功。要注意的是消息发送成功也不意味着它是可靠的。要确保不会丢失任何消息,还应启用同步Master服务器或同步刷盘,即SYNC_MASTER或SYNC_FLUSH。 + + +- **FLUSH_DISK_TIMEOUT** + +消息发送成功但是服务器刷盘超时。此时消息已经进入服务器队列(内存),只有服务器宕机,消息才会丢失。消息存储配置参数中可以设置刷盘方式和同步刷盘时间长度,如果Broker服务器设置了刷盘方式为同步刷盘,即FlushDiskType=SYNC_FLUSH(默认为异步刷盘方式),当Broker服务器未在同步刷盘时间内(默认为5s)完成刷盘,则将返回该状态——刷盘超时。 + +- **FLUSH_SLAVE_TIMEOUT** + +消息发送成功,但是服务器同步到Slave时超时。此时消息已经进入服务器队列,只有服务器宕机,消息才会丢失。如果Broker服务器的角色是同步Master,即SYNC_MASTER(默认是异步Master即ASYNC_MASTER),并且从Broker服务器未在同步刷盘时间(默认为5秒)内完成与主服务器的同步,则将返回该状态——数据同步到Slave服务器超时。 + +- **SLAVE_NOT_AVAILABLE** + +消息发送成功,但是此时Slave不可用。如果Broker服务器的角色是同步Master,即SYNC_MASTER(默认是异步Master服务器即ASYNC_MASTER),但没有配置slave Broker服务器,则将返回该状态——无Slave服务器可用。 + + +### 1.2 消息发送失败处理方式 + +Producer的send方法本身支持内部重试,重试逻辑如下: + +- 至多重试2次。 +- 如果同步模式发送失败,则轮转到下一个Broker,如果异步模式发送失败,则只会在当前Broker进行重试。这个方法的总耗时时间不超过sendMsgTimeout设置的值,默认10s。 +- 如果本身向broker发送消息产生超时异常,就不会再重试。 + +以上策略也是在一定程度上保证了消息可以发送成功。如果业务对消息可靠性要求比较高,建议应用增加相应的重试逻辑:比如调用send同步方法发送失败时,则尝试将消息存储到db,然后由后台线程定时重试,确保消息一定到达Broker。 + +上述db重试方式为什么没有集成到MQ客户端内部做,而是要求应用自己去完成,主要基于以下几点考虑:首先,MQ的客户端设计为无状态模式,方便任意的水平扩展,且对机器资源的消耗仅仅是cpu、内存、网络。其次,如果MQ客户端内部集成一个KV存储模块,那么数据只有同步落盘才能较可靠,而同步落盘本身性能开销较大,所以通常会采用异步落盘,又由于应用关闭过程不受MQ运维人员控制,可能经常会发生 kill -9 这样暴力方式关闭,造成数据没有及时落盘而丢失。第三,Producer所在机器的可靠性较低,一般为虚拟机,不适合存储重要数据。综上,建议重试过程交由应用来控制。 + +### 1.3选择oneway形式发送 +通常消息的发送是这样一个过程: + +- 客户端发送请求到服务器 +- 服务器处理请求 +- 服务器向客户端返回应答 + +所以,一次消息发送的耗时时间是上述三个步骤的总和,而某些场景要求耗时非常短,但是对可靠性要求并不高,例如日志收集类应用,此类应用可以采用oneway形式调用,oneway形式只发送请求不等待应答,而发送请求在客户端实现层面仅仅是一个操作系统系统调用的开销,即将数据写入客户端的socket缓冲区,此过程耗时通常在微秒级。 + + +## 2 消费者 + +### 2.1 消费过程幂等 + +RocketMQ无法避免消息重复(Exactly-Once),所以如果业务对消费重复非常敏感,务必要在业务层面进行去重处理。可以借助关系数据库进行去重。首先需要确定消息的唯一键,可以是msgId,也可以是消息内容中的唯一标识字段,例如订单Id等。在消费之前判断唯一键是否在关系数据库中存在。如果不存在则插入,并消费,否则跳过。(实际过程要考虑原子性问题,判断是否存在可以尝试插入,如果报主键冲突,则插入失败,直接跳过) + +msgId一定是全局唯一标识符,但是实际使用中,可能会存在相同的消息有两个不同msgId的情况(消费者主动重发、因客户端重投机制导致的重复等),这种情况就需要使业务字段进行重复消费。 + +### 2.2 消费速度慢的处理方式 + +#### 1 提高消费并行度 + +绝大部分消息消费行为都属于 IO 密集型,即可能是操作数据库,或者调用 RPC,这类消费行为的消费速度在于后端数据库或者外系统的吞吐量,通过增加消费并行度,可以提高总的消费吞吐量,但是并行度增加到一定程度,反而会下降。所以,应用必须要设置合理的并行度。 如下有几种修改消费并行度的方法: + +- 同一个 ConsumerGroup 下,通过增加 Consumer 实例数量来提高并行度(需要注意的是超过订阅队列数的 Consumer 实例无效)。可以通过加机器,或者在已有机器启动多个进程的方式。 +- 提高单个 Consumer 的消费并行线程,通过修改参数 consumeThreadMin、consumeThreadMax实现。 + +#### 2 批量方式消费 + +某些业务流程如果支持批量方式消费,则可以很大程度上提高消费吞吐量,例如订单扣款类应用,一次处理一个订单耗时 1 s,一次处理 10 个订单可能也只耗时 2 s,这样即可大幅度提高消费的吞吐量,通过设置 consumer的 consumeMessageBatchMaxSize 返个参数,默认是 1,即一次只消费一条消息,例如设置为 N,那么每次消费的消息数小于等于 N。 + +#### 3 跳过非重要消息 + +发生消息堆积时,如果消费速度一直追不上发送速度,如果业务对数据要求不高的话,可以选择丢弃不重要的消息。例如,当某个队列的消息数堆积到100000条以上,则尝试丢弃部分或全部消息,这样就可以快速追上发送消息的速度。示例代码如下: + +```java + public ConsumeConcurrentlyStatus consumeMessage( + List msgs, + ConsumeConcurrentlyContext context) { + long offset = msgs.get(0).getQueueOffset(); + String maxOffset = + msgs.get(0).getProperty(Message.PROPERTY_MAX_OFFSET); + long diff = Long.parseLong(maxOffset) - offset; + if (diff > 100000) { + // TODO 消息堆积情况的特殊处理 + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + // TODO 正常消费过程 + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } +``` + + +#### 4 优化每条消息消费过程 + +举例如下,某条消息的消费过程如下: + +- 根据消息从 DB 查询【数据 1】 +- 根据消息从 DB 查询【数据 2】 +- 复杂的业务计算 +- 向 DB 插入【数据 3】 +- 向 DB 插入【数据 4】 + +这条消息的消费过程中有4次与 DB的 交互,如果按照每次 5ms 计算,那么总共耗时 20ms,假设业务计算耗时 5ms,那么总过耗时 25ms,所以如果能把 4 次 DB 交互优化为 2 次,那么总耗时就可以优化到 15ms,即总体性能提高了 40%。所以应用如果对时延敏感的话,可以把DB部署在SSD硬盘,相比于SCSI磁盘,前者的RT会小很多。 + +### 2.3 消费打印日志 + +如果消息量较少,建议在消费入口方法打印消息,消费耗时等,方便后续排查问题。 + + +```java + public ConsumeConcurrentlyStatus consumeMessage( + List msgs, + ConsumeConcurrentlyContext context) { + log.info("RECEIVE_MSG_BEGIN: " + msgs.toString()); + // TODO 正常消费过程 + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } +``` + +如果能打印每条消息消费耗时,那么在排查消费慢等线上问题时,会更方便。 + +### 2.4 其他消费建议 + +#### 1 关于消费者和订阅 + +​第一件需要注意的事情是,不同的消费者组可以独立的消费一些 topic,并且每个消费者组都有自己的消费偏移量,请确保同一组内的每个消费者订阅信息保持一致。 + +#### 2 关于有序消息 + +消费者将锁定每个消息队列,以确保他们被逐个消费,虽然这将会导致性能下降,但是当你关心消息顺序的时候会很有用。我们不建议抛出异常,你可以返回 ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT 作为替代。 + +#### 3 关于并发消费 + +顾名思义,消费者将并发消费这些消息,建议你使用它来获得良好性能,我们不建议抛出异常,你可以返回 ConsumeConcurrentlyStatus.RECONSUME_LATER 作为替代。 + +#### 4 关于消费状态Consume Status + +对于并发的消费监听器,你可以返回 RECONSUME_LATER 来通知消费者现在不能消费这条消息,并且希望可以稍后重新消费它。然后,你可以继续消费其他消息。对于有序的消息监听器,因为你关心它的顺序,所以不能跳过消息,但是你可以返回SUSPEND_CURRENT_QUEUE_A_MOMENT 告诉消费者等待片刻。 + +#### 5 关于Blocking + +不建议阻塞监听器,因为它会阻塞线程池,并最终可能会终止消费进程 + +#### 6 关于线程数设置 + +消费者使用 ThreadPoolExecutor 在内部对消息进行消费,所以你可以通过设置 setConsumeThreadMin 或 setConsumeThreadMax 来改变它。 + +#### 7 关于消费位点 + +当建立一个新的消费者组时,需要决定是否需要消费已经存在于 Broker 中的历史消息CONSUME_FROM_LAST_OFFSET 将会忽略历史消息,并消费之后生成的任何消息。CONSUME_FROM_FIRST_OFFSET 将会消费每个存在于 Broker 中的信息。你也可以使用 CONSUME_FROM_TIMESTAMP 来消费在指定时间戳后产生的消息。 + + + + + +## 3 Broker + +### 3.1 Broker 角色 +​ Broker 角色分为 ASYNC_MASTER(异步主机)、SYNC_MASTER(同步主机)以及SLAVE(从机)。如果对消息的可靠性要求比较严格,可以采用 SYNC_MASTER加SLAVE的部署方式。如果对消息可靠性要求不高,可以采用ASYNC_MASTER加SLAVE的部署方式。如果只是测试方便,则可以选择仅ASYNC_MASTER或仅SYNC_MASTER的部署方式。 +### 3.2 FlushDiskType +​ SYNC_FLUSH(同步刷新)相比于ASYNC_FLUSH(异步处理)会损失很多性能,但是也更可靠,所以需要根据实际的业务场景做好权衡。 +### 3.3 Broker 配置 + +| 参数名 | 默认值 | 说明 | +| -------------------------------- | ----------------------------- | ------------------------------------------------------------ | +| listenPort | 10911 | 接受客户端连接的监听端口 | +| namesrvAddr | null | nameServer 地址 | +| brokerIP1 | 网卡的 InetAddress | 当前 broker 监听的 IP | +| brokerIP2 | 跟 brokerIP1 一样 | 存在主从 broker 时,如果在 broker 主节点上配置了 brokerIP2 属性,broker 从节点会连接主节点配置的 brokerIP2 进行同步 | +| brokerName | null | broker 的名称 | +| brokerClusterName | DefaultCluster | 本 broker 所属的 Cluster 名称 | +| brokerId | 0 | broker id,0 表示 master,其他的正整数表示 slave | +| storePathRootDir | $HOME/store/ | 存储根路径 | +| storePathCommitLog | $HOME/store/commitlog/ | 存储 commit log 的路径 | +| mappedFileSizeCommitLog | 1024 * 1024 * 1024(1G) | commit log 的映射文件大小 |​ +| deleteWhen | 04 | 在每天的什么时间删除已经超过文件保留时间的 commit log |​ +| fileReservedTime | 72 | 以小时计算的文件保留时间 |​ +| brokerRole | ASYNC_MASTER | SYNC_MASTER/ASYNC_MASTER/SLAVE |​ +| flushDiskType | ASYNC_FLUSH | SYNC_FLUSH/ASYNC_FLUSH SYNC_FLUSH 模式下的 broker 保证在收到确认生产者之前将消息刷盘。ASYNC_FLUSH 模式下的 broker 则利用刷盘一组消息的模式,可以取得更好的性能。 |​ + +## 4 NameServer + +​RocketMQ 中,Name Servers 被设计用来做简单的路由管理。其职责包括: + +- Brokers 定期向每个名称服务器注册路由数据。 +- 名称服务器为客户端,包括生产者,消费者和命令行客户端提供最新的路由信息。 +​ +​ + +## 5 客户端配置 + +​ 相对于RocketMQ的Broker集群,生产者和消费者都是客户端。本小节主要描述生产者和消费者公共的行为配置。 + +### 5.1 客户端寻址方式 + +RocketMQ可以令客户端找到Name Server,然后通过Name Server再找到Broker。如下所示有多种配置方式,优先级由高到低,高优先级会覆盖低优先级。 + +- 代码中指定Name Server地址,多个namesrv地址之间用分号分割 + +```java +producer.setNamesrvAddr("192.168.0.1:9876;192.168.0.2:9876"); + +consumer.setNamesrvAddr("192.168.0.1:9876;192.168.0.2:9876"); +``` +- Java启动参数中指定Name Server地址 + +```text +-Drocketmq.namesrv.addr=192.168.0.1:9876;192.168.0.2:9876 +``` +- 环境变量指定Name Server地址 + +```text +export NAMESRV_ADDR=192.168.0.1:9876;192.168.0.2:9876 +``` +- HTTP静态服务器寻址(默认) + +客户端启动后,会定时访问一个静态HTTP服务器,地址如下:,这个URL的返回内容如下: + +```text +192.168.0.1:9876;192.168.0.2:9876 +``` +客户端默认每隔2分钟访问一次这个HTTP服务器,并更新本地的Name Server地址。URL已经在代码中硬编码,可通过修改/etc/hosts文件来改变要访问的服务器,例如在/etc/hosts增加如下配置: +```text +10.232.22.67 jmenv.tbsite.net +``` +推荐使用HTTP静态服务器寻址方式,好处是客户端部署简单,且Name Server集群可以热升级。 + +### 5.2 客户端配置 + +DefaultMQProducer、TransactionMQProducer、DefaultMQPushConsumer、DefaultMQPullConsumer都继承于ClientConfig类,ClientConfig为客户端的公共配置类。客户端的配置都是get、set形式,每个参数都可以用spring来配置,也可以在代码中配置,例如namesrvAddr这个参数可以这样配置,producer.setNamesrvAddr("192.168.0.1:9876"),其他参数同理。 + +#### 1 客户端的公共配置 + +| 参数名 | 默认值 | 说明 | +| ----------------------------- | ------- | ------------------------------------------------------------ | +| namesrvAddr | | Name Server地址列表,多个NameServer地址用分号隔开 | +| clientIP | 本机IP | 客户端本机IP地址,某些机器会发生无法识别客户端IP地址情况,需要应用在代码中强制指定 | +| instanceName | DEFAULT | 客户端实例名称,客户端创建的多个Producer、Consumer实际是共用一个内部实例(这个实例包含网络连接、线程资源等) | +| clientCallbackExecutorThreads | 4 | 通信层异步回调线程数 | +| pollNameServerInteval | 30000 | 轮询Name Server间隔时间,单位毫秒 | +| heartbeatBrokerInterval | 30000 | 向Broker发送心跳间隔时间,单位毫秒 | +| persistConsumerOffsetInterval | 5000 | 持久化Consumer消费进度间隔时间,单位毫秒 | + +#### 2 Producer配置 + +| 参数名 | 默认值 | 说明 | +| -------------------------------- | ---------------- | ------------------------------------------------------------ | +| producerGroup | DEFAULT_PRODUCER | Producer组名,多个Producer如果属于一个应用,发送同样的消息,则应该将它们归为同一组 | +| createTopicKey | TBW102 | 在发送消息时,自动创建服务器不存在的topic,需要指定Key,该Key可用于配置发送消息所在topic的默认路由。 | +| defaultTopicQueueNums | 4 | 在发送消息,自动创建服务器不存在的topic时,默认创建的队列数 | +| sendMsgTimeout | 3000 | 发送消息超时时间,单位毫秒 | +| compressMsgBodyOverHowmuch | 4096 | 消息Body超过多大开始压缩(Consumer收到消息会自动解压缩),单位字节 | +| retryAnotherBrokerWhenNotStoreOK | FALSE | 如果发送消息返回sendResult,但是sendStatus!=SEND_OK,是否重试发送 | +| retryTimesWhenSendFailed | 2 | 如果消息发送失败,最大重试次数,该参数只对同步发送模式起作用 | +| maxMessageSize | 4MB | 客户端限制的消息体大小,超过报错,同时服务端也会限制,所以需要跟服务端配合使用。 | +| transactionCheckListener | | 事务消息回查监听器,如果发送事务消息,必须设置 | +| checkThreadPoolMinSize | 1 | Broker回查Producer事务状态时,线程池最小线程数 | +| checkThreadPoolMaxSize | 1 | Broker回查Producer事务状态时,线程池最大线程数 | +| checkRequestHoldMax | 2000 | Broker回查Producer事务状态时,Producer本地缓冲请求队列大小 | +| RPCHook | null | 该参数是在Producer创建时传入的,包含消息发送前的预处理和消息响应后的处理两个接口,用户可以在第一个接口中做一些安全控制或者其他操作。 | + +#### 3 PushConsumer配置 + +| 参数名 | 默认值 | 说明 | +| ---------------------------- | ----------------------------- | ------------------------------------------------------------ | +| consumerGroup | DEFAULT_CONSUMER | Consumer组名,多个Consumer如果属于一个应用,订阅同样的消息,且消费逻辑一致,则应该将它们归为同一组 | +| messageModel | CLUSTERING | 消费模型支持集群消费和广播消费两种 | +| consumeFromWhere | CONSUME_FROM_LAST_OFFSET | Consumer启动后,默认从上次消费的位置开始消费,这包含两种情况:一种是上次消费的位置未过期,则消费从上次中止的位置进行;一种是上次消费位置已经过期,则从当前队列第一条消息开始消费 | +| consumeTimestamp | 半个小时前 | 只有当consumeFromWhere值为CONSUME_FROM_TIMESTAMP时才起作用。 | +| allocateMessageQueueStrategy | AllocateMessageQueueAveragely | Rebalance算法实现策略 | +| subscription | | 订阅关系 | +| messageListener | | 消息监听器 | +| offsetStore | | 消费进度存储 | +| consumeThreadMin | 20 | 消费线程池最小线程数 | +| consumeThreadMax | 20 | 消费线程池最大线程数 | +| consumeConcurrentlyMaxSpan | 2000 | 单队列并行消费允许的最大跨度 | +| pullThresholdForQueue | 1000 | 拉消息本地队列缓存消息最大数 | +| pullInterval | 0 | 拉消息间隔,由于是长轮询,所以为0,但是如果应用为了流控,也可以设置大于0的值,单位毫秒 | +| consumeMessageBatchMaxSize | 1 | 批量消费,一次消费多少条消息 | +| pullBatchSize | 32 | 批量拉消息,一次最多拉多少条 | + +#### 4 PullConsumer配置 + +| 参数名 | 默认值 | 说明 | +| -------------------------------- | ----------------------------- | ------------------------------------------------------------ | +| consumerGroup | DEFAULT_CONSUMER | Consumer组名,多个Consumer如果属于一个应用,订阅同样的消息,且消费逻辑一致,则应该将它们归为同一组 | +| brokerSuspendMaxTimeMillis | 20000 | 长轮询,Consumer拉消息请求在Broker挂起最长时间,单位毫秒 | +| consumerTimeoutMillisWhenSuspend | 30000 | 长轮询,Consumer拉消息请求在Broker挂起超过指定时间,客户端认为超时,单位毫秒 | +| consumerPullTimeoutMillis | 10000 | 非长轮询,拉消息超时时间,单位毫秒 | +| messageModel | BROADCASTING | 消息支持两种模式:集群消费和广播消费 | +| messageQueueListener | | 监听队列变化 | +| offsetStore | | 消费进度存储 | +| registerTopics | | 注册的topic集合 | +| allocateMessageQueueStrategy | AllocateMessageQueueAveragely | Rebalance算法实现策略 | + +#### 5 Message数据结构 + +| 字段名 | 默认值 | 说明 | +| -------------- | ------ | ------------------------------------------------------------ | +| Topic | null | 必填,消息所属topic的名称 | +| Body | null | 必填,消息体 | +| Tags | null | 选填,消息标签,方便服务器过滤使用。目前只支持每个消息设置一个tag | +| Keys | null | 选填,代表这条消息的业务关键词,服务器会根据keys创建哈希索引,设置后,可以在Console系统根据Topic、Keys来查询消息,由于是哈希索引,请尽可能保证key唯一,例如订单号,商品Id等。 | +| Flag | 0 | 选填,完全由应用来设置,RocketMQ不做干预 | +| DelayTimeLevel | 0 | 选填,消息延时级别,0表示不延时,大于0会延时特定的时间才会被消费 | +| WaitStoreMsgOK | TRUE | 选填,表示消息是否在服务器落盘后才返回应答。 | + +## 6 系统配置 + +本小节主要介绍系统(JVM/OS)相关的配置。 + +### 6.1 JVM选项 + +​ 推荐使用最新发布的JDK 1.8版本。通过设置相同的Xms和Xmx值来防止JVM调整堆大小以获得更好的性能。简单的JVM配置如下所示: +​ +​``` +​ +​-server -Xms8g -Xmx8g -Xmn4g +​ ``` +​ +​ +如果您不关心RocketMQ Broker的启动时间,还有一种更好的选择,就是通过“预触摸”Java堆以确保在JVM初始化期间每个页面都将被分配。那些不关心启动时间的人可以启用它: +​ -XX:+AlwaysPreTouch +禁用偏置锁定可能会减少JVM暂停, +​ -XX:-UseBiasedLocking +至于垃圾回收,建议使用带JDK 1.8的G1收集器。 + +```text +-XX:+UseG1GC -XX:G1HeapRegionSize=16m +-XX:G1ReservePercent=25 +-XX:InitiatingHeapOccupancyPercent=30 +``` + +​ 这些GC选项看起来有点激进,但事实证明它在我们的生产环境中具有良好的性能。另外不要把-XX:MaxGCPauseMillis的值设置太小,否则JVM将使用一个小的年轻代来实现这个目标,这将导致非常频繁的minor GC,所以建议使用rolling GC日志文件: + +```text +-XX:+UseGCLogFileRotation +-XX:NumberOfGCLogFiles=5 +-XX:GCLogFileSize=30m +``` + +如果写入GC文件会增加代理的延迟,可以考虑将GC日志文件重定向到内存文件系统: + +```text +-Xloggc:/dev/shm/mq_gc_%p.log123 +``` +### 6.2 Linux内核参数 + +​ os.sh脚本在bin文件夹中列出了许多内核参数,可以进行微小的更改然后用于生产用途。下面的参数需要注意,更多细节请参考/proc/sys/vm/*的[文档](https://www.kernel.org/doc/Documentation/sysctl/vm.txt) + +- **vm.extra_free_kbytes**,告诉VM在后台回收(kswapd)启动的阈值与直接回收(通过分配进程)的阈值之间保留额外的可用内存。RocketMQ使用此参数来避免内存分配中的长延迟。(与具体内核版本相关) +- **vm.min_free_kbytes**,如果将其设置为低于1024KB,将会巧妙的将系统破坏,并且系统在高负载下容易出现死锁。 +- **vm.max_map_count**,限制一个进程可能具有的最大内存映射区域数。RocketMQ将使用mmap加载CommitLog和ConsumeQueue,因此建议将为此参数设置较大的值。(agressiveness --> aggressiveness) +- **vm.swappiness**,定义内核交换内存页面的积极程度。较高的值会增加攻击性,较低的值会减少交换量。建议将值设置为10来避免交换延迟。 +- **File descriptor limits**,RocketMQ需要为文件(CommitLog和ConsumeQueue)和网络连接打开文件描述符。我们建议设置文件描述符的值为655350。 +- [Disk scheduler](https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Performance_Tuning_Guide/ch06s04s02.html),RocketMQ建议使用I/O截止时间调度器,它试图为请求提供有保证的延迟。 +[]([]()) diff --git a/docs/cn/client/java/API_Reference_ DefaultPullConsumer.md b/docs/cn/client/java/API_Reference_ DefaultPullConsumer.md new file mode 100644 index 00000000000..66857e2fd2e --- /dev/null +++ b/docs/cn/client/java/API_Reference_ DefaultPullConsumer.md @@ -0,0 +1,143 @@ +## DefaultPullConsumer +--- +### 类简介 + +1. `DefaultMQPullConsumer extends ClientConfig implements MQPullConsumer` + +2. `DefaultMQPullConsumer`主动的从Broker拉取消息,主动权由应用控制,可以实现批量的消费消息。Pull方式取消息的过程需要用户自己写,首先通过打算消费的Topic拿到MessageQueue的集合,遍历MessageQueue集合,然后针对每个MessageQueue批量取消息,也可以自定义与控制offset位置。 + +3. 优势:consumer可以按需消费,不用担心自己处理能力,而broker堆积消息也会相对简单,无需记录每一个要发送消息的状态,只需要维护所有消息的队列和偏移量就可以了。所以对于慢消费,消息量有限且到来的速度不均匀的情况,pull模式比较合适消息延迟与忙等。 + +4. 缺点:由于主动权在消费方,消费方无法及时获取最新的消息。比较适合不及时批处理场景。 + +``` java + + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; + +public class MQPullConsumer { + + private static final Map OFFSE_TABLE = new HashMap(); + + public static void main(String[] args) throws MQClientException { + DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("groupName"); + consumer.setNamesrvAddr("127.0.0.1:9876"); + consumer.start(); + // 从指定topic中拉取所有消息队列 + Set mqs = consumer.fetchSubscribeMessageQueues("order-topic"); + for(MessageQueue mq:mqs){ + try { + // 获取消息的offset,指定从store中获取 + long offset = consumer.fetchConsumeOffset(mq,true); + System.out.println("consumer from the queue:"+mq+":"+offset); + while(true){ + PullResult pullResult = consumer.pullBlockIfNotFound(mq, null, + getMessageQueueOffset(mq), 32); + putMessageQueueOffset(mq,pullResult.getNextBeginOffset()); + switch(pullResult.getPullStatus()){ + case FOUND: + List messageExtList = pullResult.getMsgFoundList(); + for (MessageExt m : messageExtList) { + System.out.println(new String(m.getBody())); + } + break; + case NO_MATCHED_MSG: + break; + case NO_NEW_MSG: + break; + case OFFSET_ILLEGAL: + break; + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + consumer.shutdown(); + } + + // 保存上次消费的消息下标 + private static void putMessageQueueOffset(MessageQueue mq, + long nextBeginOffset) { + OFFSE_TABLE.put(mq, nextBeginOffset); + } + + // 获取上次消费的消息的下标 + private static Long getMessageQueueOffset(MessageQueue mq) { + Long offset = OFFSE_TABLE.get(mq); + if(offset != null){ + return offset; + } + return 0l; + } + + +} +``` + + + +### 字段摘要 +|类型|字段名称|描述| +|------|-------|-------| +|DefaultMQPullConsumerImpl|defaultMQPullConsumerImpl|DefaultMQPullConsumer的内部核心处理默认实现| +|String|consumerGroup|消费的唯一分组| +|long|brokerSuspendMaxTimeMillis|consumer取连接broker的最大延迟时间,不建议修改| +|long|consumerTimeoutMillisWhenSuspend|pull取连接的最大超时时间,必须大于brokerSuspendMaxTimeMillis,不建议修改| +|long|consumerPullTimeoutMillis|socket连接的最大超时时间,不建议修改| +|String|messageModel|默认cluster模式| +|int|messageQueueListener|消息queue监听器,用来获取topic的queue变化| +|int|offsetStore|RemoteBrokerOffsetStore 远程与本地offset存储器| +|int|registerTopics|注册到该consumer的topic集合| +|int|allocateMessageQueueStrategy|consumer的默认获取queue的负载分配策略算法| + +### 构造方法摘要 + +|方法名称|方法描述| +|-------|------------| +|DefaultMQPullConsumer()|由默认参数值创建一个Pull消费者 | +|DefaultMQPullConsumer(final String consumerGroup, RPCHook rpcHook)|使用指定的分组名,hook创建一个消费者| +|DefaultMQPullConsumer(final String consumerGroup)|使用指定的分组名消费者| +|DefaultMQPullConsumer(RPCHook rpcHook)|使用指定的hook创建一个生产者| + + +### 使用方法摘要 + +|返回值|方法名称| 方法描述 | +|-------|-------|----------------------------------------------------------------------------------------| +|MQAdmin接口method|-------| ------------ | +|void|createTopic(String key, String newTopic, int queueNum)| 在broker上创建指定的topic | +|void|createTopic(String key, String newTopic, int queueNum, int topicSysFlag)| 在broker上创建指定的topic | +|long|earliestMsgStoreTime(MessageQueue mq)| 查询最早的消息存储时间 | +|long|maxOffset(MessageQueue mq)| 查询给定消息队列的最大offset | +|long|minOffset(MessageQueue mq)| 查询给定消息队列的最小offset | +|QueryResult|queryMessage(String topic, String key, int maxNum, long begin, long end)| 按关键字查询消息 | +|long|searchOffset(MessageQueue mq, long timestamp)| 查找指定时间的消息队列的物理offset | +|MessageExt|viewMessage(String offsetMsgId)| 根据给定的msgId查询消息 | +|MessageExt|public MessageExt viewMessage(String topic, String msgId)| 根据给定的msgId查询消息,并指定topic | +|MQConsumer接口method|-------| ------------ | +|Set|fetchSubscribeMessageQueues(String topic)| 根据topic获取订阅的Queue | +|void|sendMessageBack(final MessageExt msg, final int delayLevel)| 如果消息出来失败,可以发送回去延迟消费,delayLevel=DelayConf.DELAY_LEVEL | +|void|sendMessageBack(final MessageExt msg, final int delayLevel, final String brokerName)| 如果消息出来失败,可以发送回去延迟消费,delayLevel=DelayConf.DELAY_LEVEL | +|MQPullConsumer接口method|-------| ------------ | +|long|fetchConsumeOffset(MessageQueue mq, boolean fromStore)| 查询给定消息队列的最大offset | +|PullResult |pull(final MessageQueue mq, final String subExpression, final long offset,final int maxNums)| 异步拉取制定匹配的消息 | +|PullResult| pull(final MessageQueue mq, final String subExpression, final long offset,final int maxNums, final long timeout)| 异步拉取制定匹配的消息 | +|PullResult|pull(final MessageQueue mq, final MessageSelector selector, final long offset,final int maxNums)| 异步拉取制定匹配的消息,通过MessageSelector器来过滤消息,参考org.apache.rocketmq.common.filter.ExpressionType | +|PullResult|pullBlockIfNotFound(final MessageQueue mq, final String subExpression,final long offset, final int maxNums)| 异步拉取制定匹配的消息,如果没有消息讲block住,并指定超时时间consumerPullTimeoutMillis | +|void|pullBlockIfNotFound(final MessageQueue mq, final String subExpression, final long offset,final int maxNums, final PullCallback pullCallback)| 异步拉取制定匹配的消息,如果没有消息讲block住,并指定超时时间consumerPullTimeoutMillis,通过回调pullCallback来消费 | +|void|updateConsumeOffset(final MessageQueue mq, final long offset)| 更新指定mq的offset | +|long|fetchMessageQueuesInBalance(String topic)| 根据topic获取订阅的Queue(是balance分配后的) | +|void|void sendMessageBack(MessageExt msg, int delayLevel, String brokerName, String consumerGroup)| 如果消息出来失败,可以发送回去延迟消费,delayLevel=DelayConf.DELAY_LEVEL,消息可能在同一个consumerGroup消费 | +|void|shutdown()| 关闭当前消费者实例并释放相关资源 | +|void|start()| 启动消费者 | + diff --git a/docs/cn/client/java/API_Reference_DefaultMQProducer.md b/docs/cn/client/java/API_Reference_DefaultMQProducer.md new file mode 100644 index 00000000000..36ad323bd1a --- /dev/null +++ b/docs/cn/client/java/API_Reference_DefaultMQProducer.md @@ -0,0 +1,1088 @@ +## DefaultMQProducer +--- +### 类简介 + +`public class DefaultMQProducer +extends ClientConfig +implements MQProducer` + +>`DefaultMQProducer`类是应用用来投递消息的入口,开箱即用,可通过无参构造方法快速创建一个生产者。主要负责消息的发送,支持同步/异步/oneway的发送方式,这些发送方式均支持批量发送。可以通过该类提供的getter/setter方法,调整发送者的参数。`DefaultMQProducer`提供了多个send方法,每个send方法略有不同,在使用前务必详细了解其意图。下面给出一个生产者示例代码,[点击查看更多示例代码](https://github.com/apache/rocketmq/blob/master/example/src/main/java/org/apache/rocketmq/example/)。 + +``` java +public class Producer { + public static void main(String[] args) throws MQClientException { + // 创建指定分组名的生产者 + DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName"); + + // 启动生产者 + producer.start(); + + for (int i = 0; i < 128; i++) + try { + // 构建消息 + Message msg = new Message("TopicTest", + "TagA", + "OrderID188", + "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); + + // 同步发送 + SendResult sendResult = producer.send(msg); + + // 打印发送结果 + System.out.printf("%s%n", sendResult); + } catch (Exception e) { + e.printStackTrace(); + } + + producer.shutdown(); + } +} +``` + +**注意**:该类是线程安全的。在配置并启动完成后可在多个线程间安全共享。 + +### 字段摘要 +|类型|字段名称|描述| +|------|-------|-------| +|DefaultMQProducerImpl|defaultMQProducerImpl|生产者的内部默认实现| +|String|producerGroup|生产者分组| +|String|createTopicKey|在发送消息时,自动创建服务器不存在的topic| +|int|defaultTopicQueueNums|创建topic时默认的队列数量| +|int|sendMsgTimeout|发送消息的超时时间| +|int|compressMsgBodyOverHowmuch|压缩消息体的阈值| +|int|retryTimesWhenSendFailed|同步模式下内部尝试发送消息的最大次数| +|int|retryTimesWhenSendAsyncFailed|异步模式下内部尝试发送消息的最大次数| +|boolean|retryAnotherBrokerWhenNotStoreOK|是否在内部发送失败时重试另一个broker| +|int|maxMessageSize|消息体的最大长度| +|TraceDispatcher|traceDispatcher|基于RPCHooK实现的消息轨迹插件| + +### 构造方法摘要 + +|方法名称|方法描述| +|-------|------------| +|DefaultMQProducer()|由默认参数值创建一个生产者 | +|DefaultMQProducer(final String producerGroup)|使用指定的分组名创建一个生产者| +|DefaultMQProducer(final String producerGroup, boolean enableMsgTrace)|使用指定的分组名创建一个生产者,并设置是否开启消息轨迹| +|DefaultMQProducer(final String producerGroup, boolean enableMsgTrace, final String customizedTraceTopic)|使用指定的分组名创建一个生产者,并设置是否开启消息轨迹及追踪topic的名称| +|DefaultMQProducer(RPCHook rpcHook)|使用指定的hook创建一个生产者| +|DefaultMQProducer(final String producerGroup, RPCHook rpcHook)|使用指定的分组名及自定义hook创建一个生产者| +|DefaultMQProducer(final String producerGroup, RPCHook rpcHook, boolean enableMsgTrace,final String customizedTraceTopic)|使用指定的分组名及自定义hook创建一个生产者,并设置是否开启消息轨迹及追踪topic的名称| + +### 使用方法摘要 + +|返回值|方法名称|方法描述| +|-------|-------|------------| +|void|createTopic(String key, String newTopic, int queueNum)|在broker上创建指定的topic| +|void|createTopic(String key, String newTopic, int queueNum, int topicSysFlag)|在broker上创建指定的topic| +|long|earliestMsgStoreTime(MessageQueue mq)|查询最早的消息存储时间| +|List|fetchPublishMessageQueues(String topic)|获取topic的消息队列| +|long|maxOffset(MessageQueue mq)|查询给定消息队列的最大offset| +|long|minOffset(MessageQueue mq)|查询给定消息队列的最小offset| +|QueryResult|queryMessage(String topic, String key, int maxNum, long begin, long end)|按关键字查询消息| +|long|searchOffset(MessageQueue mq, long timestamp)|查找指定时间的消息队列的物理offset| +|SendResult|send(Collection msgs)|同步批量发送消息| +|SendResult|send(Collection msgs, long timeout)|同步批量发送消息| +|SendResult|send(Collection msgs, MessageQueue messageQueue)|向指定的消息队列同步批量发送消息| +|SendResult|send(Collection msgs, MessageQueue messageQueue, long timeout)|向指定的消息队列同步批量发送消息,并指定超时时间| +|SendResult|send(Message msg)|同步单条发送消息| +|SendResult|send(Message msg, long timeout)|同步发送单条消息,并指定超时时间| +|SendResult|send(Message msg, MessageQueue mq)|向指定的消息队列同步发送单条消息| +|SendResult|send(Message msg, MessageQueue mq, long timeout)|向指定的消息队列同步单条发送消息,并指定超时时间| +|void|send(Message msg, MessageQueue mq, SendCallback sendCallback)|向指定的消息队列异步单条发送消息,并指定回调方法| +|void|send(Message msg, MessageQueue mq, SendCallback sendCallback, long timeout)|向指定的消息队列异步单条发送消息,并指定回调方法和超时时间| +|SendResult|send(Message msg, MessageQueueSelector selector, Object arg)|向消息队列同步单条发送消息,并指定发送队列选择器| +|SendResult|send(Message msg, MessageQueueSelector selector, Object arg, long timeout)|向消息队列同步单条发送消息,并指定发送队列选择器与超时时间| +|void|send(Message msg, MessageQueueSelector selector, Object arg, SendCallback sendCallback)|向指定的消息队列异步单条发送消息| +|void|send(Message msg, MessageQueueSelector selector, Object arg, SendCallback sendCallback, long timeout)|向指定的消息队列异步单条发送消息,并指定超时时间| +|void|send(Message msg, SendCallback sendCallback)|异步发送消息| +|void|send(Message msg, SendCallback sendCallback, long timeout)|异步发送消息,并指定回调方法和超时时间| +|TransactionSendResult|sendMessageInTransaction(Message msg, LocalTransactionExecuter tranExecuter, final Object arg)|发送事务消息,并指定本地执行事务实例| +|TransactionSendResult|sendMessageInTransaction(Message msg, Object arg)|发送事务消息| +|void|sendOneway(Message msg)|单向发送消息,不等待broker响应| +|void|sendOneway(Message msg, MessageQueue mq) |单向发送消息到指定队列,不等待broker响应| +|void|sendOneway(Message msg, MessageQueueSelector selector, Object arg)|单向发送消息到队列选择器的选中的队列,不等待broker响应| +|void|shutdown()|关闭当前生产者实例并释放相关资源| +|void|start()|启动生产者| +|MessageExt|viewMessage(String offsetMsgId)|根据给定的msgId查询消息| +|MessageExt|public MessageExt viewMessage(String topic, String msgId)|根据给定的msgId查询消息,并指定topic| + +### 字段详细信息 + +- [producerGroup](https://rocketmq.apache.org/docs/introduction/02concepts) + + `private String producerGroup` + + 生产者的分组名称。相同的分组名称表明生产者实例在概念上归属于同一分组。这对事务消息十分重要,如果原始生产者在事务之后崩溃,那么broker可以联系同一生产者分组的不同生产者实例来提交或回滚事务。 + + 默认值:DEFAULT_PRODUCER + + 注意: 由数字、字母、下划线、横杠(-)、竖线(|)或百分号组成;不能为空;长度不能超过255。 + +- defaultMQProducerImpl + + `protected final transient DefaultMQProducerImpl defaultMQProducerImpl` + + 生产者的内部默认实现,在构造生产者时内部自动初始化,提供了大部分方法的内部实现。 + +- createTopicKey + + `private String createTopicKey = MixAll.AUTO_CREATE_TOPIC_KEY_TOPIC` + + 在发送消息时,自动创建服务器不存在的topic,需要指定Key,该Key可用于配置发送消息所在topic的默认路由。 + + 默认值:TBW102 + + 建议:测试或者demo使用,生产环境下不建议打开自动创建配置。 + +- defaultTopicQueueNums + + `private volatile int defaultTopicQueueNums = 4` + + 创建topic时默认的队列数量。 + + 默认值:4 + +- sendMsgTimeout + + `private int sendMsgTimeout = 3000` + + 发送消息时的超时时间。 + + 默认值:3000,单位:毫秒 + + 建议:不建议修改该值,该值应该与broker配置中的sendTimeout一致,发送超时,可临时修改该值,建议解决超时问题,提高broker集群的Tps。 + +- compressMsgBodyOverHowmuch + + `private int compressMsgBodyOverHowmuch = 1024 * 4` + + 压缩消息体阈值。大于4K的消息体将默认进行压缩。 + + 默认值:1024 * 4,单位:字节 + + 建议:可通过DefaultMQProducerImpl.setZipCompressLevel方法设置压缩率(默认为5,可选范围[0,9]);可通过DefaultMQProducerImpl.tryToCompressMessage方法测试出compressLevel与compressMsgBodyOverHowmuch最佳值。 + +- retryTimesWhenSendFailed + + `private int retryTimesWhenSendFailed = 2` + + 同步模式下,在返回发送失败之前,内部尝试重新发送消息的最大次数。 + + 默认值:2,即:默认情况下一条消息最多会被投递3次。 + + 注意:在极端情况下,这可能会导致消息的重复。 + +- retryTimesWhenSendAsyncFailed + + `private int retryTimesWhenSendAsyncFailed = 2` + + 异步模式下,在发送失败之前,内部尝试重新发送消息的最大次数。 + + 默认值:2,即:默认情况下一条消息最多会被投递3次。 + + 注意:在极端情况下,这可能会导致消息的重复。 + +- retryAnotherBrokerWhenNotStoreOK + + `private boolean retryAnotherBrokerWhenNotStoreOK = false` + + 同步模式下,消息保存失败时是否重试其他broker。 + + 默认值:false + + 注意:此配置关闭时,非投递时产生异常情况下,会忽略retryTimesWhenSendFailed配置。 + +- maxMessageSize + + `private int maxMessageSize = 1024 * 1024 * 4` + + 消息体的最大大小。当消息体的字节数超过maxMessageSize就发送失败。 + + 默认值:1024 * 1024 * 4,单位:字节 + +- [traceDispatcher](https://github.com/apache/rocketmq/wiki/RIP-6-Message-Trace) + + `private TraceDispatcher traceDispatcher = null` + + 在开启消息轨迹后,该类通过hook的方式把消息生产者,消息存储的broker和消费者消费消息的信息像链路一样记录下来。在构造生产者时根据构造入参enableMsgTrace来决定是否创建该对象。 + +### 构造方法详细信息 + +1. DefaultMQProducer + + `public DefaultMQProducer()` + + 创建一个新的生产者。 + +2. DefaultMQProducer + + `DefaultMQProducer(final String producerGroup)` + + 使用指定的分组名创建一个生产者。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 缺省值 |描述 + ---|---|---|---|--- + producerGroup | String | 是 | DEFAULT_PRODUCER | 生产者的分组名称 + +3. DefaultMQProducer + + `DefaultMQProducer(final String producerGroup, boolean enableMsgTrace)` + + 使用指定的分组名创建一个生产者,并设置是否开启消息轨迹。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 缺省值 |描述 + ---|---|---|---|--- + producerGroup | String | 是 | DEFAULT_PRODUCER | 生产者的分组名称 + enableMsgTrace | boolean | 是 | false |是否开启消息轨迹 + +4. DefaultMQProducer + + `DefaultMQProducer(final String producerGroup, boolean enableMsgTrace, final String customizedTraceTopic)` + + 使用指定的分组名创建一个生产者,并设置是否开启消息轨迹及追踪topic的名称。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 缺省值 |描述 + ---|---|---|---|--- + producerGroup | String | 是 | DEFAULT_PRODUCER | 生产者的分组名称 + rpcHook | RPCHook | 否 | null |每个远程命令执行后会回调rpcHook + enableMsgTrace | boolean | 是 | false |是否开启消息轨迹 + customizedTraceTopic | String | 否 | RMQ_SYS_TRACE_TOPIC | 消息轨迹topic的名称 + +5. DefaultMQProducer + + `DefaultMQProducer(RPCHook rpcHook)` + + 使用指定的hook创建一个生产者。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 缺省值 |描述 + ---|---|---|---|--- + rpcHook | RPCHook | 否 | null |每个远程命令执行后会回调rpcHook + +6. DefaultMQProducer + + `DefaultMQProducer(final String producerGroup, RPCHook rpcHook)` + + 使用指定的分组名及自定义hook创建一个生产者。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 缺省值 |描述 + ---|---|---|---|--- + producerGroup | String | 是 | DEFAULT_PRODUCER | 生产者的分组名称 + rpcHook | RPCHook | 否 | null |每个远程命令执行后会回调rpcHook + +7. DefaultMQProducer + + `DefaultMQProducer(final String producerGroup, RPCHook rpcHook, boolean enableMsgTrace,final String customizedTraceTopic)` + + 使用指定的分组名及自定义hook创建一个生产者,并设置是否开启消息轨迹及追踪topic的名称。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 缺省值 |描述 + ---|---|---|---|--- + producerGroup | String | 是 | DEFAULT_PRODUCER | 生产者的分组名称 + rpcHook | RPCHook | 否 | null |每个远程命令执行后会回调rpcHook + enableMsgTrace | boolean | 是 | false |是否开启消息轨迹 + customizedTraceTopic | String | 否 | RMQ_SYS_TRACE_TOPIC | 消息轨迹topic的名称 + +### 使用方法详细信息 + +1. createTopic + + `public void createTopic(String key, String newTopic, int queueNum)` + + 在broker上创建一个topic。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 |值范围 | 说明 + ---|---|---|---|---|--- + key | String | 是 | | | 访问密钥。 + newTopic | String | 是 | | | 新建topic的名称。由数字、字母、下划线(_)、横杠(-)、竖线(|)或百分号(%)组成;
    长度小于255;不能为TBW102或空。 + queueNum | int | 是 | 0 | (0, maxIntValue] | topic的队列数量。 + + - 返回值描述: + + void + + - 异常描述: + + MQClientException - 生产者状态非Running;未找到broker等客户端异常。 + +2. createTopic + + `public void createTopic(String key, String newTopic, int queueNum, int topicSysFlag)` + + 在broker上创建一个topic。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 |值范围 | 说明 + ---|---|---|---|---|--- + key | String | 是 | | | 访问密钥。 + newTopic | String | 是 | | | 新建topic的名称。 + queueNum | int | 是 | 0 | (0, maxIntValue] | topic的队列数量。 + topicSysFlag | int | 是 | 0 | | 保留字段,暂未使用。 + + - 返回值描述: + + void + + - 异常描述: + + MQClientException - 生产者状态非Running;未找到broker等客户端异常。 + +3. earliestMsgStoreTime + + `public long earliestMsgStoreTime(MessageQueue mq)` + + 查询最早的消息存储时间。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + mq | MessageQueue | 是 | | | 要查询的消息队列 + + - 返回值描述: + + 指定队列最早的消息存储时间。单位:毫秒。 + + - 异常描述: + + MQClientException - 生产者状态非Running;没有找到broker;broker返回失败;网络异常;线程中断等客户端异常。 + +4. fetchPublishMessageQueues + + `public List fetchPublishMessageQueues(String topic)` + + 获取topic的消息队列。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + topic | String | 是 | | | topic名称 + + - 返回值描述: + + 传入topic下的消息队列。 + + - 异常描述: + + MQClientException - 生产者状态非Running;没有找到broker;broker返回失败;网络异常;线程中断等客户端异常。 + +5. maxOffset + + `public long maxOffset(MessageQueue mq)` + + 查询消息队列的最大物理偏移量。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + mq | MessageQueue | 是 | | | 要查询的消息队列 + + - 返回值描述: + + 给定消息队列的最大物理偏移量。 + + - 异常描述: + + MQClientException - 生产者状态非Running;没有找到broker;broker返回失败;网络异常;线程中断等客户端异常。 + +6. minOffset + + `public long minOffset(MessageQueue mq)` + + 查询给定消息队列的最小物理偏移量。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + mq | MessageQueue | 是 | | | 要查询的消息队列 + + - 返回值描述: + + 给定消息队列的最小物理偏移量。 + + - 异常描述: + + MQClientException - 生产者状态非Running;没有找到broker;broker返回失败;网络异常;线程中断等客户端异常。 + +7. queryMessage + + `public QueryResult queryMessage(String topic, String key, int maxNum, long begin, long end)` + + 按关键字查询消息。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + topic | String | 是 | | | topic名称 + key | String | 否 | null | | 查找的关键字 + maxNum | int | 是 | | | 返回消息的最大数量 + begin | long | 是 | | | 开始时间戳,单位:毫秒 + end | long | 是 | | | 结束时间戳,单位:毫秒 + + - 返回值描述: + + 查询到的消息集合。 + + - 异常描述: + + MQClientException - 生产者状态非Running;没有找到broker;broker返回失败;网络异常等客户端异常客户端异常。
    + InterruptedException - 线程中断。 + +8. searchOffset + + `public long searchOffset(MessageQueue mq, long timestamp)` + + 查找指定时间的消息队列的物理偏移量。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + mq | MessageQueue | 是 | | | 要查询的消息队列。 + timestamp | long | 是 | | | 指定要查询时间的时间戳。单位:毫秒。 + + - 返回值描述: + + 指定时间的消息队列的物理偏移量。 + + - 异常描述: + + MQClientException - 生产者状态非Running;没有找到broker;broker返回失败;网络异常;线程中断等客户端异常。 + +9. send + + `public SendResult send(Collection msgs)` + + 同步批量发送消息。在返回发送失败之前,内部尝试重新发送消息的最大次数(参见*retryTimesWhenSendFailed*属性)。未明确指定发送队列,默认采取轮询策略发送。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + msgs | Collection | 是 | | | 待发送的消息集合。集合内的消息必须属同一个topic。 + + - 返回值描述: + + 批量消息的发送结果,包含msgId,发送状态等信息。 + + - 异常描述: + + MQClientException - broker不存在或未找到;namesrv地址为空;未找到topic的路由信息等客户端异常。
    + RemotingException - 网络异常。
    + MQBrokerException - broker发生错误。
    + InterruptedException - 发送线程中断。
    + RemotingTooMuchRequestException - 发送超时。 + +10. send + + `public SendResult send(Collection msgs, long timeout)` + + 同步批量发送消息,如果在指定的超时时间内未完成消息投递,会抛出*RemotingTooMuchRequestException*。 + 在返回发送失败之前,内部尝试重新发送消息的最大次数(参见*retryTimesWhenSendFailed*属性)。未明确指定发送队列,默认采取轮询策略发送。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + msgs | Collection | 是 | | | 待发送的消息集合。集合内的消息必须属同一个topic。 + timeout | long | 是 | 参见*sendMsgTimeout*属性 | | 发送超时时间,单位:毫秒。 + + - 返回值描述: + + 批量消息的发送结果,包含msgId,发送状态等信息。 + + - 异常描述: + + MQClientException - broker不存在或未找到;namesrv地址为空;未找到topic的路由信息等客户端异常。
    + RemotingException - 网络异常。
    + MQBrokerException - broker发生错误。
    + InterruptedException - 发送线程中断。
    + RemotingTooMuchRequestException - 发送超时。 + +11. send + + `public SendResult send(Collection msgs, MessageQueue messageQueue)` + + 向给定队列同步批量发送消息。 + + 注意:指定队列意味着所有消息均为同一个topic。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + msgs | Collection | 是 | | | 待发送的消息集合。集合内的消息必须属同一个topic。 + messageQueue | MessageQueue | 是 | | | 待投递的消息队列。指定队列意味着待投递消息均为同一个topic。 + + - 返回值描述: + + 批量消息的发送结果,包含msgId,发送状态等信息。 + + - 异常描述: + + MQClientException - broker不存在或未找到;namesrv地址为空;未找到topic的路由信息等客户端异常。
    + RemotingException - 网络异常。
    + MQBrokerException - broker发生错误。
    + InterruptedException - 发送线程中断。
    + RemotingTooMuchRequestException - 发送超时。 + +12. send + + `public SendResult send(Collection msgs, MessageQueue messageQueue, long timeout)` + + 向给定队列同步批量发送消息,如果在指定的超时时间内未完成消息投递,会抛出*RemotingTooMuchRequestException*。 + + 注意:指定队列意味着所有消息均为同一个topic。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + msgs | Collection | 是 | | | 待发送的消息集合。集合内的消息必须属同一个topic。 + timeout | long | 是 | 参见*sendMsgTimeout*属性 | | 发送超时时间,单位:毫秒。 + messageQueue | MessageQueue | 是 | | | 待投递的消息队列。指定队列意味着待投递消息均为同一个topic。 + + - 返回值描述: + + 批量消息的发送结果,包含msgId,发送状态等信息。 + + - 异常描述: + + MQClientException - broker不存在或未找到;namesrv地址为空;未找到topic的路由信息等客户端异常。
    + RemotingException - 网络异常。
    + MQBrokerException - broker发生错误。
    + InterruptedException - 发送线程中断。
    + RemotingTooMuchRequestException - 发送超时。 + +13. send + + `public SendResult send(Message msg)` + + 以同步模式发送消息,仅当发送过程完全完成时,此方法才会返回。 + 在返回发送失败之前,内部尝试重新发送消息的最大次数(参见*retryTimesWhenSendFailed*属性)。未明确指定发送队列,默认采取轮询策略发送。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + msg | Message | 是 | | | 待发送的消息。 + + - 返回值描述: + + 消息的发送结果,包含msgId,发送状态等信息。 + + - 异常描述: + + MQClientException - broker不存在或未找到;namesrv地址为空;未找到topic的路由信息等客户端异常。
    + RemotingException - 网络异常。
    + MQBrokerException - broker发生错误。
    + InterruptedException - 发送线程中断。
    + RemotingTooMuchRequestException - 发送超时。 + +14. send + + `public SendResult send(Message msg, long timeout)` + + 以同步模式发送消息,如果在指定的超时时间内未完成消息投递,会抛出*RemotingTooMuchRequestException*。仅当发送过程完全完成时,此方法才会返回。 + 在返回发送失败之前,内部尝试重新发送消息的最大次数(参见*retryTimesWhenSendFailed*属性)。未明确指定发送队列,默认采取轮询策略发送。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + msg | Message | 是 | | | 待发送的消息。 + timeout | long | 是 | 参见*sendMsgTimeout*属性 | | 发送超时时间,单位:毫秒。 + + - 返回值描述: + + 消息的发送结果,包含msgId,发送状态等信息。 + + - 异常描述: + + MQClientException - broker不存在或未找到;namesrv地址为空;未找到topic的路由信息等客户端异常。
    + RemotingException - 网络异常。
    + MQBrokerException - broker发生错误。
    + InterruptedException - 发送线程中断。
    + RemotingTooMuchRequestException - 发送超时。 + +15. send + + `public SendResult send(Message msg, MessageQueue mq)` + + 向指定的消息队列同步发送单条消息。仅当发送过程完全完成时,此方法才会返回。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + msg | Message | 是 | | | 待发送的消息。 + mq | MessageQueue | 是 | | | 待投递的消息队列。 + + - 返回值描述: + + 消息的发送结果,包含msgId,发送状态等信息。 + + - 异常描述: + + MQClientException - broker不存在或未找到;namesrv地址为空;未找到topic的路由信息等客户端异常。
    + RemotingException - 网络异常。
    + MQBrokerException - broker发生错误。
    + InterruptedException - 发送线程中断。
    + RemotingTooMuchRequestException - 发送超时。 + +16. send + + `public SendResult send(Message msg, MessageQueue mq, long timeout)` + + 向指定的消息队列同步发送单条消息,如果在指定的超时时间内未完成消息投递,会抛出*RemotingTooMuchRequestException*。仅当发送过程完全完成时,此方法才会返回。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + msg | Message | 是 | | | 待发送的消息。 + timeout | long | 是 | 参见*sendMsgTimeout*属性 | | 发送超时时间,单位:毫秒。 + mq | MessageQueue | 是 | | | 待投递的消息队列。指定队列意味着待投递消息均为同一个topic。 + + - 返回值描述: + + 消息的发送结果,包含msgId,发送状态等信息。 + + - 异常描述: + + MQClientException - broker不存在或未找到;namesrv地址为空;未找到topic的路由信息等客户端异常。
    + RemotingException - 网络异常。
    + MQBrokerException - broker发生错误。
    + InterruptedException - 发送线程中断。
    + RemotingTooMuchRequestException - 发送超时。 + +17. send + + `public void send(Message msg, MessageQueue mq, SendCallback sendCallback)` + + 向指定的消息队列异步发送单条消息,异步发送调用后直接返回,并在在发送成功或者异常时回调`sendCallback`,所以异步发送时`sendCallback`参数不能为null,否则在回调时会抛出`NullPointerException`。 + 异步发送时,在成功发送前,其内部会尝试重新发送消息的最大次数(参见*retryTimesWhenSendAsyncFailed*属性)。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + msg | Message | 是 | | | 待发送的消息。 + mq | MessageQueue | 是 | | | 待投递的消息队列。指定队列意味着待投递消息均为同一个topic。 + sendCallback | SendCallback | 是 | | | 回调接口的实现。 + + - 返回值描述: + + void + + - 异常描述: + + MQClientException - broker不存在或未找到;namesrv地址为空;未找到topic的路由信息等客户端异常。
    + RemotingException - 网络异常。
    + InterruptedException - 发送线程中断。 + +18. send + + `public void send(Message msg, MessageQueue mq, SendCallback sendCallback, long timeout)` + + 向指定的消息队列异步发送单条消息,异步发送调用后直接返回,并在在发送成功或者异常时回调`sendCallback`,所以异步发送时`sendCallback`参数不能为null,否则在回调时会抛出`NullPointerException`。 + 若在指定时间内消息未发送成功,回调方法会收到*RemotingTooMuchRequestException*异常。 + 异步发送时,在成功发送前,其内部会尝试重新发送消息的最大次数(参见*retryTimesWhenSendAsyncFailed*属性)。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + msg | Message | 是 | | | 待发送的消息。 + mq | MessageQueue | 是 | | | 待投递的消息队列。 + sendCallback | SendCallback | 是 | | | 回调接口的实现。 + timeout | long | 是 | 参见*sendMsgTimeout*属性 | | 发送超时时间,单位:毫秒。 + + - 返回值描述: + void + + - 异常描述: + + MQClientException - broker不存在或未找到;namesrv地址为空;未找到topic的路由信息等客户端异常。
    + RemotingException - 网络异常。
    + InterruptedException - 发送线程中断。 + +19. send + + `public SendResult send(Message msg, MessageQueueSelector selector, Object arg)` + + 向通过`MessageQueueSelector`计算出的队列同步发送消息。 + + 可以通过自实现`MessageQueueSelector`接口,将某一类消息发送至固定的队列。比如:将同一个订单的状态变更消息投递至固定的队列。 + + 注意:此消息发送失败内部不会重试。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + msg | Message | 是 | | | 待发送的消息。 + selector | MessageQueueSelector | 是 | | | 队列选择器。 + arg | Object | 否 | | | 供队列选择器使用的参数对象。 + + - 返回值描述: + + 消息的发送结果,包含msgId,发送状态等信息。 + + - 异常描述: + + MQClientException - broker不存在或未找到;namesrv地址为空;未找到topic的路由信息等客户端异常。
    + RemotingException - 网络异常。
    + MQBrokerException - broker发生错误。
    + InterruptedException - 发送线程中断。
    + RemotingTooMuchRequestException - 发送超时。 + +20. send + + `public SendResult send(Message msg, MessageQueueSelector selector, Object arg, long timeout)` + + 向通过`MessageQueueSelector`计算出的队列同步发送消息,并指定发送超时时间。 + + 可以通过自实现`MessageQueueSelector`接口,将某一类消息发送至固定的队列。比如:将同一个订单的状态变更消息投递至固定的队列。 + + 注意:此消息发送失败内部不会重试。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + msg | Message | 是 | | | 待发送的消息。 + selector | MessageQueueSelector | 是 | | | 队列选择器。 + arg | Object | 否 | | | 供队列选择器使用的参数对象。 + timeout | long | 是 | 参见*sendMsgTimeout*属性 | | 发送超时时间,单位:毫秒。 + + - 返回值描述: + + 消息的发送结果,包含msgId,发送状态等信息。 + + - 异常描述: + MQClientException - broker不存在或未找到;namesrv地址为空;未找到topic的路由信息等客户端异常。
    + RemotingException - 网络异常。
    + MQBrokerException - broker发生错误。
    + InterruptedException - 发送线程中断。
    + RemotingTooMuchRequestException - 发送超时。 + +21. send + + `public void send(Message msg, MessageQueueSelector selector, Object arg, SendCallback sendCallback)` + + 向通过`MessageQueueSelector`计算出的队列异步发送单条消息,异步发送调用后直接返回,并在在发送成功或者异常时回调`sendCallback`,所以异步发送时`sendCallback`参数不能为null,否则在回调时会抛出`NullPointerException`。 + 异步发送时,在成功发送前,其内部会尝试重新发送消息的最大次数(参见*retryTimesWhenSendAsyncFailed*属性)。 + + 可以通过自实现`MessageQueueSelector`接口,将某一类消息发送至固定的队列。比如:将同一个订单的状态变更消息投递至固定的队列。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + msg | Message | 是 | | | 待发送的消息。 + selector | MessageQueueSelector | 是 | | | 队列选择器。 + arg | Object | 否 | | | 供队列选择器使用的参数对象。 + sendCallback | SendCallback | 是 | | | 回调接口的实现。 + + - 返回值描述: + + void + + - 异常描述: + + MQClientException - broker不存在或未找到;namesrv地址为空;未找到topic的路由信息等客户端异常。
    + RemotingException - 网络异常。
    + InterruptedException - 发送线程中断。 + +22. send + + `public void send(Message msg, MessageQueueSelector selector, Object arg, SendCallback sendCallback, long timeout)` + + 向通过`MessageQueueSelector`计算出的队列异步发送单条消息,异步发送调用后直接返回,并在在发送成功或者异常时回调`sendCallback`,所以异步发送时`sendCallback`参数不能为null,否则在回调时会抛出`NullPointerException`。 + 异步发送时,在成功发送前,其内部会尝试重新发送消息的最大次数(参见*retryTimesWhenSendAsyncFailed*属性)。 + + 可以通过自实现`MessageQueueSelector`接口,将某一类消息发送至固定的队列。比如:将同一个订单的状态变更消息投递至固定的队列。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + msg | Message | 是 | | | 待发送的消息。 + selector | MessageQueueSelector | 是 | | | 队列选择器。 + arg | Object | 否 | | | 供队列选择器使用的参数对象。 + sendCallback | SendCallback | 是 | | | 回调接口的实现。 + timeout | long | 是 | 参见*sendMsgTimeout*属性 | | 发送超时时间,单位:毫秒。 + + - 返回值描述: + + void + + - 异常描述: + + MQClientException - broker不存在或未找到;namesrv地址为空;未找到topic的路由信息等客户端异常。
    + RemotingException - 网络异常。
    + InterruptedException - 发送线程中断。 + +23. send + + `public void send(Message msg, SendCallback sendCallback)` + + 异步发送单条消息,异步发送调用后直接返回,并在在发送成功或者异常时回调`sendCallback`,所以异步发送时`sendCallback`参数不能为null,否则在回调时会抛出`NullPointerException`。 + 异步发送时,在成功发送前,其内部会尝试重新发送消息的最大次数(参见*retryTimesWhenSendAsyncFailed*属性)。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + msg | Message | 是 | | | 待发送的消息。 + sendCallback | SendCallback | 是 | | | 回调接口的实现。 + + - 返回值描述: + + void + + - 异常描述: + + MQClientException - broker不存在或未找到;namesrv地址为空;未找到topic的路由信息等客户端异常。
    + RemotingException - 网络异常。
    + InterruptedException - 发送线程中断。 + +24. send + + `public void send(Message msg, SendCallback sendCallback, long timeout)` + + 异步发送单条消息,异步发送调用后直接返回,并在在发送成功或者异常时回调`sendCallback`,所以异步发送时`sendCallback`参数不能为null,否则在回调时会抛出`NullPointerException`。 + 异步发送时,在成功发送前,其内部会尝试重新发送消息的最大次数(参见*retryTimesWhenSendAsyncFailed*属性)。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + msg | Message | 是 | | | 待发送的消息。 + sendCallback | SendCallback | 是 | | | 回调接口的实现。 + timeout | long | 是 | 参见*sendMsgTimeout*属性 | | 发送超时时间,单位:毫秒。 + + - 返回值描述: + + void + + - 异常描述: + + MQClientException - broker不存在或未找到;namesrv地址为空;未找到topic的路由信息等客户端异常。
    + RemotingException - 网络异常。
    + InterruptedException - 发送线程中断。 + +25. sendMessageInTransaction + + `public TransactionSendResult sendMessageInTransaction(Message msg, LocalTransactionExecuter tranExecuter, final Object arg)` + + 发送事务消息。该类不做默认实现,抛出`RuntimeException`异常。参见:`TransactionMQProducer`类。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + msg | Message | 是 | | | 待投递的事务消息 + tranExecuter | `LocalTransactionExecuter` | 是 | | | 本地事务执行器。该类*已过期*,将在5.0.0版本中移除。请勿使用该方法。 + arg | Object | 是 | | | 供本地事务执行程序使用的参数对象 + + - 返回值描述: + + 事务结果,参见:`LocalTransactionState`类。 + + - 异常描述: + + RuntimeException - 永远抛出该异常。 + +26. sendMessageInTransaction + + `public TransactionSendResult sendMessageInTransaction(Message msg, final Object arg)` + + 发送事务消息。该类不做默认实现,抛出`RuntimeException`异常。参见:`TransactionMQProducer`类。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + msg | Message | 是 | | | 待投递的事务消息 + arg | Object | 是 | | | 供本地事务执行程序使用的参数对象 + + - 返回值描述: + + 事务结果,参见:`LocalTransactionState`类。 + + - 异常描述: + + RuntimeException - 永远抛出该异常。 + +27. sendOneway + + `public void sendOneway(Message msg)` + + 以oneway形式发送消息,broker不会响应任何执行结果,和[UDP](https://en.wikipedia.org/wiki/User_Datagram_Protocol)类似。它具有最大的吞吐量但消息可能会丢失。 + + 可在消息量大,追求高吞吐量并允许消息丢失的情况下使用该方式。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + msg | Message | 是 | | | 待投递的消息 + + - 返回值描述: + + void + + - 异常描述: + + MQClientException - broker不存在或未找到;namesrv地址为空;未找到topic的路由信息等客户端异常。
    + RemotingException - 网络异常。
    + InterruptedException - 发送线程中断。 + +28. sendOneway + + `public void sendOneway(Message msg, MessageQueue mq)` + + 向指定队列以oneway形式发送消息,broker不会响应任何执行结果,和[UDP](https://en.wikipedia.org/wiki/User_Datagram_Protocol)类似。它具有最大的吞吐量但消息可能会丢失。 + + 可在消息量大,追求高吞吐量并允许消息丢失的情况下使用该方式。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + msg | Message | 是 | | | 待投递的消息 + mq | MessageQueue | 是 | | | 待投递的消息队列 + + - 返回值描述: + void + - 异常描述: + MQClientException - broker不存在或未找到;namesrv地址为空;未找到topic的路由信息等客户端异常。
    + RemotingException - 网络异常。
    + InterruptedException - 发送线程中断。 + +29. sendOneway + + `public void sendOneway(Message msg, MessageQueueSelector selector, Object arg)` + + 向通过`MessageQueueSelector`计算出的队列以oneway形式发送消息,broker不会响应任何执行结果,和[UDP](https://en.wikipedia.org/wiki/User_Datagram_Protocol)类似。它具有最大的吞吐量但消息可能会丢失。 + + 可在消息量大,追求高吞吐量并允许消息丢失的情况下使用该方式。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + msg | Message | 是 | | | 待发送的消息。 + selector | MessageQueueSelector | 是 | | | 队列选择器。 + arg | Object | 否 | | | 供队列选择器使用的参数对象。 + + - 返回值描述: + + void + + - 异常描述: + + MQClientException - broker不存在或未找到;namesrv地址为空;未找到topic的路由信息等客户端异常。
    + RemotingException - 网络异常。
    + InterruptedException - 发送线程中断。 + +30. shutdown + + `public void shutdown()` + + 关闭当前生产者实例并释放相关资源。 + + - 入参描述: + + 无。 + + - 返回值描述: + + void + + - 异常描述: + +31. start + + `public void start()` + + 启动生产者实例。在发送或查询消息之前必须调用此方法。它执行了许多内部初始化,比如:检查配置、与namesrv建立连接、启动一系列心跳等定时任务等。 + + - 入参描述: + + 无。 + + - 返回值描述: + + void + + - 异常描述: + + MQClientException - 初始化过程中出现失败。 + +32. viewMessage + + `public MessageExt viewMessage(String offsetMsgId)` + + 根据给定的msgId查询消息。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + offsetMsgId | String | 是 | | | offsetMsgId + + - 返回值描述: + + 返回`MessageExt`,包含:topic名称,消息题,消息ID,消费次数,生产者host等信息。 + + - 异常描述: + + RemotingException - 网络层发生错误。
    + MQBrokerException - broker发生错误。
    + InterruptedException - 线程被中断。
    + MQClientException - 生产者状态非Running;msgId非法等。 + +33. viewMessage + + `public MessageExt viewMessage(String topic, String msgId)` + + 根据给定的msgId查询消息,并指定topic。 + + - 入参描述: + + 参数名 | 类型 | 是否必须 | 默认值 | 值范围 | 说明 + ---|---|---|---|---|--- + msgId | String | 是 | | | msgId + topic | String | 是 | | | topic名称 + + - 返回值描述: + + 返回`MessageExt`,包含:topic名称,消息题,消息ID,消费次数,生产者host等信息。 + + - 异常描述: + + RemotingException - 网络层发生错误。
    + MQBrokerException - broker发生错误。
    + InterruptedException - 线程被中断。
    + MQClientException - 生产者状态非Running;msgId非法等。 \ No newline at end of file diff --git a/docs/cn/concept.md b/docs/cn/concept.md new file mode 100644 index 00000000000..3d67e93717f --- /dev/null +++ b/docs/cn/concept.md @@ -0,0 +1,50 @@ +# 基本概念 +---- +## 1 消息模型(Message Model) + +RocketMQ主要由 Producer、Broker、Consumer 三部分组成,其中Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息。Broker 在实际部署过程中对应一台服务器,每个 Broker 可以存储多个Topic的消息,每个Topic的消息也可以分片存储于不同的 Broker。Message Queue 用于存储消息的物理地址,每个Topic中的消息地址存储于多个 Message Queue 中。ConsumerGroup 由多个Consumer 实例构成。 + +## 2 消息生产者(Producer) + 负责生产消息,一般由业务系统负责生产消息。一个消息生产者会把业务应用系统里产生的消息发送到broker服务器。RocketMQ提供多种发送方式,同步发送、异步发送、顺序发送、单向发送。同步和异步方式均需要Broker返回确认信息,单向发送不需要。 + +## 3 消息消费者(Consumer) + 负责消费消息,一般是后台系统负责异步消费。一个消息消费者会从Broker服务器拉取消息、并将其提供给应用程序。从用户应用的角度而言提供了两种消费形式:拉取式消费、推动式消费。 + +## 4 主题(Topic) + 表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。 + +## 5 代理服务器(Broker Server) +消息中转角色,负责存储消息、转发消息。代理服务器在RocketMQ系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备。代理服务器也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等。 + +## 6 名字服务(Name Server) +名字服务充当路由消息的提供者。生产者或消费者能够通过名字服务查找各主题相应的Broker IP列表。多个Namesrv实例组成集群,但相互独立,没有信息交换。 + +## 7 拉取式消费(Pull Consumer) + Consumer消费的一种类型,应用通常主动调用Consumer的拉消息方法从Broker服务器拉消息、主动权由应用控制。一旦获取了批量消息,应用就会启动消费过程。 + +## 8 推动式消费(Push Consumer) + Consumer消费的一种类型,应用不需要主动调用Consumer的拉消息方法,在底层已经封装了拉取的调用逻辑,在用户层面看来是broker把消息推送过来的,其实底层还是consumer去broker主动拉取消息。 + +## 9 生产者组(Producer Group) + 同一类Producer的集合,这类Producer发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,则Broker服务器会联系同一生产者组的其他生产者实例以提交或回溯消费。 + +## 10 消费者组(Consumer Group) + 同一类Consumer的集合,这类Consumer通常消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。要注意的是,消费者组的消费者实例必须订阅完全相同的Topic。RocketMQ 支持两种消息模式:集群消费(Clustering)和广播消费(Broadcasting)。 + +## 11 集群消费(Clustering) +集群消费模式下,相同Consumer Group的每个Consumer实例平均分摊消息。 + +## 12 广播消费(Broadcasting) +广播消费模式下,相同Consumer Group的每个Consumer实例都接收全量的消息。 + +## 13 普通顺序消息(Normal Ordered Message) +普通顺序消费模式下,消费者通过同一个消息队列( Topic 分区,称作 Message Queue) 收到的消息是有顺序的,不同消息队列收到的消息则可能是无顺序的。 + +## 14 严格顺序消息(Strictly Ordered Message) +严格顺序消息模式下,消费者收到的所有消息均是有顺序的。 + +## 15 消息(Message) +消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。RocketMQ中每个消息拥有唯一的Message ID,且可以携带具有业务标识的Key。系统提供了通过Message ID和Key查询消息的功能。 +## 16 标签(Tag) + 为消息设置的标志,用于同一主题下区分不同类型的消息。来自同一业务单元的消息,可以根据不同业务目的在同一主题下设置不同标签。标签能够有效地保持代码的清晰度和连贯性,并优化RocketMQ提供的查询系统。消费者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。 + diff --git a/docs/cn/controller/deploy.md b/docs/cn/controller/deploy.md new file mode 100644 index 00000000000..fa599f3dccc --- /dev/null +++ b/docs/cn/controller/deploy.md @@ -0,0 +1,164 @@ +# 部署和升级指南 + +## Controller部署 + +若需要保证Controller具备容错能力,Controller部署需要三副本及以上(遵循Raft的多数派协议)。 + +> Controller若只部署单副本也能完成Broker Failover,但若该单点Controller故障,会影响切换能力,但不会影响存量集群的正常收发。 + +Controller部署有两种方式。一种是嵌入于NameServer进行部署,可以通过配置enableControllerInNamesrv打开(可以选择性打开,并不强制要求每一台NameServer都打开),在该模式下,NameServer本身能力仍然是无状态的,也就是内嵌模式下若NameServer挂掉多数派,只影响切换能力,不影响原来路由获取等功能。另一种是独立部署,需要单独部署Controller组件。 + +### 嵌入NameServer部署 + +嵌入NameServer部署时只需要在NameServer的配置文件中设置enableControllerInNamesrv=true,并填上Controller的配置即可。 + +``` +enableControllerInNamesrv = true +controllerDLegerGroup = group1 +controllerDLegerPeers = n0-127.0.0.1:9877;n1-127.0.0.1:9878;n2-127.0.0.1:9879 +controllerDLegerSelfId = n0 +controllerStorePath = /home/admin/DledgerController +enableElectUncleanMaster = false +notifyBrokerRoleChanged = true +``` + +参数解释: + +- enableControllerInNamesrv:Nameserver中是否开启controller,默认false。 +- controllerDLegerGroup:DLedger Raft Group的名字,同一个DLedger Raft Group保持一致即可。 +- controllerDLegerPeers:DLedger Group 内各节点的端口信息,同一个 Group 内的各个节点配置必须要保证一致。 +- controllerDLegerSelfId:节点 id,必须属于 controllerDLegerPeers 中的一个;同 Group 内各个节点要唯一。 +- controllerStorePath:controller日志存储位置。controller是有状态的,controller重启或宕机需要依靠日志来恢复数据,该目录非常重要,不可以轻易删除。 +- enableElectUncleanMaster:是否可以从SyncStateSet以外选举Master,若为true,可能会选取数据落后的副本作为Master而丢失消息,默认为false。 +- notifyBrokerRoleChanged:当broker副本组上角色发生变化时是否主动通知,默认为true。 +- scanNotActiveBrokerInterval:扫描 Broker是否存活的时间间隔。 + +其他一些参数可以参考ControllerConfig代码。 + +参数设置完成后,指定配置文件启动Nameserver即可。 + +### 独立部署 + +独立部署执行以下脚本即可 + +```shell +sh bin/mqcontroller -c controller.conf +``` +mqcontroller脚本在distribution/bin/mqcontroller,配置参数与内嵌模式相同。 + +## Broker Controller模式部署 + +Broker启动方法与之前相同,增加以下参数 + +- enableControllerMode:Broker controller模式的总开关,只有该值为true,controller模式才会打开。默认为false。 +- controllerAddr:controller的地址,两种方式填写。 + - 直接填写多个Controller IP地址,多个controller中间用分号隔开,例如`controllerAddr = 127.0.0.1:9877;127.0.0.1:9878;127.0.0.1:9879`。注意由于Broker需要向所有controller发送心跳,因此请填上所有的controller地址。 + - 填写域名,然后设置fetchControllerAddrByDnsLookup为true,则Broker去自动解析域名后面的多个真实controller地址。 +- fetchControllerAddrByDnsLookup:controllerAddr填写域名时,如果设置该参数为true,会自动获取所有controller的地址。默认为false。 +- controllerHeartBeatTimeoutMills:Broker和controller之间心跳超时时间,心跳超过该时间判断Broker不在线。 +- syncBrokerMetadataPeriod:向controller同步Broker副本信息的时间间隔。默认5000(5s)。 +- checkSyncStateSetPeriod:检查SyncStateSet的时间间隔,检查SyncStateSet可能会shrink SyncState。默认5000(5s)。 +- syncControllerMetadataPeriod:同步controller元数据的时间间隔,主要是获取active controller的地址。默认10000(10s)。 +- haMaxTimeSlaveNotCatchup:表示slave没有跟上Master的最大时间间隔,若在SyncStateSet中的slave超过该时间间隔会将其从SyncStateSet移除。默认为15000(15s)。 +- storePathEpochFile:存储epoch文件的位置。epoch文件非常重要,不可以随意删除。默认在store目录下。 +- allAckInSyncStateSet:若该值为true,则一条消息需要复制到SyncStateSet中的每一个副本才会向客户端返回成功,可以保证消息不丢失。默认为false。 +- syncFromLastFile:若slave是空盘启动,是否从最后一个文件进行复制。默认为false。 +- asyncLearner:若该值为true,则该副本不会进入SyncStateSet,也就是不会被选举成Master,而是一直作为一个learner副本进行异步复制。默认为false。 +- inSyncReplicas:需保持同步的副本组数量,默认为1,allAckInSyncStateSet=true时该参数无效。 +- minInSyncReplicas:最小需保持同步的副本组数量,若SyncStateSet中副本个数小于minInSyncReplicas则putMessage直接返回PutMessageStatus.IN_SYNC_REPLICAS_NOT_ENOUGH,默认为1。 + +在Controller模式下,Broker配置必须设置enableControllerMode=true,并填写controllerAddr。 + +### 重要参数解析 + +1.写入副本参数 + +其中inSyncReplicas、minInSyncReplicas等参数在普通Master-Salve部署、SlaveActingMaster模式、自动主从切换架构有重叠和不同含义,具体区别如下 + +| | inSyncReplicas | minInSyncReplicas | enableAutoInSyncReplicas | allAckInSyncStateSet | haMaxGapNotInSync | haMaxTimeSlaveNotCatchup | +|----------------------|---------------------------------------------------------------------|--------------------------------------------------------------------------|---------------------------------------------|---------------------------------------------------------------|---------------------------------------|---------------------------------------------------| +| 普通Master-Salve部署 | 同步复制下需要ACK的副本数,异步复制无效 | 无效 | 无效 | 无效 | 无效 | 无效 | +| 开启SlaveActingMaster (slaveActingMaster=true) | 不自动降级情况下同步复制下需要ACK的副本数 | 自动降级后,需要ACK最小副本数 | 是否开启自动降级,自动降级后,ACK最小副本数降级到minInSyncReplicas | 无效 | 判断降级依据:Slave与Master Commitlog差距值,单位字节 | 无效 | +| 自动主从切换架构(enableControllerMode=true) | 不开启allAckInSyncStateSet下,同步复制下需要ACK的副本数,开启allAckInSyncStateSet后该值无效 | SyncStateSet可以降低到最小的副本数,如果SyncStateSet中副本个数小于minInSyncReplicas则直接返回副本数不足 | 无效 | 若该值为true,则一条消息需要复制到SyncStateSet中的每一个副本才会向客户端返回成功,该参数可以保证消息不丢失 | 无效 | SyncStateSet收缩时,Slave最小未跟上Master的时间差,详见[RIP-44](https://shimo.im/docs/N2A1Mz9QZltQZoAD) | + +总结来说: +- 普通Master-Slave下无自动降级能力,除了inSyncReplicas其他参数均无效,inSyncReplicas表示同步复制下需要ACK的副本数。 +- slaveActingMaster模式下开启enableAutoInSyncReplicas有降级能力,最小可降级到minInSyncReplicas副本数,降级判断依据是主备Commitlog高度差(haMaxGapNotInSync)以及副本存活情况,参考[slaveActingMaster模式自动降级](../QuorumACK.md)。 +> SlaveActingMaster为其他高可用部署方式,该模式下如果不使用可不参考 +- 自动主从切换(Controller模式)依赖SyncStateSet的收缩进行自动降级,SyncStateSet副本数最小收缩到minInSyncReplicas仍能正常工作,小于minInSyncReplicas直接返回副本数不足,收缩依据之一是Slave跟上的时间间隔(haMaxTimeSlaveNotCatchup)而非Commitlog高度。 +- 自动主从切换(Controller模式)正常情况是要求保证不丢消息的,只需设置allAckInSyncStateSet = true 即可,不需要考虑inSyncReplicas参数(该参数无效),如果副本较多、距离较远对延迟有要求,可以参考设置部分副本设置为asyncLearner。 + +2.SyncStateSet收缩检查配置 + +checkSyncStateSetPeriod 参数决定定时检查SyncStateSet是否需要收缩的时间间隔 +haMaxTimeSlaveNotCatchup 参数决定备跟不上主的时间 + +当allAckInSyncState = true时(保证不丢消息), +- haMaxTimeSlaveNotCatchup 值越小,对SyncStateSet收缩越敏感,比如主备之间网络抖动就可能导致SyncStateSet收缩,造成不必要的集群抖动。 +- haMaxTimeSlaveNotCatchup 值越大,对SyncStateSet收缩虽然不敏感,但是可能加大SyncStateSet收缩时的RTO时间。该RTO时间可以按照 checkSyncStateSetPeriod/2 + haMaxTimeSlaveNotCatchup 估算。 + +3.消息可靠性配置 + +保证 allAckInSyncStateSet = true 以及 enableElectUncleanMaster = false + +4.延迟 + +当 allAckInSyncStateSet = true 后,一条消息要复制到SyncStateSet所有副本才能确认返回,假设SyncStateSet有3副本,其中1副本距离较远,则会影响到消息延迟。可以设置延迟最高距离最远的副本为asyncLearner,该副本不会进入SyncStateSet,只会进行异步复制,该副本作为冗余副本。 + +## 兼容性 + +该模式未对任何客户端层面 API 进行新增或修改,不存在客户端的兼容性问题。 + +Nameserver本身能力未做任何修改,Nameserver不存在兼容性问题。如开启enableControllerInNamesrv且controller参数配置正确,则开启controller功能。 + +Broker若设置enableControllerMode=false,则仍然以之前方式运行。若设置enableControllerMode=true,则需要部署controller且参数配置正确才能正常运行。 + +具体行为如下表所示: + +| | 旧版Nameserver | 旧版Nameserver+独立部署Controller | 新版Nameserver开启controller功能 | 新版Nameserver关闭controller功能 | +|-------------------------|--------------|-----------------------------|----------------------------|----------------------------| +| 旧版Broker | 正常运行,无法切换 | 正常运行,无法切换 | 正常运行,无法切换 | 正常运行,无法切换 | +| 新版Broker开启Controller模式 | 无法正常上线 | 正常运行,可以切换 | 正常运行,可以切换 | 无法正常上线 | +| 新版Broker不开启Controller模式 | 正常运行,无法切换 | 正常运行,无法切换 |正常运行,无法切换 | 正常运行,无法切换 | + +## 升级注意事项 + +从上述兼容性表述可以看出,NameServer正常升级即可,无兼容性问题。在不想升级Nameserver情况,可以独立部署Controller组件来获得切换能力。 + +针对Broker升级,分为两种情况: + +(1)Master-Slave部署升级成Controller切换架构 + +可以带数据进行原地升级,对于每组Broker,停机主、备Broker,**保证主、备的Commitlog对齐**(可以在升级前禁写该组Broker一段时间,或则通过拷贝方式保证一致),升级包后重新启动即可。 + +> 若主备commitlog不对齐,需要保证主上线以后再上线备,否则可能会因为数据截断而丢失消息。 + +(2)原DLedger模式升级到Controller切换架构 + +由于原DLedger模式消息数据格式与Master-Slave下数据格式存在区别,不提供带数据原地升级的路径。在部署多组Broker的情况下,可以禁写某一组Broker一段时间(只要确认存量消息被全部消费即可,比如根据消息的保存时间来决定),然后清空store目录下除config/topics.json、subscriptionGroup.json下(保留topic和订阅关系的元数据)的其他文件后,进行空盘升级。 + +### 持久化BrokerID版本的升级注意事项 + +目前版本支持采用了新的持久化BrokerID版本,详情可以参考[该文档](persistent_unique_broker_id.md),从该版本前的5.x升级到当前版本需要注意如下事项。 + +4.x版本升级遵守上述正常流程即可。 +5.x非持久化BrokerID版本升级到持久化BrokerID版本按照如下流程: + +**升级Controller** + +1. 将旧版本Controller组停机。 +2. 清除Controller数据,即默认在`~/DLedgerController`下的数据文件。 +3. 上线新版Controller组。 + +> 在上述升级Controller流程中,Broker仍可正常运行,但无法切换。 + +**升级Broker** + +1. 将Broker从节点停机。 +2. 将Broker主节点停机。 +3. 将所有的Broker的Epoch文件删除,即默认为`~/store/epochFileCheckpoint`和`~/store/epochFileCheckpoint.bak`。 +4. 将原先的主Broker先上线,等待该Broker当选为master。(可使用`admin`命令的`getSyncStateSet`来观察) +5. 将原来的从Broker全部上线。 + +> 建议停机时先停从再停主,上线时先上原先的主再上原先的从,这样可以保证原来的主备关系。 +> 若需要改变升级前后主备关系,则需要停机时保证主、备的CommitLog对齐,否则可能导致数据被截断而丢失。 \ No newline at end of file diff --git a/docs/cn/controller/design.md b/docs/cn/controller/design.md new file mode 100644 index 00000000000..563a624eddc --- /dev/null +++ b/docs/cn/controller/design.md @@ -0,0 +1,205 @@ +# 背景 + +当前 RocketMQ Raft 模式主要是利用 DLedger Commitlog 替换原来的 Commitlog,使 Commitlog 拥有选举复制能力,但这也造成了一些问题: + +- Raft 模式下,Broker组内副本数必须是三副本及以上,副本的ACK也必须遵循多数派协议。 +- RocketMQ 存在两套 HA 复制流程,且 Raft 模式下的复制无法利用 RocketMQ 原生的存储能力。 + +因此我们希望利用 DLedger 实现一个基于 Raft 的一致性模块(DLedger Controller),并当作一个可选的选主组件,支持独立部署,也可以嵌入在 Nameserver 中,Broker 通过与 Controller 的交互完成 Master 的选举,从而解决上述问题,我们将该新模式称为 Controller 模式。 + +# 架构 + +## 核心思想 + +![架构图](../image/controller/controller_design_1.png) + +如图是 Controller 模式的核心架构,介绍如下: + +- DledgerController:利⽤ DLedger ,构建⼀个保证元数据强⼀致性的 DLedger Controller 控制器,利⽤ Raft 选举会选出⼀个 Active DLedger Controller 作为主控制器,DLedger Controller 可以内嵌在 Nameserver中,也可以独立的部署。其主要作用是,用来存储和管理 Broker 的 SyncStateSet 列表,并在某个 Broker 的 Master Broker 下线或⽹络隔离时,主动发出调度指令来切换 Broker 的 Master。 +- SyncStateSet:主要表示⼀个 broker 副本组中跟上 Master 的 Slave 副本加上 Master 的集合。主要判断标准是 Master 和 Slave 之间的差距。当 Master 下线时,我们会从 SyncStateSet 列表中选出新的 Master。 SyncStateSet 列表的变更主要由 Master Broker 发起。Master通过定时任务判断和同步过程中完成 SyncStateSet 的Shrink 和 Expand,并向选举组件 Controller 发起 Alter SyncStateSet 请求。 +- AutoSwitchHAService:一个新的 HAService,在 DefaultHAService 的基础上,支持 BrokerRole 的切换,支持 Master 和 Slave 之间互相转换 (在 Controller 的控制下) 。此外,该 HAService 统一了日志复制流程,会在 HA HandShake 阶段进行日志的截断。 +- ReplicasManager:作为一个中间组件,起到承上启下的作用。对上,可以定期同步来自 Controller 的控制指令,对下,可以定期监控 HAService 的状态,并在合适的时间修改 SyncStateSet。ReplicasManager 会定期同步 Controller 中关于该 Broker 的元数据,当 Controller 选举出一个新的 Master 的时候,ReplicasManager 能够感知到元数据的变化,并进行 BrokerRole 的切换。 + +## DLedgerController 核心设计 + +![image-20220605213143645](../image/controller/quick-start/controller.png) + +如图是 DledgerController 的核心设计: + +- DLedgerController 可以内嵌在 Namesrv 中,也可以独立的部署。 +- Active DLedgerController 是 DLedger 选举出来的 Leader,其会接受来自客户端的事件请求,并通过 DLedger 发起共识,最后应用到内存元数据状态机中。 +- Not Active DLedgerController,也即 Follower 角色,其会通过 DLedger 复制来自 Active DLedgerController 的事件日志,然后直接运用到状态机中。 + +## 日志复制 + +### 基本概念与流程 + +为了统一日志复制流程,区分每一任 Master 的日志复制边界,方便日志截断,引入了 MasterEpoch 的概念,代表当前 Master 的任期号 (类似 Raft Term 的含义)。 + +对于每一任 Master,其都有 MasterEpoch 与 StartOffset,分别代表该 Master 的任期号与起始日志位移。 + +需要注意的是,MasterEpoch 是由 Controller 决定的,且其是单调递增的。 + +此外,我们还引入了 EpochFile,用于存放 序列。 + +**当⼀个 Broker 成为 Master,其会:** + +- 将 Commitlog 截断到最后⼀条消息的边界。 + +- 同时最新将 持久化到 EpochFile,startOffset 也即当前 CommitLog 的 MaxPhyOffset 。 + +- 然后 HAService 监听连接,创建 HAConnection,配合 Slave 完成流程交互。 + +**当一个 Broker 成为 Slave,其会:** + +Ready 阶段: + +- 将Commitlog截断到最后⼀条消息的边界。 + +- 与Master建⽴连接。 + +Handshake 阶段: + +- 进⾏⽇志截断,这⾥关键在于 Slave 利⽤本地的 epoch 与 startOffset 和 Master 对⽐,找到⽇志截断点,进⾏⽇志截断。 + +Transfer 阶段: + +- 从 Master 同步日志。 + +### 截断算法 + +具体的日志截断算法流程如下: + +- 在 HandShake 阶段, Slave 会从 Master 处获取 Master 的 EpochCache 。 + +- Slave ⽐较获取到的 Master EpochCahce ,从后往前依次和本地进行比对,如果二者的 Epoch 与 StartOffset 相等, 则该 Epoch 有效,截断位点为两者中较⼩的 Endoffset,截断后修正⾃⼰的 信息,进⼊Transfer 阶 段;如果不相等,对比 Slave 前⼀个epoch,直到找到截断位点。 + +```java +slave:TreeMap> epochMap; +Iterator iterator = epochMap.entrySet().iterator(); +truncateOffset = -1; + +//Epoch为从⼤到⼩排序 +while (iterator.hasNext()) { + Map.Entry> curEntry = iterator.next(); + Pair masterOffset= + findMasterOffsetByEpoch(curEntry.getKey()); + + if(masterOffset != null && + curEntry.getKey().getObejct1() == masterOffset.getObejct1()) { + truncateOffset = Math.min(curEntry.getKey().getObejct2(), masterOffset.getObejct2()); + break; + } +} +``` + +### 复制流程 + +由于 Ha 是基于流进行日志复制的,我们无法分清日志的边界 (也即传输的一批日志可能横跨多个 MasterEpoch),Slave 无法感知到 MasterEpoch 的变化,也就无法及时修改 EpochFile。 + +因此,我们做了如下改进: + +Master 传输⽇志时,保证⼀次发送的⼀个 batch 是同⼀个 epoch 中的,⽽不能横跨多个 epoch。可以在WriteSocketService 中新增两个变量: + +- currentTransferEpoch:代表当前 WriteSocketService.nextTransferFromWhere 对应在哪个 epoch 中 + +- currentTransferEpochEndOffset: 对应 currentTransferEpoch 的 end offset.。如果 currentTransferEpoch == MaxEpoch,则 currentTransferEpochEndOffset= -1,表示没有界限。 + +WriteSocketService 传输下⼀批⽇志时 (假设这⼀批⽇志总⼤⼩为 size),如果发现 + +nextTransferFromWhere + size > currentTransferEpochEndOffset,则将 selectMappedBufferResult limit ⾄ currentTransferEpochEndOffset。 最后,修改 currentTransferEpoch 和 currentTransferEpochEndOffset ⾄下⼀个 epoch。 + +相应的, Slave 接受⽇志时,如果从 header 中发现 epoch 变化,则记录到本地 epoch⽂件中。 + +### 复制协议 + +根据上文我们可以知道,AutoSwitchHaService 对日志复制划分为多个阶段,下面介绍是该 HaService 的协议。 + +#### Handshake 阶段 + +1.AutoSwitchHaClient (Slave) 会向 Master 发送 HandShake 包,如下: + +![示意图](../image/controller/controller_design_3.png) + +`current state(4byte) + Two flags(4byte) + slaveAddressLength(4byte) + slaveAddress(50byte)` + +- Current state 代表当前的 HAConnectionState,也即 HANDSHAKE。 + +- Two flags 是两个状态标志位,其中,isSyncFromLastFile 代表是否要从 Master 的最后一个文件开始复制,isAsyncLearner 代表该 Slave 是否是异步复制,并以 Learner 的形式接入 Master。 + +- slaveAddressLength 与 slaveAddress 代表了该 Slave 的地址,用于后续加入 SyncStateSet 。 + +2.AutoSwitchHaConnection (Master) 会向 Slave 回送 HandShake 包,如下: + +![示意图](../image/controller/controller_design_4.png) + +`current state(4byte) + body size(4byte) + offset(8byte) + epoch(4byte) + body` + +- Current state 代表当前的 HAConnectionState,也即 HANDSHAKE。 +- Body size 代表了 body 的长度。 +- Offset 代表 Master 端日志的最大偏移量。 +- Epoch 代表了 Master 的 Epoch 。 +- Body 中传输的是 Master 端的 EpochEntryList 。 + +Slave 收到 Master 回送的包后,就会在本地进行上文阐述的日志截断流程。 + +#### Transfer 阶段 + +1.AutoSwitchHaConnection (Master) 会不断的往 Slave 发送日志包,如下: + +![示意图](../image/controller/controller_design_5.png) + +`current state(4byte) + body size(4byte) + offset(8byte) + epoch(4byte) + epochStartOffset(8byte) + additionalInfo(confirmOffset) (8byte)+ body` + +- Current state:代表当前的 HAConnectionState,也即 Transfer 。 +- Body size:代表了 body 的长度。 +- Offset:当前这一批次的日志的起始偏移量。 +- Epoch:代表当前这一批次日志所属的 MasterEpoch。 +- epochStartOffset:代表当前这一批次日志的 MasterEpoch 对应的 StartOffset。 +- confirmOffset:代表在 SyncStateSet 中的副本的最小偏移量。 +- Body:日志。 + +2.AutoSwitchHaClient (Slave) 会向 Master 发送 ACK 包: + +![示意图](../image/controller/controller_design_6.png) + +` current state(4byte) + maxOffset(8byte)` + +- Current state:代表当前的 HAConnectionState,也即 Transfer 。 +- MaxOffset:代表当前 Slave 的最大日志偏移量。 + +## Master 选举 + +### 基本流程 + +ELectMaster 主要是在某 Broker 副本组的 Master 下线或不可访问时,重新从 SyncStateSet 列表⾥⾯选出⼀个新的 Master,该事件由 Controller ⾃身或者通过运维命令`electMaster` 发起Master选举。 + +无论 Controller 是独立部署,还是嵌入在 Namesrv 中,其都会监听每个 Broker 的连接通道,如果某个 Broker channel inActive 了,就会判断该 Broker 是否为 Master,如果是,则会触发选主的流程。 + +选举 Master 的⽅式⽐较简单,我们只需要在该组 Broker 所对应的 SyncStateSet 列表中,挑选⼀个出来成为新的 Master 即可,并通过 DLedger 共识后应⽤到内存元数据,最后将结果通知对应的Broker副本组。 + +### SyncStateSet 变更 + +SyncStateSet 是选主的重要依据,SyncStateSet 列表的变更主要由 Master Broker 发起。Master通过定时任务判断和同步过程中完成 SyncStateSet 的Shrink 和 Expand,并向选举组件 Controller 发起 Alter SyncStateSet 请求。 + +#### Shrink + +Shrink SyncStateSet ,指把 SyncStateSet 副本集合中那些与Master差距过⼤的副本移除,判断依据如下: + +- 增加 haMaxTimeSlaveNotCatchUp 参数 。 + +- HaConnection 中记录 Slave 上⼀次跟上 Master 的时间戳 lastCaughtUpTimeMs,该时间戳含义是:每次Master 向 Slave 发送数据(transferData)时记录⾃⼰当前的 MaxOffset 为 lastMasterMaxOffset 以及当前时间戳 lastTransferTimeMs。 + +- ReadSocketService 接收到 slaveAckOffset 时若 slaveAckOffset >= lastMasterMaxOffset 则将lastCaughtUpTimeMs 更新为 lastTransferTimeMs。 + +- Master 端通过定时任务扫描每一个 HaConnection,如果 (cur_time - connection.lastCaughtUpTimeMs) > haMaxTimeSlaveNotCatchUp,则该 Slave 是 Out-of-sync 的。 + +- 如果检测到 Slave out of sync ,master 会立刻向 Controller 上报SyncStateSet,从而 Shrink SyncStateSet。 + +#### Expand + +如果⼀个 Slave 副本追赶上了 Master,Master 需要及时向Controller Alter SyncStateSet 。加⼊SyncStateSet 的条件是 slaveAckOffset >= ConfirmOffset(当前 SyncStateSet 中所有副本的 MaxOffset 的最⼩值)。 + +## 参考资料 + +[RIP-44原文](https://github.com/apache/rocketmq/wiki/RIP-44-Support-DLedger-Controller) diff --git a/docs/cn/controller/persistent_unique_broker_id.md b/docs/cn/controller/persistent_unique_broker_id.md new file mode 100644 index 00000000000..1d7a289fb7f --- /dev/null +++ b/docs/cn/controller/persistent_unique_broker_id.md @@ -0,0 +1,135 @@ +# 持久化的唯一BrokerId + +## 现阶段问题 + +在 RocketMQ 5.0.0 和 5.1.0 版本中,采用`BrokerAddress`作为Broker在Controller模式下的唯一标识。导致如下情景出现问题: + +- 在容器或者K8s环境下,每次Broker的重启或升级都可能会导致IP发生变化,导致之前的`BrokerAddress`留下的记录没办法和重启后的Broker联系起来,比如说`ReplicaInfo`, `SyncStateSet`等数据。 + +## 改进方案 + +在Controller侧采用`BrokerName:BrokerId`作为唯一标识,不再以`BrokerAddress`作为唯一标识。并且需要对`BrokerId`进行持久化存储,由于`ClusterName`和`BrokerName`都是启动的时候在配置文件中配置好的,所以只需要处理`BrokerId`的分配和持久化问题。 +Broker第一次上线的时候,只有配置文件中配置的`ClusterName`和`BrokerName`,以及自身的`BrokerAddress`。那么我们需要和`Controller`协商出一个在整个集群生命周期中都唯一确定的标识:`BrokerId`,该`BrokerId`从1开始分配。当某一个Broker被选为Master的时候,在向Name Server中重新注册时,将更改为`BrokerId`为0 (兼容之前逻辑 brokerId为0代表着Broker是Master身份)。 + +### 上线流程 + +![register process](../image/controller/persistent_unique_broker_id/register_process.png) + +#### 1. GetNextBrokerId Request + +这时候发起一个`GetNextBrokerId`的请求到Controller,为了拿到当前的下一个待分配的`BrokerId`(从1开始分配)。 + +#### 1.1 ReadFromDLedger + +此时Controller接收到请求,然后走DLedger去获取到状态机的`NextBrokerId`数据。 + +#### 2. GetNextBrokerId Response + +Controller将`NextBrokerId`返回给Broker。 + +#### 2.1 CreateTempMetaFile + +Broker拿到`NextBrokerId`之后,创建一个临时文件`.broker.meta.temp`,里面记录了`NextBrokerId`(也就是期望应用的`BrokerId`),以及自己生成一个`RegisterCode`(用于之后的身份校验)也持久化到临时文件中。 + +#### 3. ApplyBrokerId Request + +Broker携带着当前自己的基本数据(`ClusterName`、`BrokerName`和`BrokerAddress`)以及此时期望应用的`BrokerId`和`RegisterCode`,发送一个`ApplyBrokerId`的请求到Controller。 + +#### 3.1 CASApplyBrokerId + +Controller通过DLedger写入该事件,当该事件(日志)被应用到状态机的时候,判断此时是否可以应用该`BrokerId`(若`BrokerId`已被分配并且也不是分配给该Broker时则失败)。并且此时会记录下来该`BrokerId`和`RegisterCode`之间的关系。 + +#### 4. ApplyBrokerId Response + +若上一步成功应用了该`BrokerId`,此时则返回成功给Broker,若失败则返回当前的`NextBrokerId`。 + +#### 4.1 CreateMetaFileFromTemp + +若上一步成功的应用了该`BrokerId`,那么此时可以视为Broker侧成功的分配了该BrokerId,那么此时我们也需要彻底将这个BrokerId的信息持久化,那么我们就可以直接原子删除`.broker.meta.temp`并创建`.broker.meta`。删除和创建这两步需为原子操作。 + +> 经过上述流程,第一次上线的broker和controller成功协商出一个双方都认同的brokeId并持久化保存起来。 + +#### 5. RegisterBrokerToController Request + +之前的步骤已经正确协商出了`BrokerId`,但是这时候有可能Controller侧保存的`BrokerAddress`是上次Broker上线的时候的`BrokerAddress`,所以现在需要更新一下`BrokerAddress`,发送一个`RegisterBrokerToController` 请求并带上当前的`BrokerAddress`。 + +#### 5.1 UpdateBrokerAddress + +Controller比对当前该Broker在Controller状态机中保存的`BrokerAddress`,若和请求中携带的不一致则更新为请求中的`BrokerAddress`。 + +#### 6. RegisterBrokerToController Response + +Controller侧在更新完`BrokerAddress`之后可携带着当前该Broker所在的`Broker-set`的主从信息返回,用于通知Broker进行相应的身份转变。 + +### 注册状态轮转 + +![register state transfer](../image/controller/persistent_unique_broker_id/register_state_transfer.png) + +### 故障容错 + +> 如果在正常上线流程中出现了各种情况的宕机,则以下流程保证正确的`BrokerId`分配 + +#### 正常重启后的节点上线 + +若是正常重启,那么则已经在双方协商出唯一的`BrokerId`,并且本地也在`broker.meta`中有该`BrokerId`的数据,那么就该注册流程不需要进行,直接继续后面的流程即可。即从`RegisterBrokerToController`处继续上线即可。 + +![restart normally](../image/controller/persistent_unique_broker_id/normal_restart.png) + +#### CreateTempMetaFile失败 + +![fail at creating temp metadata file](../image/controller/persistent_unique_broker_id/fail_create_temp_metadata_file.png) + +如果是上图中的流程失败的话,那么Broker重启后,Controller侧的状态机本身也没有分配任何`BrokerId`。Broker自身也没有任何数据被保存。因此直接重新按照上述流程从头开始走即可。 + +#### CreateTempMetaFile成功,ApplyBrokerId未成功 + +若是Controller侧已经认为本次`ApplyBrokerId`请求不对(请求去分配一个已被分配的`BrokerId`并且该 `RegisterCode`不相等),并且此时返回当前的`NextBrokerId`给Broker,那么此时Broker直接删除`.broker.meta.temp`文件,接下来回到第2步,重新开始该流程以及后续流程。 + +![fail at applying broker id](../image/controller/persistent_unique_broker_id/fail_apply_broker_id.png) + +#### ApplyBrokerId成功,CreateMetaFileFromTemp未成功 + +上述情况可以出现在`ApplyResult`丢失、CAS删除并创建`broker.meta`失败,这俩流程中。 +那么重启后,Controller侧是已经认为我们`ApplyBrokerId`流程是成功的了,而且也已经在状态机中修改了BrokerId的分配数据,那么我们这时候重新直接开始步骤3,也就是发送`ApplyBrokerId`请求的这一步。 + +![fail at create metadata file](../image/controller/persistent_unique_broker_id/fail_create_metadata_file_and_delete_temp.png) + +因为我们有`.broker.meta.temp`文件,可以从中拿到我们之前成功在Controller侧应用的`BrokerId`和`RegisterCode`,那么直接发送给Controller,如果Controller中存在该`BrokerId`并且`RegisterCode`和请求中的`RegisterCode`相等,那么视为成功。 + +### 正确上线后使用BrokerId作为唯一标识 + +当正确上线之后,之后Broker的请求和状态记录都以`BrokerId`作为唯一标识。心跳等数据的记录都以`BrokerId`为标识。 +同时Controller侧也会记录当前该`BrokerId`的`BrokerAddress`,在主从切换等时候用于通知Broker状态变化。 + +> 默认持久化ID的文件在~/store/brokerIdentity,也可以设置storePathBrokerIdentity参数来决定存储路径。在自动主备切换模式下,不要随意删除该文件,否则该 Broker 会被当作新 Broker 上线。 + +## 升级方案 + +4.x 版本升级遵守 5.0 升级文档流程即可。 +5.0.0 和 5.1.0 非持久化BrokerId版本升级到 5.1.1 及以上持久化BrokerId版本按照如下流程: + +### 升级Controller + +1. 将旧版本Controller组停机。 +2. 清除Controller数据,即默认在`~/DLedgerController`下的数据文件。 +3. 上线新版Controller组。 + +> 在上述升级Controller流程中,Broker仍可正常运行,但无法切换。 + +### 升级Broker + +1. 将Broker从节点停机。 +2. 将Broker主节点停机。 +3. 将所有的Broker的Epoch文件删除,即默认为`~/store/epochFileCheckpoint`和`~/store/epochFileCheckpoint.bak`。 +4. 将原先的主Broker先上线,等待该Broker当选为master。(可使用`admin`命令的`getSyncStateSet`来观察) +5. 将原来的从Broker全部上线。 + +> 建议停机时先停从再停主,上线时先上原先的主再上原先的从,这样可以保证原来的主备关系。 +若需要改变升级前后主备关系,则需要停机时保证主、备的CommitLog对齐,否则可能导致数据被截断而丢失。 + +### 兼容性 + +| | 5.1.0 及以下版本 Controller | 5.1.1 及以上版本 Controller | +|--------------------|------------------------|------------------------------------| +| 5.1.0 及以下版本 Broker | 正常运行,可切换 | 若已主备确定则可正常运行,不可切换。若broker重新启动则无法上线 | +| 5.1.1 及以上版本 Broker | 无法正常上线 | 正常运行,可切换 | diff --git a/docs/cn/controller/quick_start.md b/docs/cn/controller/quick_start.md new file mode 100644 index 00000000000..5cc7d6d81fe --- /dev/null +++ b/docs/cn/controller/quick_start.md @@ -0,0 +1,200 @@ +# 自动主从切换快速开始 + +## 前言 + +![架构图](../image/controller/controller_design_2.png) + +该文档主要介绍如何快速构建自动主从切换的 RocketMQ 集群,其架构如上图所示,主要增加支持自动主从切换的Controller组件,其可以独立部署也可以内嵌在NameServer中。 + +详细设计思路请参考 [设计思想](design.md). + +详细的新集群部署和旧集群升级指南请参考 [部署指南](deploy.md)。 + +## 编译 RocketMQ 源码 + +```shell +$ git clone https://github.com/apache/rocketmq.git + +$ cd rocketmq + +$ mvn -Prelease-all -DskipTests clean install -U +``` + +## 快速部署 + +在构建成功后 + +```shell +#{rocketmq-version} replace with rocketmq actual version. example: 5.0.0-SNAPSHOT +$ cd distribution/target/rocketmq-{rocketmq-version}/rocketmq-{rocketmq-version}/ + +$ sh bin/controller/fast-try.sh start +``` + +如果上面的步骤执行成功,可以通过运维命令查看Controller状态。 + +```shell +$ sh bin/mqadmin getControllerMetaData -a localhost:9878 +``` + +-a代表集群中任意一个Controller的地址 + +至此,启动成功,现在可以向集群收发消息,并进行切换测试了。 + +如果需要关闭快速集群,可以执行: + +```shell +$ sh bin/controller/fast-try.sh stop +``` + +对于快速部署,默认配置在 conf/controller/quick-start里面,默认的存储路径在 /tmp/rmqstore,且会开启一个 Controller (嵌入在 Namesrv) 和两个 Broker。 + +### 查看 SyncStateSet + +可以通过运维工具查看 SyncStateSet: + +```shell +$ sh bin/mqadmin getSyncStateSet -a localhost:9878 -b broker-a +``` + +-a 代表的是任意一个 Controller 的地址 + +如果顺利的话,可以看到以下内容: + +![image-20220605205259913](../image/controller/quick-start/syncstateset.png) + +### 查看 BrokerEpoch + +可以通过运维工具查看 BrokerEpochEntry: + +```shell +$ sh bin/mqadmin getBrokerEpoch -n localhost:9876 -b broker-a +``` + +-n 代表的是任意一个 Namesrv 的地址 + +如果顺利的话,可以看到以下内容: + +![image-20220605205247476](../image/controller/quick-start/epoch.png) + +## 切换 + +部署成功后,现在尝试进行 Master 切换。 + +首先,kill 掉原 Master 的进程,在上文的例子中,就是使用端口 30911 的进程: + +```shell +#查找端口: +$ ps -ef|grep java|grep BrokerStartup|grep ./conf/controller/quick-start/broker-n0.conf|grep -v grep|awk '{print $2}' +#杀掉 master: +$ kill -9 PID +``` + +接着,用 SyncStateSet admin 脚本查看: + +```shell +$ sh bin/mqadmin getSyncStateSet -a localhost:9878 -b broker-a +``` + +可以发现 Master 已经发生了切换。 + +![image-20220605211244128](../image/controller/quick-start/changemaster.png) + + + +## Controller内嵌Namesvr集群部署 + +Controller以插件方式内嵌Namesvr集群(3个Node组成)部署,快速启动: + +```shell +$ sh bin/controller/fast-try-namesrv-plugin.sh start +``` + +或者通过命令单独启动: + +```shell +$ nohup bin/mqnamesrv -c ./conf/controller/cluster-3n-namesrv-plugin/namesrv-n0.conf & +$ nohup bin/mqnamesrv -c ./conf/controller/cluster-3n-namesrv-plugin/namesrv-n1.conf & +$ nohup bin/mqnamesrv -c ./conf/controller/cluster-3n-namesrv-plugin/namesrv-n2.conf & +``` + +如果上面的步骤执行成功,可以通过运维命令查看Controller集群状态。 + +```shell +$ sh bin/mqadmin getControllerMetaData -a localhost:9878 +``` + +-a代表的是任意一个 Controller 的地址 + +如果controller启动成功可以看到以下内容: + +``` +#ControllerGroup group1 +#ControllerLeaderId n0 +#ControllerLeaderAddress 127.0.0.1:9878 +#Peer: n0:127.0.0.1:9878 +#Peer: n1:127.0.0.1:9868 +#Peer: n2:127.0.0.1:9858 +``` + +启动成功后Broker Controller模式部署就能使用Controller集群。 + +如果需要快速停止集群: + +```shell +$ sh bin/controller/fast-try-namesrv-plugin.sh stop +``` + +使用 fast-try-namesrv-plugin.sh 脚本快速部署,默认配置在 conf/controller/cluster-3n-namesrv-plugin里面并且会启动3个Namesvr和3个Controller(内嵌Namesrv)。 + +## Controller独立集群部署 + +Controller独立集群(3个Node组成)部署,快速启动: + +```shell +$ sh bin/controller/fast-try-independent-deployment.sh start +``` + +或者通过命令单独启动: + +```shell +$ nohup bin/mqcontroller -c ./conf/controller/cluster-3n-independent/controller-n0.conf & +$ nohup bin/mqcontroller -c ./conf/controller/cluster-3n-independent/controller-n1.conf & +$ nohup bin/mqcontroller -c ./conf/controller/cluster-3n-independent/controller-n2.conf & +``` + +如果上面的步骤执行成功,可以通过运维命令查看Controller集群状态。 + +```shell +$ sh bin/mqadmin getControllerMetaData -a localhost:9878 +``` + +-a代表的是任意一个 Controller 的地址 + +如果Controller启动成功可以看到以下内容: + +``` +#ControllerGroup group1 +#ControllerLeaderId n1 +#ControllerLeaderAddress 127.0.0.1:9868 +#Peer: n0:127.0.0.1:9878 +#Peer: n1:127.0.0.1:9868 +#Peer: n2:127.0.0.1:9858 +``` + +启动成功后Broker Controller模式部署就能使用Controller集群。 + +如果需要快速停止集群: + +```shell +$ sh bin/controller/fast-try-independent-deployment.sh stop +``` + +使用fast-try-independent-deployment.sh 脚本快速部署,默认配置在 conf/controller/cluster-3n-independent里面并且会启动3个Controller(独立部署)组成一个集群。 + + + +## 注意说明 + +- 若需要保证Controller具备容错能力,Controller部署需要三副本及以上(遵循Raft的多数派协议) +- Controller部署配置文件中配置参数`controllerDLegerPeers` 中的IP地址配置成其他节点能够访问的IP,在多机器部署的时候尤为重要。例子仅供参考需要根据实际情况进行修改调整。 diff --git a/docs/cn/design.md b/docs/cn/design.md new file mode 100644 index 00000000000..00b4de37905 --- /dev/null +++ b/docs/cn/design.md @@ -0,0 +1,213 @@ + +# 设计(design) +--- +### 1 消息存储 + +![](image/rocketmq_design_1.png) + +消息存储是RocketMQ中最为复杂和最为重要的一部分,本节将分别从RocketMQ的消息存储整体架构、PageCache与Mmap内存映射以及RocketMQ中两种不同的刷盘方式三方面来分别展开叙述。 + +#### 1.1 消息存储整体架构 +消息存储架构图中主要有下面三个跟消息存储相关的文件构成。 + +(1) CommitLog:消息主体以及元数据的存储主体,存储Producer端写入的消息主体内容,消息内容不是定长的。单个文件大小默认1G, 文件名长度为20位,左边补零,剩余为起始偏移量,比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。消息主要是顺序写入日志文件,当文件满了,写入下一个文件; + +(2) ConsumeQueue:消息消费索引,引入的目的主要是提高消息消费的性能。由于RocketMQ是基于主题topic的订阅模式,消息消费是针对主题进行的,如果要遍历commitlog文件,根据topic检索消息是非常低效的。Consumer可根据ConsumeQueue来查找待消费的消息。其中,ConsumeQueue作为消费消息的索引,保存了指定Topic下的队列消息在CommitLog中的起始物理偏移量offset,消息大小size和消息Tag的HashCode值。consumequeue文件可以看成是基于topic的commitlog索引文件,故consumequeue文件夹的组织方式如下:topic/queue/file三层组织结构,具体存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。同样consumequeue文件采取定长设计,每一个条目共20个字节,分别为8字节的commitlog物理偏移量、4字节的消息长度、8字节tag hashcode,单个文件由30W个条目组成,可以像数组一样随机访问每一个条目,每个ConsumeQueue文件大小约5.72M; + +(3) IndexFile:IndexFile(索引文件)提供了一种可以通过key或时间区间来查询消息的方法。Index文件的存储位置是:$HOME/store/index/{fileName},文件名fileName是以创建时的时间戳命名的,固定的单个IndexFile文件大小约为400M,一个IndexFile可以保存 2000W个索引,IndexFile的底层存储设计为在文件系统中实现HashMap结构,故RocketMQ的索引文件其底层实现为hash索引。 + +在上面的RocketMQ的消息存储整体架构图中可以看出,RocketMQ采用的是混合型的存储结构,即为Broker单个实例下所有的队列共用一个日志数据文件(即为CommitLog)来存储。RocketMQ的混合型存储结构(多个Topic的消息实体内容都存储于一个CommitLog中)针对Producer和Consumer分别采用了数据和索引部分相分离的存储结构,Producer发送消息至Broker端,然后Broker端使用同步或者异步的方式对消息刷盘持久化,保存至CommitLog中。只要消息被刷盘持久化至磁盘文件CommitLog中,那么Producer发送的消息就不会丢失。正因为如此,Consumer也就肯定有机会去消费这条消息。当无法拉取到消息后,可以等下一次消息拉取,同时服务端也支持长轮询模式,如果一个消息拉取请求未拉取到消息,Broker允许等待30s的时间,只要这段时间内有新消息到达,将直接返回给消费端。这里,RocketMQ的具体做法是,使用Broker端的后台服务线程—ReputMessageService不停地分发请求并异步构建ConsumeQueue(逻辑消费队列)和IndexFile(索引文件)数据。 +#### 1.2 页缓存与内存映射 +页缓存(PageCache)是OS对文件的缓存,用于加速对文件的读写。一般来说,程序对文件进行顺序读写的速度几乎接近于内存的读写速度,主要原因就是由于OS使用PageCache机制对读写访问操作进行了性能优化,将一部分的内存用作PageCache。对于数据的写入,OS会先写入至Cache内,随后通过异步的方式由pdflush内核线程将Cache内的数据刷盘至物理磁盘上。对于数据的读取,如果一次读取文件时出现未命中PageCache的情况,OS从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取。 + +在RocketMQ中,ConsumeQueue逻辑消费队列存储的数据较少,并且是顺序读取,在page cache机制的预读取作用下,Consume Queue文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。而对于CommitLog消息存储的日志数据文件来说,读取消息内容时候会产生较多的随机访问读取,严重影响性能。如果选择合适的系统IO调度算法,比如设置调度算法为“Deadline”(此时块存储采用SSD的话),随机读的性能也会有所提升。 + +另外,RocketMQ主要通过MappedByteBuffer对文件进行读写操作。其中,利用了NIO中的FileChannel模型将磁盘上的物理文件直接映射到用户态的内存地址中(这种Mmap的方式减少了传统IO将磁盘文件数据在操作系统内核地址空间的缓冲区和用户应用程序地址空间的缓冲区之间来回进行拷贝的性能开销),将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率(正因为需要使用内存映射机制,故RocketMQ的文件存储都使用定长结构来存储,方便一次将整个文件映射至内存)。 +#### 1.3 消息刷盘 + +![](image/rocketmq_design_2.png) + +(1) 同步刷盘:如上图所示,只有在消息真正持久化至磁盘后RocketMQ的Broker端才会真正返回给Producer端一个成功的ACK响应。同步刷盘对MQ消息可靠性来说是一种不错的保障,但是性能上会有较大影响,一般适用于金融业务应用该模式较多。 + +(2) 异步刷盘:能够充分利用OS的PageCache的优势,只要消息写入PageCache即可将成功的ACK返回给Producer端。消息刷盘采用后台异步线程提交的方式进行,降低了读写延迟,提高了MQ的性能和吞吐量。 + +### 2 通信机制 + +RocketMQ消息队列集群主要包括NameServer、Broker(Master/Slave)、Producer、Consumer4个角色,基本通讯流程如下: + +(1) Broker启动后需要完成一次将自己注册至NameServer的操作;随后每隔30s时间定时向NameServer上报Topic路由信息。 + +(2) 消息生产者Producer作为客户端发送消息时候,需要根据消息的Topic从本地缓存的TopicPublishInfoTable获取路由信息。如果没有则更新路由信息会从NameServer上重新拉取,同时Producer会默认每隔30s向NameServer拉取一次路由信息。 + +(3) 消息生产者Producer根据2)中获取的路由信息选择一个队列(MessageQueue)进行消息发送;Broker作为消息的接收者接收消息并落盘存储。 + +(4) 消息消费者Consumer根据2)中获取的路由信息,并再完成客户端的负载均衡后,选择其中的某一个或者某几个消息队列来拉取消息并进行消费。 + +从上面1)~3)中可以看出在消息生产者,Broker和NameServer之间都会发生通信(这里只说了MQ的部分通信),因此如何设计一个良好的网络通信模块在MQ中至关重要,它将决定RocketMQ集群整体的消息传输能力与最终的性能。 + +rocketmq-remoting 模块是 RocketMQ消息队列中负责网络通信的模块,它几乎被其他所有需要网络通信的模块(诸如rocketmq-client、rocketmq-broker、rocketmq-namesrv)所依赖和引用。为了实现客户端与服务器之间高效的数据请求与接收,RocketMQ消息队列自定义了通信协议并在Netty的基础之上扩展了通信模块。 +#### 2.1 Remoting通信类结构 + +![](image/rocketmq_design_3.png) + +#### 2.2 协议设计与编解码 +在Client和Server之间完成一次消息发送时,需要对发送的消息进行一个协议约定,因此就有必要自定义RocketMQ的消息协议。同时,为了高效地在网络中传输消息和对收到的消息读取,就需要对消息进行编解码。在RocketMQ中,RemotingCommand这个类在消息传输过程中对所有数据内容的封装,不但包含了所有的数据结构,还包含了编码解码操作。 + +Header字段 | 类型 | Request说明 | Response说明 +--- | --- | --- | --- | +code |int | 请求操作码,应答方根据不同的请求码进行不同的业务处理 | 应答响应码。0表示成功,非0则表示各种错误 +language | LanguageCode | 请求方实现的语言 | 应答方实现的语言 +version | int | 请求方程序的版本 | 应答方程序的版本 +opaque | int |相当于requestId,在同一个连接上的不同请求标识码,与响应消息中的相对应 | 应答不做修改直接返回 +flag | int | 区分是普通RPC还是onewayRPC的标志 | 区分是普通RPC还是onewayRPC的标志 +remark | String | 传输自定义文本信息 | 传输自定义文本信息 +extFields | HashMap | 请求自定义扩展信息 | 响应自定义扩展信息 + +![](image/rocketmq_design_4.png) + +可见传输内容主要可以分为以下4部分: + +(1) 消息长度:总长度,四个字节存储,占用一个int类型; + +(2) 序列化类型&消息头长度:同样占用一个int类型,第一个字节表示序列化类型,后面三个字节表示消息头长度; + +(3) 消息头数据:经过序列化后的消息头数据; + +(4) 消息主体数据:消息主体的二进制字节数据内容; + +#### 2.3 消息的通信方式和流程 +在RocketMQ消息队列中支持通信的方式主要有同步(sync)、异步(async)、单向(oneway) +三种。其中“单向”通信模式相对简单,一般用在发送心跳包场景下,无需关注其Response。这里,主要介绍RocketMQ的异步通信流程。 + +![](image/rocketmq_design_5.png) + +#### 2.4 Reactor多线程设计 +RocketMQ的RPC通信采用Netty组件作为底层通信库,同样也遵循了Reactor多线程模型,同时又在这之上做了一些扩展和优化。 + +![](image/rocketmq_design_6.png) + +从上面的框图中可以大致了解RocketMQ中NettyRemotingServer的Reactor 多线程模型。一个 Reactor 主线程(eventLoopGroupBoss,即为上面的1)负责监听 TCP网络连接请求,建立好连接,创建SocketChannel,并注册到selector上。RocketMQ的源码中会自动根据OS的类型选择NIO和Epoll,也可以通过参数配置),然后监听真正的网络数据。拿到网络数据后,再丢给Worker线程池(eventLoopGroupSelector,即为上面的“N”,源码中默认设置为3),在真正执行业务逻辑之前需要进行SSL验证、编解码、空闲检查、网络连接管理,这些工作交给defaultEventExecutorGroup(即为上面的“M1”,源码中默认设置为8)去做。而处理业务操作放在业务线程池中执行,根据 RomotingCommand 的业务请求码code去processorTable这个本地缓存变量中找到对应的 processor,然后封装成task任务后,提交给对应的业务processor处理线程池来执行(sendMessageExecutor,以发送消息为例,即为上面的 “M2”)。从入口到业务逻辑的几个步骤中线程池一直再增加,这跟每一步逻辑复杂性相关,越复杂,需要的并发通道越宽。 + +线程数 | 线程名 | 线程具体说明 + --- | --- | --- +1 | NettyBoss_%d | Reactor 主线程 +N | NettyServerEPOLLSelector_%d_%d | Reactor 线程池 +M1 | NettyServerCodecThread_%d | Worker线程池 +M2 | RemotingExecutorThread_%d | 业务processor处理线程池 + +### 3 消息过滤 +RocketMQ分布式消息队列的消息过滤方式有别于其它MQ中间件,是在Consumer端订阅消息时再做消息过滤的。RocketMQ这么做是在于其Producer端写入消息和Consumer端订阅消息采用分离存储的机制来实现的,Consumer端订阅消息是需要通过ConsumeQueue这个消息消费的逻辑队列拿到一个索引,然后再从CommitLog里面读取真正的消息实体内容,所以说到底也是还绕不开其存储结构。其ConsumeQueue的存储结构如下,可以看到其中有8个字节存储的Message Tag的哈希值,基于Tag的消息过滤正是基于这个字段值的。 + +![](image/rocketmq_design_7.png) + +主要支持如下2种的过滤方式 +(1) Tag过滤方式:Consumer端在订阅消息时除了指定Topic还可以指定TAG,如果一个消息有多个TAG,可以用||分隔。其中,Consumer端会将这个订阅请求构建成一个 SubscriptionData,发送一个Pull消息的请求给Broker端。Broker端从RocketMQ的文件存储层—Store读取数据之前,会用这些数据先构建一个MessageFilter,然后传给Store。Store从 ConsumeQueue读取到一条记录后,会用它记录的消息tag hash值去做过滤,由于在服务端只是根据hashcode进行判断,无法精确对tag原始字符串进行过滤,故在消息消费端拉取到消息后,还需要对消息的原始tag字符串进行比对,如果不同,则丢弃该消息,不进行消息消费。 + +(2) SQL92的过滤方式:这种方式的大致做法和上面的Tag过滤方式一样,只是在Store层的具体过滤过程不太一样,真正的 SQL expression 的构建和执行由rocketmq-filter模块负责的。每次过滤都去执行SQL表达式会影响效率,所以RocketMQ使用了BloomFilter避免了每次都去执行。SQL92的表达式上下文为消息的属性。 + +### 4 负载均衡 +RocketMQ中的负载均衡都在Client端完成,具体来说的话,主要可以分为Producer端发送消息时候的负载均衡和Consumer端订阅消息的负载均衡。 +#### 4.1 Producer的负载均衡 +Producer端在发送消息的时候,会先根据Topic找到指定的TopicPublishInfo,在获取了TopicPublishInfo路由信息后,RocketMQ的客户端在默认方式下selectOneMessageQueue()方法会从TopicPublishInfo中的messageQueueList中选择一个队列(MessageQueue)进行发送消息。具体的容错策略均在MQFaultStrategy这个类中定义。这里有一个sendLatencyFaultEnable开关变量,如果开启,在随机递增取模的基础上,再过滤掉not available的Broker代理。所谓的"latencyFaultTolerance",是指对之前失败的,按一定的时间做退避。例如,如果上次请求的latency超过550L ms,就退避30000L ms;超过1000L,就退避60000L;如果关闭,采用随机递增取模的方式选择一个队列(MessageQueue)来发送消息,latencyFaultTolerance机制是实现消息发送高可用的核心关键所在。 +#### 4.2 Consumer的负载均衡 +在RocketMQ中,Consumer端的两种消费模式(Push/Pull)都是基于拉模式来获取消息的,而在Push模式只是对pull模式的一种封装,其本质实现为消息拉取线程在从服务器拉取到一批消息后,然后提交到消息消费线程池后,又“马不停蹄”的继续向服务器再次尝试拉取消息。如果未拉取到消息,则延迟一下又继续拉取。在两种基于拉模式的消费方式(Push/Pull)中,均需要Consumer端知道从Broker端的哪一个消息队列中去获取消息。因此,有必要在Consumer端来做负载均衡,即Broker端中多个MessageQueue分配给同一个ConsumerGroup中的哪些Consumer消费。 + +1、Consumer端的心跳包发送 + +在Consumer启动后,它就会通过定时任务不断地向RocketMQ集群中的所有Broker实例发送心跳包(其中包含了,消息消费分组名称、订阅关系集合、消息通信模式和客户端id的值等信息)。Broker端在收到Consumer的心跳消息后,会将它维护在ConsumerManager的本地缓存变量—consumerTable,同时并将封装后的客户端网络通道信息保存在本地缓存变量—channelInfoTable中,为之后做Consumer端的负载均衡提供可以依据的元数据信息。 + +2、Consumer端实现负载均衡的核心类—RebalanceImpl + +在Consumer实例的启动流程中的启动MQClientInstance实例部分,会完成负载均衡服务线程—RebalanceService的启动(每隔20s执行一次)。通过查看源码可以发现,RebalanceService线程的run()方法最终调用的是RebalanceImpl类的rebalanceByTopic()方法,该方法是实现Consumer端负载均衡的核心。这里,rebalanceByTopic()方法会根据消费者通信类型为“广播模式”还是“集群模式”做不同的逻辑处理。这里主要来看下集群模式下的主要处理流程: + +(1) 从rebalanceImpl实例的本地缓存变量—topicSubscribeInfoTable中,获取该Topic主题下的消息消费队列集合(mqSet); + +(2) 根据topic和consumerGroup为参数调用mQClientFactory.findConsumerIdList()方法向Broker端发送获取该消费组下消费者Id列表的RPC通信请求(Broker端基于前面Consumer端上报的心跳包数据而构建的consumerTable做出响应返回,业务请求码:GET_CONSUMER_LIST_BY_GROUP); + +(3) 先对Topic下的消息消费队列、消费者Id排序,然后用消息队列分配策略算法(默认为:消息队列的平均分配算法),计算出待拉取的消息队列。这里的平均分配算法,类似于分页的算法,将所有MessageQueue排好序类似于记录,将所有消费端Consumer排好序类似页数,并求出每一页需要包含的平均size和每个页面记录的范围range,最后遍历整个range而计算出当前Consumer端应该分配到的记录(这里即为:MessageQueue)。 + +![](image/rocketmq_design_8.png) + +(4) 然后,调用updateProcessQueueTableInRebalance()方法,具体的做法是,先将分配到的消息队列集合(mqSet)与processQueueTable做一个过滤比对。 + +![](image/rocketmq_design_9.png) + +- 上图中processQueueTable标注的红色部分,表示与分配到的消息队列集合mqSet互不包含。将这些队列设置Dropped属性为true,然后查看这些队列是否可以移除出processQueueTable缓存变量,这里具体执行removeUnnecessaryMessageQueue()方法,即每隔1s 查看是否可以获取当前消费处理队列的锁,拿到的话返回true。如果等待1s后,仍然拿不到当前消费处理队列的锁则返回false。如果返回true,则从processQueueTable缓存变量中移除对应的Entry; + +- 上图中processQueueTable的绿色部分,表示与分配到的消息队列集合mqSet的交集。判断该ProcessQueue是否已经过期了,在Pull模式的不用管,如果是Push模式的,设置Dropped属性为true,并且调用removeUnnecessaryMessageQueue()方法,像上面一样尝试移除Entry; + +最后,为过滤后的消息队列集合(mqSet)中的每个MessageQueue创建一个ProcessQueue对象并存入RebalanceImpl的processQueueTable队列中(其中调用RebalanceImpl实例的computePullFromWhere(MessageQueue mq)方法获取该MessageQueue对象的下一个进度消费值offset,随后填充至接下来要创建的pullRequest对象属性中),并创建拉取请求对象—pullRequest添加到拉取列表—pullRequestList中,最后执行dispatchPullRequest()方法,将Pull消息的请求对象PullRequest依次放入PullMessageService服务线程的阻塞队列pullRequestQueue中,待该服务线程取出后向Broker端发起Pull消息的请求。 + +消息消费队列在同一消费组不同消费者之间的负载均衡,其核心设计理念是在一个消息消费队列在同一时间只允许被同一消费组内的一个消费者消费,一个消息消费者能同时消费多个消息队列。 + +### 5 事务消息 +Apache RocketMQ在4.3.0版中已经支持分布式事务消息,这里RocketMQ采用了2PC的思想来实现了提交事务消息,同时增加一个补偿逻辑来处理二阶段超时或者失败的消息,如下图所示。 + +![](image/rocketmq_design_10.png) + +#### 5.1 RocketMQ事务消息流程概要 +上图说明了事务消息的大致方案,其中分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。 + +1.事务消息发送及提交: + +(1) 发送消息(half消息)。 + +(2) 服务端响应消息写入结果。 + +(3) 根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)。 + +(4) 根据本地事务状态执行Commit或者Rollback(Commit操作生成消息索引,消息对消费者可见) + +2.补偿流程: + +(1) 对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查” + +(2) Producer收到回查消息,检查回查消息对应的本地事务的状态 + +(3) 根据本地事务状态,重新Commit或者Rollback + +其中,补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况。 +#### 5.2 RocketMQ事务消息设计 +1.事务消息在一阶段对用户不可见 + +在RocketMQ事务消息的主要流程中,一阶段的消息如何对用户不可见。其中,事务消息相对普通消息最大的特点就是一阶段发送的消息对用户是不可见的。那么,如何做到写入消息但是对用户不可见呢?RocketMQ事务消息的做法是:如果消息是half消息,将备份原消息的主题与消息消费队列,然后改变主题为RMQ_SYS_TRANS_HALF_TOPIC。由于消费组未订阅该主题,故消费端无法消费half类型的消息,然后RocketMQ会开启一个定时任务,从Topic为RMQ_SYS_TRANS_HALF_TOPIC中拉取消息进行消费,根据生产者组获取一个服务提供者发送回查事务状态请求,根据事务状态来决定是提交或回滚消息。 + +在RocketMQ中,消息在服务端的存储结构如下,每条消息都会有对应的索引信息,Consumer通过ConsumeQueue这个二级索引来读取消息实体内容,其流程如下: + +![](image/rocketmq_design_11.png) + +RocketMQ的具体实现策略是:写入的如果事务消息,对消息的Topic和Queue等属性进行替换,同时将原来的Topic和Queue信息存储到消息的属性中,正因为消息主题被替换,故消息并不会转发到该原主题的消息消费队列,消费者无法感知消息的存在,不会消费。其实改变消息主题是RocketMQ的常用“套路”,回想一下延时消息的实现机制。 + +2.Commit和Rollback操作以及Op消息的引入 + +在完成一阶段写入一条对用户不可见的消息后,二阶段如果是Commit操作,则需要让消息对用户可见;如果是Rollback则需要撤销一阶段的消息。先说Rollback的情况。对于Rollback,本身一阶段的消息对用户是不可见的,其实不需要真正撤销消息(实际上RocketMQ也无法去真正的删除一条消息,因为是顺序写文件的)。但是区别于这条消息没有确定状态(Pending状态,事务悬而未决),需要一个操作来标识这条消息的最终状态。RocketMQ事务消息方案中引入了Op消息的概念,用Op消息标识事务消息已经确定的状态(Commit或者Rollback)。如果一条事务消息没有对应的Op消息,说明这个事务的状态还无法确定(可能是二阶段失败了)。引入Op消息后,事务消息无论是Commit或者Rollback都会记录一个Op操作。Commit相对于Rollback只是在写入Op消息前创建Half消息的索引。 + +3.Op消息的存储和对应关系 + +RocketMQ将Op消息写入到全局一个特定的Topic中通过源码中的方法—TransactionalMessageUtil.buildOpTopic();这个Topic是一个内部的Topic(像Half消息的Topic一样),不会被用户消费。Op消息的内容为对应的Half消息的存储的Offset,这样通过Op消息能索引到Half消息进行后续的回查操作。 + +![](image/rocketmq_design_12.png) + +4.Half消息的索引构建 + +在执行二阶段Commit操作时,需要构建出Half消息的索引。一阶段的Half消息由于是写到一个特殊的Topic,所以二阶段构建索引时需要读取出Half消息,并将Topic和Queue替换成真正的目标的Topic和Queue,之后通过一次普通消息的写入操作来生成一条对用户可见的消息。所以RocketMQ事务消息二阶段其实是利用了一阶段存储的消息的内容,在二阶段时恢复出一条完整的普通消息,然后走一遍消息写入流程。 + +5.如何处理二阶段失败的消息? + +如果在RocketMQ事务消息的二阶段过程中失败了,例如在做Commit操作时,出现网络问题导致Commit失败,那么需要通过一定的策略使这条消息最终被Commit。RocketMQ采用了一种补偿机制,称为“回查”。Broker端对未确定状态的消息发起回查,将消息发送到对应的Producer端(同一个Group的Producer),由Producer根据消息来检查本地事务的状态,进而执行Commit或者Rollback。Broker端通过对比Half消息和Op消息进行事务消息的回查并且推进CheckPoint(记录那些事务消息的状态是确定的)。 + +值得注意的是,rocketmq并不会无休止的的信息事务状态回查,默认回查15次,如果15次回查还是无法得知事务状态,rocketmq默认回滚该消息。 +### 6 消息查询 +RocketMQ支持按照下面两种维度(“按照Message Id查询消息”、“按照Message Key查询消息”)进行消息查询。 +#### 6.1 按照MessageId查询消息 +RocketMQ中的MessageId的长度总共有16字节,其中包含了消息存储主机地址(IP地址和端口),消息Commit Log offset。“按照MessageId查询消息”在RocketMQ中具体做法是:Client端从MessageId中解析出Broker的地址(IP地址和端口)和Commit Log的偏移地址后封装成一个RPC请求后通过Remoting通信层发送(业务请求码:VIEW_MESSAGE_BY_ID)。Broker端走的是QueryMessageProcessor,读取消息的过程用其中的 commitLog offset 和 size 去 commitLog 中找到真正的记录并解析成一个完整的消息返回。 +#### 6.2 按照Message Key查询消息 +“按照Message Key查询消息”,主要是基于RocketMQ的IndexFile索引文件来实现的。RocketMQ的索引文件逻辑结构,类似JDK中HashMap的实现。索引文件的具体结构如下: + +![](image/rocketmq_design_13.png) + +IndexFile索引文件为用户提供通过“按照Message Key查询消息”的消息索引查询服务,IndexFile文件的存储位置是:$HOME\store\index\${fileName},文件名fileName是以创建时的时间戳命名的,文件大小是固定的,等于40+500W\*4+2000W\*20= 420000040个字节大小。如果消息的properties中设置了UNIQ_KEY这个属性,就用 topic + “#” + UNIQ_KEY的value作为 key 来做写入操作。如果消息设置了KEYS属性(多个KEY以空格分隔),也会用 topic + “#” + KEY 来做索引。 + +其中的索引数据包含了Key Hash/CommitLog Offset/Timestamp/NextIndex offset 这四个字段,一共20 Byte。NextIndex offset 即前面读出来的 slotValue,如果有 hash冲突,就可以用这个字段将所有冲突的索引用链表的方式串起来了。Timestamp记录的是消息storeTimestamp之间的差,并不是一个绝对的时间。整个Index File的结构如图,40 Byte 的Header用于保存一些总的统计信息,4\*500W的 Slot Table并不保存真正的索引数据,而是保存每个槽位对应的单向链表的头。20\*2000W 是真正的索引数据,即一个 Index File 可以保存 2000W个索引。 + +“按照Message Key查询消息”的方式,RocketMQ的具体做法是,主要通过Broker端的QueryMessageProcessor业务处理器来查询,读取消息的过程就是用topic和key找到IndexFile索引文件中的一条记录,根据其中的commitLog offset从CommitLog文件中读取消息的实体内容。 diff --git a/docs/cn/dledger/deploy_guide.md b/docs/cn/dledger/deploy_guide.md new file mode 100644 index 00000000000..2d04590e0b6 --- /dev/null +++ b/docs/cn/dledger/deploy_guide.md @@ -0,0 +1,79 @@ +# Dledger集群搭建 +> 该模式为4.x的切换方式,建议采用 5.x [自动主从切换](../controller/design.md) +--- +## 前言 +该文档主要介绍如何部署自动容灾切换的 RocketMQ-on-DLedger Group。 + +RocketMQ-on-DLedger Group 是指一组**相同名称**的 Broker,至少需要 3 个节点,通过 Raft 自动选举出一个 Leader,其余节点 作为 Follower,并在 Leader 和 Follower 之间复制数据以保证高可用。 +RocketMQ-on-DLedger Group 能自动容灾切换,并保证数据一致。 +RocketMQ-on-DLedger Group 是可以水平扩展的,也即可以部署任意多个 RocketMQ-on-DLedger Group 同时对外提供服务。 + +## 1. 新集群部署 + +#### 1.1 编写配置 +每个 RocketMQ-on-DLedger Group 至少准备三台机器(本文假设为 3)。 +编写 3 个配置文件,建议参考 conf/dledger 目录下的配置文件样例。 +关键配置介绍: + +| name | 含义 | 举例 | +| --- | --- | --- | +| enableDLegerCommitLog | 是否启动 DLedger  | true | +| dLegerGroup | DLedger Raft Group的名字,建议和 brokerName 保持一致 | RaftNode00 | +| dLegerPeers | DLedger Group 内各节点的端口信息,同一个 Group 内的各个节点配置必须要保证一致 | n0-127.0.0.1:40911;n1-127.0.0.1:40912;n2-127.0.0.1:40913 | +| dLegerSelfId | 节点 id,必须属于 dLegerPeers 中的一个;同 Group 内各个节点要唯一 | n0 | +| sendMessageThreadPoolNums | 发送线程个数,建议配置成 Cpu 核数 | 16 | + +这里贴出 conf/dledger/broker-n0.conf 的配置举例。 + +``` +brokerClusterName = RaftCluster +brokerName=RaftNode00 +listenPort=30911 +namesrvAddr=127.0.0.1:9876 +storePathRootDir=/tmp/rmqstore/node00 +storePathCommitLog=/tmp/rmqstore/node00/commitlog +enableDLegerCommitLog=true +dLegerGroup=RaftNode00 +dLegerPeers=n0-127.0.0.1:40911;n1-127.0.0.1:40912;n2-127.0.0.1:40913 +## must be unique +dLegerSelfId=n0 +sendMessageThreadPoolNums=16 +``` + +### 1.2 启动 Broker + +与老版本的启动方式一致。 + +`nohup sh bin/mqbroker -c conf/dledger/xxx-n0.conf & ` +`nohup sh bin/mqbroker -c conf/dledger/xxx-n1.conf & ` +`nohup sh bin/mqbroker -c conf/dledger/xxx-n2.conf & ` + + +## 2. 旧集群升级 + +如果旧集群采用 Master 方式部署,则每个 Master 都需要转换成一个 RocketMQ-on-DLedger Group。 +如果旧集群采用 Master-Slave 方式部署,则每个 Master-Slave 组都需要转换成一个 RocketMQ-on-DLedger Group。 + +### 2.1 杀掉旧的 Broker + +可以通过 kill 命令来完成,也可以调用 `bin/mqshutdown broker`。 + +### 2.2 检查旧的 Commitlog + +RocketMQ-on-DLedger 组中的每个节点,可以兼容旧的 Commitlog ,但其 Raft 复制过程,只能针对新增加的消息。因此,为了避免出现异常,需要保证 旧的 Commitlog 是一致的。 +如果旧的集群是采用 Master-Slave 方式部署,有可能在shutdown时,其数据并不是一致的,建议通过md5sum 的方式,检查最近的最少 2 个 Commmitlog 文件,如果发现不一致,则通过拷贝的方式进行对齐。 + +虽然 RocketMQ-on-DLedger Group 也可以以 2 节点方式部署,但其会丧失容灾切换能力(2n + 1 原则,至少需要3个节点才能容忍其中 1 个宕机)。 +所以在对齐了 Master 和 Slave 的 Commitlog 之后,还需要准备第 3 台机器,并把旧的 Commitlog 从 Master 拷贝到 第 3 台机器(记得同时拷贝一下 config 文件夹)。 + +在 3 台机器准备好了之后,旧 Commitlog 文件也保证一致之后,就可以开始走下一步修改配置了。 + +### 2.3 修改配置 + +参考新集群部署。 + +### 2.4 重新启动 Broker + +参考新集群部署。 + + diff --git a/docs/cn/dledger/quick_start.md b/docs/cn/dledger/quick_start.md new file mode 100644 index 00000000000..8311395d993 --- /dev/null +++ b/docs/cn/dledger/quick_start.md @@ -0,0 +1,69 @@ +# Dledger快速搭建 +> 该模式为4.x的切换方式,建议采用 5.x [自动主从切换](../controller/quick_start.md) +--- +### 前言 +该文档主要介绍如何快速构建和部署基于 DLedger 的可以自动容灾切换的 RocketMQ 集群。 + +详细的新集群部署和旧集群升级指南请参考 [部署指南](deploy_guide.md)。 + +### 1. 源码构建 +构建分为两个部分,需要先构建 DLedger,然后 构建 RocketMQ + +#### 1.1 构建 DLedger + +```shell +$ git clone https://github.com/openmessaging/dledger.git +$ cd dledger +$ mvn clean install -DskipTests +``` + +#### 1.2 构建 RocketMQ + +```shell +$ git clone https://github.com/apache/rocketmq.git +$ cd rocketmq +$ git checkout -b store_with_dledger origin/store_with_dledger +$ mvn -Prelease-all -DskipTests clean install -U +``` + +### 2. 快速部署 + +在构建成功后 + +```shell +#{rocketmq-version} replace with rocketmq actual version. example: 5.0.0-SNAPSHOT +$ cd distribution/target/rocketmq-{rocketmq-version}/rocketmq-{rocketmq-version} +$ sh bin/dledger/fast-try.sh start +``` + +如果上面的步骤执行成功,可以通过 mqadmin 运维命令查看集群状态。 + +```shell +$ sh bin/mqadmin clusterList -n 127.0.0.1:9876 +``` + +顺利的话,会看到如下内容: + +![ClusterList](https://img.alicdn.com/5476e8b07b923/TB11Z.ZyCzqK1RjSZFLXXcn2XXa) + +(BID 为 0 的表示 Master,其余都是 Follower) + +启动成功,现在可以向集群收发消息,并进行容灾切换测试了。 + +关闭快速集群,可以执行: + +```shell +$ sh bin/dledger/fast-try.sh stop +``` + +快速部署,默认配置在 conf/dledger 里面,默认的存储路径在 /tmp/rmqstore。 + + +### 3. 容灾切换 + +部署成功,杀掉 Leader 之后(在上面的例子中,杀掉端口 30931 所在的进程),等待约 10s 左右,用 clusterList 命令查看集群,就会发现 Leader 切换到另一个节点了。 + + + + + diff --git a/docs/cn/features.md b/docs/cn/features.md new file mode 100644 index 00000000000..ab67683a27d --- /dev/null +++ b/docs/cn/features.md @@ -0,0 +1,84 @@ +# 特性(features) +---- +## 1 订阅与发布 +消息的发布是指某个生产者向某个topic发送消息;消息的订阅是指某个消费者关注了某个topic中带有某些tag的消息,进而从该topic消费数据。 +## 2 消息顺序 +消息有序指的是一类消息消费时,能按照发送的顺序来消费。例如:一个订单产生了三条消息分别是订单创建、订单付款、订单完成。消费时要按照这个顺序消费才能有意义,但是同时订单之间是可以并行消费的。RocketMQ可以严格的保证消息有序。 + +顺序消息分为全局顺序消息与分区顺序消息,全局顺序是指某个Topic下的所有消息都要保证顺序;部分顺序消息只要保证每一组消息被顺序消费即可。 +- 全局顺序 +对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。 +适用场景:性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景 +- 分区顺序 +对于指定的一个 Topic,所有消息根据 sharding key 进行区块分区。 同一个分区内的消息按照严格的 FIFO 顺序进行发布和消费。 Sharding key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念。 +适用场景:性能要求高,以 sharding key 作为分区字段,在同一个区块中严格的按照 FIFO 原则进行消息发布和消费的场景。 +## 3 消息过滤 +RocketMQ的消费者可以根据Tag进行消息过滤,也支持自定义属性过滤。消息过滤目前是在Broker端实现的,优点是减少了对于Consumer无用消息的网络传输,缺点是增加了Broker的负担、而且实现相对复杂。 +## 4 消息可靠性 +RocketMQ支持消息的高可靠,影响消息可靠性的几种情况: +1) Broker非正常关闭 +2) Broker异常Crash +3) OS Crash +4) 机器掉电,但是能立即恢复供电情况 +5) 机器无法开机(可能是cpu、主板、内存等关键设备损坏) +6) 磁盘设备损坏 + +1)、2)、3)、4) 四种情况都属于硬件资源可立即恢复情况,RocketMQ在这四种情况下能保证消息不丢,或者丢失少量数据(依赖刷盘方式是同步还是异步)。 + +5)、6)属于单点故障,且无法恢复,一旦发生,在此单点上的消息全部丢失。RocketMQ在这两种情况下,通过异步复制,可保证99%的消息不丢,但是仍然会有极少量的消息可能丢失。通过同步双写技术可以完全避免单点,同步双写势必会影响性能,适合对消息可靠性要求极高的场合,例如与Money相关的应用。注:RocketMQ从3.0版本开始支持同步双写。 + +## 5 至少一次 +至少一次(At least Once)指每个消息必须投递一次。Consumer先Pull消息到本地,消费完成后,才向服务器返回ack,如果没有消费一定不会ack消息,所以RocketMQ可以很好的支持此特性。 + +## 6 回溯消费 +回溯消费是指Consumer已经消费成功的消息,由于业务上需求需要重新消费,要支持此功能,Broker在向Consumer投递成功消息后,消息仍然需要保留。并且重新消费一般是按照时间维度,例如由于Consumer系统故障,恢复后需要重新消费1小时前的数据,那么Broker要提供一种机制,可以按照时间维度来回退消费进度。RocketMQ支持按照时间回溯消费,时间维度精确到毫秒。 + +## 7 事务消息 +RocketMQ事务消息(Transactional Message)是指应用本地事务和发送消息操作可以被定义到全局事务中,要么同时成功,要么同时失败。RocketMQ的事务消息提供类似 X/Open XA 的分布事务功能,通过事务消息能达到分布式事务的最终一致。 +## 8 定时消息 +定时消息(延迟队列)是指消息发送到broker后,不会立即被消费,等待特定时间投递给真正的topic。 +broker有配置项messageDelayLevel,默认值为“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”,18个level。可以配置自定义messageDelayLevel。注意,messageDelayLevel是broker的属性,不属于某个topic。发消息时,设置delayLevel等级即可:msg.setDelayLevel(level)。level有以下三种情况: + +- level == 0,消息为非延迟消息 +- 1<=level<=maxLevel,消息延迟特定时间,例如level==1,延迟1s +- level > maxLevel,则level== maxLevel,例如level==20,延迟2h + +定时消息会暂存在名为SCHEDULE_TOPIC_XXXX的topic中,并根据delayTimeLevel存入特定的queue,queueId = delayTimeLevel – 1,即一个queue只存相同延迟的消息,保证具有相同发送延迟的消息能够顺序消费。broker会调度地消费SCHEDULE_TOPIC_XXXX,将消息写入真实的topic。 + +需要注意的是,定时消息会在第一次写入和调度写入真实topic时都会计数,因此发送数量、tps都会变高。 + +## 9 消息重试 +Consumer消费消息失败后,要提供一种重试机制,令消息再消费一次。Consumer消费消息失败通常可以认为有以下几种情况: +- 由于消息本身的原因,例如反序列化失败,消息数据本身无法处理(例如话费充值,当前消息的手机号被注销,无法充值)等。这种错误通常需要跳过这条消息,再消费其它消息,而这条失败的消息即使立刻重试消费,99%也不成功,所以最好提供一种定时重试机制,即过10秒后再重试。 +- 由于依赖的下游应用服务不可用,例如db连接不可用,外系统网络不可达等。遇到这种错误,即使跳过当前失败的消息,消费其他消息同样也会报错。这种情况建议应用sleep 30s,再消费下一条消息,这样可以减轻Broker重试消息的压力。 + +RocketMQ会为每个消费组都设置一个Topic名称为“%RETRY%+consumerGroup”的重试队列(这里需要注意的是,这个Topic的重试队列是针对消费组,而不是针对每个Topic设置的),用于暂时保存因为各种异常而导致Consumer端无法消费的消息。考虑到异常恢复起来需要一些时间,会为重试队列设置多个重试级别,每个重试级别都有与之对应的重新投递延时,重试次数越多投递延时就越大。RocketMQ对于重试消息的处理是先保存至Topic名称为“SCHEDULE_TOPIC_XXXX”的延迟队列中,后台定时任务按照对应的时间进行Delay后重新保存至“%RETRY%+consumerGroup”的重试队列中。 +## 10 消息重投 +生产者在发送消息时,同步消息失败会重投,异步消息有重试,oneway没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但可能会造成消息重复,消息重复在RocketMQ中是无法避免的问题。消息重复在一般情况下不会发生,当出现消息量大、网络抖动,消息重复就会是大概率事件。另外,生产者主动重发、consumer负载变化也会导致重复消息。如下方法可以设置消息重试策略: + +- retryTimesWhenSendFailed:同步发送失败重投次数,默认为2,因此生产者会最多尝试发送retryTimesWhenSendFailed + 1次。不会选择上次失败的broker,尝试向其他broker发送,最大程度保证消息不丢。超过重投次数,抛出异常,由客户端保证消息不丢。当出现RemotingException、MQClientException和部分MQBrokerException时会重投。 +- retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他broker,仅在同一个broker上做重试,不保证消息不丢。 +- retryAnotherBrokerWhenNotStoreOK:消息刷盘(主或备)超时或slave不可用(返回状态非SEND_OK),是否尝试发送到其他broker,默认false。十分重要消息可以开启。 + +## 11 流量控制 +生产者流控,因为broker处理能力达到瓶颈;消费者流控,因为消费能力达到瓶颈。 + +生产者流控: +- commitLog文件被锁时间超过osPageCacheBusyTimeOutMills时,参数默认为1000ms,返回流控。 +- 如果开启transientStorePoolEnable == true,且broker为异步刷盘的主机,且transientStorePool中资源不足,拒绝当前send请求,返回流控。 +- broker每隔10ms检查send请求队列头部请求的等待时间,如果超过waitTimeMillsInSendQueue,默认200ms,拒绝当前send请求,返回流控。 +- broker通过拒绝send 请求方式实现流量控制。 + +注意,生产者流控,不会尝试消息重投。 + +消费者流控: +- 消费者本地缓存消息数超过pullThresholdForQueue时,默认1000。 +- 消费者本地缓存消息大小超过pullThresholdSizeForQueue时,默认100MB。 +- 消费者本地缓存消息跨度超过consumeConcurrentlyMaxSpan时,默认2000。 + +消费者流控的结果是降低拉取频率。 +## 12 死信队列 +死信队列用于处理无法被正常消费的消息。当一条消息初次消费失败,消息队列会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。 + +RocketMQ将这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),将存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。在RocketMQ中,可以通过使用console控制台对死信队列中的消息进行重发来使得消费者实例再次进行消费。 + diff --git a/docs/cn/image/Idea_config_broker.png b/docs/cn/image/Idea_config_broker.png new file mode 100644 index 00000000000..6fbedcfb627 Binary files /dev/null and b/docs/cn/image/Idea_config_broker.png differ diff --git a/docs/cn/image/Idea_config_nameserver.png b/docs/cn/image/Idea_config_nameserver.png new file mode 100644 index 00000000000..65edd991135 Binary files /dev/null and b/docs/cn/image/Idea_config_nameserver.png differ diff --git a/docs/cn/image/LMQ_1.png b/docs/cn/image/LMQ_1.png new file mode 100644 index 00000000000..3afd0885f16 Binary files /dev/null and b/docs/cn/image/LMQ_1.png differ diff --git a/docs/cn/image/consumer_reply.png b/docs/cn/image/consumer_reply.png new file mode 100644 index 00000000000..ae00da665d7 Binary files /dev/null and b/docs/cn/image/consumer_reply.png differ diff --git a/docs/cn/image/controller/controller_design_1.png b/docs/cn/image/controller/controller_design_1.png new file mode 100644 index 00000000000..fea825641f8 Binary files /dev/null and b/docs/cn/image/controller/controller_design_1.png differ diff --git a/docs/cn/image/controller/controller_design_2.png b/docs/cn/image/controller/controller_design_2.png new file mode 100644 index 00000000000..a82339472e9 Binary files /dev/null and b/docs/cn/image/controller/controller_design_2.png differ diff --git a/docs/cn/image/controller/controller_design_3.png b/docs/cn/image/controller/controller_design_3.png new file mode 100644 index 00000000000..8c475bcecf1 Binary files /dev/null and b/docs/cn/image/controller/controller_design_3.png differ diff --git a/docs/cn/image/controller/controller_design_4.png b/docs/cn/image/controller/controller_design_4.png new file mode 100644 index 00000000000..308b936279a Binary files /dev/null and b/docs/cn/image/controller/controller_design_4.png differ diff --git a/docs/cn/image/controller/controller_design_5.png b/docs/cn/image/controller/controller_design_5.png new file mode 100644 index 00000000000..01b33cab28d Binary files /dev/null and b/docs/cn/image/controller/controller_design_5.png differ diff --git a/docs/cn/image/controller/controller_design_6.png b/docs/cn/image/controller/controller_design_6.png new file mode 100644 index 00000000000..a909a70379a Binary files /dev/null and b/docs/cn/image/controller/controller_design_6.png differ diff --git a/docs/cn/image/controller/persistent_unique_broker_id/fail_apply_broker_id.png b/docs/cn/image/controller/persistent_unique_broker_id/fail_apply_broker_id.png new file mode 100644 index 00000000000..0689bd04b31 Binary files /dev/null and b/docs/cn/image/controller/persistent_unique_broker_id/fail_apply_broker_id.png differ diff --git a/docs/cn/image/controller/persistent_unique_broker_id/fail_create_metadata_file_and_delete_temp.png b/docs/cn/image/controller/persistent_unique_broker_id/fail_create_metadata_file_and_delete_temp.png new file mode 100644 index 00000000000..cee8ddfb2a5 Binary files /dev/null and b/docs/cn/image/controller/persistent_unique_broker_id/fail_create_metadata_file_and_delete_temp.png differ diff --git a/docs/cn/image/controller/persistent_unique_broker_id/fail_create_temp_metadata_file.png b/docs/cn/image/controller/persistent_unique_broker_id/fail_create_temp_metadata_file.png new file mode 100644 index 00000000000..32425d23604 Binary files /dev/null and b/docs/cn/image/controller/persistent_unique_broker_id/fail_create_temp_metadata_file.png differ diff --git a/docs/cn/image/controller/persistent_unique_broker_id/normal_restart.png b/docs/cn/image/controller/persistent_unique_broker_id/normal_restart.png new file mode 100644 index 00000000000..a454eada92a Binary files /dev/null and b/docs/cn/image/controller/persistent_unique_broker_id/normal_restart.png differ diff --git a/docs/cn/image/controller/persistent_unique_broker_id/register_process.png b/docs/cn/image/controller/persistent_unique_broker_id/register_process.png new file mode 100644 index 00000000000..200015765d9 Binary files /dev/null and b/docs/cn/image/controller/persistent_unique_broker_id/register_process.png differ diff --git a/docs/cn/image/controller/persistent_unique_broker_id/register_state_transfer.png b/docs/cn/image/controller/persistent_unique_broker_id/register_state_transfer.png new file mode 100644 index 00000000000..d6df0aa5d08 Binary files /dev/null and b/docs/cn/image/controller/persistent_unique_broker_id/register_state_transfer.png differ diff --git a/docs/cn/image/controller/quick-start/changemaster.png b/docs/cn/image/controller/quick-start/changemaster.png new file mode 100644 index 00000000000..6f486597008 Binary files /dev/null and b/docs/cn/image/controller/quick-start/changemaster.png differ diff --git a/docs/cn/image/controller/quick-start/controller.png b/docs/cn/image/controller/quick-start/controller.png new file mode 100644 index 00000000000..d7ffed6b80d Binary files /dev/null and b/docs/cn/image/controller/quick-start/controller.png differ diff --git a/docs/cn/image/controller/quick-start/epoch.png b/docs/cn/image/controller/quick-start/epoch.png new file mode 100644 index 00000000000..67dd768883c Binary files /dev/null and b/docs/cn/image/controller/quick-start/epoch.png differ diff --git a/docs/cn/image/controller/quick-start/syncstateset.png b/docs/cn/image/controller/quick-start/syncstateset.png new file mode 100644 index 00000000000..696a4c30826 Binary files /dev/null and b/docs/cn/image/controller/quick-start/syncstateset.png differ diff --git a/docs/cn/image/producer_send_request.png b/docs/cn/image/producer_send_request.png new file mode 100644 index 00000000000..016fedac6fb Binary files /dev/null and b/docs/cn/image/producer_send_request.png differ diff --git a/docs/cn/image/rocketmq_architecture_1.png b/docs/cn/image/rocketmq_architecture_1.png new file mode 100644 index 00000000000..addb571a844 Binary files /dev/null and b/docs/cn/image/rocketmq_architecture_1.png differ diff --git a/docs/cn/image/rocketmq_architecture_2.png b/docs/cn/image/rocketmq_architecture_2.png new file mode 100644 index 00000000000..b2ab8d34c6c Binary files /dev/null and b/docs/cn/image/rocketmq_architecture_2.png differ diff --git a/docs/cn/image/rocketmq_architecture_3.png b/docs/cn/image/rocketmq_architecture_3.png new file mode 100644 index 00000000000..b5d755adbdb Binary files /dev/null and b/docs/cn/image/rocketmq_architecture_3.png differ diff --git a/docs/cn/image/rocketmq_design_1.png b/docs/cn/image/rocketmq_design_1.png new file mode 100644 index 00000000000..8c719e115a1 Binary files /dev/null and b/docs/cn/image/rocketmq_design_1.png differ diff --git a/docs/cn/image/rocketmq_design_10.png b/docs/cn/image/rocketmq_design_10.png new file mode 100644 index 00000000000..df75da58775 Binary files /dev/null and b/docs/cn/image/rocketmq_design_10.png differ diff --git a/docs/cn/image/rocketmq_design_11.png b/docs/cn/image/rocketmq_design_11.png new file mode 100644 index 00000000000..e3dc741d49c Binary files /dev/null and b/docs/cn/image/rocketmq_design_11.png differ diff --git a/docs/cn/image/rocketmq_design_12.png b/docs/cn/image/rocketmq_design_12.png new file mode 100644 index 00000000000..bf95dbc4c80 Binary files /dev/null and b/docs/cn/image/rocketmq_design_12.png differ diff --git a/docs/cn/image/rocketmq_design_13.png b/docs/cn/image/rocketmq_design_13.png new file mode 100644 index 00000000000..32ba4551f53 Binary files /dev/null and b/docs/cn/image/rocketmq_design_13.png differ diff --git a/docs/cn/image/rocketmq_design_2.png b/docs/cn/image/rocketmq_design_2.png new file mode 100644 index 00000000000..1610ae0d934 Binary files /dev/null and b/docs/cn/image/rocketmq_design_2.png differ diff --git a/docs/cn/image/rocketmq_design_3.png b/docs/cn/image/rocketmq_design_3.png new file mode 100644 index 00000000000..a0796edb9cc Binary files /dev/null and b/docs/cn/image/rocketmq_design_3.png differ diff --git a/docs/cn/image/rocketmq_design_4.png b/docs/cn/image/rocketmq_design_4.png new file mode 100644 index 00000000000..bc8981855ec Binary files /dev/null and b/docs/cn/image/rocketmq_design_4.png differ diff --git a/docs/cn/image/rocketmq_design_5.png b/docs/cn/image/rocketmq_design_5.png new file mode 100644 index 00000000000..a52d31ebe30 Binary files /dev/null and b/docs/cn/image/rocketmq_design_5.png differ diff --git a/docs/cn/image/rocketmq_design_6.png b/docs/cn/image/rocketmq_design_6.png new file mode 100644 index 00000000000..8b675d8555b Binary files /dev/null and b/docs/cn/image/rocketmq_design_6.png differ diff --git a/docs/cn/image/rocketmq_design_7.png b/docs/cn/image/rocketmq_design_7.png new file mode 100644 index 00000000000..b0faa86c29c Binary files /dev/null and b/docs/cn/image/rocketmq_design_7.png differ diff --git a/docs/cn/image/rocketmq_design_8.png b/docs/cn/image/rocketmq_design_8.png new file mode 100644 index 00000000000..ab4a1fb68fd Binary files /dev/null and b/docs/cn/image/rocketmq_design_8.png differ diff --git a/docs/cn/image/rocketmq_design_9.png b/docs/cn/image/rocketmq_design_9.png new file mode 100644 index 00000000000..4af041641f5 Binary files /dev/null and b/docs/cn/image/rocketmq_design_9.png differ diff --git a/docs/cn/msg_trace/user_guide.md b/docs/cn/msg_trace/user_guide.md new file mode 100644 index 00000000000..9cf139fd347 --- /dev/null +++ b/docs/cn/msg_trace/user_guide.md @@ -0,0 +1,118 @@ +# 消息轨迹 +---- + +## 1. 消息轨迹数据关键属性 +| Producer端| Consumer端 | Broker端 | +| --- | --- | --- | +| 生产实例信息 | 消费实例信息 | 消息的Topic | +| 发送消息时间 | 投递时间,投递轮次  | 消息存储位置 | +| 消息是否发送成功 | 消息是否消费成功 | 消息的Key值 | +| 发送耗时 | 消费耗时 | 消息的Tag值 | + +## 2. 支持消息轨迹集群部署 + +### 2.1 Broker端配置文件 +这里贴出Broker端开启消息轨迹特性的properties配置文件内容: +``` +brokerClusterName=DefaultCluster +brokerName=broker-a +brokerId=0 +deleteWhen=04 +fileReservedTime=48 +brokerRole=ASYNC_MASTER +flushDiskType=ASYNC_FLUSH +storePathRootDir=/data/rocketmq/rootdir-a-m +storePathCommitLog=/data/rocketmq/commitlog-a-m +autoCreateSubscriptionGroup=true +## if msg tracing is open,the flag will be true +traceTopicEnable=true +listenPort=10911 +brokerIP1=XX.XX.XX.XX1 +namesrvAddr=XX.XX.XX.XX:9876 +``` + +### 2.2 普通模式 +RocketMQ集群中每一个Broker节点均用于存储Client端收集并发送过来的消息轨迹数据。因此,对于RocketMQ集群中的Broker节点数量并无要求和限制。 + +### 2.3 物理IO隔离模式 +对于消息轨迹数据量较大的场景,可以在RocketMQ集群中选择其中一个Broker节点专用于存储消息轨迹,使得用户普通的消息数据与消息轨迹数据的物理IO完全隔离,互不影响。在该模式下,RocketMQ集群中至少有两个Broker节点,其中一个Broker节点定义为存储消息轨迹数据的服务端。 + +### 2.4 启动开启消息轨迹的Broker +`nohup sh mqbroker -c ../conf/2m-noslave/broker-a.properties &` + +## 3. 保存消息轨迹的Topic定义 +RocketMQ的消息轨迹特性支持两种存储轨迹数据的方式: + +### 3.1 系统级的TraceTopic +在默认情况下,消息轨迹数据是存储于系统级的TraceTopic中(其名称为:**RMQ_SYS_TRACE_TOPIC**)。该Topic在Broker节点启动时,会自动创建出来(如上所叙,需要在Broker端的配置文件中将**traceTopicEnable**的开关变量设置为**true**)。 + +### 3.2 用户自定义的TraceTopic +如果用户不准备将消息轨迹的数据存储于系统级的默认TraceTopic,也可以自己定义并创建用户级的Topic来保存轨迹(即为创建普通的Topic用于保存消息轨迹数据)。下面一节会介绍Client客户端的接口如何支持用户自定义的TraceTopic。 + +## 4. 支持消息轨迹的Client客户端实践 +为了尽可能地减少用户业务系统使用RocketMQ消息轨迹特性的改造工作量,作者在设计时候采用对原来接口增加一个开关参数(**enableMsgTrace**)来实现消息轨迹是否开启;并新增一个自定义参数(**customizedTraceTopic**)来实现用户存储消息轨迹数据至自己创建的用户级Topic。 + +### 4.1 发送消息时开启消息轨迹 +```java + DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName",true); + producer.setNamesrvAddr("XX.XX.XX.XX1"); + producer.start(); + try { + { + Message msg = new Message("TopicTest", + "TagA", + "OrderID188", + "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); + SendResult sendResult = producer.send(msg); + System.out.printf("%s%n", sendResult); + } + + } catch (Exception e) { + e.printStackTrace(); + } +``` + +### 4.2 订阅消息时开启消息轨迹 +```java + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_JODIE_1",true); + consumer.subscribe("TopicTest", "*"); + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + consumer.setConsumeTimestamp("20181109221800"); + consumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + consumer.start(); + System.out.printf("Consumer Started.%n"); +``` + +### 4.3 支持自定义存储消息轨迹Topic +在上面的发送和订阅消息时候分别将DefaultMQProducer和DefaultMQPushConsumer实例的初始化修改为如下即可支持自定义存储消息轨迹Topic。 +``` + ##其中Topic_test11111需要用户自己预先创建,来保存消息轨迹; + DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName",true,"Topic_test11111"); + ...... + + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_JODIE_1",true,"Topic_test11111"); + ...... +``` + +### 4.4 使用mqadmin命令发送和查看轨迹 +- 发送消息 +```shell +./mqadmin sendMessage -m true --topic some-topic-name -n 127.0.0.1:9876 -p "your meesgae content" +``` +- 查询轨迹 +```shell +./mqadmin QueryMsgTraceById -n 127.0.0.1:9876 -i "some-message-id" +``` +- 查询轨迹结果 +``` +RocketMQLog:WARN No appenders could be found for logger (io.netty.util.internal.PlatformDependent0). +RocketMQLog:WARN Please initialize the logger system properly. +#Type #ProducerGroup #ClientHost #SendTime #CostTimes #Status +Pub 1623305799667 xxx.xxx.xxx.xxx 2021-06-10 14:16:40 131ms success +``` diff --git a/docs/cn/operation.md b/docs/cn/operation.md new file mode 100644 index 00000000000..9f04ce1d3d9 --- /dev/null +++ b/docs/cn/operation.md @@ -0,0 +1,1434 @@ + +# 运维管理 +--- + +### 1 集群搭建 + +#### 1.1 单Master模式 + +这种方式风险较大,一旦Broker重启或者宕机时,会导致整个服务不可用。不建议线上环境使用,可以用于本地测试。 + +##### 1)启动 NameServer + +```bash +### 首先启动Name Server +$ nohup sh mqnamesrv & + +### 验证Name Server 是否启动成功 +$ tail -f ~/logs/rocketmqlogs/namesrv.log +The Name Server boot success... +``` + +##### 2)启动 Broker + +```bash +### 启动Broker +$ nohup sh bin/mqbroker -n localhost:9876 & + +### 验证Broker是否启动成功,例如Broker的IP为:192.168.1.2,且名称为broker-a +$ tail -f ~/logs/rocketmqlogs/broker.log +The broker[broker-a, 192.169.1.2:10911] boot success... +``` + +#### 1.2 多Master模式 + +一个集群无Slave,全是Master,例如2个Master或者3个Master,这种模式的优缺点如下: + +- 优点:配置简单,单个Master宕机或重启维护对应用无影响,在磁盘配置为RAID10时,即使机器宕机不可恢复情况下,由于RAID10磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高; + +- 缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受到影响。 + +##### 1)启动NameServer + +NameServer需要先于Broker启动,且如果在生产环境使用,为了保证高可用,建议一般规模的集群启动3个NameServer,各节点的启动命令相同,如下: + +```bash +### 首先启动Name Server +$ nohup sh mqnamesrv & + +### 验证Name Server 是否启动成功 +$ tail -f ~/logs/rocketmqlogs/namesrv.log +The Name Server boot success... +``` + +##### 2)启动Broker集群 + +```bash +### 在机器A,启动第一个Master,例如NameServer的IP为:192.168.1.1 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-noslave/broker-a.properties & + +### 在机器B,启动第二个Master,例如NameServer的IP为:192.168.1.1 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-noslave/broker-b.properties & + +... +``` + +如上启动命令是在单个NameServer情况下使用的。对于多个NameServer的集群,Broker启动命令中`-n`后面的地址列表用分号隔开即可,例如 `192.168.1.1:9876;192.161.2:9876`。 + +#### 1.3 多Master多Slave模式-异步复制 + +每个Master配置一个Slave,有多对Master-Slave,HA采用异步复制方式,主备有短暂消息延迟(毫秒级),这种模式的优缺点如下: + +- 优点:即使磁盘损坏,消息丢失的非常少,且消息实时性不会受影响,同时Master宕机后,消费者仍然可以从Slave消费,而且此过程对应用透明,不需要人工干预,性能同多Master模式几乎一样; + +- 缺点:Master宕机,磁盘损坏情况下会丢失少量消息。 + +##### 1)启动NameServer + +```bash +### 首先启动Name Server +$ nohup sh mqnamesrv & + +### 验证Name Server 是否启动成功 +$ tail -f ~/logs/rocketmqlogs/namesrv.log +The Name Server boot success... +``` + +##### 2)启动Broker集群 + +```bash +### 在机器A,启动第一个Master,例如NameServer的IP为:192.168.1.1 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-async/broker-a.properties & + +### 在机器B,启动第二个Master,例如NameServer的IP为:192.168.1.1 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-async/broker-b.properties & + +### 在机器C,启动第一个Slave,例如NameServer的IP为:192.168.1.1 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-async/broker-a-s.properties & + +### 在机器D,启动第二个Slave,例如NameServer的IP为:192.168.1.1 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-async/broker-b-s.properties & +``` + +#### 1.4 多Master多Slave模式-同步双写 + +每个Master配置一个Slave,有多对Master-Slave,HA采用同步双写方式,即只有主备都写成功,才向应用返回成功,这种模式的优缺点如下: + +- 优点:数据与服务都无单点故障,Master宕机情况下,消息无延迟,服务可用性与数据可用性都非常高; + +- 缺点:性能比异步复制模式略低(大约低10%左右),发送单个消息的RT会略高,且目前版本在主节点宕机后,备机不能自动切换为主机。 + +##### 1)启动NameServer + +```bash +### 首先启动Name Server +$ nohup sh mqnamesrv & + +### 验证Name Server 是否启动成功 +$ tail -f ~/logs/rocketmqlogs/namesrv.log +The Name Server boot success... +``` + +##### 2)启动Broker集群 + +```bash +### 在机器A,启动第一个Master,例如NameServer的IP为:192.168.1.1 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-sync/broker-a.properties & + +### 在机器B,启动第二个Master,例如NameServer的IP为:192.168.1.1 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-sync/broker-b.properties & + +### 在机器C,启动第一个Slave,例如NameServer的IP为:192.168.1.1 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-sync/broker-a-s.properties & + +### 在机器D,启动第二个Slave,例如NameServer的IP为:192.168.1.1 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-sync/broker-b-s.properties & +``` + +以上Broker与Slave配对是通过指定相同的BrokerName参数来配对,Master的BrokerId必须是0,Slave的BrokerId必须是大于0的数。另外一个Master下面可以挂载多个Slave,同一Master下的多个Slave通过指定不同的BrokerId来区分。$ROCKETMQ_HOME指的RocketMQ安装目录,需要用户自己设置此环境变量。 + +#### 1.5 RocketMQ 5.0 自动主从切换 + +RocketMQ 5.0 开始支持自动主从切换的模式,可参考以下文档 + +[快速开始](controller/quick_start.md) + +[部署文档](controller/deploy.md) + +[设计思想](controller/design.md) + +### 2 mqadmin管理工具 + +> 注意: +> +> 1. 执行命令方法:`./mqadmin {command} {args}` +> 2. 几乎所有命令都需要配置-n表示NameServer地址,格式为ip:port +> 3. 几乎所有命令都可以通过-h获取帮助 +> 4. 如果既有Broker地址(-b)配置项又有clusterName(-c)配置项,则优先以Broker地址执行命令,如果不配置Broker地址,则对集群中所有主机执行命令,只支持一个Broker地址。-b格式为ip:port,port默认是10911 +> 5. 在tools下可以看到很多命令,但并不是所有命令都能使用,只有在MQAdminStartup中初始化的命令才能使用,你也可以修改这个类,增加或自定义命令 +> 6. 由于版本更新问题,少部分命令可能未及时更新,遇到错误请直接阅读相关命令源码 + +#### 2.1 Topic相关 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    名称含义命令选项说明
    updateTopic创建更新Topic配置-bBroker 地址,表示 topic 所在 + Broker,只支持单台Broker,地址为ip:port
    -ccluster 名称,表示 topic 所在集群(集群可通过 + clusterList 查询)
    -h-打印帮助
    -nNameServer服务地址,格式 ip:port
    -p指定新topic的读写权限( W=2|R=4|WR=6 )
    -r可读队列数(默认为 8)
    -w可写队列数(默认为 8)
    -ttopic 名称(名称只能使用字符 + ^[a-zA-Z0-9_-]+$ )
    deleteTopic删除Topic-ccluster 名称,表示删除某集群下的某个 topic (集群 + 可通过 clusterList 查询)
    -h打印帮助
    -nNameServer 服务地址,格式 ip:port
    -ttopic 名称(名称只能使用字符 + ^[a-zA-Z0-9_-]+$ )
    topicList查看 Topic 列表信息-h打印帮助
    -c不配置-c只返回topic列表,增加-c返回clusterName, + topic, consumerGroup信息,即topic的所属集群和订阅关系,没有参数
    -nNameServer 服务地址,格式 ip:port
    topicRoute查看 Topic 路由信息-ttopic 名称
    -h打印帮助
    -nNameServer 服务地址,格式 ip:port
    topicStatus查看 Topic 消息队列offset-ttopic 名称
    -h打印帮助
    -nNameServer 服务地址,格式 ip:port
    topicClusterList查看 Topic 所在集群列表-ttopic 名称
    -h打印帮助
    -nNameServer 服务地址,格式 ip:port
    updateTopicPerm更新 Topic 读写权限-ttopic 名称
    -h打印帮助
    -nNameServer 服务地址,格式 ip:port
    -bBroker 地址,表示 topic 所在 + Broker,只支持单台Broker,地址为ip:port
    -p指定新 topic 的读写权限( W=2|R=4|WR=6 )
    -ccluster 名称,表示 topic 所在集群(集群可通过 + clusterList 查询),-b优先,如果没有-b,则对集群中所有Broker执行命令
    updateOrderConf从NameServer上创建、删除、获取特定命名空间的kv配置,目前还未启用-h打印帮助
    -nNameServer 服务地址,格式 ip:port
    -ttopic,键
    -vorderConf,值
    -mmethod,可选get、put、delete
    allocateMQ以平均负载算法计算消费者列表负载消息队列的负载结果-ttopic 名称
    -h打印帮助
    -nNameServer 服务地址,格式 ip:port
    -iipList,用逗号分隔,计算这些ip去负载Topic的消息队列
    statsAll打印Topic订阅关系、TPS、积累量、24h读写总量等信息-h打印帮助
    -nNameServer 服务地址,格式 ip:port
    -a是否只打印活跃topic
    -t指定topic
    + + + +#### 2.2 集群相关 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    名称含义命令选项说明
    clusterList查看集群信息,集群、BrokerName、BrokerId、TPS等信息-m打印更多信息 (增加打印出如下信息 #InTotalYest, + #OutTotalYest, #InTotalToday ,#OutTotalToday)
    -h打印帮助
    -nNameServer 服务地址,格式 ip:port
    -i打印间隔,单位秒
    clusterRT发送消息检测集群各Broker RT。消息发往${BrokerName} Topic。-aamount,每次探测的总数,RT = 总时间 / + amount
    -s消息大小,单位B
    -c探测哪个集群
    -p是否打印格式化日志,以|分割,默认不打印
    -h打印帮助
    -m所属机房,打印使用
    -i发送间隔,单位秒
    -nNameServer 服务地址,格式 ip:port
    + + +#### 2.3 Broker相关 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    名称含义命令选项说明
    updateBrokerConfig更新 Broker 配置文件,会修改Broker.conf-bBroker 地址,格式为ip:port
    -ccluster 名称
    -kkey 值
    -vvalue 值
    -h打印帮助
    -nNameServer 服务地址,格式 ip:port
    brokerStatus查看 Broker 统计信息、运行状态(你想要的信息几乎都在里面)-bBroker 地址,地址为ip:port
    -h打印帮助
    -nNameServer 服务地址,格式 ip:port
    brokerConsumeStatsBroker中各个消费者的消费情况,按Message Queue维度返回Consume + Offset,Broker Offset,Diff,TImestamp等信息-bBroker 地址,地址为ip:port
    -t请求超时时间
    -ldiff阈值,超过阈值才打印
    -o是否为顺序topic,一般为false
    -h打印帮助
    -nNameServer 服务地址,格式 ip:port
    getBrokerConfig获取Broker配置-bBroker 地址,地址为ip:port
    -nNameServer 服务地址,格式 ip:port
    wipeWritePerm从NameServer上清除 Broker写权限-bBrokerName
    -nNameServer 服务地址,格式 ip:port
    -h打印帮助
    addWritePerm从NameServer上添加 Broker写权限-bBrokerName
    -nNameServer 服务地址,格式 ip:port
    -h打印帮助
    cleanExpiredCQ清理Broker上过期的Consume Queue,如果手动减少对列数可能产生过期队列-nNameServer 服务地址,格式 ip:port
    -h打印帮助
    -bBroker 地址,地址为ip:port
    -c集群名称
    deleteExpiredCommitLog清理Broker上过期的CommitLog文件,Broker最多会执行20次删除操作,每次最多删除10个文件-nNameServer 服务地址,格式 ip:port
    -h打印帮助
    -bBroker 地址,地址为ip:port
    -c集群名称
    cleanUnusedTopic清理Broker上不使用的Topic,从内存中释放Topic的Consume + Queue,如果手动删除Topic会产生不使用的Topic-nNameServer 服务地址,格式 ip:port
    -h打印帮助
    -bBroker 地址,地址为ip:port
    -c集群名称
    sendMsgStatus向Broker发消息,返回发送状态和RT-nNameServer 服务地址,格式 ip:port
    -h打印帮助
    -bBrokerName,注意不同于Broker地址
    -s消息大小,单位B
    -c发送次数
    + + +#### 2.4 消息相关 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    名称含义命令选项说明
    queryMsgById根据offsetMsgId查询msg,如果使用开源控制台,应使用offsetMsgId,此命令还有其他参数,具体作用请阅读QueryMsgByIdSubCommand。-imsgId
    -h打印帮助
    -nNameServer 服务地址,格式 ip:port
    queryMsgByKey根据消息 Key 查询消息-kmsgKey
    -tTopic 名称
    -h打印帮助
    -nNameServer 服务地址,格式 ip:port
    queryMsgByOffset根据 Offset 查询消息-bBroker 名称,(这里需要注意 + 填写的是 Broker 的名称,不是 Broker 的地址,Broker 名称可以在 clusterList 查到)
    -iquery 队列 id
    -ooffset 值
    -ttopic 名称
    -h打印帮助
    -nNameServer 服务地址,格式 ip:port
    queryMsgByUniqueKey根据msgId查询,msgId不同于offsetMsgId,区别详见常见运维问题。-g,-d配合使用,查到消息后尝试让特定的消费者消费消息并返回消费结果-h打印帮助
    -nNameServer 服务地址,格式 ip:port
    -iuniqe msg id
    -gconsumerGroup
    -dclientId
    -ttopic名称
    checkMsgSendRT检测向topic发消息的RT,功能类似clusterRT-h打印帮助
    -nNameServer 服务地址,格式 ip:port
    -ttopic名称
    -a探测次数
    -s消息大小
    sendMessage发送一条消息,可以根据配置发往特定Message Queue,或普通发送。-h打印帮助
    -nNameServer 服务地址,格式 ip:port
    -ttopic名称
    -pbody,消息体
    -kkeys
    -ctags
    -bBrokerName
    -iqueueId
    consumeMessage消费消息。可以根据offset、开始&结束时间戳、消息队列消费消息,配置不同执行不同消费逻辑,详见ConsumeMessageCommand。-h打印帮助
    -nNameServer 服务地址,格式 ip:port
    -ttopic名称
    -bBrokerName
    -o从offset开始消费
    -iqueueId
    -g消费者分组
    -s开始时间戳,格式详见-h
    -d结束时间戳
    -c消费多少条消息
    printMsg从Broker消费消息并打印,可选时间段-h打印帮助
    -nNameServer 服务地址,格式 ip:port
    -ttopic名称
    -c字符集,例如UTF-8
    -ssubExpress,过滤表达式
    -b开始时间戳,格式参见-h
    -e结束时间戳
    -d是否打印消息体
    printMsgByQueue类似printMsg,但指定Message Queue-h打印帮助
    -nNameServer 服务地址,格式 ip:port
    -ttopic名称
    -iqueueId
    -aBrokerName
    -c字符集,例如UTF-8
    -ssubExpress,过滤表达式
    -b开始时间戳,格式参见-h
    -e结束时间戳
    -p是否打印消息
    -d是否打印消息体
    -f是否统计tag数量并打印
    resetOffsetByTime按时间戳重置offset,Broker和consumer都会重置-h打印帮助
    -nNameServer 服务地址,格式 ip:port
    -g消费者分组
    -ttopic名称
    -s重置为此时间戳对应的offset
    -f是否强制重置,如果false,只支持回溯offset,如果true,不管时间戳对应offset与consumeOffset关系
    -c是否重置c++客户端offset
    + + +#### 2.5 消费者、消费组相关 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    名称含义命令选项说明
    consumerProgress查看订阅组消费状态,可以查看具体的client IP的消息积累量-g消费者所属组名
    -s是否打印client IP
    -h打印帮助
    -nNameServer 服务地址,格式 ip:port
    consumerStatus查看消费者状态,包括同一个分组中是否都是相同的订阅,分析Process + Queue是否堆积,返回消费者jstack结果,内容较多,使用者参见ConsumerStatusSubCommand-h打印帮助
    -nNameServer 服务地址,格式 ip:port
    -gconsumer group
    -iclientId
    -s是否执行jstack
    updateSubGroup更新或创建订阅关系-nNameServer 服务地址,格式 ip:port
    -h打印帮助
    -bBroker地址
    -c集群名称
    -g消费者分组名称
    -s分组是否允许消费
    -m是否从最小offset开始消费
    -d是否是广播模式
    -q重试队列数量
    -r最大重试次数
    -i当slaveReadEnable开启时有效,且还未达到从slave消费时建议从哪个BrokerId消费,可以配置备机id,主动从备机消费
    -w如果Broker建议从slave消费,配置决定从哪个slave消费,配置BrokerId,例如1
    -a当消费者数量变化时是否通知其他消费者负载均衡
    deleteSubGroup从Broker删除订阅关系-nNameServer 服务地址,格式 ip:port
    -h打印帮助
    -bBroker地址
    -c集群名称
    -g消费者分组名称
    cloneGroupOffset在目标群组中使用源群组的offset-nNameServer 服务地址,格式 ip:port
    -h打印帮助
    -s源消费者组
    -d目标消费者组
    -ttopic名称
    -o暂未使用
    + + + + +#### 2.6 连接相关 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    名称含义命令选项说明
    consumerConnection查询 Consumer 的网络连接-g消费者所属组名
    -nNameServer 服务地址,格式 ip:port
    -h打印帮助
    producerConnection查询 Producer 的网络连接-g生产者所属组名
    -t主题名称
    -nNameServer 服务地址,格式 ip:port
    -h打印帮助
    + + + + +#### 2.7 NameServer相关 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    名称含义命令选项说明
    updateKvConfig更新NameServer的kv配置,目前还未使用-s命名空间
    -kkey
    -vvalue
    -nNameServer 服务地址,格式 ip:port
    -h打印帮助
    deleteKvConfig删除NameServer的kv配置-s命名空间
    -kkey
    -nNameServer 服务地址,格式 ip:port
    -h打印帮助
    getNamesrvConfig获取NameServer配置-nNameServer 服务地址,格式 ip:port
    -h打印帮助
    updateNamesrvConfig修改NameServer配置-nNameServer 服务地址,格式 ip:port
    -h打印帮助
    -kkey
    -vvalue
    + + + + +#### 2.8 其他 + + + + + + + + + + + + + + + + + + + + + + +
    名称含义命令选项说明
    startMonitoring开启监控进程,监控消息误删、重试队列消息数等-nNameServer 服务地址,格式 ip:port
    -h打印帮助
    + + +### 3 运维常见问题 + +#### 3.1 RocketMQ的mqadmin命令报错问题 + +> 问题描述:有时候在部署完RocketMQ集群后,尝试执行“mqadmin”一些运维命令,会出现下面的异常信息: +> +> ```java +> org.apache.rocketmq.remoting.exception.RemotingConnectException: connect to failed +> ``` + +解决方法:可以在部署RocketMQ集群的虚拟机上执行`export NAMESRV_ADDR=ip:9876`(ip指的是集群中部署NameServer组件的机器ip地址)命令之后再使用“mqadmin”的相关命令进行查询,即可得到结果。 + +#### 3.2 RocketMQ生产端和消费端版本不一致导致不能正常消费的问题 + +> 问题描述:同一个生产端发出消息,A消费端可消费,B消费端却无法消费,rocketMQ Console中出现: +> +> ```java +> Not found the consumer group consume stats, because return offset table is empty, maybe the consumer not consume any message的异常消息。 +> ``` + + 解决方案:RocketMQ 的jar包:rocketmq-client等包应该保持生产端,消费端使用相同的version。 + +#### 3.3 新增一个topic的消费组时,无法消费历史消息的问题 + +> 问题描述:当同一个topic的新增消费组启动时,消费的消息是当前的offset的消息,并未获取历史消息。 + +解决方案:rocketmq默认策略是从消息队列尾部,即跳过历史消息。如果想消费历史消息,则需要设置:`org.apache.rocketmq.client.consumer.DefaultMQPushConsumer#setConsumeFromWhere`。常用的有以下三种配置: + +- 默认配置,一个新的订阅组第一次启动从队列的最后位置开始消费,后续再启动接着上次消费的进度开始消费,即跳过历史消息; + +```java +consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); +``` + +- 一个新的订阅组第一次启动从队列的最前位置开始消费,后续再启动接着上次消费的进度开始消费,即消费Broker未过期的历史消息; + +```java +consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); +``` + +- 一个新的订阅组第一次启动从指定时间点开始消费,后续再启动接着上次消费的进度开始消费,和consumer.setConsumeTimestamp()配合使用,默认是半个小时以前; + +```java +consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_TIMESTAMP); +``` + +#### 3.4 如何开启从Slave读数据功能 + +在某些情况下,Consumer需要将消费位点重置到1-2天前,这时在内存有限的Master Broker上,CommitLog会承载比较重的IO压力,影响到该Broker的其它消息的读与写。可以开启`slaveReadEnable=true`,当Master Broker发现Consumer的消费位点与CommitLog的最新值的差值的容量超过该机器内存的百分比(`accessMessageInMemoryMaxRatio=40%`),会推荐Consumer从Slave Broker中去读取数据,降低Master Broker的IO。 + +#### 3.5 性能调优问题 + +异步刷盘建议使用自旋锁,同步刷盘建议使用重入锁,调整Broker配置项`useReentrantLockWhenPutMessage`,默认为false;异步刷盘建议开启`TransientStorePoolEnable`;建议关闭transferMsgByHeap,提高拉消息效率;同步刷盘建议适当增大`sendMessageThreadPoolNums`,具体配置需要经过压测。 + +#### 3.6 在RocketMQ中msgId和offsetMsgId的含义与区别 + +使用RocketMQ完成生产者客户端消息发送后,通常会看到如下日志打印信息: + +```java +SendResult [sendStatus=SEND_OK, msgId=0A42333A0DC818B4AAC246C290FD0000, offsetMsgId=0A42333A00002A9F000000000134F1F5, messageQueue=MessageQueue [topic=topicTest1, BrokerName=mac.local, queueId=3], queueOffset=4] +``` + +- msgId,对于客户端来说msgId是由客户端producer实例端生成的,具体来说,调用方法`MessageClientIDSetter.createUniqIDBuffer()`生成唯一的Id; +- offsetMsgId,offsetMsgId是由Broker服务端在写入消息时生成的(采用”IP地址+Port端口”与“CommitLog的物理偏移量地址”做了一个字符串拼接),其中offsetMsgId就是在RocketMQ控制台直接输入查询的那个messageId。 diff --git a/docs/cn/proxy/deploy_guide.md b/docs/cn/proxy/deploy_guide.md new file mode 100644 index 00000000000..5ecc1386b61 --- /dev/null +++ b/docs/cn/proxy/deploy_guide.md @@ -0,0 +1,35 @@ +# RocketMQ Proxy部署指南 + +## 概述 + +RocketMQ Proxy 支持两种代理模式: `Local` and `Cluster`。 + +## 配置 + +该配置适用于 `Cluster` 和 `Local` 两种模式, 默认路径为 `distribution/conf/rmq-proxy.json`。 + +## `Cluster` 模式 + +* 设置 `nameSrvAddr` +* 设置 `proxyMode` 为 `cluster` (不区分大小写) + +运行以下命令: + +```shell +nohup sh mqproxy & +``` + +该命令仅会启动 `Proxy` 组件本身。它假设已经在指定的 `nameSrvAddr` 地址上运行着 `Namesrv` 节点,同时也有 broker 节点通过 `nameSrvAddr` 注册自己并运行。 + +## `Local` 模式 + +* 设置 `nameSrvAddr` +* 设置 `proxyMode` 为 `local` (不区分大小写) + +运行以下命令: + +```shell +nohup sh mqproxy & +``` + +上面的命令将启动`Proxy`,并在同一进程中运行`Broker`。它假设`Namesrv`节点正在按照`nameSrvAddr`指定的地址运行。 diff --git a/docs/cn/rpc_request.md b/docs/cn/rpc_request.md new file mode 100644 index 00000000000..ffd2bb87e36 --- /dev/null +++ b/docs/cn/rpc_request.md @@ -0,0 +1,146 @@ +# “Request-Reply”特性 +--- + +## 1 使用场景 +随着服务规模的扩大,单机服务无法满足性能和容量的要求,此时需要将服务拆分为更小粒度的服务或者部署多个服务实例构成集群来提供服务。在分布式场景下,RPC是最常用的联机调用的方式。 + +在构建分布式应用时,有些领域,例如金融服务领域,常常使用消息队列来构建服务总线,实现联机调用的目的。消息队列的主要场景是解耦、削峰填谷,在联机调用的场景下,需要将服务的调用抽象成基于消息的交互,并增强同步调用的这种交互逻辑。为了更好地支持消息队列在联机调用场景下的应用,rocketmq-4.6.0推出了“Request-Reply”特性来支持RPC调用。 + +## 2 设计思路 +在rocketmq中,整个同步调用主要包括两个过程: + +(1)请求方生成消息,发送给响应方,并等待响应方回包; + +(2)响应方收到请求消息后,消费这条消息,并发出一条响应消息给请求方。 + +整个过程实质上是两个消息收发过程的组合。所以这里最关键的问题是如何将异步的消息收发过程构建成一个同步的过程。其中主要有两个问题需要解决: + +### 2.1 请求方如何同步等待回包 + +这个问题的解决方案中,一个关键的数据结构是RequestResponseFuture。 + +``` +public class RequestResponseFuture { + private final String correlationId; + private final RequestCallback requestCallback; + private final long beginTimestamp = System.currentTimeMillis(); + private final Message requestMsg = null; + private long timeoutMillis; + private CountDownLatch countDownLatch = new CountDownLatch(1); + private volatile Message responseMsg = null; + private volatile boolean sendRequestOk = true; + private volatile Throwable cause = null; +} +``` +RequestResponseFuture中,利用correlationId来标识一个请求。如下图所示,Producer发送request时创建一个RequestResponseFuture,以correlationId为key,RequestResponseFuture为value存入map,同时请求中带上RequestResponseFuture中的correlationId,收到回包后根据correlationId拿到对应的RequestResponseFuture,并设置回包内容。 +![](image/producer_send_request.png) + +### 2.2 consumer消费消息后,如何准确回包 + +(1)producer在发送消息的时候,会给每条消息生成唯一的标识符,同时还带上了producer的clientId。当consumer收到并消费消息后,从消息中取出消息的标识符correlationId和producer的标识符clientId,放入响应消息,用来确定此响应消息是哪条请求消息的回包,以及此响应消息应该发给哪个producer。同时响应消息中设置了消息的类型以及响应消息的topic,然后consumer将消息发给broker,如下图所示。 +![](image/consumer_reply.png) + +(2)broker收到响应消息后,需要将消息发回给指定的producer。Broker如何知道发回给哪个producer?因为消息中包含了producer的标识符clientId,在ProducerManager中,维护了标识符和channel信息的对应关系,通过这个对应关系,就能把回包发给对应的producer。 + +响应消息发送和一般的消息发送流程区别在于,响应消息不需要producer拉取,而是由broker直接推给producer。同时选择broker的策略也有变化:请求消息从哪个broker发过来,响应消息也发到对应的broker上。 + +Producer收到响应消息后,根据消息中的唯一标识符,从RequestResponseFuture的map中找到对应的RequestResponseFuture结构,设置响应消息,同时计数器减一,解除等待状态,使请求方收到响应消息。 + +## 3 使用方法 + +同步调用的示例在example文件夹的rpc目录下。 + +### 3.1 Producer +``` +Message msg = new Message(topic, + "", + "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); + + long begin = System.currentTimeMillis(); + Message retMsg = producer.request(msg, ttl); + long cost = System.currentTimeMillis() - begin; + System.out.printf("request to <%s> cost: %d replyMessage: %s %n", topic, cost, retMsg); +``` +调用接口替换为request即可。 + +### 3.2 Consumer +需要启动一个producer,同时在覆写consumeMessage方法的时候,自定义响应消息并发送。 + +``` + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); + for (MessageExt msg : msgs) { + try { + System.out.printf("handle message: %s", msg.toString()); + String replyTo = MessageUtil.getReplyToClient(msg); + byte[] replyContent = "reply message contents.".getBytes(); + // create reply message with given util, do not create reply message by yourself + Message replyMessage = MessageUtil.createReplyMessage(msg, replyContent); + + // send reply message with producer + SendResult replyResult = replyProducer.send(replyMessage, 3000); + System.out.printf("reply to %s , %s %n", replyTo, replyResult.toString()); + } catch (MQClientException | RemotingException | MQBrokerException | InterruptedException e) { + e.printStackTrace(); + } + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } +``` + +## 4 接口参数 + +4.1 public Message request(Message msg,long timeout) + +msg:待发送的消息 + +timeout:同步调用超时时间 + +4.2 public void request(Message msg, final RequestCallback requestCallback, long timeout) + +msg:待发送的消息 + +requestCallback:回调函数 + +timeout:同步调用超时时间 + +4.3 public Message request(final Message msg, final MessageQueueSelector selector, final Object arg,final long timeout) + +msg:待发送的消息 + +selector:消息队列选择器 + +arg:消息队列选择器需要的参数 + +timeout:同步调用超时时间 + +4.4 public void request(final Message msg, final MessageQueueSelector selector, final Object arg,final RequestCallback requestCallback, final long timeout) + +msg:待发送的消息 + +selector:消息队列选择器 + +arg:消息队列选择器需要的参数 + +requestCallback:回调函数 + +timeout:同步调用超时时间 + +4.5 public Message request(final Message msg, final MessageQueue mq, final long timeout) + +msg:待发送的消息 + +mq:目标消息队列 + +timeout:同步调用超时时间 + +4.6 public void request(final Message msg, final MessageQueue mq, final RequestCallback requestCallback, long timeout) + +msg:待发送的消息 + +mq:目标消息队列 + +requestCallback:回调函数 + +timeout:同步调用超时时间 diff --git "a/docs/cn/statictopic/RocketMQ_Static_Topic_Logic_Queue_\350\256\276\350\256\241.md" "b/docs/cn/statictopic/RocketMQ_Static_Topic_Logic_Queue_\350\256\276\350\256\241.md" new file mode 100644 index 00000000000..784a83f929f --- /dev/null +++ "b/docs/cn/statictopic/RocketMQ_Static_Topic_Logic_Queue_\350\256\276\350\256\241.md" @@ -0,0 +1,503 @@ +### Version 记录 +| 时间 | 主要内容 | 作者 | +| --- | --- | --- | +| 2021-11-01 | 初稿,包括背景、目标、SOT定义与持久化、SOT生命周期、SOT的使用、API逻辑修改、问题与风险 | dongeforever | +| 2021-11-15 | 修改 LogicQueue 的定义,不要引入新字段,完全复用旧的MessageQueue;RemappingStaticTopic时,不要迁移『位点』『幂等数据』等,而是采用Double-Check-Read 的机制| dongforever | +| 2021-12-01 | 更新问题与风险,增加关于一致性、OutOfRange、拉取中断的详细说明| dongforever | +| 2021-12-03 | 增加代码走读的说明| dongforever | +| 2021-12-10 | 引入Scope概念,保留『多集群动态零耦合』的集群设计模型 | dongforever | +| 2021-12-23 | 梳理待完成事项;讨论Admin接口的适配方式 | dongforever | +| 2021-01-05 | Offset存储改成『转换制』,以更好适配原有逻辑 | dongforever | + + + + +中文文档在描述特定专业术语时,仍然使用英文。 + +### 需求背景 +StaticTopic/LogicQueue 本质上是解决『固定队列数量』的需求。 +这个需求是不是必需的呢,如果是做应用集成,则可能不是必需的,但如果是做数据集成,则是必需的。 + +固定队列数量,首先可以解决『顺序性』的问题。 +在应用集成场景下,应用是无需感知到队列的,只要MQ能保证按顺序投递给应用即可,MQ底层队列数量如何变化,对应用来说是不关心。比如,MQ之前的那套『禁读禁写』就是可以玩转的。 + +但在数据集成场景中,队列或者叫『分片』,是要暴露给客户端的,客户端所有的数据计算场景,都是基于『分片』来进行的,如果『分片』里的数据,发生了错乱,则计算结果都是错误的。比如,计算WordCount,源数据经过预处理之后,按key写入清洗后的Topic,然后计算侧根据清洗的结果,按照分片来并行计算。如果分片发生变化,则整个清洗逻辑,需要重新处理。 + +有人可能会反驳,说计算组件清洗后,可以以批的方式写入其它存储组件。这当然是可以的,但如果是这样,MQ的价值就纯粹是一个『源头』价值,而不是『通道』价值。 + +MQ要想成为一个『数据通道』,则必需要具备可以让计算组件『回写』数据的能力,具备存储『Clean Data』的能力,这样才让MQ有可能在数据集成领域站稳脚跟。 + +如果是 RocketMQ Streams 这种轻量化的组件,则『回写』会更频繁,更重要。 + +除此之外,『固定队列数据』对于,RocketMQ 自身后续的发展,也是至关重要的: + +- compact topic,如果不能做到严格按key hash,则这个KV系统是有问题的 +- 事务或者其它Coordinator的实现,采用『固定队列数量』,可以选取到准确的Broker来充当协调器 +- Metadata 的存储,按key hash,那么就可以在Broker上,存储千万级的 Topic + +『固定队列数量』对于RocketMQ挺进『数据集成』这个领域,有着不可或缺的作用。 +LogicQueue的思路就是为了解决这一问题。 + +### 设计目标 +#### 总体目标 +提供『Static Topic』的特性。 +引入以下核心概念: +- physical message queue, physical queue for short, a shard bound to a specified broker. +- logic message queue, logic queue for short, a shard vertically composed by physical queues. +- dynamic sharded topic, dynamic topic for short, which has queues increasing with the broker numbers. +- static sharded topic, static topic for short, which has fixed queues, implemented with logic queues. + +『Static Topic』拥有固定的分片数量,每个分片称之为『Logic Queue』。 +每个『Logic Queue』由多个『Physical Queue』进行纵向分段映射组成。 + +引入以下非核心概念,对用户无感知,但对于讨论问题非常重要: +- Leader Queue, 某个『Logic Queue』最新映射的『Physical Queue』,也即可写的那个Queue +- Second Leader Queue,某个『Logic Queue』次新映射的『Physical Queue』,也即最新一次切换之前的『Leader Queue』 + +#### Scope 目标 +单集群固定 和 全网固定,参考 [The_Scope_Of_Static_Topic](The_Scope_Of_Static_Topic.md)。 + + +#### LogicQueue 目标 +在客户端,LogicQueue 与 Physical Queue 使用体感上没有任何区别,使用一样的概念和对象,遵循一样的语义。 +在服务端,针对 LogicQueue 去适配相关的API。 + +#### 队列语义 +RocketMQ Physical Queue 含有以下语义: + +- 队列内的Offset,单调递增且连续 +- 属于同一个 Broker 上的队列,编号单调递增且连续 + +LogicQueue 需要保障的语义: + +- 队列内的offset,单调递增 + +LogicQueue 可以不保障的语义: + +- 队列内的 offset 连续 +- 属于同一个 Broker 上的队列,编号单调递增且连续 + +offset连续,是一个应该尽量保证的语义,可以允许有少量空洞,但不应该出现大面积不连续的位点。 +offset不连续最直接的问题就是: + +- 计算Lag会比较麻烦 +- 不方便客户端进行各种优化计算(比如切批等) + +但只要空洞不是大量频繁出现的,那么也是问题不大的。 + +单机队列编号连续,除了在注册元数据时,可以简约部分字节外,没有其它实际用处,可以不保证。 +当前客户端使用到『单机队列编号连续』这个特点的场景主要有: + +- 客户端在获取到TopicRouteData后,转化成MessageQueue时,利用编号进行遍历 + + +#### LogicQueue 定义 +当前 MessageQueue 的定义如下 +``` +private String topic; +private String brokerName; +private int queueId; +``` + + +LogicQueue需要对客户直接暴露,为了保证使用习惯一致,采用同样的定义,其中 queueId相当于全局Id,而brokerName 固定如下: +``` +MixAll.LOGICAL_QUEUE_MOCK_BROKER_NAME = "__logical_queue_broker__"; +``` +此时,brokerName没有实际含义,但可以用来识别是否是LogicQueue。 + +采用此种定义,对于客户端内部的实现习惯改变如下: + +- **无法直接根据 brokerName 找到addr,而是需要根据 MessageQueue 找到 addr** + +具体改法是MQClientInstance中维护一个映射关系 +``` +private final ConcurrentMap> topicEndPointsTable = new ConcurrentHashMap<>(); +``` + + +基本目标与定义清楚了,接下来的设计,从 Source of Truth 开始。 + +### SOT 定义和持久化 +LogicQueue 的 Source of Truth 就是 LogicQueue 到 Physical Queue 的映射关系。 +只要这个映射关系不丢失,则整个系统的状态都是可以恢复的。 +反之,整个系统可能陷入混乱。 + +这个SOT,命名为 TopicQueueMapping。 + +#### Mapping Schema +``` +{ +"version":"1", +"bname": "broker02" //标记这份数据的原始存储位置,如果发送误拷贝,可以利用这个字段来进行标识 +"epoch": 0, //标记修改版本,用来做一致性校验 +"totalQueues":"50", //当前Topic 总共有多少 LogicQueues +"hostedQueues": { //当前Broker 所拥有的 LogicQueues +"3" : [ + { + "queue":"0", + "bname":"broker01" + "gen":"0", //标记切换代次 + "logicOffset":"0", //logicOffset的起始位置 + "startOffset":"0", // 物理offset的起始位置 + "endOffset":"1000" // 可选,物理offset的最大位置,可以根据上下文算出来 + "timeOfStart":"1561018349243" //可选,用来优化时间搜索 + "timeOfEnd":"1561018349243" //可选,用来优化时间搜索 + "updateTime":"1561018349243" //可选,记录更新的时间 + }, + { + "queue":"0", + "bname":"broker02", + "gen":"1", //可选,标记切换代次 + "logicOffset":"1000", //logicOffset的起始位置 + "startOffset":"0", // 物理offset的起始位置 + "endOffset":"-1" // 可选,物理offset的最大位置,可以根据上下文算出来,最新的一个应该是活跃的 + "timeOfStart":"1561018349243" //可选,用来优化时间搜索 + "timeOfEnd":"1561018349243" //可选,用来优化时间搜索 + "updateTime":"1561018349243" //可选,记录更新的时间 + } + ] + } +} +``` +上述示例的含义是: +* broker02 拥有 LogicQueue 3 +* LogicQueue 3 由 2 个 Physical Queue 组成 +* 位点范围『0-1000』映射到 Physical Queue 『broker01-0』上面 +* 位点范围『1000-』映射到 Physical Queue 『broker02-0』上面 + +『拥有』的定义是指,Leader Queue 在当前Broker。注意,在实现时,也会把Second Leader Queue存储下来作为备份。 + +注意以下要点: + +- 这个数据量会很大,后续需要考虑进行压缩优化(大部分字段可以压缩) +- 如果将来利用 LogicQueue 去做 Serverless 弹缩,则这个数据会加速膨胀,对这个数据的利用要谨慎 +- 要写上 bname,以具备自我识别能力 + +#### Leader Completeness +RocketMQ 没有中心化的元数据存储,那就遵循『Leader Completeness』原则。 +对于每个逻辑队列,把所有映射关系存储在『最新队列所在的Broker上面』,最新队列,其实也是可写队列。 +Leader Completeness,避免了数据的切割,对于后续其它操作有极大的便利。 + +#### Global Epoch Check +对于每个Static Topic,在每个Broker都应该拥有一份『TopicQueueMapping』,每份都带有Epoch。 +在创建和更新时,要对已有数据进行完备性校验,如果发现不完备,则说明上次操作失败,或者部分Broker数据丢失,应该先修复再操作。 + +注意: +即使当前Broker不拥有任何 LogicQueue 或者 PhysicalQueue,也应该存储一份,以做校验。 +假设某个Static Topic只拥有1个Logic Queue,而对应的Broker整好宕机,则此时可以根据其它Broker的信息判断出该Topic不完备。 + + +#### File Isolation +由于 RocketMQ 很多的运维习惯,都是直接拷贝 Topics.json 到别的机器进行部署的。 +而 TopicQueueMapping 是 Broker 相关的,如果把 TopicQueueMapping 从一个Broker拷贝到另一个Broker,则会造成SOT冲突。 + +在设计上,TopicQueueMapping 采取独立文件,避免冲突。 +在格式上,queue 里面要写上 bname,以具备自我识别能力,这样即使误拷贝到另一台机器,可以识别并报错,进行忽略即可。 + + +### SOT 生命周期 +#### 创建和更新 +映射关系的创建,第一期应该只由 MQAdmin 来进行操作。 +后续,可以考虑引入自动化组件。 +这里的要点是: + +- TopicConfig 和 TopicQueueMapping 分开存储,但写入时,需要先写 TopicQueueMapping 再写 TopicConfig(SOT先写) +- 【加强校验】需要在 TopicConfig 里面加上一个字段来标识『LogicQueue』的 Topic +- 【加强校验】允许单独更新 TopicConfig,但要带上 TotalQueues 这些基础数据 +- 允许更新单 LogicQueue + + +更多细节在API逻辑修改里面 +#### 存储 +按照 『Leader Completeness』原则进行存储。 +#### 切换 +如果为了保证严格顺序,则应该采取『禁旧再切新』的原则: +- 从旧 Leader 所在 Broker 获取信息,进行计算 +- 写入旧 Leader,也即禁写旧 Leader +- 写入新 Leader + +如果为了保证最高可用性,则应该采取『切新禁旧再切新』: +- 从旧 Leader 所在 Broker 获取信息,进行计算 +- 写入新 Leader,保证新 Leader 可写,此时 logicOffset 未定 +- 写入旧 Leader,禁写旧 Leader +- 更新新 Leader,确定 logicOffset + +切换失败处理: +- 不管哪种方式,数据存储至少成功了1份,后续可以手工恢复 + + +#### 清除 +有两部分信息需要清除 + +- 旧 Broker 上超过2代的映射关系进行清除 +- 对于单个LogicQueue,清除已经过期的 Broker Queue 映射项目 + + +### SOT 的使用 +#### Broker 注册数据 +SOT存储在Broker上,所以使用从 Broker开始。 + +在 RegisterBrokerBody 中,需要带上两个信息: + +- 对于每个Topic,带上本机队列编号和逻辑队列编号的映射关系,也即queueMap +- 对于每个Topic,需要带上 totalQueueNum 这个信息 + + + +异常情况需要考虑,假如本 Broker 不拥有任何 LogicQueue 呢?依然需要带上 totalQueueNum 这个信息。 +注意,不需要带上所有的映射关系,否则Nameserver很快会被打爆。 + + +#### Nameserver 组装数据 +原先的 QueueData 增加2个字段: + +- totalQueues,标识总逻辑队列数 +- queueMap,也即本机队列编号和逻辑队列编号的映射关系 + + +如果 QueueData 里面 totalQueues 的值 > 0 则认为是逻辑队列,在客户端解析时要进行判断。 + + +遗留问题: +是否需要尊重 readQueueNums 和 writeQueueNums ? +在LogicQueue这里,这个场景是没有意义的,但依然保持尊重。 + +#### Client 解析数据 +改动两个方法即可: + +- topicRouteData2TopicPublishInfo +- topicRouteData2TopicSubscribeInfo + + +注意,逻辑队列要求队列数是固定,如果发现,解析完之后,存在部分队列空洞,要用虚拟Broker值进行补全。 +Producer 侧如果要对无 key 场景进行优化,可以通过虚拟Broker值来判断,当前队列是不可用的。 +对于key场景,应该让客户端报错。 + +### API 逻辑修改 +#### Tools +LogicQueue是为了解决『Static Sharding』的问题。对于客户来说,『LogicQueue』是手段,『Static』才是目的。本着『用户知晓目的,开发者才需要关心手段』的原则,对用户应该只暴露『Static』的概念。所有QueueMapping的生命周期维护,应该都对用户透明。 + +#### UpdateStaticTopic +新增UpdateStaticTopic命令,对应RequestCode.UPDATE_AND_CREATE_STATIC_TOPIC=513,主要参数是: + +- -t,topic 名字 +- -qn,总队列数量 +- -c cluster列表 或者 -b broker列表 + + + +UpdateStaticTopic 命令会自动计算预期的分布情况,包括但不限于执行以下逻辑: + +- 检测Topic的命名冲突 +- 检测旧数据的一致性 +- 创建必要的物理队列 +- 创建必要的映射关系 + + +#### RemappingStaticTopic +迁移动作不引入新命令,计算好之后,执行UPDATE_AND_CREATE_STATIC_TOPIC即可. +主要参数: + +- -t, topic 名字 +- -c cluster列表 或者 -b broker列表 + +基本操作流程: + +- 1 获取旧Leader,计算映射关系 +- 2 迁移『位点』『幂等数据』等 +- 3 映射关系写入新/旧 Leader + +其中第二步,数据量可能会很大,导致迁移周期非常长,且不是并发安全的。 +但这些数据,都是覆盖型的,因此可以改造成不迁移的方式,而是在API层做兼容,也即『Double-Read-Check』机制: + +- 读取数据时,先从 Leader 读,如果Leader没有,则从Sub-Leader读取。 +- 提交数据,直接在 Leader 层面操作即可,覆盖旧数据。 + + +将来实现的幂等逻辑,也是类似。 + +#### UpdateTopic +服务端判断『StaticTopic』,禁止该命令进行修改。 + +#### DeleteTopic +复用现有逻辑,对于 StaticTopic,执行必要的清除动作。 + +#### TopicStatus +复用现有逻辑,同时展现『Logical Queue』和『Physical Queue』。 + +#### Broker +#### pullMessage +分段映射,执行远程读,在返回消息时,不进行offset转换,而是返回 OffsetDelta 变量,由客户端进行转换。 +这里的方式,类似于Batch。 + +#### getMinOffset +寻找映射关系,读最早的队列的MinOffset + +#### getMaxOffset +本机读,转换成logicOffset即可。 + + +#### getOffsetByTime +需要分段查找。 +如果要优化查找速度,应该在映射关系里面,插入时间戳。 + + +#### consumerOffsets 系列 +Offset的存储,进行转换,存储在对应PhysicalQueue 所在的 Broker上面。 +读取时,采取『Double-Read-Check』机制,并进行转换。 +这样可以最大程度与 PhysicalQueue 的相关逻辑进行适配,比如 ConsumerProgress 可以看到『最近拉取时间』。 + +#### Client + +- MQClientInstance.topicRouteData2TopicXXXInfo,修改解析 TopicRouteData的逻辑 +- Consumer解压消息时,需要加上OffsetDelta逻辑 + +#### SDK 兼容性分析 +如果要使用StaticTopic,则需要升级Client、Broker、Nameserver。 + +### 问题与风险 +#### 数据一致性问题 +RocketMQ 没有引入中心化的存储组件,那么如何保证 SOT 的全局一致性呢? +主要利用两个信息 +* TopicQueueMapping 带上的 epoch +* TopicQueueMapping 带上 totalQueues + +在更新或者切换时,获取所有Broker上的 TopicQueueMapping,校验 epoch 和 totalQueues,并且根据 TopicQueueMapping 可以完整地构建出对应的Logic Queue,则说明数据是完整一致的。 + +如果发现数据不一致,可能是以下因素引入的: +* 集群中有Broker宕机 +* 上次更新没有完全成功 + +应该要先修复数据,再执行 更新或切换 操作 + +#### No Target Brokers +Target Brokers 是指拥有 LogicQueue 的 Broker。 +考察1个场景,如果某个Topic 只有1个 LogicQueue,而拥有这个 LogicQueue 的 Broker 正好宕机了。此时去更新 Topic,会不会误认为该 Topic 不存在? +解决这个问题的办法是引入 No Target Brokers,也即集群中除去『Target Brokers』之外的 Broker。 +对于 No Target Broker,依然需要写入一份 TopicQueueMapping,带上 epoch 和 totalQueues,但不拥有任何 LogicQueue。 +有了这个信息之后,在进行一致性校验时,就可以识别出上述场景。 + +尤其要注意,如果 Nameserver 中没有任何信息,则需要主动去所有 Broker 拉取一遍。 + +#### 切换时最新 LogicQueueMappingItem 的 logicOffset 决策问题 +logicOffset的决策,依赖于上一个 PhysicalQueue 的最大位点。 +此时,要么跳跃位点,要么等待上一个 PhysicalQueue 确保已经禁写。 +当前实现,为了保障高可用,采用『切新禁旧再切新』的方式,同时跳跃位点。 + +#### logicOffset 为 -1 时的处理 +此时,可以写,但返回给 客户端的 offset 也是-1。 +此时,不可以读最新 PhysicalQueue。 +需要非常小心触发位点被重置: +- 忽略logicOffset为 -1 的item +- 计算staticOffset时,如果发现logicOffset为-1,则报错 + +目前只允许,SendMessage和GetMin时,返回-1。其余场景,要严格校验并报错。 + + +#### 队列重复映射 +如果允许1个 PhysicalQueue 被重复利用,也即多段映射给多个 LogicQueue,或者从非0开始映射。 +会带来以下麻烦: +* 所有位点相关的API,需要考虑 MappingItem endOffset,因为超过了 endOffset 可能已经不属于 当前 LogicQueue 了 +* 新建 MappingItem,需要先获取 旧 MappingItem 的 endOffset + +当前实现,为了保证简洁,禁止 PhysicalQueue 被重复利用,每次更新映射都会让物理层面的 writeQueues++ 和 readQueues++。 +后续实现,可以考虑复用已经被清除掉的Physical,也即已经没有数据,位点从0开始。 + +#### 备机更新映射 +当前,admin操作都是要求在Master操作的。因此,没有这个问题。 +Command操作时,提前预判Master是否存在,如果不存在,则提前报错,减少中间失败率。 + +#### 拉取消息时 OutOfRange 的判断 +以下情况会影响 OutOfRange 的判断 +* 从备机拉取消息(默认不会返回OFFSET_MOVED) +* 中间 MappingItem 因为Commitlog的提前删除导致 PhysicalQueue Offset Truncation + +所以,OutOfRange 的判断,遵循以下原则: +* 从 Leader Item 拉取,只有requestOffset > maxOffset,尊重 OFFSET_MOVED +* 从 Earliest Item 拉取,只有 requestOffset < minOffset,尊重 OFFSET_MOVED +* 其它情况,都忽略 OFFSET_MOVED + +如果没有恰当地处理 OFFSET_MOVED,可能造成位点被重置。 + +需要注意,这个地方,产生了对 PullMessageResponseHeader 中 minOffset 和 maxOffset 的强依赖。 +在次此之前,这两个信息,只对客户端的限流有作用,对业务没有实际的影响。 + +#### 拉取消息时的 中断问题 +当1个 PhysicalQueue 被拉取干净时,需要修正 nextBeginOffset 到下一个 PhysicalQueue。 +如果没有处理好,则直接会导致拉取中断,无法前进。 +#### pullResult 位点由谁设置的问题 +类似于Batch,由客户端设置,避免服务端解开消息: +在PullResultExt中新增字段 offsetDelta。 +#### Admin接口与User接口的适配方式区别 +User 接口,使用范围广泛如多语言等,应该尽可能简单,把适配逻辑做在服务端,对『客户端』透明。 +那么 Admin 接口呢,比如 examineTopicStats,适配逻辑是做在『服务端』还是『客户端』? +一个 Admin 接口,通常需要访问所有 Broker 的所有队列。 +如果做在服务端,则可能产生交叉访问,在极端情况下,性能会非常差,举个例子: +100 个 Broker,相互交叉映射过一遍,则Admin客户端首先要向 100 个 Broker 发请求,然后这 100 个 Broker 为了获取远程信息,各自向其余 Broker 发请求。 +其实际网络请求数就是 100 * 100 = 10000 个网络请求。放大效应十分明显。 +同时,考虑到 Admin 接口,使用范围是有限的,无需考虑多语言适配等问题,可以把适配逻辑做在 Admin 客户端。 + +#### 远程读的性能问题 +从实战经验来看,性能损耗几乎不计。 +#### 使用习惯的改变 +利用新的创建命令进行隔离。 +#### 消费SendBack问题 +目前的实现里,消费Send Back,是直接传回Commitlog Pos,这个在LogicQueue里行不通。 +需要修改API,改成传回『Logic Queue Offset』。 +#### 二阶消息的兼容性 +二阶消息,也即『原始消息』存储在『系统Topic』中,需要经过一轮『Read-ReWrite』逻辑才会被用户看见的消息。 +例如,定时消息,事务消息。 +二阶消息需要支持远程读写操作。 +一期的LogicQueue不支持『二阶消息』。 + +### 待完成事项 +#### 阻止旧客户端的请求 +旧的客户端无法解析逻辑路由,但可以识别物理路由。如果错误使用,则会影响映射关系的准确性。 +#### 阻止Pop模式、事务消息、定时消息使用 LogicQueue +不兼容 事务消息和定时消息。 +LogicQueue 当前不支持Pop模式消费。 +#### Nameserver 相关生命周期完善 +目前没有处理Nameserver中Mapping数据的生命周期 +#### ConsumeQueue 的 correctMinOffset 逻辑存在缺陷 +可能导致 LogicQueue 无法清除已经过期的 MappingItem。 +#### getOffsetInQueueByTime 语义有缺陷 +可能导致 LogicQueue 时间搜索不准确。需要专项修复。 +#### Metadata 更新机制 +当前的更新机制太慢。且访问『不存在Broker』时,会频繁访问Nameserver,有打爆Nameserver的风险。 +#### examineConsumeStats 接口获取不到『最近消费时间』 +位点相关的消息可能不在本机,需要远程访问。 +#### resetOffset 需要适配 +当前没有适配。重置位点,可能会得到不符合预期的结果。 +#### MessageQueue 没有被物理清除 +当前只是产生遗留数据,占用一点点存储空间,没有太大影响。 +如果将来要实现 物理 Queue 复用,则需要先完善相关逻辑。 + +### 代码走读要点 +#### Admin 入口 +主要看两个类: +UpdateStaticTopicSubCommand +RemappingStaticTopicSubCommand +#### Metadata 入口 +主要看: +TopicQueueMappingManager +#### Client 入口 +重点关注: +MQClientInstance.updateTopicRouteInfoFromNameServer +#### Server 入口 +以 SendMessageProcessor 为例,插桩代码普遍是以下风格: +``` +TopicQueueMappingContext mappingContext = this.brokerController.getTopicQueueMappingManager().buildTopicQueueMappingContext(requestHeader, true); +RemotingCommand rewriteResult = rewriteRequestForStaticTopic(requestHeader, mappingContext); +if (rewriteResult != null) { + return CompletableFuture.completedFuture(rewriteResult); +} +``` +其它Processor类似 +#### 测试入口 +rocketmq-test模块,statictopic目录。 + + + + + diff --git a/docs/cn/statictopic/The_Scope_Of_Static_Topic.md b/docs/cn/statictopic/The_Scope_Of_Static_Topic.md new file mode 100644 index 00000000000..886e33e2f4c --- /dev/null +++ b/docs/cn/statictopic/The_Scope_Of_Static_Topic.md @@ -0,0 +1,116 @@ +### Version 记录 +| 时间 | 主要内容 | 作者 | +| --- | --- | --- | +| 2021-11-01 | 初稿,探讨Static Topic的Scope视线范围 | dongeforever | + + +中文文档在描述特定专业术语时,仍然使用英文。 + +### 需求背景 +RocketMQ的集群设计,是一个多集群、动态、零耦合的设计,具体体现在以下地方: +- 一个 Nameserver 可以管理多个 Cluster +- Broker 与 Cluster 之间是弱关联,Cluster仅仅只是一个标识符,主要在运维时使用来界定Topic的创建范围 +- 开发用户对 Cluster 无感知 +- 不同 Broker 之间没有任何关联 + +这样的设计,在运维时带来了极大的便利,但也带来了一个问题: +- Topic 的队列数无法固定 + +基于 Logic Queue 技术而实现的 Static Topic,就是用来解决『固定队列数量』的问题。 + +但这个『固定』要到何种范围呢?是一个值得探讨的问题。 + +从理论上可以分析出来,有以下三种情况: +- 单集群固定 +- 多集群固定 +- 全网固定 + +#### 单集群固定 +一个 Static Topic,固定在一个 Cluster 内漂移。 +不同的 Cluster 内,可以拥有相同的 Static Topic。 +对应MessageQueue的Broker 命名规范为: +``` +__logic__{clusterName} +``` +#### 多集群固定 +一个 Static Topic,固定在特定的几个 Cluster 内漂移。 +没有交集的Cluster集合之间,可以拥有相同的 Static Topic。 +对应MessageQueue的Broker 命名规范为: +``` +__logic__{cluster1}_{cluster2}_{xxx} +``` +#### 全网固定 +全网是指『同一个Nameserver内』。 +一个 Static Topic,不与特定Cluster绑定,同一个Nameserver内,全网漂移。 +同一个Nameserver内,只有一个同名的 Static Topic。 +对应MessageQueue的Broker 命名规范为: +``` +__logic__global +``` +#### 为什么要引入Scope +直接全网固定不就好了吗,为啥还要引入Scope呢? +主要原因是,不想完全放弃 RocketMQ 『多集群、动态、零耦合』的设计优势。 +而全网固定,则意味着彻底失去了这个优势。 + +举1个『多活保序』的场景: +- ClusterA 部署在 SiteA 内,创建 Static Topic 『TopicTest』,有50个队列。 +- ClusterB 部署在 SiteB 内,创建 Static Topic 『TopicTest』,有50个队列。 + +对Nameserver稍作修改,支持传入标识符(比如为scope或者unitName),来获取特定范围内的 Topic Route。 + +正常情况下: +- SiteA 的Producer和Consumer 只能看见 ClusterA 的 MessageQueue,brokerName为 "__logic__clusterA"。 +- SiteB 的Producer和Consumer 只能看见 ClusterB 的 MessageQueue,brokerName为 "__logic__clusterB"。 +- 机房内就近访问,且机房内严格保序。 + +假设 SiteA 宕机,此时对Nameserver发指令允许全网读,也即忽略客户端传入的 Scope或者unitName 标识符: +- SiteB 的 Producer 仍然看见并写入 ClusterB 的 MessageQueue,brokerName为 "__logic__clusterB" +- SiteB 的 Consumer 可以同时看见并读取 ClusterA 的 MessageQueue 和 ClusterB MessageQueue, brokerName为 "__logic__clusterB" 和 "__logic__clusterA +- 在这种场景下,Consumer 在消费 ClusterB 数据的同时,同时去消费 ClusterA 未消费完的数据 + +不同地域的客户端,看见不同Scope的元数据,从而访问不同Scope的节点。 + +#### 全球容灾集群 +RocketMQ 多个集群的元数据可以无缝在Nameserver处汇聚,同时又可以无缝地根据标识符拆分给不同地域的Producer和Consumer。 +这样一个『元数据可分可合』的设计优势,是其它消息中间件所不具备的,应该值得挖掘一下。 +引入以下概念: +- 融合集群,共享同一个Nameserver的集群之和 +- 单元集群,clusterName名字一样的集群,不同单元集群之间,物理隔离 +- namespace,租户,逻辑隔离,只是命名的区别 + +如果单元集群部署在异地,所形成的『融合集群』,就是全球容灾集群: +- 客户端引入 scope 或者 unitName 字段,默认情况,不同 scope或者unitName 获取的都是单元集群的元数据 +- 顺序性,关键在于 固定Producer端可见的队列,单元内的队列是固定的,因此可以保证单元内是顺序的 +- Consumer 端按照队列消费,天然是顺序的 +- 正常情况下,单元内封闭,也即单元产生的数据在同单元内消费掉 +- 灾难发生时,改变元数据的可见性,允许读其它 单元集群 未消费完的数据,也即跨单元读 +- 跨单元读,是指读『其它clusterName』的队列,不一定是远程读,如果本单元有相应的Slave节点,则直接本地读 + +### 设计目标 +Static Topic 实现 单集群固定 和 全网固定 两种Scope,以适配『全球容灾集群』。 +多集群,暂时没有必要。 + +一期只实现 全网固定 这个Scope,但在格式上注意兼容 + +#### SOT 增加 Scope 字段 +``` +{ +"version":"1", +"scope": "clusterA", +"bname": "broker02" //标记这份数据的原始存储位置,如果发送误拷贝,可以利用这个字段来进行标识 +"epoch": 0, //标记修改版本,用来做一致性校验 +"totalQueues":"50", //当前Topic 总共有多少 LogicQueues +} +``` + +scope字段: +- 单集群固定,则就是 Cluster 名字 +- 全网固定,则为常量『global』 + + + + + + + + diff --git a/docs/en/CLITools.md b/docs/en/CLITools.md new file mode 100644 index 00000000000..208ae44f35d --- /dev/null +++ b/docs/en/CLITools.md @@ -0,0 +1,1248 @@ +# Instructions on the use of mqadmin Management tools + +Before introducing the mqadmin management tool, the following points need to be declared: + +- The way of executing a command is:./mqadmin {command} {args} +- Almost all commands need to attach the -n option to represent the nameServer address, formatted as ip:port; +- Almost all commands can get help information with the -h option; +- If the broker address -b option and clusterName -c option are both configured with specific values, the command execution will select the broker address specified by -b option. The value of the -b option can only be configured with a single address. The format is ip:port. The default port value is 10911. If the value of the -b option is not configured, the command will be applied to all brokers in the entire cluster. +- You can see many commands under tools, but not all commands can be used, only the commands initialized in MQAdminStartup can be used, you can also modify this class, add or customize commands; +- Due to the issue of version update, a small number of commands may not be updated in time, please read the related command source code to eliminate and resolve the error. + +## 1 Topic related command instructions + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameMeaningCommand optionExplain
    updateTopicCreate or update the configuration of topic-bThe -b option declares the specific address of the broker, indicating that the broker, in which the topic is located supports only a single broker and the address format is ip:port.
    -cThe -c option declares the name of the cluster, which represents the cluster in which the current topic is located. (clusters are available through clusterList query)
    -hPrint help information
    -nDeclare the service address of the nameServer, and the option format is ip:port
    -pThe -p option is used to specify the read and write permission for the new topic (W=2 | R=4 | WR=6)
    -rThe -r option declares the number of readable queues (default 8)
    -wThe -w option declares the number of writable queues (default 8)
    -tThe -t option declares the name of the topic (the name can only use characters^ [a-zA-Z0-9s -] + $)
    deleteTopicDelete the topic command-cThe -c option specifies the name of the cluster, which means that one of the topic in the specified cluster is deleted (cluster names can be queried via clusterList)
    -hPrint help information
    -nDeclare the service address of the nameServer, and the option format is ip:port
    -tThe -t option declares the name of the topic (the name can only use characters^ [a-zA-Z0-9s -] + $)
    topicListView topic list information-hPrint help information
    -cIf the -c option is not configured, only the topic list is returned, and the addition of -c option returns additional information about the clusterName, topic, consumerGroup, that is, the cluster and subscription to which the topic belongs, and no other option need to be configured.
    -nDeclare the service address of the nameServer, and the option format is ip:port
    topicRouteTo view topic specific routing information-tUsed to specify the name of the topic
    -hPrint help information
    -nDeclare the service address of the nameServer, and the option format is ip:port
    topicStatusThe location of the offset used to view the topic message queue-tUsed to specify the name of the topic
    -hPrint help information
    -nDeclare the service address of the nameServer, and the option format is ip:port
    topicClusterListTo view the list of clusters to which topic belongs-tUsed to specify the name of the topic
    -hPrint help information
    -nDeclare the service address of the nameServer, and the option format is ip:port
    updateTopicPermThis command is used to update read and write permissions for topic-tUsed to specify the name of the topic
    -hPrint help information
    -nDeclare the service address of the nameServer, and the option format is ip:port
    -bThe -b option declares the specific address of the broker, indicating that the broker, in which the topic is located supports only a single broker and the address format is ip:port.
    -pThe -p option is used to specify the read and write permission for the new topic (W=2 | R=4 | WR=6)
    -cUsed to specify the name of the cluster that represents the cluster in which the topic is located, which can be accessed through the clusterList query, but the -b parameter has a higher priority, and if no -b option related configuration is specified, the command is executed on all broker in the cluster
    updateOrderConfThe key, value configuration that creates, deletes, and retrieves specific namespaces from nameServer is not yet enabled.-hPrint help information
    -nDeclare the service address of the nameServer, and the option format is ip:port
    -ttopic, key
    -vorderConf, value
    -mmethod, available values include get, put, delete
    allocateMQComputing load result of load message queue in consumer list with average load algorithm-tUsed to specify the name of the topic
    -hPrint help information
    -nDeclare the service address of the nameServer, and the option format is ip:port
    -iIpList, is separated by commas to calculate which message queues these ip unload topic
    statsAllFor printing topic subscription, TPS, cumulative amount, 24 hours read and write total, etc.-hPrint help information
    -nDeclare the service address of the nameServer, and the option format is ip:port
    -aWhether to print only active topic
    -tUsed to specify the name of the topic
    + + + + + + + +## 2 Cluster related command instructions + +#### + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameMeaningCommand optionExplain
    clusterListView cluster information, cluster, brokerName, brokerId, TPS, and so on-mPrint more information (add print to # InTotalYest, + #OutTotalYest, #InTotalToday ,#OutTotalToday)
    -hPrint help information
    -nService address used to specify nameServer and formatted as ip:port
    -iPrint interval, unit basis is seconds
    clusterRTSend message to detect each broker RT of the cluster.the message send to ${BrokerName} Topic-aamount, total number per probe, RT = Total time/amount
    -sMessage size, unit basis is B
    -cWhich cluster to detect.
    -pWhether to print the formatted log,split with "|", not printed by default
    -hPrint help information
    -mOwned computer room for printing
    -iThe interval, in seconds, at which a message is sent.
    -nService address used to specify nameServer and formatted as ip:port
    + + + + + +## 3 Broker related command instructions + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameMeaningCommand optionExplain
    updateBrokerConfigThe configuration information used to update the broker and the contents of the Broker.conf file are modified-bDeclare the address of the broker and format as ip:port
    -cSpecify the name of the cluster
    -kthe value of k
    -vthe value of value
    -hPrint help information
    -nService address used to specify nameServer and formatted as ip:port
    brokerStatusFor viewing broker related statistics and running status (almost all the information you want is inside)-bDeclare the address of the broker and format as ip:port
    -hPrint help information
    -nService address used to specify nameServer and formatted as ip:port
    brokerConsumeStatsGet the consumption of each consumer in broker and return information such as consume Offset,broker Offset,diff,timestamp by message queue dimension-bDeclare the address of the broker and format as ip:port
    -tConfigure the timeout of the request
    -lConfigure the diff threshold beyond which to print
    -oSpecifies whether the order topic, is typically false
    -hPrint help information
    -nService address used to specify nameServer and formatted as ip:port
    getBrokerConfigGet configuration information for the broker-bDeclare the address of the broker and format as ip:port
    -nService address used to specify nameServer and formatted as ip:port
    wipeWritePermClear write permissions for broker from nameServer-bDeclare the BrokerName
    addWritePermAdd write permissions for broker from nameServer-bDeclare the BrokerName
    -nService address used to specify nameServer and formatted as ip:port
    -hPrint help information
    cleanExpiredCQClean up expired consume Queue on broker, An expired queue may be generated if the number of columns is reduced manually-nService address used to specify nameServer and formatted as ip:port
    -hPrint help information
    -bDeclare the address of the broker and format as ip:port
    -cUsed to specify the name of the cluster
    deleteExpiredCommitLogClean up expired CommitLog files on broker. A maximum of 20 deletion operations can be performed, and a maximum of 10 files can be deleted each time.-nService address used to specify nameServer and formatted as ip:port
    -hPrint help information
    -bDeclare the address of the broker and format as ip:port
    -cUsed to specify the name of the cluster
    cleanUnusedTopicClean up unused topic on broker and release topic's consume Queue from memory, If the topic is removed manually, an unused topic will be generated-nService address used to specify nameServer and formatted as ip:port
    -hPrint help information
    -bDeclare the address of the broker and format as ip:port
    -cUsed to specify the name of the cluster
    sendMsgStatusSend a message to the broker and then return the send status and RT-nService address used to specify nameServer and formatted as ip:port
    -hPrint help information
    -bbrokerName, note that this is not broker's address
    -sMessage size, the unit of account is B
    -cNumber of messages sent
    + + + +## 4 Message related command instructions + +#### + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameMeaningCommand optionExplain
    queryMsgByIdQuery msg according to offsetMsgId. If you use open source console, you should use offsetMsgId. There are other parameters for this command. For details, please read QueryMsgByIdSubCommand. +-imsgId
    -hPrint help information
    -nService address used to specify nameServer and formatted as ip:port
    queryMsgByKeyQuery messages based on message Key-kmsgKey
    -tThe name of the topic
    -hPrint help information
    -nService address used to specify nameServer and formatted as ip:port
    queryMsgByOffsetQuery messages based on Offset-bThe name of broker,(Note here: the name of broker is filled in, not the address of broker, and the broker name can be found in clusterList)
    -iQueue id of the query
    -oThe value of offset
    -tThe name of the topic
    -hPrint help information
    -nService address used to specify nameServer and formatted as ip:port
    queryMsgByUniqueKeyAccording to the msgId query, msgId is different from offsetMsgId. The specific differences can be found in common operational and maintenance problems. "-g" option and "-d" option are to be used together, and when you find the message, try to get a particular consumer to consume the message and return the result of the consumption.-hPrint help information
    -nService address used to specify nameServer and formatted as ip:port
    -iunique msg id
    -gconsumerGroup
    -dclientId
    -tThe name of the topic
    checkMsgSendRTDetect RT to send a message to topic, function similar to clusterRT-hPrint help information
    -nService address used to specify nameServer and formatted as ip:port
    -tThe name of the topic
    -athe number of probes
    -sThe size of message
    sendMessageSend a message that can be sent, as configured, to a particular message Queue, or to a normal send.-hPrint help information
    -nService address used to specify nameServer and formatted as ip:port
    -tThe name of the topic
    -pbody, message body
    -kkeys
    -ctags
    -bbrokerName
    -iqueueId
    consumeMessageConsumer messages. You can consume messages based on offset, start timestamps, end timestamps, message queues, and configure different consumption logic for different execution, as detailed in ConsumeMessageCommand.-hPrint help information
    -nService address used to specify nameServer and formatted as ip:port
    -tThe name of the topic
    -bbrokerName
    -oStart consumption from offset
    -iqueueId
    -gGroup of consumers
    -sSpecify a start timestamp in a format see -h
    -dSpecify a end timestamp
    -cSpecify how many messages to consume
    printMsgConsume messages from broker and print them, optional time periods-hPrint help information
    -nService address used to specify nameServer and formatted as ip:port
    -tThe name of the topic
    -cCharacter set, for example UTF-8
    -ssubExpress, filter expression
    -bSpecify a start timestamp in a format see -h
    -eSpecify the end timestamp
    -dWhether to print the message body
    printMsgByQueueSimilar to printMsg, but specifying message queue-hPrint help information
    -nService address used to specify nameServer and formatted as ip:port
    -tThe name of the topic
    -iqueueId
    -abrokerName
    -cCharacter set, for example UTF-8
    -ssubExpress, filter expression
    -bSpecify a start timestamp in a format see -h
    -eSpecify the end timestamp
    -pWhether to print a message
    -dWhether to print the message body
    -fWhether to count the number of tags and print +
    resetOffsetByTimeReset consumer offset by timestamp(without client restart).-hPrint help information
    -nService address used to specify nameServer and formatted as ip:port
    -gGroup of consumers
    -tThe name of the topic
    -sResets the offset corresponding to this timestamp in a format see -h, if you want to reset to maxOffset, the value is 'now'.
    -fWhether to force a reset, if set to false, only supports backtracking offset, if it is true, regardless of the relationship between offset and consume Offset with the timestamp
    -cWhether to reset the C++ client offset
    + + + +## 5 Consumer and Consumer Group related command instructions + +#### + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameMeaningCommand optionExplain
    consumerProgressTo view the subscriber consumption status, you can see the amount of message accumulation for a specific client IP-gThe group name of consumer
    -sWhether to print client IP
    -hPrint help information
    -nService address used to specify nameServer and formatted as ip:port
    consumerStatusSee the consumer status, including whether the same subscription is in the same group, analyze whether the process queue is stacked, return the consumer jstack results, more content, and see ConsumerStatusSubCommand for the user-hPrint help information
    -nService address used to specify nameServer and formatted as ip:port
    -gconsumer group
    -iclientId
    -sWhether to execute jstack
    getConsumerStatusGet Consumer consumption progress-gthe group name of consumer
    -tQuery topic
    -iIp address of consumer client
    -nService address used to specify nameServer and formatted as ip:port
    -hPrint help information
    updateSubGroupUpdate or create a subscription-nService address used to specify nameServer and formatted as ip:port
    -hPrint help information
    -bthe address of broker
    -cThe name of cluster
    -gThe group name of consumer
    -sWhether the group is allowed to consume
    -mWhether to start consumption from the minimum offset
    -dIs it a broadcast mode
    -qThe Number of retry queues
    -rMaximum number of retries
    -iWhen the slaveReadEnable is on and which brokerId consumption is recommended for consumption from slave, the brokerid of slave, can be configured to consume from the slave actively
    -wIf broker recommends consumption from slave, configuration determines which slave consumption to consume from, and configure a specific brokerId, such as 1
    -aWhether to notify other consumers of load balancing when the number of consumers changes
    deleteSubGroupRemove subscriptions from broker-nService address used to specify nameServer and formatted as ip:port
    -hPrint help information
    -bthe address of broker
    -cThe name of cluster
    -gThe group name of consumer
    cloneGroupOffsetUse the offset of the source group in the target group +-nService address used to specify nameServer and formatted as ip:port
    -hPrint help information
    -sSource consumer group
    -dTarget consumer group
    -tThe name of topic
    -oNot used yet
    + + + + +## 6 Connection related command instructions + +#### + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameMeaningCommand optionExplain
    consumerConnectionQuery the network connection of consumer-gThe group name of consumer
    -nService address used to specify nameServer and formatted as ip:port
    -hPrint help information
    producerConnectionQuery the network connection of producer-gthe group name of producer
    -tThe name of topic
    -nService address used to specify nameServer and formatted as ip:port
    -hPrint help information
    + + + + +## 7 NameServer related command instructions + +#### + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameMeaningCommand optionExplain
    updateKvConfigUpdate the kv configuration of nameServer, which is not currently used-sSpecify a specific namespace
    -kkey
    -vvalue
    -nService address used to specify nameServer and formatted as ip:port
    -hPrint help information
    deleteKvConfig Delete the kv configuration of nameServer-sSpecify a specific namespace
    -kkey
    -nService address used to specify nameServer and formatted as ip:port
    -hPrint help information
    getNamesrvConfigGet the configuration of the nameServer-nService address used to specify nameServer and formatted as ip:port
    -hPrint help information
    updateNamesrvConfigModifying the configuration of nameServer-nService address used to specify nameServer and formatted as ip:port
    -hPrint help information
    -kThe value of key
    -vThe value of value
    + + + + +## 8 Other relevant command notes + +#### + + + + + + + + + + + + + + + + + + + + + + +
    NameMeaningCommand optionExplain
    startMonitoringUsed to start the monitoring process, monitor message deletion, retry queue messages, etc.-nService address used to specify nameServer and formatted as ip:port
    -hPrint help information
    + + diff --git a/docs/en/Concept.md b/docs/en/Concept.md new file mode 100644 index 00000000000..03f23842934 --- /dev/null +++ b/docs/en/Concept.md @@ -0,0 +1,42 @@ +# Basic Concept + +## 1 Message Model + +RocketMQ message model is mainly composed of Producer, Broker and Consumer. The producer is responsible for producing messages and the consumer is for consuming messages, while the broker stores messages. +The broker is an independent server during actual deployment, and each broker can store messages from multiple topics. Even messages from the same topic can be stored in the different brokers by sharding strategy. +The message queue is used to store physical offsets of messages, and the message addresses are stored in separate queues. The consumer group consists of multiple consumer instances. +## 2 Producer +The Producer is responsible for producing messages, typically by business systems. It sends messages generated by the systems to brokers. RocketMQ provides multiple paradigms of sending: synchronous, asynchronous, sequential and one-way. Both synchronous and asynchronous methods require the confirmation information return from the Broker, but one-way method does not require it. +## 3 Consumer +The Consumer is responsible for consuming messages, typically the background system is responsible for asynchronous consumption. The consumer pulls messages from brokers and feeds them into application. From the perspective of user, two types of consumers are provided: pull consumer and push consumer. +## 4 Topic +The Topic refers to a collection of one kind of message. Each topic contains several messages and one message can only belong to one topic. The topic is the basic unit of RocketMQ for message subscription. +## 5 Broker Server +As the role of the transfer station, the Broker Server stores and forwards messages. In RocketMQ, the broker server is responsible for receiving messages sent from producers, storing them and preparing to handle pull requests. It also stores the related message meta data, including consumer groups, consuming progress, topics, queues info and so on. +## 6 Name Server +The Name Server serves as the provider of routing service. The producer or the consumer can find the list of broker IP addresses for each topic through name server. Multiple name servers can be deployed in one cluster, but they are independent of each other and do not exchange information. +## 7 Pull Consumer +A type of Consumer, the application pulls messages from brokers by actively invoking the consumer pull message method, and the application has the advantages of controlling the timing and frequency of pulling messages. Once the batch of messages is pulled, user application will initiate consuming process. +## 8 Push Consumer +A type of Consumer, the application do not invoke the consumer pull message method to pull messages, instead the client invoke pull message method itself. At the user level it seems like brokers +push to consumer when new messages arrived. +## 9 Producer Group +A collection of the same type of Producer, which sends the same type of messages with consistent logic. If a transaction message is sent and the original producer crashes after sending, the broker server will contact other producers in the same producer group to commit or rollback the transactional message. +## 10 Consumer Group +A collection of the same type of Consumer, which consume the same type of messages with consistent logic. The consumer group makes load-balance and fault-tolerance super easy in terms of message consuming. +Warning: consumer instances of one consumer group must have exactly the same topic subscription(s). + +RocketMQ supports two types of consumption mode:Clustering and Broadcasting. +## 11 Consumption Mode - Clustering +Under the Clustering mode, all the messages from one topic will be delivered to all the consumers instances averagely as much as possible. That is, one message can be consumed by only one consumer instance. +## 12 Consumption Mode - Broadcasting +Under the Broadcasting mode, each consumer instance of the same consumer group receives every message published to the corresponding topic. +## 13 Normal Ordered Message +Under the Normal Ordered Message mode, the messages received by consumers from the same ConsumeQueue are sequential, but the messages received from the different message queues may be non-sequential. +## 14 Strictly Ordered Message +Under the Strictly Ordered Message mode, all messages received by the consumers from the same topic are sequential as the order they are stored. +## 15 Message +The physical carrier of information transmitted by a messaging system, the smallest unit of production and consumption data, each message must belong to one topic. +Each Message in RocketMQ has a unique message id and can carry a key used to store business-related value. The system has the function to query messages by its id or key. +## 16 Tag +Flags set for messages to distinguish different types of messages under the same topic, functioning as a "sub-topic". Messages from the same business unit can set different tags under the same topic in terms of different business purposes. The tag can effectively maintain the clarity and consistency of the code and optimize the query system provided by RocketMQ. The consumer can realize different "sub-topic" by using tag in order to achieve better expandability. diff --git a/docs/en/Configuration_Client.md b/docs/en/Configuration_Client.md new file mode 100644 index 00000000000..4d999b2feda --- /dev/null +++ b/docs/en/Configuration_Client.md @@ -0,0 +1,119 @@ +## Client Configuration + + Relative to RocketMQ's Broker cluster, producers and consumers are client. In this section, it mainly describes the common behavior configuration of producers and consumers. +​ +### 1 Client Addressing mode + +```RocketMQ``` can let client find the ```Name Server```, and then find the ```Broker```by the ```Name Server```. Followings show a variety of configurations, and priority level from highly to lower, the highly priority configurations can override the lower priority configurations. + +- Specified ```Name Server``` address in the code, and multiple ```Name Server``` addresses are separated by semicolons + +```java +producer.setNamesrvAddr("192.168.0.1:9876;192.168.0.2:9876"); + +consumer.setNamesrvAddr("192.168.0.1:9876;192.168.0.2:9876"); +``` +- Specified ```Name Server``` address in the Java setup parameters + +```text +-Drocketmq.namesrv.addr=192.168.0.1:9876;192.168.0.2:9876 +``` +- Specified ```Name Server``` address in the environment variables + +```text +export NAMESRV_ADDR=192.168.0.1:9876;192.168.0.2:9876 +``` +- HTTP static server addressing(default) + +After client started, it will access the http static server address, as: , this URL return the following contents: + +```text +192.168.0.1:9876;192.168.0.2:9876 +``` +By default, the client accesses the HTTP server every 2 minutes, and update the local Name Server address.The URL is hardcoded in the code, you can change the target server by updating ```/etc/hosts``` file, such as add following configuration at the ```/etc/hosts```: +```text +10.232.22.67 jmenv.tbsite.net +``` +HTTP static server addressing is recommended, because it is simple client deployment, and the Name Server cluster can be upgraded hot. + +### 2 Client Configuration + +```DefaultMQProducer```,```TransactionMQProducer```,```DefaultMQPushConsumer```,```DefaultMQPullConsumer``` all extends the ```ClientConfig``` Class, ```ClientConfig``` as the client common configuration class. Client configuration style like getXXX,setXXX, each of the parameters can config by spring and also config their in the code. Such as the ```namesrvAddr``` parameter: ```producer.setNamesrvAddr("192.168.0.1:9876")```, same with the other parameters. + +#### 2.1 Client Common Configuration + +| Parameter Name | Default Value | Description | +| ----------------------------- | ------- | ------------------------------------------------------------ | +| namesrvAddr | | Name Server address list, multiple NameServer addresses are separated by semicolons | +| clientIP | local IP | Client local ip address, some machines will fail to recognize the client IP address, which needs to be enforced in the code | +| instanceName | DEFAULT | Name of the client instance, Multiple producers and consumers created by the client actually share one internal instance (this instance contains network connection, thread resources, etc.). | +| clientCallbackExecutorThreads | 4 | Number of communication layer asynchronous callback threads | +| pollNameServerInteval | 30000 | Polling the Name Server interval in milliseconds | +| heartbeatBrokerInterval | 30000 | The heartbeat interval, in milliseconds, is sent to the Broker | +| persistConsumerOffsetInterval | 5000 | The persistent Consumer consumes the progress interval in milliseconds | + +#### 2.2 Producer Configuration + +| Parameter Name | Default Value | Description | +| -------------------------------- | ---------------- | ------------------------------------------------------------ | +| producerGroup | DEFAULT_PRODUCER | The name of the Producer group. If multiple producers belong to one application and send the same message, they should be grouped into the same group | +| createTopicKey | TBW102 | When a message is sent, topics that do not exist on the server are automatically created and a Key is specified that can be used to configure the default route to the topic where the message is sent.| +| defaultTopicQueueNums | 4 | The number of default queue when sending messages and auto created topic which not exists the server| +| sendMsgTimeout | 3000 | Timeout time of sending message in milliseconds | +| compressMsgBodyOverHowmuch | 4096 | The message Body begins to compress beyond the size(the Consumer gets the message automatically unzipped.), unit of byte| +| retryAnotherBrokerWhenNotStoreOK | FALSE | If send message and return sendResult but sendStatus!=SEND_OK, Whether to resend | +| retryTimesWhenSendFailed | 2 | If send message failed, maximum number of retries, this parameter only works for synchronous send mode| +| maxMessageSize | 4MB | Client limit message body size, over it may error. Server also limit so need to work with server | +| transactionCheckListener | | The transaction message looks back to the listener, if you want send transaction message, you must setup this +| checkThreadPoolMinSize | 1 | Minimum of thread in thread pool when Broker look back Producer transaction status | +| checkThreadPoolMaxSize | 1 | Maximum of thread in thread pool when Broker look back Producer transaction status | +| checkRequestHoldMax | 2000 | Producer local buffer request queue size when Broker look back Producer transaction status | +| RPCHook | null | This parameter is passed in when the Producer is creating, including the pre-processing before the message sending and the processing after the message response. The user can do some security control or other operations in the first interface.| + +#### 2.3 PushConsumer Configuration + +| Parameter Name | Default Value | Description | +| ---------------------------- | ----------------------------- | ------------------------------------------------------------ | +| consumerGroup | DEFAULT_CONSUMER | Consumer group name. If multi Consumer belong to an application, subscribe the same message and consume logic as the same, they should be gathered together | +| messageModel | CLUSTERING | Message support two mode: cluster consumption and broadcast consumption | +| consumeFromWhere | CONSUME_FROM_LAST_OFFSET | After Consumer started, default consumption from last location, it include two situation: One is last consumption location is not expired, and consumption start at last location; The other is last location expired, start consumption at current queue's first message | +| consumeTimestamp | Half an hour ago | Only consumeFromWhere=CONSUME_FROM_TIMESTAMP, this can work | +| allocateMessageQueueStrategy | AllocateMessageQueueAveragely | Implements strategy of Rebalance algorithms | +| subscription | | subscription relation | +| messageListener | | message listener | +| offsetStore | | Consumption progress store | +| consumeThreadMin | 20 | Minimum of thread in consumption thread pool | +| consumeThreadMax | 20 | Maximum of thread in consumption thread pool | +| | | | +| consumeConcurrentlyMaxSpan | 2000 | Maximum span allowed for single queue parallel consumption | +| pullThresholdForQueue | 1000 | Pull message local queue cache maximum number of messages | +| pullInterval | 0 | Pull message interval, because long polling it is 0, but for flow control, you can set value which greater than 0 in milliseconds | +| consumeMessageBatchMaxSize | 1 | Batch consume message | +| pullBatchSize | 32 | Batch pull message | + +#### 2.4 PullConsumer Configuration + +| Parameter Name | Default Value | Description | +| -------------------------------- | ----------------------------- | ------------------------------------------------------------ | +| consumerGroup | DEFAULT_CONSUMER | Consumer group name. If multi Consumer belong to an application, subscribe the same message and consume logic as the same, they should be gathered together | +| brokerSuspendMaxTimeMillis | 20000 | Long polling, Consumer pull message request suspended for the longest time in the Broker in milliseconds | +| consumerTimeoutMillisWhenSuspend | 30000 | Long polling, Consumer pull message request suspend in the Broker over this time value, client think timeout. Unit is milliseconds | +| consumerPullTimeoutMillis | 10000 | Not long polling, timeout time of pull message in milliseconds | +| messageModel | CLUSTERING | Message support two mode: cluster consumption and broadcast consumption | +| messageQueueListener | | Listening changing of queue | +| offsetStore | | Consumption schedule store | +| registerTopics | | Collection of registered topics | +| allocateMessageQueueStrategy | AllocateMessageQueueAveragely | Implements strategy about Rebalance algorithm | + +#### 2.5 Message Data Structure + +| Field Name | Default Value | Description | +| -------------- | ------ | ------------------------------------------------------------ | +| Topic | null | Required, the name of the topic to which the message belongs | +| Body | null | Required, message body | +| Tags | null | Optional, message tag, convenient for server filtering. Currently only one tag per message is supported | +| Keys | null | Optional, represent this message's business keys, server create hash indexes based keys. After setting, you can find message by ```Topics```,```Keys``` in Console system. Because of hash indexes, please make key as unique as possible, such as order number, goods Id and so on.| +| Flag | 0 | Optional, it is entirely up to the application, and RocketMQ does not intervene | +| DelayTimeLevel | 0 | Optional, message delay level, 0 represent no delay, greater tan 0 can consume | +| WaitStoreMsgOK | TRUE | Optional, indicates whether the message is not answered until the server is down. | + diff --git a/docs/en/Configuration_System.md b/docs/en/Configuration_System.md new file mode 100644 index 00000000000..e263b0c4c28 --- /dev/null +++ b/docs/en/Configuration_System.md @@ -0,0 +1,71 @@ +# The system configuration + +This section focuses on the configuration of the system (JVM/OS) + +## **1 JVM Options** ## + +The latest released version of JDK 1.8 is recommended. Set the same Xms and Xmx value to prevent the JVM from resizing the heap for better performance. A simple JVM configuration is as follows: + + -server -Xms8g -Xmx8g -Xmn4g + +Direct ByteBuffer memory size setting. Full GC will be triggered when the Direct ByteBuffer up to the specified size: + + -XX:MaxDirectMemorySize=15g + +If you don’t care about the boot time of RocketMQ broker, pre-touch the Java heap to make sure that every page will be allocated during JVM initialization is a better choice. Those who don’t care about the boot time can enable it: + + -XX:+AlwaysPreTouch + +Disable biased locking maybe reduce JVM pauses: + + -XX:-UseBiasedLocking + +As for garbage collection, G1 collector with JDK 1.8 is recommended: + + -XX:+UseG1GC -XX:G1HeapRegionSize=16m + -XX:G1ReservePercent=25 + -XX:InitiatingHeapOccupancyPercent=30 + +These GC options looks a little aggressive, but it’s proved to have good performance in our production environment + +Don’t set a too small value for -XX:MaxGCPauseMillis, otherwise JVM will use a small young generation to achieve this goal which will cause very frequent minor GC.So use rolling GC log file is recommended: + + -XX:+UseGCLogFileRotation + -XX:NumberOfGCLogFiles=5 + -XX:GCLogFileSize=30m + +If write GC file will increase latency of broker, consider redirect GC log file to a memory file system: + + -Xloggc:/dev/shm/mq_gc_%p.log123 + +## 2 Linux Kernel Parameters ## + +There is a os.sh script that lists a lot of kernel parameters in folder bin which can be used for production use with minor changes. Below parameters need attention, and more details please refer to documentation for /proc/sys/vm/*. + + + + +- **vm.extra_free_kbytes**, tells the VM to keep extra free memory between the threshold where background reclaim (kswapd) kicks in, and the threshold where direct reclaim (by allocating processes) kicks in. RocketMQ uses this parameter to avoid high latency in memory allocation. (It is specific to the kernel version) + + + +- **vm.min_free_kbytes**, if you set this to lower than 1024KB, your system will become subtly broken, and prone to deadlock under high loads. + + + + + +- **vm.max_map_count**, limits the maximum number of memory map areas a process may have. RocketMQ will use mmap to load CommitLog and ConsumeQueue, so set a bigger value for this parameter is recommended. + + + +- **vm.swappiness**, define how aggressive the kernel will swap memory pages. Higher values will increase aggressiveness, lower values decrease the amount of swap. 10 is recommended for this value to avoid swap latency. + + + +- **File descriptor limits**, RocketMQ needs open file descriptors for files(CommitLog and ConsumeQueue) and network connections. We recommend setting 655350 for file descriptors. + + + +- **Disk scheduler**, the deadline I/O scheduler is recommended for RocketMQ, which attempts to provide a guaranteed latency for requests. + diff --git a/docs/en/Configuration_TLS.md b/docs/en/Configuration_TLS.md new file mode 100644 index 00000000000..445d186d27c --- /dev/null +++ b/docs/en/Configuration_TLS.md @@ -0,0 +1,123 @@ +# TLS Configuration +This section introduce TLS configuration in RocketMQ. + +## 1 Generate Certification Files +User can generate certification files using OpenSSL. Suggested to generate files in Linux. + +### 1.1 Generate ca.pem +```shell +openssl req -newkey rsa:2048 -keyout ca_rsa_private.pem -x509 -days 365 -out ca.pem +``` +### 1.2 Generate server.csr +```shell +openssl req -newkey rsa:2048 -keyout server_rsa.key -out server.csr +``` +### 1.3 Generate server.pem +```shell +openssl x509 -req -days 365 -in server.csr -CA ca.pem -CAkey ca_rsa_private.pem -CAcreateserial -out server.pem +``` +### 1.4 Generate client.csr +```shell +openssl req -newkey rsa:2048 -keyout client_rsa.key -out client.csr +``` +### 1.5 Generate client.pem +```shell +openssl x509 -req -days 365 -in client.csr -CA ca.pem -CAkey ca_rsa_private.pem -CAcreateserial -out client.pem +``` +### 1.6 Generate server.key +```shell +openssl pkcs8 -topk8 -v1 PBE-SHA1-RC4-128 -in server_rsa.key -out server.key +``` +### 1.7 Generate client.key +```shell +openssl pkcs8 -topk8 -v1 PBE-SHA1-RC4-128 -in client_rsa.key -out client.key +``` + +## 2 Create tls.properties +Create tls.properties, correctly configure the path and password of the generated certificates. + +```properties +# The flag to determine whether use test mode when initialize TLS context. default is true +tls.test.mode.enable=false +# Indicates how SSL engine respect to client authentication, default is none +tls.server.need.client.auth=require +# The store path of server-side private key +tls.server.keyPath=/opt/certFiles/server.key +# The password of the server-side private key +tls.server.keyPassword=123456 +# The store path of server-side X.509 certificate chain in PEM format +tls.server.certPath=/opt/certFiles/server.pem +# To determine whether verify the client endpoint's certificate strictly. default is false +tls.server.authClient=false +# The store path of trusted certificates for verifying the client endpoint's certificate +tls.server.trustCertPath=/opt/certFiles/ca.pem +``` + +If you need to authenticate the client connection, you also need to add the following content to the file. + +```properties +# The store path of client-side private key +tls.client.keyPath=/opt/certFiles/client.key +# The password of the client-side private key +tls.client.keyPassword=123456 +# The store path of client-side X.509 certificate chain in PEM format +tls.client.certPath=/opt/certFiles/client.pem +# To determine whether verify the server endpoint's certificate strictly +tls.client.authServer=false +# The store path of trusted certificates for verifying the server endpoint's certificate +tls.client.trustCertPath=/opt/certFiles/ca.pem +``` + + +## 3 Update Rocketmq JVM parameters + +Edit the configuration file under the rocketmq/bin path to make tls.properties configurations take effect. + +The value of "tls.config.file" needs to be replaced by the file path created in step 2. + +### 3.1 Edit runserver.sh +Add following content in JAVA_OPT: +```shell +JAVA_OPT="${JAVA_OPT} -Dtls.server.mode=enforcing -Dtls.config.file=/opt/rocketmq-4.9.3/conf/tls.properties" +``` + +### 3.2 Edit runbroker.sh +Add following content in JAVA_OPT: + +```shell +JAVA_OPT="${JAVA_OPT} -Dorg.apache.rocketmq.remoting.ssl.mode=enforcing -Dtls.config.file=/opt/rocketmq-4.9.3/conf/tls.properties -Dtls.enable=true" +``` + +# 4 Client connection + +Create tlsclient.properties using by client. Add following content: +```properties +# The store path of client-side private key +tls.client.keyPath=/opt/certFiles/client.key +# The password of the client-side private key +tls.client.keyPassword=123456 +# The store path of client-side X.509 certificate chain in PEM format +tls.client.certPath=/opt/certFiles/client.pem +# The store path of trusted certificates for verifying the server endpoint's certificate +tls.client.trustCertPath=/opt/certFiles/ca.pem +``` + +Add following parameters in JVM. The value of "tls.config.file" needs to be replaced by the file path we created: +```properties +-Dtls.client.authServer=true -Dtls.enable=true -Dtls.test.mode.enable=false -Dtls.config.file=/opt/certs/tlsclient.properties +``` + +Enable TLS for client linke following: +```Java +public class ExampleProducer { + public static void main(String[] args) throws Exception { + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + //setUseTLS should be true + producer.setUseTLS(true); + producer.start(); + + // Send messages as usual. + producer.shutdown(); + } +} +``` diff --git a/docs/en/Debug_In_Idea.md b/docs/en/Debug_In_Idea.md new file mode 100644 index 00000000000..9967980671f --- /dev/null +++ b/docs/en/Debug_In_Idea.md @@ -0,0 +1,55 @@ +## How to Debug RocketMQ in Idea + +### Step0: Resolve dependencies +1. To download the Maven dependencies required for running RocketMQ, you can use the following command:`mvn clean install -Dmaven.test.skip=true` +2. Ensure successful local compilation. + +### Step1: Start NameServer +1. The startup class for NameServer is located in `org.apache.rocketmq.namesrv.NamesrvStartup`. +2. Add runtime `ROCKETMQ_HOME=` parameters in `Idea-Edit Configurations`. +![Idea_config_nameserver.png](../cn/image/Idea_config_nameserver.png) +3. Run NameServer and if the following log output is observed, it indicates successful startup. +```shell +The Name Server boot success. serializeType=JSON, address 0.0.0.0:9876 +``` + +### Step2: Start Broker +1. The startup class for Broker is located in`org.apache.rocketmq.broker.BrokerStartup` +2. Create the `/rocketmq/conf/broker.conf` file or simply copy it from the official release package. +```shell +# broker.conf + +brokerClusterName = DefaultCluster +brokerName = broker-a +brokerId = 0 +deleteWhen = 04 +fileReservedTime = 48 +brokerRole = ASYNC_MASTER +flushDiskType = ASYNC_FLUSH +namesrvAddr = 127.0.0.1:9876 # name server地址 +``` +3. Add the runtime parameter `ROCKETMQ_HOME=` and the environment variable `-c /Users/xxx/rocketmq/conf/broker.conf` in `Idea-Edit Configurations`. +![Idea_config_broker.png](../cn/image/Idea_config_broker.png) +4. Run the Broker and if the following log is observed, it indicates successful startup. +```shell +The broker[broker-a,192.169.1.2:10911] boot success... +``` + +### Step3: Send or Consume Messages +RocketMQ startup is now complete. You can use the examples provided in `/example` to send and consume messages. + +### Additional: Start the Proxy locally. +1. RocketMQ 5.x introduced the Proxy mode. Using the `LOCAL` mode eliminates the need for `Step2`. The startup class is located at `org.apache.rocketmq.proxy.ProxyStartup`. +2. Add the runtime parameter `ROCKETMQ_HOME=` in `Idea-Edit Configurations`. +3. Create a new configuration file named `rmq-proxy.json` in the `/conf/` directory. +```json +{ + "rocketMQClusterName": "DefaultCluster", + "nameSrvAddr": "127.0.0.1:9876", + "proxyMode": "local" +} +``` +4. Run the Proxy, and if the following log is observed, it indicates successful startup. +```shell +Sat Aug 26 15:29:33 CST 2023 rocketmq-proxy startup successfully +``` \ No newline at end of file diff --git a/docs/en/Deployment.md b/docs/en/Deployment.md new file mode 100644 index 00000000000..5dc93488a77 --- /dev/null +++ b/docs/en/Deployment.md @@ -0,0 +1,159 @@ +# Deployment Architectures and Setup Steps + +## Cluster Setup + +### 1 Single Master mode + +This is the simplest, but also the riskiest mode, that makes the entire service unavailable once the broker restarts or goes down. Production environments are not recommended, but can be used for local testing and development. Here are the steps to build. + +**1)Start NameServer** + +```shell +### Start Name Server first +$ nohup sh mqnamesrv & + +### Then verify that the Name Server starts successfully +$ tail -f ~/logs/rocketmqlogs/namesrv.log +The Name Server boot success... +``` + +We can see 'The Name Server boot success.. ' in namesrv.log that indicates the NameServer has been started successfully. + +**2)Start Broker** + +```shell +### Also start broker first +$ nohup sh bin/mqbroker -n localhost:9876 & + +### Then verify that the broker is started successfully, for example, the IP of broker is 192.168.1.2 and the name is broker-a +$ tail -f ~/logs/rocketmqlogs/Broker.log +The broker[broker-a,192.169.1.2:10911] boot success... +``` + +We can see 'The broker[brokerName,ip:port] boot success..' in Broker.log that indicates the broker has been started successfully. + +### 2 Multiple Master mode + +Multiple master mode means a mode with all master nodes(such as 2 or 3 master nodes) and no slave node. The advantages and disadvantages of this mode are as follows: + +- Advantages: + 1. Simple configuration. + 2. Outage or restart(for maintenance) of one master node has no impact on the application. + 3. When the disk is configured as RAID10, messages are not lost because the RAID10 disk is very reliable, even if the machine is not recoverable (In the case of asynchronous flush disk mode of the message, a small number of messages are lost; If the brush mode of a message is synchronous, no message will be lost). + 4. In this mode, the performance is the highest. +- Disadvantages: + 1. During a single machine outage, messages that are not consumed on this machine are not subscribed to until the machine recovers, and message real-time is affected. + +The starting steps for multiple master mode are as follows: + +**1)Start NameServer** + +```shell +### Start Name Server first +$ nohup sh mqnamesrv & + +### Then verify that the Name Server starts successfully +$ tail -f ~/logs/rocketmqlogs/namesrv.log +The Name Server boot success... +``` + +**2)Start the Broker cluster** + +```shell +### For example, starting the first Master on machine A, assuming that the configured NameServer IP is: 192.168.1.1. +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-noslave/broker-a.properties & + +### Then starting the second Master on machine B, assuming that the configured NameServer IP is: 192.168.1.1. +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-noslave/broker-b.properties & + +... +``` + +The boot command shown above is used in the case of a single NameServer.For clusters of multiple NameServer, the address list after the -n argument in the broker boot command is separated by semicolons, for example, 192.168.1.1: 9876;192.161.2: 9876. + +### 3 Multiple Master And Multiple Slave Mode-Asynchronous replication + +Each master node configures more than one slave nodes, with multiple pairs of master-slave.HA uses asynchronous replication, with a short message delay (millisecond) between master node and slave node.The advantages and disadvantages of this mode are as follows: + +- Advantages: + 1. Even if the disk is corrupted, very few messages will be lost and the real-time performance of the message will not be affected. + 2. At the same time, when master node is down, consumers can still consume messages from slave node, and the process is transparent to the application itself and does not require human intervention. + 3. Performance is almost as high as multiple master mode. +- Disadvantages: + 1. A small number of messages will be lost when master node is down and the disk is corrupted. + +The starting steps for multiple master and multiple slave mode are as follows: + +**1)Start NameServer** + +```shell +### Start Name Server first +$ nohup sh mqnamesrv & + +### Then verify that the Name Server starts successfully +$ tail -f ~/logs/rocketmqlogs/namesrv.log +The Name Server boot success... +``` + +**2)Start the Broker cluster** + +```shell +### For example, starting the first Master on machine A, assuming that the configured NameServer IP is: 192.168.1.1 and port is 9876. +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-async/broker-a.properties & + +### Then starting the second Master on machine B, assuming that the configured NameServer IP is: 192.168.1.1 and port is 9876. +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-async/broker-b.properties & + +### Then starting the first Slave on machine C, assuming that the configured NameServer IP is: 192.168.1.1 and port is 9876. +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-async/broker-a-s.properties & + +### Last starting the second Slave on machine D, assuming that the configured NameServer IP is: 192.168.1.1 and port is 9876. +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-async/broker-b-s.properties & +``` + +The above shows a startup command for 2M-2S-Async mode, similar to other nM-nS-Async modes. + +### 4 Multiple Master And Multiple Slave Mode-Synchronous dual write + +In this mode, multiple slave node are configured for each master node and there are multiple pairs of Master-Slave.HA uses synchronous double-write, that is, the success response will be returned to the application only when the message is successfully written into the master node and replicated to more than one slave node. + +The advantages and disadvantages of this model are as follows: + +- Advantages: + 1. Neither the data nor the service has a single point of failure. + 2. In the case of master node shutdown, the message is also undelayed. + 3. Service availability and data availability are very high; +- Disadvantages: + 1. The performance in this mode is slightly lower than in asynchronous replication mode (about 10% lower). + 2. The RT sending a single message is slightly higher, and the current version, the slave node cannot automatically switch to the master after the master node is down. + +The starting steps are as follows: + +**1)Start NameServer** + +```shell +### Start Name Server first +$ nohup sh mqnamesrv & + +### Then verify that the Name Server starts successfully +$ tail -f ~/logs/rocketmqlogs/namesrv.log +The Name Server boot success... +``` + +**2)Start the Broker cluster** + +```shell +### For example, starting the first Master on machine A, assuming that the configured NameServer IP is: 192.168.1.1 and port is 9876. +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-sync/broker-a.properties & + +### Then starting the second Master on machine B, assuming that the configured NameServer IP is: 192.168.1.1 and port is 9876. +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-sync/broker-b.properties & + +### Then starting the first Slave on machine C, assuming that the configured NameServer IP is: 192.168.1.1 and port is 9876. +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-sync/broker-a-s.properties & + +### Last starting the second Slave on machine D, assuming that the configured NameServer IP is: 192.168.1.1 and port is 9876. +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-sync/broker-b-s.properties & +``` + +The above Master and Slave are paired by specifying the same config named "brokerName", the "brokerId" of the master node must be 0, and the "brokerId" of the slave node must be greater than 0. \ No newline at end of file diff --git a/docs/en/Design_Filter.md b/docs/en/Design_Filter.md new file mode 100644 index 00000000000..7ccb94fa200 --- /dev/null +++ b/docs/en/Design_Filter.md @@ -0,0 +1,10 @@ +# Message Filter +RocketMQ - a distributed message queue, is different with all other MQ middleware, on the way of filtering messages. It's do the filter when the messages are subscribed via consumer side.RocketMQ do it lies in the separate storage mechanism that Producer side writing messages and Consumer subscribe messages, Consumer side will get an index from a logical message queue ConsumeQueue when subscribing, then read message entity from CommitLog using the index. So in the end, it is still impossible to get around its storage structure.The storage structure of ConsumeQueue is as follows, and there is a 8-byte Message Tag hashcode, The message filter based on Tag value is just used this Message Tag hash-code. + +![](images/rocketmq_design_7.png) + +The RocketMQ has two mainly filter types: + +* Tag filtering: Consumer can specify not only the message topic but also the message tag values, when subscribing. Multiple tag values need to be separated by '||'. When consumer subscribing a message, it builds the subscription request into a `SubscriptionData` object and sends a pull message request to the Broker side. Before the Broker side reads data from the RocketMQ file storage layer - Store, it will construct a `MessageFilter` using the `SubscriptionData` object and then pass it to the Store. Store get a record from `ConsumeQueue`, and it will filter the message by the saved tag hashcode, it is unable to filter the messages exactly in the server side because of only the hashcode will be used when filtering, Therefore, after the Consumer pulls the message, it also needs to compare the original tag string of the message. If the original tag string is not same with the expected, the message will be ignored. + +* SQL92 filtering: This filter behavior is almost same with the above `Tag filtering` method. The only difference is on the way how Store works. The rocketmq-filter module is responsible for the construction and execution of the real SQL expression. Executing an SQL expression every time a filter is executed affects efficiency, so RocketMQ uses BloomFilter to avoid doing it every time. The expression context of SQL92 is a property of the message. diff --git a/docs/en/Design_LoadBlancing.md b/docs/en/Design_LoadBlancing.md new file mode 100644 index 00000000000..86c47b16536 --- /dev/null +++ b/docs/en/Design_LoadBlancing.md @@ -0,0 +1,44 @@ +## Load Balancing +Load balancing in RocketMQ is accomplished on Client side. Specifically, it can be divided into load balancing at Producer side when sending messages and load balancing at Consumer side when subscribing messages. + +### Producer Load Balancing +When the Producer sends a message, it will first find the specified TopicPublishInfo according to Topic. After getting the routing information of TopicPublishInfo, the RocketMQ client will select a queue (MessageQueue) from the messageQueue List in TopicPublishInfo to send the message by default.Specific fault-tolerant strategies are defined in the MQFaultStrategy class. +Here is a sendLatencyFaultEnable switch variable, which, if turned on, filters out the Broker agent of not available on the basis of randomly gradually increasing modular arithmetic selection. The so-called "latencyFault Tolerance" refers to a certain period of time to avoid previous failures. For example, if the latency of the last request exceeds 550 Lms, it will evade 30000 Lms; if it exceeds 1000L, it will evade 60000L; if it is closed, it will choose a queue (MessageQueue) to send messages by randomly gradually increasing modular arithmetic, and the latencyFault Tolerance mechanism is the key to achieve high availability of message sending. + +### Consumer Load Balancing +In RocketMQ, the two consumption modes (Push/Pull) on the Consumer side are both based on the pull mode to get the message, while in the Push mode it is only a kind of encapsulation of the pull mode, which is essentially implemented as the message pulling thread after pulling a batch of messages from the server. After submitting to the message consuming thread pool, it continues to try again to pull the message to the server. If the message is not pulled, the pull is delayed and continues. In both pull mode based consumption patterns (Push/Pull), the Consumer needs to know which message queue - queue from the Broker side to get the message. Therefore, it is necessary to do load balancing on the Consumer side, that is, which Consumer consumption is allocated to the same ConsumerGroup by more than one MessageQueue on the Broker side. + +#### 1 Heartbeat Packet Sending on Consumer side +After Consumer is started, it continuously sends heartbeat packets to all Broker instances in the RocketMQ cluster via timing task (which contains the message consumption group name, subscription relationship collection,Message communication mode and the value of the client id,etc). After receiving the heartbeat message from Consumer, Broker side maintains it in Consumer Manager's local caching variable—consumerTable, At the same time, the encapsulated client network channel information is stored in the local caching variable—channelInfoTable, which can provide metadata information for the later load balancing of Consumer. +#### 2 Core Class for Load Balancing on Consumer side—RebalanceImpl +Starting the MQClientInstance instance in the startup process of the Consumer instance will complete the start of the load balancing service thread-RebalanceService (executed every 20 s). By looking at the source code, we can find that the run () method of the RebalanceService thread calls the rebalanceByTopic () method of the RebalanceImpl class, which is the core of the Consumer end load balancing. Here, rebalanceByTopic () method will do different logical processing depending on whether the consumer communication type is "broadcast mode" or "cluster mode". Here we mainly look at the main processing flow in cluster mode: +##### 1) Get the message consumption queue set (mqSet) under the Topic from the local cache variable—topicSubscribeInfoTable of the rebalanceImpl instance. +##### 2) Call mQClientFactory. findConsumerIdList () method to send a RPC communication request to Broker side to obtain the consumer Id list under the consumer group based on the parameters of topic and consumer group (consumer table constructed by Broker side based on the heartbeat data reported by the front consumer side responds and returns, business request code: GET_CONSUMER_LIST_BY_GROUP); +##### 3) First, the message consumption queue and the consumer Id under Topic are sorted, then the message queue to be pulled is calculated by using the message queue allocation strategy algorithm (default: the average allocation algorithm of the message queue). The average allocation algorithm here is similar to the paging algorithm. It ranks all MessageQueues like records. It ranks all consumers like pages. It calculates the average size of each page and the range of each page record. Finally, it traverses the whole range and calculates the records that the current consumer should allocate to (MessageQueue here). +![Image text](https://github.com/apache/rocketmq/raw/develop/docs/cn/image/rocketmq_design_8.png) + + +##### 4) Then, the updateProcessQueueTableInRebalance () method is invoked, which first compares the allocated message queue set (mqSet) with processQueueTable for filtering. +![Image text](https://github.com/apache/rocketmq/raw/develop/docs/cn/image/rocketmq_design_9.png) + + - The red part of the processQueueTable annotation in the figure above + indicates that it is not included with the assigned message queue set + mqSet. Set the Dropped attribute to true for these queues, and then + check whether these queues can remove the processQueueTable cache + variable or not. The removeUnnecessaryMessageQueue () method is + executed here, that is, check every 1s to see if the locks of the + current consumption processing queue can be retrieved and return true + if they are retrieved. If the lock of the current consumer processing + queue is still not available after waiting for 1s, it returns false. + If true is returned, the corresponding Entry is removed from the + processQueueTable cache variable. + - The green section in processQueueTable above represents the + intersection with the assigned message queue set mqSet. Determine + whether the ProcessQueue has expired, regardless of Pull mode, if it + is Push mode, set the Dropped attribute to true, and call the + removeUnnecessaryMessageQueue () method to try to remove Entry as + above; + +Finally, a ProcessQueue object is created for each MessageQueue in the filtered message queue set (mqSet) and stored in the processQueueTable queue of RebalanceImpl (where the computePullFromWhere (MessageQueue mq) method of the RebalanceImpl instance is invoked to obtain the next progress consumption value offset of the MessageQueue object, which is then populated into the attribute of pullRequest object to be created next time.), and create pull request object—pullRequest to add to pull list—pullRequestList, and finally execute dispatchPullRequest () method. PullRequest object of Pull message is put into the blocking queue pullRequestQueue of PullMessageService service thread in turn, and the request of Pull message is sent to Broker end after the service thread takes out. + +The core design idea of message consumption queue is that a message consumption queue can only be consumed by one consumer in the same consumer group at the same time, and a message consumer can consume multiple message queues at the same time. diff --git a/docs/en/Design_Query.md b/docs/en/Design_Query.md new file mode 100644 index 00000000000..887a073a9d9 --- /dev/null +++ b/docs/en/Design_Query.md @@ -0,0 +1,19 @@ +# Message Queries + +RocketMQ supports message queries by two dimensions, which are "Query Message by Message Id" and "Query Message by Message Key". + +## 1. Query Message by Message Id +The MessageId in RocketMQ has a total length of 16 bytes, including the broker address (IP address and port) and CommitLog offset. In RocketMQ, the specific approach is that the Client resolves the Broker's address (IP address and port) and the CommitLog's offset address from the MessageId. Then both of them are encapsulated into an RPC request, and finally it will be sent through the communication layer (business request code: VIEW_MESSAGE_BY_ID). The Broker reads a message by using the CommitLog offset and size to find the real message in the CommitLog and then return, which is how QueryMessageProcessor works. + +## 2. Query Message by Message Key +"Query Messages by Message Key" is mainly based on RocketMQ's IndexFile. The logical structure of the IndexFile is similar to the implementation of HashMap in JDK. The specific structure of the IndexFile is as follows: + +![](images/rocketmq_design_message_query.png) + +The IndexFile provides the user with the querying service by “Querying Messages by Message Key”. The IndexFile is stored in $HOME\store\index${fileName}, and the file name is named after the timestamp at the time of creation. The file size is fixed, which is 420,000,040 bytes (40+5million\*4+20million\*20). If the UNIQ_KEY is set in the properties of the message, then the "topic + ‘#’ + UNIQ_KEY" will be used as the index. Likewise, if the KEYS is set in the properties of the message (multiple KEYs should be separated by spaces), then the "topic + ‘#’ + KEY" will be used as the index. + +The header of IndexFile contains eight fields, `beginTimestamp`(8 bytes), `endTimestamp`(8 bytes), `beginPhyOffset`(8 bytes), `endPhyOffset`(8 bytes), `hashSlotCount`(4 bytes), and `indexCount`(4 bytes).`beginTimestamp` and `endTimestamp` represents the storeTimestamp of the message corresponding to the first and last index, `beginPhyOffset` and `endPhyOffset` represent the physical offset of the message corresponding to the first and last index. `hashSlotCount` represents the count of hash slot. `indexCount` represents the count of indexes. + +The index data contains four fields, `Key Hash`, `CommitLog offset`, `Timestamp` and `NextIndex offset`, for a total of 20 Bytes. The `NextIndex offset` of the index data will point to the previous index data if the `Key Hash` of the index data is the same as that of the previous index data. If a hash conflict occurs, then the `NextIndex offset` can be used as the field to string all conflicting indexes in a linked list. What the `Timestamp` records is the time difference between the `storeTimestamp` of the message associated with the current key and the `BeginTimestamp` in the `IndexHeader`, instead of a specific time. The structure of the entire IndexFile is shown in the graph. The Header is used to store some general statistics, which needs 40 bytes. The Slot Table of 4\*5million bytes does not save the real index data, but saves the header of the singly linked list corresponding to each slot. The Index Linked List of 20\*20million is the real index data, that is, an Index File can hold 20million indexes. + +The specific method of "Query Message by Message Key" is that the topic and message key are used to find the record in the IndexFile, and then read the message from the file of CommitLog according to the CommitLog offset in this record. \ No newline at end of file diff --git a/docs/en/Design_Remoting.md b/docs/en/Design_Remoting.md new file mode 100644 index 00000000000..46e001a1dc5 --- /dev/null +++ b/docs/en/Design_Remoting.md @@ -0,0 +1,50 @@ + +RocketMQ message queue cluster mainly includes four roles: NameServer, Broker (Master/Slave), Producer and Consumer. The basic communication process is as follows: +(1) After Broker start-up, it needs to complete one operation: register itself to NameServer, and then report Topic routing information to NameServer at regular intervals of 30 seconds. +(2) When message Producer sends a message as a client, it needs to obtain routing information from the local cache TopicPublishInfoTable according to the Topic of the message. If not, it will be retrieved from NameServer and update to local cache, at the same time, Producer will retrieve routing information from NameServer every 30 seconds by default. +(3) Message Producer chooses a queue to send the message according to the routing information obtained in 2); Broker receives the message and records it in disk as the receiver of the message. +(4) After message Consumer gets the routing information according to 2) and complete the load balancing of the client, then select one or several message queues to pull messages and consume them. + +From 1) ~ 3) above, we can see that both Producer, Broker and NameServer communicate with each other(only part of MQ communication is mentioned here), so how to design a good network communication module is very important in MQ. It will determine the overall messaging capability and final performance of the RocketMQ cluster. + +rocketmq-remoting module is the module responsible for network communication in RocketMQ message queue. It is relied on and referenced by almost all other modules (such as rocketmq-client,rocketmq-broker,rocketmq-namesrv) that need network communication. In order to realize the efficient data request and reception between the client and the server, the RocketMQ message queue defines the communication protocol and extends the communication module on the basis of Netty. + +### 1 Remoting Communication Class Structure +![](https://github.com/apache/rocketmq/raw/develop/docs/cn/image/rocketmq_design_3.png) +### 2 Protocol Design and Code +When a message is sent between Client and Server, a protocol convention is needed for the message sent, so it is necessary to customize the message protocol of RocketMQ. At the same time, in order to efficiently transmit messages and read the received messages, it is necessary to encode and decode the messages. In RocketMQ, the RemotingCommand class encapsulates all data content in the process of message transmission, which includes not only all data structures, but also encoding and decoding operations. + +Header field | Type | Request desc | Response desc +--- | --- | --- | --- | +code |int | Request code. answering business processing is different according to different requests code | Response code. 0 means success, and non-zero means errors. +language | LanguageCode | Language implemented by the requester | Language implemented by the responder +version | int | Version of Request Equation | Version of Response Equation +opaque | int |Equivalent to requestId, the different request identification codes on the same connection correspond to those in the response message| The response returns directly without modification +flag | int | Sign, used to distinguish between ordinary RPC or oneway RPC | Sign, used to distinguish between ordinary RPC or oneway RPC +remark | String | Transfer custom text information | Transfer custom text information +extFields | HashMap | Request custom extension information| Response custom extension information +![](https://github.com/apache/rocketmq/raw/develop/docs/cn/image/rocketmq_design_4.png) +From the above figure, the transport content can be divided into four parts: + + (1) Message length: total length, four bytes of storage, occupying an int type; + +(2) Serialization type header length: occupying an int type. The first byte represents the serialization type, and the last three bytes represent the header length; + +(3) Header data: serialized header data; + +(4) Message body data: binary byte data content of message body; +### 3 Message Communication Mode and Procedure +There are three main ways to support communication in RocketMQ message queue: synchronous (sync), asynchronous (async), one-way (oneway). The "one-way" communication mode is relatively simple and is generally used in sending heartbeat packets without paying attention to its Response. Here, mainly introduce the asynchronous communication flow of RocketMQ. +![](https://github.com/apache/rocketmq/raw/develop/docs/cn/image/rocketmq_design_5.png) +### 4 Reactor Multithread Design +The RPC communication of RocketMQ uses Netty component as the underlying communication library, and also follows the Reactor multithread model. At the same time, some extensions and optimizations are made on it. +![](https://github.com/apache/rocketmq/raw/develop/docs/cn/image/rocketmq_design_6.png) +Above block diagram can roughly understand the Reactor multi-thread model of NettyRemotingServer in RocketMQ. A Reactor main thread (eventLoopGroupBoss, is 1 above) is responsible for listening to TCP network connection requests, establishing connections, creating SocketChannel, and registering on selector. The source code of RocketMQ automatically selects NIO and Epoll according to the type of OS. Then listen to real network data. After you get the network data, you throw it to the Worker thread pool (eventLoopGroupSelector, is the "N" above, the default is 3 in the source code). You need to do SSL verification, codec, idle check, network connection management before you really execute the business logic. These tasks to defaultEventExecutorGroup (that is, "M1" above, the default set to 8 in the source code) to do. The processing business operations are executed in the business thread pool. According to the RomotingCommand business request code, the corresponding processor is found in the processorTable local cache variable and encapsulated into the task, and then submitted to the corresponding business processor processing thread pool for execution (sendMessageExecutor,). Take sending a message, for example, the "M2" above. The thread pool continues to increase in several steps from entry to business logic, which is related to the complexity of each step. The more complex the thread pool is, the wider the concurrent channel is required. +Number of thread | Name of thread | Desc of thread + --- | --- | --- +1 | NettyBoss_%d | Reactor Main thread +N | NettyServerEPOLLSelector_%d_%d | Reactor thread pool +M1 | NettyServerCodecThread_%d | Worker thread pool +M2 | RemotingExecutorThread_%d | business processor thread pool + + diff --git a/docs/en/Design_Store.md b/docs/en/Design_Store.md new file mode 100644 index 00000000000..747c98e6fb6 --- /dev/null +++ b/docs/en/Design_Store.md @@ -0,0 +1,39 @@ +# Message Storage + + +![](images/rocketmq_storage_arch.png) + +Message storage is the most complicated and important part of RocketMQ. This section will describe the three aspects of RocketMQ: +* Message storage architecture +* PageCache and memory mapping +* RocketMQ's two different disk flushing methods. + +## 1 Message Storage Architecture + + +The message storage architecture diagram consists of 3 files related to message storage: `CommitLog` file, `ConsumeQueue` file, and `IndexFile`. + + +* `CommitLog`: The `CommitLog` file stores message body and metadata sent by producer, and the message content is not fixed length. The default size of one `CommitLog` file is 1G, the length of the file name is 20 digits, the left side is zero padded, and the remaining is the starting offset. For example, `00000000000000000000` represents the first file, the starting offset is 0, and the file size is 1G=1073741824, when the first `CommitLog` file is full, the second `CommitLog` file is `00000000001073741824`, the starting offset is 1073741824, and so on. The message is mainly appended to the log file sequentially. When one `CommitLog` file is full, the next will be written. +* `ConsumeQueue`: The `ConsumeQueue` is used to improve the performance of message consumption. Since RocketMQ uses topic-based subscription mode, message consumption is specific to the topic. Traversing the commitlog file to retrieve messages of one topic is very inefficient. The consumer can find the messages to be consumed according to the `ConsumeQueue`. The `ConsumeQueue`(logic consume queue) as an index of the consuming message stores the starting physical offset `offset` in `CommitLog` of the specified topic, the message size `size` and the hash code of the message tag. The `ConsumeQueue` file can be regarded as a topic-based `CommitLog` index file, so the consumequeue folder is organized as follows: `topic/queue/file` three-layer organization structure, the specific storage path is `$HOME/store/consumequeue/{topic}/{queueId }/{fileName}`. The consumequeue file uses a fixed-length design, each entry occupies 20 bytes, which is an 8-byte commitlog physical offset, a 4-byte message length, and an 8-byte tag hashcode. One consumequeue file consists of 0.3 million entries, each entry can be randomly accessed like an array, each `ConsumeQueue` file's size is about 5.72MB. +* `IndexFile`: The `IndexFile` provides a way to query messages by key or time interval. The path of the `IndexFile` is `$HOME/store/index/${fileName}`, the file name `fileName` is named after the timestamp when it was created. One IndexFile's size is about 400M, and it can store 2000W indexes. The underlying storage of `IndexFile` is designed to implement the `HashMap` structure in the file system, so RocketMQ's index file is a hash index. + + +From the above architecture of the RocketMQ message storage, we can see RocketMQ uses a hybrid storage structure, that is, all the queues in an instance of the broker share a single log file `CommitLog` to store messages. RocketMQ's hybrid storage structure(messages of multiple topics are stored in one CommitLog) uses a separate storage structure for the data and index parts for Producer and Consumer respectively. The Producer sends the message to the Broker, then the Broker persists the message to the CommitLog file synchronously or asynchronously. As long as the message is persisted to the CommitLog on the disk, the message sent by the Producer will not be lost. Because of this, Consumer will definitely have the opportunity to consume this message. When no message can be pulled, the consumer can wait for the next pull. And the server also supports the long polling mode: if a pull request pulls no messages, the Broker can wait for 30 seconds, as long as new message arrives in this interval, it will be returned directly to the consumer. Here, RocketMQ's specific approach is using Broker's background service thread `ReputMessageService` to continuously dispatch requests and asynchronously build ConsumeQueue (Logical Queue) and IndexFile data. + +## 2 PageCache and Memory Map + +PageCache is a cache of files by the operating system to speed up the reading and writing of files. In general, the speed of sequential read and write files is almost the same as the speed of read and write memory. The main reason is that the OS uses a portion of the memory as PageCache to optimize the performance of the read and write operations. For data writing, the OS will first write to the Cache, and then the `pdflush` kernel thread asynchronously flush the data in the Cache to the physical disk. For data reading, if it can not hit the page cache when reading a file at a time, the OS will read the file from the physical disk and prefetch the data files of other neighboring blocks sequentially. + +In RocketMQ, the logic consumption queue `ConsumeQueue` stores less data and is read sequentially. With the help of prefetch of the page cache mechanism, the read performance of the `ConsumeQueue` file is almost close to the memory read, even in the case of message accumulation, it does not affect performance. But for the log data file `CommitLog`, it will generate many random access reads when reading the message content, which seriously affects the performance. If you choose the appropriate IO scheduling algorithm, such as setting the IO scheduling algorithm to "Deadline" (when the block storage uses SSD), the performance of random reads will also be improved. + + +In addition, RocketMQ mainly reads and writes files through `MappedByteBuffer`. `MappedByteBuffer` uses the `FileChannel` model in NIO to directly map the physical files on the disk to the memory address in user space (`Mmap` method reduces the performance overhead of traditional IO copying disk file data back and forth between the buffer in kernel space and the buffer in user space), it converts the file operation into direct memory address manipulation, which greatly improves the efficiency of reading and writing files (Because of the need to use the memory mapping mechanism, RocketMQ's file storage is fixed-length, making it easy to map the entire file to memory at a time). + +## 3 Message Disk Flush + +![](images/rocketmq_storage_flush.png) + + +* synchronous flush: As shown above, the RocketMQ's Broker will return a successful `ACK` response to the Producer after the message is truly persisted to disk. Synchronous flushing is a good guarantee for the reliability of MQ messages, but it will have a big impact on performance. Generally, it is suitable for financial business applications. +* asynchronous flush: Asynchronous flushing can take full advantage of the PageCache of the OS, as long as the message is written to the PageCache, the successful `ACK` can be returned to the Producer. The message flushing is performed by the background asynchronous thread, which reduces the read and write delay and improves the performance and throughput of the MQ. diff --git a/docs/en/Design_Trancation.md b/docs/en/Design_Trancation.md new file mode 100644 index 00000000000..930697b7241 --- /dev/null +++ b/docs/en/Design_Trancation.md @@ -0,0 +1,51 @@ +# Transaction Message +## 1 Transaction Message +Apache RocketMQ supports distributed transaction message from version 4.3.0. RocketMQ implements transaction message by using the protocol of 2PC(two-phase commit), in addition adding a compensation logic to handle timeout-case or failure-case of commit-phase, as shown below. + +![](../cn/image/rocketmq_design_10.png) + +### 1.1 The Process of RocketMQ Transaction Message +The picture above shows the overall architecture of transaction message, including the sending of message(commit-request phase), the sending of commit/rollback(commit phase) and the compensation process. + +1 The sending of message and Commit/Rollback. + (1) Sending the message(named Half message in RocketMQ) + (2) The server responds the writing result(success or failure) of Half message. + (3) Handle local transaction according to the result(local transaction won't be executed when the result is failure). + (4) Sending Commit/Rollback to broker according to the result of local transaction(Commit will generate message index and make the message visible to consumers). + +2 Compensation process + (1) For a transaction message without a Commit/Rollback (means the message in the pending status), a "back-check" request is initiated from the broker. + (2) The Producer receives the "back-check" request and checks the status of the local transaction corresponding to the "back-check" message. + (3) Redo Commit or Rollback based on local transaction status. +The compensation phase is used to resolve the timeout or failure case of the message Commit or Rollback. + +### 1.2 The design of RocketMQ Transaction Message +1 Transaction message is invisible to users in first phase(commit-request phase) + + Upon on the main process of transaction message, the message of first phase is invisible to the user. This is also the biggest difference from normal message. So how do we write the message while making it invisible to the user? And below is the solution of RocketMQ: if the message is a Half message, the topic and queueId of the original message will be backed up, and then changes the topic to RMQ_SYS_TRANS_HALF_TOPIC. Since the consumer group does not subscribe to the topic, the consumer cannot consume the Half message. Then RocketMQ starts a timing task, pulls the message for RMQ_SYS_TRANS_HALF_TOPIC, obtains a channel according to producer group and sends a back-check to query local transaction status, and decide whether to submit or roll back the message according to the status. + + In RocketMQ, the storage structure of the message in the broker is as follows. Each message has corresponding index information. The Consumer reads the content of the message through the secondary index of the ConsumeQueue. The flow is as follows: + +![](../cn/image/rocketmq_design_11.png) + + The specific implementation strategy of RocketMQ is: if the transaction message is written, topic and queueId of the message are replaced, and the original topic and queueId are stored in the properties of the message. Because the replace of the topic, the message will not be forwarded to the Consumer Queue of the original topic, and the consumer cannot perceive the existence of the message and will not consume it. In fact, changing the topic is the conventional method of RocketMQ(just recall the implementation mechanism of the delay message). + +2 Commit/Rollback operation and introduction of Op message + + After finishing writing a message that is invisible to the user in the first phase, here comes two cases in the second phase. One is Commit operation, after which the message needs to be visible to the user; the other one is Rollback operation, after which the first phase message(Half message) needs to be revoked. For the case of Rollback, since first-phase message itself is invisible to the user, there is no need to actually revoke the message (in fact, RocketMQ can't actually delete a message because it is a sequential-write file). But still some operation needs to be done to identity the final status of the message, to differ it from pending status message. To do this, the concept of "Op message" is introduced, which means the message has a certain status(Commit or Rollback). If a transaction message does not have a corresponding Op message, the status of the transaction is still undetermined (probably the second-phase failed). By introducing the Op message, the RocketMQ records an Op message for every Half message regardless it is Commit or Rollback. The only difference between Commit and Rollback is that when it comes to Commit, the index of the Half message is created before the Op message is written. + +3 How Op message stored and the correspondence between Op message and Half message + + RocketMQ writes the Op message to a specific system topic(RMQ_SYS_TRANS_OP_HALF_TOPIC) which will be created via the method - TransactionalMessageUtil.buildOpTopic(); this topic is an internal Topic (like the topic of RMQ_SYS_TRANS_HALF_TOPIC) and will not be consumed by the user. The content of the Op message is the physical offset of the corresponding Half message. Through the Op message we can index to the Half message for subsequent check-back operation. + +![](../cn/image/rocketmq_design_12.png) + +4 Index construction of Half messages + + When performing Commit operation of the second phase, the index of the Half message needs to be built. Since the Half message is written to a special topic(RMQ_SYS_TRANS_HALF_TOPIC) in the first phase of 2PC, so it needs to be read out from the special topic when building index, and replace the topic and queueId with the real target topic and queueId, and then write through a normal message that is visible to the user. Therefore, in conclusion, the second phase recovers a complete normal message using the content of the Half message stored in the first phase, and then goes through the message-writing process. + +5 How to handle the message failed in the second phase? + + If commit/rollback phase fails, for example, a network problem causes the Commit to fail when you do Commit. Then certain strategy is required to make sure the message finally commit. RocketMQ uses a compensation mechanism called "back-check". The broker initiates a back-check request for the message in pending status, and sends the request to the corresponding producer side (the same producer group as the producer group who sent the Half message). The producer checks the status of local transaction and redo Commit or Rollback. The broker performs the back-check by comparing the RMQ_SYS_TRANS_HALF_TOPIC messages and the RMQ_SYS_TRANS_OP_HALF_TOPIC messages and advances the checkpoint(recording those transaction messages that the status are certain). + + RocketMQ does not back-check the status of transaction messages endlessly. The default time is 15. If the transaction status is still unknown after 15 times, RocketMQ will roll back the message by default. diff --git a/docs/en/Example_Batch.md b/docs/en/Example_Batch.md new file mode 100644 index 00000000000..e62f4638111 --- /dev/null +++ b/docs/en/Example_Batch.md @@ -0,0 +1,83 @@ +# Batch Message Sample +------ +Sending messages in batch improves performance of delivering small messages. Messages of the same batch should have: same topic, same waitStoreMsgOK and no schedule support. You can send messages up to 4MiB at a time, but if you need to send a larger message, it is recommended to divide the larger messages into multiple small messages of no more than 1MiB. +### 1 Send Batch Messages +If you just send messages of no more than 4MiB at a time, it is easy to use batch: +```java +String topic = "BatchTest"; +List messages = new ArrayList<>(); +messages.add(new Message(topic, "TagA", "OrderID001", "Hello world 0".getBytes())); +messages.add(new Message(topic, "TagA", "OrderID002", "Hello world 1".getBytes())); +messages.add(new Message(topic, "TagA", "OrderID003", "Hello world 2".getBytes())); +try { + producer.send(messages); +} catch (Exception e) { + e.printStackTrace(); + //handle the error +} +``` +### 2 Split into Lists +The complexity only grow when you send large batch and you may not sure if it exceeds the size limit (4MiB). At this time, you’d better split the lists: +```java +public class ListSplitter implements Iterator> { + private final int SIZE_LIMIT = 1024 * 1024 * 4; + private final List messages; + private int currIndex; + public ListSplitter(List messages) { + this.messages = messages; + } + @Override + public boolean hasNext() { + return currIndex < messages.size(); + } + @Override + public List next() { + int startIndex = getStartIndex(); + int nextIndex = startIndex; + int totalSize = 0; + for (; nextIndex < messages.size(); nextIndex++) { + Message message = messages.get(nextIndex); + int tmpSize = calcMessageSize(message); + if (tmpSize + totalSize > SIZE_LIMIT) { + break; + } else { + totalSize += tmpSize; + } + } + List subList = messages.subList(startIndex, nextIndex); + currIndex = nextIndex; + return subList; + } + private int getStartIndex() { + Message currMessage = messages.get(currIndex); + int tmpSize = calcMessageSize(currMessage); + while(tmpSize > SIZE_LIMIT) { + currIndex += 1; + Message message = messages.get(curIndex); + tmpSize = calcMessageSize(message); + } + return currIndex; + } + private int calcMessageSize(Message message) { + int tmpSize = message.getTopic().length() + message.getBody().length; + Map properties = message.getProperties(); + for (Map.Entry entry : properties.entrySet()) { + tmpSize += entry.getKey().length() + entry.getValue().length(); + } + tmpSize = tmpSize + 20; // Increase the log overhead by 20 bytes + return tmpSize; + } +} + +// then you could split the large list into small ones: +ListSplitter splitter = new ListSplitter(messages); +while (splitter.hasNext()) { + try { + List listItem = splitter.next(); + producer.send(listItem); + } catch (Exception e) { + e.printStackTrace(); + // handle the error + } +} +``` diff --git a/docs/en/Example_Compaction_Topic.md b/docs/en/Example_Compaction_Topic.md new file mode 100644 index 00000000000..ed5528686f5 --- /dev/null +++ b/docs/en/Example_Compaction_Topic.md @@ -0,0 +1,68 @@ +# Compaction Topic + +## use example + +### Turn on the opening of support for orderMessages on namesrv +CompactionTopic relies on orderMessages to ensure consistency +```shell +$ bin/mqadmin updateNamesrvConfig -k orderMessageEnable -v true +``` + +### create compaction topic +```shell +$ bin/mqadmin updateTopic -w 8 -r 8 -a +cleanup.policy=COMPACTION -n localhost:9876 -t ctopic -o true -c DefaultCluster +create topic to 127.0.0.1:10911 success. +TopicConfig [topicName=ctopic, readQueueNums=8, writeQueueNums=8, perm=RW-, topicFilterType=SINGLE_TAG, topicSysFlag=0, order=false, attributes={+cleanup.policy=COMPACTION}] +``` + +### produce message +the same with ordinary message +```java +DefaultMQProducer producer = new DefaultMQProducer("CompactionTestGroup"); +producer.setNamesrvAddr("localhost:9876"); +producer.start(); + +String topic = "ctopic"; +String tag = "tag1"; +String key = "key1"; +Message msg = new Message(topic, tag, key, "bodys".getBytes(StandardCharsets.UTF_8)); +SendResult sendResult = producer.send(msg, (mqs, message, shardingKey) -> { + int select = Math.abs(shardingKey.hashCode()); + if (select < 0) { + select = 0; + } + return mqs.get(select % mqs.size()); +}, key); + +System.out.printf("%s%n", sendResult); +``` +### consume message +the message offset remains unchanged after compaction. If the consumer specified offset does not exist, return the most recent message after the offset. + +In the compaction scenario, most consumption was started from the beginning of the queue. +```java +DefaultLitePullConsumer consumer = new DefaultLitePullConsumer("compactionTestGroup"); +consumer.setNamesrvAddr("localhost:9876"); +consumer.setPullThreadNums(4); +consumer.start(); + +Collection messageQueueList = consumer.fetchMessageQueues("ctopic"); +consumer.assign(messageQueueList); +messageQueueList.forEach(mq -> { + try { + consumer.seekToBegin(mq); + } catch (MQClientException e) { + e.printStackTrace(); + } +}); + +Map kvStore = Maps.newHashMap(); +while (true) { + List msgList = consumer.poll(1000); + if (CollectionUtils.isNotEmpty(msgList)) { + msgList.forEach(msg -> kvStore.put(msg.getKeys(), msg.getBody())); + } +} + +//use the kvStore +``` diff --git a/docs/en/Example_CreateTopic.md b/docs/en/Example_CreateTopic.md new file mode 100644 index 00000000000..c3fb5a68cd0 --- /dev/null +++ b/docs/en/Example_CreateTopic.md @@ -0,0 +1,26 @@ +# Create Topic + +## Background + +The `TopicMessageType` concept is introduced in RocketMQ 5.0, using the existing topic attribute feature to implement it. + +The topic is created by `mqadmin` tool declaring the `message.type` attribute. + +## User Example + +```shell +# default +sh ./mqadmin updateTopic -n -t -c DefaultCluster + +# normal topic +sh ./mqadmin updateTopic -n -t -c DefaultCluster -a +message.type=NORMAL + +# fifo topic +sh ./mqadmin updateTopic -n -t -c DefaultCluster -a +message.type=FIFO + +# delay topic +sh ./mqadmin updateTopic -n -t -c DefaultCluster -a +message.type=DELAY + +# transaction topic +sh ./mqadmin updateTopic -n -t -c DefaultCluster -a +message.type=TRANSACTION +``` diff --git a/docs/en/Example_Delay.md b/docs/en/Example_Delay.md new file mode 100644 index 00000000000..a136d25f314 --- /dev/null +++ b/docs/en/Example_Delay.md @@ -0,0 +1,89 @@ +# Schedule example + +### 1 Start consumer to wait for incoming subscribed messages + +```java +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.common.message.MessageExt; +import java.util.List; + +public class ScheduledMessageConsumer { + + public static void main(String[] args) throws Exception { + // Instantiate message consumer + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ExampleConsumer"); + // Specify name server addresses + consumer.setNamesrvAddr("localhost:9876"); + // Subscribe topics + consumer.subscribe("TestTopic", "*"); + // Register message listener + consumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List messages, ConsumeConcurrentlyContext context) { + for (MessageExt message : messages) { + // Print approximate delay time period + System.out.println("Receive message[msgId=" + message.getMsgId() + "] " + + (System.currentTimeMillis() - message.getStoreTimestamp()) + "ms later"); + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + // Launch consumer + consumer.start(); + } +} +``` + +### 2 Send scheduled messages + +```java +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.common.message.Message; + +public class ScheduledMessageProducer { + + public static void main(String[] args) throws Exception { + // Instantiate a producer to send scheduled messages + DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup"); + // Specify name server addresses + producer.setNamesrvAddr("localhost:9876"); + // Launch producer + producer.start(); + int totalMessagesToSend = 100; + for (int i = 0; i < totalMessagesToSend; i++) { + Message message = new Message("TestTopic", ("Hello scheduled message " + i).getBytes()); + // This message will be delivered to consumer 10 seconds later. + message.setDelayTimeLevel(3); + // Send the message + producer.send(message); + } + + // Shutdown producer after use. + producer.shutdown(); + } + +} +``` + +### 3 Verification + +You should see messages are consumed about 10 seconds later than their storing time. + +### 4 Use scenarios for scheduled messages + +For example, in e-commerce, if an order is submitted, a delay message can be sent, and the status of the order can be checked after 1 hour. If the order is still unpaid, the order can be cancelled and the inventory released. + +### 5 Restrictions on the use of scheduled messages + +```java +// org/apache/rocketmq/store/config/MessageStoreConfig.java + +private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"; +``` + +Nowadays RocketMq does not support any time delay. It needs to set several fixed delay levels, which correspond to level 1 to 18 from 1s to 2h. Message consumption failure will enter the delay message queue. Message sending time is related to the set delay level and the number of retries. + + See `SendMessageProcessor.java` diff --git a/docs/en/Example_Filter.md b/docs/en/Example_Filter.md new file mode 100644 index 00000000000..768692d9dff --- /dev/null +++ b/docs/en/Example_Filter.md @@ -0,0 +1,86 @@ +# Filter Example +---------- + +In most cases, tag is a simple and useful design to select messages you want. For example: + +```java +DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_EXAMPLE"); +consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC"); +``` + +The consumer will receive messages that contains TAGA or TAGB or TAGC. But the limitation is that one message only can have one tag, and this may not work for sophisticated scenarios. In this case, you can use SQL expression to filter out messages. +SQL feature could do some calculation through the properties you put in when sending messages. Under the grammars defined by RocketMQ, you can implement some interesting logic. Here is an example: + +``` +------------ +| message | +|----------| a > 5 AND b = 'abc' +| a = 10 | --------------------> Gotten +| b = 'abc'| +| c = true | +------------ +------------ +| message | +|----------| a > 5 AND b = 'abc' +| a = 1 | --------------------> Missed +| b = 'abc'| +| c = true | +------------ +``` + +## 1 Grammars +RocketMQ only defines some basic grammars to support this feature. You could also extend it easily. + +- Numeric comparison, like **>**, **>=**, **<**, **<=**, **BETWEEN**, **=**; +- Character comparison, like **=**, **<>**, **IN**; +- **IS NULL** or **IS NOT NULL**; +- Logical **AND**, **OR**, **NOT**; + +Constant types are: + +- Numeric, like **123, 3.1415**; +- Character, like **‘abc’**, must be made with single quotes; +- **NULL**, special constant; +- Boolean, **TRUE** or **FALSE**; + +## 2 Usage constraints +Only push consumer could select messages by SQL92. The interface is: +``` +public void subscribe(finalString topic, final MessageSelector messageSelector) +``` + +## 3 Producer example +You can put properties in message through method putUserProperty when sending. + +```java +DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); +producer.start(); +Message msg = new Message("TopicTest", + tag, + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) +); +// Set some properties. +msg.putUserProperty("a", String.valueOf(i)); +SendResult sendResult = producer.send(msg); + +producer.shutdown(); + +``` + +## 4 Consumer example +Use `MessageSelector.bySql` to select messages through SQL when consuming. + + +```java +DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4"); +// only subscribe messages have property a, also a >=0 and a <= 3 +consumer.subscribe("TopicTest", MessageSelector.bySql("a between 0 and 3")); +consumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } +}); +consumer.start(); + +``` diff --git a/docs/en/Example_OpenMessaging.md b/docs/en/Example_OpenMessaging.md new file mode 100644 index 00000000000..a79fd10f2c7 --- /dev/null +++ b/docs/en/Example_OpenMessaging.md @@ -0,0 +1,118 @@ +# OpenMessaging Example +[OpenMessaging](https://openmessaging.github.io/), which includes the establishment of industry guidelines and messaging, streaming specifications to provide a common framework for finance, e-commerce, IoT and big-data area. The design principles are the cloud-oriented, simplicity, flexibility, and language independent in distributed heterogeneous environments. Conformance to these specifications will make it possible to develop a heterogeneous messaging applications across all major platforms and operating systems. + +RocketMQ provides a partial implementation of OpenMessaging 0.1.0-alpha, the following examples demonstrate how to access RocketMQ based on OpenMessaging. + +## OMSProducer +The following example shows how to send message to RocketMQ broker in synchronous, asynchronous, or one-way transmissions. + +``` +public class OMSProducer { + public static void main(String[] args) { + final MessagingAccessPoint messagingAccessPoint = MessagingAccessPointFactory + .getMessagingAccessPoint("openmessaging:rocketmq://IP1:9876,IP2:9876/namespace"); + + final Producer producer = messagingAccessPoint.createProducer(); + + messagingAccessPoint.startup(); + System.out.printf("MessagingAccessPoint startup OK%n"); + + producer.startup(); + System.out.printf("Producer startup OK%n"); + + { + Message message = producer.createBytesMessageToTopic("OMS_HELLO_TOPIC", "OMS_HELLO_BODY".getBytes(Charset.forName("UTF-8"))); + SendResult sendResult = producer.send(message); + System.out.printf("Send sync message OK, msgId: %s%n", sendResult.messageId()); + } + + { + final Promise result = producer.sendAsync(producer.createBytesMessageToTopic("OMS_HELLO_TOPIC", "OMS_HELLO_BODY".getBytes(Charset.forName("UTF-8")))); + result.addListener(new PromiseListener() { + @Override + public void operationCompleted(Promise promise) { + System.out.printf("Send async message OK, msgId: %s%n", promise.get().messageId()); + } + + @Override + public void operationFailed(Promise promise) { + System.out.printf("Send async message Failed, error: %s%n", promise.getThrowable().getMessage()); + } + }); + } + + { + producer.sendOneway(producer.createBytesMessageToTopic("OMS_HELLO_TOPIC", "OMS_HELLO_BODY".getBytes(Charset.forName("UTF-8")))); + System.out.printf("Send oneway message OK%n"); + } + + producer.shutdown(); + messagingAccessPoint.shutdown(); + } +} +``` +## OMSPullConsumer +Use OMS PullConsumer to poll messages from a specified queue. + +``` +public class OMSPullConsumer { + public static void main(String[] args) { + final MessagingAccessPoint messagingAccessPoint = MessagingAccessPointFactory + .getMessagingAccessPoint("openmessaging:rocketmq://IP1:9876,IP2:9876/namespace"); + + final PullConsumer consumer = messagingAccessPoint.createPullConsumer("OMS_HELLO_TOPIC", + OMS.newKeyValue().put(NonStandardKeys.CONSUMER_GROUP, "OMS_CONSUMER")); + + messagingAccessPoint.startup(); + System.out.printf("MessagingAccessPoint startup OK%n"); + + consumer.startup(); + System.out.printf("Consumer startup OK%n"); + + Message message = consumer.poll(); + if (message != null) { + String msgId = message.headers().getString(MessageHeader.MESSAGE_ID); + System.out.printf("Received one message: %s%n", msgId); + consumer.ack(msgId); + } + + consumer.shutdown(); + messagingAccessPoint.shutdown(); + } +} + +``` +## OMSPushConsumer +Attaches OMS PushConsumer to a specified queue and consumes messages by MessageListener + +``` +public class OMSPushConsumer { + public static void main(String[] args) { + final MessagingAccessPoint messagingAccessPoint = MessagingAccessPointFactory + .getMessagingAccessPoint("openmessaging:rocketmq://IP1:9876,IP2:9876/namespace"); + + final PushConsumer consumer = messagingAccessPoint. + createPushConsumer(OMS.newKeyValue().put(NonStandardKeys.CONSUMER_GROUP, "OMS_CONSUMER")); + + messagingAccessPoint.startup(); + System.out.printf("MessagingAccessPoint startup OK%n"); + + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + @Override + public void run() { + consumer.shutdown(); + messagingAccessPoint.shutdown(); + } + })); + + consumer.attachQueue("OMS_HELLO_TOPIC", new MessageListener() { + @Override + public void onMessage(final Message message, final ReceivedMessageContext context) { + System.out.printf("Received one message: %s%n", message.headers().getString(MessageHeader.MESSAGE_ID)); + context.ack(); + } + }); + + } +} +``` diff --git a/docs/en/Example_Orderly.md b/docs/en/Example_Orderly.md new file mode 100644 index 00000000000..e9cde37491e --- /dev/null +++ b/docs/en/Example_Orderly.md @@ -0,0 +1,233 @@ +# Example for Ordered Messages + +RocketMQ provides ordered messages using FIFO order. All related messages need to be sent into the same message queue in an orderly manner. + +The following demonstrates ordered messages by ensuring order of create, pay, send and finish steps of sales order process. + +## 1 produce ordered messages + +```java +package org.apache.rocketmq.example.order2 + +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.MessageQueueSelector; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageQueue; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/* +* ordered messages producer +*/ +public class Producer { + + public static void main(String[] args) throws Exception { + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + producer.setNamesrvAddr("127.0.0.1:9876"); + producer.start(); + String[] tags = new String[]{"TagA", "TagC", "TagD"}; + // sales orders list + List orderList = new Producer().buildOrders(); + + Date date = new Date(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String dateStr = sdf.format(date); + + for (int i = 0; i < 10; i++) { + // generate message timestamp + String body = dateStr + " Hello RocketMQ " + orderList.get(i); + Message msg = new Message("TopicTest", tags[i % tags.length], "KEY" + i, body.getBytes()); + + SendResult sendResult = producer.send(msg, new MessageQueueSelector() { + @Override + public MessageQueue select(List mqs, Message msg, Object arg) { + Long id = (Long) arg; //message queue is selected by #salesOrderID + long index = id % mqs.size(); + return mqs.get((int) index); + } + }, orderList.get(i).getOrderId()); + + System.out.println(String.format("SendResult status:%s, queueId:%d, body:%s", + sendResult.getSendStatus(), + sendResult.getMessageQueue().getQueueId(), + body)); + } + + producer.shutdown(); + } + + /** + * each sales order step + */ + private static class OrderStep { + private long orderId; + private String desc; + + public long getOrderId() { + return orderId; + } + + public void setOrderId(long orderId) { + this.orderId = orderId; + } + + public String getDesc() { + return desc; + } + + public void setDesc(String desc) { + this.desc = desc; + } + + @Override + public String toString() { + return "OrderStep{" + + "orderId=" + orderId + + ", desc='" + desc + '\'' + + '}'; + } + } + + /** + * to generate ten OrderStep objects for three sales orders: + * #SalesOrder "15103111039L": create, pay, send, finish; + * #SalesOrder "15103111065L": create, pay, finish; + * #SalesOrder "15103117235L": create, pay, finish; + */ + private List buildOrders() { + + List orderList = new ArrayList(); + + //create sales order with orderid="15103111039L" + OrderStep orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111039L); + orderDemo.setDesc("create"); + orderList.add(orderDemo); + + //create sales order with orderid="15103111065L" + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111065L); + orderDemo.setDesc("create"); + orderList.add(orderDemo); + + //pay sales order #"15103111039L" + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111039L); + orderDemo.setDesc("pay"); + orderList.add(orderDemo); + + //create sales order with orderid="15103117235L" + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103117235L); + orderDemo.setDesc("create"); + orderList.add(orderDemo); + + //pay sales order #"15103111065L" + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111065L); + orderDemo.setDesc("pay"); + orderList.add(orderDemo); + + //pay sales order #"15103117235L" + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103117235L); + orderDemo.setDesc("pay"); + orderList.add(orderDemo); + + //mark sales order #"15103111065L" as "finish" + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111065L); + orderDemo.setDesc("finish"); + orderList.add(orderDemo); + + //mark mark sales order #"15103111039L" as "send" + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111039L); + orderDemo.setDesc("send"); + orderList.add(orderDemo); + + ////mark sales order #"15103117235L" as "finish" + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103117235L); + orderDemo.setDesc("finish"); + orderList.add(orderDemo); + + //mark sales order #"15103111039L" as "finish" + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111039L); + orderDemo.setDesc("finish"); + orderList.add(orderDemo); + + return orderList; + } +} + +``` + +## 2 Consume ordered messages + +```java + +package org.apache.rocketmq.example.order2; + +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.MessageExt; + +import java.util.List; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +/** + * consume messages in order + */ +public class ConsumerInOrder { + + public static void main(String[] args) throws Exception { + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3"); + consumer.setNamesrvAddr("127.0.0.1:9876"); + /** + * when the consumer is first run, the start point of message queue where it can get messages will be set. + * or if it is restarted, it will continue from the last place to get messages. + */ + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + + consumer.subscribe("TopicTest", "TagA || TagC || TagD"); + + consumer.registerMessageListener(new MessageListenerOrderly() { + + Random random = new Random(); + + @Override + public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) { + context.setAutoCommit(true); + for (MessageExt msg : msgs) { + // one consumer for each message queue, and messages order are kept in a single message queue. + System.out.println("consumeThread=" + Thread.currentThread().getName() + "queueId=" + msg.getQueueId() + ", content:" + new String(msg.getBody())); + } + + try { + TimeUnit.SECONDS.sleep(random.nextInt(10)); + } catch (Exception e) { + e.printStackTrace(); + } + return ConsumeOrderlyStatus.SUCCESS; + } + }); + + consumer.start(); + + System.out.println("Consumer Started."); + } +} + +``` + + diff --git a/docs/en/Example_Simple.md b/docs/en/Example_Simple.md new file mode 100644 index 00000000000..0ce4924cc78 --- /dev/null +++ b/docs/en/Example_Simple.md @@ -0,0 +1,136 @@ +# Basic Sample +------ +Two functions below are provided in the basic sample: +* The RocketMQ can be utilized to send messages in three ways: reliable synchronous, reliable asynchronous, and one-way transmission. The first two message types are reliable because there is a response whether they were sent successfully. +* The RocketMQ can be utilized to consume messages. +### 1 Add Dependency +maven: +``` java + + org.apache.rocketmq + rocketmq-client + 4.3.0 + +``` +gradle: +``` java +compile 'org.apache.rocketmq:rocketmq-client:4.3.0' +``` +### 2 Send Messages +##### 2.1 Use Producer to Send Synchronous Messages +Reliable synchronous transmission is used in extensive scenes, such as important notification messages, SMS notification. +``` java +public class SyncProducer { + public static void main(String[] args) throws Exception { + // Instantiate with a producer group name + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + // Specify name server addresses + producer.setNamesrvAddr("localhost:9876"); + // Launch the producer instance + producer.start(); + for (int i = 0; i < 100; i++) { + // Create a message instance with specifying topic, tag and message body + Message msg = new Message("TopicTest" /* Topic */, + "TagA" /* Tag */, + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */ + ); + // Send message to one of brokers + SendResult sendResult = producer.send(msg); + // Check whether the message has been delivered by the callback of sendResult + System.out.printf("%s%n", sendResult); + } + // Shut down once the producer instance is not longer in use + producer.shutdown(); + } +} +``` +##### 2.2 Send Asynchronous Messages +Asynchronous transmission is generally used in response time sensitive business scenarios. It means that it is unable for the sender to wait the response of the Broker too long. +``` java +public class AsyncProducer { + public static void main(String[] args) throws Exception { + // Instantiate with a producer group name + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + // Specify name server addresses + producer.setNamesrvAddr("localhost:9876"); + // Launch the producer instance + producer.start(); + producer.setRetryTimesWhenSendAsyncFailed(0); + for (int i = 0; i < 100; i++) { + final int index = i; + // Create a message instance with specifying topic, tag and message body + Message msg = new Message("TopicTest", + "TagA", + "OrderID188", + "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); + // SendCallback: receive the callback of the asynchronous return result. + producer.send(msg, new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + System.out.printf("%-10d OK %s %n", index, + sendResult.getMsgId()); + } + @Override + public void onException(Throwable e) { + System.out.printf("%-10d Exception %s %n", index, e); + e.printStackTrace(); + } + }); + } + // Shut down once the producer instance is not longer in use + producer.shutdown(); + } +} +``` +##### 2.3 Send Messages in One-way Mode +One-way transmission is used for cases requiring moderate reliability, such as log collection. +``` java +public class OnewayProducer { + public static void main(String[] args) throws Exception{ + // Instantiate with a producer group name + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + // Specify name server addresses + producer.setNamesrvAddr("localhost:9876"); + // Launch the producer instance + producer.start(); + for (int i = 0; i < 100; i++) { + // Create a message instance with specifying topic, tag and message body + Message msg = new Message("TopicTest" /* Topic */, + "TagA" /* Tag */, + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */ + ); + // Send in one-way mode, no return result + producer.sendOneway(msg); + } + // Shut down once the producer instance is not longer in use + producer.shutdown(); + } +} +``` +### 3 Consume Messages +``` java +public class Consumer { + public static void main(String[] args) throws InterruptedException, MQClientException { + // Instantiate with specified consumer group name + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name"); + + // Specify name server addresses + consumer.setNamesrvAddr("localhost:9876"); + + // Subscribe one or more topics and tags for finding those messages need to be consumed + consumer.subscribe("TopicTest", "*"); + // Register callback to execute on arrival of messages fetched from brokers + consumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); + // Mark the message that have been consumed successfully + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + // Launch the consumer instance + consumer.start(); + System.out.printf("Consumer Started.%n"); + } +} +``` \ No newline at end of file diff --git a/docs/en/Example_Transaction.md b/docs/en/Example_Transaction.md new file mode 100644 index 00000000000..e2170738e9d --- /dev/null +++ b/docs/en/Example_Transaction.md @@ -0,0 +1,96 @@ +# Transaction Message Example + +## 1 Transaction message status +There are three states for transaction message: +- LocalTransactionState.COMMIT_MESSAGE: commit transaction, it means that allow consumers to consume this message. +- LocalTransactionState.ROLLBACK_MESSAGE: rollback transaction, it means that the message will be deleted and not allowed to consume. +- LocalTransactionState.UNKNOW: intermediate state, it means that MQ is needed to check back to determine the status. + +## 2 Send transactional message example + +### 2.1 Create the transactional producer +Use ```TransactionMQProducer```class to create producer client, and specify a unique ```ProducerGroup```, and you can set up a custom thread pool to process check requests. After executing the local transaction, you need to reply to MQ according to the execution result, and the reply status is described in the above section. +```java +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.common.message.MessageExt; +import java.util.List; +public class TransactionProducer { + public static void main(String[] args) throws MQClientException, InterruptedException { + TransactionListener transactionListener = new TransactionListenerImpl(); + TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name"); + ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue(2000), new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r); + thread.setName("client-transaction-msg-check-thread"); + return thread; + } + }); + producer.setExecutorService(executorService); + producer.setTransactionListener(transactionListener); + producer.start(); + String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"}; + for (int i = 0; i < 10; i++) { + try { + Message msg = + new Message("TopicTest1234", tags[i % tags.length], "KEY" + i, + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); + SendResult sendResult = producer.sendMessageInTransaction(msg, null); + System.out.printf("%s%n", sendResult); + Thread.sleep(10); + } catch (MQClientException | UnsupportedEncodingException e) { + e.printStackTrace(); + } + } + for (int i = 0; i < 100000; i++) { + Thread.sleep(1000); + } + producer.shutdown(); + } +} +``` + +### 2.2 Implement the TransactionListener interface +The ```executeLocalTransaction``` method is used to execute local transaction when send half message succeed. It returns one of three transaction status mentioned in the previous section. + +The ```checkLocalTransaction``` method is used to check the local transaction status and respond to MQ check requests. It also returns one of three transaction status mentioned in the previous section. +```java +public class TransactionListenerImpl implements TransactionListener { + private AtomicInteger transactionIndex = new AtomicInteger(0); + private ConcurrentHashMap localTrans = new ConcurrentHashMap<>(); + @Override + public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { + int value = transactionIndex.getAndIncrement(); + int status = value % 3; + localTrans.put(msg.getTransactionId(), status); + return LocalTransactionState.UNKNOW; + } + @Override + public LocalTransactionState checkLocalTransaction(MessageExt msg) { + Integer status = localTrans.get(msg.getTransactionId()); + if (null != status) { + switch (status) { + case 0: + return LocalTransactionState.UNKNOW; + case 1: + return LocalTransactionState.COMMIT_MESSAGE; + case 2: + return LocalTransactionState.ROLLBACK_MESSAGE; + } + } + return LocalTransactionState.COMMIT_MESSAGE; + } +} +``` + +## 3 Usage Constraint +1. Messages of the transactional have no schedule and batch support. +2. In order to avoid a single message being checked too many times and lead to half queue message accumulation, we limited the number of checks for a single message to 15 times by default, but users can change this limit by change the ```transactionCheckMax``` parameter in the configuration of the broker, if one message has been checked over ```transactionCheckMax``` times, broker will discard this message and print an error log at the same time by default. Users can change this behavior by override the ```AbstractTransactionalMessageCheckListener``` class. +3. A transactional message will be checked after a certain period of time that determined by parameter ```transactionTimeout``` in the configuration of the broker. And users also can change this limit by set user property ```CHECK_IMMUNITY_TIME_IN_SECONDS``` when sending transactional message, this parameter takes precedence over the ```transactionTimeout``` parameter. +4. A transactional message maybe checked or consumed more than once. +5. Committed message reput to the user’s target topic may fail. Currently, it depends on the log record. High availability is ensured by the high availability mechanism of RocketMQ itself. If you want to ensure that the transactional message isn’t lost and the transaction integrity is guaranteed, it is recommended to use synchronous double write mechanism. +6. `producerGroup` for producers of transactional messages cannot be shared with `producerGroup` for producers of other types of messages. Unlike other types of message, transactional messages allow backward queries. MQ Server query clients by their `producerGroup` of producers. + diff --git a/docs/en/FAQ.md b/docs/en/FAQ.md new file mode 100644 index 00000000000..dac53ecbfd4 --- /dev/null +++ b/docs/en/FAQ.md @@ -0,0 +1,109 @@ +# Frequently Asked Questions + +The following questions are frequently asked with regard to the RocketMQ project in general. + +## 1 General + +1. Why did we create rocketmq project instead of selecting other products? + + Please refer to [Why RocketMQ](http://rocketmq.apache.org/docs/motivation) + +2. Do I have to install other softeware, such as zookeeper, to use RocketMQ? + + No. RocketMQ can run independently. + +## 2 Usage + +### 1. Where does the newly created Consumer ID start consuming messages? + + 1) If the topic sends a message within three days, then the consumer start consuming messages from the first message saved in the server. + + 2) If the topic sends a message three days ago, the consumer starts to consume messages from the latest message in the server, in other words, starting from the tail of message queue. + + 3) If such consumer is rebooted, then it starts to consume messages from the last consumption location. + +### 2. How to reconsume message when consumption fails? + + 1) Cluster consumption pattern, The consumer business logic code returns Action.ReconsumerLater, NULL, or throws an exception, if a message failed to be consumed, it will retry for up to 16 times, after that, the message would be descarded. + + 2) Broadcast consumption patternThe broadcaset consumption still ensures that a message is consumered at least once, but no resend option is provided. + +### 3. How to query the failed message if there is a consumption failure? + + 1) Using topic query by time, you can query messages within a period of time. + + 2) Using Topic and Message Id to accurately query the message. + + 3) Using Topic and Message Key accurately query a class of messages with the same Message Key. + +### 4. Are messages delivered exactly once? + +RocketMQ ensures that all messages are delivered at least once. In most cases, the messages are not repeated. + +### 5. How to add a new broker? + + 1) Start up a new broker and register it to the same list of name servers. + + 2) By default, only internal system topics and consumer groups are created automatically. If you would like to have your business topic and consumer groups on the new node, please replicate them from the existing broker. Admin tool and command lines are provided to handle this. + +## 3 Configuration related + +The following answers are all default values and can be modified by configuration. + +### 1. How long are the messages saved on the server? + +Stored messages will be saved for up to 3 days, and messages that are not consumed for more than 3 days will be deleted. + +### 2. What is the size limit for message Body? + +Generally 256KB. + +### 3. How to set the number of consumer threads? + +When you start Consumer, set a ConsumeThreadNums property, example is as follows: +``` +consumer.setConsumeThreadMin(20); +consumer.setConsumeThreadMax(20); +``` + +## 4 Errors + +### 1. If you start a producer or consumer failed and the error message is producer group or consumer repeat. + +Reason: Using the same Producer /Consumer Group to launch multiple instances of Producer/Consumer in the same JVM may cause the client fail to start. + +Solution: Make sure that a JVM corresponding to one Producer /Consumer Group starts only with one Producer/Consumer instance. + +### 2. Consumer failed to start loading json file in broadcast mode. + +Reason: Fastjson version is too low to allow the broadcast consumer to load local offsets.json, causing the consumer boot failure. Damaged fastjson file can also cause the same problem. + +Solution: Fastjson version has to be upgraded to rocketmq client dependent version to ensure that the local offsets.json can be loaded. By default offsets.json file is in /home/{user}/.rocketmq_offsets. Or check the integrity of fastjson. + +### 3. What is the impact of a broker crash. + + 1) Master crashes + +Messages can no longer be sent to this broker set, but if you have another broker set available, messages can still be sent given the topic is present. Messages can still be consumed from slaves. + + 2) Some slave crash + +As long as there is another working slave, there will be no impact on sending messages. There will also be no impact on consuming messages except when the consumer group is set to consume from this slave preferably. By default, consumer group consumes from master. + + 3) All slaves crash + +There will be no impact on sending messages to master, but, if the master is SYNC_MASTER, producer will get a SLAVE_NOT_AVAILABLE indicating that the message is not sent to any slaves. There will also be no impact on consuming messages except that if the consumer group is set to consume from slave preferably. By default, consumer group consumes from master. + +### 4. Producer complains “No Topic Route Info”, how to diagnose? + +This happens when you are trying to send messages to a topic whose routing info is not available to the producer. + + 1) Make sure that the producer can connect to a name server and is capable of fetching routing meta info from it. + + 2) Make sure that name servers do contain routing meta info of the topic. You may query the routing meta info from name server through topicRoute using admin tools or web console. + + 3) Make sure that your brokers are sending heartbeats to the same list of name servers your producer is connecting to. + + 4) Make sure that the topic’s permission is 6(rw-), or at least 2(-w-). + +If you can’t find this topic, create it on a broker via admin tools command updateTopic or web console. diff --git a/docs/en/Feature.md b/docs/en/Feature.md new file mode 100644 index 00000000000..c51b965f25c --- /dev/null +++ b/docs/en/Feature.md @@ -0,0 +1,91 @@ +# Features +## 1 Subscribe and Publish +Message publication refers to that a producer sends messages to a topic; Message subscription means a consumer follows a topic with certain tags and then consumes data from that topic. + +## 2 Message Ordering +Message ordering refers to that a group of messages can be consumed orderly as they are published. For example, an order generates three messages: order creation, order payment, and order completion. It only makes sense to consume them in their generated order, but orders can be consumed in parallel at the same time. RocketMQ can strictly guarantee these messages are in order. + +Orderly message is divided into global orderly message and partitioned orderly message. Global order means that all messages under a certain topic must be in order, partitioned order only requires each group of messages are consumed orderly. +- Global message ordering: +For a given Topic, all messages are published and consumed in strict first-in-first-out (FIFO) order. +Applicable scenario: the performance requirement is not high, and all messages are published and consumed according to FIFO principle strictly. +- Partitioned message ordering: +For a given Topic, all messages are partitioned according to sharding key. Messages within the same partition are published and consumed in strict FIFO order. Sharding key is the key field to distinguish message's partition, which is a completely different concept from the key of ordinary messages. +Applicable scenario: high performance requirement, with sharding key as the partition field, messages within the same partition are published and consumed according to FIFO principle strictly. + +## 3 Message Filter +Consumers of RocketMQ can filter messages based on tags as well as supporting for user-defined attribute filtering. Message filter is currently implemented on the Broker side, with the advantage of reducing the network transmission of useless messages for Consumer and the disadvantage of increasing the burden on the Broker and relatively complex implementation. + +## 4 Message Reliability +RocketMQ supports high reliability of messages in several situations: +1 Broker shutdown normally +2 Broker abnormal crash +3 OS Crash +4 The machine is out of power, but it can be recovered immediately +5 The machine cannot be started up (the CPU, motherboard, memory and other key equipment may be damaged) +6 Disk equipment damaged + +In the four cases of 1), 2), 3), and 4) where the hardware resource can be recovered immediately, RocketMQ guarantees that the message will not be lost or a small amount of data will be lost (depending on whether the flush disk type is synchronous or asynchronous). + +5 ) and 6) are single point of failure and cannot be recovered. Once it happens, all messages on the single point will be lost. In both cases, RocketMQ ensures that 99% of the messages are not lost through asynchronous replication, but a very few number of messages may still be lost. Synchronous double write mode can completely avoid single point of failure, which will surely affect the performance and suitable for the occasion of high demand for message reliability, such as money related applications. Note: RocketMQ supports synchronous double writes since version 3.0. + +## 5 At Least Once +At least Once refers to that every message will be delivered at least once. RocketMQ supports this feature because the Consumer pulls the message locally and does not send an ack back to the server until it has consumed it. + +## 6 Backtracking Consumption +Backtracking consumption refers to that the Consumer has consumed the message successfully, but the business needs to consume again. To support this function, the message still needs to be retained after the Broker sends the message to the Consumer successfully. The re-consumption is normally based on time dimension. For example, after the recovery of the Consumer system failures, the data one hour ago needs to be re-consumed, then the Broker needs to provide a mechanism to reverse the consumption progress according to the time dimension. RocketMQ supports backtracking consumption by time trace, with the time dimension down to milliseconds. + +## 7 Transactional Message +RocketMQ transactional message refers to the fact that the application of a local transaction and the sending of a Message operation can be defined in a global transaction which means both succeed or failed simultaneously. RocketMQ transactional message provides distributed transaction functionality similar to X/Open XA, enabling the ultimate consistency of distributed transactions through transactional message. + +## 8 Scheduled Message +Scheduled message(delay queue) refers to that messages are not consumed immediately after they are sent to the broker, but waiting to be delivered to the real topic after a specific time. +The broker has a configuration item, `messageDelayLevel`, with default values “1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”, 18 levels. Users can configure a custom `messageDelayLevel`. Note that `messageDelayLevel` is a broker's property rather than a topic's. When sending a message, just set the delayLevel level: msg.setDelayLevel(level). There are three types of levels: + +- level == 0, The message is not a delayed message +- 1<=level<=maxLevel, Message delay specific time, such as level==1, delay for 1s +- level > maxLevel, than level== maxLevel, such as level==20, delay for 2h + +Scheduled messages are temporarily saved in a topic named SCHEDULE_TOPIC_XXXX, and saved in a specific queue according to delayTimeLevel, queueId = delayTimeLevel - 1, that is, only messages with the same delay are saved in a queue, ensuring that messages with the same sending delay can be consumed orderly. The broker consumes SCHEDULE_TOPIC_XXXX on schedule and writes messages to the real topic. + +Note that Scheduled messages are counted both the first time they are written and the time they are scheduled to be written to the real topic, so both the send number and the TPS are increased. + +## 9 Message Retry +When the Consumer fails to consume the message, a retry mechanism is needed to make the message to be consumed again. Consumer's consume failure can usually be classified as follows: +- Due to the reasons of the message itself, such as deserialization failure, the message data itself cannot be processed (for example, the phone number of the current message is cancelled and cannot be charged), etc. This kind of error usually requires skipping this message and consuming others since immediately retry would be failed 99%, so it is better to provide a timed retry mechanism that retries after 10 seconds. +- Due to the reasons of dependent downstream application services are not available, such as db connection is not usable, perimeter network is not unreachable, etc. When this kind of error is encountered, consuming other messages will also result in an error even if the current failed message is skipped. In this case, it is recommended to sleep for 30s before consuming the next message, which will reduce the pressure on the broker to retry the message. + +RocketMQ will set up a retry queue named “%RETRY%+consumerGroup” for each consumer group(Note that the retry queue for this topic is for consumer groups, not for each topic) to temporarily save messages cannot be consumed by customer due to all kinds of reasons. Considering that it takes some time for the exception to recover, multiple retry levels are set for the retry queue, and each retry level has a corresponding re-deliver delay. The more retries, the greater the deliver delay. RocketMQ first save retry messages to the delay queue which topic is named “SCHEDULE_TOPIC_XXXX”, then background schedule task will save the messages to “%RETRY%+consumerGroup” retry queue according to their corresponding delay. + +## 10 Message Resend +When a producer sends a message, the synchronous message will be resent if fails, the asynchronous message will retry and oneway message is without any guarantee. Message resend ensures that messages are sent successfully and without lost as much as possible, but it can lead to message duplication, which is an unavoidable problem in RocketMQ. Under normal circumstances, message duplication will not occur, but when there is a large number of messages and network jitter, message duplication will be a high-probability event. In addition, producer initiative messages resend and the consumer load changes will also result in duplicate messages. The message retry policy can be set as follows: + +- `retryTimesWhenSendFailed`: Synchronous message retry times when send failed, default value is 2, so the producer will try to send `retryTimesWhenSendFailed` + 1 times at most. To ensure that the message is not lost, producer will try sending the message to another broker instead of selecting the broker that failed last time. An exception will be thrown if it reaches the retry limit, and the client should guarantee that the message will not be lost. Messages will resend when RemotingException, MQClientException, and partial MQBrokerException occur. +- `retryTimesWhenSendAsyncFailed`: Asynchronous message retry times when send failed, asynchronous retry sends message to the same broker instead of selecting another one and does not guarantee that the message wont lost. +- `retryAnotherBrokerWhenNotStoreOK`: Message flush disk (master or slave) timeout or slave not available (return status is not SEND_OK), whether to try to send to another broker, default value is false. Very important messages can set to true. + +## 11 Flow Control +Producer flow control, because broker processing capacity reaches a bottleneck; Consumer flow control, because the consumption capacity reaches a bottleneck. + +Producer flow control: +- When commitLog file locked time exceeds osPageCacheBusyTimeOutMills, default value of `osPageCacheBusyTimeOutMills` is 1000 ms, then return flow control. +- If `transientStorePoolEnable` == true, and the broker is asynchronous flush disk type, and resources are insufficient in the transientStorePool, reject the current send request and return flow control. +- The broker checks the head request wait time of the send request queue every 10ms. If the wait time exceeds waitTimeMillsInSendQueue, which default value is 200ms, the current send request is rejected and the flow control is returned. +- The broker implements flow control by rejecting send requests. + +Consumer flow control: +- When consumer local cache messages number exceeds pullThresholdForQueue, default value is 1000. +- When consumer local cache messages size exceeds pullThresholdSizeForQueue, default value is 100MB. +- When consumer local cache messages span exceeds consumeConcurrentlyMaxSpan, default value is 2000. + +The result of consumer flow control is to reduce the pull frequency. + +## 12 Dead Letter Queue +Dead letter queue is used to deal messages that cannot be consumed normally. When a message is consumed failed at first time, the message queue will automatically resend the message. If the consumption still fails after the maximum number retry, it indicates that the consumer cannot properly consume the message under normal circumstances. At this time, the message queue will not immediately abandon the message, but send it to the special queue corresponding to the consumer. + +RocketMQ defines the messages that could not be consumed under normal circumstances as Dead-Letter Messages, and the special queue in which the Dead-Letter Messages are saved as Dead-Letter Queues. In RocketMQ, the consumer instance can consume again by resending messages in the Dead-Letter Queue using console. + +## 13 Pop Consuming + +Pop consuming refers to that broker fetches messages from queues owned by same broker and returns to clients, which ensures one queue will be consumed by multiple clients. The whole behavior is like a queue +pop process. By invoking `setConsumeMode` sub command of mqadmin, one consumer group can be switch to POP consuming instead of classical PULL consuming without changing a single code line. The new pop consuming will help to mitigate the impact for one queue consuming of an abnormal behaving client. \ No newline at end of file diff --git a/docs/en/Operations_Broker.md b/docs/en/Operations_Broker.md new file mode 100644 index 00000000000..cf3ea581913 --- /dev/null +++ b/docs/en/Operations_Broker.md @@ -0,0 +1,23 @@ +# 3 Broker + +## 3.1 Broker Role +Broker Role is ASYNC_MASTER, SYNC_MASTER or SLAVE. If you cannot tolerate message missing, we suggest you deploy SYNC_MASTER and attach a SLAVE to it. If you feel ok about missing, but you want the Broker to be always available, you may deploy ASYNC_MASTER with SLAVE. If you just want to make it easy, you may only need a ASYNC_MASTER without SLAVE. +## 3.2 FlushDiskType +ASYNC_FLUSH is recommended, for SYNC_FLUSH is expensive and will cause too much performance loss. If you want reliability, we recommend you use SYNC_MASTER with SLAVE. +## 3.3 Broker Configuration +| Parameter name | Default | Description | +| -------------------------------- | ----------------------------- | ------------------------------------------------------------ | +| listenPort | 10911 | listen port for client | +| namesrvAddr | null | name server address | +| brokerIP1 | InetAddress for network interface | Should be configured if having multiple addresses | +| brokerIP2 | InetAddress for network interface | If configured for the Master broker in the Master/Slave cluster, slave broker will connect to this port for data synchronization | +| brokerName | null | broker name | +| brokerClusterName | DefaultCluster | this broker belongs to which cluster | +| brokerId | 0 | broker id, 0 means master, positive integers mean slave | +| storePathCommitLog | $HOME/store/commitlog/ | file path for commit log | +| storePathConsumerQueue | $HOME/store/consumequeue/ | file path for consume queue | +| mappedFileSizeCommitLog | 1024 * 1024 * 1024(1G) | mapped file size for commit log |​ +| deleteWhen | 04 | When to delete the commitlog which is out of the reserve time |​ +| fileReserverdTime | 72 | The number of hours to keep a commitlog before deleting it |​ +| brokerRole | ASYNC_MASTER | SYNC_MASTER/ASYNC_MASTER/SLAVE |​ +| flushDiskType | ASYNC_FLUSH | {SYNC_FLUSH/ASYNC_FLUSH}. Broker of SYNC_FLUSH mode flushes each message onto disk before acknowledging producer. Broker of ASYNC_FLUSH mode, on the other hand, takes advantage of group-committing, achieving better performance. |​ \ No newline at end of file diff --git a/docs/en/Operations_Consumer.md b/docs/en/Operations_Consumer.md new file mode 100644 index 00000000000..8944be16d45 --- /dev/null +++ b/docs/en/Operations_Consumer.md @@ -0,0 +1,106 @@ +## Consumer + +---- + +### 1 Consumption process idempotent + +RocketMQ cannot achieve Exactly-Once, so if the business is very sensitive to consumption repetition, it is important to perform deduplication at the business level. Deduplication can be done with a relational database. First, you need to determine the unique key of the message, which can be either msgId or a unique identifier field in the message content, such as the order Id. Determine if a unique key exists in the relational database before consumption. If it does not exist, insert it and consume it, otherwise skip it. (The actual process should consider the atomic problem, determine whether there is an attempt to insert, if the primary key conflicts, the insertion fails, skip directly) + +### 2 Slow message processing + +#### 2.1 Increase consumption parallelism + +Most messages consumption behaviors are IO-intensive, That is, it may be to operate the database, or call RPC. The consumption speed of such consumption behavior lies in the throughput of the back-end database or the external system. By increasing the consumption parallelism, the total consumption throughput can be increased, but the degree of parallelism is increased to a certain extent. Instead it will fall.Therefore, the application must set a reasonable degree of parallelism. There are several ways to modify the degree of parallelism of consumption as follows: + +* Under the same ConsumerGroup, increase the degree of parallelism by increasing the number of Consumer instances (note that the Consumer instance that exceeds the number of subscription queues is invalid). Can be done by adding machines, or by starting multiple processes on an existing machine. +* Improve the consumption parallel thread of a single Consumer by modifying the parameters consumeThreadMin and consumeThreadMax. + +#### 2.2 Batch mode consumption + +Some business processes can increase consumption throughput to a large extent if they support batch mode consumption. For example, order deduction application, it takes 1s to process one order at a time, and it takes only 2s to process 10 orders at a time. In this way, the throughput of consumption can be greatly improved. By setting the consumer's consumeMessageBatchMaxSize to return a parameter, the default is 1, that is, only one message is consumed at a time, for example, set to N, then the number of messages consumed each time is less than or equal to N. + +#### 2.3 Skip non-critical messages + +When a message is accumulated, if the consumption speed cannot keep up with the transmission speed, if the service does not require high data, you can choose to discard the unimportant message. For example, when the number of messages in a queue is more than 100,000 , try to discard some or all of the messages, so that you can quickly catch up with the speed of sending messages. The sample code is as follows: + +```java +public ConsumeConcurrentlyStatus consumeMessage( + List msgs, + ConsumeConcurrentlyContext context){ + long offset = msgs.get(0).getQueueOffset(); + String maxOffset = + msgs.get(0).getProperty(Message.PROPERTY_MAX_OFFSET); + long diff = Long.parseLong(maxOffset) - offset; + if(diff > 100000){ + //TODO Special handling of message accumulation + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + //TODO Normal consumption process + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; +} +``` + +#### 2.4 Optimize each message consumption process + +For example, the consumption process of a message is as follows: + +* Query from DB according to the message [data 1] +* Query from DB according to the message [data 2] +* Complex business calculations +* Insert [Data 3] into the DB +* Insert [Data 4] into the DB + +There are 4 interactions with the DB in the consumption process of this message. If it is calculated by 5ms each time, it takes a total of 20ms. If the business calculation takes 5ms, then the total time is 25ms, So if you can optimize 4 DB interactions to 2 times, the total time can be optimized to 15ms, which means the overall performance is increased by 40%. Therefore, if the application is sensitive to delay, the DB can be deployed on the SSD hard disk. Compared with the SCSI disk, the former RT will be much smaller. + +### 3 Print Log + +If the amount of messages is small, it is recommended to print the message in the consumption entry method, consume time, etc., to facilitate subsequent troubleshooting. + +```java +public ConsumeConcurrentlyStatus consumeMessage( + List msgs, + ConsumeConcurrentlyContext context){ + log.info("RECEIVE_MSG_BEGIN: " + msgs.toString()); + //TODO Normal consumption process + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; +} +``` + +If you can print the time spent on each message, it will be more convenient when troubleshooting online problems such as slow consumption. + +### 4 Other consumption suggestions + +#### 4.1、Consumer Group and Subscriptions + +The first thing you should be aware of is that different Consumer Group can consume the same topic independently, and each of them will have their own consuming offsets. Please make sure each Consumer within the same Group to subscribe the same topics. + +#### 4.2、Orderly + +The Consumer will lock each MessageQueue to make sure it is consumed one by one in order. This will cause a performance loss, but it is useful when you care about the order of the messages. It is not recommended to throw exceptions, you can return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT instead. + +#### 4.3、Concurrently + +As the name tells, the Consumer will consume the messages concurrently. It is recommended to use this for good performance. It is not recommended to throw exceptions, you can return ConsumeConcurrentlyStatus.RECONSUME_LATER instead. + +#### 4.4、Consume Status + +For MessageListenerConcurrently, you can return RECONSUME_LATER to tell the consumer that you can not consume it right now and want to reconsume it later. Then you can continue to consume other messages. For MessageListenerOrderly, because you care about the order, you can not jump over the message, but you can return SUSPEND_CURRENT_QUEUE_A_MOMENT to tell the consumer to wait for a moment. + +#### 4.5、Blocking + +It is not recommend to block the Listener, because it will block the thread pool, and eventually may stop the consuming process. + +#### 4.6、Thread Number + +The consumer use a ThreadPoolExecutor to process consuming internally, so you can change it by setting setConsumeThreadMin or setConsumeThreadMax. + +#### 4.7、ConsumeFromWhere + +When a new Consumer Group is established, it will need to decide whether it needs to consume the historical messages which had already existed in the Broker. CONSUME_FROM_LAST_OFFSET will ignore the historical messages, and consume anything produced after that. CONSUME_FROM_FIRST_OFFSET will consume every message existed in the Broker. You can also use CONSUME_FROM_TIMESTAMP to consume messages produced after the specified timestamp. + + + + + + + diff --git a/docs/en/Operations_Producer.md b/docs/en/Operations_Producer.md new file mode 100644 index 00000000000..c7f0d5711aa --- /dev/null +++ b/docs/en/Operations_Producer.md @@ -0,0 +1,44 @@ +### Producer +---- +##### 1 Message Sending Tips +###### 1.1 The Use of Tags +One application instance should use one topic as much as possible and the subtype of messages can be marked by tags. Tag provides extra flexibility to users. In the consume subscribing process, the messages filtering can only be handled by using tags when the tags are specified in the message sending process: `message.setTags("TagA")`. +###### 1.2 The Use of Keys +A business key can be set in one message and it will be easier to look up the message on a broker server to diagnose issues during development. Each message will be created index(hash index) by server, instance can query the content of this message by topic and key and who consumes the message.Because of the hash index, make sure that the key should be unique in order to avoid potential hash index conflict. +``` java +// Order Id +String orderId = "20034568923546"; +message.setKeys(orderId); +``` +###### 1.3 The Log Print +When sending a message,no matter success or fail, a message log must be printed which contains SendResult and Key. It is assumed that we will always get SEND_OK if no exception is thrown. Below is a list of descriptions about each status: +* SEND_OK + +SEND_OK means sending message successfully. SEND_OK does not mean it is reliable. To make sure no message would be lost, you should also enable SYNC_MASTER or SYNC_FLUSH. +* FLUSH_DISK_TIMEOUT + +FLUSH_DISK_TIMEOUT means sending message successfully but the Broker flushing the disk with timeout. In this kind of condition, the Broker has saved this message in memory, this message will be lost only if the Broker was down. The FlushDiskType and SyncFlushTimeout could be specified in MessageStoreConfig. If the Broker set MessageStoreConfig’s FlushDiskType=SYNC_FLUSH(default is ASYNC_FLUSH), and the Broker doesn’t finish flushing the disk within MessageStoreConfig’s syncFlushTimeout(default is 5 secs), you will get this status. +* FLUSH_SLAVE_TIMEOUT + +FLUSH_SLAVE_TIMEOUT means sending messages successfully but the slave Broker does not finish synchronizing with the master. If the Broker’s role is SYNC_MASTER(default is ASYNC_MASTER), and the slave Broker doesn’t finish synchronizing with the master within the MessageStoreConfig’s syncFlushTimeout(default is 5 secs), you will get this status. +* SLAVE_NOT_AVAILABLE + +SLAVE_NOT_AVAILABLE means sending messages successfully but no slave Broker configured. If the Broker’s role is SYNC_MASTER(default is ASYNC_MASTER), but no slave Broker is configured, you will get this status. + +##### 2 Operations on Message Sending failed +The send method of Producer can be retried, the retry process is illustrated below: +* The method will retry at most 2 times(2 times in synchronous mode, 0 times in asynchronous mode). +* If sending failed, it will turn to the next Broker. This strategy will be executed when the total costing time is less then sendMsgTimeout(default is 10 seconds). +* The retry method will be terminated if timeout exception was thrown when sending messages to Broker. + +The strategy above could make sure message sending successfully to a certain degree. Some more retry strategies, such as we could try to save the message to database if calling the send synchronous method failed and then retry by background thread's timed tasks, which will make sure the message is sent to Broker,could be improved if asking for high reliability business requirement. + +The reasons why the retry strategy using database have not integrated by the RocketMQ client will be explained below: Firstly, the design mode of the RocketMQ client is stateless mode. It means that the client is designed to be horizontally scalable at each level and the consumption of the client to physical resources is only CPU, memory and network. Then, if a key-value memory module is integrated by the client itself, the Async-Saving strategy will be utilized in consideration of the high resource consumption of the Syn-Saving strategy. However, given that operations staff does not manage the client shutoff, some special commands, such as kill -9, may be used which will lead to the lost of message because of no saving in time. Furthermore, the physical resource running Producer is not appropriate to save some significant data because of low reliability. Above all, the retry process should be controlled by program itself. + +##### 3 Send Messages in One-way Mode +The message sending is usually a process like below: +* Client sends request to sever. +* Sever handles request +* Sever returns response to client + +The total costing time of sending one message is the sum of costing time of three steps above. Some situations demand that total costing time must be in a quite low level, however, do not take reliable performance into consideration, such as log collection. This kind of application could be called in one-way mode, which means client sends request but not wait for response. In this kind of mode, the cost of sending request is only a call of system operation which means one operation writing data to client socket buffer. Generally, the time cost of this process will be controlled n microseconds level. diff --git a/docs/en/Operations_Trace.md b/docs/en/Operations_Trace.md new file mode 100644 index 00000000000..74c2cde1af2 --- /dev/null +++ b/docs/en/Operations_Trace.md @@ -0,0 +1,104 @@ +# Message Trace + +## 1 Key Attributes of Message Trace Data + +| Producer | Consumer | Broker | +| ---------------- | ----------------- | ------------ | +| production instance information | consumption instance information | message Topic | +| send message time | post time, post round | message storage location | +| whether the message was sent successfully | Whether the message was successfully consumed | The Key of the message | +| Time spent sending | Time spent consuming | Tag of the message | + +## 2 Support for Message Trace Cluster Deployment + +### 2.1 Broker Configuration Fille + +The properties profile content of the Broker side enabled message trace feature is pasted here: + +``` +brokerClusterName=DefaultCluster +brokerName=broker-a +brokerId=0 +deleteWhen=04 +fileReservedTime=48 +brokerRole=ASYNC_MASTER +flushDiskType=ASYNC_FLUSH +storePathRootDir=/data/rocketmq/rootdir-a-m +storePathCommitLog=/data/rocketmq/commitlog-a-m +autoCreateSubscriptionGroup=true +## if msg tracing is open,the flag will be true +traceTopicEnable=true +listenPort=10911 +brokerIP1=XX.XX.XX.XX1 +namesrvAddr=XX.XX.XX.XX:9876 +``` + +### 2.2 Normal Mode +Each Broker node in the RocketMQ cluster is used to store message trace data collected and sent from the Client.Therefore, there are no requirements or restrictions on the number of Broker nodes in the RocketMQ cluster. + +### 2.3 Physical IO Isolation Mode +For scenarios with large amount of trace message data , one of the Broker nodes in the RocketMQ cluster can be selected to store the trace message , so that the common message data of the user and the physical IO of the trace message data are completely isolated from each other.In this mode, there are at least two Broker nodes in the RocketMQ cluster, one of which is defined as the server on which message trace data is stored. + +### 2.4 Start the Broker that Starts the MessageTrace +`nohup sh mqbroker -c ../conf/2m-noslave/broker-a.properties &` + +## 3 Save the Topic Definition of Message Trace +RocketMQ's message trace feature supports two ways to store trace data: + +### 3.1 System-level TraceTopic +By default, message track data is stored in the system-level TraceTopic(names: **RMQ_SYS_TRACE_TOPIC**).This Topic is automatically created when the Broker node is started(As described above, the switch variable **traceTopicEnable** needs to be set to **true** in the Broker configuration file). + +### 3.2 Custom TraceTopic +If the user is not prepared to store the message track data in the system-level default TraceTopic, you can also define and create a user-level Topic to save the track (that is, to create a regular Topic to save the message track data).The following section introduces how the Client interface supports the user-defined TraceTopic. + +## 4 Client Practices that Support Message Trace +In order to reduce as much as possible the transformation work of RocketMQ message trace feature used in the user service system, the author added a switch parameter (**enableMsgTrace**) to the original interface in the design to realize whether the message trace is opened or not. + +### 4.1 Opening the Message Trace when Sending the Message +``` + DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName",true); + producer.setNamesrvAddr("XX.XX.XX.XX1"); + producer.start(); + try { + { + Message msg = new Message("TopicTest", + "TagA", + "OrderID188", + "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); + SendResult sendResult = producer.send(msg); + System.out.printf("%s%n", sendResult); + } + + } catch (Exception e) { + e.printStackTrace(); + } +``` + +### 4.2 Opening Message Trace whenSubscribing to a Message +``` + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_JODIE_1",true); + consumer.subscribe("TopicTest", "*"); + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + consumer.setConsumeTimestamp("20181109221800"); + consumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + consumer.start(); + System.out.printf("Consumer Started.%n"); +``` + +### 4.3 Support for Custom Storage Message Trace Topic +The initialization of `DefaultMQProducer` and `DefaultMQPushConsumer` instances can be changed to support the custom storage message trace Topic as follows when sending and subscribing messages above. + +``` + ##Where Topic_test11111 needs to be pre-created by the user to save the message trace; + DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName",true,"Topic_test11111"); + ...... + + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_JODIE_1",true,"Topic_test11111"); + ...... +``` \ No newline at end of file diff --git a/docs/en/QuorumACK.md b/docs/en/QuorumACK.md new file mode 100644 index 00000000000..bd8c565abe0 --- /dev/null +++ b/docs/en/QuorumACK.md @@ -0,0 +1,73 @@ +# Quorum write and automatic downgrade + +## Background + +![](https://s4.ax1x.com/2022/02/05/HnWo2d.png) + +In RocketMQ, there are two main replication modes between primary and secondary servers: synchronous replication and asynchronous replication. As shown in the above figure, the replication of Slave1 is synchronous, and the Master needs to wait for Slave1 to successfully replicate the message and confirm before reporting success to the Producer. The replication of Slave2 is asynchronous, and the Master does not need to wait for the response from Slave2. In RocketMQ, if everything goes well when sending a message, the Producer client will eventually receive a PUT_OK status. If the Slave synchronization times out, it will return a FLUSH_SLAVE_TIMEOUT status. If the Slave is unavailable or the difference between the CommitLog of the Slave and Master exceeds a certain value (default is 256MB), it will return a SLAVE_NOT_AVAILABLE status. The latter two states will not cause system exceptions and prevent the next message from being written. + +Synchronous replication ensures that the data can still be found in the Slave after the Master fails, which is suitable for scenarios with high reliability requirements. Although asynchronous replication may result in message loss, it is more efficient than synchronous replication because it does not need to wait for the Slave's confirmation, and is suitable for scenarios with certain efficiency requirements. However, only two modes are not flexible enough. For example, in scenarios with three or even five copies and high reliability requirements, asynchronous replication cannot meet the requirements, but synchronous replication needs to wait for each copy to confirm before returning, which seriously affects efficiency in the case of many copies. On the other hand, in the synchronous replication mode, if one of the Slaves in the copy group becomes inactive, the entire send will fail until manual processing is performed. + +Therefore, RocketMQ 5 introduces quorum write for copy groups. In the synchronous replication mode, the user can specify on the broker side how many copies need to be written before returning after sending, and provides an adaptive downgrade method that can automatically downgrade based on the number of surviving copies and the CommitLog gap. + +## Architecture and Parameters + +### Quorum Write + +quorum write is supported by adding two parameters: + +- **totalReplicas**:Total number of brokers in the copy replica. default is 1. +- **inSyncReplicas**:The number of replica groups that should normally be kept in synchronization. default is 1. + +With these two parameters, you can flexibly specify the number of copies that need ACK in the synchronous replication mode. + +![](https://s4.ax1x.com/2022/02/05/HnWHKI.png) + +As shown in the above figure, in the case of two copies, if inSyncReplicas is 2, the message needs to be copied in both the Master and the Slave before it is returned to the client; in the case of three copies, if inSyncReplicas is 2, the message needs to be copied in the Master and any slave before it is returned to the client. In the case of four copies, if inSyncReplicas is 3, the message needs to be copied in the Master and any two slaves before it is returned to the client. By flexibly setting totalReplicas and inSyncReplicas, users can meet the needs of various scenarios. + +### Automatic downgrade + +The standards for automatic downgrade are: + +- The number of surviving replicas in the current replica group +- The height difference between the Master Commitlog and the Slave CommitLog + +> **NOTE: Automatic downgrade is only effective after the slaveActingMaster mode is enabled** + +The current survival information of the copy group can be obtained through the reverse notification of the Nameserver and the GetBrokerMemberGroup request, and the height difference between the Master and the Slave Commitlog can also be calculated through the position record in the HA service. The following parameters will be added to complete the automatic downgrade: + +- **minInSyncReplicas**:The minimum number of copies in the group that must be kept in sync, only effective when enableAutoInSyncReplicas is true, default is 1 +- **enableAutoInSyncReplicas**:The switch for automatic synchronization downgrade, when turned on, if the number of brokers in the current copy group in the synchronization state (including the master itself) does not meet the number specified by inSyncReplicas, it will be synchronized according to minInSyncReplicas. The synchronization state judgment condition is that the slave commitLog lags behind the master length by no more than haSlaveFallBehindMax. The default is false. +- **haMaxGapNotInSync**:The value for determining whether the slave is in sync with the master. If the slave commitLog lags behind the master length by more than this value, the slave is considered to be out of sync. When enableAutoInSyncReplicas is turned on, the smaller the value, the easier it is to trigger automatic downgrade of the master. When enableAutoInSyncReplicas is turned off and `totalReplicas == inSyncReplicas`, the smaller the value, the more likely it is to cause requests to fail during high traffic. Therefore, in this case, it is appropriate to increase haMaxGapNotInSync. The default is 256K. + +Note: In RocketMQ 4.x, there is a haSlaveFallbehindMax parameter, with a default value of 256MB, indicating the CommitLog height difference at which the Slave is considered unavailable. This parameter was cancelled in [RIP-34](https://github.com/apache/rocketmq/wiki/RIP-34-Support-quorum-write-and-adaptive-degradation-in-master-slave-architecture). + +```java +//calculate needAckNums +int inSyncReplicas = Math.min(this.defaultMessageStore.getAliveReplicaNumInGroup(), + this.defaultMessageStore.getHaService().inSyncSlaveNums(currOffset) + 1); +needAckNums = calcNeedAckNums(inSyncReplicas); +if (needAckNums > inSyncReplicas) { + // Tell the producer, don't have enough slaves to handle the send request + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.IN_SYNC_REPLICAS_NOT_ENOUGH, null)); +} + +private int calcNeedAckNums(int inSyncReplicas) { + int needAckNums = this.defaultMessageStore.getMessageStoreConfig().getInSyncReplicas(); + if (this.defaultMessageStore.getMessageStoreConfig().isEnableAutoInSyncReplicas()) { + needAckNums = Math.min(needAckNums, inSyncReplicas); + needAckNums = Math.max(needAckNums, this.defaultMessageStore.getMessageStoreConfig().getMinInSyncReplicas()); + } + return needAckNums; +} +``` + +When enableAutoInSyncReplicas=true, the adaptive downgrade mode is enabled. When the number of surviving replicas in the replica group decreases or the height difference between the Master and the Slave Commitlog is too large, automatic downgrade will be performed, with a minimum of minInSyncReplicas replicas. For example, in two replicas, if totalReplicas=2, InSyncReplicas=2, minInSyncReplicas=1, and enableAutoInSyncReplicas=true are set, under normal circumstances, the two replicas will be in synchronous replication. When the Slave goes offline or hangs, adaptive downgrade will be performed, and the producer only needs to send to the master to succeed. + +## Compatibility + +To ensure backward compatibility, users need to set the correct parameters. For example, if the user's original cluster is a two-replica synchronous replication and no parameters are modified, when upgrading to the RocketMQ 5 version, due to the default totalReplicas and inSyncReplicas both being 1, it will downgrade to asynchronous replication. If you want to maintain the same behavior as before, you need to set both totalReplicas and inSyncReplicas to 2. + +**references:** + +- [RIP-34](https://github.com/apache/rocketmq/wiki/RIP-34-Support-quorum-write-and-adaptive-degradation-in-master-slave-architecture) \ No newline at end of file diff --git a/docs/en/README.md b/docs/en/README.md new file mode 100644 index 00000000000..2a096c3340a --- /dev/null +++ b/docs/en/README.md @@ -0,0 +1,52 @@ +Apache RocketMQ Developer Guide +-------- + +##### This guide helps developers to understand and use Apache RocketMQ quickly. + +### 1. Concepts & Features + +- [Concept](Concept.md): introduce basic concepts in RocketMQ. + +- [Feature](Feature.md): introduce functional features of RocketMQ's implementations. + + +### 2. Architecture Design + +- [Architecture](architecture.md): introduce RocketMQ's deployment and technical architecture. + +- [Design](design.md): introduce design concept of RocketMQ's key mechanisms, including message storage, communication mechanisms, message filter, loadbalance, transaction message, etc. + + +### 3. Example + +- [Example](RocketMQ_Example.md): introduce RocketMQ's common usage, including basic example, sequence message example, delay message example, batch message example, filter message example, transaction message example, etc. + + +### 4. Best Practice +- [Best Practice](best_practice.md): introduce RocketMQ's best practice, including producer, consumer, broker, NameServer, configuration of client, and the best parameter configuration of JVM, linux. + +- [Message Trace](msg_trace/user_guide.md): introduce how to use RocketMQ's message tracing feature. + +- [Auth Management](acl/Operations_ACL.md): introduce how to deployment quickly and how to use RocketMQ cluster enabling auth management feature. + +- [Quick Start](dledger/quick_start.md): introduce how to deploy Dledger quickly. + +- [Cluster Deployment](dledger/deploy_guide.md): introduce how to deploy Dledger in cluster. + +- [Proxy Deployment](proxy/deploy_guide.md) + Introduce how to deploy proxy (both `Local` mode and `Cluster` mode). + +### 5. Operation and maintenance management +- [Operation](operation.md): introduce RocketMQ's deployment modes that including single-master mode, multi-master mode, multi-master multi-slave mode and so on, as well as the usage of operation tool mqadmin. + + +### 6. API Reference(TODO) + +- [DefaultMQProducer API Reference](client/java/API_Reference_DefaultMQProducer.md) + + + + + + + diff --git a/docs/en/RocketMQ_Example.md b/docs/en/RocketMQ_Example.md new file mode 100644 index 00000000000..48011aff9aa --- /dev/null +++ b/docs/en/RocketMQ_Example.md @@ -0,0 +1,15 @@ +### Examples List + +- [basic example](Example_Simple.md) + +- [sequence message example](Example_Orderly.md) + +- [delay message example](Example_Delay.md) + +- [batch message example](Example_Batch.md) + +- [filter message example](Example_Filter.md) + +- [transaction message example](Example_Transaction.md) + +- [openmessaging example](Example_OpenMessaging.md) diff --git a/docs/en/Troubleshoopting.md b/docs/en/Troubleshoopting.md new file mode 100644 index 00000000000..ee4adab8cfb --- /dev/null +++ b/docs/en/Troubleshoopting.md @@ -0,0 +1,76 @@ +# Operation FAQ + +## 1 RocketMQ's mqadmin command error. + +> Problem: Sometimes after deploying the RocketMQ cluster, when you try to execute some commands of "mqadmin", the following exception will appear: +> +> ```java +> org.apache.rocketmq.remoting.exception.RemotingConnectException: connect to failed +> ``` + +Solution: Execute `export NAMESRV_ADDR=ip:9876` (ip refers to the address of NameServer deployed in the cluster) on the VM that deploys the RocketMQ cluster.Then you will execute commands of "mqadmin" successfully. + +## 2 The inconsistent version of RocketMQ between the producer and consumer leads to the problem that message can't be consumed normally. + +> Problem: The same producer sends a message, consumer A can consume, but consumer B can't consume, and the RocketMQ Console appears: +> +> ```java +> Not found the consumer group consume stats, because return offset table is empty, maybe the consumer not consume any message +> ``` + +Solution: The jar package of RocketMQ, such as rocketmq-client, should be the same version on the consumer and producer. + +## 3 When adding a new topic consumer group, historical messages can't be consumed. + +> Problem: When a new consumer group of the same topic is started, the consumed message is the current offset message, and the historical message is not obtained. + +Solution: The default policy of rocketmq is to start from the end of the message queue and skip the historical message. If you want to consume historical message, you need to set: + +```java +org.apache.rocketmq.client.consumer.DefaultMQPushConsumer#setConsumeFromWhere +``` + +There are three common configurations: + +- By default, a new subscription group starts to consume from the end of the queue for the first time, and then restarts and continue to consume from the last consume position, that is, to skip the historical message. + +```java +consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); +``` + +- A new subscription group starts to consume from the head of the queue for the first time, and then restarts and continue to consume from the last consume position, that is, to consume the historical message that is not expired on Broker. + +```java +consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); +``` + +- A new subscription group starts to consume from the specified time point for the first time, and then restarts and continue to consume from the last consume position. It is used together with `consumer.setConsumeTimestamp()`. The default is half an hour ago. + +```java +consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_TIMESTAMP); +``` + +## 4 How to enable reading data from Slave + +In some cases, the Consumer needs to reset the consume position to 1-2 days ago. At this time, on the Master Broker with limited memory, the CommitLog will carry a relatively heavy IO pressure, affecting the reading and writing of other messages on that Broker. You can enable `slaveReadEnable=true`. When Master Broker finds that the difference between the Consumer's consume position and the latest value of CommitLog exceeds the percentage of machine's memory (`accessMessageInMemoryMaxRatio=40%`), it will recommend Consumer to read from Slave Broker and relieve Master Broker's IO. + +## 5 Performance + +Asynchronous flush disk is recommended to use spin lock. + +Synchronous flush disk is recommended to use reentrant lock. Adjust the Broker configuration item `useReentrantLockWhenPutMessage`, and the default value is false. + +Asynchronous flush disk is recommended to open `TransientStorePoolEnable` and close `transferMsgByHeap` to improve the efficiency of pulling message; + +Synchronous flush disk is recommended to increase the `sendMessageThreadPoolNums` appropriately. The specific configuration needs to be tested. + +## 6 The meaning and difference between msgId and offsetMsgId in RocketMQ + +After sending message with RocketMQ, you will usually see the following log: + +```java +SendResult [sendStatus=SEND_OK, msgId=0A42333A0DC818B4AAC246C290FD0000, offsetMsgId=0A42333A00002A9F000000000134F1F5, messageQueue=MessageQueue [topic=topicTest1, BrokerName=mac.local, queueId=3], queueOffset=4] +``` + +- msgId, for the client, the msgId is generated by the producer instance. Specifically, the method `MessageClientIDSetter.createUniqIDBuffer()` is called to generate a unique Id. +- offsetMsgId, offsetMsgId is generated by the Broker server when writing a message ( string consists of "IP address + port" and "CommitLog's physical offset address"), and offsetMsgId is the messageId used to query in the RocketMQ console. diff --git a/docs/en/acl/Operations_ACL.md b/docs/en/acl/Operations_ACL.md new file mode 100644 index 00000000000..0651ea8b060 --- /dev/null +++ b/docs/en/acl/Operations_ACL.md @@ -0,0 +1,76 @@ +# Access control list +## Overview +This document focuses on how to quickly deploy and use a RocketMQ cluster that supports the privilege control feature. + +## 1. Access control features +Access Control (ACL) mainly provides Topic resource level user access control for RocketMQ.If you want to enable RocketMQ permission control, you can inject the AccessKey and SecretKey signatures through the RPCHook on the Client side.And then, the corresponding permission control attributes (including Topic access rights, IP whitelist and AccessKey and SecretKey signature) are set in the configuration file of distribution/conf/plain_acl.yml.The Broker side will check the permissions owned by the AccessKey, and if the verification fails, an exception is thrown; +The source code about ACL on the Client side can be find in **org.apache.rocketmq.example.simple.AclClient.java** + +## 2. Access control definition and attribute values +### 2.1 Access control definition +The definition of Topic resource access control for RocketMQ is mainly as shown in the following table. + +| Permission | explanation | +| --- | --- | +| DENY | permission deny | +| ANY | PUB or SUB permission | +| PUB | Publishing permission | +| SUB | Subscription permission | + +### 2.2 Main properties +| key | value | explanation | +| --- | --- | --- | +| globalWhiteRemoteAddresses | string |Global IP whitelist,example:
    \*;
    192.168.\*.\*;
    192.168.0.1 | +| accessKey | string | Access Key | +| secretKey | string | Secret Key | +| whiteRemoteAddress | string | User IP whitelist,example:
    \*;
    192.168.\*.\*;
    192.168.0.1 | +| admin | true;false | Whether an administrator account | +| defaultTopicPerm | DENY;PUB;SUB;PUB\|SUB | Default Topic permission | +| defaultGroupPerm | DENY;PUB;SUB;PUB\|SUB | Default ConsumerGroup permission | +| topicPerms | topic=permission | Topic only permission | +| groupPerms | group=permission | ConsumerGroup only permission | + +For details, please refer to the **distribution/conf/plain_acl.yml** configuration file. + +## 3. Cluster deployment with permission control +After defining the permission attribute in the **distribution/conf/plain_acl.yml** configuration file as described above, open the **aclEnable** switch variable to enable the ACL feature of the RocketMQ cluster.The configuration file of the ACL feature enabled on the broker is as follows: +```properties +brokerClusterName=DefaultCluster +brokerName=broker-a +brokerId=0 +deleteWhen=04 +fileReservedTime=48 +brokerRole=ASYNC_MASTER +flushDiskType=ASYNC_FLUSH +storePathRootDir=/data/rocketmq/rootdir-a-m +storePathCommitLog=/data/rocketmq/commitlog-a-m +autoCreateSubscriptionGroup=true +## if acl is open,the flag will be true +aclEnable=true +listenPort=10911 +brokerIP1=XX.XX.XX.XX1 +namesrvAddr=XX.XX.XX.XX:9876 +``` +## 4. Main process of access control +The main ACL process is divided into two parts, including privilege resolution and privilege check. + +### 4.1 Privilege resolution +The Broker side parses the client's RequestCommand request and obtains the attribute field that needs to be authenticated. +main attributes: + (1) AccessKey:Similar to the user name, on behalf of the user entity, the permission data corresponds to it; + (2) Signature:The client obtains the string according to the signature of the SecretKey, and the server uses the SecretKey to perform signature verification. + +### 4.2 Privilege check +The check logic of the right side of the broker is mainly divided into the following steps: + (1) Check if the global IP whitelist is hit; if yes, the check passes; otherwise, go to step (2); + (2) Check if the user IP whitelist is hit; if yes, the check passes; otherwise, go to step (3); + (3) Check the signature, if the verification fails, throw an exception; if the verification passes, go to step (4); + (4) Check the permissions required by the user request and the permissions owned by the user; if not, throw an exception; + + +The verification of the required permissions of the user requires attention to the following points: + (1) Special requests such as UPDATE_AND_CREATE_TOPIC can only be operated by the admin account; + (2) For a resource, if there is explicit configuration permission, the configured permission is used; if there is no explicit configuration permission, the default permission is adopted; + +## 5. Hot loading modified Access control +The default implementation of RocketMQ's permission control store is based on the yml configuration file. Users can dynamically modify the properties defined by the permission control without restarting the Broker service node. diff --git a/docs/en/architecture.md b/docs/en/architecture.md new file mode 100644 index 00000000000..863a62200a2 --- /dev/null +++ b/docs/en/architecture.md @@ -0,0 +1,46 @@ +# Architecture design + +## Technology Architecture +![](image/rocketmq_architecture_1.png) + +The RocketMQ architecture is divided into four parts, as shown in the figure above: + + +- Producer: The role of message publishing supports distributed cluster mode deployment. Producer selects the corresponding Broker cluster queue for message delivery through MQ's load balancing module. The delivery process supports fast failure and low latency. + +- Consumer: The role of message consumption supports distributed cluster deployment. Support push, pull two modes to consume messages. It also supports cluster mode and broadcast mode consumption, and it provides a real-time message subscription mechanism to meet the needs of most users. + +- NameServer: NameServer is a very simple Topic routing registry with a role similar to ZooKeeper in Dubbo, which supports dynamic registration and discovery of Broker. It mainly includes two functions: Broker management, NameServer accepts the registration information of the Broker cluster and saves it as the basic data of the routing information. Then provide a heartbeat detection mechanism to check whether the broker is still alive; routing information management, each NameServer will save the entire routing information about the Broker cluster and the queue information for the client query. Then the Producer and Consumer can know the routing information of the entire Broker cluster through the NameServer, so as to deliver and consume the message. The NameServer is usually deployed in a cluster mode, and each instance does not communicate with each other. Broker registers its own routing information with each NameServer, so each NameServer instance stores a complete routing information. When a NameServer is offline for some reason, the Broker can still synchronize its routing information with other NameServers. The Producer and Consumer can still dynamically sense the information of the Broker's routing. + +- BrokerServer: Broker is responsible for the storage, delivery and query of messages and high availability guarantees. In order to achieve these functions, Broker includes the following important sub-modules. +1. Remoting Module: The entire broker entity handles requests from the clients side. +2. Client Manager: Topic subscription information for managing the client (Producer/Consumer) and maintaining the Consumer +3. Store Service: Provides a convenient and simple API interface for handling message storage to physical hard disks and query functions. +4. HA Service: Highly available service that provides data synchronization between Master Broker and Slave Broker. +5. Index Service: The message delivered to the Broker is indexed according to a specific Message key to provide a quick query of the message. + +![](image/rocketmq_architecture_2.png) + +## Deployment architecture + + +![](image/rocketmq_architecture_3.png) + + +### RocketMQ Network deployment features + +- NameServer is an almost stateless node that can be deployed in a cluster without any information synchronization between nodes. + +- The broker deployment is relatively complex. The Broker is divided into the Master and the Slave. One Master can correspond to multiple Slaves. However, one Slave can only correspond to one Master. The correspondence between the Master and the Slave is defined by specifying the same BrokerName and different BrokerId. The BrokerId is 0. Indicates Master, non-zero means Slave. The Master can also deploy multiple. Each broker establishes a long connection with all nodes in the NameServer cluster, and periodically registers Topic information to all NameServers. Note: The current RocketMQ version supports a Master Multi Slave on the deployment architecture, but only the slave server with BrokerId=1 will participate in the read load of the message. + +- The Producer establishes a long connection with one of the nodes in the NameServer cluster (randomly selected), periodically obtains Topic routing information from the NameServer, and establishes a long connection to the Master that provides the Topic service, and periodically sends a heartbeat to the Master. Producer is completely stateless and can be deployed in a cluster. + +- The Consumer establishes a long connection with one of the nodes in the NameServer cluster (randomly selected), periodically obtains Topic routing information from the NameServer, and establishes a long connection to the Master and Slave that provides the Topic service, and periodically sends heartbeats to the Master and Slave. The Consumer can subscribe to the message from the Master or subscribe to the message from the Slave. When the consumer pulls the message to the Master, the Master server will generate a read according to the distance between the offset and the maximum offset. I/O), and whether the server is readable or not, the next time it is recommended to pull from the Master or Slave. + +Describe the cluster workflow in conjunction with the deployment architecture diagram: + +- Start the NameServer, listen to the port after the NameServer, and wait for the Broker, Producer, and Consumer to connect, which is equivalent to a routing control center. +- The Broker starts, keeps a long connection with all NameServers, and sends heartbeat packets periodically. The heartbeat packet contains the current broker information (IP+ port, etc.) and stores all Topic information. After the registration is successful, there is a mapping relationship between Topic and Broker in the NameServer cluster. +- Before sending and receiving a message, create a Topic. When creating a Topic, you need to specify which Brokers the Topic should be stored on, or you can automatically create a Topic when sending a message. +- Producer sends a message. When starting, it first establishes a long connection with one of the NameServer clusters, and obtains from the NameServer which Brokers are currently sent by the Topic. Polling selects a queue from the queue list and then establishes with the broker where the queue is located. Long connection to send a message to the broker. +- The Consumer is similar to the Producer. It establishes a long connection with one of the NameServers, obtains which Brokers the current Topic exists on, and then directly establishes a connection channel with the Broker to start consuming messages. diff --git a/docs/en/best_practice.md b/docs/en/best_practice.md new file mode 100755 index 00000000000..da279e297ed --- /dev/null +++ b/docs/en/best_practice.md @@ -0,0 +1,108 @@ +# Best practices + +## 1 Producer +### 1.1 Attention of send message + +#### 1 Uses of tags +An application should use one topic as far as possible, but identify the message's subtype with tags.Tags can be set freely by the application. +Only when producers set tags while sending messages, can consumers to filter messages through broker with tags when subscribing messages: message.setTags("TagA"). + +#### 2 Uses of keys +The unique identifier for each message at the business level set to the Keys field to help locate message loss problems in the future. +The server creates an index(hash index) for each message, and the application can query the message content via Topic,key,and who consumed the message. +Since it is a hash index, make sure that the key is as unique as possible to avoid potential hash conflicts. + +```java + // order id + String orderId = "20034568923546"; + message.setKeys(orderId); +``` +If you have multiple keys for a message, please concatenate them with 'KEY_SEPARATOR' char, as shown below: +```java + // order id + String orderId = "20034568923546"; + String otherKey = "19101121210831"; + String keys = new StringBuilder(orderId) + .append(org.apache.rocketmq.common.message.MessageConst.KEY_SEPARATOR) + .append(otherKey).toString(); + message.setKeys(keys); +``` +And if you want to query the message, please use `orderId` and `otherKey` to query respectively instead of `keys`, +because the server will unwrap `keys` with `KEY_SEPARATOR` and create corresponding index. +In the above example, the server will create two indexes, one for `orderId` and one for `otherKey`. +#### 3 Log print +Print the message log when send success or failed, make sure to print the SendResult and key fields. +Send messages is successful as long as it does not throw exception. Send successful will have multiple states defined in sendResult. +Each state is describing below: + +- **SEND_OK** + +Message send successfully.Note that even though message send successfully, but it doesn't mean than it is reliable. +To make sure nothing lost, you should also enable the SYNC_MASTER or SYNC_FLUSH. + +- **FLUSH_DISK_TIMEOUT** + +Message send successfully, but the server flush messages to disk timeout.At this point, the message has entered the server's memory, and the message will be lost only when the server is down. +Flush mode and sync flush time interval can be set in the configuration parameters. It will return FLUSH_DISK_TIMEOUT when Broker server doesn't finish flush message to disk in timeout(default is 5s +) when sets FlushDiskType=SYNC_FLUSH(default is async flush). + +- **FLUSH_SLAVE_TIMEOUT** + +Message send successfully, but sync to slave timeout.At this point, the message has entered the server's memory, and the message will be lost only when the server is down. +It will return FLUSH_SLAVE_TIMEOUT when Broker server role is SYNC_MASTER(default is ASYNC_MASTER),and it doesn't sync message to slave successfully in the timeout(default 5s). + +- **SLAVE_NOT_AVAILABLE** + +Message send successfully, but slave is not available.It will return SLAVE_NOT_AVAILABLE when Broker role is SYNC_MASTER(default is ASYNC_MASTER), and it doesn't have a slave server available. + +### 1.2 Handling of message send failure +Send method of producer itself supports internal retry. The logic of retry is as follows: +- At most twice. +- Try next broker when sync send mode, try current broker when async mode. The total elapsed time of this method does not exceed the value of sendMsgTimeout(default is 10s). +- It will not be retried when the message is sent to the Broker with a timeout exception. + +The strategy above ensures the success of message sending to some extent.If the business has a high requirement for message reliability, it is recommended to add the corresponding retry logic: +for example, if the sync send method fails, try to store the message in DB, and then retry periodically by the bg thread to ensure the message must send to broker successfully. + +Why the above DB retry strategy is not integrated into the MQ client, but requires the application to complete it by itself is mainly based on the following considerations: +First, the MQ client is designed to be stateless mode, convenient for arbitrary horizontal expansion, and only consumes CPU, memory and network resources. +Second, if the MQ client internal integration a KV storage module, the data can only be relatively reliable when sync flush to disk, but the sync flush will cause performance lose, so it's usually + use async flush.Because the application shutdown is not controlled by the MQ operators, A violent shutdown like kill -9 may often occur, resulting in data not flushed to disk and being lost. +Thirdly, the producer is a virtual machine with low reliability, which is not suitable for storing important data. +In conclusion, it is recommended that the retry process must be controlled by the application. + +### 1.3 Send message by oneway +Typically, this is the process by which messages are sent: + +- Client send request to server +- Server process request +- Server response to client +So, the time taken to send a message is the sum of the three steps above.Some scenarios require very little time, but not much reliability, such as log collect application. +This type application can use oneway to send messages. Oneway only send request without waiting for a reply, and send a request at the client implementation level is simply the overhead of an + operating system call that writes data to the client's socket buffer, this process that typically takes microseconds. + +## 2 Consumer + +## 3 Broker + +### 3.1 Broker Role + +### 3.2 FlushDiskType + +### 3.3 Broker Configuration +| Parameter name | Default | Description | +| -------------------------------- | ----------------------------- | ------------------------------------------------------------ | +| listenPort | 10911 | listen port for client | +| namesrvAddr | null | name server address | +| brokerIP1 | InetAddress for network interface | Should be configured if having multiple addresses | +| brokerIP2 | InetAddress for network interface | If configured for the Master broker in the Master/Slave cluster, slave broker will connect to this port for data synchronization | +| brokerName | null | broker name | +| brokerClusterName | DefaultCluster | this broker belongs to which cluster | +| brokerId | 0 | broker id, 0 means master, positive integers mean slave | +| storePathRootDir | $HOME/store/ | file path for root store | +| storePathCommitLog | $HOME/store/commitlog/ | file path for commit log | +| mappedFileSizeCommitLog | 1024 * 1024 * 1024(1G) | mapped file size for commit log |​ +| deleteWhen | 04 | When to delete the commitlog which is out of the reserve time |​ +| fileReserverdTime | 72 | The number of hours to keep a commitlog before deleting it |​ +| brokerRole | ASYNC_MASTER | SYNC_MASTER/ASYNC_MASTER/SLAVE |​ +| flushDiskType | ASYNC_FLUSH | {SYNC_FLUSH/ASYNC_FLUSH}. Broker of SYNC_FLUSH mode flushes each message onto disk before acknowledging producer. Broker of ASYNC_FLUSH mode, on the other hand, takes advantage of group-committing, achieving better performance. |​ \ No newline at end of file diff --git a/docs/en/client/java/API_Reference_DefaultMQProducer.md b/docs/en/client/java/API_Reference_DefaultMQProducer.md new file mode 100644 index 00000000000..e32422b6f12 --- /dev/null +++ b/docs/en/client/java/API_Reference_DefaultMQProducer.md @@ -0,0 +1,71 @@ +## DefaultMQProducer +--- +### Class introduction + +`public class DefaultMQProducer +extends ClientConfig +implements MQProducer` + +>`DefaultMQProducer` is the entry point for an application to post messages, out of the box, ca quickly create a producer with a no-argument construction. it is mainly responsible for message sending, support synchronous、asynchronous、one-way send. All of these send methods support batch send. The parameters of the sender can be adjusted through the getter/setter methods , provided by this class. `DefaultMQProducer` has multi send method and each method is slightly different. Make sure you know the usage before you use it . Blow is a producer example . [see more examples](https://github.com/apache/rocketmq/blob/master/example/src/main/java/org/apache/rocketmq/example/). + +``` java +public class Producer { + public static void main(String[] args) throws MQClientException { + // create a produce with producer_group_name + DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName"); + + // start the producer + producer.start(); + + for (int i = 0; i < 128; i++) + try { + // construct the msg + Message msg = new Message("TopicTest", + "TagA", + "OrderID188", + "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); + + // send sync + SendResult sendResult = producer.send(msg); + + // print the result + System.out.printf("%s%n", sendResult); + } catch (Exception e) { + e.printStackTrace(); + } + + producer.shutdown(); + } +} +``` + +**Note** : This class is thread safe. It can be safely shared between multiple threads after configuration and startup is complete. + +### Variable + +|Type|Name| description | +|------|-------|-------| +|DefaultMQProducerImpl|defaultMQProducerImpl|The producer's internal default implementation| +|String|producerGroup|The producer's group| +|String|createTopicKey| Topics that do not exist on the server are automatically created when the message is sent | +|int|defaultTopicQueueNums|The default number of queues to create a topic| +|int|sendMsgTimeout|The timeout for the message to be sent| +|int|compressMsgBodyOverHowmuch|the threshold of the compress of message body| +|int|retryTimesWhenSendFailed|Maximum number of internal attempts to send a message in synchronous mode| +|int|retryTimesWhenSendAsyncFailed|Maximum number of internal attempts to send a message in asynchronous mode| +|boolean|retryAnotherBrokerWhenNotStoreOK|Whether to retry another broker if an internal send fails| +|int|maxMessageSize| Maximum length of message body | +|TraceDispatcher|traceDispatcher| Message trackers. Use rcpHook to track messages | + +### construction method + +|Method name|Method description| +|-------|------------| +|DefaultMQProducer()| creates a producer with default parameter values | +|DefaultMQProducer(final String producerGroup)| creates a producer with producer group name. | +|DefaultMQProducer(final String producerGroup, boolean enableMsgTrace)|creates a producer with producer group name and set whether to enable message tracking| +|DefaultMQProducer(final String producerGroup, boolean enableMsgTrace, final String customizedTraceTopic)|creates a producer with producer group name and set whether to enable message tracking、the trace topic.| +|DefaultMQProducer(RPCHook rpcHook)|creates a producer with a rpc hook.| +|DefaultMQProducer(final String producerGroup, RPCHook rpcHook)|creates a producer with a rpc hook and producer group.| +|DefaultMQProducer(final String producerGroup, RPCHook rpcHook, boolean enableMsgTrace,final String customizedTraceTopic)|all of above.| + diff --git a/docs/en/controller/deploy.md b/docs/en/controller/deploy.md new file mode 100644 index 00000000000..84849510ef3 --- /dev/null +++ b/docs/en/controller/deploy.md @@ -0,0 +1,137 @@ +# Deployment and upgrade guidelines + +## Controller deployment + + If the controller needs to be fault-tolerant, it needs to be deployed in three or more replicas (following the Raft majority protocol). + +> Controller can also complete Broker Failover with only one deployment, but if the single point Controller fails, it will affect the switching ability, but will not affect the normal reception and transmission of the existing cluster. + +There are two ways to deploy Controller. One is to embed it in NameServer for deployment, which can be opened through the configuration enableControllerInNamesrv (it can be opened selectively and is not required to be opened on every NameServer). In this mode, the NameServer itself is still stateless, that is, if the NameServer crashes in the embedded mode, it will only affect the switching ability and not affect the original routing acquisition and other functions. The other is independent deployment, which requires separate deployment of the controller. + +### Embed NameServer deployment + +When embedded in NameServer deployment, you only need to set `enableControllerInNamesrv=true` in the NameServer configuration file and fill in the controller configuration. + +``` +enableControllerInNamesrv = true +controllerDLegerGroup = group1 +controllerDLegerPeers = n0-127.0.0.1:9877;n1-127.0.0.1:9878;n2-127.0.0.1:9879 +controllerDLegerSelfId = n0 +controllerStorePath = /home/admin/DledgerController +enableElectUncleanMaster = false +notifyBrokerRoleChanged = true +``` + +Parameter explain: + +- enableControllerInNamesrv: Whether to enable controller in Nameserver, default is false. +- controllerDLegerGroup: The name of the DLedger Raft Group, all nodes in the same DLedger Raft Group should be consistent. +- controllerDLegerPeers: The port information of the nodes in the DLedger Group, the configuration of each node in the same Group must be consistent. +- controllerDLegerSelfId: The node id, must belong to one of the controllerDLegerPeers; unique within the Group. +- controllerStorePath: The location to store controller logs. Controller is stateful and needs to rely on logs to recover data when restarting or crashing, this directory is very important and should not be easily deleted. +- enableElectUncleanMaster: Whether it is possible to elect Master from outside SyncStateSet, if true, it may select a replica with lagging data as Master and lose messages, default is false. +- notifyBrokerRoleChanged: Whether to actively notify when the role of the broker replica group changes, default is true. + +Some other parameters can be referred to in the ControllerConfig code. + +After setting the parameters, start the Nameserver by specifying the configuration file. + +### Independent deployment + +To deploy independently, execute the following script: + +```shell +sh bin/mqcontroller -c controller.conf +``` +The mqcontroller script is located at distribution/bin/mqcontroller, and the configuration parameters are the same as in embedded mode. + +## Broker Controller mode deployment + +The Broker start method is the same as before, with the following parameters added: + +- enableControllerMode: The overall switch for the Broker controller mode, only when this value is true will the controller mode be opened. Default is false. +- controllerAddr: The address of the controller, separated by semicolons if there are multiple controllers. For example, `controllerAddr = 127.0.0.1:9877;127.0.0.1:9878;127.0.0.1:9879` +- syncBrokerMetadataPeriod: The interval for synchronizing Broker replica information with the controller. Default is 5000 (5s). +- checkSyncStateSetPeriod: The interval for checking SyncStateSet, checking SyncStateSet may shrink SyncState. Default is 5000 (5s). +- syncControllerMetadataPeriod: The interval for synchronizing controller metadata, mainly to obtain the address of the active controller. Default is 10000 (10s). +- haMaxTimeSlaveNotCatchup: The maximum interval that a slave has not caught up to the Master, if a slave in SyncStateSet exceeds this interval, it will be removed from SyncStateSet. Default is 15000 (15s). +- storePathEpochFile: The location to store the epoch file. The epoch file is very important and should not be deleted arbitrarily. Default is in the store directory. +- allAckInSyncStateSet: If this value is true, a message needs to be replicated to each replica in SyncStateSet before it is returned to the client as successful, ensuring that the message is not lost. Default is false. +- syncFromLastFile: If the slave is a blank disk start, whether to replicate from the last file. Default is false. +- asyncLearner: If this value is true, the replica will not enter SyncStateSet, that is, it will not be elected as Master, but will always be a learner replica that performs asynchronous replication. Default is false. +- inSyncReplicas: The number of replica groups that need to be kept in sync, default is 1, inSyncReplicas is invalid when allAckInSyncStateSet=true. +- minInSyncReplicas: The minimum number of replica groups that need to be kept in sync, if the number of replicas in SyncStateSet is less than minInSyncReplicas, putMessage will return PutMessageStatus.IN_SYNC_REPLICAS_NOT_ENOUGH directly, default is 1. + +In Controller mode, the Broker configuration must set `enableControllerMode=true` and fill in controllerAddr. + +### Analysis of important parameters + +Among the parameters such as inSyncReplicas and minInSyncReplicas, there are overlapping and different meanings in normal Master-Slave deployment, SlaveActingMaster mode, and automatic master-slave switching architecture. The specific differences are as follows: + +| | inSyncReplicas | minInSyncReplicas | enableAutoInSyncReplicas | allAckInSyncStateSet | haMaxGapNotInSync | haMaxTimeSlaveNotCatchup | +|----------------------|---------------------------------------------------------------------|--------------------------------------------------------------------------|---------------------------------------------|---------------------------------------------------------------|---------------------------------------|---------------------------------------------------| +| Normal Master-Slave deployment | The number of replicas that need to be ACKed in synchronous replication, invalid in asynchronous replication | invalid | invalid | invalid | invalid | invalid | +| Enable SlaveActingMaster (slaveActingMaster=true) | The number of replicas that need to be ACKed in synchronous replication in the absence of auto-degradation | The minimum number of replicas that need to be ACKed after auto-degradation | Whether to enable auto-degradation, and the minimum number of replicas that need to be ACKed after auto-degradation is reduced to minInSyncReplicas | invalid | Basis for degradation determination: the difference in Commitlog heights between Slave and Master, in bytes | invalid | +| Automatic master-slave switching architecture(enableControllerMode=true) | The number of replicas that need to be ACKed in synchronous replication when allAckInSyncStateSet is not enabled, and this value is invalid when allAckInSyncStateSet is enabled | SyncStateSet can be reduced to the minimum number of replicas, and if the number of replicas in SyncStateSet is less than minInSyncReplicas, it will return directly with insufficient number of replicas | invalid | If this value is true, a message needs to be replicated to every replica in SyncStateSet before it is returned to the client as successful, and this parameter can ensure that the message is not lost | invalid | The minimum time difference between Slave and Master when SyncStateSet is contracted, see [RIP-44](https://shimo.im/docs/N2A1Mz9QZltQZoAD) for details. | + +To summarize: +- In a normal Master-Slave configuration, there is no ability for auto-degradation, and all parameters except for inSyncReplicas are invalid. inSyncReplicas indicates the number of replicas that need to be ACKed in synchronous replication. +- In slaveActingMaster mode, enabling enableAutoInSyncReplicas enables the ability for degradation, and the minimum number of replicas that can be degraded to is minInSyncReplicas. The basis for degradation is the difference in Commitlog heights (haMaxGapNotInSync) and the survival of the replicas, refer to [SlaveActingMaster mode adaptive degradation](../QuorumACK.md). +- Automatic master-slave switching (Controller mode) relies on SyncStateSet contraction for auto-degradation. SyncStateSet replicas can work normally as long as they are contracted to a minimum of minInSyncReplicas. If it is less than minInSyncReplicas, it will return directly with insufficient number of replicas. One of the basis for contraction is the time interval (haMaxTimeSlaveNotCatchup) at which the Slave catches up, rather than the Commitlog height. If allAckInSyncStateSet=true, the inSyncReplicas parameter is invalid. + +## Compatibility + +This mode does not make any changes or modifications to any client-level APIs, and there are no compatibility issues with clients. + +The Nameserver itself has not been modified and there are no compatibility issues with the Nameserver. If enableControllerInNamesrv is enabled and the controller parameters are configured correctly, the controller function is enabled. + +If Broker is set to **`enableControllerMode=false`**, it will still operate as before. If **`enableControllerMode=true`**, the Controller must be deployed and the parameters must be configured correctly in order to operate properly. + +The specific behavior is shown in the following table: + +| | Old nameserver | Old nameserver + Deploy controllers independently | New nameserver enables controller | New nameserver disable controller | +| ---------------------------------- | ------------------------------- | ------------------------------------------------- | --------------------------------- | --------------------------------- | +| Old broker | Normal running, cannot failover | Normal running, cannot failover | Normal running, cannot failover | Normal running, cannot failover | +| New broker enable controller mode | Unable to go online normally | Normal running, can failover | Normal running, can failover | Unable to go online normally | +| New broker disable controller mode | Normal running, cannot failover | Normal running, cannot failover | Normal running, cannot failover | Normal running, cannot failover | + +## Upgrade Considerations + +From the compatibility statements above, it can be seen that NameServer can be upgraded normally without compatibility issues. In the case where the Nameserver is not to be upgraded, the controller component can be deployed independently to obtain switching capabilities. For broker upgrades, there are two cases: + +1. Master-Slave deployment is upgraded to controller switching architecture + + In-place upgrade with data is possible. For each group of Brokers, stop the primary and secondary Brokers and ensure that the CommitLogs of the primary and secondary are aligned (you can either disable writing to this group of Brokers for a certain period of time before the upgrade or ensure consistency by copying). After upgrading the package, restart it. + + > If the primary and secondary CommitLogs are not aligned, it is necessary to ensure that the primary is online before the secondary is online, otherwise messages may be lost due to data truncation. + +2. Upgrade from DLedger mode to Controller switching architecture + + Due to the differences in the format of message data in DLedger mode and Master-Slave mode, there is no in-place upgrade with data. In the case of deploying multiple groups of Brokers, it is possible to disable writing to a group of Brokers for a certain period of time (as long as it is confirmed that all existing messages have been consumed), and then upgrade and deploy the Controller and new Brokers. In this way, the new Brokers will consume messages from the existing Brokers and the existing Brokers will consume messages from the new Brokers until the consumption is balanced, and then the existing Brokers can be decommissioned. + +### Upgrade considerations for persistent BrokerID version + +The current version supports a new high-availability architecture with persistent BrokerID version. Upgrading from version 5.x to the current version requires the following considerations: + +For version 4.x, follow the above procedure to upgrade. + +For upgrading from non-persistent BrokerID version in 5.x to persistent BrokerID version, follow the procedure below: + +**Upgrade Controller** + +1. Stop the old version Controller group. +2. Clear Controller data, i.e., data files located in `~/DLedgerController` by default. +3. Bring up the new version Controller group. + +> During the Controller upgrade process, Broker can still run normally but cannot failover. + +**Upgrade Broker** + +1. Stop the secondary Broker. +2. Stop the primary Broker. +3. Delete all Epoch files of all Brokers, i.e., `~/store/epochFileCheckpoint` and `~/store/epochFileCheckpoint.bak` by default. +4. Bring up the original primary Broker and wait for it to be elected as master (you can use the `getSyncStateSet` command of admin to observe). +5. Bring up all the original secondary Brokers. + +> It is recommended to stop the secondary Broker before stopping the primary Broker and bring up the original primary Broker before the original secondary during the online process. This can ensure the original primary-secondary relationship. +> If you need to change the primary-secondary relationship before and after the upgrade, make sure that the CommitLog of the primary and secondary are aligned when shutting down. Otherwise, data may be truncated and lost. \ No newline at end of file diff --git a/docs/en/controller/design.md b/docs/en/controller/design.md new file mode 100644 index 00000000000..ba2de58af14 --- /dev/null +++ b/docs/en/controller/design.md @@ -0,0 +1,192 @@ +# Background + +In the current RocketMQ Raft mode, the DLedger Commitlog is mainly used to replace the original Commitlog, enabling the Commitlog to have the ability to elect and replicate. However, this also causes some problems: + +- In the Raft mode, the number of replicas within the Broker group must be three or more, and the ACK of the replicas must also follow the majority protocol. +- RocketMQ has two sets of HA replication processes, and the replication in Raft mode cannot utilize RocketMQ's native storage capability. + +Therefore, we hope to use DLedger to implement a consistency module (DLedger Controller) based on Raft, and use it as an optional leader election component. It can be deployed independently or embedded in the Nameserver. The Broker completes the election of the Master through interaction with the Controller, thus solving the above problems. We refer to this new mode as the Controller mode. + +# Architecture + +### Core idea + +![架构图](../image/controller/controller_design_1.png) + +- The following is a description of the core architecture of the Controller mode, as shown in the figure: + - DledgerController: Using DLedger, a DLedger controller that ensures the consistency of metadata is constructed. The Raft election will select an Active DLedger Controller as the main controller. The DLedger Controller can be embedded in the Nameserver or deployed independently. Its main function is to store and manage the SyncStateSet list of Brokers, and actively issue scheduling instructions to switch the Master of the Broker when the Master of the Broker is offline or network isolated. + - SyncStateSet: Mainly represents a set of Slave replicas following the Master in a broker replica group, with the main criterion for judgment being the gap between the Master and the Slave. When the Master is offline, we will select a new Master from the SyncStateSet list. The SyncStateSet list is mainly initiated by the Master Broker. The Master completes the Shrink and Expand of the SyncStateSet through a periodic task to determine and synchronize the SyncStateSet, and initiates an Alter SyncStateSet request to the election component Controller. + - AutoSwitchHAService: A new HAService that, based on DefaultHAService, supports the switching of BrokerRole and the mutual conversion between Master and Slave (under the control of the Controller). In addition, this HAService unifies the log replication process and truncates the logs during the HA HandShake stage. + - ReplicasManager: As an intermediate component, it serves as a link between the upper and lower levels. Upward, it can regularly synchronize control instructions from the Controller, and downward, it can regularly monitor the state of the HAService and modify the SyncStateSet at the appropriate time. The ReplicasManager regularly synchronizes metadata about the Broker from the Controller, and when the Controller elects a new Master, the ReplicasManager can detect the change in metadata and switch the BrokerRole. + +## DLedgerController core design + +![image-20220605213143645](../image/controller/quick-start/controller.png) + +- The following is a description of the core design of the DLedgerController: + - DLedgerController can be embedded in Namesrv or deployed independently. + - Active DLedgerController is the Leader elected by DLedger. It will accept event requests from clients and initiate consensus through DLedger, and finally apply them to the in-memory metadata state machine. + - Not Active DLedgerController, also known as the Follower role, will replicate the event logs from the Active DLedgerController through DLedger and then apply them directly to the state machine. + +## Log replication + +### Basic concepts and processes + +In order to unify the log replication process, distinguish the log replication boundary of each Master, and facilitate log truncation, the concept of MasterEpoch is introduced, which represents the current Master's term number (similar to the meaning of Raft Term). + +For each Master, it has a MasterEpoch and a StartOffset, which respectively represent the term number and the starting log offset of the Master. + +It should be noted that the MasterEpoch is determined by the Controller and is monotonically increasing. + +In addition, we have introduced the EpochFile, which is used to store the \ sequence. + +**When a Broker becomes the Master, it will:** + +- Truncate the Commitlog to the boundary of the last message. +- Persist the latest \ to the EpochFile, where startOffset is the current CommitLog's MaxPhyOffset. +- Then the HAService listens for connections and creates the HAConnection to interact with the Slave to complete the process. + +**When a Broker becomes the Slave, it will:** + +Ready stage: + +- Truncate the Commitlog to the boundary of the last message. +- Establish a connection with the Master. + +Handshake stage: + +- Perform log truncation, where the key is for the Slave to compare its local epoch and startOffset with the Master to find the log truncation point and perform log truncation. + +Transfer stage: + +- Synchronize logs from the Master. + +### Truncation algorithm + +The specific log truncation algorithm flow is as follows: + +- During the Handshake stage, the Slave obtains the Master's EpochCache from the Master. +- The Slave compares the obtained Master EpochCache \, and compares them with the local cache from back to front. If the Epoch and StartOffset of the two are equal, the Epoch is valid, and the truncation point is the smaller Endoffset between them. After truncation, the \ information is corrected and enters the Transfer stage. If they are not equal, the previous epoch of the Slave is compared until the truncation point is found. + +```java +slave:TreeMap> epochMap; +Iterator iterator = epochMap.entrySet().iterator(); +truncateOffset = -1; + +//The epochs are sorted from largest to smallest +while (iterator.hasNext()) { + Map.Entry> curEntry = iterator.next(); + Pair masterOffset= + findMasterOffsetByEpoch(curEntry.getKey()); + + if(masterOffset != null && + curEntry.getKey().getObejct1() == masterOffset.getObejct1()) { + truncateOffset = Math.min(curEntry.getKey().getObejct2(), masterOffset.getObejct2()); + break; + } +} +``` + +### Replication process + +Since HA replicates logs based on stream, we cannot distinguish the boundaries of the logs (that is, a batch of transmitted logs may span multiple MasterEpochs), and the Slave cannot detect changes in MasterEpoch and cannot timely modify EpochFile. + +Therefore, we have made the following improvements: + +When the Master transfers logs, it ensures that a batch of logs sent at a time is in the same epoch, but not spanning multiple epochs. We can add two variables in WriteSocketService: + +- currentTransferEpoch: represents which epoch WriteSocketService.nextTransferFromWhere belongs to +- currentTransferEpochEndOffset: corresponds to the end offset of currentTransferEpoch. If currentTransferEpoch == MaxEpoch, then currentTransferEpochEndOffset= -1, indicating no boundary. + +When WriteSocketService transfers the next batch of logs (assuming the total size of this batch is size), if it finds that nextTransferFromWhere + size > currentTransferEpochEndOffset, it sets selectMappedBufferResult limit to currentTransferEpochEndOffset. Finally, modify currentTransferEpoch and currentTransferEpochEndOffset to the next epoch. + +Correspondingly, when the Slave receives logs, if it finds a change in epoch from the header, it records it in the local epoch file. + +### Replication protocol + +According to the above, we can know the AutoSwitchHaService protocol divides log replication into multiple stages. Below is the protocol for the HaService. + +#### Handshake stage + +1.AutoSwitchHaClient (Slave) will send a HandShake packet to the Master as follows: + +![示意图](../image/controller/controller_design_3.png) + +`current state(4byte) + Two flags(4byte) + slaveAddressLength(4byte) + slaveAddress(50byte)` + +- `Current state` represents the current HAConnectionState, which is HANDSHAKE. + +- Two flags are two status flags, where `isSyncFromLastFile` indicates whether to start copying from the Master's last file, and `isAsyncLearner` indicates whether the Slave is an asynchronous copy and joins the Master as a Learner. + +- `slaveAddressLength` and `slaveAddress` represent the address of the Slave, which will be used later to join the SyncStateSet. + +2.AutoSwitchHaConnection (Master) will send a HandShake packet back to the Slave as follows: + +![示意图](../image/controller/controller_design_4.png) + +`current state(4byte) + body size(4byte) + offset(8byte) + epoch(4byte) + body` + +- `Current state` represents the current HAConnectionState, which is HANDSHAKE. +- `Body size` represents the length of the body. +- `Offset` represents the maximum offset of the log on the Master side. +- `Epoch` represents the Master's Epoch. +- The Body contains the EpochEntryList on the Master side. + +After the Slave receives the packet sent back by the Master, it will perform the log truncation process described above locally. + +#### Transfer stage + +1.AutoSwitchHaConnection (Master) will continually send log packets to the Slave as follows: + +![示意图](../image/controller/controller_design_5.png) + +`current state(4byte) + body size(4byte) + offset(8byte) + epoch(4byte) + epochStartOffset(8byte) + additionalInfo(confirmOffset) (8byte)+ body` + +- `Current state`: represents the current HAConnectionState, which is Transfer. +- `Body size`: represents the length of the body. +- `Offset`: the starting offset of the current batch of logs. +- `Epoch`: represents the MasterEpoch to which the current batch of logs belongs. +- `epochStartOffset`: represents the StartOffset of the MasterEpoch corresponding to the current batch of logs. +- `confirmOffset`: represents the minimum offset among replicas in SyncStateSet. +- `Body`: logs. + +2.AutoSwitchHaClient (Slave) will send an ACK packet to the Master: + +![示意图](../image/controller/controller_design_6.png) + +` current state(4byte) + maxOffset(8byte)` + +- `Current state`: represents the current HAConnectionState, which is Transfer. +- `MaxOffset`: represents the current maximum log offset of the Slave. + +## Elect Master + +### Basic process + +ELectMaster mainly selects a new Master from the SyncStateSet list when the Master of a Broker replica group is offline or inaccessible. This event is initiated by the Controller itself or through the `electMaster` operation command. + +Whether the Controller is deployed independently or embedded in Namesrv, it listens to the connection channels of each Broker. If a Broker channel becomes inactive, it checks whether the Broker is the Master, and if so, it triggers the Master election process. + +The process of electing a Master is relatively simple. We just need to select one from the SyncStateSet list corresponding to the group of Brokers and make it the new Master, and apply the result to the in-memory metadata through the DLedger consensus. Finally, the result is notified to the corresponding Broker replica group. + +### SyncStateSet change + +SyncStateSet is an important basis for electing a Master. Changes to the SyncStateSet list are mainly initiated by the Master Broker. The Master completes the Shrink and Expand of SyncStateSet through a periodic task and initiates an Alter SyncStateSet request to the election component Controller during the synchronization process. + +#### Shrink + +Shrink SyncStateSet refers to the removal of replicas from the SyncStateSet replica set that are significantly behind the Master, based on the following criteria: + +- Increase the haMaxTimeSlaveNotCatchUp parameter. +- HaConnection records the last time the Slave caught up with the Master's timestamp, lastCaughtUpTimeMs, which means: every time the Master sends data (transferData) to the Slave, it records its current MaxOffset as lastMasterMaxOffset and the current timestamp lastTransferTimeMs. +- When ReadSocketService receives slaveAckOffset, if slaveAckOffset >= lastMasterMaxOffset, it updates lastCaughtUpTimeMs to lastTransferTimeMs. +- The Master scans each HaConnection through a periodic task and if (cur_time - connection.lastCaughtUpTimeMs) > haMaxTimeSlaveNotCatchUp, the Slave is Out-of-sync. +- If a Slave is detected to be out of sync, the master immediately reports SyncStateSet to the Controller, thereby shrinking SyncStateSet. + +#### Expand + +If a Slave replica catches up with the Master, the Master needs to timely alter SyncStateSet with the Controller. The condition for adding to SyncStateSet is slaveAckOffset >= ConfirmOffset (the minimum value of MaxOffset among all replicas in the current SyncStateSet). + +## Reference + +[RIP-44](https://github.com/apache/rocketmq/wiki/RIP-44-Support-DLedger-Controller) diff --git a/docs/en/controller/persistent_unique_broker_id.md b/docs/en/controller/persistent_unique_broker_id.md new file mode 100644 index 00000000000..cd076578543 --- /dev/null +++ b/docs/en/controller/persistent_unique_broker_id.md @@ -0,0 +1,129 @@ +# Persistent unique BrokerId + +## Current Issue + +Currently, `BrokerAddress` is used as the unique identifier for the Broker in Controller mode, which causes the following problems: + +* In a container environment, each restart or upgrade of the Broker may result in an IP address change, making it impossible to associate the previous `BrokerAddress` records with the restarted Broker, such as `ReplicaInfo`, `SyncStateSet`, and other data. + +## Improvement Plan + +In the Controller side, `BrokerName:BrokerId` is used as the unique identifier instead of `BrokerAddress`. Also, `BrokerId` needs to be persistently stored. Since `ClusterName` and `BrokerName` are both configured in the configuration file when starting up, only the allocation and persistence of `BrokerId` need to be addressed.When the Broker first comes online, only the `ClusterName`, `BrokerName`, and its own `BrokerAddress` configured in the configuration file are available. Therefore, a unique identifier, `BrokerId`, that is determined throughout the lifecycle of the entire cluster needs to be negotiated with the Controller. The `BrokerId` is assigned starting from 1. When the Broker is selected as the Master, it will be re-registered in the Name Server, and at this point, to be compatible with the previous non-HA Master-Slave architecture, the `BrokerId` needs to be temporarily changed to 0 (where id 0 previously represented that the Broker was a Master). + +### Online Process + +![register process](../image/controller/persistent_unique_broker_id/register_process.png) + +#### 1. GetNextBrokerId Request + +Send a GetNextBrokerId request to the Controller to obtain the next available BrokerId (allocated starting from 1). + +#### 1.1 ReadFromDLedger + +Upon receiving the request, the Controller uses DLedger to retrieve the NextBrokerId data from the state machine. + +#### 2. GetNextBrokerId Response + +The Controller returns the NextBrokerId to the Broker. + +#### 2.1 CreateTempMetaFile + +After receiving the NextBrokerId, the Broker creates a temporary file .broker.meta.temp, which records the NextBrokerId (the expected BrokerId to be applied) and generates a RegisterCode (used for subsequent identity verification), which is also persisted to the temporary file. + +#### 3. ApplyBrokerId Request + +The Broker sends an ApplyBrokerId request to the Controller, carrying its basic data (ClusterName, BrokerName, and BrokerAddress) and the expected BrokerId and RegisterCode. + +#### 3.1 CASApplyBrokerId + +The Controller writes this event to DLedger. When the event (log) is applied to the state machine, it checks whether the BrokerId can be applied (if the BrokerId has already been allocated and is not assigned to the Broker, the application fails). It also records the relationship between the BrokerId and RegisterCode. + +#### 4. ApplyBrokerId Response + +If the previous step successfully applies the BrokerId, the Controller returns success to the Broker; otherwise, it returns the current NextBrokerId. + +#### 4.1 CreateMetaFileFromTemp + +If the BrokerId is successfully applied in the previous step, it can be considered as successfully allocated on the Broker side. At this point, the information of this BrokerId needs to be persisted. This is achieved by atomically deleting the .broker.meta.temp file and creating a .broker.meta file. These two steps need to be atomic operations. + +> After the above process, the Broker and Controller that come online for the first time successfully negotiate a BrokerId that both sides agree on and persist it. + +#### 5. RegisterBrokerToController Request + +The previous steps have correctly negotiated the BrokerId, but at this point, it is possible that the BrokerAddress saved on the Controller side is the BrokerAddress when the last Broker came online. Therefore, the BrokerAddress needs to be updated now by sending a RegisterBrokerToController request with the current BrokerAddress. + +#### 5.1 UpdateBrokerAddress + +The Controller compares the BrokerAddress currently saved in the Controller state machine for this Broker. If it does not match the BrokerAddress carried in the request, it updates it to the BrokerAddress in the request. + +#### 6. RegisterBrokerToController Response + +After updating the BrokerAddress, the Controller can return the master-slave information of the Broker-set where the Broker is located, to notify the Broker to perform the corresponding identity transformation. + +### Registration status rotation + +![register state transfer](../image/controller/persistent_unique_broker_id/register_state_transfer.png) + +### Fault tolerance + +> If various crashes occur during the normal online process, the following process ensures the correct allocation of BrokerId. + +#### Node online after normal restart + +If it is a normal restart, then a unique BrokerId has already been negotiated by both sides, and the broker.meta already has the data for that BrokerId. Therefore, the registration process is not necessary and the subsequent process can be continued directly. That is, continue to come online from RegisterBrokerToController. + +![restart normally](../image/controller/persistent_unique_broker_id/normal_restart.png) + +#### CreateTempMetaFile Failure + +![fail at creating temp metadata file](../image/controller/persistent_unique_broker_id/fail_create_temp_metadata_file.png) + +If the process shown in the figure fails, then after the Broker restarts, the Controller's state machine has not allocated any BrokerId. The Broker itself has not saved any data. Therefore, just restart the process from the beginning as described above. + +#### CreateTempMetaFile success,ApplyBrokerId fail + +If the Controller already considers the ApplyBrokerId request to be incorrect (i.e., requesting to allocate a BrokerId that has already been allocated and the RegisterCode is not equal), and at this time returns the current NextBrokerId to the Broker, then the Broker directly deletes the .broker.meta.temp file and goes back to step 2 to restart the process and subsequent steps. + +![fail at applying broker id](../image/controller/persistent_unique_broker_id/fail_apply_broker_id.png) + +#### ApplyBrokerId success,CreateMetaFileFromTemp fail + +The above situation can occur in the ApplyResult loss, and in the CAS deletion and creation of broker.meta failure processes. After restart, the Controller side thinks that our ApplyBrokerId process has succeeded and has already modified the BrokerId allocation data in the state machine. So at this point, we can directly start step 3 again, which is to send the ApplyBrokerId request. + +![fail at create metadata file](../image/controller/persistent_unique_broker_id/fail_create_metadata_file_and_delete_temp.png) + +Since we have the .broker.meta.temp file, we can retrieve the BrokerId and RegisterCode that were successfully applied on the Controller side, and send them directly to the Controller. If the BrokerId exists in the Controller and the RegisterCode is equal to the one in the request, it is considered successful. + +### After successful registration, use the BrokerId as the unique identifier. + +After successful registration, all subsequent requests and state records for the Broker are identified by BrokerId. The recording of heartbeats and other data is also identified by BrokerId. At the same time, the Controller side will also record the BrokerAddress of the current BrokerId, which will be used to notify the Broker of changes in state such as switching between master and slave. + +## Upgrade plan + +To upgrade to version 4.x, follow the 5.0 upgrade documentation process. +For upgrading from the non-persistent BrokerId version in 5.0.0 or 5.1.0 to the persistent BrokerId version 5.1.1 or above, follow the following steps: + +### Upgrade Controller + +1. Shut down the old version of the Controller group. +2. Clear the Controller data, i.e., the data files located by default in `~/DLedgerController`. +3. Bring up the new version of the Controller group. + +> During the above Controller upgrade process, the Broker can still run normally but cannot be switched. + +### Upgrade Broker + +1. Shut down the Broker slave node. +2. Shut down the Broker master node. +3. Delete all the Epoch files for all Brokers, i.e., the ones located at `~/store/epochFileCheckpoint` and `~/store/epochFileCheckpoint.bak` by default. +4. Bring up the original master Broker and wait for it to be elected as the new master (you can use the `getSyncStateSet` command in the `admin` tool to check). +5. Bring up all the original slave Brokers. + +> It is recommended to shut down the slave Brokers before shutting down the master Broker and bring up the original master Broker before bringing up the original slave Brokers. This will ensure that the original master-slave relationship is maintained. If you need to change the master-slave relationship after the upgrade, you need to make sure that the CommitLog of the old master and slave Brokers are aligned before shutting them down, otherwise data may be truncated and lost. + +### Compatibility + +| | Controller for version 5.1.0 and below | Controller for version 5.1.1 and above | +|------------------------------------|--------------------------------| ------------------------------------------------------------ | +| Broker for version 5.1.0 and below | Normal operation and switch. | Normal operation and no switch if the master-slave relationship is already determined. The Broker cannot be brought up if it is restarted. | +| Broker for version 5.1.1 and above | Cannot be brought up normally. | Normal operation and switch. | \ No newline at end of file diff --git a/docs/en/controller/quick_start.md b/docs/en/controller/quick_start.md new file mode 100644 index 00000000000..e7997213b0f --- /dev/null +++ b/docs/en/controller/quick_start.md @@ -0,0 +1,200 @@ +# Master-Slave automatic switch Quick start + +## Introduction + +![架构图](../image/controller/controller_design_2.png) + +This document mainly introduces how to quickly build a RocketMQ cluster that supports automatic master-slave switch, as shown in the above diagram. The main addition is the Controller component, which can be deployed independently or embedded in the NameServer. + +For detailed design ideas, please refer to [Design ideas](design.md). + +For detailed guidelines on new cluster deployment and old cluster upgrades, please refer to [Deployment guide](deploy.md). + +## Compile RocketMQ source code + +```shell +$ git clone https://github.com/apache/rocketmq.git + +$ cd rocketmq + +$ mvn -Prelease-all -DskipTests clean install -U +``` + +## Quick deployment + +After successful build + +```shell +#{rocketmq-version} replace with rocketmq actual version. example: 5.0.0-SNAPSHOT +$ cd distribution/target/rocketmq-{rocketmq-version}/rocketmq-{rocketmq-version}/ + +$ sh bin/controller/fast-try.sh start +``` + +If the above steps are successful, you can view the status of the Controller using the operation and maintenance command. + +```shell +$ sh bin/mqadmin getControllerMetaData -a localhost:9878 +``` + +`-a` represents the address of any controller in the cluster + +At this point, you can send and receive messages in the cluster and perform switch testing. + +If you need to shut down the cluster quickly , you can execute: + +```shell +$ sh bin/controller/fast-try.sh stop +``` + +For quick deployment, the default configuration is in `conf/controller/quick-start`, the default storage path is `/tmp/rmqstore`, and a controller (embedded in Namesrv) and two brokers will be started. + +### Query SyncStateSet + + Use the operation and maintenance tool to query SyncStateSet: + +```shell +$ sh bin/mqadmin getSyncStateSet -a localhost:9878 -b broker-a +``` + +`-a` represents the address of any controller + +If successful, you should see the following content: + +![image-20220605205259913](../image/controller/quick-start/syncstateset.png) + +### Query BrokerEpoch + + Use the operation and maintenance tool to query BrokerEpochEntry: + +```shell +$ sh bin/mqadmin getBrokerEpoch -n localhost:9876 -b broker-a +``` + +`-n` represents the address of any Namesrv + +If successful, you should see the following content: + +![image-20220605205247476](../image/controller/quick-start/epoch.png) + +## Switch + +After successful deployment, try to perform a master switch now. + +First, kill the process of the original master, in the example above, it is the process using port 30911: + +```shell +#query port: +$ ps -ef|grep java|grep BrokerStartup|grep ./conf/controller/quick-start/broker-n0.conf|grep -v grep|awk '{print $2}' +#kill master: +$ kill -9 PID +``` + +Next,use `SyncStateSet admin` script to query: + +```shell +$ sh bin/mqadmin getSyncStateSet -a localhost:9878 -b broker-a +``` + +The master has switched. + +![image-20220605211244128](../image/controller/quick-start/changemaster.png) + + + +## Deploying controller embedded in Nameserver cluster + +The Controller component is embedded in the Nameserver cluster (consisting of 3 nodes) and quickly started through the plugin mode: + +```shell +$ sh bin/controller/fast-try-namesrv-plugin.sh start +``` + +Alternatively, it can be started separately through a command: + +```shell +$ nohup bin/mqnamesrv -c ./conf/controller/cluster-3n-namesrv-plugin/namesrv-n0.conf & +$ nohup bin/mqnamesrv -c ./conf/controller/cluster-3n-namesrv-plugin/namesrv-n1.conf & +$ nohup bin/mqnamesrv -c ./conf/controller/cluster-3n-namesrv-plugin/namesrv-n2.conf & +``` + +If the above steps are successful, you can check the status of the Controller cluster through operational commands: + +```shell +$ sh bin/mqadmin getControllerMetaData -a localhost:9878 +``` + +`-a` represents the address of any Controller nodes + +If the Controller starts successfully, you can see the following content: + +``` +#ControllerGroup group1 +#ControllerLeaderId n0 +#ControllerLeaderAddress 127.0.0.1:9878 +#Peer: n0:127.0.0.1:9878 +#Peer: n1:127.0.0.1:9868 +#Peer: n2:127.0.0.1:9858 +``` + +After the successful start, the broker controller mode deployment can use the controller cluster. + +If you need to quickly stop the cluster: + +```shell +$ sh bin/controller/fast-try-namesrv-plugin.sh stop +``` + +The `fast-try-namesrv-plugin.sh` script is used for quick deployment with default configurations in the `conf/controller/cluster-3n-namesrv-plugin` directory, and it will start 3 Nameservers and 3 controllers (embedded in Nameserver). + +## Deploying Controller in independent cluster + +The Controller component is deployed in an independent cluster (consisting of 3 nodes) and quickly started.: + +```shell +$ sh bin/controller/fast-try-independent-deployment.sh start +``` + +Alternatively, it can be started separately through a command: + +```shell +$ nohup bin/mqcontroller -c ./conf/controller/cluster-3n-independent/controller-n0.conf & +$ nohup bin/mqcontroller -c ./conf/controller/cluster-3n-independent/controller-n1.conf & +$ nohup bin/mqcontroller -c ./conf/controller/cluster-3n-independent/controller-n2.conf & +``` + +If the previous steps are successful, you can check the status of the Controller cluster using the operational command. + +```shell +$ sh bin/mqadmin getControllerMetaData -a localhost:9878 +``` + +`-a` represents the address of any controller. + +If the controller starts successfully, you will see the following content: + +``` +#ControllerGroup group1 +#ControllerLeaderId n1 +#ControllerLeaderAddress 127.0.0.1:9868 +#Peer: n0:127.0.0.1:9878 +#Peer: n1:127.0.0.1:9868 +#Peer: n2:127.0.0.1:9858 +``` + +After starting successfully, the broker controller mode deployment can use the controller cluster. + +If you need to quickly stop the cluster: + +```shell +$ sh bin/controller/fast-try-independent-deployment.sh stop +``` + +Use the `fast-try-independent-deployment.sh` script to quickly deploy, the default configuration is in `conf/controller/cluster-3n-independent` and it will start 3 controllers (independent deployment) to form a cluster. + + + +## Note + +- If you want to ensure that the Controller has fault tolerance, the Controller deployment requires at least three copies (in accordance with the majority protocol of Raft). +- In the controller deployment configuration file, the IP addresses configured in the `controllerDLegerPeers` parameter should be configured as IPs that can be accessed by other nodes. This is especially important when deploying on multiple machines. The example is for reference only and needs to be modified and adjusted according to the actual situation. diff --git a/docs/en/design.md b/docs/en/design.md new file mode 100644 index 00000000000..c919862b7bc --- /dev/null +++ b/docs/en/design.md @@ -0,0 +1,110 @@ + +## Design +### 1 Message Store + +![](../cn/image/rocketmq_design_1.png) + + +#### 1.1 The Architecture of Message Store + +#### 1.2 PageCache and Memory-Map(Mmap) + +#### 1.3 Message Flush + +![](../cn/image/rocketmq_design_2.png) + + +### 2 Communication Mechanism + +#### 2.1 The class diagram of Remoting module + +![](../cn/image/rocketmq_design_3.png) + +#### 2.2 The design of protocol and encode/decode + +![](../cn/image/rocketmq_design_4.png) + + +#### 2.3 The three ways and process of message communication + +![](../cn/image/rocketmq_design_5.png) + +#### 2.4 The multi-thread design of Reactor + +![](../cn/image/rocketmq_design_6.png) + + +### 3 Message Filter + +![](../cn/image/rocketmq_design_7.png) + +### 4 LoadBalancing + +#### 4.1 The loadBalance of Producer + +#### 4.2 The loadBalance of Consumer + +![](../cn/image/rocketmq_design_8.png) + + +![](../cn/image/rocketmq_design_9.png) + + + +### 5 Transactional Message +Apache RocketMQ supports distributed transactional message from version 4.3.0. RocketMQ implements transactional message by using the protocol of 2PC(two-phase commit), in addition adding a compensation logic to handle timeout-case or failure-case of commit-phase, as shown below. + +![](../cn/image/rocketmq_design_10.png) + +#### 5.1 The Process of RocketMQ Transactional Message +The picture above shows the overall architecture of transactional message, including the sending of message(commit-request phase), the sending of commit/rollback(commit phase) and the compensation process. + +1. The sending of message and Commit/Rollback. + (1) Sending the message(named Half message in RocketMQ) + (2) The server responds the writing result(success or failure) of Half message. + (3) Handle local transaction according to the result(local transaction won't be executed when the result is failure). + (4) Sending Commit/Rollback to broker according to the result of local transaction(Commit will generate message index and make the message visible to consumers). + +2. Compensation process + (1) For a transactional message without a Commit/Rollback (means the message in the pending status), a "back-check" request is initiated from the broker. + (2) The Producer receives the "back-check" request and checks the status of the local transaction corresponding to the "back-check" message. + (3) Redo Commit or Rollback based on local transaction status. +The compensation phase is used to resolve the timeout or failure case of the message Commit or Rollback. + +#### 5.2 The design of RocketMQ Transactional Message +1. Transactional message is invisible to users in first phase(commit-request phase) + + Upon on the main process of transactional message, the message of first phase is invisible to the user. This is also the biggest difference from normal message. So how do we write the message while making it invisible to the user? And below is the solution of RocketMQ: if the message is a Half message, the topic and queueId of the original message will be backed up, and then changes the topic to RMQ_SYS_TRANS_HALF_TOPIC. Since the consumer group does not subscribe to the topic, the consumer cannot consume the Half message. Then RocketMQ starts a timing task, pulls the message for RMQ_SYS_TRANS_HALF_TOPIC, obtains a channel according to producer group and sends a back-check to query local transaction status, and decide whether to submit or roll back the message according to the status. + + In RocketMQ, the storage structure of the message in the broker is as follows. Each message has corresponding index information. The Consumer reads the content of the message through the secondary index of the ConsumeQueue. The flow is as follows: + +![](../cn/image/rocketmq_design_11.png) + + The specific implementation strategy of RocketMQ is: if the transactional message is written, topic and queueId of the message are replaced, and the original topic and queueId are stored in the properties of the message. Because the replace of the topic, the message will not be forwarded to the Consumer Queue of the original topic, and the consumer cannot perceive the existence of the message and will not consume it. In fact, changing the topic is the conventional method of RocketMQ(just recall the implementation mechanism of the delay message). + +2. Commit/Rollback operation and introduction of Op message + + After finishing writing a message that is invisible to the user in the first phase, here comes two cases in the second phase. One is Commit operation, after which the message needs to be visible to the user; the other one is Rollback operation, after which the first phase message(Half message) needs to be revoked. For the case of Rollback, since first-phase message itself is invisible to the user, there is no need to actually revoke the message (in fact, RocketMQ can't actually delete a message because it is a sequential-write file). But still some operation needs to be done to identity the final status of the message, to differ it from pending status message. To do this, the concept of "Op message" is introduced, which means the message has a certain status(Commit or Rollback). If a transactional message does not have a corresponding Op message, the status of the transaction is still undetermined (probably the second-phase failed). By introducing the Op message, the RocketMQ records an Op message for every Half message regardless it is Commit or Rollback. The only difference between Commit and Rollback is that when it comes to Commit, the index of the Half message is created before the Op message is written. + +3. How Op message stored and the correspondence between Op message and Half message + + RocketMQ writes the Op message to a specific system topic(RMQ_SYS_TRANS_OP_HALF_TOPIC) which will be created via the method - TransactionalMessageUtil.buildOpTopic(); this topic is an internal Topic (like the topic of RMQ_SYS_TRANS_HALF_TOPIC) and will not be consumed by the user. The content of the Op message is the physical offset of the corresponding Half message. Through the Op message we can index to the Half message for subsequent check-back operation. + +![](../cn/image/rocketmq_design_12.png) + +4. Index construction of Half messages + + When performing Commit operation of the second phase, the index of the Half message needs to be built. Since the Half message is written to a special topic(RMQ_SYS_TRANS_HALF_TOPIC) in the first phase of 2PC, so it needs to be read out from the special topic when building index, and replace the topic and queueId with the real target topic and queueId, and then write through a normal message that is visible to the user. Therefore, in conclusion, the second phase recovers a complete normal message using the content of the Half message stored in the first phase, and then goes through the message-writing process. + +5. How to handle the message failed in the second phase? + + If commit/rollback phase fails, for example, a network problem causes the Commit to fail when you do Commit. Then certain strategy is required to make sure the message finally commit. RocketMQ uses a compensation mechanism called "back-check". The broker initiates a back-check request for the message in pending status, and sends the request to the corresponding producer side (the same producer group as the producer group who sent the Half message). The producer checks the status of local transaction and redo Commit or Rollback. The broker performs the back-check by comparing the RMQ_SYS_TRANS_HALF_TOPIC messages and the RMQ_SYS_TRANS_OP_HALF_TOPIC messages and advances the checkpoint(recording those transactional messages that the status are certain). + + RocketMQ does not back-check the status of transactional messages endlessly. The default time is 15. If the transaction status is still unknown after 15 times, RocketMQ will roll back the message by default. +### 6 Message Query + +#### 6.1 Query messages by messageId + +#### 6.2 Query messages by message key + +![](../cn/image/rocketmq_design_13.png) diff --git a/docs/en/dledger/deploy_guide.md b/docs/en/dledger/deploy_guide.md new file mode 100644 index 00000000000..06cf333a69a --- /dev/null +++ b/docs/en/dledger/deploy_guide.md @@ -0,0 +1,78 @@ +# Dledger cluster deployment +--- +## preface +This document introduces how to deploy auto failover RocketMQ-on-DLedger Group. + +RocketMQ-on-DLedger Group is a broker group with **same name**, needs at least 3 nodes, elect a Leader by Raft algorithm automatically, the others as Follower, replicating data between Leader and Follower for system high available. +RocketMQ-on-DLedger Group can failover automatically, and maintains consistent. +RocketMQ-on-DLedger Group can scale up horizontal, that is, can deploy any RocketMQ-on-DLedger Groups providing services external. + +## 1. New cluster deployment + +#### 1.1 Write the configuration +each RocketMQ-on-DLedger Group needs at least 3 machines.(assuming 3 in this document) +write 3 configuration files, advising refer to the directory of conf/dledger 's example configuration file. +key configuration items: + +| name | meaning | example | +| --- | --- | --- | +| enableDLegerCommitLog | whether enable DLedger  | true | +| dLegerGroup | DLedger Raft Group's name, advising maintain consistent to brokerName | RaftNode00 | +| dLegerPeers | DLedger Group's nodes port infos, each node's configuration stay consistent in the same group. | n0-127.0.0.1:40911;n1-127.0.0.1:40912;n2-127.0.0.1:40913 | +| dLegerSelfId | node id, must belongs to dLegerPeers; each node is unique in the same group. | n0 | +| sendMessageThreadPoolNums | the count of sending thread, advising set equal to the cpu cores. | 16 | + +the following presents an example configuration conf/dledger/broker-n0.conf. + +``` +brokerClusterName = RaftCluster +brokerName=RaftNode00 +listenPort=30911 +namesrvAddr=127.0.0.1:9876 +storePathRootDir=/tmp/rmqstore/node00 +storePathCommitLog=/tmp/rmqstore/node00/commitlog +enableDLegerCommitLog=true +dLegerGroup=RaftNode00 +dLegerPeers=n0-127.0.0.1:40911;n1-127.0.0.1:40912;n2-127.0.0.1:40913 +## must be unique +dLegerSelfId=n0 +sendMessageThreadPoolNums=16 +``` + +### 1.2 Start Broker + +Startup stays consistent with the old version. + +`nohup sh bin/mqbroker -c conf/dledger/xxx-n0.conf & ` +`nohup sh bin/mqbroker -c conf/dledger/xxx-n1.conf & ` +`nohup sh bin/mqbroker -c conf/dledger/xxx-n2.conf & ` + + +## 2. Upgrade old cluster + +If old cluster deployed in Master mode, then each Master needs to be transformed into a RocketMQ-on-DLedger Group. +If old cluster deployed in Master-Slave mode, then each Master-Slave group needs to be transformed into a RocketMQ-on-DLedger Group. + +### 2.1 Kill old Broker + +execute kill command, or call `bin/mqshutdown broker`. + +### 2.2 Check old Commitlog + +Each node in RocketMQ-on-DLedger group is compatible with old Commitlog, but Raft replicating process works on the adding message only. So, to avoid occurring exceptions, old Commitlog must be consistent. +If old cluster deployed in Master-Slave mode, it maybe inconsistent after shutdown. Advising use md5sum to check at least 2 recently Commitlog file, if occur inconsistent, maintain consistent by copy. + +Although RocketMQ-on-DLedger Group can deployed with 2 nodes, it lacks failover ability(at least 3 nodes can tolerate one node fail). +Make sure that both Master and Slave's Commitlog is consistent, then prepare 3 machines, copy old Commitlog from Master to this 3 machines(BTW, copy the config directory). + +Then, go ahead to set configurations. + +### 2.3 Modify configuration + +Refer to New cluster deployment. + +### 2.4 Restart Broker + +Refer to New cluster deployment. + + diff --git a/docs/en/dledger/quick_start.md b/docs/en/dledger/quick_start.md new file mode 100644 index 00000000000..dfc7894af56 --- /dev/null +++ b/docs/en/dledger/quick_start.md @@ -0,0 +1,68 @@ +# Dledger Quick Deployment +--- +### preface +This document is mainly introduced for how to build and deploy auto failover RocketMQ cluster based on DLedger. + +For detailed new cluster deployment and old cluster upgrade document, please refer to [Deployment Guide](deploy_guide.md). + +### 1. Build from source code +Build phase contains two parts, first, build DLedger, then build RocketMQ. + +#### 1.1 Build DLedger + +```shell +$ git clone https://github.com/openmessaging/dledger.git +$ cd dledger +$ mvn clean install -DskipTests +``` + +#### 1.2 Build RocketMQ + +```shell +$ git clone https://github.com/apache/rocketmq.git +$ cd rocketmq +$ git checkout -b store_with_dledger origin/store_with_dledger +$ mvn -Prelease-all -DskipTests clean install -U +``` + +### 2. Quick Deployment + +after build successful + +```shell +#{rocketmq-version} replace with rocketmq actual version. example: 5.0.0-SNAPSHOT +$ cd distribution/target/rocketmq-{rocketmq-version}/rocketmq-{rocketmq-version} +$ sh bin/dledger/fast-try.sh start +``` + +if the above commands executed successfully, then check cluster status by using mqadmin operation commands. + +```shell +$ sh bin/mqadmin clusterList -n 127.0.0.1:9876 +``` + +If everything goes well, the following content will appear: + +![ClusterList](https://img.alicdn.com/5476e8b07b923/TB11Z.ZyCzqK1RjSZFLXXcn2XXa) + +(BID is 0 indicate Master, the others are Follower) + +After startup successful, producer can produce message, and then test failover scenario. + +Stop cluster fastly, execute the following command: + +```shell +$ sh bin/dledger/fast-try.sh stop +``` + +Quick deployment, default configuration is in directory conf/dledger, default storage path is /tmp/rmqstore. + + +### 3. Failover + +After successful deployment, kill Leader process(as the above example, kill process that binds port 30931), about 10 seconds elapses, use clusterList command check cluster's status, Leader switch to another node. + + + + + diff --git a/docs/en/image/controller/controller_design_1.png b/docs/en/image/controller/controller_design_1.png new file mode 100644 index 00000000000..fea825641f8 Binary files /dev/null and b/docs/en/image/controller/controller_design_1.png differ diff --git a/docs/en/image/controller/controller_design_2.png b/docs/en/image/controller/controller_design_2.png new file mode 100644 index 00000000000..a82339472e9 Binary files /dev/null and b/docs/en/image/controller/controller_design_2.png differ diff --git a/docs/en/image/controller/controller_design_3.png b/docs/en/image/controller/controller_design_3.png new file mode 100644 index 00000000000..8c475bcecf1 Binary files /dev/null and b/docs/en/image/controller/controller_design_3.png differ diff --git a/docs/en/image/controller/controller_design_4.png b/docs/en/image/controller/controller_design_4.png new file mode 100644 index 00000000000..308b936279a Binary files /dev/null and b/docs/en/image/controller/controller_design_4.png differ diff --git a/docs/en/image/controller/controller_design_5.png b/docs/en/image/controller/controller_design_5.png new file mode 100644 index 00000000000..01b33cab28d Binary files /dev/null and b/docs/en/image/controller/controller_design_5.png differ diff --git a/docs/en/image/controller/controller_design_6.png b/docs/en/image/controller/controller_design_6.png new file mode 100644 index 00000000000..a909a70379a Binary files /dev/null and b/docs/en/image/controller/controller_design_6.png differ diff --git a/docs/en/image/controller/persistent_unique_broker_id/fail_apply_broker_id.png b/docs/en/image/controller/persistent_unique_broker_id/fail_apply_broker_id.png new file mode 100644 index 00000000000..0689bd04b31 Binary files /dev/null and b/docs/en/image/controller/persistent_unique_broker_id/fail_apply_broker_id.png differ diff --git a/docs/en/image/controller/persistent_unique_broker_id/fail_create_metadata_file_and_delete_temp.png b/docs/en/image/controller/persistent_unique_broker_id/fail_create_metadata_file_and_delete_temp.png new file mode 100644 index 00000000000..cee8ddfb2a5 Binary files /dev/null and b/docs/en/image/controller/persistent_unique_broker_id/fail_create_metadata_file_and_delete_temp.png differ diff --git a/docs/en/image/controller/persistent_unique_broker_id/fail_create_temp_metadata_file.png b/docs/en/image/controller/persistent_unique_broker_id/fail_create_temp_metadata_file.png new file mode 100644 index 00000000000..32425d23604 Binary files /dev/null and b/docs/en/image/controller/persistent_unique_broker_id/fail_create_temp_metadata_file.png differ diff --git a/docs/en/image/controller/persistent_unique_broker_id/normal_restart.png b/docs/en/image/controller/persistent_unique_broker_id/normal_restart.png new file mode 100644 index 00000000000..a454eada92a Binary files /dev/null and b/docs/en/image/controller/persistent_unique_broker_id/normal_restart.png differ diff --git a/docs/en/image/controller/persistent_unique_broker_id/register_process.png b/docs/en/image/controller/persistent_unique_broker_id/register_process.png new file mode 100644 index 00000000000..200015765d9 Binary files /dev/null and b/docs/en/image/controller/persistent_unique_broker_id/register_process.png differ diff --git a/docs/en/image/controller/persistent_unique_broker_id/register_state_transfer.png b/docs/en/image/controller/persistent_unique_broker_id/register_state_transfer.png new file mode 100644 index 00000000000..d6df0aa5d08 Binary files /dev/null and b/docs/en/image/controller/persistent_unique_broker_id/register_state_transfer.png differ diff --git a/docs/en/image/controller/quick-start/changemaster.png b/docs/en/image/controller/quick-start/changemaster.png new file mode 100644 index 00000000000..6f486597008 Binary files /dev/null and b/docs/en/image/controller/quick-start/changemaster.png differ diff --git a/docs/en/image/controller/quick-start/controller.png b/docs/en/image/controller/quick-start/controller.png new file mode 100644 index 00000000000..d7ffed6b80d Binary files /dev/null and b/docs/en/image/controller/quick-start/controller.png differ diff --git a/docs/en/image/controller/quick-start/epoch.png b/docs/en/image/controller/quick-start/epoch.png new file mode 100644 index 00000000000..67dd768883c Binary files /dev/null and b/docs/en/image/controller/quick-start/epoch.png differ diff --git a/docs/en/image/controller/quick-start/syncstateset.png b/docs/en/image/controller/quick-start/syncstateset.png new file mode 100644 index 00000000000..696a4c30826 Binary files /dev/null and b/docs/en/image/controller/quick-start/syncstateset.png differ diff --git a/docs/en/image/rocketmq_architecture_1.png b/docs/en/image/rocketmq_architecture_1.png new file mode 100644 index 00000000000..addb571a844 Binary files /dev/null and b/docs/en/image/rocketmq_architecture_1.png differ diff --git a/docs/en/image/rocketmq_architecture_2.png b/docs/en/image/rocketmq_architecture_2.png new file mode 100644 index 00000000000..b2ab8d34c6c Binary files /dev/null and b/docs/en/image/rocketmq_architecture_2.png differ diff --git a/docs/en/image/rocketmq_architecture_3.png b/docs/en/image/rocketmq_architecture_3.png new file mode 100644 index 00000000000..b5d755adbdb Binary files /dev/null and b/docs/en/image/rocketmq_architecture_3.png differ diff --git a/docs/en/images/rocketmq_design_7.png b/docs/en/images/rocketmq_design_7.png new file mode 100644 index 00000000000..b0faa86c29c Binary files /dev/null and b/docs/en/images/rocketmq_design_7.png differ diff --git a/docs/en/images/rocketmq_design_message_query.png b/docs/en/images/rocketmq_design_message_query.png new file mode 100644 index 00000000000..f5ca945cafc Binary files /dev/null and b/docs/en/images/rocketmq_design_message_query.png differ diff --git a/docs/en/images/rocketmq_proxy_cluster_mode.png b/docs/en/images/rocketmq_proxy_cluster_mode.png new file mode 100644 index 00000000000..1b4eb5eb31b Binary files /dev/null and b/docs/en/images/rocketmq_proxy_cluster_mode.png differ diff --git a/docs/en/images/rocketmq_proxy_local_mode.png b/docs/en/images/rocketmq_proxy_local_mode.png new file mode 100644 index 00000000000..12e6354a8e3 Binary files /dev/null and b/docs/en/images/rocketmq_proxy_local_mode.png differ diff --git a/docs/en/images/rocketmq_storage_arch.png b/docs/en/images/rocketmq_storage_arch.png new file mode 100644 index 00000000000..8c719e115a1 Binary files /dev/null and b/docs/en/images/rocketmq_storage_arch.png differ diff --git a/docs/en/images/rocketmq_storage_flush.png b/docs/en/images/rocketmq_storage_flush.png new file mode 100644 index 00000000000..1610ae0d934 Binary files /dev/null and b/docs/en/images/rocketmq_storage_flush.png differ diff --git a/docs/en/msg_trace/user_guide.md b/docs/en/msg_trace/user_guide.md new file mode 100644 index 00000000000..64c4b2bb434 --- /dev/null +++ b/docs/en/msg_trace/user_guide.md @@ -0,0 +1,121 @@ +# message trace +---- + +## 1. Message trace data's key properties +| Producer End| Consumer End| Broker End| +| --- | --- | --- | +| produce message | consume message | message's topic | +| send message time | delivery time, delivery rounds  | message store location | +| whether the message was sent successfully | whether message was consumed successfully | message's key | +| send cost-time | consume cost-time | message's tag value | + +## 2. Enable message trace in cluster deployment + +### 2.1 Broker's configuration file +following by Broker's properties file configuration that enable message trace: +``` +brokerClusterName=DefaultCluster +brokerName=broker-a +brokerId=0 +deleteWhen=04 +fileReservedTime=48 +brokerRole=ASYNC_MASTER +flushDiskType=ASYNC_FLUSH +storePathRootDir=/data/rocketmq/rootdir-a-m +storePathCommitLog=/data/rocketmq/commitlog-a-m +autoCreateSubscriptionGroup=true +## if msg tracing is open,the flag will be true +traceTopicEnable=true +listenPort=10911 +brokerIP1=XX.XX.XX.XX1 +namesrvAddr=XX.XX.XX.XX:9876 +``` + +### 2.2 Common mode +Each Broker node in RocketMQ cluster used for storing message trace data that client collected and sent. So, there is no requirements and limitations to the size of Broker node in RocketMQ cluster. + +### 2.3 IO physical isolation mode +For huge amounts of message trace data scenario, we can select any one Broker node in RocketMQ cluster used for storing message trace data special, thus, common message data's IO are isolated from message trace data's IO in physical, not impact each other. In this mode, RocketMQ cluster must have at least two Broker nodes, the one that defined as storing message trace data. + +### 2.4 Start Broker that enable message trace +`nohup sh mqbroker -c ../conf/2m-noslave/broker-a.properties &` + +## 3. Save the definition of topic that with support message trace +RocketMQ's message trace feature supports two types of storage. + +### 3.1 System level TraceTopic +Be default, message trace data is stored in system level TraceTopic(topic name: **RMQ_SYS_TRACE_TOPIC**). That topic will be created at startup of broker(As mentioned above, set **traceTopicEnable** to **true** in Broker's configuration). + +### 3.2 User defined TraceTopic +If user don't want to store message trace data in system level TraceTopic, he can create user defined TraceTopic used for storing message trace data(that is, create common topic for storing message trace data). The following part will introduce how client SDK support user defined TraceTopic. + +## 4. Client SDK demo with message trace feature +For business system adapting to use RocketMQ's message trace feature easily, in design phase, the author add a switch parameter(**enableMsgTrace**) for enable message trace; add a custom parameter(**customizedTraceTopic**) for user defined TraceTopic. + +### 4.1 Enable message trace when sending messages +``` + DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName",true); + producer.setNamesrvAddr("XX.XX.XX.XX1"); + producer.start(); + try { + { + Message msg = new Message("TopicTest", + "TagA", + "OrderID188", + "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); + SendResult sendResult = producer.send(msg); + System.out.printf("%s%n", sendResult); + } + + } catch (Exception e) { + e.printStackTrace(); + } +``` + +### 4.2 Enable message trace when subscribe messages +``` + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_JODIE_1",true); + consumer.subscribe("TopicTest", "*"); + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + consumer.setConsumeTimestamp("20181109221800"); + consumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + consumer.start(); + System.out.printf("Consumer Started.%n"); +``` + +### 4.3 Self-defined topic support message trace +Adjusting instantiation of DefaultMQProducer and DefaultMQPushConsumer as following code to support user defined TraceTopic. +``` + ##Topic_test11111 should be created by user, used for storing message trace data. + DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName",true,"Topic_test11111"); + ...... + + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_JODIE_1",true,"Topic_test11111"); + ...... +``` + + +### 4.4 Send and query message trace by mqadmin command +- send message +```shell +./mqadmin sendMessage -m true --topic some-topic-name -n 127.0.0.1:9876 -p "your message content" +``` +- query trace +```shell +./mqadmin QueryMsgTraceById -n 127.0.0.1:9876 -i "some-message-id" +``` +- query trace result +``` +RocketMQLog:WARN No appenders could be found for logger (io.netty.util.internal.PlatformDependent0). +RocketMQLog:WARN Please initialize the logger system properly. +#Type #ProducerGroup #ClientHost #SendTime #CostTimes #Status +Pub 1623305799667 xxx.xxx.xxx.xxx 2021-06-10 14:16:40 131ms success +``` + + diff --git a/docs/en/operation.md b/docs/en/operation.md new file mode 100644 index 00000000000..a6b707bab13 --- /dev/null +++ b/docs/en/operation.md @@ -0,0 +1,1394 @@ + +# Operation Management +--- + +### 1 Deploy cluster + +#### 1.1 Single Master mode + +This mode is risky, upon broker restart or broken down, the whole service is unavailable. It's not recommended in production environment, it can be used for local test. + +##### 1)Start NameServer + +```bash +### Start Name Server +$ nohup sh mqnamesrv & + +### check whether Name Server is successfully started +$ tail -f ~/logs/rocketmqlogs/namesrv.log +The Name Server boot success... +``` + +##### 2)Start Broker + +```bash +### start Broker +$ nohup sh bin/mqbroker -n localhost:9876 & + +### check whether Broker is successfully started, eg: Broker's IP is 192.168.1.2, Broker's name is broker-a +$ tail -f ~/logs/rocketmqlogs/broker.log +The broker[broker-a, 192.169.1.2:10911] boot success... +``` + +#### 1.2 Multi Master mode + +Cluster contains Master node only, no Slave node, eg: 2 Master nodes, 3 Master nodes, advantages and disadvantages of this mode are shown below: + +- advantages: simple configuration, single Master node broke down or restart do not impact application. Under RAID10 disk config, even if machine broken down and cannot recover, message do not get lost because of RAID10's high reliable(async flush to disk lost little message, sync to disk do not lost message), this mode get highest performance. + +- disadvantages: during the machine's down time, messages have not be consumed on this machine can not be subscribed before recovery. That will impacts message's instantaneity. + +##### 1)Start NameServer + +NameServer should be started before broker. If under production environment, we recommend start 3 NameServer nodes for high available. Startup command is equal, as shown below: + +```bash +### start Name Server +$ nohup sh mqnamesrv & + +### check whether Name Server is successfully started +$ tail -f ~/logs/rocketmqlogs/namesrv.log +The Name Server boot success... +``` + +##### 2)start Broker cluster + +```bash +### start the first Master on machine A, eg:NameServer's IP is 192.168.1.1 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-noslave/broker-a.properties & + +### start the second Master on machine B, eg:NameServer's IP is 192.168.1.1 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-noslave/broker-b.properties & + +... +``` + +The above commands only used for single NameServer. In multi NameServer cluster, multi addresses concat by semicolon followed by -n in broker start command. + +#### 1.3 Multi Master Multi Slave mode - async replication + +Each Master node is equipped with one Slave node, this mode has many Master-Slave group, using async replication for HA, slaver has a lag(ms level) behind master, advantages and disadvantages of this mode are shown below: + +- advantages: message lost a little, even if disk is broken; message instantaneity do not loss; Consumer can still consume from slave when master is down, this process is transparency to user, no human intervention is required; Performance is almost equal to Multi Master mode. + +- disadvantages: message lost a little data, when Master is down and disk broken. + +##### 1)Start NameServer + +```bash +### start Name Server +$ nohup sh mqnamesrv & + +### check whether Name Server is successfully started +$ tail -f ~/logs/rocketmqlogs/namesrv.log +The Name Server boot success... +``` + +##### 2)Start Broker cluster + +```bash +### start first Master on machine A, eg: NameServer's IP is 192.168.1.1 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-async/broker-a.properties & + +### start second Master on machine B, eg: NameServer's IP is 192.168.1.1 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-async/broker-b.properties & + +### start first Slave on machine C, eg: NameServer's IP is 192.168.1.1 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-async/broker-a-s.properties & + +### start second Slave on machine D, eg: NameServer's IP is 192.168.1.1 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-async/broker-b-s.properties & +``` + +#### 1.4 Multi Master Multi Slave mode - synchronous double write + +Each Master node is equipped with one Slave node, this mode has many Master-Slave group, using synchronous double write for HA, application's write operation is successful means both master and slave write successful, advantages and disadvantages of this mode are shown below: + +- advantages:both data and service have no single point failure, message has no latency even if Master is down, service available and data available is very high; + +- disadvantages:this mode's performance is 10% lower than async replication mode, sending latency is a little high, in the current version, it do not have auto Master-Slave switch when Master is down. + +##### 1)Start NameServer + +```bash +### start Name Server +$ nohup sh mqnamesrv & + +### check whether Name Server is successfully started +$ tail -f ~/logs/rocketmqlogs/namesrv.log +The Name Server boot success... +``` + +##### 2)Start Broker cluster + +```bash +### start first Master on machine A, eg:NameServer's IP is 192.168.1.1 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-sync/broker-a.properties & + +### start second Master on machine B, eg:NameServer's IP is 192.168.1.1 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-sync/broker-b.properties & + +### start first Slave on machine C, eg: NameServer's IP is 192.168.1.1 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-sync/broker-a-s.properties & + +### start second Slave on machine D, eg: NameServer's IP is 192.168.1.1 +$ nohup sh mqbroker -n 192.168.1.1:9876 -c $ROCKETMQ_HOME/conf/2m-2s-sync/broker-b-s.properties & +``` + +The above Broker matches Slave by specifying the same BrokerName, Master's BrokerId must be 0, Slave's BrokerId must larger than 0. Besides, a Master can have multi Slaves that each has a distinct BrokerId. $ROCKETMQ_HOME indicates RocketMQ's install directory, user needs to set this environment parameter. + +### 2 mqadmin management tool + +> Attentions: +> +> 1. execute command: `./mqadmin {command} {args}` +> 2. almost all commands need -n indicates NameSerer address, format is ip:port +> 3. almost all commands can get help info by -h +> 4. if command contains both Broker address(-b) and cluster name(-c), it's prior to use broker address. If command do not contains broker address, it will executed on all hosts in this cluster. Support only one broker host. -b format is ip:port, default port is 10911 +> 5. there are many commands under tools, but not all command can be used, only commands that initialized in MQAdminStartup can be used, you can modify this class, add or self-define command. +> 6. because of version update, little command do not update timely, please refer to source code directly when occur error. +#### 2.1 Topic + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    namemeaningcommand itemsexplanation
    updateTopiccreate or update Topic's config-bBroker address, means which Broker that topic is located, only support single Broker, address format is ip:port
    -ccluster name, which cluster that topic belongs to(query cluster info by clusterList)
    -h-print help info
    -nNameServer Service address, format is ip:port
    -passign read write authority to new topic(W=2|R=4|WR=6)
    -rthe count of queue that can be read(default is 8)
    -wthe count of queue that can be wrote(default is 8)
    -ttopic name(can only use characters ^[a-zA-Z0-9_-]+$ )
    deleteTopicdelete Topic-ccluster name, which cluster that topic will be deleted belongs to(query cluster info by clusterList)
    -hprint help info
    -nNameServer Service address, format is ip:port
    -ttopic name(can only use characters ^[a-zA-Z0-9_-]+$ )
    topicListquery Topic list info-hprint help info
    -creturn topic list only if do not contains -c, if contains -c, it will return cluster name, topic name, consumer group name
    -nNameServer Service address, format is ip:port
    topicRoutequery Topic's route info-ttopic name
    -hprint help info
    -nNameServer Service address, format is ip:port
    topicStatusquery Topic's offset-ttopic name
    -hprint help info
    -nNameServer Service address, format is ip:port
    topicClusterListquery cluster list where Topic belongs to-ttopic name
    -hprint help info
    -nNameServer Service address, format is ip:port
    updateTopicPermupdate Topic's produce and consume authority-ttopic name
    -hprint help info
    -nNameServer Service address, format is ip:port
    -bBroker address which topic belongs to, support single broker only, format is ip:port
    -passign read and write authority to the new topic(W=2|R=4|WR=6)
    -ccluster name, which topic belongs to(query cluster info by clusterList), if do not have -b, execute command on all brokers.
    updateOrderConfcreate delete get specified namespace's kv config from NameServer, have not enabled at present-hprint help info
    -nNameServer Service address, format is ip:port
    -ttopic, key
    -vorderConf, value
    -mmethod, including get, put, delete
    allocateMQcalculate consumer list rebalance result by average rebalance algorithm-ttopic name
    -hprint help info
    -nNameServer Service address, format is ip:port
    -iipList, separate by comma, calculate which topic queue that ips will load.
    statsAllprint Topic's subscribe info, TPS, size of message blocked, count of read and write at last 24h, eg.-hprint help info
    -nNameServer Service address, format is ip:port
    -aonly print active topic or not
    -tassign topic
    + + + +#### 2.2 Cluster + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    名称meaningcommand itemsexplanation
    clusterListquery cluster info, including cluster, BrokerName, BrokerId, TPS, eg.-mprint more infos(eg: #InTotalYest, #OutTotalYest, #InTotalToday ,#OutTotalToday)
    -hprint help info
    -nNameServer Service address, format is ip:port
    -iprint interval, unit second
    clusterRTsend message to detect each cluster's Broker RT. Message will be sent to ${BrokerName} Topic.-aamount, count of detection, RT = sum time / + amount
    -ssize of message, unit B
    -cwhich cluster will be detected
    -pwhether print format log, split by |, default is not print
    -hprint help info
    -mwhich machine room it belongs to, just for print
    -isend interval, unit second
    -nNameServer Service address, format is ip:port
    + + +#### 2.3 Broker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    名称meaningcommand itemsexplanation
    updateBrokerConfigupdate Broker's config file, it will modify Broker.conf-bBroker address, format is ip:port
    -ccluster name
    -kkey
    -vvalue
    -hprint help info
    -nNameServer Service address, format is ip:port
    brokerStatusget Broker's statistics info, running status(including whatever you want).-bBroker address, format is ip:port
    -hprint help info
    -nNameServer Service address, format is ip:port
    brokerConsumeStatsBroker's consumer info, including Consume Offset, Broker Offset, Diff, Timestamp that ordered by message Queue-bBroker address, format is ip:port
    -trequest timeout time
    -ldiff threshold, it will print when exceed this threshold.
    -owhether is sequential topic, generally false
    -hprint help info
    -nNameServer Service address, format is ip:port
    getBrokerConfigget Broker's config-bBroker address, format is ip:port
    -nNameServer Service address, format is ip:port
    wipeWritePermrevoke broker's write authority from NameServer.-bBroker address, format is ip:port
    -nNameServer Service address, format is ip:port
    -hprint help info
    cleanExpiredCQclean Broker's expired Consume Queue that maybe generated by decrease queue count.-nNameServer Service address, format is ip:port
    -hprint help info
    -bBroker address, format is ip:port
    -ccluster name
    deleteExpiredCommitLogdelete Broker's expired CommitLog files.-nNameServer Service address, format is ip:port
    -hprint help info
    -bBroker address, format is ip:port
    -ccluster name
    cleanUnusedTopicclean Broker's unused Topic that deleted manually to release memory that Topic's Consume Queue occupied.-nNameServer Service address, format is ip:port
    -hprint help info
    -bBroker address, format is ip:port
    -ccluster name
    sendMsgStatussend message to Broker, return send status and RT-nNameServer Service address, format is ip:port
    -hprint help info
    -bBrokerName, is different from broker address
    -smessage size, unit B
    -csend count
    + + +#### 2.4 Message + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    名称meaningcommand itemsexplanation
    queryMsgByIdquery message by offsetMsgId. If use opensource console, it should use offsetMsgId. Please refer to QueryMsgByIdSubCommand for detail.-imsgId
    -hprint help info
    -nNameServer Service address, format is ip:port
    queryMsgByKeyquery message by Message's Key-kmsgKey
    -ttopic name
    -hprint help info
    -nNameServer Service address, format is ip:port
    queryMsgByOffsetquery message by Offset-bBroker name(it's not broker address, can query Broker name by clusterList).
    -iquery queue id
    -ooffset value
    -ttopic name
    -hprint help info
    -nNameServer Service address, format is ip:port
    queryMsgByUniqueKeyquery by msgId, msgId is different from offsetMsgId, please refer to Frequently asked questions about operations for details. Use -g and -d to let specified consumer return consume result.-hprint help info
    -nNameServer Service address, format is ip:port
    -iunique msg id
    -gconsumerGroup
    -dclientId
    -ttopic name
    checkMsgSendRTdetect RT of sending a message to a topic, similar to clusterRT-hprint help info
    -nNameServer Service address, format is ip:port
    -ttopic name
    -adetection count
    -ssize of the message
    sendMessagesend a message, also can send to a specified Message Queue.-hprint help info
    -nNameServer Service address, format is ip:port
    -ttopic name
    -pbody, message entity
    -kkeys
    -ctags
    -bBrokerName
    -iqueueId
    consumeMessageconsume message. Different consume logic depends on offset, start & end timestamp, message queue, please refer to ConsumeMessageCommand for details.-hprint help info
    -nNameServer Service address, format is ip:port
    -ttopic name
    -bBrokerName
    -ooffset that consumer start consume
    -iqueueId
    -gconsumer group
    -stimestamp at start, refer to -h to get format开
    -dtimestamp at the end
    -csize of message that consumed
    printMsgconsume and print messages from broker, support a time range-hprint help info
    -nNameServer Service address, format is ip:port
    -ttopic name
    -ccharset, eg: UTF-8
    -ssubExpress, filter expression
    -btimestamp at start, refer to -h to get format
    -etimestamp at the end
    -dwhether print message entity or not
    printMsgByQueuesimilar to printMsg, but it need specified Message Queue-hprint help info
    -nNameServer Service address, format is ip:port
    -ttopic name
    -iqueueId
    -aBrokerName
    -ccharset, eg: UTF-8
    -ssubExpress, filter expression
    -btimestamp at start, refer to -h to get format
    -etimestamp at the end
    -pwhether print message or not
    -dwhether print message entity or not
    -fwhether count and print tag or not
    resetOffsetByTimereset offset by timestamp, Broker and consumer will all be reset-hprint help info
    -nNameServer Service address, format is ip:port
    -gconsumer group
    -ttopic name
    -sreset offset corresponding to this timestamp
    -fwhether enforce to reset or not, if set false, only can reset offset, if set true, it omit the relationship between timestamp and consumer offset.
    -cwhether reset c++ sdk's offset or not
    + + +#### 2.5 Consumer, Consumer Group + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    namemeaningcommand itemsexplanation
    consumerProgressquery subscribe status, can get blocking counts of a concrete client ip.-gconsumer group name
    -swhether print client IP or not
    -hprint help info
    -nNameServer Service address, format is ip:port
    consumerStatusquery consumer status, including message blocking, and consumer's jstack result(please refer to ConsumerStatusSubCommand)-hprint help info
    -nNameServer Service address, format is ip:port
    -gconsumer group
    -iclientId
    -swhether execute jstack or not
    updateSubGroupcreate or update subscribe info-nNameServer Service address, format is ip:port
    -hprint help info
    -bBroker address
    -ccluster name
    -gconsumer group name
    -sconsumer group is allowed to consume or not
    -mstart consume from minimal offset or not
    -dbroadcast mode or not
    -qcapacity of retry queue
    -rmax retry count
    -iIt works when slaveReadEnable enabled, and that not consumed from slave. Suggesting that consume from slave node by specify slave id.
    -wIf broker consume from slave, which slave node depends on this config that defined by BrokerId, eg: 1.
    -awhether notify other consumers to rebalance or not when the count of consumer changes
    deleteSubGroupdelete subscribe info from Broker-nNameServer Service address, format is ip:port
    -hprint help info
    -bBroker address
    -ccluster name
    -gconsumer group name
    cloneGroupOffsetuse source group's offset at target group-nNameServer Service address, format is ip:port
    -hprint help info
    -ssource consumer group
    -dtarget consumer group
    -ttopic name
    -onot used at present
    + + + + +#### 2.6 Connection + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    namemeaningcommand itemsexplanation
    consumerConnectionquery Consumer's connection-gconsumer group name
    -nNameServer Service address, format is ip:port
    -hprint help info
    producerConnectionquery Producer's connection-gproducer group name
    -ttopic name
    -nNameServer Service address, format is ip:port
    -hprint help info
    + + + + +#### 2.7 NameServer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    namemeaningcommand itemsexplanation
    updateKvConfigupdate NameServer's kv config, not used at present-snamespace
    -kkey
    -vvalue
    -nNameServer Service address, format is ip:port
    -hprint help info
    deleteKvConfigdelete NameServer's kv config-snamespace
    -kkey
    -nNameServer Service address, format is ip:port
    -hprint help info
    getNamesrvConfigget NameServer's config-nNameServer Service address, format is ip:port
    -hprint help info
    updateNamesrvConfigmodify NameServer's config-nNameServer Service address, format is ip:port
    -hprint help info
    -kkey
    -vvalue
    + + + + +#### 2.8 Other + + + + + + + + + + + + + + + + + + + + + + +
    namemeaningcommand itemsexplanation
    startMonitoringStart the monitoring process, monitor message deletion and the number of retried messages in the queue-nNameServer Service address, format is ip:port
    -hprint help info
    + + +### 3 Frequently asked questions about operations + +#### 3.1 RocketMQ's mqadmin command error + +> question description: execute mqadmin occur below exception after deploy RocketMQ cluster. +> +> ```java +> org.apache.rocketmq.remoting.exception.RemotingConnectException: connect to failed +> ``` + +Solution: execute command `export NAMESRV_ADDR=ip:9876` (ip is NameServer's ip address), then execute mqadmin commands. + +#### 3.2 RocketMQ consumer cannot consume, because of different version of producer and consumer. + +> question description: one producer produce message, consumer A can consume, consume B cannot consume, RocketMQ console print: +> +> ```java +> Not found the consumer group consume stats, because return offset table is empty, maybe the consumer not consume any message. +> ``` + +Solution: make sure that producer and consumer has the same version of rocketmq-client. + +#### 3.3 Consumer cannot consume oldest message, when a new consumer group is added. + +> question description: when a new consumer group start, it consumes from current offset, do not fetch oldest message. + +Solution: rocketmq's default policy is consume from latest, that is skip oldest message. If you want consume oldest message, you need to set `org.apache.rocketmq.client.consumer.DefaultMQPushConsumer#setConsumeFromWhere`. The following is three common configurations: + +- default configuration, a new consumer group consume from latest position at first startup, then consume from last time's offset at next startup, that is skip oldest message; + +```java +consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); +``` + +- a new consumer group consume from oldest position at first startup, then consume from last time's offset at next startup, that is consume the unexpired message; + +```java +consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); +``` + +- a new consumer group consume from specified timestamp at first startup, then consume from last time's offset at next startup, cooperate with consumer.setConsumeTimestamp(), default is half an hour before; + +```java +consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_TIMESTAMP); +``` + +#### 3.4 How to enable consume from Slave + +In some cases, consumer need reset offset to a day or two before, if Master Broker has limited memory, it's CommitLog will have a high IO load, then it will impact other message's read and write that on this broker. When `slaveReadEnable=true` is set, and consumer's offset exceeds `accessMessageInMemoryMaxRatio=40%`, Master Broker will recommend consumer consume from Slave Broker to lower Master Broker IO. + +#### 3.5 Performance tuning + +A spin lock is recommended for asynchronous disk flush, a reentrant lock is recommended for synchronous disk flush, configuration item is `useReentrantLockWhenPutMessage`, default is false; Enable `TransientStorePoolEnable` is recommended when use asynchronous disk flush; Recommend to close `transferMsgByHeap` to improve fetch efficiency; Set a little larger `sendMessageThreadPoolNums`, when use synchronous disk flush. + +#### 3.6 The meaning and difference between msgId and offsetMsgId in RocketMQ + +You will usually see the following log print message after sending message by using RocketMQ sdk. + +```java +SendResult [sendStatus=SEND_OK, msgId=0A42333A0DC818B4AAC246C290FD0000, offsetMsgId=0A42333A00002A9F000000000134F1F5, messageQueue=MessageQueue [topic=topicTest1, BrokerName=mac.local, queueId=3], queueOffset=4] +``` + +- msgId, is generated by producer sdk. In particular, call method `MessageClientIDSetter.createUniqIDBuffer()` to generate unique Id; +- offsetMsgId, offsetMsgId is generated by Broker server(format is "Ip Address + port + CommitLog offset"). offsetMsgId is messageId that is RocketMQ console's input. diff --git a/docs/en/proxy/deploy_guide.md b/docs/en/proxy/deploy_guide.md new file mode 100644 index 00000000000..346964856ca --- /dev/null +++ b/docs/en/proxy/deploy_guide.md @@ -0,0 +1,36 @@ +# RocketMQ Proxy Deployment Guide + +## Overview + +RocketMQ Proxy supports two deployment modes: `Local` and `Cluster`. + +## Configuration + +The configuration file applies to both `Cluster` and `Local` mode, whose default path is +distribution/conf/rmq-proxy.json. + +## `Cluster` Mode + +* Set configuration field `nameSrvAddr`. +* Set configuration field `proxyMode` to `cluster` (case insensitive). + +Run the command below. + +```shell +nohup sh mqproxy & +``` + +The command will only launch the `Proxy` component itself. It assumes that `Namesrv` nodes are already running at the address specified `nameSrvAddr`, and broker nodes, registering themselves with `nameSrvAddr`, are running too. + +## `Local` Mode + +* Set configuration field `nameSrvAddr`. +* Set configuration field `proxyMode` to `local` (case insensitive). + +Run the command below. + +```shell +nohup sh mqproxy & +``` + +The previous command will launch the `Proxy`, with `Broker` in the same process. It assumes `Namesrv` nodes are running at the address specified by `nameSrvAddr`. diff --git a/example/pom.xml b/example/pom.xml index d246de429d4..cb3f7c00826 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -17,9 +17,9 @@ - org.apache.rocketmq rocketmq-all - 4.2.0 + org.apache.rocketmq + 5.2.0 4.0.0 @@ -27,31 +27,54 @@ rocketmq-example rocketmq-example ${project.version} + + ${basedir}/.. + + ${project.groupId} rocketmq-client + + ${project.groupId} + rocketmq-tools + + + ${project.groupId} + rocketmq-remoting + ${project.groupId} rocketmq-srvutil - ch.qos.logback - logback-classic + ${project.groupId} + rocketmq-openmessaging + + + ${project.groupId} + rocketmq-acl org.javassist javassist - io.openmessaging - openmessaging-api + io.jaegertracing + jaeger-core - org.apache.rocketmq - rocketmq-openmessaging - 4.2.0 + io.jaegertracing + jaeger-client + + + io.jaegertracing + jaeger-thrift + + + commons-cli + commons-cli diff --git a/example/src/main/java/org/apache/rocketmq/example/batch/SimpleBatchProducer.java b/example/src/main/java/org/apache/rocketmq/example/batch/SimpleBatchProducer.java index cf566aa15bd..cf82c2a874d 100644 --- a/example/src/main/java/org/apache/rocketmq/example/batch/SimpleBatchProducer.java +++ b/example/src/main/java/org/apache/rocketmq/example/batch/SimpleBatchProducer.java @@ -17,25 +17,34 @@ package org.apache.rocketmq.example.batch; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.common.message.Message; public class SimpleBatchProducer { + public static final String PRODUCER_GROUP = "BatchProducerGroupName"; + public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876"; + public static final String TOPIC = "BatchTest"; + public static final String TAG = "Tag"; + public static void main(String[] args) throws Exception { - DefaultMQProducer producer = new DefaultMQProducer("BatchProducerGroupName"); + DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP); + // Uncomment the following line while debugging, namesrvAddr should be set to your local address +// producer.setNamesrvAddr(DEFAULT_NAMESRVADDR); producer.start(); //If you just send messages of no more than 1MiB at a time, it is easy to use batch //Messages of the same batch should have: same topic, same waitStoreMsgOK and no schedule support - String topic = "BatchTest"; List messages = new ArrayList<>(); - messages.add(new Message(topic, "Tag", "OrderID001", "Hello world 0".getBytes())); - messages.add(new Message(topic, "Tag", "OrderID002", "Hello world 1".getBytes())); - messages.add(new Message(topic, "Tag", "OrderID003", "Hello world 2".getBytes())); + messages.add(new Message(TOPIC, TAG, "OrderID001", "Hello world 0".getBytes(StandardCharsets.UTF_8))); + messages.add(new Message(TOPIC, TAG, "OrderID002", "Hello world 1".getBytes(StandardCharsets.UTF_8))); + messages.add(new Message(TOPIC, TAG, "OrderID003", "Hello world 2".getBytes(StandardCharsets.UTF_8))); - producer.send(messages); + SendResult sendResult = producer.send(messages); + System.out.printf("%s", sendResult); } } diff --git a/example/src/main/java/org/apache/rocketmq/example/batch/SplitBatchProducer.java b/example/src/main/java/org/apache/rocketmq/example/batch/SplitBatchProducer.java index f9495c41714..d33a5a5bbaf 100644 --- a/example/src/main/java/org/apache/rocketmq/example/batch/SplitBatchProducer.java +++ b/example/src/main/java/org/apache/rocketmq/example/batch/SplitBatchProducer.java @@ -17,39 +17,50 @@ package org.apache.rocketmq.example.batch; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.common.message.Message; public class SplitBatchProducer { + public static final String PRODUCER_GROUP = "BatchProducerGroupName"; + public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876"; + + public static final int MESSAGE_COUNT = 100 * 1000; + public static final String TOPIC = "BatchTest"; + public static final String TAG = "Tag"; + public static void main(String[] args) throws Exception { - DefaultMQProducer producer = new DefaultMQProducer("BatchProducerGroupName"); + DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP); + // Uncomment the following line while debugging, namesrvAddr should be set to your local address +// producer.setNamesrvAddr(DEFAULT_NAMESRVADDR); producer.start(); //large batch - String topic = "BatchTest"; - List messages = new ArrayList<>(100 * 1000); - for (int i = 0; i < 100 * 1000; i++) { - messages.add(new Message(topic, "Tag", "OrderID" + i, ("Hello world " + i).getBytes())); + List messages = new ArrayList<>(MESSAGE_COUNT); + for (int i = 0; i < MESSAGE_COUNT; i++) { + messages.add(new Message(TOPIC, TAG, "OrderID" + i, ("Hello world " + i).getBytes(StandardCharsets.UTF_8))); } //split the large batch into small ones: ListSplitter splitter = new ListSplitter(messages); while (splitter.hasNext()) { List listItem = splitter.next(); - producer.send(listItem); + SendResult sendResult = producer.send(listItem); + System.out.printf("%s", sendResult); } } } class ListSplitter implements Iterator> { - private int sizeLimit = 1000 * 1000; + private static final int SIZE_LIMIT = 1000 * 1000; private final List messages; private int currIndex; @@ -73,8 +84,9 @@ public List next() { for (Map.Entry entry : properties.entrySet()) { tmpSize += entry.getKey().length() + entry.getValue().length(); } - tmpSize = tmpSize + 20; //for log overhead - if (tmpSize > sizeLimit) { + //for log overhead + tmpSize = tmpSize + 20; + if (tmpSize > SIZE_LIMIT) { //it is unexpected that single message exceeds the sizeLimit //here just let it go, otherwise it will block the splitting process if (nextIndex - currIndex == 0) { @@ -83,7 +95,7 @@ public List next() { } break; } - if (tmpSize + totalSize > sizeLimit) { + if (tmpSize + totalSize > SIZE_LIMIT) { break; } else { totalSize += tmpSize; diff --git a/example/src/main/java/org/apache/rocketmq/example/benchmark/AclClient.java b/example/src/main/java/org/apache/rocketmq/example/benchmark/AclClient.java new file mode 100644 index 00000000000..b3d6fb47e9b --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/benchmark/AclClient.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.example.benchmark; + +import org.apache.rocketmq.acl.common.AclClientRPCHook; +import org.apache.rocketmq.acl.common.SessionCredentials; +import org.apache.rocketmq.remoting.RPCHook; + +public class AclClient { + + public static final String ACL_ACCESS_KEY = "rocketmq2"; + + public static final String ACL_SECRET_KEY = "12345678"; + + public static RPCHook getAclRPCHook() { + return getAclRPCHook(ACL_ACCESS_KEY, ACL_SECRET_KEY); + } + + public static RPCHook getAclRPCHook(String ak, String sk) { + return new AclClientRPCHook(new SessionCredentials(ak, sk)); + } +} diff --git a/example/src/main/java/org/apache/rocketmq/example/benchmark/BatchProducer.java b/example/src/main/java/org/apache/rocketmq/example/benchmark/BatchProducer.java new file mode 100644 index 00000000000..c4a6162a5f5 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/benchmark/BatchProducer.java @@ -0,0 +1,452 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.example.benchmark; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.LongAdder; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.compression.CompressionType; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.SerializeType; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.srvutil.ServerUtil; + +public class BatchProducer { + + private static byte[] msgBody; + + public static void main(String[] args) throws MQClientException { + System.setProperty(RemotingCommand.SERIALIZE_TYPE_PROPERTY, SerializeType.ROCKETMQ.name()); + + Options options = ServerUtil.buildCommandlineOptions(new Options()); + CommandLine commandLine = ServerUtil.parseCmdLine("benchmarkBatchProducer", args, buildCommandlineOptions(options), new DefaultParser()); + if (null == commandLine) { + System.exit(-1); + } + + final String namesrv = getOptionValue(commandLine, 'n', "127.0.0.1:9876"); + final String topic = getOptionValue(commandLine, 't', "BenchmarkTest"); + final int threadCount = getOptionValue(commandLine, 'w', 64); + final int messageSize = getOptionValue(commandLine, 's', 128); + final int batchSize = getOptionValue(commandLine, 'b', 16); + final boolean keyEnable = getOptionValue(commandLine, 'k', false); + final int propertySize = getOptionValue(commandLine, 'p', 0); + final int tagCount = getOptionValue(commandLine, 'l', 0); + final boolean msgTraceEnable = getOptionValue(commandLine, 'm', false); + final boolean aclEnable = getOptionValue(commandLine, 'a', false); + final boolean enableCompress = commandLine.hasOption('c') && Boolean.parseBoolean(commandLine.getOptionValue('c')); + final int reportInterval = commandLine.hasOption("ri") ? Integer.parseInt(commandLine.getOptionValue("ri")) : 10000; + + System.out.printf("topic: %s, threadCount: %d, messageSize: %d, batchSize: %d, keyEnable: %s, propertySize: %d, tagCount: %d, traceEnable: %s, " + + "aclEnable: %s%n compressEnable: %s, reportInterval: %d%n", + topic, threadCount, messageSize, batchSize, keyEnable, propertySize, tagCount, msgTraceEnable, aclEnable, enableCompress, reportInterval); + + StringBuilder sb = new StringBuilder(messageSize); + for (int i = 0; i < messageSize; i++) { + sb.append(RandomStringUtils.randomAlphanumeric(1)); + } + msgBody = sb.toString().getBytes(StandardCharsets.UTF_8); + + final StatsBenchmarkBatchProducer statsBenchmark = new StatsBenchmarkBatchProducer(reportInterval); + statsBenchmark.start(); + + RPCHook rpcHook = null; + if (aclEnable) { + String ak = commandLine.hasOption("ak") ? String.valueOf(commandLine.getOptionValue("ak")) : AclClient.ACL_ACCESS_KEY; + String sk = commandLine.hasOption("sk") ? String.valueOf(commandLine.getOptionValue("sk")) : AclClient.ACL_SECRET_KEY; + rpcHook = AclClient.getAclRPCHook(ak, sk); + } + + final DefaultMQProducer producer = initInstance(namesrv, msgTraceEnable, rpcHook); + + if (enableCompress) { + String compressType = commandLine.hasOption("ct") ? commandLine.getOptionValue("ct").trim() : "ZLIB"; + int compressLevel = commandLine.hasOption("cl") ? Integer.parseInt(commandLine.getOptionValue("cl")) : 5; + int compressOverHowMuch = commandLine.hasOption("ch") ? Integer.parseInt(commandLine.getOptionValue("ch")) : 4096; + producer.getDefaultMQProducerImpl().setCompressType(CompressionType.of(compressType)); + producer.getDefaultMQProducerImpl().setCompressLevel(compressLevel); + producer.setCompressMsgBodyOverHowmuch(compressOverHowMuch); + System.out.printf("compressType: %s compressLevel: %s%n", compressType, compressLevel); + } else { + producer.setCompressMsgBodyOverHowmuch(Integer.MAX_VALUE); + } + + producer.start(); + + final Logger logger = LoggerFactory.getLogger(BatchProducer.class); + final ExecutorService sendThreadPool = Executors.newFixedThreadPool(threadCount); + for (int i = 0; i < threadCount; i++) { + sendThreadPool.execute(new Runnable() { + @Override + public void run() { + while (true) { + List msgs = buildBathMessage(batchSize, topic); + + if (CollectionUtils.isEmpty(msgs)) { + return; + } + + try { + long beginTimestamp = System.currentTimeMillis(); + long sendSucCount = statsBenchmark.getSendMessageSuccessCount().longValue(); + + setKeys(keyEnable, msgs, String.valueOf(beginTimestamp / 1000)); + setTags(tagCount, msgs, sendSucCount); + setProperties(propertySize, msgs); + SendResult sendResult = producer.send(msgs); + if (sendResult.getSendStatus() == SendStatus.SEND_OK) { + statsBenchmark.getSendRequestSuccessCount().increment(); + statsBenchmark.getSendMessageSuccessCount().add(msgs.size()); + } else { + statsBenchmark.getSendRequestFailedCount().increment(); + statsBenchmark.getSendMessageFailedCount().add(msgs.size()); + } + long currentRT = System.currentTimeMillis() - beginTimestamp; + statsBenchmark.getSendMessageSuccessTimeTotal().add(currentRT); + long prevMaxRT = statsBenchmark.getSendMessageMaxRT().longValue(); + while (currentRT > prevMaxRT) { + boolean updated = statsBenchmark.getSendMessageMaxRT().compareAndSet(prevMaxRT, currentRT); + if (updated) { + break; + } + + prevMaxRT = statsBenchmark.getSendMessageMaxRT().get(); + } + } catch (RemotingException e) { + statsBenchmark.getSendRequestFailedCount().increment(); + statsBenchmark.getSendMessageFailedCount().add(msgs.size()); + logger.error("[BENCHMARK_PRODUCER] Send Exception", e); + + try { + Thread.sleep(3000); + } catch (InterruptedException ignored) { + } + } catch (InterruptedException e) { + statsBenchmark.getSendRequestFailedCount().increment(); + statsBenchmark.getSendMessageFailedCount().add(msgs.size()); + try { + Thread.sleep(3000); + } catch (InterruptedException e1) { + } + statsBenchmark.getSendRequestFailedCount().increment(); + statsBenchmark.getSendMessageFailedCount().add(msgs.size()); + logger.error("[BENCHMARK_PRODUCER] Send Exception", e); + } catch (MQClientException e) { + statsBenchmark.getSendRequestFailedCount().increment(); + statsBenchmark.getSendMessageFailedCount().add(msgs.size()); + logger.error("[BENCHMARK_PRODUCER] Send Exception", e); + } catch (MQBrokerException e) { + statsBenchmark.getSendRequestFailedCount().increment(); + statsBenchmark.getSendMessageFailedCount().add(msgs.size()); + logger.error("[BENCHMARK_PRODUCER] Send Exception", e); + try { + Thread.sleep(3000); + } catch (InterruptedException ignored) { + } + } + } + } + }); + } + } + + public static Options buildCommandlineOptions(final Options options) { + Option opt = new Option("w", "threadCount", true, "Thread count, Default: 64"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("s", "messageSize", true, "Message Size, Default: 128"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("b", "batchSize", true, "Batch Size, Default: 16"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("k", "keyEnable", true, "Message Key Enable, Default: false"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("t", "topic", true, "Topic name, Default: BenchmarkTest"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("l", "tagCount", true, "Tag count, Default: 0"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("m", "msgTraceEnable", true, "Message Trace Enable, Default: false"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("a", "aclEnable", true, "Acl Enable, Default: false"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("ak", "accessKey", true, "Acl Access Key, Default: rocketmq2"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("sk", "secretKey", true, "Acl Secret Key, Default: 123456789"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("p", "propertySize", true, "Property Size, Default: 0"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("n", "namesrv", true, "name server, Default: 127.0.0.1:9876"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "compressEnable", true, "Enable compress msg over 4K, Default: false"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("ct", "compressType", true, "Message compressed type, Default: ZLIB"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("cl", "compressLevel", true, "Message compressed level, Default: 5"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("ch", "compressOverHowMuch", true, "Compress message when body over how much(unit Byte), Default: 4096"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("ri", "reportInterval", true, "The number of ms between reports, Default: 10000"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + private static String getOptionValue(CommandLine commandLine, char key, String defaultValue) { + if (commandLine.hasOption(key)) { + return commandLine.getOptionValue(key).trim(); + } + return defaultValue; + } + + private static int getOptionValue(CommandLine commandLine, char key, int defaultValue) { + if (commandLine.hasOption(key)) { + return Integer.parseInt(commandLine.getOptionValue(key).trim()); + } + return defaultValue; + } + + private static boolean getOptionValue(CommandLine commandLine, char key, boolean defaultValue) { + if (commandLine.hasOption(key)) { + return Boolean.parseBoolean(commandLine.getOptionValue(key).trim()); + } + return defaultValue; + } + + private static List buildBathMessage(final int batchSize, final String topic) { + List batchMessage = new ArrayList<>(batchSize); + for (int i = 0; i < batchSize; i++) { + Message msg = new Message(topic, msgBody); + batchMessage.add(msg); + } + return batchMessage; + } + + private static void setKeys(boolean keyEnable, List msgs, String keys) { + if (!keyEnable) { + return; + } + + for (Message msg : msgs) { + msg.setKeys(keys); + } + } + + private static void setTags(int tagCount, List msgs, long startTagId) { + if (tagCount <= 0) { + return; + } + + long tagId = startTagId % tagCount; + for (Message msg : msgs) { + msg.setTags(String.format("tag%d", tagId++)); + } + } + + private static void setProperties(int propertySize, List msgs) { + if (propertySize <= 0) { + return; + } + + for (Message msg : msgs) { + if (msg.getProperties() != null) { + msg.getProperties().clear(); + } + + int startValue = (new Random(System.currentTimeMillis())).nextInt(100); + int size = 0; + for (int i = 0; ; i++) { + String prop1 = "prop" + i, prop1V = "hello" + startValue; + msg.putUserProperty(prop1, prop1V); + size += prop1.length() + prop1V.length(); + if (size > propertySize) { + break; + } + startValue++; + } + } + } + + private static DefaultMQProducer initInstance(String namesrv, boolean traceEnable, RPCHook rpcHook) { + final DefaultMQProducer producer = new DefaultMQProducer("benchmark_batch_producer", rpcHook, traceEnable, null); + producer.setInstanceName(Long.toString(System.currentTimeMillis())); + + producer.setNamesrvAddr(namesrv); + return producer; + } +} + +class StatsBenchmarkBatchProducer { + + private final LongAdder sendRequestSuccessCount = new LongAdder(); + + private final LongAdder sendRequestFailedCount = new LongAdder(); + + private final LongAdder sendMessageSuccessTimeTotal = new LongAdder(); + + private final AtomicLong sendMessageMaxRT = new AtomicLong(0L); + + private final LongAdder sendMessageSuccessCount = new LongAdder(); + + private final LongAdder sendMessageFailedCount = new LongAdder(); + + private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl( + "BenchmarkTimerThread", Boolean.TRUE)); + + private final LinkedList snapshotList = new LinkedList<>(); + + private final int reportInterval; + + public StatsBenchmarkBatchProducer(int reportInterval) { + this.reportInterval = reportInterval; + } + + public Long[] createSnapshot() { + Long[] snap = new Long[] { + System.currentTimeMillis(), + this.sendRequestSuccessCount.longValue(), + this.sendRequestFailedCount.longValue(), + this.sendMessageSuccessCount.longValue(), + this.sendMessageFailedCount.longValue(), + this.sendMessageSuccessTimeTotal.longValue(), + }; + + return snap; + } + + public LongAdder getSendRequestSuccessCount() { + return sendRequestSuccessCount; + } + + public LongAdder getSendRequestFailedCount() { + return sendRequestFailedCount; + } + + public LongAdder getSendMessageSuccessTimeTotal() { + return sendMessageSuccessTimeTotal; + } + + public AtomicLong getSendMessageMaxRT() { + return sendMessageMaxRT; + } + + public LongAdder getSendMessageSuccessCount() { + return sendMessageSuccessCount; + } + + public LongAdder getSendMessageFailedCount() { + return sendMessageFailedCount; + } + + public void start() { + + executorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + snapshotList.addLast(createSnapshot()); + if (snapshotList.size() > 10) { + snapshotList.removeFirst(); + } + } + }, 1000, 1000, TimeUnit.MILLISECONDS); + + executorService.scheduleAtFixedRate(new Runnable() { + private void printStats() { + if (snapshotList.size() >= 10) { + Long[] begin = snapshotList.getFirst(); + Long[] end = snapshotList.getLast(); + + final long sendTps = (long) (((end[1] - begin[1]) / (double) (end[0] - begin[0])) * 1000L); + final long sendMps = (long) (((end[3] - begin[3]) / (double) (end[0] - begin[0])) * 1000L); + final double averageRT = (end[5] - begin[5]) / (double) (end[1] - begin[1]); + final double averageMsgRT = (end[5] - begin[5]) / (double) (end[3] - begin[3]); + + System.out.printf("Current Time: %s | Send TPS: %d | Send MPS: %d | Max RT(ms): %d | Average RT(ms): %7.3f | Average Message RT(ms): %7.3f | Send Failed: %d | Send Message Failed: %d%n", + UtilAll.timeMillisToHumanString2(System.currentTimeMillis()), sendTps, sendMps, getSendMessageMaxRT().longValue(), averageRT, averageMsgRT, end[2], end[4]); + } + } + + @Override + public void run() { + try { + this.printStats(); + } catch (Exception e) { + e.printStackTrace(); + } + } + }, reportInterval, reportInterval, TimeUnit.MILLISECONDS); + } + + public void shutdown() { + executorService.shutdown(); + } +} \ No newline at end of file diff --git a/example/src/main/java/org/apache/rocketmq/example/benchmark/Consumer.java b/example/src/main/java/org/apache/rocketmq/example/benchmark/Consumer.java index d431d3ecc6a..57270fcd006 100644 --- a/example/src/main/java/org/apache/rocketmq/example/benchmark/Consumer.java +++ b/example/src/main/java/org/apache/rocketmq/example/benchmark/Consumer.java @@ -20,52 +20,72 @@ import java.io.IOException; import java.util.LinkedList; import java.util.List; -import java.util.Timer; import java.util.TimerTask; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.LongAdder; import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; -import org.apache.commons.cli.PosixParser; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.MessageSelector; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.consumer.rebalance.AllocateMessageQueueAveragely; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.UtilAll; import org.apache.rocketmq.common.filter.ExpressionType; import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.SerializeType; import org.apache.rocketmq.srvutil.ServerUtil; public class Consumer { public static void main(String[] args) throws MQClientException, IOException { + System.setProperty(RemotingCommand.SERIALIZE_TYPE_PROPERTY, SerializeType.ROCKETMQ.name()); Options options = ServerUtil.buildCommandlineOptions(new Options()); - CommandLine commandLine = ServerUtil.parseCmdLine("benchmarkConsumer", args, buildCommandlineOptions(options), new PosixParser()); + CommandLine commandLine = ServerUtil.parseCmdLine("benchmarkConsumer", args, buildCommandlineOptions(options), new DefaultParser()); if (null == commandLine) { System.exit(-1); } final String topic = commandLine.hasOption('t') ? commandLine.getOptionValue('t').trim() : "BenchmarkTest"; + final int threadCount = commandLine.hasOption('w') ? Integer.parseInt(commandLine.getOptionValue('w')) : 20; final String groupPrefix = commandLine.hasOption('g') ? commandLine.getOptionValue('g').trim() : "benchmark_consumer"; - final String isPrefixEnable = commandLine.hasOption('p') ? commandLine.getOptionValue('p').trim() : "true"; + final String isSuffixEnable = commandLine.hasOption('p') ? commandLine.getOptionValue('p').trim() : "false"; final String filterType = commandLine.hasOption('f') ? commandLine.getOptionValue('f').trim() : null; final String expression = commandLine.hasOption('e') ? commandLine.getOptionValue('e').trim() : null; + final double failRate = commandLine.hasOption('r') ? Double.parseDouble(commandLine.getOptionValue('r').trim()) : 0.0; + final boolean msgTraceEnable = commandLine.hasOption('m') && Boolean.parseBoolean(commandLine.getOptionValue('m')); + final boolean aclEnable = commandLine.hasOption('a') && Boolean.parseBoolean(commandLine.getOptionValue('a')); + final boolean clientRebalanceEnable = commandLine.hasOption('c') ? Boolean.parseBoolean(commandLine.getOptionValue('c')) : true; + final int reportInterval = commandLine.hasOption("ri") ? Integer.parseInt(commandLine.getOptionValue("ri")) : 10000; + String group = groupPrefix; - if (Boolean.parseBoolean(isPrefixEnable)) { - group = groupPrefix + "_" + Long.toString(System.currentTimeMillis() % 100); + if (Boolean.parseBoolean(isSuffixEnable)) { + group = groupPrefix + "_" + (System.currentTimeMillis() % 100); } - System.out.printf("topic: %s, group: %s, prefix: %s, filterType: %s, expression: %s%n", topic, group, isPrefixEnable, filterType, expression); + System.out.printf("topic: %s, threadCount %d, group: %s, suffix: %s, filterType: %s, expression: %s, msgTraceEnable: %s, aclEnable: %s, reportInterval: %d%n", + topic, threadCount, group, isSuffixEnable, filterType, expression, msgTraceEnable, aclEnable, reportInterval); final StatsBenchmarkConsumer statsBenchmarkConsumer = new StatsBenchmarkConsumer(); - final Timer timer = new Timer("BenchmarkTimerThread", true); + ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1, + new BasicThreadFactory.Builder().namingPattern("BenchmarkTimerThread-%d").daemon(true).build()); - final LinkedList snapshotList = new LinkedList(); + final LinkedList snapshotList = new LinkedList<>(); - timer.scheduleAtFixedRate(new TimerTask() { + executorService.scheduleAtFixedRate(new TimerTask() { @Override public void run() { snapshotList.addLast(statsBenchmarkConsumer.createSnapshot()); @@ -73,9 +93,9 @@ public void run() { snapshotList.removeFirst(); } } - }, 1000, 1000); + }, 1000, 1000, TimeUnit.MILLISECONDS); - timer.scheduleAtFixedRate(new TimerTask() { + executorService.scheduleAtFixedRate(new TimerTask() { private void printStats() { if (snapshotList.size() >= 10) { Long[] begin = snapshotList.getFirst(); @@ -85,10 +105,15 @@ private void printStats() { (long) (((end[1] - begin[1]) / (double) (end[0] - begin[0])) * 1000L); final double averageB2CRT = (end[2] - begin[2]) / (double) (end[1] - begin[1]); final double averageS2CRT = (end[3] - begin[3]) / (double) (end[1] - begin[1]); + final long failCount = end[4] - begin[4]; + final long b2cMax = statsBenchmarkConsumer.getBorn2ConsumerMaxRT().get(); + final long s2cMax = statsBenchmarkConsumer.getStore2ConsumerMaxRT().get(); + + statsBenchmarkConsumer.getBorn2ConsumerMaxRT().set(0); + statsBenchmarkConsumer.getStore2ConsumerMaxRT().set(0); - System.out.printf("Consume TPS: %d Average(B2C) RT: %7.3f Average(S2C) RT: %7.3f MAX(B2C) RT: %d MAX(S2C) RT: %d%n", - consumeTps, averageB2CRT, averageS2CRT, end[4], end[5] - ); + System.out.printf("Current Time: %s | Consume TPS: %d | AVG(B2C) RT(ms): %7.3f | AVG(S2C) RT(ms): %7.3f | MAX(B2C) RT(ms): %d | MAX(S2C) RT(ms): %d | Consume Fail: %d%n", + UtilAll.timeMillisToHumanString2(System.currentTimeMillis()), consumeTps, averageB2CRT, averageS2CRT, b2cMax, s2cMax, failCount); } } @@ -100,10 +125,23 @@ public void run() { e.printStackTrace(); } } - }, 10000, 10000); + }, reportInterval, reportInterval, TimeUnit.MILLISECONDS); - DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(group); + RPCHook rpcHook = null; + if (aclEnable) { + String ak = commandLine.hasOption("ak") ? String.valueOf(commandLine.getOptionValue("ak")) : AclClient.ACL_ACCESS_KEY; + String sk = commandLine.hasOption("sk") ? String.valueOf(commandLine.getOptionValue("sk")) : AclClient.ACL_SECRET_KEY; + rpcHook = AclClient.getAclRPCHook(ak, sk); + } + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(group, rpcHook, new AllocateMessageQueueAveragely(), msgTraceEnable, null); + if (commandLine.hasOption('n')) { + String ns = commandLine.getOptionValue('n'); + consumer.setNamesrvAddr(ns); + } + consumer.setConsumeThreadMin(threadCount); + consumer.setConsumeThreadMax(threadCount); consumer.setInstanceName(Long.toString(System.currentTimeMillis())); + consumer.setClientRebalance(clientRebalanceEnable); if (filterType == null || expression == null) { consumer.subscribe(topic, "*"); @@ -128,19 +166,24 @@ public ConsumeConcurrentlyStatus consumeMessage(List msgs, MessageExt msg = msgs.get(0); long now = System.currentTimeMillis(); - statsBenchmarkConsumer.getReceiveMessageTotalCount().incrementAndGet(); + statsBenchmarkConsumer.getReceiveMessageTotalCount().increment(); long born2ConsumerRT = now - msg.getBornTimestamp(); - statsBenchmarkConsumer.getBorn2ConsumerTotalRT().addAndGet(born2ConsumerRT); + statsBenchmarkConsumer.getBorn2ConsumerTotalRT().add(born2ConsumerRT); long store2ConsumerRT = now - msg.getStoreTimestamp(); - statsBenchmarkConsumer.getStore2ConsumerTotalRT().addAndGet(store2ConsumerRT); + statsBenchmarkConsumer.getStore2ConsumerTotalRT().add(store2ConsumerRT); compareAndSetMax(statsBenchmarkConsumer.getBorn2ConsumerMaxRT(), born2ConsumerRT); compareAndSetMax(statsBenchmarkConsumer.getStore2ConsumerMaxRT(), store2ConsumerRT); - return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + if (ThreadLocalRandom.current().nextDouble() < failRate) { + statsBenchmarkConsumer.getFailCount().increment(); + return ConsumeConcurrentlyStatus.RECONSUME_LATER; + } else { + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } } }); @@ -154,11 +197,14 @@ public static Options buildCommandlineOptions(final Options options) { opt.setRequired(false); options.addOption(opt); - opt = new Option("g", "group", true, "Consumer group name, Default: benchmark_consumer"); + opt = new Option("w", "threadCount", true, "Thread count, Default: 20"); opt.setRequired(false); options.addOption(opt); - opt = new Option("p", "group prefix enable", true, "Consumer group name, Default: false"); + opt = new Option("g", "group", true, "Consumer group name, Default: benchmark_consumer"); + opt.setRequired(false); + options.addOption(opt); + opt = new Option("p", "group prefix enable", true, "Is group prefix enable, Default: false"); opt.setRequired(false); options.addOption(opt); @@ -170,6 +216,30 @@ public static Options buildCommandlineOptions(final Options options) { opt.setRequired(false); options.addOption(opt); + opt = new Option("r", "fail rate", true, "consumer fail rate, default 0"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("m", "msgTraceEnable", true, "Message Trace Enable, Default: false"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("a", "aclEnable", true, "Acl Enable, Default: false"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("ak", "accessKey", true, "Acl access key, Default: 12345678"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("sk", "secretKey", true, "Acl secret key, Default: rocketmq2"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("ri", "reportInterval", true, "The number of ms between reports, Default: 10000"); + opt.setRequired(false); + options.addOption(opt); + return options; } @@ -186,38 +256,39 @@ public static void compareAndSetMax(final AtomicLong target, final long value) { } class StatsBenchmarkConsumer { - private final AtomicLong receiveMessageTotalCount = new AtomicLong(0L); + private final LongAdder receiveMessageTotalCount = new LongAdder(); - private final AtomicLong born2ConsumerTotalRT = new AtomicLong(0L); + private final LongAdder born2ConsumerTotalRT = new LongAdder(); - private final AtomicLong store2ConsumerTotalRT = new AtomicLong(0L); + private final LongAdder store2ConsumerTotalRT = new LongAdder(); private final AtomicLong born2ConsumerMaxRT = new AtomicLong(0L); private final AtomicLong store2ConsumerMaxRT = new AtomicLong(0L); + private final LongAdder failCount = new LongAdder(); + public Long[] createSnapshot() { Long[] snap = new Long[] { System.currentTimeMillis(), - this.receiveMessageTotalCount.get(), - this.born2ConsumerTotalRT.get(), - this.store2ConsumerTotalRT.get(), - this.born2ConsumerMaxRT.get(), - this.store2ConsumerMaxRT.get(), + this.receiveMessageTotalCount.longValue(), + this.born2ConsumerTotalRT.longValue(), + this.store2ConsumerTotalRT.longValue(), + this.failCount.longValue() }; return snap; } - public AtomicLong getReceiveMessageTotalCount() { + public LongAdder getReceiveMessageTotalCount() { return receiveMessageTotalCount; } - public AtomicLong getBorn2ConsumerTotalRT() { + public LongAdder getBorn2ConsumerTotalRT() { return born2ConsumerTotalRT; } - public AtomicLong getStore2ConsumerTotalRT() { + public LongAdder getStore2ConsumerTotalRT() { return store2ConsumerTotalRT; } @@ -228,4 +299,8 @@ public AtomicLong getBorn2ConsumerMaxRT() { public AtomicLong getStore2ConsumerMaxRT() { return store2ConsumerMaxRT; } + + public LongAdder getFailCount() { + return failCount; + } } diff --git a/example/src/main/java/org/apache/rocketmq/example/benchmark/Producer.java b/example/src/main/java/org/apache/rocketmq/example/benchmark/Producer.java index 9bd52662830..480d16b7581 100644 --- a/example/src/main/java/org/apache/rocketmq/example/benchmark/Producer.java +++ b/example/src/main/java/org/apache/rocketmq/example/benchmark/Producer.java @@ -16,57 +16,106 @@ */ package org.apache.rocketmq.example.benchmark; -import java.io.UnsupportedEncodingException; -import java.util.LinkedList; -import java.util.Random; -import java.util.Timer; -import java.util.TimerTask; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicLong; - +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.atomic.LongAdder; import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; -import org.apache.commons.cli.PosixParser; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; -import org.apache.rocketmq.client.log.ClientLogger; import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendCallback; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.compression.CompressionType; import org.apache.rocketmq.common.message.Message; -import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.RPCHook; import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.SerializeType; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.srvutil.ServerUtil; -import org.slf4j.Logger; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.Random; +import java.util.TimerTask; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicLong; public class Producer { - public static void main(String[] args) throws MQClientException, UnsupportedEncodingException { + + private static final Logger log = LoggerFactory.getLogger(Producer.class); + + private static byte[] msgBody; + private static final int MAX_LENGTH_ASYNC_QUEUE = 10000; + private static final int SLEEP_FOR_A_WHILE = 100; + + public static void main(String[] args) throws MQClientException { + System.setProperty(RemotingCommand.SERIALIZE_TYPE_PROPERTY, SerializeType.ROCKETMQ.name()); Options options = ServerUtil.buildCommandlineOptions(new Options()); - CommandLine commandLine = ServerUtil.parseCmdLine("benchmarkProducer", args, buildCommandlineOptions(options), new PosixParser()); + CommandLine commandLine = ServerUtil.parseCmdLine("benchmarkProducer", args, buildCommandlineOptions(options), new DefaultParser()); if (null == commandLine) { System.exit(-1); } final String topic = commandLine.hasOption('t') ? commandLine.getOptionValue('t').trim() : "BenchmarkTest"; - final int threadCount = commandLine.hasOption('w') ? Integer.parseInt(commandLine.getOptionValue('w')) : 64; final int messageSize = commandLine.hasOption('s') ? Integer.parseInt(commandLine.getOptionValue('s')) : 128; final boolean keyEnable = commandLine.hasOption('k') && Boolean.parseBoolean(commandLine.getOptionValue('k')); final int propertySize = commandLine.hasOption('p') ? Integer.parseInt(commandLine.getOptionValue('p')) : 0; - - System.out.printf("topic %s threadCount %d messageSize %d keyEnable %s%n", topic, threadCount, messageSize, keyEnable); - - final Logger log = ClientLogger.getLog(); + final int tagCount = commandLine.hasOption('l') ? Integer.parseInt(commandLine.getOptionValue('l')) : 0; + final boolean msgTraceEnable = commandLine.hasOption('m') && Boolean.parseBoolean(commandLine.getOptionValue('m')); + final boolean aclEnable = commandLine.hasOption('a') && Boolean.parseBoolean(commandLine.getOptionValue('a')); + final long messageNum = commandLine.hasOption('q') ? Long.parseLong(commandLine.getOptionValue('q')) : 0; + final boolean delayEnable = commandLine.hasOption('d') && Boolean.parseBoolean(commandLine.getOptionValue('d')); + final int delayLevel = commandLine.hasOption('e') ? Integer.parseInt(commandLine.getOptionValue('e')) : 1; + final boolean asyncEnable = commandLine.hasOption('y') && Boolean.parseBoolean(commandLine.getOptionValue('y')); + final int threadCount = asyncEnable ? 1 : commandLine.hasOption('w') ? Integer.parseInt(commandLine.getOptionValue('w')) : 64; + final boolean enableCompress = commandLine.hasOption('c') && Boolean.parseBoolean(commandLine.getOptionValue('c')); + final int reportInterval = commandLine.hasOption("ri") ? Integer.parseInt(commandLine.getOptionValue("ri")) : 10000; + + System.out.printf("topic: %s, threadCount: %d, messageSize: %d, keyEnable: %s, propertySize: %d, tagCount: %d, " + + "traceEnable: %s, aclEnable: %s, messageQuantity: %d, delayEnable: %s, delayLevel: %s, " + + "asyncEnable: %s%n compressEnable: %s, reportInterval: %d%n", + topic, threadCount, messageSize, keyEnable, propertySize, tagCount, msgTraceEnable, aclEnable, messageNum, + delayEnable, delayLevel, asyncEnable, enableCompress, reportInterval); + + StringBuilder sb = new StringBuilder(messageSize); + for (int i = 0; i < messageSize; i++) { + sb.append(RandomStringUtils.randomAlphanumeric(1)); + } + msgBody = sb.toString().getBytes(StandardCharsets.UTF_8); final ExecutorService sendThreadPool = Executors.newFixedThreadPool(threadCount); final StatsBenchmarkProducer statsBenchmark = new StatsBenchmarkProducer(); - final Timer timer = new Timer("BenchmarkTimerThread", true); + ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1, + new BasicThreadFactory.Builder().namingPattern("BenchmarkTimerThread-%d").daemon(true).build()); + + final LinkedList snapshotList = new LinkedList<>(); - final LinkedList snapshotList = new LinkedList(); + final long[] msgNums = new long[threadCount]; - timer.scheduleAtFixedRate(new TimerTask() { + if (messageNum > 0) { + Arrays.fill(msgNums, messageNum / threadCount); + long mod = messageNum % threadCount; + if (mod > 0) { + msgNums[0] += mod; + } + } + + executorService.scheduleAtFixedRate(new TimerTask() { @Override public void run() { snapshotList.addLast(statsBenchmark.createSnapshot()); @@ -74,19 +123,12 @@ public void run() { snapshotList.removeFirst(); } } - }, 1000, 1000); + }, 1000, 1000, TimeUnit.MILLISECONDS); - timer.scheduleAtFixedRate(new TimerTask() { + executorService.scheduleAtFixedRate(new TimerTask() { private void printStats() { if (snapshotList.size() >= 10) { - Long[] begin = snapshotList.getFirst(); - Long[] end = snapshotList.getLast(); - - final long sendTps = (long) (((end[3] - begin[3]) / (double) (end[0] - begin[0])) * 1000L); - final double averageRT = (end[5] - begin[5]) / (double) (end[3] - begin[3]); - - System.out.printf("Send TPS: %d Max RT: %d Average RT: %7.3f Send Failed: %d Response Failed: %d%n", - sendTps, statsBenchmark.getSendMessageMaxRT().get(), averageRT, end[2], end[4]); + doPrintStats(snapshotList, statsBenchmark, false); } } @@ -98,9 +140,15 @@ public void run() { e.printStackTrace(); } } - }, 10000, 10000); + }, reportInterval, reportInterval, TimeUnit.MILLISECONDS); - final DefaultMQProducer producer = new DefaultMQProducer("benchmark_producer"); + RPCHook rpcHook = null; + if (aclEnable) { + String ak = commandLine.hasOption("ak") ? String.valueOf(commandLine.getOptionValue("ak")) : AclClient.ACL_ACCESS_KEY; + String sk = commandLine.hasOption("sk") ? String.valueOf(commandLine.getOptionValue("sk")) : AclClient.ACL_SECRET_KEY; + rpcHook = AclClient.getAclRPCHook(ak, sk); + } + final DefaultMQProducer producer = new DefaultMQProducer("benchmark_producer", rpcHook, msgTraceEnable, null); producer.setInstanceName(Long.toString(System.currentTimeMillis())); if (commandLine.hasOption('n')) { @@ -108,27 +156,42 @@ public void run() { producer.setNamesrvAddr(ns); } - producer.setCompressMsgBodyOverHowmuch(Integer.MAX_VALUE); + if (enableCompress) { + String compressType = commandLine.hasOption("ct") ? commandLine.getOptionValue("ct").trim() : "ZLIB"; + int compressLevel = commandLine.hasOption("cl") ? Integer.parseInt(commandLine.getOptionValue("cl")) : 5; + int compressOverHowMuch = commandLine.hasOption("ch") ? Integer.parseInt(commandLine.getOptionValue("ch")) : 4096; + producer.getDefaultMQProducerImpl().setCompressType(CompressionType.of(compressType)); + producer.getDefaultMQProducerImpl().setCompressLevel(compressLevel); + producer.setCompressMsgBodyOverHowmuch(compressOverHowMuch); + System.out.printf("compressType: %s compressLevel: %s%n", compressType, compressLevel); + } else { + producer.setCompressMsgBodyOverHowmuch(Integer.MAX_VALUE); + } producer.start(); for (int i = 0; i < threadCount; i++) { + final long msgNumLimit = msgNums[i]; + if (messageNum > 0 && msgNumLimit == 0) { + break; + } sendThreadPool.execute(new Runnable() { @Override public void run() { + int num = 0; while (true) { try { - final Message msg; - try { - msg = buildMessage(messageSize, topic); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - return; - } + final Message msg = buildMessage(topic); final long beginTimestamp = System.currentTimeMillis(); if (keyEnable) { msg.setKeys(String.valueOf(beginTimestamp / 1000)); } + if (delayEnable) { + msg.setDelayTimeLevel(delayLevel); + } + if (tagCount > 0) { + msg.setTags(String.format("tag%d", System.currentTimeMillis() % tagCount)); + } if (propertySize > 0) { if (msg.getProperties() != null) { msg.getProperties().clear(); @@ -149,21 +212,29 @@ public void run() { startValue += 2; } } - producer.send(msg); - statsBenchmark.getSendRequestSuccessCount().incrementAndGet(); - statsBenchmark.getReceiveResponseSuccessCount().incrementAndGet(); - final long currentRT = System.currentTimeMillis() - beginTimestamp; - statsBenchmark.getSendMessageSuccessTimeTotal().addAndGet(currentRT); - long prevMaxRT = statsBenchmark.getSendMessageMaxRT().get(); - while (currentRT > prevMaxRT) { - boolean updated = statsBenchmark.getSendMessageMaxRT().compareAndSet(prevMaxRT, currentRT); - if (updated) - break; - - prevMaxRT = statsBenchmark.getSendMessageMaxRT().get(); + if (asyncEnable) { + ThreadPoolExecutor e = (ThreadPoolExecutor) producer.getDefaultMQProducerImpl().getAsyncSenderExecutor(); + // Flow control + while (e.getQueue().size() > MAX_LENGTH_ASYNC_QUEUE) { + Thread.sleep(SLEEP_FOR_A_WHILE); + } + producer.send(msg, new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + updateStatsSuccess(statsBenchmark, beginTimestamp); + } + + @Override + public void onException(Throwable e) { + statsBenchmark.getSendRequestFailedCount().increment(); + } + }); + } else { + producer.send(msg); + updateStatsSuccess(statsBenchmark, beginTimestamp); } } catch (RemotingException e) { - statsBenchmark.getSendRequestFailedCount().incrementAndGet(); + statsBenchmark.getSendRequestFailedCount().increment(); log.error("[BENCHMARK_PRODUCER] Send Exception", e); try { @@ -171,26 +242,64 @@ public void run() { } catch (InterruptedException ignored) { } } catch (InterruptedException e) { - statsBenchmark.getSendRequestFailedCount().incrementAndGet(); + statsBenchmark.getSendRequestFailedCount().increment(); try { Thread.sleep(3000); } catch (InterruptedException e1) { } } catch (MQClientException e) { - statsBenchmark.getSendRequestFailedCount().incrementAndGet(); + statsBenchmark.getSendRequestFailedCount().increment(); log.error("[BENCHMARK_PRODUCER] Send Exception", e); } catch (MQBrokerException e) { - statsBenchmark.getReceiveResponseFailedCount().incrementAndGet(); + statsBenchmark.getReceiveResponseFailedCount().increment(); log.error("[BENCHMARK_PRODUCER] Send Exception", e); try { Thread.sleep(3000); } catch (InterruptedException ignored) { } } + if (messageNum > 0 && ++num >= msgNumLimit) { + break; + } } } }); } + try { + sendThreadPool.shutdown(); + sendThreadPool.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS); + executorService.shutdown(); + try { + executorService.awaitTermination(5000, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + } + + if (snapshotList.size() > 1) { + doPrintStats(snapshotList, statsBenchmark, true); + } else { + System.out.printf("[Complete] Send Total: %d Send Failed: %d Response Failed: %d%n", + statsBenchmark.getSendRequestSuccessCount().longValue() + statsBenchmark.getSendRequestFailedCount().longValue(), + statsBenchmark.getSendRequestFailedCount().longValue(), statsBenchmark.getReceiveResponseFailedCount().longValue()); + } + producer.shutdown(); + } catch (InterruptedException e) { + log.error("[Exit] Thread Interrupted Exception", e); + } + } + + private static void updateStatsSuccess(StatsBenchmarkProducer statsBenchmark, long beginTimestamp) { + statsBenchmark.getSendRequestSuccessCount().increment(); + statsBenchmark.getReceiveResponseSuccessCount().increment(); + final long currentRT = System.currentTimeMillis() - beginTimestamp; + statsBenchmark.getSendMessageSuccessTimeTotal().add(currentRT); + long prevMaxRT = statsBenchmark.getSendMessageMaxRT().longValue(); + while (currentRT > prevMaxRT) { + boolean updated = statsBenchmark.getSendMessageMaxRT().compareAndSet(prevMaxRT, currentRT); + if (updated) + break; + + prevMaxRT = statsBenchmark.getSendMessageMaxRT().longValue(); + } } public static Options buildCommandlineOptions(final Options options) { @@ -210,67 +319,130 @@ public static Options buildCommandlineOptions(final Options options) { opt.setRequired(false); options.addOption(opt); + opt = new Option("l", "tagCount", true, "Tag count, Default: 0"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("m", "msgTraceEnable", true, "Message Trace Enable, Default: false"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("a", "aclEnable", true, "Acl Enable, Default: false"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("ak", "accessKey", true, "Acl access key, Default: 12345678"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("sk", "secretKey", true, "Acl secret key, Default: rocketmq2"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("q", "messageQuantity", true, "Send message quantity, Default: 0, running forever"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("d", "delayEnable", true, "Delay message Enable, Default: false"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("e", "delayLevel", true, "Delay message level, Default: 1"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("y", "asyncEnable", true, "Enable async produce, Default: false"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("c", "compressEnable", true, "Enable compress msg over 4K, Default: false"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("ct", "compressType", true, "Message compressed type, Default: ZLIB"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("cl", "compressLevel", true, "Message compressed level, Default: 5"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("ch", "compressOverHowMuch", true, "Compress message when body over how much(unit Byte), Default: 4096"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("ri", "reportInterval", true, "The number of ms between reports, Default: 10000"); + opt.setRequired(false); + options.addOption(opt); + return options; } - private static Message buildMessage(final int messageSize, final String topic) throws UnsupportedEncodingException { - Message msg = new Message(); - msg.setTopic(topic); + private static Message buildMessage(final String topic) { + return new Message(topic, msgBody); + } - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < messageSize; i += 10) { - sb.append("hello baby"); - } + private static void doPrintStats(final LinkedList snapshotList, final StatsBenchmarkProducer statsBenchmark, boolean done) { + Long[] begin = snapshotList.getFirst(); + Long[] end = snapshotList.getLast(); - msg.setBody(sb.toString().getBytes(RemotingHelper.DEFAULT_CHARSET)); + final long sendTps = (long) (((end[3] - begin[3]) / (double) (end[0] - begin[0])) * 1000L); + final double averageRT = (end[5] - begin[5]) / (double) (end[3] - begin[3]); - return msg; + if (done) { + System.out.printf("[Complete] Send Total: %d | Send TPS: %d | Max RT(ms): %d | Average RT(ms): %7.3f | Send Failed: %d | Response Failed: %d%n", + statsBenchmark.getSendRequestSuccessCount().longValue() + statsBenchmark.getSendRequestFailedCount().longValue(), + sendTps, statsBenchmark.getSendMessageMaxRT().longValue(), averageRT, end[2], end[4]); + } else { + System.out.printf("Current Time: %s | Send TPS: %d | Max RT(ms): %d | Average RT(ms): %7.3f | Send Failed: %d | Response Failed: %d%n", + UtilAll.timeMillisToHumanString2(System.currentTimeMillis()), sendTps, statsBenchmark.getSendMessageMaxRT().longValue(), averageRT, end[2], end[4]); + } } } class StatsBenchmarkProducer { - private final AtomicLong sendRequestSuccessCount = new AtomicLong(0L); + private final LongAdder sendRequestSuccessCount = new LongAdder(); - private final AtomicLong sendRequestFailedCount = new AtomicLong(0L); + private final LongAdder sendRequestFailedCount = new LongAdder(); - private final AtomicLong receiveResponseSuccessCount = new AtomicLong(0L); + private final LongAdder receiveResponseSuccessCount = new LongAdder(); - private final AtomicLong receiveResponseFailedCount = new AtomicLong(0L); + private final LongAdder receiveResponseFailedCount = new LongAdder(); - private final AtomicLong sendMessageSuccessTimeTotal = new AtomicLong(0L); + private final LongAdder sendMessageSuccessTimeTotal = new LongAdder(); private final AtomicLong sendMessageMaxRT = new AtomicLong(0L); public Long[] createSnapshot() { Long[] snap = new Long[] { System.currentTimeMillis(), - this.sendRequestSuccessCount.get(), - this.sendRequestFailedCount.get(), - this.receiveResponseSuccessCount.get(), - this.receiveResponseFailedCount.get(), - this.sendMessageSuccessTimeTotal.get(), + this.sendRequestSuccessCount.longValue(), + this.sendRequestFailedCount.longValue(), + this.receiveResponseSuccessCount.longValue(), + this.receiveResponseFailedCount.longValue(), + this.sendMessageSuccessTimeTotal.longValue(), }; return snap; } - public AtomicLong getSendRequestSuccessCount() { + public LongAdder getSendRequestSuccessCount() { return sendRequestSuccessCount; } - public AtomicLong getSendRequestFailedCount() { + public LongAdder getSendRequestFailedCount() { return sendRequestFailedCount; } - public AtomicLong getReceiveResponseSuccessCount() { + public LongAdder getReceiveResponseSuccessCount() { return receiveResponseSuccessCount; } - public AtomicLong getReceiveResponseFailedCount() { + public LongAdder getReceiveResponseFailedCount() { return receiveResponseFailedCount; } - public AtomicLong getSendMessageSuccessTimeTotal() { + public LongAdder getSendMessageSuccessTimeTotal() { return sendMessageSuccessTimeTotal; } diff --git a/example/src/main/java/org/apache/rocketmq/example/benchmark/TransactionProducer.java b/example/src/main/java/org/apache/rocketmq/example/benchmark/TransactionProducer.java index d9fafdd08e8..7b6350e3bd1 100644 --- a/example/src/main/java/org/apache/rocketmq/example/benchmark/TransactionProducer.java +++ b/example/src/main/java/org/apache/rocketmq/example/benchmark/TransactionProducer.java @@ -17,46 +17,80 @@ package org.apache.rocketmq.example.benchmark; -import java.io.UnsupportedEncodingException; -import java.util.LinkedList; -import java.util.Timer; -import java.util.TimerTask; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicLong; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.apache.rocketmq.client.exception.MQClientException; -import org.apache.rocketmq.client.producer.LocalTransactionExecuter; import org.apache.rocketmq.client.producer.LocalTransactionState; import org.apache.rocketmq.client.producer.SendResult; -import org.apache.rocketmq.client.producer.TransactionCheckListener; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.client.producer.TransactionListener; import org.apache.rocketmq.client.producer.TransactionMQProducer; +import org.apache.rocketmq.common.UtilAll; import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageConst; import org.apache.rocketmq.common.message.MessageExt; -import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.SerializeType; +import org.apache.rocketmq.srvutil.ServerUtil; -public class TransactionProducer { - private static int threadCount; - private static int messageSize; - private static boolean ischeck; - private static boolean ischeckffalse; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.TimerTask; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.LongAdder; - public static void main(String[] args) throws MQClientException, UnsupportedEncodingException { - threadCount = args.length >= 1 ? Integer.parseInt(args[0]) : 32; - messageSize = args.length >= 2 ? Integer.parseInt(args[1]) : 1024 * 2; - ischeck = args.length >= 3 && Boolean.parseBoolean(args[2]); - ischeckffalse = args.length >= 4 && Boolean.parseBoolean(args[3]); +public class TransactionProducer { + private static final long START_TIME = System.currentTimeMillis(); + private static final LongAdder MSG_COUNT = new LongAdder(); - final Message msg = buildMessage(messageSize); + //broker max check times should less than this value + static final int MAX_CHECK_RESULT_IN_MSG = 20; - final ExecutorService sendThreadPool = Executors.newFixedThreadPool(threadCount); + public static void main(String[] args) throws MQClientException, UnsupportedEncodingException { + System.setProperty(RemotingCommand.SERIALIZE_TYPE_PROPERTY, SerializeType.ROCKETMQ.name()); + Options options = ServerUtil.buildCommandlineOptions(new Options()); + CommandLine commandLine = ServerUtil.parseCmdLine("TransactionProducer", args, buildCommandlineOptions(options), new DefaultParser()); + TxSendConfig config = new TxSendConfig(); + config.topic = commandLine.hasOption('t') ? commandLine.getOptionValue('t').trim() : "BenchmarkTest"; + config.threadCount = commandLine.hasOption('w') ? Integer.parseInt(commandLine.getOptionValue('w')) : 32; + config.messageSize = commandLine.hasOption('s') ? Integer.parseInt(commandLine.getOptionValue('s')) : 2048; + config.sendRollbackRate = commandLine.hasOption("sr") ? Double.parseDouble(commandLine.getOptionValue("sr")) : 0.0; + config.sendUnknownRate = commandLine.hasOption("su") ? Double.parseDouble(commandLine.getOptionValue("su")) : 0.0; + config.checkRollbackRate = commandLine.hasOption("cr") ? Double.parseDouble(commandLine.getOptionValue("cr")) : 0.0; + config.checkUnknownRate = commandLine.hasOption("cu") ? Double.parseDouble(commandLine.getOptionValue("cu")) : 0.0; + config.batchId = commandLine.hasOption("b") ? Long.parseLong(commandLine.getOptionValue("b")) : System.currentTimeMillis(); + config.sendInterval = commandLine.hasOption("i") ? Integer.parseInt(commandLine.getOptionValue("i")) : 0; + config.aclEnable = commandLine.hasOption('a') && Boolean.parseBoolean(commandLine.getOptionValue('a')); + config.msgTraceEnable = commandLine.hasOption('m') && Boolean.parseBoolean(commandLine.getOptionValue('m')); + config.reportInterval = commandLine.hasOption("ri") ? Integer.parseInt(commandLine.getOptionValue("ri")) : 10000; + + final ExecutorService sendThreadPool = Executors.newFixedThreadPool(config.threadCount); final StatsBenchmarkTProducer statsBenchmark = new StatsBenchmarkTProducer(); - final Timer timer = new Timer("BenchmarkTimerThread", true); + ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1, + new BasicThreadFactory.Builder().namingPattern("BenchmarkTimerThread-%d").daemon(true).build()); - final LinkedList snapshotList = new LinkedList(); + final LinkedList snapshotList = new LinkedList<>(); - timer.scheduleAtFixedRate(new TimerTask() { + executorService.scheduleAtFixedRate(new TimerTask() { @Override public void run() { snapshotList.addLast(statsBenchmark.createSnapshot()); @@ -64,21 +98,28 @@ public void run() { snapshotList.removeFirst(); } } - }, 1000, 1000); + }, 1000, 1000, TimeUnit.MILLISECONDS); - timer.scheduleAtFixedRate(new TimerTask() { + executorService.scheduleAtFixedRate(new TimerTask() { private void printStats() { if (snapshotList.size() >= 10) { - Long[] begin = snapshotList.getFirst(); - Long[] end = snapshotList.getLast(); + Snapshot begin = snapshotList.getFirst(); + Snapshot end = snapshotList.getLast(); - final long sendTps = - (long) (((end[3] - begin[3]) / (double) (end[0] - begin[0])) * 1000L); - final double averageRT = (end[5] - begin[5]) / (double) (end[3] - begin[3]); + final long sendCount = end.sendRequestSuccessCount - begin.sendRequestSuccessCount; + final long sendTps = (sendCount * 1000L) / (end.endTime - begin.endTime); + final double averageRT = (end.sendMessageTimeTotal - begin.sendMessageTimeTotal) / (double) (end.sendRequestSuccessCount - begin.sendRequestSuccessCount); + + final long failCount = end.sendRequestFailedCount - begin.sendRequestFailedCount; + final long checkCount = end.checkCount - begin.checkCount; + final long unexpectedCheck = end.unexpectedCheckCount - begin.unexpectedCheckCount; + final long dupCheck = end.duplicatedCheck - begin.duplicatedCheck; System.out.printf( - "Send TPS: %d Max RT: %d Average RT: %7.3f Send Failed: %d Response Failed: %d transaction checkCount: %d %n", - sendTps, statsBenchmark.getSendMessageMaxRT().get(), averageRT, end[2], end[4], end[6]); + "Current Time: %s | Send TPS: %5d | Max RT(ms): %5d | AVG RT(ms): %3.1f | Send Failed: %d | Check: %d | UnexpectedCheck: %d | DuplicatedCheck: %d%n", + UtilAll.timeMillisToHumanString2(System.currentTimeMillis()), sendTps, statsBenchmark.getSendMessageMaxRT().get(), averageRT, failCount, checkCount, + unexpectedCheck, dupCheck); + statsBenchmark.getSendMessageMaxRT().set(0); } } @@ -90,47 +131,65 @@ public void run() { e.printStackTrace(); } } - }, 10000, 10000); + }, config.reportInterval, config.reportInterval, TimeUnit.MILLISECONDS); - final TransactionCheckListener transactionCheckListener = - new TransactionCheckListenerBImpl(ischeckffalse, statsBenchmark); - final TransactionMQProducer producer = new TransactionMQProducer("benchmark_transaction_producer"); + RPCHook rpcHook = null; + if (config.aclEnable) { + String ak = commandLine.hasOption("ak") ? String.valueOf(commandLine.getOptionValue("ak")) : AclClient.ACL_ACCESS_KEY; + String sk = commandLine.hasOption("sk") ? String.valueOf(commandLine.getOptionValue("sk")) : AclClient.ACL_SECRET_KEY; + rpcHook = AclClient.getAclRPCHook(ak, sk); + } + final TransactionListener transactionCheckListener = new TransactionListenerImpl(statsBenchmark, config); + final TransactionMQProducer producer = new TransactionMQProducer( + "benchmark_transaction_producer", + rpcHook, + config.msgTraceEnable, + null); producer.setInstanceName(Long.toString(System.currentTimeMillis())); - producer.setTransactionCheckListener(transactionCheckListener); + producer.setTransactionListener(transactionCheckListener); producer.setDefaultTopicQueueNums(1000); + if (commandLine.hasOption('n')) { + String ns = commandLine.getOptionValue('n'); + producer.setNamesrvAddr(ns); + } producer.start(); - final TransactionExecuterBImpl tranExecuter = new TransactionExecuterBImpl(ischeck); - - for (int i = 0; i < threadCount; i++) { + for (int i = 0; i < config.threadCount; i++) { sendThreadPool.execute(new Runnable() { @Override public void run() { while (true) { + boolean success = false; + final long beginTimestamp = System.currentTimeMillis(); try { - // Thread.sleep(1000); - final long beginTimestamp = System.currentTimeMillis(); SendResult sendResult = - producer.sendMessageInTransaction(msg, tranExecuter, null); - if (sendResult != null) { - statsBenchmark.getSendRequestSuccessCount().incrementAndGet(); - statsBenchmark.getReceiveResponseSuccessCount().incrementAndGet(); - } - + producer.sendMessageInTransaction(buildMessage(config), null); + success = sendResult != null && sendResult.getSendStatus() == SendStatus.SEND_OK; + } catch (Throwable e) { + success = false; + } finally { final long currentRT = System.currentTimeMillis() - beginTimestamp; - statsBenchmark.getSendMessageSuccessTimeTotal().addAndGet(currentRT); + statsBenchmark.getSendMessageTimeTotal().add(currentRT); long prevMaxRT = statsBenchmark.getSendMessageMaxRT().get(); while (currentRT > prevMaxRT) { - boolean updated = - statsBenchmark.getSendMessageMaxRT().compareAndSet(prevMaxRT, - currentRT); + boolean updated = statsBenchmark.getSendMessageMaxRT() + .compareAndSet(prevMaxRT, currentRT); if (updated) break; prevMaxRT = statsBenchmark.getSendMessageMaxRT().get(); } - } catch (MQClientException e) { - statsBenchmark.getSendRequestFailedCount().incrementAndGet(); + if (success) { + statsBenchmark.getSendRequestSuccessCount().increment(); + } else { + statsBenchmark.getSendRequestFailedCount().increment(); + } + if (config.sendInterval > 0) { + try { + Thread.sleep(config.sendInterval); + } catch (InterruptedException e) { + } + } } } } @@ -138,113 +197,300 @@ public void run() { } } - private static Message buildMessage(final int messageSize) throws UnsupportedEncodingException { - Message msg = new Message(); - msg.setTopic("BenchmarkTest"); + private static Message buildMessage(TxSendConfig config) { + byte[] bs = new byte[config.messageSize]; + ThreadLocalRandom r = ThreadLocalRandom.current(); + r.nextBytes(bs); + + ByteBuffer buf = ByteBuffer.wrap(bs); + buf.putLong(config.batchId); + long sendMachineId = START_TIME << 32; + long count = MSG_COUNT.longValue(); + long msgId = sendMachineId | count; + MSG_COUNT.increment(); + buf.putLong(msgId); + + // save send tx result in message + if (r.nextDouble() < config.sendRollbackRate) { + buf.put((byte) LocalTransactionState.ROLLBACK_MESSAGE.ordinal()); + } else if (r.nextDouble() < config.sendUnknownRate) { + buf.put((byte) LocalTransactionState.UNKNOW.ordinal()); + } else { + buf.put((byte) LocalTransactionState.COMMIT_MESSAGE.ordinal()); + } - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < messageSize; i += 10) { - sb.append("hello baby"); + // save check tx result in message + for (int i = 0; i < MAX_CHECK_RESULT_IN_MSG; i++) { + if (r.nextDouble() < config.checkRollbackRate) { + buf.put((byte) LocalTransactionState.ROLLBACK_MESSAGE.ordinal()); + } else if (r.nextDouble() < config.checkUnknownRate) { + buf.put((byte) LocalTransactionState.UNKNOW.ordinal()); + } else { + buf.put((byte) LocalTransactionState.COMMIT_MESSAGE.ordinal()); + } } - msg.setBody(sb.toString().getBytes(RemotingHelper.DEFAULT_CHARSET)); + Message msg = new Message(); + msg.setTopic(config.topic); + msg.setBody(bs); return msg; } + + public static Options buildCommandlineOptions(final Options options) { + Option opt = new Option("w", "threadCount", true, "Thread count, Default: 32"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("s", "messageSize", true, "Message Size, Default: 2048"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("t", "topic", true, "Topic name, Default: BenchmarkTest"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("sr", "send rollback rate", true, "Send rollback rate, Default: 0.0"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("su", "send unknown rate", true, "Send unknown rate, Default: 0.0"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("cr", "check rollback rate", true, "Check rollback rate, Default: 0.0"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("cu", "check unknown rate", true, "Check unknown rate, Default: 0.0"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("b", "test batch id", true, "test batch id, Default: System.currentMillis()"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("i", "send interval", true, "sleep interval in millis between messages, Default: 0"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("a", "aclEnable", true, "Acl Enable, Default: false"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("ak", "accessKey", true, "Acl access key, Default: 12345678"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("sk", "secretKey", true, "Acl secret key, Default: rocketmq2"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("m", "msgTraceEnable", true, "Message Trace Enable, Default: false"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("ri", "reportInterval", true, "The number of ms between reports, Default: 10000"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } } -class TransactionExecuterBImpl implements LocalTransactionExecuter { +class TransactionListenerImpl implements TransactionListener { + private StatsBenchmarkTProducer statBenchmark; + private TxSendConfig sendConfig; + private final LRUMap cache = new LRUMap<>(200000); - private boolean ischeck; + private class MsgMeta { + long batchId; + long msgId; + LocalTransactionState sendResult; + List checkResult; + } - public TransactionExecuterBImpl(boolean ischeck) { - this.ischeck = ischeck; + public TransactionListenerImpl(StatsBenchmarkTProducer statsBenchmark, TxSendConfig sendConfig) { + this.statBenchmark = statsBenchmark; + this.sendConfig = sendConfig; } @Override - public LocalTransactionState executeLocalTransactionBranch(final Message msg, final Object arg) { - if (ischeck) { - return LocalTransactionState.UNKNOW; - } - return LocalTransactionState.COMMIT_MESSAGE; + public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { + return parseFromMsg(msg).sendResult; } -} - -class TransactionCheckListenerBImpl implements TransactionCheckListener { - private boolean ischeckffalse; - private StatsBenchmarkTProducer statsBenchmarkTProducer; - public TransactionCheckListenerBImpl(boolean ischeckffalse, - StatsBenchmarkTProducer statsBenchmarkTProducer) { - this.ischeckffalse = ischeckffalse; - this.statsBenchmarkTProducer = statsBenchmarkTProducer; + private MsgMeta parseFromMsg(Message msg) { + byte[] bs = msg.getBody(); + ByteBuffer buf = ByteBuffer.wrap(bs); + MsgMeta msgMeta = new MsgMeta(); + msgMeta.batchId = buf.getLong(); + msgMeta.msgId = buf.getLong(); + msgMeta.sendResult = LocalTransactionState.values()[buf.get()]; + msgMeta.checkResult = new ArrayList<>(); + for (int i = 0; i < TransactionProducer.MAX_CHECK_RESULT_IN_MSG; i++) { + msgMeta.checkResult.add(LocalTransactionState.values()[buf.get()]); + } + return msgMeta; } @Override - public LocalTransactionState checkLocalTransactionState(MessageExt msg) { - statsBenchmarkTProducer.getCheckRequestSuccessCount().incrementAndGet(); - if (ischeckffalse) { + public LocalTransactionState checkLocalTransaction(MessageExt msg) { + MsgMeta msgMeta = parseFromMsg(msg); + if (msgMeta.batchId != sendConfig.batchId) { + // message not generated in this test + return LocalTransactionState.ROLLBACK_MESSAGE; + } + statBenchmark.getCheckCount().increment(); + int times = 0; + try { + String checkTimes = msg.getUserProperty(MessageConst.PROPERTY_TRANSACTION_CHECK_TIMES); + times = Integer.parseInt(checkTimes); + } catch (Exception e) { return LocalTransactionState.ROLLBACK_MESSAGE; } + times = times <= 0 ? 1 : times; + + boolean dup; + synchronized (cache) { + Integer oldCheckLog = cache.get(msgMeta.msgId); + Integer newCheckLog; + if (oldCheckLog == null) { + newCheckLog = 1 << (times - 1); + } else { + newCheckLog = oldCheckLog | (1 << (times - 1)); + } + dup = newCheckLog.equals(oldCheckLog); + } + if (dup) { + statBenchmark.getDuplicatedCheckCount().increment(); + } + if (msgMeta.sendResult != LocalTransactionState.UNKNOW) { + System.out.printf("%s unexpected check: msgId=%s,txId=%s,checkTimes=%s,sendResult=%s\n", + new SimpleDateFormat("HH:mm:ss,SSS").format(new Date()), + msg.getMsgId(), msg.getTransactionId(), + msg.getUserProperty(MessageConst.PROPERTY_TRANSACTION_CHECK_TIMES), + msgMeta.sendResult.toString()); + statBenchmark.getUnexpectedCheckCount().increment(); + return msgMeta.sendResult; + } - return LocalTransactionState.COMMIT_MESSAGE; + for (int i = 0; i < times - 1; i++) { + LocalTransactionState s = msgMeta.checkResult.get(i); + if (s != LocalTransactionState.UNKNOW) { + System.out.printf("%s unexpected check: msgId=%s,txId=%s,checkTimes=%s,sendResult,lastCheckResult=%s\n", + new SimpleDateFormat("HH:mm:ss,SSS").format(new Date()), + msg.getMsgId(), msg.getTransactionId(), + msg.getUserProperty(MessageConst.PROPERTY_TRANSACTION_CHECK_TIMES), s); + statBenchmark.getUnexpectedCheckCount().increment(); + return s; + } + } + return msgMeta.checkResult.get(times - 1); } } -class StatsBenchmarkTProducer { - private final AtomicLong sendRequestSuccessCount = new AtomicLong(0L); +class Snapshot { + long endTime; + + long sendRequestSuccessCount; + + long sendRequestFailedCount; + + long sendMessageTimeTotal; - private final AtomicLong sendRequestFailedCount = new AtomicLong(0L); + long sendMessageMaxRT; - private final AtomicLong receiveResponseSuccessCount = new AtomicLong(0L); + long checkCount; - private final AtomicLong receiveResponseFailedCount = new AtomicLong(0L); + long unexpectedCheckCount; - private final AtomicLong sendMessageSuccessTimeTotal = new AtomicLong(0L); + long duplicatedCheck; +} + +class StatsBenchmarkTProducer { + private final LongAdder sendRequestSuccessCount = new LongAdder(); + + private final LongAdder sendRequestFailedCount = new LongAdder(); + + private final LongAdder sendMessageTimeTotal = new LongAdder(); private final AtomicLong sendMessageMaxRT = new AtomicLong(0L); - private final AtomicLong checkRequestSuccessCount = new AtomicLong(0L); + private final LongAdder checkCount = new LongAdder(); - public Long[] createSnapshot() { - Long[] snap = new Long[] { - System.currentTimeMillis(), - this.sendRequestSuccessCount.get(), - this.sendRequestFailedCount.get(), - this.receiveResponseSuccessCount.get(), - this.receiveResponseFailedCount.get(), - this.sendMessageSuccessTimeTotal.get(), - this.checkRequestSuccessCount.get()}; + private final LongAdder unexpectedCheckCount = new LongAdder(); - return snap; + private final LongAdder duplicatedCheckCount = new LongAdder(); + + public Snapshot createSnapshot() { + Snapshot s = new Snapshot(); + s.endTime = System.currentTimeMillis(); + s.sendRequestSuccessCount = sendRequestSuccessCount.longValue(); + s.sendRequestFailedCount = sendRequestFailedCount.longValue(); + s.sendMessageTimeTotal = sendMessageTimeTotal.longValue(); + s.sendMessageMaxRT = sendMessageMaxRT.get(); + s.checkCount = checkCount.longValue(); + s.unexpectedCheckCount = unexpectedCheckCount.longValue(); + s.duplicatedCheck = duplicatedCheckCount.longValue(); + return s; } - public AtomicLong getSendRequestSuccessCount() { + public LongAdder getSendRequestSuccessCount() { return sendRequestSuccessCount; } - public AtomicLong getSendRequestFailedCount() { + public LongAdder getSendRequestFailedCount() { return sendRequestFailedCount; } - public AtomicLong getReceiveResponseSuccessCount() { - return receiveResponseSuccessCount; + public LongAdder getSendMessageTimeTotal() { + return sendMessageTimeTotal; } - public AtomicLong getReceiveResponseFailedCount() { - return receiveResponseFailedCount; + public AtomicLong getSendMessageMaxRT() { + return sendMessageMaxRT; } - public AtomicLong getSendMessageSuccessTimeTotal() { - return sendMessageSuccessTimeTotal; + public LongAdder getCheckCount() { + return checkCount; } - public AtomicLong getSendMessageMaxRT() { - return sendMessageMaxRT; + public LongAdder getUnexpectedCheckCount() { + return unexpectedCheckCount; } - public AtomicLong getCheckRequestSuccessCount() { - return checkRequestSuccessCount; + public LongAdder getDuplicatedCheckCount() { + return duplicatedCheckCount; + } +} + +class TxSendConfig { + String topic; + int threadCount; + int messageSize; + double sendRollbackRate; + double sendUnknownRate; + double checkRollbackRate; + double checkUnknownRate; + long batchId; + int sendInterval; + boolean aclEnable; + boolean msgTraceEnable; + int reportInterval; +} + +class LRUMap extends LinkedHashMap { + + private int maxSize; + + public LRUMap(int maxSize) { + this.maxSize = maxSize; + } + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > maxSize; } } diff --git a/example/src/main/java/org/apache/rocketmq/example/benchmark/timer/TimerConsumer.java b/example/src/main/java/org/apache/rocketmq/example/benchmark/timer/TimerConsumer.java new file mode 100644 index 00000000000..f2ab6b57310 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/benchmark/timer/TimerConsumer.java @@ -0,0 +1,191 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.example.benchmark.timer; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.srvutil.ServerUtil; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +public class TimerConsumer { + private final String topic; + + private final ScheduledExecutorService scheduledExecutor = new ScheduledThreadPoolExecutor(1, new ThreadFactoryImpl("ConsumerScheduleThread_")); + + private final StatsBenchmarkConsumer statsBenchmark = new StatsBenchmarkConsumer(); + private final LinkedList snapshotList = new LinkedList<>(); + + private final DefaultMQPushConsumer consumer; + + public TimerConsumer(String[] args) { + Options options = ServerUtil.buildCommandlineOptions(new Options()); + final CommandLine commandLine = ServerUtil.parseCmdLine("benchmarkTimerConsumer", args, buildCommandlineOptions(options), new DefaultParser()); + if (null == commandLine) { + System.exit(-1); + } + + final String namesrvAddr = commandLine.hasOption('n') ? commandLine.getOptionValue('t').trim() : "localhost:9876"; + topic = commandLine.hasOption('t') ? commandLine.getOptionValue('t').trim() : "BenchmarkTest"; + System.out.printf("namesrvAddr: %s, topic: %s%n", namesrvAddr, topic); + + consumer = new DefaultMQPushConsumer("benchmark_consumer"); + consumer.setInstanceName(Long.toString(System.currentTimeMillis())); + consumer.setNamesrvAddr(namesrvAddr); + } + + public void startScheduleTask() { + scheduledExecutor.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + snapshotList.addLast(statsBenchmark.createSnapshot()); + if (snapshotList.size() > 10) { + snapshotList.removeFirst(); + } + } + }, 1000, 1000, TimeUnit.MILLISECONDS); + + scheduledExecutor.scheduleAtFixedRate(new TimerTask() { + private void printStats() { + if (snapshotList.size() >= 10) { + Long[] begin = snapshotList.getFirst(); + Long[] end = snapshotList.getLast(); + + final long consumeTps = + (long) (((end[1] - begin[1]) / (double) (end[0] - begin[0])) * 1000L); + final double avgDelayedDuration = (end[2] - begin[2]) / (double) (end[1] - begin[1]); + + List delayedDurationList = new ArrayList<>(TimerConsumer.this.statsBenchmark.getDelayedDurationMsSet()); + if (delayedDurationList.isEmpty()) { + System.out.printf("Consume TPS: %d, Avg delayedDuration: %7.3f, Max delayedDuration: 0, %n", + consumeTps, avgDelayedDuration); + } else { + long delayedDuration25 = delayedDurationList.get((int) (delayedDurationList.size() * 0.25)); + long delayedDuration50 = delayedDurationList.get((int) (delayedDurationList.size() * 0.5)); + long delayedDuration80 = delayedDurationList.get((int) (delayedDurationList.size() * 0.8)); + long delayedDuration90 = delayedDurationList.get((int) (delayedDurationList.size() * 0.9)); + long delayedDuration99 = delayedDurationList.get((int) (delayedDurationList.size() * 0.99)); + long delayedDuration999 = delayedDurationList.get((int) (delayedDurationList.size() * 0.999)); + + System.out.printf("Consume TPS: %d, Avg delayedDuration: %7.3f, Max delayedDuration: %d, " + + "delayDuration %%25: %d, %%50: %d; %%80: %d; %%90: %d; %%99: %d; %%99.9: %d%n", + consumeTps, avgDelayedDuration, delayedDurationList.get(delayedDurationList.size() - 1), + delayedDuration25, delayedDuration50, delayedDuration80, delayedDuration90, delayedDuration99, delayedDuration999); + } + } + } + + @Override + public void run() { + try { + this.printStats(); + } catch (Exception e) { + e.printStackTrace(); + } + } + }, 10000, 10000, TimeUnit.MILLISECONDS); + } + + public void start() throws MQClientException { + consumer.subscribe(topic, "*"); + + consumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + MessageExt msg = msgs.get(0); + long now = System.currentTimeMillis(); + + statsBenchmark.getReceiveMessageTotalCount().incrementAndGet(); + + long deliverTimeMs = Long.parseLong(msg.getProperty("MY_RECORD_TIMER_DELIVER_MS")); + long delayedDuration = now - deliverTimeMs; + + statsBenchmark.getDelayedDurationMsSet().add(delayedDuration); + statsBenchmark.getDelayedDurationMsTotal().addAndGet(delayedDuration); + + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + + consumer.start(); + System.out.printf("Start receiving messages%n"); + } + + private Options buildCommandlineOptions(Options options) { + Option opt = new Option("n", "namesrvAddr", true, "Nameserver address, default: localhost:9876"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("t", "topic", true, "Send messages to which topic, default: BenchmarkTest"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + public static void main(String[] args) throws MQClientException { + TimerConsumer timerConsumer = new TimerConsumer(args); + timerConsumer.startScheduleTask(); + timerConsumer.start(); + } + + + public static class StatsBenchmarkConsumer { + private final AtomicLong receiveMessageTotalCount = new AtomicLong(0L); + + private final AtomicLong delayedDurationMsTotal = new AtomicLong(0L); + private final ConcurrentSkipListSet delayedDurationMsSet = new ConcurrentSkipListSet<>(); + + public Long[] createSnapshot() { + return new Long[]{ + System.currentTimeMillis(), + this.receiveMessageTotalCount.get(), + this.delayedDurationMsTotal.get(), + }; + } + + public AtomicLong getReceiveMessageTotalCount() { + return receiveMessageTotalCount; + } + + public AtomicLong getDelayedDurationMsTotal() { + return delayedDurationMsTotal; + } + + public ConcurrentSkipListSet getDelayedDurationMsSet() { + return delayedDurationMsSet; + } + } + +} diff --git a/example/src/main/java/org/apache/rocketmq/example/benchmark/timer/TimerProducer.java b/example/src/main/java/org/apache/rocketmq/example/benchmark/timer/TimerProducer.java new file mode 100644 index 00000000000..3e92ff1b0be --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/benchmark/timer/TimerProducer.java @@ -0,0 +1,322 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.example.benchmark.timer; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.srvutil.ServerUtil; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.TimerTask; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +public class TimerProducer { + private static final Logger log = LoggerFactory.getLogger(TimerProducer.class); + + private final String topic; + private final int threadCount; + private final int messageSize; + + private final int precisionMs; + private final int slotsTotal; + private final int msgsTotalPerSlotThread; + private final int slotDis; + + private final ScheduledExecutorService scheduledExecutor = new ScheduledThreadPoolExecutor(1, new ThreadFactoryImpl("ProducerScheduleThread_")); + private final ExecutorService sendThreadPool; + + private final StatsBenchmarkProducer statsBenchmark = new StatsBenchmarkProducer(); + private final LinkedList snapshotList = new LinkedList<>(); + + private final DefaultMQProducer producer; + + public TimerProducer(String[] args) { + Options options = ServerUtil.buildCommandlineOptions(new Options()); + final CommandLine commandLine = ServerUtil.parseCmdLine("benchmarkTimerProducer", args, buildCommandlineOptions(options), new DefaultParser()); + if (null == commandLine) { + System.exit(-1); + } + + final String namesrvAddr = commandLine.hasOption('n') ? commandLine.getOptionValue('t').trim() : "localhost:9876"; + topic = commandLine.hasOption('t') ? commandLine.getOptionValue('t').trim() : "BenchmarkTest"; + threadCount = commandLine.hasOption("tc") ? Integer.parseInt(commandLine.getOptionValue("tc")) : 16; + messageSize = commandLine.hasOption("ms") ? Integer.parseInt(commandLine.getOptionValue("ms")) : 1024; + precisionMs = commandLine.hasOption('p') ? Integer.parseInt(commandLine.getOptionValue("p")) : 1000; + slotsTotal = commandLine.hasOption("st") ? Integer.parseInt(commandLine.getOptionValue("st")) : 100; + msgsTotalPerSlotThread = commandLine.hasOption("mt") ? Integer.parseInt(commandLine.getOptionValue("mt")) : 5000; + slotDis = commandLine.hasOption("sd") ? Integer.parseInt(commandLine.getOptionValue("sd")) : 1000; + System.out.printf("namesrvAddr: %s, topic: %s, threadCount: %d, messageSize: %d, precisionMs: %d, slotsTotal: %d, msgsTotalPerSlotThread: %d, slotDis: %d%n", + namesrvAddr, topic, threadCount, messageSize, precisionMs, slotsTotal, msgsTotalPerSlotThread, slotDis); + + sendThreadPool = new ThreadPoolExecutor( + threadCount, + threadCount, + 0L, + TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(), + new ThreadFactoryImpl("ProducerSendMessageThread_")); + + producer = new DefaultMQProducer("benchmark_producer"); + producer.setInstanceName(Long.toString(System.currentTimeMillis())); + producer.setNamesrvAddr(namesrvAddr); + producer.setCompressMsgBodyOverHowmuch(Integer.MAX_VALUE); + } + + public void startScheduleTask() { + scheduledExecutor.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + snapshotList.addLast(statsBenchmark.createSnapshot()); + if (snapshotList.size() > 10) { + snapshotList.removeFirst(); + } + } + }, 1000, 1000, TimeUnit.MILLISECONDS); + + scheduledExecutor.scheduleAtFixedRate(new TimerTask() { + private void printStats() { + if (snapshotList.size() >= 10) { + Long[] begin = snapshotList.getFirst(); + Long[] end = snapshotList.getLast(); + + final long sendTps = (long) (((end[3] - begin[3]) / (double) (end[0] - begin[0])) * 1000L); + final double averageRT = (end[5] - begin[5]) / (double) (end[3] - begin[3]); + + System.out.printf("Send TPS: %d, Max RT: %d, Average RT: %7.3f, Send Failed: %d, Response Failed: %d%n", + sendTps, statsBenchmark.getSendMessageMaxRT().get(), averageRT, end[2], end[4]); + } + } + + @Override + public void run() { + try { + this.printStats(); + } catch (Exception e) { + e.printStackTrace(); + } + } + }, 10000, 10000, TimeUnit.MILLISECONDS); + } + + public void start() throws MQClientException { + producer.start(); + System.out.printf("Start sending messages%n"); + List delayList = new ArrayList<>(); + final long startDelayTime = System.currentTimeMillis() / precisionMs * precisionMs + 2 * 60 * 1000 + 10; + for (int slotCnt = 0; slotCnt < slotsTotal; slotCnt++) { + for (int msgCnt = 0; msgCnt < msgsTotalPerSlotThread; msgCnt++) { + long delayTime = startDelayTime + slotCnt * slotDis; + delayList.add(delayTime); + } + } + Collections.shuffle(delayList); + // DelayTime is from 2 minutes later. + + for (int i = 0; i < threadCount; i++) { + sendThreadPool.execute(new Runnable() { + @Override + public void run() { + for (int slotCnt = 0; slotCnt < slotsTotal; slotCnt++) { + + for (int msgCnt = 0; msgCnt < msgsTotalPerSlotThread; msgCnt++) { + final long beginTimestamp = System.currentTimeMillis(); + + long delayTime = delayList.get(slotCnt * msgsTotalPerSlotThread + msgCnt); + + final Message msg; + try { + msg = buildMessage(messageSize, topic); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + return; + } + msg.putUserProperty("MY_RECORD_TIMER_DELIVER_MS", String.valueOf(delayTime)); + msg.getProperties().put(MessageConst.PROPERTY_TIMER_DELIVER_MS, String.valueOf(delayTime)); + + try { + producer.send(msg); + + statsBenchmark.getSendRequestSuccessCount().incrementAndGet(); + statsBenchmark.getReceiveResponseSuccessCount().incrementAndGet(); + + final long currentRT = System.currentTimeMillis() - beginTimestamp; + statsBenchmark.getSendMessageSuccessTimeTotal().addAndGet(currentRT); + + long prevMaxRT = statsBenchmark.getSendMessageMaxRT().get(); + while (currentRT > prevMaxRT) { + if (statsBenchmark.getSendMessageMaxRT().compareAndSet(prevMaxRT, currentRT)) { + break; + } + prevMaxRT = statsBenchmark.getSendMessageMaxRT().get(); + } + } catch (RemotingException e) { + statsBenchmark.getSendRequestFailedCount().incrementAndGet(); + log.error("[BENCHMARK_PRODUCER] Send Exception", e); + sleep(3000); + } catch (InterruptedException e) { + statsBenchmark.getSendRequestFailedCount().incrementAndGet(); + sleep(3000); + } catch (MQClientException e) { + statsBenchmark.getSendRequestFailedCount().incrementAndGet(); + log.error("[BENCHMARK_PRODUCER] Send Exception", e); + } catch (MQBrokerException e) { + statsBenchmark.getReceiveResponseFailedCount().incrementAndGet(); + log.error("[BENCHMARK_PRODUCER] Send Exception", e); + sleep(3000); + } + } + + } + } + }); + } + } + + private Options buildCommandlineOptions(Options options) { + Option opt = new Option("n", "namesrvAddr", true, "Nameserver address, default: localhost:9876"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("t", "topic", true, "Send messages to which topic, default: BenchmarkTest"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("tc", "threadCount", true, "Thread count, default: 64"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("ms", "messageSize", true, "Message Size, default: 128"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("p", "precisionMs", true, "Precision (ms) for TimerMessage, default: 1000"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("st", "slotsTotal", true, "Send messages to how many slots, default: 100"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("mt", "msgsTotalPerSlotThread", true, "Messages total for each slot and each thread, default: 100"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("sd", "slotDis", true, "Time distance between two slots, default: 1000"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + private Message buildMessage(final int messageSize, final String topic) throws UnsupportedEncodingException { + Message msg = new Message(); + msg.setTopic(topic); + + String body = StringUtils.repeat('a', messageSize); + msg.setBody(body.getBytes(RemotingHelper.DEFAULT_CHARSET)); + + return msg; + } + + private void sleep(long timeMs) { + try { + Thread.sleep(timeMs); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public static void main(String[] args) throws MQClientException { + TimerProducer timerProducer = new TimerProducer(args); + timerProducer.startScheduleTask(); + timerProducer.start(); + } + + + public static class StatsBenchmarkProducer { + private final AtomicLong sendRequestSuccessCount = new AtomicLong(0L); + + private final AtomicLong sendRequestFailedCount = new AtomicLong(0L); + + private final AtomicLong receiveResponseSuccessCount = new AtomicLong(0L); + + private final AtomicLong receiveResponseFailedCount = new AtomicLong(0L); + + private final AtomicLong sendMessageSuccessTimeTotal = new AtomicLong(0L); + + private final AtomicLong sendMessageMaxRT = new AtomicLong(0L); + + public Long[] createSnapshot() { + return new Long[]{ + System.currentTimeMillis(), + this.sendRequestSuccessCount.get(), + this.sendRequestFailedCount.get(), + this.receiveResponseSuccessCount.get(), + this.receiveResponseFailedCount.get(), + this.sendMessageSuccessTimeTotal.get(), + }; + } + + public AtomicLong getSendRequestSuccessCount() { + return sendRequestSuccessCount; + } + + public AtomicLong getSendRequestFailedCount() { + return sendRequestFailedCount; + } + + public AtomicLong getReceiveResponseSuccessCount() { + return receiveResponseSuccessCount; + } + + public AtomicLong getReceiveResponseFailedCount() { + return receiveResponseFailedCount; + } + + public AtomicLong getSendMessageSuccessTimeTotal() { + return sendMessageSuccessTimeTotal; + } + + public AtomicLong getSendMessageMaxRT() { + return sendMessageMaxRT; + } + } + +} diff --git a/example/src/main/java/org/apache/rocketmq/example/broadcast/PushConsumer.java b/example/src/main/java/org/apache/rocketmq/example/broadcast/PushConsumer.java index fb1f9bbde71..e991dfeab2c 100644 --- a/example/src/main/java/org/apache/rocketmq/example/broadcast/PushConsumer.java +++ b/example/src/main/java/org/apache/rocketmq/example/broadcast/PushConsumer.java @@ -16,35 +16,37 @@ */ package org.apache.rocketmq.example.broadcast; -import java.util.List; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; -import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.consumer.ConsumeFromWhere; -import org.apache.rocketmq.common.message.MessageExt; -import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; public class PushConsumer { + public static final String CONSUMER_GROUP = "please_rename_unique_group_name_1"; + public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876"; + public static final String TOPIC = "TopicTest"; + + public static final String SUB_EXPRESSION = "TagA || TagC || TagD"; + public static void main(String[] args) throws InterruptedException, MQClientException { - DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_1"); + + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP); + + // Uncomment the following line while debugging, namesrvAddr should be set to your local address +// consumer.setNamesrvAddr(DEFAULT_NAMESRVADDR); consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); consumer.setMessageModel(MessageModel.BROADCASTING); - consumer.subscribe("TopicTest", "TagA || TagC || TagD"); - - consumer.registerMessageListener(new MessageListenerConcurrently() { + consumer.subscribe(TOPIC, SUB_EXPRESSION); - @Override - public ConsumeConcurrentlyStatus consumeMessage(List msgs, - ConsumeConcurrentlyContext context) { - System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); - return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; - } + consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; }); consumer.start(); diff --git a/example/src/main/java/org/apache/rocketmq/example/filter/Consumer.java b/example/src/main/java/org/apache/rocketmq/example/filter/Consumer.java deleted file mode 100644 index bb491ac40af..00000000000 --- a/example/src/main/java/org/apache/rocketmq/example/filter/Consumer.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.example.filter; - -import java.io.File; -import java.io.IOException; -import java.util.List; -import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; -import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; -import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; -import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; -import org.apache.rocketmq.client.exception.MQClientException; -import org.apache.rocketmq.common.MixAll; -import org.apache.rocketmq.common.message.MessageExt; - -public class Consumer { - - public static void main(String[] args) throws InterruptedException, MQClientException, IOException { - DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerGroupNamecc4"); - - ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); - File classFile = new File(classLoader.getResource("MessageFilterImpl.java").getFile()); - - String filterCode = MixAll.file2String(classFile); - consumer.subscribe("TopicTest", "org.apache.rocketmq.example.filter.MessageFilterImpl", - filterCode); - - consumer.registerMessageListener(new MessageListenerConcurrently() { - - @Override - public ConsumeConcurrentlyStatus consumeMessage(List msgs, - ConsumeConcurrentlyContext context) { - System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); - return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; - } - }); - - consumer.start(); - - System.out.printf("Consumer Started.%n"); - } -} diff --git a/example/src/main/java/org/apache/rocketmq/example/filter/Producer.java b/example/src/main/java/org/apache/rocketmq/example/filter/Producer.java deleted file mode 100644 index 2a0da6546c8..00000000000 --- a/example/src/main/java/org/apache/rocketmq/example/filter/Producer.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.example.filter; - -import org.apache.rocketmq.client.exception.MQClientException; -import org.apache.rocketmq.client.producer.DefaultMQProducer; -import org.apache.rocketmq.client.producer.SendResult; -import org.apache.rocketmq.common.message.Message; -import org.apache.rocketmq.remoting.common.RemotingHelper; - -public class Producer { - public static void main(String[] args) throws MQClientException, InterruptedException { - DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName"); - producer.start(); - - try { - for (int i = 0; i < 6000000; i++) { - Message msg = new Message("TopicFilter7", - "TagA", - "OrderID001", - "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); - - msg.putUserProperty("SequenceId", String.valueOf(i)); - SendResult sendResult = producer.send(msg); - System.out.printf("%s%n", sendResult); - } - } catch (Exception e) { - e.printStackTrace(); - } - producer.shutdown(); - } -} diff --git a/example/src/main/java/org/apache/rocketmq/example/filter/SqlConsumer.java b/example/src/main/java/org/apache/rocketmq/example/filter/SqlConsumer.java deleted file mode 100644 index c41c9c14c3c..00000000000 --- a/example/src/main/java/org/apache/rocketmq/example/filter/SqlConsumer.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.example.filter; - -import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; -import org.apache.rocketmq.client.consumer.MessageSelector; -import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; -import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; -import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; -import org.apache.rocketmq.client.exception.MQClientException; -import org.apache.rocketmq.common.message.MessageExt; - -import java.util.List; - -public class SqlConsumer { - - public static void main(String[] args) { - DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4"); - try { - consumer.subscribe("TopicTest", - MessageSelector.bySql("(TAGS is not null and TAGS in ('TagA', 'TagB'))" + - "and (a is not null and a between 0 3)")); - } catch (MQClientException e) { - e.printStackTrace(); - return; - } - - consumer.registerMessageListener(new MessageListenerConcurrently() { - - @Override - public ConsumeConcurrentlyStatus consumeMessage(List msgs, - ConsumeConcurrentlyContext context) { - System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); - return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; - } - }); - - try { - consumer.start(); - } catch (MQClientException e) { - e.printStackTrace(); - return; - } - System.out.printf("Consumer Started.%n"); - } -} diff --git a/example/src/main/java/org/apache/rocketmq/example/filter/SqlFilterConsumer.java b/example/src/main/java/org/apache/rocketmq/example/filter/SqlFilterConsumer.java new file mode 100644 index 00000000000..8dd6d20eb79 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/filter/SqlFilterConsumer.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.example.filter; + +import java.util.List; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.MessageSelector; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.common.message.MessageExt; + +public class SqlFilterConsumer { + + public static void main(String[] args) throws Exception { + + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name"); + + // Don't forget to set enablePropertyFilter=true in broker + consumer.subscribe("SqlFilterTest", + MessageSelector.bySql("(TAGS is not null and TAGS in ('TagA', 'TagB'))" + + "and (a is not null and a between 0 and 3)")); + + consumer.registerMessageListener(new MessageListenerConcurrently() { + + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, + ConsumeConcurrentlyContext context) { + System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + + consumer.start(); + System.out.printf("Consumer Started.%n"); + } +} diff --git a/example/src/main/java/org/apache/rocketmq/example/filter/SqlFilterProducer.java b/example/src/main/java/org/apache/rocketmq/example/filter/SqlFilterProducer.java new file mode 100644 index 00000000000..001827068a0 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/filter/SqlFilterProducer.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.example.filter; + +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.remoting.common.RemotingHelper; + +public class SqlFilterProducer { + + public static void main(String[] args) throws Exception { + + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + + producer.start(); + + String[] tags = new String[] {"TagA", "TagB", "TagC"}; + + for (int i = 0; i < 10; i++) { + Message msg = new Message("SqlFilterTest", + tags[i % tags.length], + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) + ); + msg.putUserProperty("a", String.valueOf(i)); + + SendResult sendResult = producer.send(msg); + System.out.printf("%s%n", sendResult); + } + + producer.shutdown(); + } +} diff --git a/example/src/main/java/org/apache/rocketmq/example/filter/SqlProducer.java b/example/src/main/java/org/apache/rocketmq/example/filter/SqlProducer.java deleted file mode 100644 index 3f3a0e65215..00000000000 --- a/example/src/main/java/org/apache/rocketmq/example/filter/SqlProducer.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.example.filter; - -import org.apache.rocketmq.client.exception.MQClientException; -import org.apache.rocketmq.client.producer.DefaultMQProducer; -import org.apache.rocketmq.client.producer.SendResult; -import org.apache.rocketmq.common.message.Message; -import org.apache.rocketmq.remoting.common.RemotingHelper; - -public class SqlProducer { - - public static void main(String[] args) { - DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); - try { - producer.start(); - } catch (MQClientException e) { - e.printStackTrace(); - return; - } - - for (int i = 0; i < 10; i++) { - try { - String tag; - int div = i % 3; - if (div == 0) { - tag = "TagA"; - } else if (div == 1) { - tag = "TagB"; - } else { - tag = "TagC"; - } - Message msg = new Message("TopicTest", - tag, - ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) - ); - msg.putUserProperty("a", String.valueOf(i)); - - SendResult sendResult = producer.send(msg); - System.out.printf("%s%n", sendResult); - } catch (Exception e) { - e.printStackTrace(); - try { - Thread.sleep(1000); - } catch (InterruptedException e1) { - e1.printStackTrace(); - } - } - } - producer.shutdown(); - } -} diff --git a/example/src/main/java/org/apache/rocketmq/example/filter/TagFilterConsumer.java b/example/src/main/java/org/apache/rocketmq/example/filter/TagFilterConsumer.java new file mode 100644 index 00000000000..74fc4a9ee43 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/filter/TagFilterConsumer.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.example.filter; + +import java.util.List; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.message.MessageExt; + +public class TagFilterConsumer { + + public static void main(String[] args) throws MQClientException { + + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name"); + + consumer.subscribe("TagFilterTest", "TagA || TagC"); + + consumer.registerMessageListener(new MessageListenerConcurrently() { + + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, + ConsumeConcurrentlyContext context) { + System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + + consumer.start(); + + System.out.printf("Consumer Started.%n"); + } +} diff --git a/example/src/main/java/org/apache/rocketmq/example/filter/TagFilterProducer.java b/example/src/main/java/org/apache/rocketmq/example/filter/TagFilterProducer.java new file mode 100644 index 00000000000..b0a9e2dd023 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/filter/TagFilterProducer.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.example.filter; + +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.remoting.common.RemotingHelper; + +public class TagFilterProducer { + + public static void main(String[] args) throws Exception { + + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + producer.start(); + + String[] tags = new String[] {"TagA", "TagB", "TagC"}; + + for (int i = 0; i < 60; i++) { + Message msg = new Message("TagFilterTest", + tags[i % tags.length], + "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); + + SendResult sendResult = producer.send(msg); + System.out.printf("%s%n", sendResult); + } + + producer.shutdown(); + } +} diff --git a/example/src/main/java/org/apache/rocketmq/example/namespace/ProducerWithNamespace.java b/example/src/main/java/org/apache/rocketmq/example/namespace/ProducerWithNamespace.java new file mode 100644 index 00000000000..2a7af58a6ee --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/namespace/ProducerWithNamespace.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.example.namespace; + +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.Message; + +import java.nio.charset.StandardCharsets; + +public class ProducerWithNamespace { + + public static final String NAMESPACE = "InstanceTest"; + public static final String PRODUCER_GROUP = "pidTest"; + public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876"; + public static final int MESSAGE_COUNT = 100; + public static final String TOPIC = "NAMESPACE_TOPIC"; + public static final String TAG = "tagTest"; + + public static void main(String[] args) throws Exception { + + DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP); + producer.setNamespaceV2(NAMESPACE); + + producer.setNamesrvAddr(DEFAULT_NAMESRVADDR); + producer.start(); + for (int i = 0; i < MESSAGE_COUNT; i++) { + Message message = new Message(TOPIC, TAG, "Hello world".getBytes(StandardCharsets.UTF_8)); + try { + SendResult result = producer.send(message); + System.out.printf("Topic:%s send success, misId is:%s%n", message.getTopic(), result.getMsgId()); + } catch (Exception e) { + e.printStackTrace(); + } + } + producer.shutdown(); + } +} \ No newline at end of file diff --git a/example/src/main/java/org/apache/rocketmq/example/namespace/PullConsumerWithNamespace.java b/example/src/main/java/org/apache/rocketmq/example/namespace/PullConsumerWithNamespace.java new file mode 100644 index 00000000000..b5509d31ecc --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/namespace/PullConsumerWithNamespace.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.example.namespace; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.common.message.MessageQueue; + +public class PullConsumerWithNamespace { + + public static final String NAMESPACE = "InstanceTest"; + public static final String CONSUMER_GROUP = "cidTest"; + public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876"; + public static final String TOPIC = "NAMESPACE_TOPIC"; + + private static final Map OFFSET_TABLE = new HashMap<>(); + + public static void main(String[] args) throws Exception { + DefaultMQPullConsumer pullConsumer = new DefaultMQPullConsumer(CONSUMER_GROUP); + pullConsumer.setNamespaceV2(NAMESPACE); + pullConsumer.setNamesrvAddr(DEFAULT_NAMESRVADDR); + pullConsumer.start(); + + Set mqs = pullConsumer.fetchSubscribeMessageQueues(TOPIC); + for (MessageQueue mq : mqs) { + System.out.printf("Consume from the topic: %s, queue: %s%n", mq.getTopic(), mq); + SINGLE_MQ: + while (true) { + try { + PullResult pullResult = + pullConsumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32); + System.out.printf("%s%n", pullResult); + + putMessageQueueOffset(mq, pullResult.getNextBeginOffset()); + switch (pullResult.getPullStatus()) { + case FOUND: + dealWithPullResult(pullResult); + break; + case NO_MATCHED_MSG: + break; + case NO_NEW_MSG: + break SINGLE_MQ; + case OFFSET_ILLEGAL: + break; + default: + break; + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + pullConsumer.shutdown(); + } + + private static long getMessageQueueOffset(MessageQueue mq) { + Long offset = OFFSET_TABLE.get(mq); + if (offset != null) { + return offset; + } + + return 0; + } + + private static void dealWithPullResult(PullResult pullResult) { + if (null == pullResult || pullResult.getMsgFoundList().isEmpty()) { + return; + } + pullResult.getMsgFoundList().forEach( + msg -> System.out.printf("Topic is:%s, msgId is:%s%n", msg.getTopic(), msg.getMsgId())); + } + + private static void putMessageQueueOffset(MessageQueue mq, long offset) { + OFFSET_TABLE.put(mq, offset); + } +} \ No newline at end of file diff --git a/example/src/main/java/org/apache/rocketmq/example/namespace/PushConsumerWithNamespace.java b/example/src/main/java/org/apache/rocketmq/example/namespace/PushConsumerWithNamespace.java new file mode 100644 index 00000000000..f12383a7a32 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/namespace/PushConsumerWithNamespace.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.example.namespace; + +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; + +public class PushConsumerWithNamespace { + public static final String NAMESPACE = "InstanceTest"; + public static final String CONSUMER_GROUP = "cidTest"; + public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876"; + public static final String TOPIC = "NAMESPACE_TOPIC"; + + public static void main(String[] args) throws Exception { + DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer(CONSUMER_GROUP); + defaultMQPushConsumer.setNamespaceV2(NAMESPACE); + defaultMQPushConsumer.setNamesrvAddr(DEFAULT_NAMESRVADDR); + defaultMQPushConsumer.subscribe(TOPIC, "*"); + defaultMQPushConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + msgs.forEach(msg -> System.out.printf("Msg topic is:%s, MsgId is:%s, reconsumeTimes is:%s%n", msg.getTopic(), msg.getMsgId(), msg.getReconsumeTimes())); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + + defaultMQPushConsumer.start(); + } +} \ No newline at end of file diff --git a/example/src/main/java/org/apache/rocketmq/example/openmessaging/SimpleProducer.java b/example/src/main/java/org/apache/rocketmq/example/openmessaging/SimpleProducer.java index 9d162ac1463..ec72aa0dd77 100644 --- a/example/src/main/java/org/apache/rocketmq/example/openmessaging/SimpleProducer.java +++ b/example/src/main/java/org/apache/rocketmq/example/openmessaging/SimpleProducer.java @@ -16,19 +16,24 @@ */ package org.apache.rocketmq.example.openmessaging; +import io.openmessaging.Future; import io.openmessaging.Message; import io.openmessaging.MessagingAccessPoint; -import io.openmessaging.MessagingAccessPointFactory; -import io.openmessaging.Producer; -import io.openmessaging.Promise; -import io.openmessaging.PromiseListener; -import io.openmessaging.SendResult; -import java.nio.charset.Charset; +import io.openmessaging.OMS; +import io.openmessaging.producer.Producer; +import io.openmessaging.producer.SendResult; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CountDownLatch; public class SimpleProducer { + + public static final String URL = "oms:rocketmq://localhost:9876/default:default"; + public static final String QUEUE = "OMS_HELLO_TOPIC"; + public static void main(String[] args) { - final MessagingAccessPoint messagingAccessPoint = MessagingAccessPointFactory - .getMessagingAccessPoint("openmessaging:rocketmq://IP1:9876,IP2:9876/namespace"); + // You need to set the environment variable OMS_RMQ_DIRECT_NAME_SRV=true + final MessagingAccessPoint messagingAccessPoint = + OMS.getMessagingAccessPoint(URL); final Producer producer = messagingAccessPoint.createProducer(); @@ -38,39 +43,40 @@ public static void main(String[] args) { producer.startup(); System.out.printf("Producer startup OK%n"); - Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { - @Override - public void run() { - producer.shutdown(); - messagingAccessPoint.shutdown(); - } - })); - { - Message message = producer.createBytesMessageToTopic("OMS_HELLO_TOPIC", "OMS_HELLO_BODY".getBytes(Charset.forName("UTF-8"))); + Message message = producer.createBytesMessage(QUEUE, "OMS_HELLO_BODY".getBytes(StandardCharsets.UTF_8)); + SendResult sendResult = producer.send(message); //final Void aVoid = result.get(3000L); System.out.printf("Send async message OK, msgId: %s%n", sendResult.messageId()); } + final CountDownLatch countDownLatch = new CountDownLatch(1); { - final Promise result = producer.sendAsync(producer.createBytesMessageToTopic("OMS_HELLO_TOPIC", "OMS_HELLO_BODY".getBytes(Charset.forName("UTF-8")))); - result.addListener(new PromiseListener() { - @Override - public void operationCompleted(Promise promise) { - System.out.printf("Send async message OK, msgId: %s%n", promise.get().messageId()); - } - - @Override - public void operationFailed(Promise promise) { - System.out.printf("Send async message Failed, error: %s%n", promise.getThrowable().getMessage()); + final Future result = producer.sendAsync(producer.createBytesMessage(QUEUE, + "OMS_HELLO_BODY".getBytes(StandardCharsets.UTF_8))); + result.addListener(future -> { + if (future.getThrowable() != null) { + System.out.printf("Send async message Failed, error: %s%n", future.getThrowable().getMessage()); + } else { + System.out.printf("Send async message OK, msgId: %s%n", future.get().messageId()); } + countDownLatch.countDown(); }); } { - producer.sendOneway(producer.createBytesMessageToTopic("OMS_HELLO_TOPIC", "OMS_HELLO_BODY".getBytes(Charset.forName("UTF-8")))); + producer.sendOneway(producer.createBytesMessage("OMS_HELLO_TOPIC", "OMS_HELLO_BODY".getBytes(StandardCharsets.UTF_8))); System.out.printf("Send oneway message OK%n"); } + + try { + countDownLatch.await(); + // Wait some time for one-way delivery. + Thread.sleep(500); + } catch (InterruptedException ignore) { + } + + producer.shutdown(); } } diff --git a/example/src/main/java/org/apache/rocketmq/example/openmessaging/SimplePullConsumer.java b/example/src/main/java/org/apache/rocketmq/example/openmessaging/SimplePullConsumer.java index 8e067724dee..9ad69b31b33 100644 --- a/example/src/main/java/org/apache/rocketmq/example/openmessaging/SimplePullConsumer.java +++ b/example/src/main/java/org/apache/rocketmq/example/openmessaging/SimplePullConsumer.java @@ -17,42 +17,67 @@ package org.apache.rocketmq.example.openmessaging; import io.openmessaging.Message; -import io.openmessaging.MessageHeader; import io.openmessaging.MessagingAccessPoint; -import io.openmessaging.MessagingAccessPointFactory; import io.openmessaging.OMS; -import io.openmessaging.PullConsumer; -import io.openmessaging.rocketmq.domain.NonStandardKeys; +import io.openmessaging.OMSBuiltinKeys; +import io.openmessaging.consumer.PullConsumer; +import io.openmessaging.producer.Producer; +import io.openmessaging.producer.SendResult; +import java.nio.charset.StandardCharsets; public class SimplePullConsumer { + + public static final String URL = "oms:rocketmq://localhost:9876/default:default"; + public static final String QUEUE = "OMS_CONSUMER"; + public static void main(String[] args) { - final MessagingAccessPoint messagingAccessPoint = MessagingAccessPointFactory - .getMessagingAccessPoint("openmessaging:rocketmq://IP1:9876,IP2:9876/namespace"); + // You need to set the environment variable OMS_RMQ_DIRECT_NAME_SRV=true + + final MessagingAccessPoint messagingAccessPoint = + OMS.getMessagingAccessPoint(URL); + + messagingAccessPoint.startup(); + + final Producer producer = messagingAccessPoint.createProducer(); - final PullConsumer consumer = messagingAccessPoint.createPullConsumer("OMS_HELLO_TOPIC", - OMS.newKeyValue().put(NonStandardKeys.CONSUMER_GROUP, "OMS_CONSUMER")); + final PullConsumer consumer = messagingAccessPoint.createPullConsumer( + OMS.newKeyValue().put(OMSBuiltinKeys.CONSUMER_ID, QUEUE)); messagingAccessPoint.startup(); System.out.printf("MessagingAccessPoint startup OK%n"); - Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { - @Override - public void run() { - consumer.shutdown(); - messagingAccessPoint.shutdown(); - } - })); + final String queueName = "TopicTest"; + + producer.startup(); + Message msg = producer.createBytesMessage(queueName, "Hello Open Messaging".getBytes(StandardCharsets.UTF_8)); + SendResult sendResult = producer.send(msg); + System.out.printf("Send Message OK. MsgId: %s%n", sendResult.messageId()); + producer.shutdown(); + + consumer.attachQueue(queueName); consumer.startup(); System.out.printf("Consumer startup OK%n"); - while (true) { - Message message = consumer.poll(); + // Keep running until we find the one that has just been sent + boolean stop = false; + while (!stop) { + Message message = consumer.receive(); if (message != null) { - String msgId = message.headers().getString(MessageHeader.MESSAGE_ID); + String msgId = message.sysHeaders().getString(Message.BuiltinKeys.MESSAGE_ID); System.out.printf("Received one message: %s%n", msgId); consumer.ack(msgId); + + if (!stop) { + stop = msgId.equalsIgnoreCase(sendResult.messageId()); + } + + } else { + System.out.printf("Return without any message%n"); } } + + consumer.shutdown(); + messagingAccessPoint.shutdown(); } } diff --git a/example/src/main/java/org/apache/rocketmq/example/openmessaging/SimplePushConsumer.java b/example/src/main/java/org/apache/rocketmq/example/openmessaging/SimplePushConsumer.java index b0935d4c743..92ccff1ba4d 100644 --- a/example/src/main/java/org/apache/rocketmq/example/openmessaging/SimplePushConsumer.java +++ b/example/src/main/java/org/apache/rocketmq/example/openmessaging/SimplePushConsumer.java @@ -17,40 +17,36 @@ package org.apache.rocketmq.example.openmessaging; import io.openmessaging.Message; -import io.openmessaging.MessageHeader; -import io.openmessaging.MessageListener; import io.openmessaging.MessagingAccessPoint; -import io.openmessaging.MessagingAccessPointFactory; import io.openmessaging.OMS; -import io.openmessaging.PushConsumer; -import io.openmessaging.ReceivedMessageContext; -import io.openmessaging.rocketmq.domain.NonStandardKeys; +import io.openmessaging.OMSBuiltinKeys; +import io.openmessaging.consumer.PushConsumer; public class SimplePushConsumer { + + public static final String URL = "oms:rocketmq://localhost:9876/default:default"; + public static final String QUEUE = "OMS_HELLO_TOPIC"; + public static void main(String[] args) { - final MessagingAccessPoint messagingAccessPoint = MessagingAccessPointFactory - .getMessagingAccessPoint("openmessaging:rocketmq://IP1:9876,IP2:9876/namespace"); + // You need to set the environment variable OMS_RMQ_DIRECT_NAME_SRV=true + + final MessagingAccessPoint messagingAccessPoint = OMS + .getMessagingAccessPoint(URL); final PushConsumer consumer = messagingAccessPoint. - createPushConsumer(OMS.newKeyValue().put(NonStandardKeys.CONSUMER_GROUP, "OMS_CONSUMER")); + createPushConsumer(OMS.newKeyValue().put(OMSBuiltinKeys.CONSUMER_ID, "OMS_CONSUMER")); messagingAccessPoint.startup(); System.out.printf("MessagingAccessPoint startup OK%n"); - Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { - @Override - public void run() { - consumer.shutdown(); - messagingAccessPoint.shutdown(); - } + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + consumer.shutdown(); + messagingAccessPoint.shutdown(); })); - consumer.attachQueue("OMS_HELLO_TOPIC", new MessageListener() { - @Override - public void onMessage(final Message message, final ReceivedMessageContext context) { - System.out.printf("Received one message: %s%n", message.headers().getString(MessageHeader.MESSAGE_ID)); - context.ack(); - } + consumer.attachQueue(QUEUE, (message, context) -> { + System.out.printf("Received one message: %s%n", message.sysHeaders().getString(Message.BuiltinKeys.MESSAGE_ID)); + context.ack(); }); consumer.startup(); diff --git a/example/src/main/java/org/apache/rocketmq/example/operation/Consumer.java b/example/src/main/java/org/apache/rocketmq/example/operation/Consumer.java index 6936f1dcea3..90f2e133a1c 100644 --- a/example/src/main/java/org/apache/rocketmq/example/operation/Consumer.java +++ b/example/src/main/java/org/apache/rocketmq/example/operation/Consumer.java @@ -19,11 +19,11 @@ import java.util.List; import java.util.concurrent.atomic.AtomicLong; import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; -import org.apache.commons.cli.PosixParser; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; @@ -33,7 +33,7 @@ public class Consumer { - public static void main(String[] args) throws InterruptedException, MQClientException { + public static void main(String[] args) throws MQClientException { CommandLine commandLine = buildCommandline(args); if (commandLine != null) { String group = commandLine.getOptionValue('g'); @@ -91,7 +91,7 @@ public static CommandLine buildCommandline(String[] args) { opt.setRequired(true); options.addOption(opt); - PosixParser parser = new PosixParser(); + DefaultParser parser = new DefaultParser(); HelpFormatter hf = new HelpFormatter(); hf.setWidth(110); CommandLine commandLine = null; diff --git a/example/src/main/java/org/apache/rocketmq/example/operation/Producer.java b/example/src/main/java/org/apache/rocketmq/example/operation/Producer.java index 1d4336d7fa9..0cf260ddb74 100644 --- a/example/src/main/java/org/apache/rocketmq/example/operation/Producer.java +++ b/example/src/main/java/org/apache/rocketmq/example/operation/Producer.java @@ -17,11 +17,11 @@ package org.apache.rocketmq.example.operation; import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; -import org.apache.commons.cli.PosixParser; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.SendResult; @@ -89,7 +89,7 @@ public static CommandLine buildCommandline(String[] args) { opt.setRequired(true); options.addOption(opt); - PosixParser parser = new PosixParser(); + DefaultParser parser = new DefaultParser(); HelpFormatter hf = new HelpFormatter(); hf.setWidth(110); CommandLine commandLine = null; diff --git a/example/src/main/java/org/apache/rocketmq/example/ordermessage/Consumer.java b/example/src/main/java/org/apache/rocketmq/example/ordermessage/Consumer.java index abb274d6129..257c6a045f2 100644 --- a/example/src/main/java/org/apache/rocketmq/example/ordermessage/Consumer.java +++ b/example/src/main/java/org/apache/rocketmq/example/ordermessage/Consumer.java @@ -40,15 +40,11 @@ public static void main(String[] args) throws MQClientException { @Override public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) { - context.setAutoCommit(false); + context.setAutoCommit(true); System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); this.consumeTimes.incrementAndGet(); if ((this.consumeTimes.get() % 2) == 0) { return ConsumeOrderlyStatus.SUCCESS; - } else if ((this.consumeTimes.get() % 3) == 0) { - return ConsumeOrderlyStatus.ROLLBACK; - } else if ((this.consumeTimes.get() % 4) == 0) { - return ConsumeOrderlyStatus.COMMIT; } else if ((this.consumeTimes.get() % 5) == 0) { context.setSuspendCurrentQueueTimeMillis(3000); return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT; diff --git a/example/src/main/java/org/apache/rocketmq/example/ordermessage/Producer.java b/example/src/main/java/org/apache/rocketmq/example/ordermessage/Producer.java index 6a6bdc7d710..8ee11bf2b47 100644 --- a/example/src/main/java/org/apache/rocketmq/example/ordermessage/Producer.java +++ b/example/src/main/java/org/apache/rocketmq/example/ordermessage/Producer.java @@ -16,23 +16,20 @@ */ package org.apache.rocketmq.example.ordermessage; -import java.io.UnsupportedEncodingException; -import java.util.List; -import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.DefaultMQProducer; -import org.apache.rocketmq.client.producer.MQProducer; import org.apache.rocketmq.client.producer.MessageQueueSelector; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageQueue; import org.apache.rocketmq.remoting.common.RemotingHelper; -import org.apache.rocketmq.remoting.exception.RemotingException; + +import java.util.List; public class Producer { - public static void main(String[] args) throws UnsupportedEncodingException { + public static void main(String[] args) throws MQClientException { try { - MQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); producer.start(); String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"}; @@ -54,8 +51,9 @@ public MessageQueue select(List mqs, Message msg, Object arg) { } producer.shutdown(); - } catch (MQClientException | RemotingException | MQBrokerException | InterruptedException e) { + } catch (Exception e) { e.printStackTrace(); + throw new MQClientException(e.getMessage(), null); } } } diff --git a/example/src/main/java/org/apache/rocketmq/example/quickstart/Consumer.java b/example/src/main/java/org/apache/rocketmq/example/quickstart/Consumer.java index 6d3b936507e..3a101bf664f 100644 --- a/example/src/main/java/org/apache/rocketmq/example/quickstart/Consumer.java +++ b/example/src/main/java/org/apache/rocketmq/example/quickstart/Consumer.java @@ -16,26 +16,27 @@ */ package org.apache.rocketmq.example.quickstart; -import java.util.List; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; -import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.consumer.ConsumeFromWhere; -import org.apache.rocketmq.common.message.MessageExt; /** * This example shows how to subscribe and consume messages using providing {@link DefaultMQPushConsumer}. */ public class Consumer { - public static void main(String[] args) throws InterruptedException, MQClientException { + public static final String CONSUMER_GROUP = "please_rename_unique_group_name_4"; + public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876"; + public static final String TOPIC = "TopicTest"; + + public static void main(String[] args) throws MQClientException { /* * Instantiate with specified consumer group name. */ - DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4"); + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP); /* * Specify name server addresses. @@ -48,28 +49,25 @@ public static void main(String[] args) throws InterruptedException, MQClientExce * } * */ + // Uncomment the following line while debugging, namesrvAddr should be set to your local address + // consumer.setNamesrvAddr(DEFAULT_NAMESRVADDR); /* - * Specify where to start in case the specified consumer group is a brand new one. + * Specify where to start in case the specific consumer group is a brand-new one. */ consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); /* - * Subscribe one more more topics to consume. + * Subscribe one more topic to consume. */ - consumer.subscribe("TopicTest", "*"); + consumer.subscribe(TOPIC, "*"); /* * Register callback to execute on arrival of messages fetched from brokers. */ - consumer.registerMessageListener(new MessageListenerConcurrently() { - - @Override - public ConsumeConcurrentlyStatus consumeMessage(List msgs, - ConsumeConcurrentlyContext context) { - System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); - return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; - } + consumer.registerMessageListener((MessageListenerConcurrently) (msg, context) -> { + System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msg); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; }); /* diff --git a/example/src/main/java/org/apache/rocketmq/example/quickstart/Producer.java b/example/src/main/java/org/apache/rocketmq/example/quickstart/Producer.java index 53a1d4dd64a..cae16cec52d 100644 --- a/example/src/main/java/org/apache/rocketmq/example/quickstart/Producer.java +++ b/example/src/main/java/org/apache/rocketmq/example/quickstart/Producer.java @@ -26,45 +26,89 @@ * This class demonstrates how to send messages to brokers using provided {@link DefaultMQProducer}. */ public class Producer { + + /** + * The number of produced messages. + */ + public static final int MESSAGE_COUNT = 1000; + public static final String PRODUCER_GROUP = "please_rename_unique_group_name"; + public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876"; + public static final String TOPIC = "TopicTest"; + public static final String TAG = "TagA"; + public static void main(String[] args) throws MQClientException, InterruptedException { /* * Instantiate with a producer group name. */ - DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP); /* * Specify name server addresses. - *

    * * Alternatively, you may specify name server addresses via exporting environmental variable: NAMESRV_ADDR *

              * {@code
    -         * producer.setNamesrvAddr("name-server1-ip:9876;name-server2-ip:9876");
    +         *  producer.setNamesrvAddr("name-server1-ip:9876;name-server2-ip:9876");
              * }
              * 
    */ + // Uncomment the following line while debugging, namesrvAddr should be set to your local address + // producer.setNamesrvAddr(DEFAULT_NAMESRVADDR); /* * Launch the instance. */ producer.start(); - for (int i = 0; i < 1000; i++) { + for (int i = 0; i < MESSAGE_COUNT; i++) { try { /* * Create a message instance, specifying topic, tag and message body. */ - Message msg = new Message("TopicTest" /* Topic */, - "TagA" /* Tag */, + Message msg = new Message(TOPIC /* Topic */, + TAG /* Tag */, ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */ ); /* * Call send message to deliver message to one of brokers. */ - SendResult sendResult = producer.send(msg); + SendResult sendResult = producer.send(msg, 20 * 1000); + /* + * There are different ways to send message, if you don't care about the send result,you can use this way + * {@code + * producer.sendOneway(msg); + * } + */ + + /* + * if you want to get the send result in a synchronize way, you can use this send method + * {@code + * SendResult sendResult = producer.send(msg); + * System.out.printf("%s%n", sendResult); + * } + */ + + /* + * if you want to get the send result in a asynchronize way, you can use this send method + * {@code + * + * producer.send(msg, new SendCallback() { + * @Override + * public void onSuccess(SendResult sendResult) { + * // do something + * } + * + * @Override + * public void onException(Throwable e) { + * // do something + * } + *}); + * + *} + */ System.out.printf("%s%n", sendResult); } catch (Exception e) { @@ -74,7 +118,7 @@ public static void main(String[] args) throws MQClientException, InterruptedExce } /* - * Shut down once the producer instance is not longer in use. + * Shut down once the producer instance is no longer in use. */ producer.shutdown(); } diff --git a/example/src/main/java/org/apache/rocketmq/example/rpc/AsyncRequestProducer.java b/example/src/main/java/org/apache/rocketmq/example/rpc/AsyncRequestProducer.java new file mode 100644 index 00000000000..31df559b15d --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/rpc/AsyncRequestProducer.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.example.rpc; + +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.RequestCallback; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class AsyncRequestProducer { + private static final Logger log = LoggerFactory.getLogger(AsyncRequestProducer.class); + + public static void main(String[] args) throws MQClientException, InterruptedException { + String producerGroup = "please_rename_unique_group_name"; + String topic = "RequestTopic"; + long ttl = 3000; + + DefaultMQProducer producer = new DefaultMQProducer(producerGroup); + producer.start(); + + try { + Message msg = new Message(topic, + "", + "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); + + long begin = System.currentTimeMillis(); + producer.request(msg, new RequestCallback() { + @Override + public void onSuccess(Message message) { + long cost = System.currentTimeMillis() - begin; + System.out.printf("request to <%s> cost: %d replyMessage: %s %n", topic, cost, message); + } + + @Override + public void onException(Throwable e) { + System.err.printf("request to <%s> fail.", topic); + } + }, ttl); + } catch (Exception e) { + log.warn("", e); + } + /* shutdown after your request callback is finished */ +// producer.shutdown(); + } +} diff --git a/example/src/main/java/org/apache/rocketmq/example/rpc/RequestProducer.java b/example/src/main/java/org/apache/rocketmq/example/rpc/RequestProducer.java new file mode 100644 index 00000000000..69048de2d0e --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/rpc/RequestProducer.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.example.rpc; + +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.remoting.common.RemotingHelper; + +public class RequestProducer { + public static void main(String[] args) throws MQClientException, InterruptedException { + String producerGroup = "please_rename_unique_group_name"; + String topic = "RequestTopic"; + long ttl = 3000; + + DefaultMQProducer producer = new DefaultMQProducer(producerGroup); + + //You need to set namesrvAddr to the address of the local namesrv + producer.setNamesrvAddr("127.0.0.1:9876"); + + producer.start(); + + try { + Message msg = new Message(topic, + "", + "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); + + long begin = System.currentTimeMillis(); + Message retMsg = producer.request(msg, ttl); + long cost = System.currentTimeMillis() - begin; + System.out.printf("request to <%s> cost: %d replyMessage: %s %n", topic, cost, retMsg); + } catch (Exception e) { + e.printStackTrace(); + } + producer.shutdown(); + } +} diff --git a/example/src/main/java/org/apache/rocketmq/example/rpc/ResponseConsumer.java b/example/src/main/java/org/apache/rocketmq/example/rpc/ResponseConsumer.java new file mode 100644 index 00000000000..a1c18ae698d --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/rpc/ResponseConsumer.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.example.rpc; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.utils.MessageUtil; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.remoting.exception.RemotingException; + +public class ResponseConsumer { + public static void main(String[] args) throws InterruptedException, MQClientException { + String producerGroup = "please_rename_unique_group_name"; + String consumerGroup = "please_rename_unique_group_name"; + String topic = "RequestTopic"; + + // create a producer to send reply message + DefaultMQProducer replyProducer = new DefaultMQProducer(producerGroup); + replyProducer.setNamesrvAddr("127.0.0.1:9876"); + replyProducer.start(); + + // create consumer + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(consumerGroup); + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); + consumer.setNamesrvAddr("127.0.0.1:9876"); + + // recommend client configs + consumer.setPullTimeDelayMillsWhenException(0L); + + consumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); + for (MessageExt msg : msgs) { + try { + System.out.printf("handle message: %s %n", msg.toString()); + String replyTo = MessageUtil.getReplyToClient(msg); + byte[] replyContent = "reply message contents.".getBytes(StandardCharsets.UTF_8); + // create reply message with given util, do not create reply message by yourself + Message replyMessage = MessageUtil.createReplyMessage(msg, replyContent); + + // send reply message with producer + SendResult replyResult = replyProducer.send(replyMessage, 3000); + System.out.printf("reply to %s , %s %n", replyTo, replyResult.toString()); + } catch (MQClientException | RemotingException | MQBrokerException | InterruptedException e) { + e.printStackTrace(); + } + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + + consumer.subscribe(topic, "*"); + consumer.start(); + System.out.printf("Consumer Started.%n"); + } +} diff --git a/example/src/main/java/org/apache/rocketmq/example/schedule/ScheduledMessageConsumer.java b/example/src/main/java/org/apache/rocketmq/example/schedule/ScheduledMessageConsumer.java new file mode 100644 index 00000000000..b3fab65f0ab --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/schedule/ScheduledMessageConsumer.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.example.schedule; + +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.common.message.MessageExt; + +public class ScheduledMessageConsumer { + + public static final String CONSUMER_GROUP = "ExampleConsumer"; + public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876"; + public static final String TOPIC = "TestTopic"; + + public static void main(String[] args) throws Exception { + // Instantiate message consumer + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP); + + // Uncomment the following line while debugging, namesrvAddr should be set to your local address +// consumer.setNamesrvAddr(DEFAULT_NAMESRVADDR); + + // Subscribe topics + consumer.subscribe(TOPIC, "*"); + // Register message listener + consumer.registerMessageListener((MessageListenerConcurrently) (messages, context) -> { + for (MessageExt message : messages) { + // Print approximate delay time period + System.out.printf("Receive message[msgId=%s %d ms later]\n", message.getMsgId(), + System.currentTimeMillis() - message.getStoreTimestamp()); + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + // Launch consumer + consumer.start(); + //info:to see the time effect, run the consumer first , it will wait for the msg + //then start the producer + } +} \ No newline at end of file diff --git a/example/src/main/java/org/apache/rocketmq/example/schedule/ScheduledMessageProducer.java b/example/src/main/java/org/apache/rocketmq/example/schedule/ScheduledMessageProducer.java new file mode 100644 index 00000000000..aeae492dd9e --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/schedule/ScheduledMessageProducer.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.example.schedule; + +import java.nio.charset.StandardCharsets; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.Message; + +public class ScheduledMessageProducer { + + public static final String PRODUCER_GROUP = "ExampleProducerGroup"; + public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876"; + public static final String TOPIC = "TestTopic"; + + public static void main(String[] args) throws Exception { + // Instantiate a producer to send scheduled messages + DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP); + + // Uncomment the following line while debugging, namesrvAddr should be set to your local address +// producer.setNamesrvAddr(DEFAULT_NAMESRVADDR); + + // Launch producer + producer.start(); + int totalMessagesToSend = 100; + for (int i = 0; i < totalMessagesToSend; i++) { + Message message = new Message(TOPIC, ("Hello scheduled message " + i).getBytes(StandardCharsets.UTF_8)); + // This message will be delivered to consumer 10 seconds later. + message.setDelayTimeLevel(3); + // Send the message + SendResult result = producer.send(message); + System.out.print(result); + } + + // Shutdown producer after use. + producer.shutdown(); + } + +} \ No newline at end of file diff --git a/example/src/main/java/org/apache/rocketmq/example/schedule/TimerMessageConsumer.java b/example/src/main/java/org/apache/rocketmq/example/schedule/TimerMessageConsumer.java new file mode 100644 index 00000000000..788983592f9 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/schedule/TimerMessageConsumer.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.example.schedule; + +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.common.message.MessageExt; + +public class TimerMessageConsumer { + + //Note: TimerMessage is a new feature in version 5.0, so be sure to upgrade RocketMQ to version 5.0+ before using it. + + public static final String CONSUMER_GROUP = "TimerMessageConsumerGroup"; + public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876"; + public static final String TOPIC = "TimerTopic"; + + public static void main(String[] args) throws Exception { + // Instantiate message consumer + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP); + + // Uncomment the following line while debugging, namesrvAddr should be set to your local address +// consumer.setNamesrvAddr(DEFAULT_NAMESRVADDR); + + // Subscribe topics + consumer.subscribe(TOPIC, "*"); + // Register message listener + consumer.registerMessageListener((MessageListenerConcurrently) (messages, context) -> { + for (MessageExt message : messages) { + // Print approximate delay time period + System.out.printf("Receive message[msgId=%s %d ms later]\n", message.getMsgId(), + System.currentTimeMillis() - message.getBornTimestamp()); + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + // Launch consumer + consumer.start(); + //info:to see the time effect, run the consumer first , it will wait for the msg + //then start the producer + } +} diff --git a/example/src/main/java/org/apache/rocketmq/example/schedule/TimerMessageProducer.java b/example/src/main/java/org/apache/rocketmq/example/schedule/TimerMessageProducer.java new file mode 100644 index 00000000000..c4e3b4f3a19 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/schedule/TimerMessageProducer.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.example.schedule; + +import java.nio.charset.StandardCharsets; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.Message; + +public class TimerMessageProducer { + + //Note: TimerMessage is a new feature in version 5.0, so be sure to upgrade RocketMQ to version 5.0+ before using it. + + public static final String PRODUCER_GROUP = "TimerMessageProducerGroup"; + public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876"; + public static final String TOPIC = "TimerTopic"; + + public static void main(String[] args) throws Exception { + // Instantiate a producer to send scheduled messages + DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP); + + // Uncomment the following line while debugging, namesrvAddr should be set to your local address +// producer.setNamesrvAddr(DEFAULT_NAMESRVADDR); + + // Launch producer + producer.start(); + int totalMessagesToSend = 10; + for (int i = 0; i < totalMessagesToSend; i++) { + Message message = new Message(TOPIC, ("Hello scheduled message " + i).getBytes(StandardCharsets.UTF_8)); + // This message will be delivered to consumer 10 seconds later. + //message.setDelayTimeSec(10); + // The effect is the same as the above + // message.setDelayTimeMs(10_000L); + // Set the specific delivery time, and the effect is the same as the above + message.setDeliverTimeMs(System.currentTimeMillis() + 10_000L); + // Send the message + SendResult result = producer.send(message); + System.out.printf(result + "\n"); + } + + // Shutdown producer after use. + producer.shutdown(); + } +} diff --git a/example/src/main/java/org/apache/rocketmq/example/simple/AclClient.java b/example/src/main/java/org/apache/rocketmq/example/simple/AclClient.java new file mode 100644 index 00000000000..15134a52109 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/simple/AclClient.java @@ -0,0 +1,169 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.example.simple; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.rocketmq.acl.common.AclClientRPCHook; +import org.apache.rocketmq.acl.common.SessionCredentials; +import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.consumer.rebalance.AllocateMessageQueueAveragely; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.common.RemotingHelper; + + +public class AclClient { + + private static final Map OFFSE_TABLE = new HashMap<>(); + + private static final String ACL_ACCESS_KEY = "RocketMQ"; + + private static final String ACL_SECRET_KEY = "1234567"; + + public static void main(String[] args) throws MQClientException, InterruptedException { + producer(); + pushConsumer(); + pullConsumer(); + } + + public static void producer() throws MQClientException { + DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName", getAclRPCHook()); + producer.setNamesrvAddr("127.0.0.1:9876"); + producer.start(); + + for (int i = 0; i < 128; i++) + try { + { + Message msg = new Message("TopicTest", + "TagA", + "OrderID188", + "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); + SendResult sendResult = producer.send(msg); + System.out.printf("%s%n", sendResult); + } + + } catch (Exception e) { + e.printStackTrace(); + } + + producer.shutdown(); + } + + public static void pushConsumer() throws MQClientException { + + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_5", getAclRPCHook(), new AllocateMessageQueueAveragely()); + consumer.setNamesrvAddr("127.0.0.1:9876"); + consumer.subscribe("TopicTest", "*"); + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + // Wrong time format 2017_0422_221800 + consumer.setConsumeTimestamp("20180422221800"); + consumer.registerMessageListener(new MessageListenerConcurrently() { + + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); + printBody(msgs); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + consumer.start(); + System.out.printf("Consumer Started.%n"); + } + + public static void pullConsumer() throws MQClientException { + DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("please_rename_unique_group_name_6", getAclRPCHook()); + consumer.setNamesrvAddr("127.0.0.1:9876"); + consumer.start(); + + Set mqs = consumer.fetchSubscribeMessageQueues("TopicTest"); + for (MessageQueue mq : mqs) { + System.out.printf("Consume from the queue: %s%n", mq); + SINGLE_MQ: + while (true) { + try { + PullResult pullResult = + consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32); + System.out.printf("%s%n", pullResult); + putMessageQueueOffset(mq, pullResult.getNextBeginOffset()); + printBody(pullResult); + switch (pullResult.getPullStatus()) { + case FOUND: + break; + case NO_MATCHED_MSG: + break; + case NO_NEW_MSG: + break SINGLE_MQ; + case OFFSET_ILLEGAL: + break; + default: + break; + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + consumer.shutdown(); + } + + private static void printBody(PullResult pullResult) { + printBody(pullResult.getMsgFoundList()); + } + + private static void printBody(List msg) { + if (msg == null || msg.size() == 0) + return; + for (MessageExt m : msg) { + if (m != null) { + System.out.printf("msgId : %s body : %s \n\r", m.getMsgId(), new String(m.getBody(), StandardCharsets.UTF_8)); + } + } + } + + private static long getMessageQueueOffset(MessageQueue mq) { + Long offset = OFFSE_TABLE.get(mq); + if (offset != null) + return offset; + + return 0; + } + + private static void putMessageQueueOffset(MessageQueue mq, long offset) { + OFFSE_TABLE.put(mq, offset); + } + + static RPCHook getAclRPCHook() { + return new AclClientRPCHook(new SessionCredentials(ACL_ACCESS_KEY,ACL_SECRET_KEY)); + } +} diff --git a/example/src/main/java/org/apache/rocketmq/example/simple/AsyncProducer.java b/example/src/main/java/org/apache/rocketmq/example/simple/AsyncProducer.java index aa15cafff2d..42d19b1b6fd 100644 --- a/example/src/main/java/org/apache/rocketmq/example/simple/AsyncProducer.java +++ b/example/src/main/java/org/apache/rocketmq/example/simple/AsyncProducer.java @@ -17,6 +17,8 @@ package org.apache.rocketmq.example.simple; import java.io.UnsupportedEncodingException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.SendCallback; @@ -30,9 +32,13 @@ public static void main( DefaultMQProducer producer = new DefaultMQProducer("Jodie_Daily_test"); producer.start(); + // suggest to on enableBackpressureForAsyncMode in heavy traffic, default is false + producer.setEnableBackpressureForAsyncMode(true); producer.setRetryTimesWhenSendAsyncFailed(0); - for (int i = 0; i < 10000000; i++) { + int messageCount = 100; + final CountDownLatch countDownLatch = new CountDownLatch(messageCount); + for (int i = 0; i < messageCount; i++) { try { final int index = i; Message msg = new Message("Jodie_topic_1023", @@ -42,11 +48,13 @@ public static void main( producer.send(msg, new SendCallback() { @Override public void onSuccess(SendResult sendResult) { + countDownLatch.countDown(); System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId()); } @Override public void onException(Throwable e) { + countDownLatch.countDown(); System.out.printf("%-10d Exception %s %n", index, e); e.printStackTrace(); } @@ -55,6 +63,7 @@ public void onException(Throwable e) { e.printStackTrace(); } } + countDownLatch.await(5, TimeUnit.SECONDS); producer.shutdown(); } } diff --git a/example/src/main/java/org/apache/rocketmq/example/simple/CachedQueue.java b/example/src/main/java/org/apache/rocketmq/example/simple/CachedQueue.java index 9e6ab92c7d5..41e28c59152 100644 --- a/example/src/main/java/org/apache/rocketmq/example/simple/CachedQueue.java +++ b/example/src/main/java/org/apache/rocketmq/example/simple/CachedQueue.java @@ -21,7 +21,7 @@ import org.apache.rocketmq.common.message.MessageExt; public class CachedQueue { - private final TreeMap msgCachedTable = new TreeMap(); + private final TreeMap msgCachedTable = new TreeMap<>(); public TreeMap getMsgCachedTable() { return msgCachedTable; diff --git a/example/src/main/java/org/apache/rocketmq/example/simple/LitePullConsumerAssign.java b/example/src/main/java/org/apache/rocketmq/example/simple/LitePullConsumerAssign.java new file mode 100644 index 00000000000..0d8fc1c6985 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/simple/LitePullConsumerAssign.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.example.simple; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.apache.rocketmq.client.consumer.DefaultLitePullConsumer; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; + +public class LitePullConsumerAssign { + + public static volatile boolean running = true; + + public static void main(String[] args) throws Exception { + DefaultLitePullConsumer litePullConsumer = new DefaultLitePullConsumer("please_rename_unique_group_name"); + litePullConsumer.setAutoCommit(false); + litePullConsumer.start(); + Collection mqSet = litePullConsumer.fetchMessageQueues("TopicTest"); + List list = new ArrayList<>(mqSet); + List assignList = new ArrayList<>(); + for (int i = 0; i < list.size() / 2; i++) { + assignList.add(list.get(i)); + } + litePullConsumer.assign(assignList); + litePullConsumer.seek(assignList.get(0), 10); + try { + while (running) { + List messageExts = litePullConsumer.poll(); + System.out.printf("%s %n", messageExts); + litePullConsumer.commit(); + } + } finally { + litePullConsumer.shutdown(); + } + + } +} diff --git a/example/src/main/java/org/apache/rocketmq/example/simple/LitePullConsumerAssignWithSubExpression.java b/example/src/main/java/org/apache/rocketmq/example/simple/LitePullConsumerAssignWithSubExpression.java new file mode 100644 index 00000000000..fb673df3f82 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/simple/LitePullConsumerAssignWithSubExpression.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.example.simple; + +import org.apache.rocketmq.client.consumer.DefaultLitePullConsumer; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class LitePullConsumerAssignWithSubExpression { + + public static volatile boolean running = true; + + public static void main(String[] args) throws Exception { + DefaultLitePullConsumer litePullConsumer = new DefaultLitePullConsumer("please_rename_unique_group_name"); + litePullConsumer.setAutoCommit(false); + litePullConsumer.setSubExpressionForAssign("TopicTest", "TagA"); + litePullConsumer.start(); + Collection mqSet = litePullConsumer.fetchMessageQueues("TopicTest"); + List list = new ArrayList<>(mqSet); + List assignList = new ArrayList<>(); + for (int i = 0; i < list.size(); i++) { + assignList.add(list.get(i)); + } + mqSet = litePullConsumer.fetchMessageQueues("TopicTest1"); + list = new ArrayList<>(mqSet); + for (int i = 0; i < list.size(); i++) { + assignList.add(list.get(i)); + } + litePullConsumer.assign(assignList); + litePullConsumer.seek(assignList.get(0), 10); + try { + while (running) { + List messageExts = litePullConsumer.poll(); + System.out.printf("%s %n", messageExts); + litePullConsumer.commit(); + } + } finally { + litePullConsumer.shutdown(); + } + + } +} diff --git a/example/src/main/java/org/apache/rocketmq/example/simple/LitePullConsumerSubscribe.java b/example/src/main/java/org/apache/rocketmq/example/simple/LitePullConsumerSubscribe.java new file mode 100644 index 00000000000..e5c1a6134b4 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/simple/LitePullConsumerSubscribe.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.example.simple; + +import java.util.List; +import org.apache.rocketmq.client.consumer.DefaultLitePullConsumer; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.MessageExt; + +public class LitePullConsumerSubscribe { + + public static volatile boolean running = true; + + public static void main(String[] args) throws Exception { + DefaultLitePullConsumer litePullConsumer = new DefaultLitePullConsumer("lite_pull_consumer_test"); + litePullConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + litePullConsumer.subscribe("TopicTest", "*"); + litePullConsumer.start(); + try { + while (running) { + List messageExts = litePullConsumer.poll(); + System.out.printf("%s%n", messageExts); + } + } finally { + litePullConsumer.shutdown(); + } + } +} diff --git a/example/src/main/java/org/apache/rocketmq/example/simple/OnewayProducer.java b/example/src/main/java/org/apache/rocketmq/example/simple/OnewayProducer.java new file mode 100644 index 00000000000..e4bb06678ea --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/simple/OnewayProducer.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.example.simple; + +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.common.message.Message; + +import java.nio.charset.StandardCharsets; + +public class OnewayProducer { + public static void main(String[] args) throws Exception { + //Instantiate with a producer group name. + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + // Specify name server addresses. + producer.setNamesrvAddr("localhost:9876"); + //Launch the instance. + producer.start(); + for (int i = 0; i < 100; i++) { + //Create a message instance, specifying topic, tag and message body. + Message msg = new Message("TopicTest" /* Topic */, + "TagA" /* Tag */, + ("Hello RocketMQ " + + i).getBytes(StandardCharsets.UTF_8) /* Message body */ + ); + //Call send message to deliver message to one of brokers. + producer.sendOneway(msg); + } + //Wait for sending to complete + Thread.sleep(5000); + producer.shutdown(); + } +} \ No newline at end of file diff --git a/example/src/main/java/org/apache/rocketmq/example/simple/PopConsumer.java b/example/src/main/java/org/apache/rocketmq/example/simple/PopConsumer.java new file mode 100644 index 00000000000..6321e36d9ac --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/simple/PopConsumer.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.example.simple; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageRequestMode; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; + +public class PopConsumer { + public static final String TOPIC = "TopicTest"; + public static final String CONSUMER_GROUP = "CID_JODIE_1"; + public static void main(String[] args) throws Exception { + switchPop(); + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP); + consumer.subscribe(TOPIC, "*"); + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + consumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + consumer.setClientRebalance(false); + consumer.start(); + System.out.printf("Consumer Started.%n"); + } + private static void switchPop() throws Exception { + DefaultMQAdminExt mqAdminExt = new DefaultMQAdminExt(); + mqAdminExt.start(); + List brokerDatas = mqAdminExt.examineTopicRouteInfo(TOPIC).getBrokerDatas(); + for (BrokerData brokerData : brokerDatas) { + Set brokerAddrs = new HashSet<>(brokerData.getBrokerAddrs().values()); + for (String brokerAddr : brokerAddrs) { + mqAdminExt.setMessageRequestMode(brokerAddr, TOPIC, CONSUMER_GROUP, MessageRequestMode.POP, 8, 3_000); + } + } + } +} diff --git a/example/src/main/java/org/apache/rocketmq/example/simple/Producer.java b/example/src/main/java/org/apache/rocketmq/example/simple/Producer.java index 5751d22c7f1..920d481b939 100644 --- a/example/src/main/java/org/apache/rocketmq/example/simple/Producer.java +++ b/example/src/main/java/org/apache/rocketmq/example/simple/Producer.java @@ -20,29 +20,32 @@ import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.common.message.Message; -import org.apache.rocketmq.remoting.common.RemotingHelper; +import java.nio.charset.StandardCharsets; public class Producer { + + public static final String PRODUCER_GROUP = "ProducerGroupName"; + public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876"; + public static final String TOPIC = "TopicTest"; + public static final String TAG = "TagA"; + public static void main(String[] args) throws MQClientException, InterruptedException { - DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName"); + DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP); - producer.start(); + // Uncomment the following line while debugging, namesrvAddr should be set to your local address + //producer.setNamesrvAddr(DEFAULT_NAMESRVADDR); - for (int i = 0; i < 10000000; i++) + producer.start(); + for (int i = 0; i < 128; i++) { try { - { - Message msg = new Message("TopicTest", - "TagA", - "OrderID188", - "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); - SendResult sendResult = producer.send(msg); - System.out.printf("%s%n", sendResult); - } - + Message msg = new Message(TOPIC, TAG, "OrderID188", "Hello world".getBytes(StandardCharsets.UTF_8)); + SendResult sendResult = producer.send(msg); + System.out.printf("%s%n", sendResult); } catch (Exception e) { e.printStackTrace(); } + } producer.shutdown(); } diff --git a/example/src/main/java/org/apache/rocketmq/example/simple/PullConsumer.java b/example/src/main/java/org/apache/rocketmq/example/simple/PullConsumer.java index efffa36d59d..e1a02aa2664 100644 --- a/example/src/main/java/org/apache/rocketmq/example/simple/PullConsumer.java +++ b/example/src/main/java/org/apache/rocketmq/example/simple/PullConsumer.java @@ -16,63 +16,128 @@ */ package org.apache.rocketmq.example.simple; -import java.util.HashMap; -import java.util.Map; +import java.util.HashSet; +import java.util.List; import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.consumer.store.ReadOffsetType; +import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.exception.RemotingException; +@SuppressWarnings("deprecation") public class PullConsumer { - private static final Map OFFSE_TABLE = new HashMap(); public static void main(String[] args) throws MQClientException { - DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("please_rename_unique_group_name_5"); + DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("please_rename_unique_group_name_5"); + consumer.setNamesrvAddr("127.0.0.1:9876"); + Set topics = new HashSet<>(); + //You would be better to register topics,It will use in rebalance when starting + topics.add("TopicTest"); + consumer.setRegisterTopics(topics); consumer.start(); - Set mqs = consumer.fetchSubscribeMessageQueues("TopicTest1"); - for (MessageQueue mq : mqs) { - System.out.printf("Consume from the queue: %s%n", mq); - SINGLE_MQ: - while (true) { - try { - PullResult pullResult = - consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32); - System.out.printf("%s%n", pullResult); - putMessageQueueOffset(mq, pullResult.getNextBeginOffset()); - switch (pullResult.getPullStatus()) { - case FOUND: - break; - case NO_MATCHED_MSG: - break; - case NO_NEW_MSG: - break SINGLE_MQ; - case OFFSET_ILLEGAL: - break; - default: - break; - } - } catch (Exception e) { - e.printStackTrace(); + ExecutorService executors = Executors.newFixedThreadPool(topics.size(), new ThreadFactoryImpl("PullConsumerThread")); + for (String topic : consumer.getRegisterTopics()) { + + executors.execute(new Runnable() { + + public void doSomething(List msgs) { + //do your business + } - } - } - consumer.shutdown(); - } + @Override + public void run() { + while (true) { + try { + Set messageQueues = consumer.fetchMessageQueuesInBalance(topic); + if (messageQueues == null || messageQueues.isEmpty()) { + Thread.sleep(1000); + continue; + } + PullResult pullResult = null; + for (MessageQueue messageQueue : messageQueues) { + try { + long offset = this.consumeFromOffset(messageQueue); + pullResult = consumer.pull(messageQueue, "*", offset, 32); + switch (pullResult.getPullStatus()) { + case FOUND: + List msgs = pullResult.getMsgFoundList(); + + if (msgs != null && !msgs.isEmpty()) { + this.doSomething(msgs); + //update offset to local memory, eventually to broker + consumer.updateConsumeOffset(messageQueue, pullResult.getNextBeginOffset()); + //print pull tps + this.incPullTPS(topic, pullResult.getMsgFoundList().size()); + } + break; + case OFFSET_ILLEGAL: + consumer.updateConsumeOffset(messageQueue, pullResult.getNextBeginOffset()); + break; + case NO_NEW_MSG: + Thread.sleep(1); + consumer.updateConsumeOffset(messageQueue, pullResult.getNextBeginOffset()); + break; + case NO_MATCHED_MSG: + consumer.updateConsumeOffset(messageQueue, pullResult.getNextBeginOffset()); + break; + default: + } + } catch (RemotingException e) { + e.printStackTrace(); + } catch (MQBrokerException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } catch (MQClientException e) { + //reblance error + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } - private static long getMessageQueueOffset(MessageQueue mq) { - Long offset = OFFSE_TABLE.get(mq); - if (offset != null) - return offset; + public long consumeFromOffset(MessageQueue messageQueue) throws MQClientException { + //-1 when started + long offset = consumer.getOffsetStore().readOffset(messageQueue, ReadOffsetType.READ_FROM_MEMORY); + if (offset < 0) { + //query from broker + offset = consumer.getOffsetStore().readOffset(messageQueue, ReadOffsetType.READ_FROM_STORE); + } + if (offset < 0) { + //first time start from last offset + offset = consumer.maxOffset(messageQueue); + } + //make sure + if (offset < 0) { + offset = 0; + } + return offset; + } - return 0; - } + public void incPullTPS(String topic, int pullSize) { + consumer.getDefaultMQPullConsumerImpl().getRebalanceImpl().getmQClientFactory() + .getConsumerStatsManager().incPullTPS(consumer.getConsumerGroup(), topic, pullSize); + } + }); - private static void putMessageQueueOffset(MessageQueue mq, long offset) { - OFFSE_TABLE.put(mq, offset); + } +// executors.shutdown(); +// consumer.shutdown(); } - } diff --git a/example/src/main/java/org/apache/rocketmq/example/simple/PullConsumerTest.java b/example/src/main/java/org/apache/rocketmq/example/simple/PullConsumerTest.java deleted file mode 100644 index 16108b8c6a8..00000000000 --- a/example/src/main/java/org/apache/rocketmq/example/simple/PullConsumerTest.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.example.simple; - -import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; -import org.apache.rocketmq.client.consumer.PullResult; -import org.apache.rocketmq.client.exception.MQClientException; -import org.apache.rocketmq.common.message.MessageQueue; - -public class PullConsumerTest { - public static void main(String[] args) throws MQClientException { - DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("please_rename_unique_group_name_5"); - consumer.start(); - - try { - MessageQueue mq = new MessageQueue(); - mq.setQueueId(0); - mq.setTopic("TopicTest3"); - mq.setBrokerName("vivedeMacBook-Pro.local"); - - long offset = 26; - - long beginTime = System.currentTimeMillis(); - PullResult pullResult = consumer.pullBlockIfNotFound(mq, null, offset, 32); - System.out.printf("%s%n", System.currentTimeMillis() - beginTime); - System.out.printf("%s%n", pullResult); - } catch (Exception e) { - e.printStackTrace(); - } - - consumer.shutdown(); - } -} diff --git a/example/src/main/java/org/apache/rocketmq/example/simple/PullScheduleService.java b/example/src/main/java/org/apache/rocketmq/example/simple/PullScheduleService.java index 151628f9a80..c652b065e4d 100644 --- a/example/src/main/java/org/apache/rocketmq/example/simple/PullScheduleService.java +++ b/example/src/main/java/org/apache/rocketmq/example/simple/PullScheduleService.java @@ -24,7 +24,7 @@ import org.apache.rocketmq.client.consumer.PullTaskContext; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; public class PullScheduleService { @@ -32,7 +32,7 @@ public static void main(String[] args) throws MQClientException { final MQPullConsumerScheduleService scheduleService = new MQPullConsumerScheduleService("GroupName1"); scheduleService.setMessageModel(MessageModel.CLUSTERING); - scheduleService.registerPullTaskCallback("TopicTest1", new PullTaskCallback() { + scheduleService.registerPullTaskCallback("TopicTest", new PullTaskCallback() { @Override public void doPullTask(MessageQueue mq, PullTaskContext context) { diff --git a/example/src/main/java/org/apache/rocketmq/example/simple/PushConsumer.java b/example/src/main/java/org/apache/rocketmq/example/simple/PushConsumer.java index c6c7e39d174..9de2b01d49b 100644 --- a/example/src/main/java/org/apache/rocketmq/example/simple/PushConsumer.java +++ b/example/src/main/java/org/apache/rocketmq/example/simple/PushConsumer.java @@ -26,13 +26,20 @@ import org.apache.rocketmq.common.message.MessageExt; public class PushConsumer { - + public static final String TOPIC = "TopicTest"; + public static final String CONSUMER_GROUP = "CID_JODIE_1"; + public static final String NAMESRV_ADDR = "127.0.0.1:9876"; public static void main(String[] args) throws InterruptedException, MQClientException { - DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_JODIE_1"); - consumer.subscribe("Jodie_topic_1023", "*"); + + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP); + + // Uncomment the following line while debugging, namesrvAddr should be set to your local address +// consumer.setNamesrvAddr(NAMESRV_ADDR); + + consumer.subscribe(TOPIC, "*"); consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); //wrong time format 2017_0422_221800 - consumer.setConsumeTimestamp("20170422221800"); + consumer.setConsumeTimestamp("20181109221800"); consumer.registerMessageListener(new MessageListenerConcurrently() { @Override diff --git a/example/src/main/java/org/apache/rocketmq/example/simple/RandomAsyncCommit.java b/example/src/main/java/org/apache/rocketmq/example/simple/RandomAsyncCommit.java index 031cb151360..d4bd0ea6e4f 100644 --- a/example/src/main/java/org/apache/rocketmq/example/simple/RandomAsyncCommit.java +++ b/example/src/main/java/org/apache/rocketmq/example/simple/RandomAsyncCommit.java @@ -24,7 +24,7 @@ public class RandomAsyncCommit { private final ConcurrentHashMap mqCachedTable = - new ConcurrentHashMap(); + new ConcurrentHashMap<>(); public void putMessages(final MessageQueue mq, final List msgs) { CachedQueue cachedQueue = this.mqCachedTable.get(mq); diff --git a/example/src/main/java/org/apache/rocketmq/example/simple/TestProducer.java b/example/src/main/java/org/apache/rocketmq/example/simple/TestProducer.java deleted file mode 100644 index 576f9cb6fbf..00000000000 --- a/example/src/main/java/org/apache/rocketmq/example/simple/TestProducer.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.example.simple; - -import org.apache.rocketmq.client.QueryResult; -import org.apache.rocketmq.client.exception.MQClientException; -import org.apache.rocketmq.client.producer.DefaultMQProducer; -import org.apache.rocketmq.client.producer.SendResult; -import org.apache.rocketmq.common.message.Message; -import org.apache.rocketmq.common.message.MessageExt; -import org.apache.rocketmq.remoting.common.RemotingHelper; - -public class TestProducer { - public static void main(String[] args) throws MQClientException, InterruptedException { - DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName"); - producer.start(); - - for (int i = 0; i < 1; i++) - try { - { - Message msg = new Message("TopicTest1", - "TagA", - "key113", - "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); - SendResult sendResult = producer.send(msg); - System.out.printf("%s%n", sendResult); - - QueryResult queryMessage = - producer.queryMessage("TopicTest1", "key113", 10, 0, System.currentTimeMillis()); - for (MessageExt m : queryMessage.getMessageList()) { - System.out.printf("%s%n", m); - } - } - - } catch (Exception e) { - e.printStackTrace(); - } - producer.shutdown(); - } -} diff --git a/example/src/main/java/org/apache/rocketmq/example/tracemessage/OpenTracingProducer.java b/example/src/main/java/org/apache/rocketmq/example/tracemessage/OpenTracingProducer.java new file mode 100644 index 00000000000..0c41b5b6f18 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/tracemessage/OpenTracingProducer.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.example.tracemessage; + +import io.jaegertracing.Configuration; +import io.jaegertracing.internal.samplers.ConstSampler; +import io.opentracing.Tracer; +import io.opentracing.util.GlobalTracer; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.trace.hook.SendMessageOpenTracingHookImpl; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.remoting.common.RemotingHelper; + +public class OpenTracingProducer { + + public static final String PRODUCER_GROUP = "ProducerGroupName"; + public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876"; + public static final String TOPIC = "TopicTest"; + public static final String TAG = "TagA"; + public static final String KEY = "OrderID188"; + + public static void main(String[] args) throws MQClientException { + + Tracer tracer = initTracer(); + + DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP); + + // Uncomment the following line while debugging, namesrvAddr should be set to your local address +// producer.setNamesrvAddr(DEFAULT_NAMESRVADDR); + producer.getDefaultMQProducerImpl().registerSendMessageHook(new SendMessageOpenTracingHookImpl(tracer)); + producer.start(); + + try { + Message msg = new Message(TOPIC, + TAG, + KEY, + "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); + SendResult sendResult = producer.send(msg); + System.out.printf("%s%n", sendResult); + + } catch (Exception e) { + e.printStackTrace(); + } + + producer.shutdown(); + } + + private static Tracer initTracer() { + Configuration.SamplerConfiguration samplerConfig = Configuration.SamplerConfiguration.fromEnv() + .withType(ConstSampler.TYPE) + .withParam(1); + Configuration.ReporterConfiguration reporterConfig = Configuration.ReporterConfiguration.fromEnv() + .withLogSpans(true); + + Configuration config = new Configuration("rocketmq") + .withSampler(samplerConfig) + .withReporter(reporterConfig); + GlobalTracer.registerIfAbsent(config.getTracer()); + return config.getTracer(); + } +} diff --git a/example/src/main/java/org/apache/rocketmq/example/tracemessage/OpenTracingPushConsumer.java b/example/src/main/java/org/apache/rocketmq/example/tracemessage/OpenTracingPushConsumer.java new file mode 100644 index 00000000000..9ac7c1634c7 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/tracemessage/OpenTracingPushConsumer.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.example.tracemessage; + +import io.jaegertracing.Configuration; +import io.jaegertracing.internal.samplers.ConstSampler; +import io.opentracing.Tracer; +import io.opentracing.util.GlobalTracer; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.trace.hook.ConsumeMessageOpenTracingHookImpl; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; + +public class OpenTracingPushConsumer { + + public static final String CONSUMER_GROUP = "CID_JODIE_1"; + public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876"; + public static final String TOPIC = "TopicTest"; + + public static void main(String[] args) throws InterruptedException, MQClientException { + Tracer tracer = initTracer(); + + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP); + + // Uncomment the following line while debugging, namesrvAddr should be set to your local address +// consumer.setNamesrvAddr(DEFAULT_NAMESRVADDR); + consumer.registerConsumeMessageHook(new ConsumeMessageOpenTracingHookImpl(tracer)); + + consumer.subscribe(TOPIC, "*"); + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + + consumer.setConsumeTimestamp("20181109221800"); + consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + consumer.start(); + System.out.printf("Consumer Started.%n"); + } + + private static Tracer initTracer() { + Configuration.SamplerConfiguration samplerConfig = Configuration.SamplerConfiguration.fromEnv() + .withType(ConstSampler.TYPE) + .withParam(1); + Configuration.ReporterConfiguration reporterConfig = Configuration.ReporterConfiguration.fromEnv() + .withLogSpans(true); + + Configuration config = new Configuration("rocketmq") + .withSampler(samplerConfig) + .withReporter(reporterConfig); + GlobalTracer.registerIfAbsent(config.getTracer()); + return config.getTracer(); + } +} diff --git a/example/src/main/java/org/apache/rocketmq/example/tracemessage/OpenTracingTransactionProducer.java b/example/src/main/java/org/apache/rocketmq/example/tracemessage/OpenTracingTransactionProducer.java new file mode 100644 index 00000000000..dc05c72b1c9 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/tracemessage/OpenTracingTransactionProducer.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.example.tracemessage; + +import io.jaegertracing.Configuration; +import io.jaegertracing.internal.samplers.ConstSampler; +import io.opentracing.Tracer; +import io.opentracing.util.GlobalTracer; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.producer.LocalTransactionState; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.TransactionListener; +import org.apache.rocketmq.client.producer.TransactionMQProducer; +import org.apache.rocketmq.client.trace.hook.EndTransactionOpenTracingHookImpl; +import org.apache.rocketmq.client.trace.hook.SendMessageOpenTracingHookImpl; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.remoting.common.RemotingHelper; + +import java.io.UnsupportedEncodingException; + +public class OpenTracingTransactionProducer { + + public static final String PRODUCER_GROUP = "please_rename_unique_group_name"; + public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876"; + public static final String TOPIC = "TopicTest"; + public static final String TAG = "Tag"; + public static final String KEY = "KEY"; + public static final int MESSAGE_COUNT = 100000; + + public static void main(String[] args) throws MQClientException, InterruptedException { + Tracer tracer = initTracer(); + + TransactionMQProducer producer = new TransactionMQProducer(PRODUCER_GROUP); + + // Uncomment the following line while debugging, namesrvAddr should be set to your local address +// producer.setNamesrvAddr(DEFAULT_NAMESRVADDR); + producer.getDefaultMQProducerImpl().registerSendMessageHook(new SendMessageOpenTracingHookImpl(tracer)); + producer.getDefaultMQProducerImpl().registerEndTransactionHook(new EndTransactionOpenTracingHookImpl(tracer)); + + producer.setTransactionListener(new TransactionListener() { + @Override + public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { + return LocalTransactionState.COMMIT_MESSAGE; + } + + @Override + public LocalTransactionState checkLocalTransaction(MessageExt msg) { + return LocalTransactionState.COMMIT_MESSAGE; + } + }); + producer.start(); + + try { + Message msg = new Message(TOPIC, TAG, KEY, + "Hello RocketMQ".getBytes(RemotingHelper.DEFAULT_CHARSET)); + SendResult sendResult = producer.sendMessageInTransaction(msg, null); + System.out.printf("%s%n", sendResult); + } catch (MQClientException | UnsupportedEncodingException e) { + e.printStackTrace(); + } + + for (int i = 0; i < MESSAGE_COUNT; i++) { + Thread.sleep(1000); + } + producer.shutdown(); + } + + private static Tracer initTracer() { + Configuration.SamplerConfiguration samplerConfig = Configuration.SamplerConfiguration.fromEnv() + .withType(ConstSampler.TYPE) + .withParam(1); + Configuration.ReporterConfiguration reporterConfig = Configuration.ReporterConfiguration.fromEnv() + .withLogSpans(true); + + Configuration config = new Configuration("rocketmq") + .withSampler(samplerConfig) + .withReporter(reporterConfig); + GlobalTracer.registerIfAbsent(config.getTracer()); + return config.getTracer(); + } +} diff --git a/example/src/main/java/org/apache/rocketmq/example/tracemessage/TraceProducer.java b/example/src/main/java/org/apache/rocketmq/example/tracemessage/TraceProducer.java new file mode 100644 index 00000000000..72dde674c01 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/tracemessage/TraceProducer.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.example.tracemessage; + +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.remoting.common.RemotingHelper; + +public class TraceProducer { + + public static final String PRODUCER_GROUP = "ProducerGroupName"; + public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876"; + public static final String TOPIC = "TopicTest"; + public static final String TAG = "TagA"; + public static final String KEY = "OrderID188"; + public static final int MESSAGE_COUNT = 128; + + public static void main(String[] args) throws MQClientException, InterruptedException { + + DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP, true, null); + + // Uncomment the following line while debugging, namesrvAddr should be set to your local address +// producer.setNamesrvAddr(DEFAULT_NAMESRVADDR); + producer.start(); + + for (int i = 0; i < MESSAGE_COUNT; i++) { + try { + { + Message msg = new Message(TOPIC, + TAG, + KEY, + "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); + SendResult sendResult = producer.send(msg); + System.out.printf("%s%n", sendResult); + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + + producer.shutdown(); + } +} diff --git a/example/src/main/java/org/apache/rocketmq/example/tracemessage/TracePushConsumer.java b/example/src/main/java/org/apache/rocketmq/example/tracemessage/TracePushConsumer.java new file mode 100644 index 00000000000..81c5e31ca58 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/tracemessage/TracePushConsumer.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.example.tracemessage; + +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; + +public class TracePushConsumer { + + public static final String CONSUMER_GROUP = "ProducerGroupName"; + public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876"; + public static final String TOPIC = "TopicTest"; + + public static void main(String[] args) throws InterruptedException, MQClientException { + // Here,we use the default message track trace topic name + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP, true, null); + + // Uncomment the following line while debugging, namesrvAddr should be set to your local address +// consumer.setNamesrvAddr(DEFAULT_NAMESRVADDR); + consumer.subscribe(TOPIC, "*"); + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + // Wrong time format 2017_0422_221800 + consumer.setConsumeTimestamp("20181109221800"); + consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { + System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + }); + consumer.start(); + System.out.printf("Consumer Started.%n"); + } +} diff --git a/example/src/main/java/org/apache/rocketmq/example/transaction/TransactionCheckListenerImpl.java b/example/src/main/java/org/apache/rocketmq/example/transaction/TransactionCheckListenerImpl.java deleted file mode 100644 index acb755ecfd7..00000000000 --- a/example/src/main/java/org/apache/rocketmq/example/transaction/TransactionCheckListenerImpl.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.example.transaction; - -import java.util.concurrent.atomic.AtomicInteger; -import org.apache.rocketmq.client.producer.LocalTransactionState; -import org.apache.rocketmq.client.producer.TransactionCheckListener; -import org.apache.rocketmq.common.message.MessageExt; - -public class TransactionCheckListenerImpl implements TransactionCheckListener { - private AtomicInteger transactionIndex = new AtomicInteger(0); - - @Override - public LocalTransactionState checkLocalTransactionState(MessageExt msg) { - System.out.printf("server checking TrMsg %s%n", msg); - - int value = transactionIndex.getAndIncrement(); - if ((value % 6) == 0) { - throw new RuntimeException("Could not find db"); - } else if ((value % 5) == 0) { - return LocalTransactionState.ROLLBACK_MESSAGE; - } else if ((value % 4) == 0) { - return LocalTransactionState.COMMIT_MESSAGE; - } - - return LocalTransactionState.UNKNOW; - } -} diff --git a/example/src/main/java/org/apache/rocketmq/example/transaction/TransactionExecuterImpl.java b/example/src/main/java/org/apache/rocketmq/example/transaction/TransactionExecuterImpl.java deleted file mode 100644 index e7d193ee85b..00000000000 --- a/example/src/main/java/org/apache/rocketmq/example/transaction/TransactionExecuterImpl.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.example.transaction; - -import java.util.concurrent.atomic.AtomicInteger; -import org.apache.rocketmq.client.producer.LocalTransactionExecuter; -import org.apache.rocketmq.client.producer.LocalTransactionState; -import org.apache.rocketmq.common.message.Message; - -public class TransactionExecuterImpl implements LocalTransactionExecuter { - private AtomicInteger transactionIndex = new AtomicInteger(1); - - @Override - public LocalTransactionState executeLocalTransactionBranch(final Message msg, final Object arg) { - int value = transactionIndex.getAndIncrement(); - - if (value == 0) { - throw new RuntimeException("Could not find db"); - } else if ((value % 5) == 0) { - return LocalTransactionState.ROLLBACK_MESSAGE; - } else if ((value % 4) == 0) { - return LocalTransactionState.COMMIT_MESSAGE; - } - - return LocalTransactionState.UNKNOW; - } -} diff --git a/example/src/main/java/org/apache/rocketmq/example/transaction/TransactionListenerImpl.java b/example/src/main/java/org/apache/rocketmq/example/transaction/TransactionListenerImpl.java new file mode 100644 index 00000000000..cb066d21d39 --- /dev/null +++ b/example/src/main/java/org/apache/rocketmq/example/transaction/TransactionListenerImpl.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.example.transaction; + +import org.apache.rocketmq.client.producer.LocalTransactionState; +import org.apache.rocketmq.client.producer.TransactionListener; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageExt; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +public class TransactionListenerImpl implements TransactionListener { + private AtomicInteger transactionIndex = new AtomicInteger(0); + + private ConcurrentHashMap localTrans = new ConcurrentHashMap<>(); + + @Override + public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { + int value = transactionIndex.getAndIncrement(); + int status = value % 3; + localTrans.put(msg.getTransactionId(), status); + return LocalTransactionState.UNKNOW; + } + + @Override + public LocalTransactionState checkLocalTransaction(MessageExt msg) { + Integer status = localTrans.get(msg.getTransactionId()); + if (null != status) { + switch (status) { + case 0: + return LocalTransactionState.UNKNOW; + case 1: + return LocalTransactionState.COMMIT_MESSAGE; + case 2: + return LocalTransactionState.ROLLBACK_MESSAGE; + default: + return LocalTransactionState.COMMIT_MESSAGE; + } + } + return LocalTransactionState.COMMIT_MESSAGE; + } +} diff --git a/example/src/main/java/org/apache/rocketmq/example/transaction/TransactionProducer.java b/example/src/main/java/org/apache/rocketmq/example/transaction/TransactionProducer.java index edfad2485f6..d1d57c55ef6 100644 --- a/example/src/main/java/org/apache/rocketmq/example/transaction/TransactionProducer.java +++ b/example/src/main/java/org/apache/rocketmq/example/transaction/TransactionProducer.java @@ -16,32 +16,51 @@ */ package org.apache.rocketmq.example.transaction; -import java.io.UnsupportedEncodingException; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.SendResult; -import org.apache.rocketmq.client.producer.TransactionCheckListener; +import org.apache.rocketmq.client.producer.TransactionListener; import org.apache.rocketmq.client.producer.TransactionMQProducer; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.remoting.common.RemotingHelper; +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + public class TransactionProducer { + + public static final String PRODUCER_GROUP = "please_rename_unique_group_name"; + public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876"; + public static final String TOPIC = "TopicTest1234"; + + public static final int MESSAGE_COUNT = 10; + public static void main(String[] args) throws MQClientException, InterruptedException { - TransactionCheckListener transactionCheckListener = new TransactionCheckListenerImpl(); - TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name"); - producer.setCheckThreadPoolMinSize(2); - producer.setCheckThreadPoolMaxSize(2); - producer.setCheckRequestHoldMax(2000); - producer.setTransactionCheckListener(transactionCheckListener); + TransactionListener transactionListener = new TransactionListenerImpl(); + TransactionMQProducer producer = new TransactionMQProducer(PRODUCER_GROUP, Arrays.asList(TOPIC)); + + // Uncomment the following line while debugging, namesrvAddr should be set to your local address +// producer.setNamesrvAddr(DEFAULT_NAMESRVADDR); + ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2000), r -> { + Thread thread = new Thread(r); + thread.setName("client-transaction-msg-check-thread"); + return thread; + }); + + producer.setExecutorService(executorService); + producer.setTransactionListener(transactionListener); producer.start(); String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"}; - TransactionExecuterImpl tranExecuter = new TransactionExecuterImpl(); - for (int i = 0; i < 100; i++) { + for (int i = 0; i < MESSAGE_COUNT; i++) { try { Message msg = - new Message("TopicTest", tags[i % tags.length], "KEY" + i, + new Message(TOPIC, tags[i % tags.length], "KEY" + i, ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); - SendResult sendResult = producer.sendMessageInTransaction(msg, tranExecuter, null); + SendResult sendResult = producer.sendMessageInTransaction(msg, null); System.out.printf("%s%n", sendResult); Thread.sleep(10); diff --git a/example/src/main/resources/MessageFilterImpl.java b/example/src/main/resources/MessageFilterImpl.java deleted file mode 100644 index 6cb5d158a38..00000000000 --- a/example/src/main/resources/MessageFilterImpl.java +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.example.filter; - -import org.apache.rocketmq.common.filter.FilterContext; -import org.apache.rocketmq.common.filter.MessageFilter; -import org.apache.rocketmq.common.message.MessageExt; - -public class MessageFilterImpl implements MessageFilter { - - @Override - public boolean match(MessageExt msg, FilterContext context) { - String property = msg.getProperty("SequenceId"); - if (property != null) { - int id = Integer.parseInt(property); - if (((id % 10) == 0) && - (id > 100)) { - return true; - } - } - - return false; - } -} diff --git a/filter/BUILD.bazel b/filter/BUILD.bazel new file mode 100644 index 00000000000..048c3bdb623 --- /dev/null +++ b/filter/BUILD.bazel @@ -0,0 +1,58 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +load("//bazel:GenTestRules.bzl", "GenTestRules") + +java_library( + name = "filter", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//common", + "//remoting", + "//srvutil", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:commons_validator_commons_validator", + "@maven//:com_github_luben_zstd_jni", + "@maven//:org_lz4_lz4_java", + "@maven//:com_alibaba_fastjson", + "@maven//:io_netty_netty_all", + "@maven//:com_google_guava_guava", + ], +) + +java_library( + name = "tests", + srcs = glob(["src/test/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + ":filter", + "//common", + "//remoting", + "//:test_deps", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:io_netty_netty_all", + ], + resources = glob(["src/test/resources/certs/*.pem"]) + glob(["src/test/resources/certs/*.key"]) +) + +GenTestRules( + name = "GeneratedTestRules", + test_files = glob(["src/test/java/**/*Test.java"]), + deps = [ + ":tests", + ], +) diff --git a/filter/pom.xml b/filter/pom.xml index c024effdb6e..5914b816d8e 100644 --- a/filter/pom.xml +++ b/filter/pom.xml @@ -20,7 +20,7 @@ rocketmq-all org.apache.rocketmq - 4.2.0 + 5.2.0 4.0.0 @@ -28,6 +28,10 @@ rocketmq-filter rocketmq-filter ${project.version} + + ${basedir}/.. + + ${project.groupId} @@ -37,5 +41,9 @@ ${project.groupId} rocketmq-srvutil + + com.google.guava + guava + \ No newline at end of file diff --git a/filter/src/main/java/org/apache/rocketmq/filter/FilterFactory.java b/filter/src/main/java/org/apache/rocketmq/filter/FilterFactory.java index f1e1c7d73fa..71a8b4d4bd3 100644 --- a/filter/src/main/java/org/apache/rocketmq/filter/FilterFactory.java +++ b/filter/src/main/java/org/apache/rocketmq/filter/FilterFactory.java @@ -27,7 +27,7 @@ public class FilterFactory { public static final FilterFactory INSTANCE = new FilterFactory(); - protected static final Map FILTER_SPI_HOLDER = new HashMap(4); + protected static final Map FILTER_SPI_HOLDER = new HashMap<>(4); static { FilterFactory.INSTANCE.register(new SqlFilter()); diff --git a/filter/src/main/java/org/apache/rocketmq/filter/expression/BooleanConstantExpression.java b/filter/src/main/java/org/apache/rocketmq/filter/expression/BooleanConstantExpression.java new file mode 100644 index 00000000000..958e622f6c3 --- /dev/null +++ b/filter/src/main/java/org/apache/rocketmq/filter/expression/BooleanConstantExpression.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.filter.expression; + +/** + * BooleanConstantExpression + */ +public class BooleanConstantExpression extends ConstantExpression implements BooleanExpression { + + public static final BooleanConstantExpression NULL = new BooleanConstantExpression(null); + public static final BooleanConstantExpression TRUE = new BooleanConstantExpression(Boolean.TRUE); + public static final BooleanConstantExpression FALSE = new BooleanConstantExpression(Boolean.FALSE); + + public BooleanConstantExpression(Object value) { + super(value); + } + + public boolean matches(EvaluationContext context) throws Exception { + Object object = evaluate(context); + return object != null && object == Boolean.TRUE; + } +} diff --git a/filter/src/main/java/org/apache/rocketmq/filter/expression/ComparisonExpression.java b/filter/src/main/java/org/apache/rocketmq/filter/expression/ComparisonExpression.java index fe898e78848..14fd7045b41 100644 --- a/filter/src/main/java/org/apache/rocketmq/filter/expression/ComparisonExpression.java +++ b/filter/src/main/java/org/apache/rocketmq/filter/expression/ComparisonExpression.java @@ -32,7 +32,7 @@ */ public abstract class ComparisonExpression extends BinaryExpression implements BooleanExpression { - public static final ThreadLocal CONVERT_STRING_EXPRESSIONS = new ThreadLocal(); + public static final ThreadLocal CONVERT_STRING_EXPRESSIONS = new ThreadLocal<>(); boolean convertStringExpressions = false; @@ -69,6 +69,258 @@ public static BooleanExpression createNotBetween(Expression value, Expression le return LogicExpression.createOR(createLessThan(value, left), createGreaterThan(value, right)); } + static class ContainsExpression extends UnaryExpression implements BooleanExpression { + + String search; + + public ContainsExpression(Expression right, String search) { + super(right); + this.search = search; + } + + public String getExpressionSymbol() { + return "CONTAINS"; + } + + public Object evaluate(EvaluationContext message) throws Exception { + + if (search == null || search.length() == 0) { + return Boolean.FALSE; + } + + Object rv = this.getRight().evaluate(message); + + if (rv == null) { + return Boolean.FALSE; + } + + if (!(rv instanceof String)) { + return Boolean.FALSE; + } + + return ((String)rv).contains(search) ? Boolean.TRUE : Boolean.FALSE; + } + + public boolean matches(EvaluationContext message) throws Exception { + Object object = evaluate(message); + return object != null && object == Boolean.TRUE; + } + } + + static class NotContainsExpression extends UnaryExpression implements BooleanExpression { + + String search; + + public NotContainsExpression(Expression right, String search) { + super(right); + this.search = search; + } + + public String getExpressionSymbol() { + return "NOT CONTAINS"; + } + + public Object evaluate(EvaluationContext message) throws Exception { + + if (search == null || search.length() == 0) { + return Boolean.FALSE; + } + + Object rv = this.getRight().evaluate(message); + + if (rv == null) { + return Boolean.FALSE; + } + + if (!(rv instanceof String)) { + return Boolean.FALSE; + } + + return ((String)rv).contains(search) ? Boolean.FALSE : Boolean.TRUE; + } + + public boolean matches(EvaluationContext message) throws Exception { + Object object = evaluate(message); + return object != null && object == Boolean.TRUE; + } + } + + public static BooleanExpression createContains(Expression left, String search) { + return new ContainsExpression(left, search); + } + + public static BooleanExpression createNotContains(Expression left, String search) { + return new NotContainsExpression(left, search); + } + + static class StartsWithExpression extends UnaryExpression implements BooleanExpression { + + String search; + + public StartsWithExpression(Expression right, String search) { + super(right); + this.search = search; + } + + public String getExpressionSymbol() { + return "STARTSWITH"; + } + + public Object evaluate(EvaluationContext message) throws Exception { + + if (search == null || search.length() == 0) { + return Boolean.FALSE; + } + + Object rv = this.getRight().evaluate(message); + + if (rv == null) { + return Boolean.FALSE; + } + + if (!(rv instanceof String)) { + return Boolean.FALSE; + } + + return ((String)rv).startsWith(search) ? Boolean.TRUE : Boolean.FALSE; + } + + public boolean matches(EvaluationContext message) throws Exception { + Object object = evaluate(message); + return object != null && object == Boolean.TRUE; + } + } + + static class NotStartsWithExpression extends UnaryExpression implements BooleanExpression { + + String search; + + public NotStartsWithExpression(Expression right, String search) { + super(right); + this.search = search; + } + + public String getExpressionSymbol() { + return "NOT STARTSWITH"; + } + + public Object evaluate(EvaluationContext message) throws Exception { + + if (search == null || search.length() == 0) { + return Boolean.FALSE; + } + + Object rv = this.getRight().evaluate(message); + + if (rv == null) { + return Boolean.FALSE; + } + + if (!(rv instanceof String)) { + return Boolean.FALSE; + } + + return ((String)rv).startsWith(search) ? Boolean.FALSE : Boolean.TRUE; + } + + public boolean matches(EvaluationContext message) throws Exception { + Object object = evaluate(message); + return object != null && object == Boolean.TRUE; + } + } + + public static BooleanExpression createStartsWith(Expression left, String search) { + return new StartsWithExpression(left, search); + } + + public static BooleanExpression createNotStartsWith(Expression left, String search) { + return new NotStartsWithExpression(left, search); + } + + static class EndsWithExpression extends UnaryExpression implements BooleanExpression { + + String search; + + public EndsWithExpression(Expression right, String search) { + super(right); + this.search = search; + } + + public String getExpressionSymbol() { + return "ENDSWITH"; + } + + public Object evaluate(EvaluationContext message) throws Exception { + + if (search == null || search.length() == 0) { + return Boolean.FALSE; + } + + Object rv = this.getRight().evaluate(message); + + if (rv == null) { + return Boolean.FALSE; + } + + if (!(rv instanceof String)) { + return Boolean.FALSE; + } + + return ((String)rv).endsWith(search) ? Boolean.TRUE : Boolean.FALSE; + } + + public boolean matches(EvaluationContext message) throws Exception { + Object object = evaluate(message); + return object != null && object == Boolean.TRUE; + } + } + + static class NotEndsWithExpression extends UnaryExpression implements BooleanExpression { + + String search; + + public NotEndsWithExpression(Expression right, String search) { + super(right); + this.search = search; + } + + public String getExpressionSymbol() { + return "NOT ENDSWITH"; + } + + public Object evaluate(EvaluationContext message) throws Exception { + + if (search == null || search.length() == 0) { + return Boolean.FALSE; + } + + Object rv = this.getRight().evaluate(message); + + if (rv == null) { + return Boolean.FALSE; + } + + if (!(rv instanceof String)) { + return Boolean.FALSE; + } + + return ((String)rv).endsWith(search) ? Boolean.FALSE : Boolean.TRUE; + } + + public boolean matches(EvaluationContext message) throws Exception { + Object object = evaluate(message); + return object != null && object == Boolean.TRUE; + } + } + + public static BooleanExpression createEndsWith(Expression left, String search) { + return new EndsWithExpression(left, search); + } + + public static BooleanExpression createNotEndsWith(Expression left, String search) { + return new NotEndsWithExpression(left, search); + } + @SuppressWarnings({"rawtypes", "unchecked"}) public static BooleanExpression createInFilter(Expression left, List elements) { @@ -90,11 +342,11 @@ public static BooleanExpression createNotInFilter(Expression left, List elements } public static BooleanExpression createIsNull(Expression left) { - return doCreateEqual(left, ConstantExpression.NULL); + return doCreateEqual(left, BooleanConstantExpression.NULL); } public static BooleanExpression createIsNotNull(Expression left) { - return UnaryExpression.createNOT(doCreateEqual(left, ConstantExpression.NULL)); + return UnaryExpression.createNOT(doCreateEqual(left, BooleanConstantExpression.NULL)); } public static BooleanExpression createNotEqual(Expression left, Expression right) { diff --git a/filter/src/main/java/org/apache/rocketmq/filter/expression/ConstantExpression.java b/filter/src/main/java/org/apache/rocketmq/filter/expression/ConstantExpression.java index e649f5a2328..127bd8ba7bd 100644 --- a/filter/src/main/java/org/apache/rocketmq/filter/expression/ConstantExpression.java +++ b/filter/src/main/java/org/apache/rocketmq/filter/expression/ConstantExpression.java @@ -30,21 +30,6 @@ */ public class ConstantExpression implements Expression { - static class BooleanConstantExpression extends ConstantExpression implements BooleanExpression { - public BooleanConstantExpression(Object value) { - super(value); - } - - public boolean matches(EvaluationContext context) throws Exception { - Object object = evaluate(context); - return object != null && object == Boolean.TRUE; - } - } - - public static final BooleanConstantExpression NULL = new BooleanConstantExpression(null); - public static final BooleanConstantExpression TRUE = new BooleanConstantExpression(Boolean.TRUE); - public static final BooleanConstantExpression FALSE = new BooleanConstantExpression(Boolean.FALSE); - private Object value; public ConstantExpression(Object value) { @@ -60,16 +45,10 @@ public static ConstantExpression createFromDecimal(String text) { // only support Long.MIN_VALUE ~ Long.MAX_VALUE Number value = new Long(text); -// try { -// value = new Long(text); -// } catch (NumberFormatException e) { -// // The number may be too big to fit in a long. -// value = new BigDecimal(text); -// } long l = value.longValue(); if (Integer.MIN_VALUE <= l && l <= Integer.MAX_VALUE) { - value = Integer.valueOf(value.intValue()); + value = value.intValue(); } return new ConstantExpression(value); } @@ -106,7 +85,7 @@ public String toString() { return "NULL"; } if (value instanceof Boolean) { - return ((Boolean) value).booleanValue() ? "TRUE" : "FALSE"; + return (Boolean) value ? "TRUE" : "FALSE"; } if (value instanceof String) { return encodeString((String) value); @@ -138,17 +117,19 @@ public boolean equals(Object o) { * it was provided in a selector. */ public static String encodeString(String s) { - StringBuffer b = new StringBuffer(); - b.append('\''); + + StringBuilder builder = new StringBuilder(); + + builder.append('\''); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (c == '\'') { - b.append(c); + builder.append(c); } - b.append(c); + builder.append(c); } - b.append('\''); - return b.toString(); + builder.append('\''); + return builder.toString(); } } diff --git a/filter/src/main/java/org/apache/rocketmq/filter/expression/UnaryExpression.java b/filter/src/main/java/org/apache/rocketmq/filter/expression/UnaryExpression.java index 24845fc0589..7a62624b514 100644 --- a/filter/src/main/java/org/apache/rocketmq/filter/expression/UnaryExpression.java +++ b/filter/src/main/java/org/apache/rocketmq/filter/expression/UnaryExpression.java @@ -53,6 +53,7 @@ public UnaryExpression(Expression left, UnaryType unaryType) { public static Expression createNegate(Expression left) { return new UnaryExpression(left, UnaryType.NEGATE) { + @Override public Object evaluate(EvaluationContext context) throws Exception { Object rvalue = right.evaluate(context); if (rvalue == null) { @@ -64,6 +65,7 @@ public Object evaluate(EvaluationContext context) throws Exception { return null; } + @Override public String getExpressionSymbol() { return "-"; } @@ -80,11 +82,12 @@ public static BooleanExpression createInExpression(PropertyExpression right, Lis } else if (elements.size() < 5) { t = elements; } else { - t = new HashSet(elements); + t = new HashSet<>(elements); } final Collection inList = t; return new UnaryInExpression(right, UnaryType.IN, inList, not) { + @Override public Object evaluate(EvaluationContext context) throws Exception { Object rvalue = right.evaluate(context); @@ -103,8 +106,9 @@ public Object evaluate(EvaluationContext context) throws Exception { } + @Override public String toString() { - StringBuffer answer = new StringBuffer(); + StringBuilder answer = new StringBuilder(); answer.append(right); answer.append(" "); answer.append(getExpressionSymbol()); @@ -124,6 +128,7 @@ public String toString() { return answer.toString(); } + @Override public String getExpressionSymbol() { if (not) { return "NOT IN"; @@ -139,6 +144,7 @@ public BooleanUnaryExpression(Expression left, UnaryType unaryType) { super(left, unaryType); } + @Override public boolean matches(EvaluationContext context) throws Exception { Object object = evaluate(context); return object != null && object == Boolean.TRUE; @@ -147,6 +153,7 @@ public boolean matches(EvaluationContext context) throws Exception { public static BooleanExpression createNOT(BooleanExpression left) { return new BooleanUnaryExpression(left, UnaryType.NOT) { + @Override public Object evaluate(EvaluationContext context) throws Exception { Boolean lvalue = (Boolean) right.evaluate(context); if (lvalue == null) { @@ -155,6 +162,7 @@ public Object evaluate(EvaluationContext context) throws Exception { return lvalue.booleanValue() ? Boolean.FALSE : Boolean.TRUE; } + @Override public String getExpressionSymbol() { return "NOT"; } @@ -163,6 +171,7 @@ public String getExpressionSymbol() { public static BooleanExpression createBooleanCast(Expression left) { return new BooleanUnaryExpression(left, UnaryType.BOOLEANCAST) { + @Override public Object evaluate(EvaluationContext context) throws Exception { Object rvalue = right.evaluate(context); if (rvalue == null) { @@ -174,10 +183,12 @@ public Object evaluate(EvaluationContext context) throws Exception { return ((Boolean) rvalue).booleanValue() ? Boolean.TRUE : Boolean.FALSE; } + @Override public String toString() { return right.toString(); } + @Override public String getExpressionSymbol() { return ""; } @@ -233,6 +244,7 @@ public void setUnaryType(UnaryType unaryType) { /** * @see Object#toString() */ + @Override public String toString() { return "(" + getExpressionSymbol() + " " + right.toString() + ")"; } @@ -240,6 +252,7 @@ public String toString() { /** * @see Object#hashCode() */ + @Override public int hashCode() { return toString().hashCode(); } @@ -247,6 +260,7 @@ public int hashCode() { /** * @see Object#equals(Object) */ + @Override public boolean equals(Object o) { if (o == null || !this.getClass().equals(o.getClass())) { diff --git a/filter/src/main/java/org/apache/rocketmq/filter/parser/ParseException.java b/filter/src/main/java/org/apache/rocketmq/filter/parser/ParseException.java index 06014cbcbed..39762509e0d 100644 --- a/filter/src/main/java/org/apache/rocketmq/filter/parser/ParseException.java +++ b/filter/src/main/java/org/apache/rocketmq/filter/parser/ParseException.java @@ -106,7 +106,7 @@ private static String initialise(Token currentToken, int[][] expectedTokenSequences, String[] tokenImage) { String eol = System.getProperty("line.separator", "\n"); - StringBuffer expected = new StringBuffer(); + StringBuilder expected = new StringBuilder(); int maxSize = 0; for (int i = 0; i < expectedTokenSequences.length; i++) { if (maxSize < expectedTokenSequences[i].length) { @@ -123,8 +123,9 @@ private static String initialise(Token currentToken, String retval = "Encountered \""; Token tok = currentToken.next; for (int i = 0; i < maxSize; i++) { - if (i != 0) + if (i != 0) { retval += " "; + } if (tok.kind == 0) { retval += tokenImage[0]; break; @@ -157,7 +158,7 @@ private static String initialise(Token currentToken, * string literal. */ static String add_escapes(String str) { - StringBuffer retval = new StringBuffer(); + StringBuilder retval = new StringBuilder(); char ch; for (int i = 0; i < str.length(); i++) { switch (str.charAt(i)) { @@ -201,4 +202,4 @@ static String add_escapes(String str) { } } -/* JavaCC - OriginalChecksum=4c829b0daa2c9af00ddafe2441eb9097 (do not edit this line) */ +/* JavaCC - OriginalChecksum=60cf9c227a487e4be49599bc903f0a6a (do not edit this line) */ diff --git a/filter/src/main/java/org/apache/rocketmq/filter/parser/SelectorParser.java b/filter/src/main/java/org/apache/rocketmq/filter/parser/SelectorParser.java index 198aacf83e2..0aaf2bc01a0 100644 --- a/filter/src/main/java/org/apache/rocketmq/filter/parser/SelectorParser.java +++ b/filter/src/main/java/org/apache/rocketmq/filter/parser/SelectorParser.java @@ -20,6 +20,7 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; +import org.apache.rocketmq.filter.expression.BooleanConstantExpression; import org.apache.rocketmq.filter.expression.BooleanExpression; import org.apache.rocketmq.filter.expression.ComparisonExpression; import org.apache.rocketmq.filter.expression.ConstantExpression; @@ -40,10 +41,10 @@ public class SelectorParser implements SelectorParserConstants { private static final Cache PARSE_CACHE = CacheBuilder.newBuilder().maximumSize(100).build(); - // private static final String CONVERT_STRING_EXPRESSIONS_PREFIX = "convert_string_expressions:"; +// private static final String CONVERT_STRING_EXPRESSIONS_PREFIX = "convert_string_expressions:"; public static BooleanExpression parse(String sql) throws MQFilterException { - // sql = "("+sql+")"; +// sql = "("+sql+")"; Object result = PARSE_CACHE.getIfPresent(sql); if (result instanceof MQFilterException) { throw (MQFilterException) result; @@ -51,14 +52,14 @@ public static BooleanExpression parse(String sql) throws MQFilterException { return (BooleanExpression) result; } else { - // boolean convertStringExpressions = false; - // if( sql.startsWith(CONVERT_STRING_EXPRESSIONS_PREFIX)) { - // convertStringExpressions = true; - // sql = sql.substring(CONVERT_STRING_EXPRESSIONS_PREFIX.length()); - // } - // if( convertStringExpressions ) { - // ComparisonExpression.CONVERT_STRING_EXPRESSIONS.set(true); - // } +// boolean convertStringExpressions = false; +// if( sql.startsWith(CONVERT_STRING_EXPRESSIONS_PREFIX)) { +// convertStringExpressions = true; +// sql = sql.substring(CONVERT_STRING_EXPRESSIONS_PREFIX.length()); +// } +// if( convertStringExpressions ) { +// ComparisonExpression.CONVERT_STRING_EXPRESSIONS.set(true); +// } ComparisonExpression.CONVERT_STRING_EXPRESSIONS.set(true); try { @@ -70,9 +71,9 @@ public static BooleanExpression parse(String sql) throws MQFilterException { throw t; } finally { ComparisonExpression.CONVERT_STRING_EXPRESSIONS.remove(); - // if( convertStringExpressions ) { - // ComparisonExpression.CONVERT_STRING_EXPRESSIONS.remove(); - // } +// if( convertStringExpressions ) { +// ComparisonExpression.CONVERT_STRING_EXPRESSIONS.remove(); +// } } } } @@ -107,14 +108,13 @@ private BooleanExpression asBooleanExpression(Expression value) throws ParseExce } // ---------------------------------------------------------------------------- - // Grammer + // Grammar // ---------------------------------------------------------------------------- final public BooleanExpression JmsSelector() throws ParseException { Expression left = null; left = orExpression(); { - if (true) - return asBooleanExpression(left); + if (true) return asBooleanExpression(left); } throw new Error("Missing return statement in function"); } @@ -137,8 +137,7 @@ final public Expression orExpression() throws ParseException { left = LogicExpression.createOR(asBooleanExpression(left), asBooleanExpression(right)); } { - if (true) - return left; + if (true) return left; } throw new Error("Missing return statement in function"); } @@ -161,8 +160,7 @@ final public Expression andExpression() throws ParseException { left = LogicExpression.createAND(asBooleanExpression(left), asBooleanExpression(right)); } { - if (true) - return left; + if (true) return left; } throw new Error("Missing return statement in function"); } @@ -175,21 +173,21 @@ final public Expression equalityExpression() throws ParseException { while (true) { switch ((jjNtk == -1) ? jj_ntk() : jjNtk) { case IS: - case 22: - case 23: + case 25: + case 26: break; default: jjLa1[2] = jjGen; break label_3; } switch ((jjNtk == -1) ? jj_ntk() : jjNtk) { - case 22: - jj_consume_token(22); + case 25: + jj_consume_token(25); right = comparisonExpression(); left = ComparisonExpression.createEqual(left, right); break; - case 23: - jj_consume_token(23); + case 26: + jj_consume_token(26); right = comparisonExpression(); left = ComparisonExpression.createNotEqual(left, right); break; @@ -216,8 +214,7 @@ final public Expression equalityExpression() throws ParseException { } } { - if (true) - return left; + if (true) return left; } throw new Error("Missing return statement in function"); } @@ -237,111 +234,161 @@ final public Expression comparisonExpression() throws ParseException { case NOT: case BETWEEN: case IN: - case 24: - case 25: - case 26: + case CONTAINS: + case STARTSWITH: + case ENDSWITH: case 27: + case 28: + case 29: + case 30: break; default: jjLa1[5] = jjGen; break label_4; } switch ((jjNtk == -1) ? jj_ntk() : jjNtk) { - case 24: - jj_consume_token(24); + case 27: + jj_consume_token(27); right = unaryExpr(); left = ComparisonExpression.createGreaterThan(left, right); break; - case 25: - jj_consume_token(25); + case 28: + jj_consume_token(28); right = unaryExpr(); left = ComparisonExpression.createGreaterThanEqual(left, right); break; - case 26: - jj_consume_token(26); + case 29: + jj_consume_token(29); right = unaryExpr(); left = ComparisonExpression.createLessThan(left, right); break; - case 27: - jj_consume_token(27); + case 30: + jj_consume_token(30); right = unaryExpr(); left = ComparisonExpression.createLessThanEqual(left, right); break; - case BETWEEN: - jj_consume_token(BETWEEN); - low = unaryExpr(); - jj_consume_token(AND); - high = unaryExpr(); - left = ComparisonExpression.createBetween(left, low, high); + case CONTAINS: + jj_consume_token(CONTAINS); + t = stringLitteral(); + left = ComparisonExpression.createContains(left, t); break; default: jjLa1[8] = jjGen; if (jj_2_2(2)) { jj_consume_token(NOT); - jj_consume_token(BETWEEN); - low = unaryExpr(); - jj_consume_token(AND); - high = unaryExpr(); - left = ComparisonExpression.createNotBetween(left, low, high); + jj_consume_token(CONTAINS); + t = stringLitteral(); + left = ComparisonExpression.createNotContains(left, t); } else { switch ((jjNtk == -1) ? jj_ntk() : jjNtk) { - case IN: - jj_consume_token(IN); - jj_consume_token(28); + case STARTSWITH: + jj_consume_token(STARTSWITH); t = stringLitteral(); - list = new ArrayList(); - list.add(t); - label_5: - while (true) { - switch ((jjNtk == -1) ? jj_ntk() : jjNtk) { - case 29: - break; - default: - jjLa1[6] = jjGen; - break label_5; - } - jj_consume_token(29); - t = stringLitteral(); - list.add(t); - } - jj_consume_token(30); - left = ComparisonExpression.createInFilter(left, list); + left = ComparisonExpression.createStartsWith(left, t); break; default: jjLa1[9] = jjGen; if (jj_2_3(2)) { jj_consume_token(NOT); - jj_consume_token(IN); - jj_consume_token(28); + jj_consume_token(STARTSWITH); t = stringLitteral(); - list = new ArrayList(); - list.add(t); - label_6: - while (true) { - switch ((jjNtk == -1) ? jj_ntk() : jjNtk) { - case 29: - break; - default: - jjLa1[7] = jjGen; - break label_6; - } - jj_consume_token(29); - t = stringLitteral(); - list.add(t); - } - jj_consume_token(30); - left = ComparisonExpression.createNotInFilter(left, list); + left = ComparisonExpression.createNotStartsWith(left, t); } else { - jj_consume_token(-1); - throw new ParseException(); + switch ((jjNtk == -1) ? jj_ntk() : jjNtk) { + case ENDSWITH: + jj_consume_token(ENDSWITH); + t = stringLitteral(); + left = ComparisonExpression.createEndsWith(left, t); + break; + default: + jjLa1[10] = jjGen; + if (jj_2_4(2)) { + jj_consume_token(NOT); + jj_consume_token(ENDSWITH); + t = stringLitteral(); + left = ComparisonExpression.createNotEndsWith(left, t); + } else { + switch ((jjNtk == -1) ? jj_ntk() : jjNtk) { + case BETWEEN: + jj_consume_token(BETWEEN); + low = unaryExpr(); + jj_consume_token(AND); + high = unaryExpr(); + left = ComparisonExpression.createBetween(left, low, high); + break; + default: + jjLa1[11] = jjGen; + if (jj_2_5(2)) { + jj_consume_token(NOT); + jj_consume_token(BETWEEN); + low = unaryExpr(); + jj_consume_token(AND); + high = unaryExpr(); + left = ComparisonExpression.createNotBetween(left, low, high); + } else { + switch ((jjNtk == -1) ? jj_ntk() : jjNtk) { + case IN: + jj_consume_token(IN); + jj_consume_token(31); + t = stringLitteral(); + list = new ArrayList(); + list.add(t); + label_5: + while (true) { + switch ((jjNtk == -1) ? jj_ntk() : jjNtk) { + case 32: + break; + default: + jjLa1[6] = jjGen; + break label_5; + } + jj_consume_token(32); + t = stringLitteral(); + list.add(t); + } + jj_consume_token(33); + left = ComparisonExpression.createInFilter(left, list); + break; + default: + jjLa1[12] = jjGen; + if (jj_2_6(2)) { + jj_consume_token(NOT); + jj_consume_token(IN); + jj_consume_token(31); + t = stringLitteral(); + list = new ArrayList(); + list.add(t); + label_6: + while (true) { + switch ((jjNtk == -1) ? jj_ntk() : jjNtk) { + case 32: + break; + default: + jjLa1[7] = jjGen; + break label_6; + } + jj_consume_token(32); + t = stringLitteral(); + list.add(t); + } + jj_consume_token(33); + left = ComparisonExpression.createNotInFilter(left, list); + } else { + jj_consume_token(-1); + throw new ParseException(); + } + } + } + } + } + } } } } } } { - if (true) - return left; + if (true) return left; } throw new Error("Missing return statement in function"); } @@ -349,13 +396,13 @@ final public Expression comparisonExpression() throws ParseException { final public Expression unaryExpr() throws ParseException { String s = null; Expression left = null; - if (jj_2_4(2147483647)) { - jj_consume_token(31); + if (jj_2_7(2147483647)) { + jj_consume_token(34); left = unaryExpr(); } else { switch ((jjNtk == -1) ? jj_ntk() : jjNtk) { - case 32: - jj_consume_token(32); + case 35: + jj_consume_token(35); left = unaryExpr(); left = UnaryExpression.createNegate(left); break; @@ -371,18 +418,17 @@ final public Expression unaryExpr() throws ParseException { case FLOATING_POINT_LITERAL: case STRING_LITERAL: case ID: - case 28: + case 31: left = primaryExpr(); break; default: - jjLa1[10] = jjGen; + jjLa1[13] = jjGen; jj_consume_token(-1); throw new ParseException(); } } { - if (true) - return left; + if (true) return left; } throw new Error("Missing return statement in function"); } @@ -401,19 +447,18 @@ final public Expression primaryExpr() throws ParseException { case ID: left = variable(); break; - case 28: - jj_consume_token(28); + case 31: + jj_consume_token(31); left = orExpression(); - jj_consume_token(30); + jj_consume_token(33); break; default: - jjLa1[11] = jjGen; + jjLa1[14] = jjGen; jj_consume_token(-1); throw new ParseException(); } { - if (true) - return left; + if (true) return left; } throw new Error("Missing return statement in function"); } @@ -437,24 +482,23 @@ final public ConstantExpression literal() throws ParseException { break; case TRUE: jj_consume_token(TRUE); - left = ConstantExpression.TRUE; + left = BooleanConstantExpression.TRUE; break; case FALSE: jj_consume_token(FALSE); - left = ConstantExpression.FALSE; + left = BooleanConstantExpression.FALSE; break; case NULL: jj_consume_token(NULL); - left = ConstantExpression.NULL; + left = BooleanConstantExpression.NULL; break; default: - jjLa1[12] = jjGen; + jjLa1[15] = jjGen; jj_consume_token(-1); throw new ParseException(); } { - if (true) - return left; + if (true) return left; } throw new Error("Missing return statement in function"); } @@ -468,13 +512,12 @@ final public String stringLitteral() throws ParseException { String image = t.image; for (int i = 1; i < image.length() - 1; i++) { char c = image.charAt(i); - if (c == '\'') + if (c == '\u005c'') i++; rc.append(c); } { - if (true) - return rc.toString(); + if (true) return rc.toString(); } throw new Error("Missing return statement in function"); } @@ -485,8 +528,7 @@ final public PropertyExpression variable() throws ParseException { t = jj_consume_token(ID); left = new PropertyExpression(t.image); { - if (true) - return left; + if (true) return left; } throw new Error("Missing return statement in function"); } @@ -539,94 +581,53 @@ private boolean jj_2_4(int xla) { } } - private boolean jj_3R_7() { - Token xsp; - xsp = jjScanpos; - if (jj_3R_8()) { - jjScanpos = xsp; - if (jj_3R_9()) { - jjScanpos = xsp; - if (jj_3R_10()) { - jjScanpos = xsp; - if (jj_3R_11()) - return true; - } - } - } - return false; - } - - private boolean jj_3R_43() { - if (jj_scan_token(29)) - return true; - if (jj_3R_27()) - return true; - return false; - } - - private boolean jj_3R_24() { - if (jj_scan_token(NULL)) - return true; - return false; - } - - private boolean jj_3R_35() { - if (jj_scan_token(IS)) - return true; - if (jj_scan_token(NOT)) - return true; - if (jj_scan_token(NULL)) + private boolean jj_2_5(int xla) { + jjLa = xla; + jjLastpos = jjScanpos = token; + try { + return !jj_3_5(); + } catch (LookaheadSuccess ls) { return true; - return false; + } finally { + jj_save(4, xla); + } } - private boolean jj_3_1() { - if (jj_scan_token(IS)) - return true; - if (jj_scan_token(NULL)) + private boolean jj_2_6(int xla) { + jjLa = xla; + jjLastpos = jjScanpos = token; + try { + return !jj_3_6(); + } catch (LookaheadSuccess ls) { return true; - return false; + } finally { + jj_save(5, xla); + } } - private boolean jj_3R_23() { - if (jj_scan_token(FALSE)) + private boolean jj_2_7(int xla) { + jjLa = xla; + jjLastpos = jjScanpos = token; + try { + return !jj_3_7(); + } catch (LookaheadSuccess ls) { return true; - return false; + } finally { + jj_save(6, xla); + } } private boolean jj_3R_34() { - if (jj_scan_token(23)) - return true; - if (jj_3R_30()) - return true; + if (jj_scan_token(26)) return true; + if (jj_3R_30()) return true; return false; } - private boolean jj_3R_22() { - if (jj_scan_token(TRUE)) - return true; - return false; - } - - private boolean jj_3_3() { - if (jj_scan_token(NOT)) - return true; - if (jj_scan_token(IN)) - return true; - if (jj_scan_token(28)) - return true; - if (jj_3R_27()) - return true; - Token xsp; - while (true) { - xsp = jjScanpos; - if (jj_3R_43()) { - jjScanpos = xsp; - break; - } - } - if (jj_scan_token(30)) - return true; + private boolean jj_3R_43() { + if (jj_scan_token(BETWEEN)) return true; + if (jj_3R_7()) return true; + if (jj_scan_token(AND)) return true; + if (jj_3R_7()) return true; return false; } @@ -639,8 +640,7 @@ private boolean jj_3R_31() { jjScanpos = xsp; if (jj_3_1()) { jjScanpos = xsp; - if (jj_3R_35()) - return true; + if (jj_3R_35()) return true; } } } @@ -648,133 +648,134 @@ private boolean jj_3R_31() { } private boolean jj_3R_33() { - if (jj_scan_token(22)) - return true; - if (jj_3R_30()) - return true; + if (jj_scan_token(25)) return true; + if (jj_3R_30()) return true; return false; } - private boolean jj_3R_42() { - if (jj_scan_token(29)) - return true; - if (jj_3R_27()) - return true; + private boolean jj_3_4() { + if (jj_scan_token(NOT)) return true; + if (jj_scan_token(ENDSWITH)) return true; + if (jj_3R_27()) return true; return false; } - private boolean jj_3R_21() { - if (jj_scan_token(FLOATING_POINT_LITERAL)) - return true; + private boolean jj_3R_15() { + if (jj_scan_token(31)) return true; + if (jj_3R_18()) return true; + if (jj_scan_token(33)) return true; return false; } - private boolean jj_3R_20() { - if (jj_scan_token(DECIMAL_LITERAL)) - return true; + private boolean jj_3R_14() { + if (jj_3R_17()) return true; return false; } - private boolean jj_3R_28() { - if (jj_3R_30()) - return true; + private boolean jj_3R_13() { + if (jj_3R_16()) return true; + return false; + } + + private boolean jj_3R_42() { + if (jj_scan_token(ENDSWITH)) return true; + if (jj_3R_27()) return true; + return false; + } + + private boolean jj_3R_17() { + if (jj_scan_token(ID)) return true; + return false; + } + + private boolean jj_3R_12() { Token xsp; - while (true) { - xsp = jjScanpos; - if (jj_3R_31()) { + xsp = jjScanpos; + if (jj_3R_13()) { + jjScanpos = xsp; + if (jj_3R_14()) { jjScanpos = xsp; - break; + if (jj_3R_15()) return true; } } return false; } - private boolean jj_3R_41() { - if (jj_scan_token(IN)) - return true; - if (jj_scan_token(28)) - return true; - if (jj_3R_27()) - return true; + private boolean jj_3R_28() { + if (jj_3R_30()) return true; Token xsp; while (true) { xsp = jjScanpos; - if (jj_3R_42()) { + if (jj_3R_31()) { jjScanpos = xsp; break; } } - if (jj_scan_token(30)) - return true; return false; } - private boolean jj_3R_19() { - if (jj_3R_27()) - return true; + private boolean jj_3_3() { + if (jj_scan_token(NOT)) return true; + if (jj_scan_token(STARTSWITH)) return true; + if (jj_3R_27()) return true; + return false; + } + + private boolean jj_3R_41() { + if (jj_scan_token(STARTSWITH)) return true; + if (jj_3R_27()) return true; + return false; + } + + private boolean jj_3R_11() { + if (jj_3R_12()) return true; return false; } private boolean jj_3R_29() { - if (jj_scan_token(AND)) - return true; - if (jj_3R_28()) - return true; + if (jj_scan_token(AND)) return true; + if (jj_3R_28()) return true; return false; } - private boolean jj_3R_16() { - Token xsp; - xsp = jjScanpos; - if (jj_3R_19()) { - jjScanpos = xsp; - if (jj_3R_20()) { - jjScanpos = xsp; - if (jj_3R_21()) { - jjScanpos = xsp; - if (jj_3R_22()) { - jjScanpos = xsp; - if (jj_3R_23()) { - jjScanpos = xsp; - if (jj_3R_24()) - return true; - } - } - } - } - } + private boolean jj_3_7() { + if (jj_scan_token(34)) return true; + if (jj_3R_7()) return true; return false; } private boolean jj_3_2() { - if (jj_scan_token(NOT)) - return true; - if (jj_scan_token(BETWEEN)) - return true; - if (jj_3R_7()) - return true; - if (jj_scan_token(AND)) - return true; - if (jj_3R_7()) - return true; + if (jj_scan_token(NOT)) return true; + if (jj_scan_token(CONTAINS)) return true; + if (jj_3R_27()) return true; + return false; + } + + private boolean jj_3R_10() { + if (jj_scan_token(NOT)) return true; + if (jj_3R_7()) return true; return false; } private boolean jj_3R_40() { - if (jj_scan_token(BETWEEN)) - return true; - if (jj_3R_7()) - return true; - if (jj_scan_token(AND)) - return true; - if (jj_3R_7()) - return true; + if (jj_scan_token(CONTAINS)) return true; + if (jj_3R_27()) return true; + return false; + } + + private boolean jj_3R_9() { + if (jj_scan_token(35)) return true; + if (jj_3R_7()) return true; + return false; + } + + private boolean jj_3R_27() { + if (jj_scan_token(STRING_LITERAL)) return true; return false; } private boolean jj_3R_25() { - if (jj_3R_28()) - return true; + if (jj_3R_28()) return true; Token xsp; while (true) { xsp = jjScanpos; @@ -786,77 +787,66 @@ private boolean jj_3R_25() { return false; } - private boolean jj_3R_39() { - if (jj_scan_token(27)) - return true; - if (jj_3R_7()) - return true; + private boolean jj_3R_8() { + if (jj_scan_token(34)) return true; + if (jj_3R_7()) return true; return false; } - private boolean jj_3R_15() { - if (jj_scan_token(28)) - return true; - if (jj_3R_18()) - return true; - if (jj_scan_token(30)) - return true; + private boolean jj_3R_39() { + if (jj_scan_token(30)) return true; + if (jj_3R_7()) return true; return false; } - private boolean jj_3R_14() { - if (jj_3R_17()) - return true; + private boolean jj_3R_7() { + Token xsp; + xsp = jjScanpos; + if (jj_3R_8()) { + jjScanpos = xsp; + if (jj_3R_9()) { + jjScanpos = xsp; + if (jj_3R_10()) { + jjScanpos = xsp; + if (jj_3R_11()) return true; + } + } + } return false; } private boolean jj_3R_38() { - if (jj_scan_token(26)) - return true; - if (jj_3R_7()) - return true; + if (jj_scan_token(29)) return true; + if (jj_3R_7()) return true; return false; } - private boolean jj_3R_13() { - if (jj_3R_16()) - return true; + private boolean jj_3R_46() { + if (jj_scan_token(32)) return true; + if (jj_3R_27()) return true; return false; } private boolean jj_3R_26() { - if (jj_scan_token(OR)) - return true; - if (jj_3R_25()) - return true; + if (jj_scan_token(OR)) return true; + if (jj_3R_25()) return true; return false; } - private boolean jj_3R_17() { - if (jj_scan_token(ID)) - return true; + private boolean jj_3R_37() { + if (jj_scan_token(28)) return true; + if (jj_3R_7()) return true; return false; } - private boolean jj_3R_37() { - if (jj_scan_token(25)) - return true; - if (jj_3R_7()) - return true; + private boolean jj_3R_24() { + if (jj_scan_token(NULL)) return true; return false; } - private boolean jj_3R_12() { - Token xsp; - xsp = jjScanpos; - if (jj_3R_13()) { - jjScanpos = xsp; - if (jj_3R_14()) { - jjScanpos = xsp; - if (jj_3R_15()) - return true; - } - } + private boolean jj_3R_36() { + if (jj_scan_token(27)) return true; + if (jj_3R_7()) return true; return false; } @@ -877,8 +867,25 @@ private boolean jj_3R_32() { jjScanpos = xsp; if (jj_3R_41()) { jjScanpos = xsp; - if (jj_3_3()) - return true; + if (jj_3_3()) { + jjScanpos = xsp; + if (jj_3R_42()) { + jjScanpos = xsp; + if (jj_3_4()) { + jjScanpos = xsp; + if (jj_3R_43()) { + jjScanpos = xsp; + if (jj_3_5()) { + jjScanpos = xsp; + if (jj_3R_44()) { + jjScanpos = xsp; + if (jj_3_6()) return true; + } + } + } + } + } + } } } } @@ -889,83 +896,137 @@ private boolean jj_3R_32() { return false; } - private boolean jj_3R_36() { - if (jj_scan_token(24)) - return true; - if (jj_3R_7()) - return true; + private boolean jj_3R_23() { + if (jj_scan_token(FALSE)) return true; return false; } - private boolean jj_3R_11() { - if (jj_3R_12()) - return true; + private boolean jj_3R_18() { + if (jj_3R_25()) return true; + Token xsp; + while (true) { + xsp = jjScanpos; + if (jj_3R_26()) { + jjScanpos = xsp; + break; + } + } return false; } - private boolean jj_3R_18() { - if (jj_3R_25()) - return true; + private boolean jj_3R_22() { + if (jj_scan_token(TRUE)) return true; + return false; + } + + private boolean jj_3_6() { + if (jj_scan_token(NOT)) return true; + if (jj_scan_token(IN)) return true; + if (jj_scan_token(31)) return true; + if (jj_3R_27()) return true; Token xsp; while (true) { xsp = jjScanpos; - if (jj_3R_26()) { + if (jj_3R_46()) { jjScanpos = xsp; break; } } + if (jj_scan_token(33)) return true; return false; } - private boolean jj_3_4() { - if (jj_scan_token(31)) - return true; - if (jj_3R_7()) - return true; + private boolean jj_3R_45() { + if (jj_scan_token(32)) return true; + if (jj_3R_27()) return true; return false; } - private boolean jj_3R_10() { - if (jj_scan_token(NOT)) - return true; - if (jj_3R_7()) - return true; + private boolean jj_3R_30() { + if (jj_3R_7()) return true; + Token xsp; + while (true) { + xsp = jjScanpos; + if (jj_3R_32()) { + jjScanpos = xsp; + break; + } + } return false; } - private boolean jj_3R_9() { - if (jj_scan_token(32)) - return true; - if (jj_3R_7()) - return true; + private boolean jj_3R_21() { + if (jj_scan_token(FLOATING_POINT_LITERAL)) return true; return false; } - private boolean jj_3R_27() { - if (jj_scan_token(STRING_LITERAL)) - return true; + private boolean jj_3R_20() { + if (jj_scan_token(DECIMAL_LITERAL)) return true; return false; } - private boolean jj_3R_30() { - if (jj_3R_7()) - return true; + private boolean jj_3R_35() { + if (jj_scan_token(IS)) return true; + if (jj_scan_token(NOT)) return true; + if (jj_scan_token(NULL)) return true; + return false; + } + + private boolean jj_3R_44() { + if (jj_scan_token(IN)) return true; + if (jj_scan_token(31)) return true; + if (jj_3R_27()) return true; Token xsp; while (true) { xsp = jjScanpos; - if (jj_3R_32()) { + if (jj_3R_45()) { jjScanpos = xsp; break; } } + if (jj_scan_token(33)) return true; return false; } - private boolean jj_3R_8() { - if (jj_scan_token(31)) - return true; - if (jj_3R_7()) - return true; + private boolean jj_3R_19() { + if (jj_3R_27()) return true; + return false; + } + + private boolean jj_3_1() { + if (jj_scan_token(IS)) return true; + if (jj_scan_token(NULL)) return true; + return false; + } + + private boolean jj_3R_16() { + Token xsp; + xsp = jjScanpos; + if (jj_3R_19()) { + jjScanpos = xsp; + if (jj_3R_20()) { + jjScanpos = xsp; + if (jj_3R_21()) { + jjScanpos = xsp; + if (jj_3R_22()) { + jjScanpos = xsp; + if (jj_3R_23()) { + jjScanpos = xsp; + if (jj_3R_24()) return true; + } + } + } + } + } + return false; + } + + private boolean jj_3_5() { + if (jj_scan_token(NOT)) return true; + if (jj_scan_token(BETWEEN)) return true; + if (jj_3R_7()) return true; + if (jj_scan_token(AND)) return true; + if (jj_3R_7()) return true; return false; } @@ -986,7 +1047,7 @@ private boolean jj_3R_8() { private Token jjScanpos, jjLastpos; private int jjLa; private int jjGen; - final private int[] jjLa1 = new int[13]; + final private int[] jjLa1 = new int[16]; static private int[] jjLa10; static private int[] jjLa11; @@ -996,16 +1057,14 @@ private boolean jj_3R_8() { } private static void jj_la1_init_0() { - jjLa10 = new int[] { - 0x400, 0x200, 0xc10000, 0xc00000, 0x10000, 0xf001900, 0x20000000, 0x20000000, 0xf000800, - 0x1000, 0x1036e100, 0x1036e000, 0x16e000}; + jjLa10 = new int[]{0x400, 0x200, 0x6010000, 0x6000000, 0x10000, 0x780e1900, 0x0, 0x0, 0x78020000, 0x40000, 0x80000, 0x800, 0x1000, 0x81b0e100, 0x81b0e000, 0xb0e000,}; } private static void jj_la1_init_1() { - jjLa11 = new int[] {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0}; + jjLa11 = new int[]{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0,}; } - final private JJCalls[] jj2Rtns = new JJCalls[4]; + final private JJCalls[] jj2Rtns = new JJCalls[7]; private boolean jjRescan = false; private int jjGc = 0; @@ -1029,10 +1088,8 @@ public SelectorParser(java.io.InputStream stream, String encoding) { token = new Token(); jjNtk = -1; jjGen = 0; - for (int i = 0; i < 13; i++) - jjLa1[i] = -1; - for (int i = 0; i < jj2Rtns.length; i++) - jj2Rtns[i] = new JJCalls(); + for (int i = 0; i < 16; i++) jjLa1[i] = -1; + for (int i = 0; i < jj2Rtns.length; i++) jj2Rtns[i] = new JJCalls(); } /** @@ -1055,10 +1112,8 @@ public void ReInit(java.io.InputStream stream, String encoding) { token = new Token(); jjNtk = -1; jjGen = 0; - for (int i = 0; i < 13; i++) - jjLa1[i] = -1; - for (int i = 0; i < jj2Rtns.length; i++) - jj2Rtns[i] = new JJCalls(); + for (int i = 0; i < 16; i++) jjLa1[i] = -1; + for (int i = 0; i < jj2Rtns.length; i++) jj2Rtns[i] = new JJCalls(); } /** @@ -1070,10 +1125,8 @@ public SelectorParser(java.io.Reader stream) { token = new Token(); jjNtk = -1; jjGen = 0; - for (int i = 0; i < 13; i++) - jjLa1[i] = -1; - for (int i = 0; i < jj2Rtns.length; i++) - jj2Rtns[i] = new JJCalls(); + for (int i = 0; i < 16; i++) jjLa1[i] = -1; + for (int i = 0; i < jj2Rtns.length; i++) jj2Rtns[i] = new JJCalls(); } /** @@ -1085,10 +1138,8 @@ public void ReInit(java.io.Reader stream) { token = new Token(); jjNtk = -1; jjGen = 0; - for (int i = 0; i < 13; i++) - jjLa1[i] = -1; - for (int i = 0; i < jj2Rtns.length; i++) - jj2Rtns[i] = new JJCalls(); + for (int i = 0; i < 16; i++) jjLa1[i] = -1; + for (int i = 0; i < jj2Rtns.length; i++) jj2Rtns[i] = new JJCalls(); } /** @@ -1099,10 +1150,8 @@ public SelectorParser(SelectorParserTokenManager tm) { token = new Token(); jjNtk = -1; jjGen = 0; - for (int i = 0; i < 13; i++) - jjLa1[i] = -1; - for (int i = 0; i < jj2Rtns.length; i++) - jj2Rtns[i] = new JJCalls(); + for (int i = 0; i < 16; i++) jjLa1[i] = -1; + for (int i = 0; i < jj2Rtns.length; i++) jj2Rtns[i] = new JJCalls(); } /** @@ -1113,18 +1162,14 @@ public void ReInit(SelectorParserTokenManager tm) { token = new Token(); jjNtk = -1; jjGen = 0; - for (int i = 0; i < 13; i++) - jjLa1[i] = -1; - for (int i = 0; i < jj2Rtns.length; i++) - jj2Rtns[i] = new JJCalls(); + for (int i = 0; i < 16; i++) jjLa1[i] = -1; + for (int i = 0; i < jj2Rtns.length; i++) jj2Rtns[i] = new JJCalls(); } private Token jj_consume_token(int kind) throws ParseException { Token oldToken; - if ((oldToken = token).next != null) - token = token.next; - else - token = token.next = tokenSource.getNextToken(); + if ((oldToken = token).next != null) token = token.next; + else token = token.next = tokenSource.getNextToken(); jjNtk = -1; if (token.kind == kind) { jjGen++; @@ -1133,8 +1178,7 @@ private Token jj_consume_token(int kind) throws ParseException { for (int i = 0; i < jj2Rtns.length; i++) { JJCalls c = jj2Rtns[i]; while (c != null) { - if (c.gen < jjGen) - c.first = null; + if (c.gen < jjGen) c.first = null; c = c.next; } } @@ -1169,24 +1213,20 @@ private boolean jj_scan_token(int kind) { i++; tok = tok.next; } - if (tok != null) - jj_add_error_token(kind, i); + if (tok != null) jj_add_error_token(kind, i); } - if (jjScanpos.kind != kind) - return true; - if (jjLa == 0 && jjScanpos == jjLastpos) - throw jjLs; + if (jjScanpos.kind != kind) return true; + if (jjLa == 0 && jjScanpos == jjLastpos) throw jjLs; return false; } + /** * Get the next Token. */ final public Token getNextToken() { - if (token.next != null) - token = token.next; - else - token = token.next = tokenSource.getNextToken(); + if (token.next != null) token = token.next; + else token = token.next = tokenSource.getNextToken(); jjNtk = -1; jjGen++; return token; @@ -1198,10 +1238,8 @@ final public Token getNextToken() { final public Token getToken(int index) { Token t = token; for (int i = 0; i < index; i++) { - if (t.next != null) - t = t.next; - else - t = t.next = tokenSource.getNextToken(); + if (t.next != null) t = t.next; + else t = t.next = tokenSource.getNextToken(); } return t; } @@ -1213,15 +1251,14 @@ private int jj_ntk() { return jjNtk = jjNt.kind; } - private java.util.List jjExpentries = new java.util.ArrayList(); + private java.util.List jjExpentries = new java.util.ArrayList<>(); private int[] jjExpentry; private int jjKind = -1; private int[] jjLasttokens = new int[100]; private int jjEndpos; private void jj_add_error_token(int kind, int pos) { - if (pos >= 100) - return; + if (pos >= 100) return; if (pos == jjEndpos + 1) { jjLasttokens[jjEndpos++] = kind; } else if (jjEndpos != 0) { @@ -1229,21 +1266,22 @@ private void jj_add_error_token(int kind, int pos) { for (int i = 0; i < jjEndpos; i++) { jjExpentry[i] = jjLasttokens[i]; } - jj_entries_loop: + boolean exists = false; for (java.util.Iterator it = jjExpentries.iterator(); it.hasNext(); ) { + exists = true; int[] oldentry = (int[]) (it.next()); if (oldentry.length == jjExpentry.length) { for (int i = 0; i < jjExpentry.length; i++) { if (oldentry[i] != jjExpentry[i]) { - continue jj_entries_loop; + exists = false; + break; } } - jjExpentries.add(jjExpentry); - break jj_entries_loop; + if (exists) break; } } - if (pos != 0) - jjLasttokens[(jjEndpos = pos) - 1] = kind; + if (!exists) jjExpentries.add(jjExpentry); + if (pos != 0) jjLasttokens[(jjEndpos = pos) - 1] = kind; } } @@ -1252,12 +1290,12 @@ private void jj_add_error_token(int kind, int pos) { */ public ParseException generateParseException() { jjExpentries.clear(); - boolean[] la1tokens = new boolean[33]; + boolean[] la1tokens = new boolean[36]; if (jjKind >= 0) { la1tokens[jjKind] = true; jjKind = -1; } - for (int i = 0; i < 13; i++) { + for (int i = 0; i < 16; i++) { if (jjLa1[i] == jjGen) { for (int j = 0; j < 32; j++) { if ((jjLa10[i] & (1 << j)) != 0) { @@ -1269,7 +1307,7 @@ public ParseException generateParseException() { } } } - for (int i = 0; i < 33; i++) { + for (int i = 0; i < 36; i++) { if (la1tokens[i]) { jjExpentry = new int[1]; jjExpentry[0] = i; @@ -1300,7 +1338,7 @@ final public void disable_tracing() { private void jj_rescan_token() { jjRescan = true; - for (int i = 0; i < 4; i++) { + for (int i = 0; i < 7; i++) { try { JJCalls p = jj2Rtns[i]; do { @@ -1320,11 +1358,19 @@ private void jj_rescan_token() { case 3: jj_3_4(); break; + case 4: + jj_3_5(); + break; + case 5: + jj_3_6(); + break; + case 6: + jj_3_7(); + break; } } p = p.next; - } - while (p != null); + } while (p != null); } catch (LookaheadSuccess ls) { } } diff --git a/filter/src/main/java/org/apache/rocketmq/filter/parser/SelectorParser.jj b/filter/src/main/java/org/apache/rocketmq/filter/parser/SelectorParser.jj index b533ac1778d..f2636969276 100644 --- a/filter/src/main/java/org/apache/rocketmq/filter/parser/SelectorParser.jj +++ b/filter/src/main/java/org/apache/rocketmq/filter/parser/SelectorParser.jj @@ -53,7 +53,6 @@ import org.apache.rocketmq.filter.expression.LogicExpression; import org.apache.rocketmq.filter.expression.MQFilterException; import org.apache.rocketmq.filter.expression.PropertyExpression; import org.apache.rocketmq.filter.expression.UnaryExpression; -import org.apache.rocketmq.filter.util.LRUCache; import java.io.StringReader; import java.util.ArrayList; @@ -170,6 +169,9 @@ TOKEN [IGNORE_CASE] : | < FALSE : "FALSE" > | < NULL : "NULL" > | < IS : "IS" > + | < CONTAINS : "CONTAINS"> + | < STARTSWITH : "STARTSWITH"> + | < ENDSWITH : "ENDSWITH"> } /* Literals */ @@ -193,7 +195,7 @@ TOKEN [IGNORE_CASE] : } // ---------------------------------------------------------------------------- -// Grammer +// Grammar // ---------------------------------------------------------------------------- BooleanExpression JmsSelector() : { @@ -322,6 +324,39 @@ Expression comparisonExpression() : { left = ComparisonExpression.createLessThanEqual(left, right); } + | + t = stringLitteral() + { + left = ComparisonExpression.createContains(left, t); + } + | + LOOKAHEAD(2) + t = stringLitteral() + { + left = ComparisonExpression.createNotContains(left, t); + } + | + t = stringLitteral() + { + left = ComparisonExpression.createStartsWith(left, t); + } + | + LOOKAHEAD(2) + t = stringLitteral() + { + left = ComparisonExpression.createNotStartsWith(left, t); + } + | + t = stringLitteral() + { + left = ComparisonExpression.createEndsWith(left, t); + } + | + LOOKAHEAD(2) + t = stringLitteral() + { + left = ComparisonExpression.createNotEndsWith(left, t); + } | low = unaryExpr() high = unaryExpr() { diff --git a/filter/src/main/java/org/apache/rocketmq/filter/parser/SelectorParserConstants.java b/filter/src/main/java/org/apache/rocketmq/filter/parser/SelectorParserConstants.java index 915658ca60b..8f228be8bd3 100644 --- a/filter/src/main/java/org/apache/rocketmq/filter/parser/SelectorParserConstants.java +++ b/filter/src/main/java/org/apache/rocketmq/filter/parser/SelectorParserConstants.java @@ -75,23 +75,35 @@ public interface SelectorParserConstants { /** * RegularExpression Id. */ - int DECIMAL_LITERAL = 17; + int CONTAINS = 17; /** * RegularExpression Id. */ - int FLOATING_POINT_LITERAL = 18; + int STARTSWITH = 18; /** * RegularExpression Id. */ - int EXPONENT = 19; + int ENDSWITH = 19; /** * RegularExpression Id. */ - int STRING_LITERAL = 20; + int DECIMAL_LITERAL = 20; /** * RegularExpression Id. */ - int ID = 21; + int FLOATING_POINT_LITERAL = 21; + /** + * RegularExpression Id. + */ + int EXPONENT = 22; + /** + * RegularExpression Id. + */ + int STRING_LITERAL = 23; + /** + * RegularExpression Id. + */ + int ID = 24; /** * Lexical state. @@ -119,6 +131,9 @@ public interface SelectorParserConstants { "\"FALSE\"", "\"NULL\"", "\"IS\"", + "\"CONTAINS\"", + "\"STARTSWITH\"", + "\"ENDSWITH\"", "", "", "", diff --git a/filter/src/main/java/org/apache/rocketmq/filter/parser/SelectorParserTokenManager.java b/filter/src/main/java/org/apache/rocketmq/filter/parser/SelectorParserTokenManager.java index b5bac982405..6d9b8551730 100644 --- a/filter/src/main/java/org/apache/rocketmq/filter/parser/SelectorParserTokenManager.java +++ b/filter/src/main/java/org/apache/rocketmq/filter/parser/SelectorParserTokenManager.java @@ -59,33 +59,37 @@ private int jjMoveStringLiteralDfa0_0() { jjmatchedKind = 1; return jjMoveNfa_0(5, 0); case 40: - jjmatchedKind = 28; + jjmatchedKind = 31; return jjMoveNfa_0(5, 0); case 41: - jjmatchedKind = 30; + jjmatchedKind = 33; return jjMoveNfa_0(5, 0); case 43: - jjmatchedKind = 31; + jjmatchedKind = 34; return jjMoveNfa_0(5, 0); case 44: - jjmatchedKind = 29; + jjmatchedKind = 32; return jjMoveNfa_0(5, 0); case 45: - jjmatchedKind = 32; + jjmatchedKind = 35; return jjMoveNfa_0(5, 0); case 60: - jjmatchedKind = 26; - return jjMoveStringLiteralDfa1_0(0x8800000L); + jjmatchedKind = 29; + return jjMoveStringLiteralDfa1_0(0x44000000L); case 61: - jjmatchedKind = 22; + jjmatchedKind = 25; return jjMoveNfa_0(5, 0); case 62: - jjmatchedKind = 24; - return jjMoveStringLiteralDfa1_0(0x2000000L); + jjmatchedKind = 27; + return jjMoveStringLiteralDfa1_0(0x10000000L); case 65: return jjMoveStringLiteralDfa1_0(0x200L); case 66: return jjMoveStringLiteralDfa1_0(0x800L); + case 67: + return jjMoveStringLiteralDfa1_0(0x20000L); + case 69: + return jjMoveStringLiteralDfa1_0(0x80000L); case 70: return jjMoveStringLiteralDfa1_0(0x4000L); case 73: @@ -94,12 +98,18 @@ private int jjMoveStringLiteralDfa0_0() { return jjMoveStringLiteralDfa1_0(0x8100L); case 79: return jjMoveStringLiteralDfa1_0(0x400L); + case 83: + return jjMoveStringLiteralDfa1_0(0x40000L); case 84: return jjMoveStringLiteralDfa1_0(0x2000L); case 97: return jjMoveStringLiteralDfa1_0(0x200L); case 98: return jjMoveStringLiteralDfa1_0(0x800L); + case 99: + return jjMoveStringLiteralDfa1_0(0x20000L); + case 101: + return jjMoveStringLiteralDfa1_0(0x80000L); case 102: return jjMoveStringLiteralDfa1_0(0x4000L); case 105: @@ -108,6 +118,8 @@ private int jjMoveStringLiteralDfa0_0() { return jjMoveStringLiteralDfa1_0(0x8100L); case 111: return jjMoveStringLiteralDfa1_0(0x400L); + case 115: + return jjMoveStringLiteralDfa1_0(0x40000L); case 116: return jjMoveStringLiteralDfa1_0(0x2000L); default: @@ -123,17 +135,17 @@ private int jjMoveStringLiteralDfa1_0(long active0) { } switch (curChar) { case 61: - if ((active0 & 0x2000000L) != 0L) { - jjmatchedKind = 25; + if ((active0 & 0x10000000L) != 0L) { + jjmatchedKind = 28; jjmatchedPos = 1; - } else if ((active0 & 0x8000000L) != 0L) { - jjmatchedKind = 27; + } else if ((active0 & 0x40000000L) != 0L) { + jjmatchedKind = 30; jjmatchedPos = 1; } break; case 62: - if ((active0 & 0x800000L) != 0L) { - jjmatchedKind = 23; + if ((active0 & 0x4000000L) != 0L) { + jjmatchedKind = 26; jjmatchedPos = 1; } break; @@ -146,9 +158,9 @@ private int jjMoveStringLiteralDfa1_0(long active0) { jjmatchedKind = 12; jjmatchedPos = 1; } - return jjMoveStringLiteralDfa2_0(active0, 0x200L); + return jjMoveStringLiteralDfa2_0(active0, 0x80200L); case 79: - return jjMoveStringLiteralDfa2_0(active0, 0x100L); + return jjMoveStringLiteralDfa2_0(active0, 0x20100L); case 82: if ((active0 & 0x400L) != 0L) { jjmatchedKind = 10; @@ -161,6 +173,8 @@ private int jjMoveStringLiteralDfa1_0(long active0) { jjmatchedPos = 1; } break; + case 84: + return jjMoveStringLiteralDfa2_0(active0, 0x40000L); case 85: return jjMoveStringLiteralDfa2_0(active0, 0x8000L); case 97: @@ -172,9 +186,9 @@ private int jjMoveStringLiteralDfa1_0(long active0) { jjmatchedKind = 12; jjmatchedPos = 1; } - return jjMoveStringLiteralDfa2_0(active0, 0x200L); + return jjMoveStringLiteralDfa2_0(active0, 0x80200L); case 111: - return jjMoveStringLiteralDfa2_0(active0, 0x100L); + return jjMoveStringLiteralDfa2_0(active0, 0x20100L); case 114: if ((active0 & 0x400L) != 0L) { jjmatchedKind = 10; @@ -187,6 +201,8 @@ private int jjMoveStringLiteralDfa1_0(long active0) { jjmatchedPos = 1; } break; + case 116: + return jjMoveStringLiteralDfa2_0(active0, 0x40000L); case 117: return jjMoveStringLiteralDfa2_0(active0, 0x8000L); default: @@ -204,14 +220,18 @@ private int jjMoveStringLiteralDfa2_0(long old0, long active0) { return jjMoveNfa_0(5, 1); } switch (curChar) { + case 65: + return jjMoveStringLiteralDfa3_0(active0, 0x40000L); case 68: if ((active0 & 0x200L) != 0L) { jjmatchedKind = 9; jjmatchedPos = 2; } - break; + return jjMoveStringLiteralDfa3_0(active0, 0x80000L); case 76: return jjMoveStringLiteralDfa3_0(active0, 0xc000L); + case 78: + return jjMoveStringLiteralDfa3_0(active0, 0x20000L); case 84: if ((active0 & 0x100L) != 0L) { jjmatchedKind = 8; @@ -220,14 +240,18 @@ private int jjMoveStringLiteralDfa2_0(long old0, long active0) { return jjMoveStringLiteralDfa3_0(active0, 0x800L); case 85: return jjMoveStringLiteralDfa3_0(active0, 0x2000L); + case 97: + return jjMoveStringLiteralDfa3_0(active0, 0x40000L); case 100: if ((active0 & 0x200L) != 0L) { jjmatchedKind = 9; jjmatchedPos = 2; } - break; + return jjMoveStringLiteralDfa3_0(active0, 0x80000L); case 108: return jjMoveStringLiteralDfa3_0(active0, 0xc000L); + case 110: + return jjMoveStringLiteralDfa3_0(active0, 0x20000L); case 116: if ((active0 & 0x100L) != 0L) { jjmatchedKind = 8; @@ -263,8 +287,12 @@ private int jjMoveStringLiteralDfa3_0(long old0, long active0) { jjmatchedPos = 3; } break; + case 82: + return jjMoveStringLiteralDfa4_0(active0, 0x40000L); case 83: - return jjMoveStringLiteralDfa4_0(active0, 0x4000L); + return jjMoveStringLiteralDfa4_0(active0, 0x84000L); + case 84: + return jjMoveStringLiteralDfa4_0(active0, 0x20000L); case 87: return jjMoveStringLiteralDfa4_0(active0, 0x800L); case 101: @@ -279,8 +307,12 @@ private int jjMoveStringLiteralDfa3_0(long old0, long active0) { jjmatchedPos = 3; } break; + case 114: + return jjMoveStringLiteralDfa4_0(active0, 0x40000L); case 115: - return jjMoveStringLiteralDfa4_0(active0, 0x4000L); + return jjMoveStringLiteralDfa4_0(active0, 0x84000L); + case 116: + return jjMoveStringLiteralDfa4_0(active0, 0x20000L); case 119: return jjMoveStringLiteralDfa4_0(active0, 0x800L); default: @@ -298,18 +330,30 @@ private int jjMoveStringLiteralDfa4_0(long old0, long active0) { return jjMoveNfa_0(5, 3); } switch (curChar) { + case 65: + return jjMoveStringLiteralDfa5_0(active0, 0x20000L); case 69: if ((active0 & 0x4000L) != 0L) { jjmatchedKind = 14; jjmatchedPos = 4; } return jjMoveStringLiteralDfa5_0(active0, 0x800L); + case 84: + return jjMoveStringLiteralDfa5_0(active0, 0x40000L); + case 87: + return jjMoveStringLiteralDfa5_0(active0, 0x80000L); + case 97: + return jjMoveStringLiteralDfa5_0(active0, 0x20000L); case 101: if ((active0 & 0x4000L) != 0L) { jjmatchedKind = 14; jjmatchedPos = 4; } return jjMoveStringLiteralDfa5_0(active0, 0x800L); + case 116: + return jjMoveStringLiteralDfa5_0(active0, 0x40000L); + case 119: + return jjMoveStringLiteralDfa5_0(active0, 0x80000L); default: break; } @@ -327,8 +371,16 @@ private int jjMoveStringLiteralDfa5_0(long old0, long active0) { switch (curChar) { case 69: return jjMoveStringLiteralDfa6_0(active0, 0x800L); + case 73: + return jjMoveStringLiteralDfa6_0(active0, 0xa0000L); + case 83: + return jjMoveStringLiteralDfa6_0(active0, 0x40000L); case 101: return jjMoveStringLiteralDfa6_0(active0, 0x800L); + case 105: + return jjMoveStringLiteralDfa6_0(active0, 0xa0000L); + case 115: + return jjMoveStringLiteralDfa6_0(active0, 0x40000L); default: break; } @@ -349,19 +401,116 @@ private int jjMoveStringLiteralDfa6_0(long old0, long active0) { jjmatchedKind = 11; jjmatchedPos = 6; } - break; + return jjMoveStringLiteralDfa7_0(active0, 0x20000L); + case 84: + return jjMoveStringLiteralDfa7_0(active0, 0x80000L); + case 87: + return jjMoveStringLiteralDfa7_0(active0, 0x40000L); case 110: if ((active0 & 0x800L) != 0L) { jjmatchedKind = 11; jjmatchedPos = 6; } - break; + return jjMoveStringLiteralDfa7_0(active0, 0x20000L); + case 116: + return jjMoveStringLiteralDfa7_0(active0, 0x80000L); + case 119: + return jjMoveStringLiteralDfa7_0(active0, 0x40000L); default: break; } return jjMoveNfa_0(5, 6); } + private int jjMoveStringLiteralDfa7_0(long old0, long active0) { + if (((active0 &= old0)) == 0L) + return jjMoveNfa_0(5, 6); + try { + curChar = inputStream.readChar(); + } catch (java.io.IOException e) { + return jjMoveNfa_0(5, 6); + } + switch (curChar) { + case 72: + if ((active0 & 0x80000L) != 0L) { + jjmatchedKind = 19; + jjmatchedPos = 7; + } + break; + case 73: + return jjMoveStringLiteralDfa8_0(active0, 0x40000L); + case 83: + if ((active0 & 0x20000L) != 0L) { + jjmatchedKind = 17; + jjmatchedPos = 7; + } + break; + case 104: + if ((active0 & 0x80000L) != 0L) { + jjmatchedKind = 19; + jjmatchedPos = 7; + } + break; + case 105: + return jjMoveStringLiteralDfa8_0(active0, 0x40000L); + case 115: + if ((active0 & 0x20000L) != 0L) { + jjmatchedKind = 17; + jjmatchedPos = 7; + } + break; + default: + break; + } + return jjMoveNfa_0(5, 7); + } + + private int jjMoveStringLiteralDfa8_0(long old0, long active0) { + if (((active0 &= old0)) == 0L) + return jjMoveNfa_0(5, 7); + try { + curChar = inputStream.readChar(); + } catch (java.io.IOException e) { + return jjMoveNfa_0(5, 7); + } + switch (curChar) { + case 84: + return jjMoveStringLiteralDfa9_0(active0, 0x40000L); + case 116: + return jjMoveStringLiteralDfa9_0(active0, 0x40000L); + default: + break; + } + return jjMoveNfa_0(5, 8); + } + + private int jjMoveStringLiteralDfa9_0(long old0, long active0) { + if (((active0 &= old0)) == 0L) + return jjMoveNfa_0(5, 8); + try { + curChar = inputStream.readChar(); + } catch (java.io.IOException e) { + return jjMoveNfa_0(5, 8); + } + switch (curChar) { + case 72: + if ((active0 & 0x40000L) != 0L) { + jjmatchedKind = 18; + jjmatchedPos = 9; + } + break; + case 104: + if ((active0 & 0x40000L) != 0L) { + jjmatchedKind = 18; + jjmatchedPos = 9; + } + break; + default: + break; + } + return jjMoveNfa_0(5, 9); + } + static final long[] JJ_BIT_VEC_0 = { 0xfffffffffffffffeL, 0xffffffffffffffffL, 0xffffffffffffffffL, 0xffffffffffffffffL }; @@ -396,8 +545,8 @@ private int jjMoveNfa_0(int startState, int curPos) { if ((0x3ff000000000000L & l) != 0L) jjCheckNAddStates(0, 3); else if (curChar == 36) { - if (kind > 21) - kind = 21; + if (kind > 24) + kind = 24; jjCheckNAdd(28); } else if (curChar == 39) jjCheckNAddStates(4, 6); @@ -408,12 +557,12 @@ else if (curChar == 47) else if (curChar == 45) jjstateSet[jjnewStateCnt++] = 0; if ((0x3fe000000000000L & l) != 0L) { - if (kind > 17) - kind = 17; + if (kind > 20) + kind = 20; jjCheckNAddTwoStates(15, 16); } else if (curChar == 48) { - if (kind > 17) - kind = 17; + if (kind > 20) + kind = 20; } break; case 0: @@ -465,21 +614,21 @@ else if (curChar == 45) jjstateSet[jjnewStateCnt++] = 6; break; case 13: - if (curChar == 48 && kind > 17) - kind = 17; + if (curChar == 48 && kind > 20) + kind = 20; break; case 14: if ((0x3fe000000000000L & l) == 0L) break; - if (kind > 17) - kind = 17; + if (kind > 20) + kind = 20; jjCheckNAddTwoStates(15, 16); break; case 15: if ((0x3ff000000000000L & l) == 0L) break; - if (kind > 17) - kind = 17; + if (kind > 20) + kind = 20; jjCheckNAddTwoStates(15, 16); break; case 17: @@ -489,8 +638,8 @@ else if (curChar == 45) case 18: if ((0x3ff000000000000L & l) == 0L) break; - if (kind > 18) - kind = 18; + if (kind > 21) + kind = 21; jjCheckNAddTwoStates(18, 19); break; case 20: @@ -500,8 +649,8 @@ else if (curChar == 45) case 21: if ((0x3ff000000000000L & l) == 0L) break; - if (kind > 18) - kind = 18; + if (kind > 21) + kind = 21; jjCheckNAdd(21); break; case 22: @@ -518,21 +667,21 @@ else if (curChar == 45) jjCheckNAddStates(4, 6); break; case 26: - if (curChar == 39 && kind > 20) - kind = 20; + if (curChar == 39 && kind > 23) + kind = 23; break; case 27: if (curChar != 36) break; - if (kind > 21) - kind = 21; + if (kind > 24) + kind = 24; jjCheckNAdd(28); break; case 28: if ((0x3ff001000000000L & l) == 0L) break; - if (kind > 21) - kind = 21; + if (kind > 24) + kind = 24; jjCheckNAdd(28); break; case 29: @@ -546,15 +695,15 @@ else if (curChar == 45) case 31: if (curChar != 46) break; - if (kind > 18) - kind = 18; + if (kind > 21) + kind = 21; jjCheckNAddTwoStates(32, 33); break; case 32: if ((0x3ff000000000000L & l) == 0L) break; - if (kind > 18) - kind = 18; + if (kind > 21) + kind = 21; jjCheckNAddTwoStates(32, 33); break; case 34: @@ -564,8 +713,8 @@ else if (curChar == 45) case 35: if ((0x3ff000000000000L & l) == 0L) break; - if (kind > 18) - kind = 18; + if (kind > 21) + kind = 21; jjCheckNAdd(35); break; case 36: @@ -579,15 +728,14 @@ else if (curChar == 45) case 39: if ((0x3ff000000000000L & l) == 0L) break; - if (kind > 18) - kind = 18; + if (kind > 21) + kind = 21; jjCheckNAdd(39); break; default: break; } - } - while (i != startsAt); + } while (i != startsAt); } else if (curChar < 128) { long l = 1L << (curChar & 077); do { @@ -596,8 +744,8 @@ else if (curChar == 45) case 28: if ((0x7fffffe87fffffeL & l) == 0L) break; - if (kind > 21) - kind = 21; + if (kind > 24) + kind = 24; jjCheckNAdd(28); break; case 1: @@ -611,8 +759,8 @@ else if (curChar == 45) jjCheckNAddTwoStates(10, 8); break; case 16: - if ((0x100000001000L & l) != 0L && kind > 17) - kind = 17; + if ((0x100000001000L & l) != 0L && kind > 20) + kind = 20; break; case 19: if ((0x2000000020L & l) != 0L) @@ -632,8 +780,7 @@ else if (curChar == 45) default: break; } - } - while (i != startsAt); + } while (i != startsAt); } else { int hiByte = (int) (curChar >> 8); int i1 = hiByte >> 6; @@ -662,8 +809,7 @@ else if (curChar == 45) default: break; } - } - while (i != startsAt); + } while (i != startsAt); } if (kind != 0x7fffffff) { jjmatchedKind = kind; @@ -722,8 +868,8 @@ private static final boolean jjCanMove_0(int hiByte, int i1, int i2, long l1, lo */ public static final String[] JJ_STR_LITERAL_IMAGES = { "", null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, "\75", "\74\76", "\76", - "\76\75", "\74", "\74\75", "\50", "\54", "\51", "\53", "\55"}; + null, null, null, null, null, null, null, null, null, null, null, null, "\75", + "\74\76", "\76", "\76\75", "\74", "\74\75", "\50", "\54", "\51", "\53", "\55",}; /** * Lexer state names. @@ -732,7 +878,7 @@ private static final boolean jjCanMove_0(int hiByte, int i1, int i2, long l1, lo "DEFAULT", }; static final long[] JJ_TO_TOKEN = { - 0x1fff7ff01L, + 0xfffbfff01L, }; static final long[] JJ_TO_SKIP = { 0xfeL, @@ -792,8 +938,7 @@ public void ReInit(SimpleCharStream stream, int lexState) { */ public void SwitchTo(int lexState) { if (lexState >= 1 || lexState < 0) - throw new TokenMgrError("Error: Ignoring invalid lexical state : " + lexState + ". State unchanged.", - TokenMgrError.INVALID_LEXICAL_STATE); + throw new TokenMgrError("Error: Ignoring invalid lexical state : " + lexState + ". State unchanged.", TokenMgrError.INVALID_LEXICAL_STATE); else curLexState = lexState; } @@ -890,8 +1035,7 @@ public Token getNextToken() { inputStream.backup(1); errorAfter = curPos <= 1 ? "" : inputStream.GetImage(); } - throw new TokenMgrError(eofSeen, curLexState, errorLine, errorColumn, errorAfter, curChar, - TokenMgrError.LEXICAL_ERROR); + throw new TokenMgrError(eofSeen, curLexState, errorLine, errorColumn, errorAfter, curChar, TokenMgrError.LEXICAL_ERROR); } } @@ -905,8 +1049,7 @@ private void jjCheckNAdd(int state) { private void jjAddStates(int start, int end) { do { jjstateSet[jjnewStateCnt++] = JJ_NEXT_STATES[start]; - } - while (start++ != end); + } while (start++ != end); } private void jjCheckNAddTwoStates(int state1, int state2) { @@ -917,8 +1060,7 @@ private void jjCheckNAddTwoStates(int state1, int state2) { private void jjCheckNAddStates(int start, int end) { do { jjCheckNAdd(JJ_NEXT_STATES[start]); - } - while (start++ != end); + } while (start++ != end); } } diff --git a/filter/src/main/java/org/apache/rocketmq/filter/parser/SimpleCharStream.java b/filter/src/main/java/org/apache/rocketmq/filter/parser/SimpleCharStream.java index 53f7e1c297e..b8e375e51cd 100644 --- a/filter/src/main/java/org/apache/rocketmq/filter/parser/SimpleCharStream.java +++ b/filter/src/main/java/org/apache/rocketmq/filter/parser/SimpleCharStream.java @@ -19,6 +19,8 @@ /* JavaCCOptions:STATIC=false,SUPPORT_CLASS_VISIBILITY_PUBLIC=true */ package org.apache.rocketmq.filter.parser; +import java.nio.charset.StandardCharsets; + /** * An implementation of interface CharStream, where the stream is assumed to * contain only ASCII characters (without unicode processing). @@ -36,8 +38,8 @@ public class SimpleCharStream { * Position in buffer. */ public int bufpos = -1; - protected int bufline[]; - protected int bufcolumn[]; + protected int[] bufline; + protected int[] bufcolumn; protected int column = 0; protected int line = 1; @@ -62,8 +64,8 @@ protected int getTabSize(int i) { protected void ExpandBuff(boolean wrapAround) { char[] newbuffer = new char[bufsize + 2048]; - int newbufline[] = new int[bufsize + 2048]; - int newbufcolumn[] = new int[bufsize + 2048]; + int[] newbufline = new int[bufsize + 2048]; + int[] newbufcolumn = new int[bufsize + 2048]; try { if (wrapAround) { @@ -331,7 +333,7 @@ public void ReInit(java.io.Reader dstream) { public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline, int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException { this(encoding == null ? - new java.io.InputStreamReader(dstream) : + new java.io.InputStreamReader(dstream, StandardCharsets.UTF_8) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); } @@ -340,7 +342,7 @@ public SimpleCharStream(java.io.InputStream dstream, String encoding, int startl */ public SimpleCharStream(java.io.InputStream dstream, int startline, int startcolumn, int buffersize) { - this(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize); + this(new java.io.InputStreamReader(dstream, StandardCharsets.UTF_8), startline, startcolumn, buffersize); } /** @@ -379,7 +381,7 @@ public SimpleCharStream(java.io.InputStream dstream) { public void ReInit(java.io.InputStream dstream, String encoding, int startline, int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException { ReInit(encoding == null ? - new java.io.InputStreamReader(dstream) : + new java.io.InputStreamReader(dstream, StandardCharsets.UTF_8) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); } @@ -388,7 +390,7 @@ public void ReInit(java.io.InputStream dstream, String encoding, int startline, */ public void ReInit(java.io.InputStream dstream, int startline, int startcolumn, int buffersize) { - ReInit(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize); + ReInit(new java.io.InputStreamReader(dstream, StandardCharsets.UTF_8), startline, startcolumn, buffersize); } /** @@ -499,4 +501,4 @@ public void adjustBeginLineColumn(int newLine, int newCol) { } } -/* JavaCC - OriginalChecksum=af79bfe4b18b4b4ea9720ffeb7e52fc5 (do not edit this line) */ +/* JavaCC - OriginalChecksum=ea3493f692d4975c1ad70c4a750107d3 (do not edit this line) */ diff --git a/filter/src/main/java/org/apache/rocketmq/filter/parser/Token.java b/filter/src/main/java/org/apache/rocketmq/filter/parser/Token.java index 8e6a48a0868..edb78800867 100644 --- a/filter/src/main/java/org/apache/rocketmq/filter/parser/Token.java +++ b/filter/src/main/java/org/apache/rocketmq/filter/parser/Token.java @@ -149,4 +149,4 @@ public static Token newToken(int ofKind) { } } -/* JavaCC - OriginalChecksum=6b0af88eb45a551d929d3cdd9582f827 (do not edit this line) */ +/* JavaCC - OriginalChecksum=20094f1ccfbf423c6d9e770d6a7a0188 (do not edit this line) */ diff --git a/filter/src/main/java/org/apache/rocketmq/filter/parser/TokenMgrError.java b/filter/src/main/java/org/apache/rocketmq/filter/parser/TokenMgrError.java index e8132df5aac..4a8f2c86a30 100644 --- a/filter/src/main/java/org/apache/rocketmq/filter/parser/TokenMgrError.java +++ b/filter/src/main/java/org/apache/rocketmq/filter/parser/TokenMgrError.java @@ -66,7 +66,7 @@ public class TokenMgrError extends Error { * equivalents in the given string */ protected static final String addEscapes(String str) { - StringBuffer retval = new StringBuffer(); + StringBuilder retval = new StringBuilder(); char ch; for (int i = 0; i < str.length(); i++) { switch (str.charAt(i)) { @@ -141,6 +141,7 @@ protected static String LexicalError(boolean eofSeen, int lexState, int errorLin *

    * from this method for such cases in the release version of your parser. */ + @Override public String getMessage() { return super.getMessage(); } @@ -171,4 +172,4 @@ public TokenMgrError(boolean eofSeen, int lexState, int errorLine, int errorColu this(LexicalError(eofSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason); } } -/* JavaCC - OriginalChecksum=e960778c8dcd73e167ed5bfddd59f288 (do not edit this line) */ +/* JavaCC - OriginalChecksum=de79709675790dcbad2e0d728aa630d1 (do not edit this line) */ diff --git a/filter/src/main/java/org/apache/rocketmq/filter/util/BloomFilter.java b/filter/src/main/java/org/apache/rocketmq/filter/util/BloomFilter.java index 9a3de6016b6..d909cc2a57c 100644 --- a/filter/src/main/java/org/apache/rocketmq/filter/util/BloomFilter.java +++ b/filter/src/main/java/org/apache/rocketmq/filter/util/BloomFilter.java @@ -20,13 +20,14 @@ import com.google.common.hash.Hashing; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; /** * Simple implement of bloom filter. */ public class BloomFilter { - public static final Charset UTF_8 = Charset.forName("UTF-8"); + public static final Charset UTF_8 = StandardCharsets.UTF_8; // as error rate, 10/100 = 0.1 private int f = 10; @@ -145,7 +146,7 @@ public void hashTo(BloomFilterData filterData, BitsArray bits) { if (!isValid(filterData)) { throw new IllegalArgumentException( String.format("Bloom filter data may not belong to this filter! %s, %s", - filterData, this.toString()) + filterData, this) ); } hashTo(filterData.getBitPos(), bits); @@ -183,7 +184,7 @@ public boolean isHit(BloomFilterData filterData, BitsArray bits) { if (!isValid(filterData)) { throw new IllegalArgumentException( String.format("Bloom filter data may not belong to this filter! %s, %s", - filterData, this.toString()) + filterData, this) ); } return isHit(filterData.getBitPos(), bits); diff --git a/filter/src/test/java/org/apache/rocketmq/filter/ExpressionTest.java b/filter/src/test/java/org/apache/rocketmq/filter/ExpressionTest.java index 7fb606ac13b..df883458ed6 100644 --- a/filter/src/test/java/org/apache/rocketmq/filter/ExpressionTest.java +++ b/filter/src/test/java/org/apache/rocketmq/filter/ExpressionTest.java @@ -46,6 +46,372 @@ public class ExpressionTest { private static String nullOrExpression = "a is null OR a='hello'"; private static String stringHasString = "TAGS is not null and TAGS='''''tag'''''"; + + @Test + public void testContains_StartsWith_EndsWith_has() throws Exception { + EvaluationContext context = genContext( + KeyValue.c("value", "axb") + ); + eval(genExp("value contains 'x'"), context, Boolean.TRUE); + eval(genExp("value startswith 'ax'"), context, Boolean.TRUE); + eval(genExp("value endswith 'xb'"), context, Boolean.TRUE); + } + + @Test + public void test_notContains_notStartsWith_notEndsWith_has() throws Exception { + EvaluationContext context = genContext( + KeyValue.c("value", "axb") + ); + eval(genExp("value not contains 'x'"), context, Boolean.FALSE); + eval(genExp("value not startswith 'ax'"), context, Boolean.FALSE); + eval(genExp("value not endswith 'xb'"), context, Boolean.FALSE); + } + + @Test + public void testContains_StartsWith_EndsWith_has_not() throws Exception { + EvaluationContext context = genContext( + KeyValue.c("value", "abb") + ); + eval(genExp("value contains 'x'"), context, Boolean.FALSE); + eval(genExp("value startswith 'x'"), context, Boolean.FALSE); + eval(genExp("value endswith 'x'"), context, Boolean.FALSE); + } + + @Test + public void test_notContains_notStartsWith_notEndsWith_has_not() throws Exception { + EvaluationContext context = genContext( + KeyValue.c("value", "abb") + ); + eval(genExp("value not contains 'x'"), context, Boolean.TRUE); + eval(genExp("value not startswith 'x'"), context, Boolean.TRUE); + eval(genExp("value not endswith 'x'"), context, Boolean.TRUE); + } + + @Test + public void testContains_StartsWith_EndsWith_hasEmpty() throws Exception { + EvaluationContext context = genContext( + KeyValue.c("value", "axb") + ); + eval(genExp("value contains ''"), context, Boolean.FALSE); + eval(genExp("value startswith ''"), context, Boolean.FALSE); + eval(genExp("value endswith ''"), context, Boolean.FALSE); + } + + @Test + public void test_notContains_notStartsWith_notEndsWith_hasEmpty() throws Exception { + EvaluationContext context = genContext( + KeyValue.c("value", "axb") + ); + eval(genExp("value not contains ''"), context, Boolean.FALSE); + eval(genExp("value not startswith ''"), context, Boolean.FALSE); + eval(genExp("value not endswith ''"), context, Boolean.FALSE); + } + + @Test + public void testContains_StartsWith_EndsWith_null_has_1() throws Exception { + EvaluationContext context = genContext( + KeyValue.c("value", null) + ); + eval(genExp("value contains 'x'"), context, Boolean.FALSE); + eval(genExp("value startswith 'x'"), context, Boolean.FALSE); + eval(genExp("value endswith 'x'"), context, Boolean.FALSE); + } + + @Test + public void test_notContains_notStartsWith_notEndsWith_null_has_1() throws Exception { + EvaluationContext context = genContext( + KeyValue.c("value", null) + ); + eval(genExp("value not contains 'x'"), context, Boolean.FALSE); + eval(genExp("value not startswith 'x'"), context, Boolean.FALSE); + eval(genExp("value not endswith 'x'"), context, Boolean.FALSE); + } + + @Test + public void testContains_StartsWith_EndsWith_null_has_2() throws Exception { + EvaluationContext context = genContext( +// KeyValue.c("value", null) + ); + eval(genExp("value contains 'x'"), context, Boolean.FALSE); + eval(genExp("value startswith 'x'"), context, Boolean.FALSE); + eval(genExp("value endswith 'x'"), context, Boolean.FALSE); + } + + @Test + public void test_notContains_notStartsWith_notEndsWith_null_has_2() throws Exception { + EvaluationContext context = genContext( +// KeyValue.c("value", null) + ); + eval(genExp("value not contains 'x'"), context, Boolean.FALSE); + eval(genExp("value not startswith 'x'"), context, Boolean.FALSE); + eval(genExp("value not endswith 'x'"), context, Boolean.FALSE); + } + + @Test + public void testContains_StartsWith_EndsWith_number_has() throws Exception { + EvaluationContext context = genContext( + KeyValue.c("value", 1.23) + ); + eval(genExp("value contains 'x'"), context, Boolean.FALSE); + eval(genExp("value startswith 'x'"), context, Boolean.FALSE); + eval(genExp("value endswith 'x'"), context, Boolean.FALSE); + } + + @Test + public void test_notContains_notStartsWith_notEndsWith_number_has() throws Exception { + EvaluationContext context = genContext( + KeyValue.c("value", 1.23) + ); + eval(genExp("value not contains 'x'"), context, Boolean.FALSE); + eval(genExp("value not startswith 'x'"), context, Boolean.FALSE); + eval(genExp("value not endswith 'x'"), context, Boolean.FALSE); + } + + @Test + public void testContains_StartsWith_EndsWith_boolean_has() throws Exception { + EvaluationContext context = genContext( + KeyValue.c("value", Boolean.TRUE) + ); + eval(genExp("value contains 'x'"), context, Boolean.FALSE); + eval(genExp("value startswith 'x'"), context, Boolean.FALSE); + eval(genExp("value endswith 'x'"), context, Boolean.FALSE); + } + + @Test + public void test_notContains_notStartsWith_notEndsWith_boolean_has() throws Exception { + EvaluationContext context = genContext( + KeyValue.c("value", Boolean.TRUE) + ); + eval(genExp("value not contains 'x'"), context, Boolean.FALSE); + eval(genExp("value not startswith 'x'"), context, Boolean.FALSE); + eval(genExp("value not endswith 'x'"), context, Boolean.FALSE); + } + + @Test + public void testContains_StartsWith_EndsWith_object_has() throws Exception { + EvaluationContext context = genContext( + KeyValue.c("value", new Object()) + ); + eval(genExp("value contains 'x'"), context, Boolean.FALSE); + eval(genExp("value startswith 'x'"), context, Boolean.FALSE); + eval(genExp("value endswith 'x'"), context, Boolean.FALSE); + } + + @Test + public void testContains_has_not_string_1() throws Exception { + try { + Expression expr = genExp("value contains x"); // will throw parse exception. + EvaluationContext context = genContext( + KeyValue.c("value", "axb") + ); + eval(expr, context, Boolean.FALSE); + } catch (Throwable e) { + } + } + + @Test + public void test_notContains_has_not_string_1() throws Exception { + try { + Expression expr = genExp("value not contains x"); // will throw parse exception. + EvaluationContext context = genContext( + KeyValue.c("value", "axb") + ); + eval(expr, context, Boolean.FALSE); + } catch (Throwable e) { + } + } + + @Test + public void testContains_has_not_string_2() throws Exception { + try { + Expression expr = genExp("value contains 123"); // will throw parse exception. + EvaluationContext context = genContext( + KeyValue.c("value", "axb") + ); + eval(expr, context, Boolean.FALSE); + } catch (Throwable e) { + } + } + + @Test + public void test_notContains_has_not_string_2() throws Exception { + try { + Expression expr = genExp("value not contains 123"); // will throw parse exception. + EvaluationContext context = genContext( + KeyValue.c("value", "axb") + ); + eval(expr, context, Boolean.FALSE); + } catch (Throwable e) { + } + } + + @Test + public void testContains_StartsWith_EndsWith_string_has_string() throws Exception { + EvaluationContext context = genContext( + KeyValue.c("whatever", "whatever") + ); + eval(genExp("'axb' contains 'x'"), context, Boolean.TRUE); + eval(genExp("'axb' startswith 'ax'"), context, Boolean.TRUE); + eval(genExp("'axb' endswith 'xb'"), context, Boolean.TRUE); + } + + @Test + public void test_notContains_notStartsWith_notEndsWith_string_has_string() throws Exception { + EvaluationContext context = genContext( + KeyValue.c("whatever", "whatever") + ); + eval(genExp("'axb' not contains 'x'"), context, Boolean.FALSE); + eval(genExp("'axb' not startswith 'ax'"), context, Boolean.FALSE); + eval(genExp("'axb' not endswith 'xb'"), context, Boolean.FALSE); + } + + @Test + public void testContains_startsWith_endsWith_string_has_not_string() throws Exception { + EvaluationContext context = genContext( + KeyValue.c("whatever", "whatever") + ); + eval(genExp("'axb' contains 'u'"), context, Boolean.FALSE); + eval(genExp("'axb' startswith 'u'"), context, Boolean.FALSE); + eval(genExp("'axb' endswith 'u'"), context, Boolean.FALSE); + } + + @Test + public void test_notContains_notStartsWith_notEndsWith_string_has_not_string() throws Exception { + EvaluationContext context = genContext( + KeyValue.c("whatever", "whatever") + ); + eval(genExp("'axb' not contains 'u'"), context, Boolean.TRUE); + eval(genExp("'axb' not startswith 'u'"), context, Boolean.TRUE); + eval(genExp("'axb' not endswith 'u'"), context, Boolean.TRUE); + } + + @Test + public void testContains_StartsWith_EndsWith_string_has_empty() throws Exception { + EvaluationContext context = genContext( + KeyValue.c("whatever", "whatever") + ); + eval(genExp("'axb' contains ''"), context, Boolean.FALSE); + eval(genExp("'axb' startswith ''"), context, Boolean.FALSE); + eval(genExp("'axb' endswith ''"), context, Boolean.FALSE); + } + + @Test + public void test_notContains_notStartsWith_notEndsWith_string_has_empty() throws Exception { + EvaluationContext context = genContext( + KeyValue.c("whatever", "whatever") + ); + eval(genExp("'axb' not contains ''"), context, Boolean.FALSE); + eval(genExp("'axb' not startswith ''"), context, Boolean.FALSE); + eval(genExp("'axb' not endswith ''"), context, Boolean.FALSE); + } + + @Test + public void testContains_StartsWith_EndsWith_string_has_space() throws Exception { + EvaluationContext context = genContext( + KeyValue.c("whatever", "whatever") + ); + eval(genExp("' ' contains ' '"), context, Boolean.TRUE); + eval(genExp("' ' startswith ' '"), context, Boolean.TRUE); + eval(genExp("' ' endswith ' '"), context, Boolean.TRUE); + } + + @Test + public void test_notContains_notStartsWith_notEndsWith_string_has_space() throws Exception { + EvaluationContext context = genContext( + KeyValue.c("whatever", "whatever") + ); + eval(genExp("' ' not contains ' '"), context, Boolean.FALSE); + eval(genExp("' ' not startswith ' '"), context, Boolean.FALSE); + eval(genExp("' ' not endswith ' '"), context, Boolean.FALSE); + } + + @Test + public void testContains_string_has_nothing() throws Exception { + try { + Expression expr = genExp("'axb' contains "); // will throw parse exception. + EvaluationContext context = genContext( + KeyValue.c("whatever", "whatever") + ); + eval(expr, context, Boolean.TRUE); + } catch (Throwable e) { + } + } + + @Test + public void test_notContains_string_has_nothing() throws Exception { + try { + Expression expr = genExp("'axb' not contains "); // will throw parse exception. + EvaluationContext context = genContext( + KeyValue.c("whatever", "whatever") + ); + eval(expr, context, Boolean.TRUE); + } catch (Throwable e) { + } + } + + @Test + public void testContains_StartsWith_EndsWith_string_has_special_1() throws Exception { + EvaluationContext context = genContext( + KeyValue.c("whatever", "whatever") + ); + eval(genExp("'axb' contains '.'"), context, Boolean.FALSE); + eval(genExp("'axb' startswith '.'"), context, Boolean.FALSE); + eval(genExp("'axb' endswith '.'"), context, Boolean.FALSE); + } + + @Test + public void test_notContains_notStartsWith_notEndsWith_string_has_special_1() throws Exception { + EvaluationContext context = genContext( + KeyValue.c("whatever", "whatever") + ); + eval(genExp("'axb' not contains '.'"), context, Boolean.TRUE); + eval(genExp("'axb' not startswith '.'"), context, Boolean.TRUE); + eval(genExp("'axb' not endswith '.'"), context, Boolean.TRUE); + } + + @Test + public void testContains_StartsWith_EndsWith_string_has_special_2() throws Exception { + EvaluationContext context = genContext( + KeyValue.c("whatever", "whatever") + ); + eval(genExp("'s' contains '\\'"), context, Boolean.FALSE); + eval(genExp("'s' startswith '\\'"), context, Boolean.FALSE); + eval(genExp("'s' endswith '\\'"), context, Boolean.FALSE); + } + + @Test + public void testContainsAllInOne() throws Exception { + Expression expr = genExp("a not in ('4', '4', '5') and b between 3 and 10 and c not contains 'axbc'"); + EvaluationContext context = genContext( + KeyValue.c("a", "3"), + KeyValue.c("b", 3), + KeyValue.c("c", "axbdc") + ); + eval(expr, context, Boolean.TRUE); + } + + @Test + public void testStartsWithAllInOne() throws Exception { + Expression expr = genExp("a not in ('4', '4', '5') and b between 3 and 10 and c not startswith 'axbc'"); + EvaluationContext context = genContext( + KeyValue.c("a", "3"), + KeyValue.c("b", 3), + KeyValue.c("c", "axbdc") + ); + eval(expr, context, Boolean.TRUE); + } + + @Test + public void testEndsWithAllInOne() throws Exception { + Expression expr = genExp("a not in ('4', '4', '5') and b between 3 and 10 and c not endswith 'axbc'"); + EvaluationContext context = genContext( + KeyValue.c("a", "3"), + KeyValue.c("b", 3), + KeyValue.c("c", "axbdc") + ); + eval(expr, context, Boolean.TRUE); + } + @Test public void testEvaluate_stringHasString() throws Exception { Expression expr = genExp(stringHasString); @@ -576,7 +942,7 @@ public KeyValue(String key, Object value) { class PropertyContext implements EvaluationContext { - public Map properties = new HashMap(8); + public Map properties = new HashMap<>(8); @Override public Object get(final String name) { diff --git a/filter/src/test/java/org/apache/rocketmq/filter/ParserTest.java b/filter/src/test/java/org/apache/rocketmq/filter/ParserTest.java index 36ef2714fcc..9e6291ff10b 100644 --- a/filter/src/test/java/org/apache/rocketmq/filter/ParserTest.java +++ b/filter/src/test/java/org/apache/rocketmq/filter/ParserTest.java @@ -37,7 +37,7 @@ public class ParserTest { private static String equalNullExpression = "a is null"; private static String notEqualNullExpression = "a is not null"; private static String nowExpression = "a <= now"; - + private static String containsExpression = "a=3 and b contains 'xxx' and c not contains 'xxx'"; private static String invalidExpression = "a and between 2 and 10"; private static String illegalBetween = " a between 10 and 0"; @@ -45,7 +45,7 @@ public class ParserTest { public void testParse_valid() { for (String expr : Arrays.asList( andExpression, orExpression, inExpression, notInExpression, betweenExpression, - equalNullExpression, notEqualNullExpression, nowExpression + equalNullExpression, notEqualNullExpression, nowExpression, containsExpression )) { try { @@ -84,14 +84,17 @@ public void testParse_decimalOverFlow() { @Test public void testParse_floatOverFlow() { try { - String str = "1"; - for (int i = 0; i < 2048; i++) { - str += "111111111111111111111111111111111111111111111111111"; + StringBuilder sb = new StringBuilder(210000); + sb.append("1"); + for (int i = 0; i < 2048; i ++) { + sb.append("111111111111111111111111111111111111111111111111111"); } - str += "."; - for (int i = 0; i < 2048; i++) { - str += "111111111111111111111111111111111111111111111111111"; + sb.append("."); + for (int i = 0; i < 2048; i ++) { + sb.append("111111111111111111111111111111111111111111111111111"); } + String str = sb.toString(); + SelectorParser.parse("a > " + str); diff --git a/filter/src/test/resources/rmq.logback-test.xml b/filter/src/test/resources/rmq.logback-test.xml new file mode 100644 index 00000000000..8695d52d57c --- /dev/null +++ b/filter/src/test/resources/rmq.logback-test.xml @@ -0,0 +1,36 @@ + + + + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + \ No newline at end of file diff --git a/filtersrv/pom.xml b/filtersrv/pom.xml deleted file mode 100644 index 96ce62a5d6c..00000000000 --- a/filtersrv/pom.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - org.apache.rocketmq - rocketmq-all - 4.2.0 - - - 4.0.0 - jar - rocketmq-filtersrv - rocketmq-filtersrv ${project.version} - - - - ${project.groupId} - rocketmq-client - - - ${project.groupId} - rocketmq-store - - - ${project.groupId} - rocketmq-srvutil - - - ch.qos.logback - logback-classic - - - ch.qos.logback - logback-core - - - org.apache.commons - commons-lang3 - - - diff --git a/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/FilterServerOuterAPI.java b/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/FilterServerOuterAPI.java deleted file mode 100644 index 45c827bad68..00000000000 --- a/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/FilterServerOuterAPI.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.filtersrv; - -import org.apache.rocketmq.client.exception.MQBrokerException; -import org.apache.rocketmq.common.protocol.RequestCode; -import org.apache.rocketmq.common.protocol.ResponseCode; -import org.apache.rocketmq.common.protocol.header.filtersrv.RegisterFilterServerRequestHeader; -import org.apache.rocketmq.common.protocol.header.filtersrv.RegisterFilterServerResponseHeader; -import org.apache.rocketmq.remoting.RemotingClient; -import org.apache.rocketmq.remoting.exception.RemotingCommandException; -import org.apache.rocketmq.remoting.exception.RemotingConnectException; -import org.apache.rocketmq.remoting.exception.RemotingSendRequestException; -import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; -import org.apache.rocketmq.remoting.netty.NettyClientConfig; -import org.apache.rocketmq.remoting.netty.NettyRemotingClient; -import org.apache.rocketmq.remoting.protocol.RemotingCommand; - -public class FilterServerOuterAPI { - private final RemotingClient remotingClient; - - public FilterServerOuterAPI() { - this.remotingClient = new NettyRemotingClient(new NettyClientConfig()); - } - - public void start() { - this.remotingClient.start(); - } - - public void shutdown() { - this.remotingClient.shutdown(); - } - - public RegisterFilterServerResponseHeader registerFilterServerToBroker( - final String brokerAddr, - final String filterServerAddr - ) throws RemotingCommandException, RemotingConnectException, RemotingSendRequestException, - RemotingTimeoutException, InterruptedException, MQBrokerException { - RegisterFilterServerRequestHeader requestHeader = new RegisterFilterServerRequestHeader(); - requestHeader.setFilterServerAddr(filterServerAddr); - RemotingCommand request = - RemotingCommand.createRequestCommand(RequestCode.REGISTER_FILTER_SERVER, requestHeader); - - RemotingCommand response = this.remotingClient.invokeSync(brokerAddr, request, 3000); - assert response != null; - switch (response.getCode()) { - case ResponseCode.SUCCESS: { - RegisterFilterServerResponseHeader responseHeader = - (RegisterFilterServerResponseHeader) response - .decodeCommandCustomHeader(RegisterFilterServerResponseHeader.class); - - return responseHeader; - } - default: - break; - } - - throw new MQBrokerException(response.getCode(), response.getRemark()); - } -} diff --git a/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/FiltersrvConfig.java b/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/FiltersrvConfig.java deleted file mode 100644 index 65551eb3a31..00000000000 --- a/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/FiltersrvConfig.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.filtersrv; - -import org.apache.rocketmq.common.MixAll; -import org.apache.rocketmq.common.annotation.ImportantField; -import org.apache.rocketmq.remoting.common.RemotingUtil; - -public class FiltersrvConfig { - private String rocketmqHome = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, - System.getenv(MixAll.ROCKETMQ_HOME_ENV)); - - @ImportantField - private String namesrvAddr = System.getProperty(MixAll.NAMESRV_ADDR_PROPERTY, - System.getenv(MixAll.NAMESRV_ADDR_ENV)); - - private String connectWhichBroker = "127.0.0.1:10911"; - private String filterServerIP = RemotingUtil.getLocalAddress(); - - private int compressMsgBodyOverHowmuch = 1024 * 8; - private int zipCompressLevel = 5; - - private boolean clientUploadFilterClassEnable = true; - - private String filterClassRepertoryUrl = "http://fsrep.tbsite.net/filterclass"; - - private int fsServerAsyncSemaphoreValue = 2048; - private int fsServerCallbackExecutorThreads = 64; - private int fsServerWorkerThreads = 64; - - public String getRocketmqHome() { - return rocketmqHome; - } - - public void setRocketmqHome(String rocketmqHome) { - this.rocketmqHome = rocketmqHome; - } - - public String getNamesrvAddr() { - return namesrvAddr; - } - - public void setNamesrvAddr(String namesrvAddr) { - this.namesrvAddr = namesrvAddr; - } - - public String getConnectWhichBroker() { - return connectWhichBroker; - } - - public void setConnectWhichBroker(String connectWhichBroker) { - this.connectWhichBroker = connectWhichBroker; - } - - public String getFilterServerIP() { - return filterServerIP; - } - - public void setFilterServerIP(String filterServerIP) { - this.filterServerIP = filterServerIP; - } - - public int getCompressMsgBodyOverHowmuch() { - return compressMsgBodyOverHowmuch; - } - - public void setCompressMsgBodyOverHowmuch(int compressMsgBodyOverHowmuch) { - this.compressMsgBodyOverHowmuch = compressMsgBodyOverHowmuch; - } - - public int getZipCompressLevel() { - return zipCompressLevel; - } - - public void setZipCompressLevel(int zipCompressLevel) { - this.zipCompressLevel = zipCompressLevel; - } - - public boolean isClientUploadFilterClassEnable() { - return clientUploadFilterClassEnable; - } - - public void setClientUploadFilterClassEnable(boolean clientUploadFilterClassEnable) { - this.clientUploadFilterClassEnable = clientUploadFilterClassEnable; - } - - public String getFilterClassRepertoryUrl() { - return filterClassRepertoryUrl; - } - - public void setFilterClassRepertoryUrl(String filterClassRepertoryUrl) { - this.filterClassRepertoryUrl = filterClassRepertoryUrl; - } - - public int getFsServerAsyncSemaphoreValue() { - return fsServerAsyncSemaphoreValue; - } - - public void setFsServerAsyncSemaphoreValue(int fsServerAsyncSemaphoreValue) { - this.fsServerAsyncSemaphoreValue = fsServerAsyncSemaphoreValue; - } - - public int getFsServerCallbackExecutorThreads() { - return fsServerCallbackExecutorThreads; - } - - public void setFsServerCallbackExecutorThreads(int fsServerCallbackExecutorThreads) { - this.fsServerCallbackExecutorThreads = fsServerCallbackExecutorThreads; - } - - public int getFsServerWorkerThreads() { - return fsServerWorkerThreads; - } - - public void setFsServerWorkerThreads(int fsServerWorkerThreads) { - this.fsServerWorkerThreads = fsServerWorkerThreads; - } -} diff --git a/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/FiltersrvController.java b/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/FiltersrvController.java deleted file mode 100644 index f6485bada59..00000000000 --- a/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/FiltersrvController.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.filtersrv; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; -import org.apache.rocketmq.common.MixAll; -import org.apache.rocketmq.common.ThreadFactoryImpl; -import org.apache.rocketmq.common.UtilAll; -import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.common.protocol.header.filtersrv.RegisterFilterServerResponseHeader; -import org.apache.rocketmq.filtersrv.filter.FilterClassManager; -import org.apache.rocketmq.filtersrv.processor.DefaultRequestProcessor; -import org.apache.rocketmq.filtersrv.stats.FilterServerStatsManager; -import org.apache.rocketmq.remoting.RemotingServer; -import org.apache.rocketmq.remoting.netty.NettyRemotingServer; -import org.apache.rocketmq.remoting.netty.NettyServerConfig; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class FiltersrvController { - private static final Logger log = LoggerFactory.getLogger(LoggerName.FILTERSRV_LOGGER_NAME); - - private final FiltersrvConfig filtersrvConfig; - - private final NettyServerConfig nettyServerConfig; - private final FilterClassManager filterClassManager; - - private final FilterServerOuterAPI filterServerOuterAPI = new FilterServerOuterAPI(); - private final DefaultMQPullConsumer defaultMQPullConsumer = new DefaultMQPullConsumer( - MixAll.FILTERSRV_CONSUMER_GROUP); - - private final ScheduledExecutorService scheduledExecutorService = Executors - .newSingleThreadScheduledExecutor(new ThreadFactoryImpl("FSScheduledThread")); - private final FilterServerStatsManager filterServerStatsManager = new FilterServerStatsManager(); - - private RemotingServer remotingServer; - - private ExecutorService remotingExecutor; - private volatile String brokerName = null; - - public FiltersrvController(FiltersrvConfig filtersrvConfig, NettyServerConfig nettyServerConfig) { - this.filtersrvConfig = filtersrvConfig; - this.nettyServerConfig = nettyServerConfig; - this.filterClassManager = new FilterClassManager(this); - } - - public boolean initialize() { - - MixAll.printObjectProperties(log, this.filtersrvConfig); - - this.remotingServer = new NettyRemotingServer(this.nettyServerConfig); - - this.remotingExecutor = - Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), - new ThreadFactoryImpl("RemotingExecutorThread_")); - - this.registerProcessor(); - - this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { - - @Override - public void run() { - FiltersrvController.this.registerFilterServerToBroker(); - } - }, 3, 10, TimeUnit.SECONDS); - - this.defaultMQPullConsumer.setBrokerSuspendMaxTimeMillis(this.defaultMQPullConsumer - .getBrokerSuspendMaxTimeMillis() - 1000); - this.defaultMQPullConsumer.setConsumerTimeoutMillisWhenSuspend(this.defaultMQPullConsumer - .getConsumerTimeoutMillisWhenSuspend() - 1000); - - this.defaultMQPullConsumer.setNamesrvAddr(this.filtersrvConfig.getNamesrvAddr()); - this.defaultMQPullConsumer.setInstanceName(String.valueOf(UtilAll.getPid())); - - return true; - } - - private void registerProcessor() { - this.remotingServer - .registerDefaultProcessor(new DefaultRequestProcessor(this), this.remotingExecutor); - } - - public void registerFilterServerToBroker() { - try { - RegisterFilterServerResponseHeader responseHeader = - this.filterServerOuterAPI.registerFilterServerToBroker( - this.filtersrvConfig.getConnectWhichBroker(), this.localAddr()); - this.defaultMQPullConsumer.getDefaultMQPullConsumerImpl().getPullAPIWrapper() - .setDefaultBrokerId(responseHeader.getBrokerId()); - - if (null == this.brokerName) { - this.brokerName = responseHeader.getBrokerName(); - } - - log.info("register filter server<{}> to broker<{}> OK, Return: {} {}", - this.localAddr(), - this.filtersrvConfig.getConnectWhichBroker(), - responseHeader.getBrokerName(), - responseHeader.getBrokerId()); - } catch (Exception e) { - log.warn("register filter server Exception", e); - - log.warn("access broker failed, kill oneself"); - System.exit(-1); - } - } - - public String localAddr() { - return String.format("%s:%d", this.filtersrvConfig.getFilterServerIP(), - this.remotingServer.localListenPort()); - } - - public void start() throws Exception { - this.defaultMQPullConsumer.start(); - this.remotingServer.start(); - this.filterServerOuterAPI.start(); - this.defaultMQPullConsumer.getDefaultMQPullConsumerImpl().getPullAPIWrapper() - .setConnectBrokerByUser(true); - this.filterClassManager.start(); - this.filterServerStatsManager.start(); - } - - public void shutdown() { - this.remotingServer.shutdown(); - this.remotingExecutor.shutdown(); - this.scheduledExecutorService.shutdown(); - this.defaultMQPullConsumer.shutdown(); - this.filterServerOuterAPI.shutdown(); - this.filterClassManager.shutdown(); - this.filterServerStatsManager.shutdown(); - } - - public RemotingServer getRemotingServer() { - return remotingServer; - } - - public void setRemotingServer(RemotingServer remotingServer) { - this.remotingServer = remotingServer; - } - - public ExecutorService getRemotingExecutor() { - return remotingExecutor; - } - - public void setRemotingExecutor(ExecutorService remotingExecutor) { - this.remotingExecutor = remotingExecutor; - } - - public FiltersrvConfig getFiltersrvConfig() { - return filtersrvConfig; - } - - public NettyServerConfig getNettyServerConfig() { - return nettyServerConfig; - } - - public ScheduledExecutorService getScheduledExecutorService() { - return scheduledExecutorService; - } - - public FilterServerOuterAPI getFilterServerOuterAPI() { - return filterServerOuterAPI; - } - - public FilterClassManager getFilterClassManager() { - return filterClassManager; - } - - public DefaultMQPullConsumer getDefaultMQPullConsumer() { - return defaultMQPullConsumer; - } - - public String getBrokerName() { - return brokerName; - } - - public void setBrokerName(String brokerName) { - this.brokerName = brokerName; - } - - public FilterServerStatsManager getFilterServerStatsManager() { - return filterServerStatsManager; - } -} diff --git a/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/FiltersrvStartup.java b/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/FiltersrvStartup.java deleted file mode 100644 index 585024d4a77..00000000000 --- a/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/FiltersrvStartup.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.filtersrv; - -import ch.qos.logback.classic.LoggerContext; -import ch.qos.logback.classic.joran.JoranConfigurator; -import java.io.BufferedInputStream; -import java.io.FileInputStream; -import java.io.InputStream; -import java.util.Properties; -import java.util.concurrent.Callable; -import org.apache.commons.cli.CommandLine; -import org.apache.commons.cli.Option; -import org.apache.commons.cli.Options; -import org.apache.commons.cli.PosixParser; -import org.apache.rocketmq.common.MQVersion; -import org.apache.rocketmq.common.MixAll; -import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.remoting.netty.NettyServerConfig; -import org.apache.rocketmq.remoting.netty.NettySystemConfig; -import org.apache.rocketmq.remoting.protocol.RemotingCommand; -import org.apache.rocketmq.srvutil.ServerUtil; -import org.apache.rocketmq.srvutil.ShutdownHookThread; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class FiltersrvStartup { - public static Logger log; - - public static void main(String[] args) { - start(createController(args)); - } - - public static FiltersrvController start(FiltersrvController controller) { - - try { - controller.start(); - } catch (Exception e) { - e.printStackTrace(); - System.exit(-1); - } - - String tip = "The Filter Server boot success, " + controller.localAddr(); - log.info(tip); - System.out.printf("%s%n", tip); - - return controller; - } - - public static FiltersrvController createController(String[] args) { - System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION)); - - if (null == System.getProperty(NettySystemConfig.COM_ROCKETMQ_REMOTING_SOCKET_SNDBUF_SIZE)) { - NettySystemConfig.socketSndbufSize = 65535; - } - - if (null == System.getProperty(NettySystemConfig.COM_ROCKETMQ_REMOTING_SOCKET_RCVBUF_SIZE)) { - NettySystemConfig.socketRcvbufSize = 1024; - } - - try { - Options options = ServerUtil.buildCommandlineOptions(new Options()); - final CommandLine commandLine = - ServerUtil.parseCmdLine("mqfiltersrv", args, buildCommandlineOptions(options), - new PosixParser()); - if (null == commandLine) { - System.exit(-1); - return null; - } - - final FiltersrvConfig filtersrvConfig = new FiltersrvConfig(); - final NettyServerConfig nettyServerConfig = new NettyServerConfig(); - - if (commandLine.hasOption('c')) { - String file = commandLine.getOptionValue('c'); - if (file != null) { - InputStream in = new BufferedInputStream(new FileInputStream(file)); - Properties properties = new Properties(); - properties.load(in); - MixAll.properties2Object(properties, filtersrvConfig); - System.out.printf("load config properties file OK, %s%n", file); - in.close(); - - String port = properties.getProperty("listenPort"); - if (port != null) { - filtersrvConfig.setConnectWhichBroker(String.format("127.0.0.1:%s", port)); - } - } - } - - nettyServerConfig.setListenPort(0); - nettyServerConfig.setServerAsyncSemaphoreValue(filtersrvConfig.getFsServerAsyncSemaphoreValue()); - nettyServerConfig.setServerCallbackExecutorThreads(filtersrvConfig - .getFsServerCallbackExecutorThreads()); - nettyServerConfig.setServerWorkerThreads(filtersrvConfig.getFsServerWorkerThreads()); - - if (commandLine.hasOption('p')) { - MixAll.printObjectProperties(null, filtersrvConfig); - MixAll.printObjectProperties(null, nettyServerConfig); - System.exit(0); - } - - MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), filtersrvConfig); - if (null == filtersrvConfig.getRocketmqHome()) { - System.out.printf("Please set the %s variable in your environment to match the location of the RocketMQ installation%n", MixAll.ROCKETMQ_HOME_ENV); - System.exit(-2); - } - - LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); - JoranConfigurator configurator = new JoranConfigurator(); - configurator.setContext(lc); - lc.reset(); - configurator.doConfigure(filtersrvConfig.getRocketmqHome() + "/conf/logback_filtersrv.xml"); - log = LoggerFactory.getLogger(LoggerName.FILTERSRV_LOGGER_NAME); - - final FiltersrvController controller = - new FiltersrvController(filtersrvConfig, nettyServerConfig); - boolean initResult = controller.initialize(); - if (!initResult) { - controller.shutdown(); - System.exit(-3); - } - - Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable() { - @Override - public Void call() throws Exception { - controller.shutdown(); - return null; - } - })); - - return controller; - } catch (Throwable e) { - e.printStackTrace(); - System.exit(-1); - } - return null; - } - - public static Options buildCommandlineOptions(final Options options) { - Option opt = new Option("c", "configFile", true, "Filter server config properties file"); - opt.setRequired(false); - options.addOption(opt); - - opt = new Option("p", "printConfigItem", false, "Print all config item"); - opt.setRequired(false); - options.addOption(opt); - - return options; - } -} diff --git a/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/filter/DynaCode.java b/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/filter/DynaCode.java deleted file mode 100644 index 92bbf8dad47..00000000000 --- a/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/filter/DynaCode.java +++ /dev/null @@ -1,387 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.filtersrv.filter; - -import java.io.BufferedWriter; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.OutputStreamWriter; -import java.io.UnsupportedEncodingException; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; -import java.net.URLDecoder; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import javax.tools.JavaCompiler; -import javax.tools.ToolProvider; -import org.apache.commons.lang3.StringUtils; -import org.apache.rocketmq.common.UtilAll; -import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.common.filter.FilterAPI; -import org.apache.rocketmq.remoting.common.RemotingHelper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class DynaCode { - private static final Logger log = LoggerFactory.getLogger(LoggerName.FILTERSRV_LOGGER_NAME); - - private static final String FILE_SP = System.getProperty("file.separator"); - - private static final String LINE_SP = System.getProperty("line.separator"); - - private String sourcePath = System.getProperty("user.home") + FILE_SP + "rocketmq_filter_class" + FILE_SP - + UtilAll.getPid(); - - private String outPutClassPath = sourcePath; - - private ClassLoader parentClassLoader; - - private List codeStrs; - - private Map/* class */> loadClass; - - private String classpath; - - private String bootclasspath; - - private String extdirs; - - private String encoding = "UTF-8"; - - private String target; - - public DynaCode(String code) { - this(Thread.currentThread().getContextClassLoader(), Collections.singletonList(code)); - } - - public DynaCode(ClassLoader parentClassLoader, List codeStrs) { - this(extractClasspath(parentClassLoader), parentClassLoader, codeStrs); - } - - public DynaCode(String classpath, ClassLoader parentClassLoader, List codeStrs) { - this.classpath = classpath; - this.parentClassLoader = parentClassLoader; - this.codeStrs = codeStrs; - this.loadClass = new HashMap>(codeStrs.size()); - } - - public DynaCode(List codeStrs) { - this(Thread.currentThread().getContextClassLoader(), codeStrs); - } - - private static String extractClasspath(ClassLoader cl) { - StringBuffer buf = new StringBuffer(); - while (cl != null) { - if (cl instanceof URLClassLoader) { - URL urls[] = ((URLClassLoader) cl).getURLs(); - for (int i = 0; i < urls.length; i++) { - if (buf.length() > 0) { - buf.append(File.pathSeparatorChar); - } - String s = urls[i].getFile(); - try { - s = URLDecoder.decode(s, "UTF-8"); - } catch (UnsupportedEncodingException e) { - continue; - } - File f = new File(s); - buf.append(f.getAbsolutePath()); - } - } - cl = cl.getParent(); - } - return buf.toString(); - } - - public static Class compileAndLoadClass(final String className, final String javaSource) - throws Exception { - String classSimpleName = FilterAPI.simpleClassName(className); - String javaCode = javaSource; - - final String newClassSimpleName = classSimpleName + System.currentTimeMillis(); - String newJavaCode = javaCode.replaceAll(classSimpleName, newClassSimpleName); - - List codes = new ArrayList(); - codes.add(newJavaCode); - DynaCode dc = new DynaCode(codes); - dc.compileAndLoadClass(); - Map> map = dc.getLoadClass(); - - Class clazz = map.get(getQualifiedName(newJavaCode)); - return clazz; - } - - public static String getQualifiedName(String code) { - StringBuilder sb = new StringBuilder(); - String className = getClassName(code); - if (StringUtils.isNotBlank(className)) { - - String packageName = getPackageName(code); - if (StringUtils.isNotBlank(packageName)) { - sb.append(packageName).append("."); - } - sb.append(className); - } - return sb.toString(); - } - - public static String getClassName(String code) { - String className = StringUtils.substringBefore(code, "{"); - if (StringUtils.isBlank(className)) { - return className; - } - if (StringUtils.contains(code, " class ")) { - className = StringUtils.substringAfter(className, " class "); - if (StringUtils.contains(className, " extends ")) { - className = StringUtils.substringBefore(className, " extends ").trim(); - } else if (StringUtils.contains(className, " implements ")) { - className = StringUtils.trim(StringUtils.substringBefore(className, " implements ")); - } else { - className = StringUtils.trim(className); - } - } else if (StringUtils.contains(code, " interface ")) { - className = StringUtils.substringAfter(className, " interface "); - if (StringUtils.contains(className, " extends ")) { - className = StringUtils.substringBefore(className, " extends ").trim(); - } else { - className = StringUtils.trim(className); - } - } else if (StringUtils.contains(code, " enum ")) { - className = StringUtils.trim(StringUtils.substringAfter(className, " enum ")); - } else { - return StringUtils.EMPTY; - } - return className; - } - - public static String getPackageName(String code) { - String packageName = - StringUtils.substringBefore(StringUtils.substringAfter(code, "package "), ";").trim(); - return packageName; - } - - public static String getFullClassName(String code) { - String packageName = getPackageName(code); - String className = getClassName(code); - return StringUtils.isBlank(packageName) ? className : packageName + "." + className; - } - - public void compileAndLoadClass() throws Exception { - String[] sourceFiles = this.uploadSrcFile(); - this.compile(sourceFiles); - this.loadClass(this.loadClass.keySet()); - } - - public Map> getLoadClass() { - return loadClass; - } - - private String[] uploadSrcFile() throws Exception { - List srcFileAbsolutePaths = new ArrayList(codeStrs.size()); - for (String code : codeStrs) { - if (StringUtils.isNotBlank(code)) { - String packageName = getPackageName(code); - String className = getClassName(code); - if (StringUtils.isNotBlank(className)) { - File srcFile = null; - BufferedWriter bufferWriter = null; - try { - if (StringUtils.isBlank(packageName)) { - File pathFile = new File(sourcePath); - - if (!pathFile.exists()) { - if (!pathFile.mkdirs()) { - throw new RuntimeException("create PathFile Error!"); - } - } - srcFile = new File(sourcePath + FILE_SP + className + ".java"); - } else { - String srcPath = StringUtils.replace(packageName, ".", FILE_SP); - File pathFile = new File(sourcePath + FILE_SP + srcPath); - - if (!pathFile.exists()) { - if (!pathFile.mkdirs()) { - throw new RuntimeException("create PathFile Error!"); - } - } - srcFile = new File(pathFile.getAbsolutePath() + FILE_SP + className + ".java"); - } - synchronized (loadClass) { - loadClass.put(getFullClassName(code), null); - } - if (null != srcFile) { - log.warn("Dyna Create Java Source File:----> {}", srcFile.getAbsolutePath()); - srcFileAbsolutePaths.add(srcFile.getAbsolutePath()); - srcFile.deleteOnExit(); - } - OutputStreamWriter outputStreamWriter = - new OutputStreamWriter(new FileOutputStream(srcFile), encoding); - bufferWriter = new BufferedWriter(outputStreamWriter); - for (String lineCode : code.split(LINE_SP)) { - bufferWriter.write(lineCode); - bufferWriter.newLine(); - } - bufferWriter.flush(); - } finally { - if (null != bufferWriter) { - bufferWriter.close(); - } - } - } - } - } - return srcFileAbsolutePaths.toArray(new String[srcFileAbsolutePaths.size()]); - } - - private void compile(String[] srcFiles) throws Exception { - String args[] = this.buildCompileJavacArgs(srcFiles); - ByteArrayOutputStream err = new ByteArrayOutputStream(); - JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); - if (compiler == null) { - throw new NullPointerException( - "ToolProvider.getSystemJavaCompiler() return null,please use JDK replace JRE!"); - } - int resultCode = compiler.run(null, null, err, args); - if (resultCode != 0) { - throw new Exception(err.toString(RemotingHelper.DEFAULT_CHARSET)); - } - } - - private void loadClass(Set classFullNames) throws ClassNotFoundException, MalformedURLException { - synchronized (loadClass) { - ClassLoader classLoader = - new URLClassLoader(new URL[] {new File(outPutClassPath).toURI().toURL()}, - parentClassLoader); - for (String key : classFullNames) { - Class classz = classLoader.loadClass(key); - if (null != classz) { - loadClass.put(key, classz); - log.info("Dyna Load Java Class File OK:----> className: {}", key); - } else { - log.error("Dyna Load Java Class File Fail:----> className: {}", key); - } - } - } - } - - private String[] buildCompileJavacArgs(String srcFiles[]) { - ArrayList args = new ArrayList(); - if (StringUtils.isNotBlank(classpath)) { - args.add("-classpath"); - args.add(classpath); - } - if (StringUtils.isNotBlank(outPutClassPath)) { - args.add("-d"); - args.add(outPutClassPath); - } - if (StringUtils.isNotBlank(sourcePath)) { - args.add("-sourcepath"); - args.add(sourcePath); - } - if (StringUtils.isNotBlank(bootclasspath)) { - args.add("-bootclasspath"); - args.add(bootclasspath); - } - if (StringUtils.isNotBlank(extdirs)) { - args.add("-extdirs"); - args.add(extdirs); - } - if (StringUtils.isNotBlank(encoding)) { - args.add("-encoding"); - args.add(encoding); - } - if (StringUtils.isNotBlank(target)) { - args.add("-target"); - args.add(target); - } - for (int i = 0; i < srcFiles.length; i++) { - args.add(srcFiles[i]); - } - return args.toArray(new String[args.size()]); - } - - public String getOutPutClassPath() { - return outPutClassPath; - } - - public void setOutPutClassPath(String outPutClassPath) { - this.outPutClassPath = outPutClassPath; - } - - public String getSourcePath() { - return sourcePath; - } - - public void setSourcePath(String sourcePath) { - this.sourcePath = sourcePath; - } - - public ClassLoader getParentClassLoader() { - return parentClassLoader; - } - - public void setParentClassLoader(ClassLoader parentClassLoader) { - this.parentClassLoader = parentClassLoader; - } - - public String getClasspath() { - return classpath; - } - - public void setClasspath(String classpath) { - this.classpath = classpath; - } - - public String getBootclasspath() { - return bootclasspath; - } - - public void setBootclasspath(String bootclasspath) { - this.bootclasspath = bootclasspath; - } - - public String getExtdirs() { - return extdirs; - } - - public void setExtdirs(String extdirs) { - this.extdirs = extdirs; - } - - public String getEncoding() { - return encoding; - } - - public void setEncoding(String encoding) { - this.encoding = encoding; - } - - public String getTarget() { - return target; - } - - public void setTarget(String target) { - this.target = target; - } -} \ No newline at end of file diff --git a/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/filter/FilterClassFetchMethod.java b/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/filter/FilterClassFetchMethod.java deleted file mode 100644 index 15e1bb094e3..00000000000 --- a/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/filter/FilterClassFetchMethod.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.filtersrv.filter; - -public interface FilterClassFetchMethod { - public String fetch(final String topic, final String consumerGroup, final String className); -} diff --git a/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/filter/FilterClassInfo.java b/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/filter/FilterClassInfo.java deleted file mode 100644 index b6753928374..00000000000 --- a/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/filter/FilterClassInfo.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.filtersrv.filter; - -import org.apache.rocketmq.common.filter.MessageFilter; - -public class FilterClassInfo { - private String className; - private int classCRC; - private MessageFilter messageFilter; - - public int getClassCRC() { - return classCRC; - } - - public void setClassCRC(int classCRC) { - this.classCRC = classCRC; - } - - public MessageFilter getMessageFilter() { - return messageFilter; - } - - public void setMessageFilter(MessageFilter messageFilter) { - this.messageFilter = messageFilter; - } - - public String getClassName() { - return className; - } - - public void setClassName(String className) { - this.className = className; - } -} diff --git a/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/filter/FilterClassLoader.java b/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/filter/FilterClassLoader.java deleted file mode 100644 index 70f714aa091..00000000000 --- a/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/filter/FilterClassLoader.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.filtersrv.filter; - -public class FilterClassLoader extends ClassLoader { - public final Class createNewClass(String name, byte[] b, int off, int len) throws ClassFormatError { - return this.defineClass(name, b, off, len); - } -} diff --git a/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/filter/FilterClassManager.java b/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/filter/FilterClassManager.java deleted file mode 100644 index 490c5821ab4..00000000000 --- a/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/filter/FilterClassManager.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.filtersrv.filter; - -import java.util.Iterator; -import java.util.Map.Entry; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import org.apache.rocketmq.common.MixAll; -import org.apache.rocketmq.common.ThreadFactoryImpl; -import org.apache.rocketmq.common.UtilAll; -import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.common.filter.MessageFilter; -import org.apache.rocketmq.filtersrv.FiltersrvController; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class FilterClassManager { - private static final Logger log = LoggerFactory.getLogger(LoggerName.FILTERSRV_LOGGER_NAME); - - private final Object compileLock = new Object(); - private final FiltersrvController filtersrvController; - - private final ScheduledExecutorService scheduledExecutorService = Executors - .newSingleThreadScheduledExecutor(new ThreadFactoryImpl("FSGetClassScheduledThread")); - private ConcurrentMap filterClassTable = - new ConcurrentHashMap(128); - private FilterClassFetchMethod filterClassFetchMethod; - - public FilterClassManager(FiltersrvController filtersrvController) { - this.filtersrvController = filtersrvController; - this.filterClassFetchMethod = - new HttpFilterClassFetchMethod(this.filtersrvController.getFiltersrvConfig() - .getFilterClassRepertoryUrl()); - } - - private static String buildKey(final String consumerGroup, final String topic) { - return topic + "@" + consumerGroup; - } - - public void start() { - if (!this.filtersrvController.getFiltersrvConfig().isClientUploadFilterClassEnable()) { - this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { - - @Override - public void run() { - fetchClassFromRemoteHost(); - } - }, 1, 1, TimeUnit.MINUTES); - } - } - - private void fetchClassFromRemoteHost() { - Iterator> it = this.filterClassTable.entrySet().iterator(); - while (it.hasNext()) { - try { - Entry next = it.next(); - FilterClassInfo filterClassInfo = next.getValue(); - String[] topicAndGroup = next.getKey().split("@"); - String responseStr = - this.filterClassFetchMethod.fetch(topicAndGroup[0], topicAndGroup[1], - filterClassInfo.getClassName()); - byte[] filterSourceBinary = responseStr.getBytes("UTF-8"); - int classCRC = UtilAll.crc32(responseStr.getBytes("UTF-8")); - if (classCRC != filterClassInfo.getClassCRC()) { - String javaSource = new String(filterSourceBinary, MixAll.DEFAULT_CHARSET); - Class newClass = - DynaCode.compileAndLoadClass(filterClassInfo.getClassName(), javaSource); - Object newInstance = newClass.newInstance(); - filterClassInfo.setMessageFilter((MessageFilter) newInstance); - filterClassInfo.setClassCRC(classCRC); - - log.info("fetch Remote class File OK, {} {}", next.getKey(), - filterClassInfo.getClassName()); - } - } catch (Exception e) { - log.error("fetchClassFromRemoteHost Exception", e); - } - } - } - - public void shutdown() { - this.scheduledExecutorService.shutdown(); - } - - public boolean registerFilterClass(final String consumerGroup, final String topic, - final String className, final int classCRC, final byte[] filterSourceBinary) { - final String key = buildKey(consumerGroup, topic); - - boolean registerNew = false; - FilterClassInfo filterClassInfoPrev = this.filterClassTable.get(key); - if (null == filterClassInfoPrev) { - registerNew = true; - } else { - if (this.filtersrvController.getFiltersrvConfig().isClientUploadFilterClassEnable()) { - if (filterClassInfoPrev.getClassCRC() != classCRC && classCRC != 0) { - registerNew = true; - } - } - } - - if (registerNew) { - synchronized (this.compileLock) { - filterClassInfoPrev = this.filterClassTable.get(key); - if (null != filterClassInfoPrev && filterClassInfoPrev.getClassCRC() == classCRC) { - return true; - } - - try { - - FilterClassInfo filterClassInfoNew = new FilterClassInfo(); - filterClassInfoNew.setClassName(className); - filterClassInfoNew.setClassCRC(0); - filterClassInfoNew.setMessageFilter(null); - - if (this.filtersrvController.getFiltersrvConfig().isClientUploadFilterClassEnable()) { - String javaSource = new String(filterSourceBinary, MixAll.DEFAULT_CHARSET); - Class newClass = DynaCode.compileAndLoadClass(className, javaSource); - Object newInstance = newClass.newInstance(); - filterClassInfoNew.setMessageFilter((MessageFilter) newInstance); - filterClassInfoNew.setClassCRC(classCRC); - } - - this.filterClassTable.put(key, filterClassInfoNew); - } catch (Throwable e) { - String info = - String - .format( - "FilterServer, registerFilterClass Exception, consumerGroup: %s topic: %s className: %s", - consumerGroup, topic, className); - log.error(info, e); - return false; - } - } - } - - return true; - } - - public FilterClassInfo findFilterClass(final String consumerGroup, final String topic) { - return this.filterClassTable.get(buildKey(consumerGroup, topic)); - } - - public FilterClassFetchMethod getFilterClassFetchMethod() { - return filterClassFetchMethod; - } - - public void setFilterClassFetchMethod(FilterClassFetchMethod filterClassFetchMethod) { - this.filterClassFetchMethod = filterClassFetchMethod; - } -} diff --git a/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/filter/HttpFilterClassFetchMethod.java b/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/filter/HttpFilterClassFetchMethod.java deleted file mode 100644 index 8e7d695f6d2..00000000000 --- a/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/filter/HttpFilterClassFetchMethod.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.filtersrv.filter; - -import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.common.utils.HttpTinyClient; -import org.apache.rocketmq.common.utils.HttpTinyClient.HttpResult; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class HttpFilterClassFetchMethod implements FilterClassFetchMethod { - private static final Logger log = LoggerFactory.getLogger(LoggerName.FILTERSRV_LOGGER_NAME); - private final String url; - - public HttpFilterClassFetchMethod(String url) { - this.url = url; - } - - @Override - public String fetch(String topic, String consumerGroup, String className) { - String thisUrl = String.format("%s/%s.java", this.url, className); - - try { - HttpResult result = HttpTinyClient.httpGet(thisUrl, null, null, "UTF-8", 5000); - if (200 == result.code) { - return result.content; - } - } catch (Exception e) { - log.error( - String.format("call <%s> exception, Topic: %s Group: %s", thisUrl, topic, consumerGroup), e); - } - - return null; - } -} diff --git a/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/processor/DefaultRequestProcessor.java b/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/processor/DefaultRequestProcessor.java deleted file mode 100644 index e459b1aeb6d..00000000000 --- a/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/processor/DefaultRequestProcessor.java +++ /dev/null @@ -1,347 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.filtersrv.processor; - -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelFutureListener; -import io.netty.channel.ChannelHandlerContext; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; -import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; -import org.apache.rocketmq.client.consumer.PullCallback; -import org.apache.rocketmq.client.consumer.PullResult; -import org.apache.rocketmq.common.MixAll; -import org.apache.rocketmq.common.UtilAll; -import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.common.filter.FilterContext; -import org.apache.rocketmq.common.message.MessageDecoder; -import org.apache.rocketmq.common.message.MessageExt; -import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.RequestCode; -import org.apache.rocketmq.common.protocol.ResponseCode; -import org.apache.rocketmq.common.protocol.header.PullMessageRequestHeader; -import org.apache.rocketmq.common.protocol.header.PullMessageResponseHeader; -import org.apache.rocketmq.common.protocol.header.filtersrv.RegisterMessageFilterClassRequestHeader; -import org.apache.rocketmq.common.sysflag.MessageSysFlag; -import org.apache.rocketmq.filtersrv.FiltersrvController; -import org.apache.rocketmq.filtersrv.filter.FilterClassInfo; -import org.apache.rocketmq.remoting.common.RemotingHelper; -import org.apache.rocketmq.remoting.exception.RemotingCommandException; -import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; -import org.apache.rocketmq.remoting.protocol.RemotingCommand; -import org.apache.rocketmq.store.CommitLog; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class DefaultRequestProcessor implements NettyRequestProcessor { - private static final Logger log = LoggerFactory.getLogger(LoggerName.FILTERSRV_LOGGER_NAME); - - private final FiltersrvController filtersrvController; - - public DefaultRequestProcessor(FiltersrvController filtersrvController) { - this.filtersrvController = filtersrvController; - } - - @Override - public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) throws Exception { - if (log.isDebugEnabled()) { - log.debug("receive request, {} {} {}", - request.getCode(), - RemotingHelper.parseChannelRemoteAddr(ctx.channel()), - request); - } - - switch (request.getCode()) { - case RequestCode.REGISTER_MESSAGE_FILTER_CLASS: - return registerMessageFilterClass(ctx, request); - case RequestCode.PULL_MESSAGE: - return pullMessageForward(ctx, request); - } - - return null; - } - - @Override - public boolean rejectRequest() { - return false; - } - - private RemotingCommand registerMessageFilterClass(ChannelHandlerContext ctx, - RemotingCommand request) throws RemotingCommandException { - final RemotingCommand response = RemotingCommand.createResponseCommand(null); - final RegisterMessageFilterClassRequestHeader requestHeader = - (RegisterMessageFilterClassRequestHeader) request.decodeCommandCustomHeader(RegisterMessageFilterClassRequestHeader.class); - - try { - boolean ok = this.filtersrvController.getFilterClassManager().registerFilterClass(requestHeader.getConsumerGroup(), - requestHeader.getTopic(), - requestHeader.getClassName(), - requestHeader.getClassCRC(), - request.getBody()); - if (!ok) { - throw new Exception("registerFilterClass error"); - } - } catch (Exception e) { - response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark(RemotingHelper.exceptionSimpleDesc(e)); - return response; - } - - response.setCode(ResponseCode.SUCCESS); - response.setRemark(null); - return response; - } - - private RemotingCommand pullMessageForward(final ChannelHandlerContext ctx, - final RemotingCommand request) throws Exception { - final RemotingCommand response = RemotingCommand.createResponseCommand(PullMessageResponseHeader.class); - final PullMessageResponseHeader responseHeader = (PullMessageResponseHeader) response.readCustomHeader(); - final PullMessageRequestHeader requestHeader = - (PullMessageRequestHeader) request.decodeCommandCustomHeader(PullMessageRequestHeader.class); - - final FilterContext filterContext = new FilterContext(); - filterContext.setConsumerGroup(requestHeader.getConsumerGroup()); - - response.setOpaque(request.getOpaque()); - - DefaultMQPullConsumer pullConsumer = this.filtersrvController.getDefaultMQPullConsumer(); - final FilterClassInfo findFilterClass = - this.filtersrvController.getFilterClassManager() - .findFilterClass(requestHeader.getConsumerGroup(), requestHeader.getTopic()); - if (null == findFilterClass) { - response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark("Find Filter class failed, not registered"); - return response; - } - - if (null == findFilterClass.getMessageFilter()) { - response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark("Find Filter class failed, registered but no class"); - return response; - } - - responseHeader.setSuggestWhichBrokerId(MixAll.MASTER_ID); - - MessageQueue mq = new MessageQueue(); - mq.setTopic(requestHeader.getTopic()); - mq.setQueueId(requestHeader.getQueueId()); - mq.setBrokerName(this.filtersrvController.getBrokerName()); - long offset = requestHeader.getQueueOffset(); - int maxNums = requestHeader.getMaxMsgNums(); - - final PullCallback pullCallback = new PullCallback() { - - @Override - public void onSuccess(PullResult pullResult) { - responseHeader.setMaxOffset(pullResult.getMaxOffset()); - responseHeader.setMinOffset(pullResult.getMinOffset()); - responseHeader.setNextBeginOffset(pullResult.getNextBeginOffset()); - response.setRemark(null); - - switch (pullResult.getPullStatus()) { - case FOUND: - response.setCode(ResponseCode.SUCCESS); - - List msgListOK = new ArrayList(); - try { - for (MessageExt msg : pullResult.getMsgFoundList()) { - boolean match = findFilterClass.getMessageFilter().match(msg, filterContext); - if (match) { - msgListOK.add(msg); - } - } - - if (!msgListOK.isEmpty()) { - returnResponse(requestHeader.getConsumerGroup(), requestHeader.getTopic(), ctx, response, msgListOK); - return; - } else { - response.setCode(ResponseCode.PULL_RETRY_IMMEDIATELY); - } - } catch (Throwable e) { - final String error = - String.format("do Message Filter Exception, ConsumerGroup: %s Topic: %s ", - requestHeader.getConsumerGroup(), requestHeader.getTopic()); - log.error(error, e); - - response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark(error + RemotingHelper.exceptionSimpleDesc(e)); - returnResponse(requestHeader.getConsumerGroup(), requestHeader.getTopic(), ctx, response, null); - return; - } - - break; - case NO_MATCHED_MSG: - response.setCode(ResponseCode.PULL_RETRY_IMMEDIATELY); - break; - case NO_NEW_MSG: - response.setCode(ResponseCode.PULL_NOT_FOUND); - break; - case OFFSET_ILLEGAL: - response.setCode(ResponseCode.PULL_OFFSET_MOVED); - break; - default: - break; - } - - returnResponse(requestHeader.getConsumerGroup(), requestHeader.getTopic(), ctx, response, null); - } - - @Override - public void onException(Throwable e) { - response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark("Pull Callback Exception, " + RemotingHelper.exceptionSimpleDesc(e)); - returnResponse(requestHeader.getConsumerGroup(), requestHeader.getTopic(), ctx, response, null); - return; - } - }; - - pullConsumer.pullBlockIfNotFound(mq, null, offset, maxNums, pullCallback); - - return null; - } - - private void returnResponse(final String group, final String topic, ChannelHandlerContext ctx, - final RemotingCommand response, - final List msgList) { - if (null != msgList) { - ByteBuffer[] msgBufferList = new ByteBuffer[msgList.size()]; - int bodyTotalSize = 0; - for (int i = 0; i < msgList.size(); i++) { - try { - msgBufferList[i] = messageToByteBuffer(msgList.get(i)); - bodyTotalSize += msgBufferList[i].capacity(); - } catch (Exception e) { - log.error("messageToByteBuffer UnsupportedEncodingException", e); - } - } - - ByteBuffer body = ByteBuffer.allocate(bodyTotalSize); - for (ByteBuffer bb : msgBufferList) { - bb.flip(); - body.put(bb); - } - - response.setBody(body.array()); - - this.filtersrvController.getFilterServerStatsManager().incGroupGetNums(group, topic, msgList.size()); - - this.filtersrvController.getFilterServerStatsManager().incGroupGetSize(group, topic, bodyTotalSize); - } - - try { - ctx.writeAndFlush(response).addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) throws Exception { - if (!future.isSuccess()) { - log.error("FilterServer response to " + future.channel().remoteAddress() + " failed", future.cause()); - log.error(response.toString()); - } - } - }); - } catch (Throwable e) { - log.error("FilterServer process request over, but response failed", e); - log.error(response.toString()); - } - } - - private ByteBuffer messageToByteBuffer(final MessageExt msg) throws IOException { - int sysFlag = MessageSysFlag.clearCompressedFlag(msg.getSysFlag()); - if (msg.getBody() != null) { - if (msg.getBody().length >= this.filtersrvController.getFiltersrvConfig().getCompressMsgBodyOverHowmuch()) { - byte[] data = UtilAll.compress(msg.getBody(), this.filtersrvController.getFiltersrvConfig().getZipCompressLevel()); - if (data != null) { - msg.setBody(data); - sysFlag |= MessageSysFlag.COMPRESSED_FLAG; - } - } - } - - final int bodyLength = msg.getBody() != null ? msg.getBody().length : 0; - byte[] topicData = msg.getTopic().getBytes(MixAll.DEFAULT_CHARSET); - final int topicLength = topicData.length; - String properties = MessageDecoder.messageProperties2String(msg.getProperties()); - byte[] propertiesData = properties.getBytes(MixAll.DEFAULT_CHARSET); - final int propertiesLength = propertiesData.length; - final int msgLen = 4 // 1 TOTALSIZE - + 4 // 2 MAGICCODE - + 4 // 3 BODYCRC - + 4 // 4 QUEUEID - + 4 // 5 FLAG - + 8 // 6 QUEUEOFFSET - + 8 // 7 PHYSICALOFFSET - + 4 // 8 SYSFLAG - + 8 // 9 BORNTIMESTAMP - + 8 // 10 BORNHOST - + 8 // 11 STORETIMESTAMP - + 8 // 12 STOREHOSTADDRESS - + 4 // 13 RECONSUMETIMES - + 8 // 14 Prepared Transaction Offset - + 4 + bodyLength // 14 BODY - + 1 + topicLength // 15 TOPIC - + 2 + propertiesLength // 16 propertiesLength - + 0; - - ByteBuffer msgStoreItemMemory = ByteBuffer.allocate(msgLen); - - final MessageExt msgInner = msg; - - // 1 TOTALSIZE - msgStoreItemMemory.putInt(msgLen); - // 2 MAGICCODE - msgStoreItemMemory.putInt(CommitLog.MESSAGE_MAGIC_CODE); - // 3 BODYCRC - msgStoreItemMemory.putInt(UtilAll.crc32(msgInner.getBody())); - // 4 QUEUEID - msgStoreItemMemory.putInt(msgInner.getQueueId()); - // 5 FLAG - msgStoreItemMemory.putInt(msgInner.getFlag()); - // 6 QUEUEOFFSET - msgStoreItemMemory.putLong(msgInner.getQueueOffset()); - // 7 PHYSICALOFFSET - msgStoreItemMemory.putLong(msgInner.getCommitLogOffset()); - // 8 SYSFLAG - msgStoreItemMemory.putInt(sysFlag); - // 9 BORNTIMESTAMP - msgStoreItemMemory.putLong(msgInner.getBornTimestamp()); - // 10 BORNHOST - msgStoreItemMemory.put(msgInner.getBornHostBytes()); - // 11 STORETIMESTAMP - msgStoreItemMemory.putLong(msgInner.getStoreTimestamp()); - // 12 STOREHOSTADDRESS - msgStoreItemMemory.put(msgInner.getStoreHostBytes()); - // 13 RECONSUMETIMES - msgStoreItemMemory.putInt(msgInner.getReconsumeTimes()); - // 14 Prepared Transaction Offset - msgStoreItemMemory.putLong(msgInner.getPreparedTransactionOffset()); - // 15 BODY - msgStoreItemMemory.putInt(bodyLength); - if (bodyLength > 0) - msgStoreItemMemory.put(msgInner.getBody()); - // 16 TOPIC - msgStoreItemMemory.put((byte) topicLength); - msgStoreItemMemory.put(topicData); - // 17 PROPERTIES - msgStoreItemMemory.putShort((short) propertiesLength); - if (propertiesLength > 0) - msgStoreItemMemory.put(propertiesData); - - return msgStoreItemMemory; - } -} diff --git a/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/stats/FilterServerStatsManager.java b/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/stats/FilterServerStatsManager.java deleted file mode 100644 index 67e722c67ea..00000000000 --- a/filtersrv/src/main/java/org/apache/rocketmq/filtersrv/stats/FilterServerStatsManager.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.filtersrv.stats; - -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import org.apache.rocketmq.common.ThreadFactoryImpl; -import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.common.stats.StatsItemSet; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class FilterServerStatsManager { - private static final Logger log = LoggerFactory.getLogger(LoggerName.FILTERSRV_LOGGER_NAME); - private final ScheduledExecutorService scheduledExecutorService = Executors - .newSingleThreadScheduledExecutor(new ThreadFactoryImpl("FSStatsThread")); - - // ConsumerGroup Get Nums - private final StatsItemSet groupGetNums = new StatsItemSet("GROUP_GET_NUMS", - this.scheduledExecutorService, log); - - // ConsumerGroup Get Size - private final StatsItemSet groupGetSize = new StatsItemSet("GROUP_GET_SIZE", - this.scheduledExecutorService, log); - - public FilterServerStatsManager() { - } - - public void start() { - } - - public void shutdown() { - this.scheduledExecutorService.shutdown(); - } - - public void incGroupGetNums(final String group, final String topic, final int incValue) { - this.groupGetNums.addValue(topic + "@" + group, incValue, 1); - } - - public void incGroupGetSize(final String group, final String topic, final int incValue) { - this.groupGetSize.addValue(topic + "@" + group, incValue, 1); - } -} diff --git a/logappender/pom.xml b/logappender/pom.xml deleted file mode 100644 index 0e0ac44911b..00000000000 --- a/logappender/pom.xml +++ /dev/null @@ -1,85 +0,0 @@ - - - - - org.apache.rocketmq - rocketmq-all - 4.2.0 - - 4.0.0 - rocketmq-logappender - jar - rocketmq-logappender ${project.version} - - - - org.slf4j - slf4j-api - true - - - log4j - log4j - true - - - org.apache.logging.log4j - log4j-core - true - - - ch.qos.logback - logback-core - true - - - ch.qos.logback - logback-classic - true - - - ${project.groupId} - rocketmq-client - - - ${project.groupId} - rocketmq-namesrv - test - - - ch.qos.logback - logback-classic - - - - - ${project.groupId} - rocketmq-broker - test - - - ch.qos.logback - logback-classic - - - - - - - - diff --git a/logappender/src/main/java/org/apache/rocketmq/logappender/common/ProducerInstance.java b/logappender/src/main/java/org/apache/rocketmq/logappender/common/ProducerInstance.java deleted file mode 100644 index d2adac53dbc..00000000000 --- a/logappender/src/main/java/org/apache/rocketmq/logappender/common/ProducerInstance.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.logappender.common; - -import org.apache.commons.lang3.StringUtils; -import org.apache.rocketmq.client.exception.MQClientException; -import org.apache.rocketmq.client.producer.DefaultMQProducer; -import org.apache.rocketmq.client.producer.MQProducer; - -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Common Producer component - */ -public class ProducerInstance { - - public static final String APPENDER_TYPE = "APPENDER_TYPE"; - - public static final String LOG4J_APPENDER = "LOG4J_APPENDER"; - - public static final String LOG4J2_APPENDER = "LOG4J2_APPENDER"; - - public static final String LOGBACK_APPENDER = "LOGBACK_APPENDER"; - - public static final String DEFAULT_GROUP = "rocketmq_appender"; - - private ConcurrentHashMap producerMap = new ConcurrentHashMap(); - - private static ProducerInstance instance = new ProducerInstance(); - - public static ProducerInstance getProducerInstance() { - return instance; - } - - private String genKey(String nameServerAddress, String group) { - return nameServerAddress + "_" + group; - } - - public MQProducer getInstance(String nameServerAddress, String group) throws MQClientException { - if (StringUtils.isBlank(group)) { - group = DEFAULT_GROUP; - } - - String genKey = genKey(nameServerAddress, group); - MQProducer p = getProducerInstance().producerMap.get(genKey); - if (p != null) { - return p; - } - - DefaultMQProducer defaultMQProducer = new DefaultMQProducer(group); - defaultMQProducer.setNamesrvAddr(nameServerAddress); - MQProducer beforeProducer = null; - beforeProducer = getProducerInstance().producerMap.putIfAbsent(genKey, defaultMQProducer); - if (beforeProducer != null) { - return beforeProducer; - } - defaultMQProducer.start(); - return defaultMQProducer; - } - - public void removeAndClose(String nameServerAddress, String group) { - if (group == null) { - group = DEFAULT_GROUP; - } - String genKey = genKey(nameServerAddress, group); - MQProducer producer = getProducerInstance().producerMap.remove(genKey); - - if (producer != null) { - producer.shutdown(); - } - } - - public void closeAll() { - Set> entries = getProducerInstance().producerMap.entrySet(); - for (Map.Entry entry : entries) { - getProducerInstance().producerMap.remove(entry.getKey()); - entry.getValue().shutdown(); - } - } - -} diff --git a/logappender/src/main/java/org/apache/rocketmq/logappender/log4j/RocketmqLog4jAppender.java b/logappender/src/main/java/org/apache/rocketmq/logappender/log4j/RocketmqLog4jAppender.java deleted file mode 100644 index 646e924828a..00000000000 --- a/logappender/src/main/java/org/apache/rocketmq/logappender/log4j/RocketmqLog4jAppender.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.logappender.log4j; - -import org.apache.rocketmq.common.message.Message; -import org.apache.rocketmq.logappender.common.ProducerInstance; -import org.apache.log4j.AppenderSkeleton; -import org.apache.log4j.helpers.LogLog; -import org.apache.log4j.spi.ErrorCode; -import org.apache.log4j.spi.LoggingEvent; -import org.apache.rocketmq.client.producer.MQProducer; - -/** - * Log4j Appender Component - */ -public class RocketmqLog4jAppender extends AppenderSkeleton { - - /** - * Appended message tag define - */ - private String tag; - - /** - * Whitch topic to send log messages - */ - private String topic; - - private boolean locationInfo; - - /** - * Log producer send instance - */ - private MQProducer producer; - - /** - * RocketMQ nameserver address - */ - private String nameServerAddress; - - /** - * Log producer group - */ - private String producerGroup; - - public RocketmqLog4jAppender() { - } - - public void activateOptions() { - LogLog.debug("Getting initial context."); - if (!checkEntryConditions()) { - return; - } - try { - producer = ProducerInstance.getProducerInstance().getInstance(nameServerAddress, producerGroup); - } catch (Exception e) { - LogLog.error("activateOptions nameserver:" + nameServerAddress + " group:" + producerGroup + " " + e.getMessage()); - } - } - - /** - * Info,error,warn,callback method implementation - */ - public void append(LoggingEvent event) { - if (null == producer) { - return; - } - if (locationInfo) { - event.getLocationInformation(); - } - byte[] data = this.layout.format(event).getBytes(); - try { - Message msg = new Message(topic, tag, data); - msg.getProperties().put(ProducerInstance.APPENDER_TYPE, ProducerInstance.LOG4J_APPENDER); - - //Send message and do not wait for the ack from the message broker. - producer.sendOneway(msg); - } catch (Exception e) { - String msg = new String(data); - errorHandler.error("Could not send message in RocketmqLog4jAppender [" + name + "].Message is :" + msg, e, - ErrorCode.GENERIC_FAILURE); - } - } - - protected boolean checkEntryConditions() { - String fail = null; - - if (this.topic == null) { - fail = "No topic"; - } else if (this.tag == null) { - fail = "No tag"; - } - - if (fail != null) { - errorHandler.error(fail + " for RocketmqLog4jAppender named [" + name + "]."); - return false; - } else { - return true; - } - } - - /** - * When system exit,this method will be called to close resources - */ - public synchronized void close() { - // The synchronized modifier avoids concurrent append and close operations - - if (this.closed) - return; - - LogLog.debug("Closing RocketmqLog4jAppender [" + name + "]."); - this.closed = true; - - try { - ProducerInstance.getProducerInstance().removeAndClose(this.nameServerAddress, this.producerGroup); - } catch (Exception e) { - LogLog.error("Closing RocketmqLog4jAppender [" + name + "] nameServerAddress:" + nameServerAddress + " group:" + producerGroup + " " + e.getMessage()); - } - // Help garbage collection - producer = null; - } - - public boolean requiresLayout() { - return true; - } - - public String getTopic() { - return topic; - } - - public void setTopic(String topic) { - this.topic = topic; - } - - public String getTag() { - return tag; - } - - public void setTag(String tag) { - this.tag = tag; - } - - /** - * Returns value of the LocationInfo property which - * determines whether location (stack) info is sent to the remote - * subscriber. - */ - public boolean isLocationInfo() { - return locationInfo; - } - - /** - * If true, the information sent to the remote subscriber will - * include caller's location information. By default no location - * information is sent to the subscriber. - */ - public void setLocationInfo(boolean locationInfo) { - this.locationInfo = locationInfo; - } - - /** - * Returns the message producer,Only valid after - * activateOptions() method has been invoked. - */ - protected MQProducer getProducer() { - return producer; - } - - public void setNameServerAddress(String nameServerAddress) { - this.nameServerAddress = nameServerAddress; - } - - public void setProducerGroup(String producerGroup) { - this.producerGroup = producerGroup; - } -} diff --git a/logappender/src/main/java/org/apache/rocketmq/logappender/log4j2/RocketmqLog4j2Appender.java b/logappender/src/main/java/org/apache/rocketmq/logappender/log4j2/RocketmqLog4j2Appender.java deleted file mode 100644 index 9543f1c2917..00000000000 --- a/logappender/src/main/java/org/apache/rocketmq/logappender/log4j2/RocketmqLog4j2Appender.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.logappender.log4j2; - -import org.apache.logging.log4j.core.Appender; -import org.apache.logging.log4j.core.Filter; -import org.apache.logging.log4j.core.ErrorHandler; -import org.apache.logging.log4j.core.Layout; -import org.apache.logging.log4j.core.LogEvent; -import org.apache.logging.log4j.core.config.Node; -import org.apache.logging.log4j.core.config.plugins.Plugin; -import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; -import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; -import org.apache.logging.log4j.core.config.plugins.PluginElement; -import org.apache.rocketmq.common.message.Message; -import org.apache.rocketmq.logappender.common.ProducerInstance; -import org.apache.logging.log4j.core.appender.AbstractAppender; -import org.apache.logging.log4j.core.config.plugins.validation.constraints.Required; -import org.apache.logging.log4j.core.layout.SerializedLayout; -import org.apache.rocketmq.client.producer.MQProducer; - -import java.io.Serializable; -import java.util.concurrent.TimeUnit; - -/** - * Log4j2 Appender Component - */ -@Plugin(name = "RocketMQ", - category = Node.CATEGORY, - elementType = Appender.ELEMENT_TYPE, - printObject = true) -public class RocketmqLog4j2Appender extends AbstractAppender { - - /** - * RocketMQ nameserver address - */ - private String nameServerAddress; - - /** - * Log producer group - */ - private String producerGroup; - - /** - * Log producer send instance - */ - private MQProducer producer; - - /** - * Appended message tag define - */ - private String tag; - - /** - * Whitch topic to send log messages - */ - private String topic; - - protected RocketmqLog4j2Appender(String name, Filter filter, Layout layout, - boolean ignoreExceptions, String nameServerAddress, String producerGroup, - String topic, String tag) { - super(name, filter, layout, ignoreExceptions); - this.producer = producer; - this.topic = topic; - this.tag = tag; - this.nameServerAddress = nameServerAddress; - this.producerGroup = producerGroup; - try { - this.producer = ProducerInstance.getProducerInstance().getInstance(this.nameServerAddress, this.producerGroup); - } catch (Exception e) { - ErrorHandler handler = this.getHandler(); - if (handler != null) { - handler.error("Starting RocketmqLog4j2Appender [" + this.getName() - + "] nameServerAddress:" + nameServerAddress + " group:" + producerGroup + " " + e.getMessage()); - } - } - } - - /** - * Info,error,warn,callback method implementation - */ - public void append(LogEvent event) { - if (null == producer) { - return; - } - byte[] data = this.getLayout().toByteArray(event); - try { - Message msg = new Message(topic, tag, data); - msg.getProperties().put(ProducerInstance.APPENDER_TYPE, ProducerInstance.LOG4J2_APPENDER); - - //Send message and do not wait for the ack from the message broker. - producer.sendOneway(msg); - } catch (Exception e) { - ErrorHandler handler = this.getHandler(); - if (handler != null) { - String msg = new String(data); - handler.error("Could not send message in RocketmqLog4j2Appender [" + this.getName() + "].Message is : " + msg, e); - } - - } - } - - /** - * When system exit,this method will be called to close resources - */ - public boolean stop(long timeout, TimeUnit timeUnit) { - this.setStopping(); - try { - ProducerInstance.getProducerInstance().removeAndClose(this.nameServerAddress, this.producerGroup); - } catch (Exception e) { - ErrorHandler handler = this.getHandler(); - if (handler != null) { - handler.error("Closeing RocketmqLog4j2Appender [" + this.getName() - + "] nameServerAddress:" + nameServerAddress + " group:" + producerGroup + " " + e.getMessage()); - } - } - - boolean stopped = super.stop(timeout, timeUnit, false); - this.setStopped(); - return stopped; - } - - /** - * Log4j2 builder creator - */ - @PluginBuilderFactory - public static RocketmqLog4j2Appender.Builder newBuilder() { - return new RocketmqLog4j2Appender.Builder(); - } - - /** - * Log4j2 xml builder define - */ - public static class Builder implements org.apache.logging.log4j.core.util.Builder { - - @PluginBuilderAttribute - @Required(message = "A name for the RocketmqLog4j2Appender must be specified") - private String name; - - @PluginElement("Layout") - private Layout layout; - - @PluginElement("Filter") - private Filter filter; - - @PluginBuilderAttribute - private boolean ignoreExceptions; - - @PluginBuilderAttribute - private String tag; - - @PluginBuilderAttribute - private String nameServerAddress; - - @PluginBuilderAttribute - private String producerGroup; - - @PluginBuilderAttribute - @Required(message = "A topic name must be specified") - private String topic; - - private Builder() { - this.layout = SerializedLayout.createLayout(); - this.ignoreExceptions = true; - } - - public RocketmqLog4j2Appender.Builder setName(String name) { - this.name = name; - return this; - } - - public RocketmqLog4j2Appender.Builder setLayout(Layout layout) { - this.layout = layout; - return this; - } - - public RocketmqLog4j2Appender.Builder setFilter(Filter filter) { - this.filter = filter; - return this; - } - - public RocketmqLog4j2Appender.Builder setIgnoreExceptions(boolean ignoreExceptions) { - this.ignoreExceptions = ignoreExceptions; - return this; - } - - public RocketmqLog4j2Appender.Builder setTag(final String tag) { - this.tag = tag; - return this; - } - - public RocketmqLog4j2Appender.Builder setTopic(final String topic) { - this.topic = topic; - return this; - } - - public RocketmqLog4j2Appender.Builder setNameServerAddress(String nameServerAddress) { - this.nameServerAddress = nameServerAddress; - return this; - } - - public RocketmqLog4j2Appender.Builder setProducerGroup(String producerGroup) { - this.producerGroup = producerGroup; - return this; - } - - public RocketmqLog4j2Appender build() { - return new RocketmqLog4j2Appender(name, filter, layout, ignoreExceptions, - nameServerAddress, producerGroup, topic, tag); - } - } -} diff --git a/logappender/src/main/java/org/apache/rocketmq/logappender/logback/RocketmqLogbackAppender.java b/logappender/src/main/java/org/apache/rocketmq/logappender/logback/RocketmqLogbackAppender.java deleted file mode 100644 index 4018cd44a17..00000000000 --- a/logappender/src/main/java/org/apache/rocketmq/logappender/logback/RocketmqLogbackAppender.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.logappender.logback; - -import ch.qos.logback.classic.net.LoggingEventPreSerializationTransformer; -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.AppenderBase; -import ch.qos.logback.core.Layout; -import ch.qos.logback.core.spi.PreSerializationTransformer; -import ch.qos.logback.core.status.ErrorStatus; -import org.apache.rocketmq.common.message.Message; -import org.apache.rocketmq.logappender.common.ProducerInstance; -import org.apache.rocketmq.client.producer.MQProducer; - -/** - * Logback Appender Component - */ -public class RocketmqLogbackAppender extends AppenderBase { - - /** - * Message tag define - */ - private String tag; - - /** - * Whitch topic to send log messages - */ - private String topic; - - /** - * RocketMQ nameserver address - */ - private String nameServerAddress; - - /** - * Log producer group - */ - private String producerGroup; - - /** - * Log producer send instance - */ - private MQProducer producer; - - private Layout layout; - - private PreSerializationTransformer pst = new LoggingEventPreSerializationTransformer(); - - /** - * Info,error,warn,callback method implementation - */ - @Override - protected void append(ILoggingEvent event) { - if (!isStarted()) { - return; - } - String logStr = this.layout.doLayout(event); - try { - Message msg = new Message(topic, tag, logStr.getBytes()); - msg.getProperties().put(ProducerInstance.APPENDER_TYPE, ProducerInstance.LOGBACK_APPENDER); - - //Send message and do not wait for the ack from the message broker. - producer.sendOneway(msg); - } catch (Exception e) { - addError("Could not send message in RocketmqLogbackAppender [" + name + "]. Message is : " + logStr, e); - } - } - - /** - * Options are activated and become effective only after calling this method. - */ - public void start() { - int errors = 0; - - if (this.layout == null) { - addStatus(new ErrorStatus("No layout set for the RocketmqLogbackAppender named \"" + name + "\".", this)); - errors++; - } - - if (errors > 0 || !checkEntryConditions()) { - return; - } - try { - producer = ProducerInstance.getProducerInstance().getInstance(nameServerAddress, producerGroup); - } catch (Exception e) { - addError("Starting RocketmqLogbackAppender [" + this.getName() - + "] nameServerAddress:" + nameServerAddress + " group:" + producerGroup + " " + e.getMessage()); - } - if (producer != null) { - super.start(); - } - } - - /** - * When system exit,this method will be called to close resources - */ - public synchronized void stop() { - // The synchronized modifier avoids concurrent append and close operations - if (!this.started) { - return; - } - - this.started = false; - - try { - ProducerInstance.getProducerInstance().removeAndClose(this.nameServerAddress, this.producerGroup); - } catch (Exception e) { - addError("Closeing RocketmqLogbackAppender [" + this.getName() - + "] nameServerAddress:" + nameServerAddress + " group:" + producerGroup + " " + e.getMessage()); - } - - // Help garbage collection - producer = null; - } - - protected boolean checkEntryConditions() { - String fail = null; - - if (this.topic == null) { - fail = "No topic"; - } - - if (fail != null) { - addError(fail + " for RocketmqLogbackAppender named [" + name + "]."); - return false; - } else { - return true; - } - } - - public Layout getLayout() { - return this.layout; - } - - /** - * Set the pattern layout to format the log. - */ - public void setLayout(Layout layout) { - this.layout = layout; - } - - public String getTag() { - return tag; - } - - public void setTag(String tag) { - this.tag = tag; - } - - public String getTopic() { - return topic; - } - - public void setTopic(String topic) { - this.topic = topic; - } - - public void setNameServerAddress(String nameServerAddress) { - this.nameServerAddress = nameServerAddress; - } - - public void setProducerGroup(String producerGroup) { - this.producerGroup = producerGroup; - } -} diff --git a/logappender/src/test/java/org/apache/rocketmq/logappender/AbstractTestCase.java b/logappender/src/test/java/org/apache/rocketmq/logappender/AbstractTestCase.java deleted file mode 100644 index 38904c0bfc7..00000000000 --- a/logappender/src/test/java/org/apache/rocketmq/logappender/AbstractTestCase.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.logappender; - -import org.apache.rocketmq.client.producer.DefaultMQProducer; - -import org.apache.rocketmq.common.message.*; -import org.apache.rocketmq.logappender.common.ProducerInstance; -import org.junit.Before; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -import static org.mockito.Mockito.*; - -import java.lang.reflect.Field; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * Basic test rocketmq broker and name server init - */ -public class AbstractTestCase { - - private static CopyOnWriteArrayList messages = new CopyOnWriteArrayList<>(); - - @Before - public void mockLoggerAppender() throws Exception { - DefaultMQProducer defaultMQProducer = spy(new DefaultMQProducer("loggerAppender")); - doAnswer(new Answer() { - @Override - public Void answer(InvocationOnMock invocationOnMock) throws Throwable { - Message message = (Message) invocationOnMock.getArgument(0); - messages.add(message); - return null; - } - }).when(defaultMQProducer).sendOneway(any(Message.class)); - ProducerInstance spy = mock(ProducerInstance.class); - Field instance = ProducerInstance.class.getDeclaredField("instance"); - instance.setAccessible(true); - instance.set(ProducerInstance.class, spy); - doReturn(defaultMQProducer).when(spy).getInstance(anyString(), anyString()); - } - - public void clear() { - - } - - protected int consumeMessages(int count, final String key, int timeout) { - final AtomicInteger cc = new AtomicInteger(0); - for (Message message : messages) { - String body = new String(message.getBody()); - if (body.contains(key)) { - cc.incrementAndGet(); - } - } - return cc.get(); - } -} diff --git a/logappender/src/test/java/org/apache/rocketmq/logappender/Log4jPropertiesTest.java b/logappender/src/test/java/org/apache/rocketmq/logappender/Log4jPropertiesTest.java deleted file mode 100644 index 86752301a19..00000000000 --- a/logappender/src/test/java/org/apache/rocketmq/logappender/Log4jPropertiesTest.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.logappender; - -import org.apache.log4j.PropertyConfigurator; - -public class Log4jPropertiesTest extends Log4jTest { - - @Override - public void init() { - PropertyConfigurator.configure("src/test/resources/log4j-example.properties"); - } - - @Override - public String getType() { - return "properties"; - } -} diff --git a/logappender/src/test/java/org/apache/rocketmq/logappender/Log4jTest.java b/logappender/src/test/java/org/apache/rocketmq/logappender/Log4jTest.java deleted file mode 100644 index c139283a0c9..00000000000 --- a/logappender/src/test/java/org/apache/rocketmq/logappender/Log4jTest.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.logappender; - -import org.apache.log4j.Logger; -import org.apache.rocketmq.client.exception.MQClientException; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -public abstract class Log4jTest extends AbstractTestCase { - - @Before - public abstract void init(); - - public abstract String getType(); - - @Test - public void testLog4j() throws InterruptedException, MQClientException { - clear(); - Logger logger = Logger.getLogger("testLogger"); - for (int i = 0; i < 10; i++) { - logger.info("log4j " + this.getType() + " simple test message " + i); - } - int received = consumeMessages(10, "log4j", 10); - Assert.assertTrue(received > 5); - } - -} diff --git a/logappender/src/test/java/org/apache/rocketmq/logappender/Log4jXmlTest.java b/logappender/src/test/java/org/apache/rocketmq/logappender/Log4jXmlTest.java deleted file mode 100644 index 6743f7c1668..00000000000 --- a/logappender/src/test/java/org/apache/rocketmq/logappender/Log4jXmlTest.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.logappender; - -import org.apache.log4j.xml.DOMConfigurator; - -public class Log4jXmlTest extends Log4jTest { - - @Override - public void init() { - DOMConfigurator.configure("src/test/resources/log4j-example.xml"); - } - - @Override - public String getType() { - return "xml"; - } -} diff --git a/logappender/src/test/java/org/apache/rocketmq/logappender/LogbackTest.java b/logappender/src/test/java/org/apache/rocketmq/logappender/LogbackTest.java deleted file mode 100644 index d7ec184a4b7..00000000000 --- a/logappender/src/test/java/org/apache/rocketmq/logappender/LogbackTest.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.logappender; - -import ch.qos.logback.classic.LoggerContext; -import ch.qos.logback.classic.joran.JoranConfigurator; -import ch.qos.logback.core.joran.spi.JoranException; -import ch.qos.logback.core.util.StatusPrinter; -import org.apache.rocketmq.client.exception.MQClientException; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; - -public class LogbackTest extends AbstractTestCase { - - @Before - public void init() throws JoranException { - LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); - JoranConfigurator configurator = new JoranConfigurator(); - configurator.setContext(lc); - lc.reset(); - configurator.doConfigure(new File("src/test/resources/logback-example.xml")); - StatusPrinter.printInCaseOfErrorsOrWarnings(lc); - } - - @Test - public void testLogback() throws InterruptedException, MQClientException { - clear(); - Logger logger = LoggerFactory.getLogger("testLogger"); - for (int i = 0; i < 10; i++) { - logger.info("logback test message " + i); - } - int received = consumeMessages(10, "logback", 10); - Assert.assertTrue(received >= 5); - } -} diff --git a/logappender/src/test/java/org/apache/rocketmq/logappender/log4j2Test.java b/logappender/src/test/java/org/apache/rocketmq/logappender/log4j2Test.java deleted file mode 100644 index 6f6af60876e..00000000000 --- a/logappender/src/test/java/org/apache/rocketmq/logappender/log4j2Test.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.logappender; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.core.config.Configurator; -import org.apache.rocketmq.client.exception.MQClientException; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -public class log4j2Test extends AbstractTestCase { - - @Before - public void init() { - Configurator.initialize("log4j2", "src/test/resources/log4j2-example.xml"); - } - - @Test - public void testLog4j2() throws InterruptedException, MQClientException { - clear(); - Logger logger = LogManager.getLogger("test"); - for (int i = 0; i < 10; i++) { - logger.info("log4j2 log message " + i); - } - int received = consumeMessages(10, "log4j2", 10); - Assert.assertTrue(received > 5); - } -} diff --git a/logappender/src/test/resources/log4j-example.properties b/logappender/src/test/resources/log4j-example.properties deleted file mode 100644 index 63b2a988c49..00000000000 --- a/logappender/src/test/resources/log4j-example.properties +++ /dev/null @@ -1,33 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -log4j.rootLogger=INFO,stdout -log4j.logger.testLogger=INFO,mq -log4j.appender.stdout=org.apache.log4j.ConsoleAppender -log4j.appender.stdout.Target=System.out -log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%d %-4r [%t] (%F:%L) %-5p - %m%n -log4j.appender.store=org.apache.log4j.DailyRollingFileAppender -log4j.appender.store.File=${user.home}/logs/rocketmqlogs/appender.log -log4j.appender.store.Append=true -log4j.appender.store.DatePattern='_'yyyy-MM-dd'.log' -log4j.appender.store.layout=org.apache.log4j.PatternLayout -log4j.appender.store.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-4r [%t] (%F:%L) %-5p - %m%n -log4j.appender.mq=org.apache.rocketmq.logappender.log4j.RocketmqLog4jAppender -log4j.appender.mq.Tag=log -log4j.appender.mq.Topic=TopicTest -log4j.appender.mq.ProducerGroup=loggerAppender -log4j.appender.mq.NameServerAddress=127.0.0.1:9876 -log4j.appender.mq.layout=org.apache.log4j.PatternLayout -log4j.appender.mq.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-4r [%t] (%F:%L) %-5p - %m%n \ No newline at end of file diff --git a/logappender/src/test/resources/log4j-example.xml b/logappender/src/test/resources/log4j-example.xml deleted file mode 100644 index 6bddde9825c..00000000000 --- a/logappender/src/test/resources/log4j-example.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/logappender/src/test/resources/log4j2-example.xml b/logappender/src/test/resources/log4j2-example.xml deleted file mode 100644 index c310855498b..00000000000 --- a/logappender/src/test/resources/log4j2-example.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/logappender/src/test/resources/logback-example.xml b/logappender/src/test/resources/logback-example.xml deleted file mode 100644 index 3786137baf4..00000000000 --- a/logappender/src/test/resources/logback-example.xml +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - ${user.home}/logs/simple/system.log - true - - ${user.home}/logs/simple/system.%i.log - - 1 - 30 - - - 100MB - - - %date %p %t - %m%n - UTF-8 - - - - - System.out - - %date %p %t - %m%n - UTF-8 - - - - - ${user.home}/logs/simple/daily.log - - ${user.home}/logs/simple/daily.log.%d{yyyy-MM-dd_HH} - 30 - - - %date %p %t - %m%n - - - - - log1 - TopicTest - loggerAppender - 127.0.0.1:9876 - - %date %p %t - %m%n - - - - - - - - - - - - - - - - - diff --git a/namesrv/BUILD.bazel b/namesrv/BUILD.bazel new file mode 100644 index 00000000000..fec42eaa3e4 --- /dev/null +++ b/namesrv/BUILD.bazel @@ -0,0 +1,75 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +load("//bazel:GenTestRules.bzl", "GenTestRules") + +java_library( + name = "namesrv", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//remoting", + "//srvutil", + "//tools", + "//client", + "//common", + "//controller", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:commons_validator_commons_validator", + "@maven//:com_github_luben_zstd_jni", + "@maven//:org_lz4_lz4_java", + "@maven//:com_alibaba_fastjson", + "@maven//:io_netty_netty_all", + "@maven//:ch_qos_logback_logback_classic", + "@maven//:ch_qos_logback_logback_core", + "@maven//:org_slf4j_slf4j_api", + "@maven//:org_bouncycastle_bcpkix_jdk15on", + "@maven//:commons_cli_commons_cli", + "@maven//:com_google_guava_guava", + "@maven//:io_github_aliyunmq_rocketmq_slf4j_api", + "@maven//:io_github_aliyunmq_rocketmq_logback_classic", + ], +) + +java_library( + name = "tests", + srcs = glob(["src/test/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + ":namesrv", + "//remoting", + "//srvutil", + "//tools", + "//client", + "//common", + "//:test_deps", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:commons_cli_commons_cli", + "@maven//:io_netty_netty_all", + "@maven//:com_google_guava_guava", + "@maven//:com_alibaba_fastjson", + "@maven//:io_github_aliyunmq_rocketmq_slf4j_api", + ], + resources = glob(["src/test/resources/certs/*.pem"]) + glob(["src/test/resources/certs/*.key"]) +) + +GenTestRules( + name = "GeneratedTestRules", + test_files = glob(["src/test/java/**/*Test.java"]), + deps = [ + ":tests", + ], +) diff --git a/namesrv/pom.xml b/namesrv/pom.xml index c2134c1cd2c..ebb2f5b2c43 100644 --- a/namesrv/pom.xml +++ b/namesrv/pom.xml @@ -19,7 +19,7 @@ org.apache.rocketmq rocketmq-all - 4.2.0 + 5.2.0 4.0.0 @@ -27,13 +27,21 @@ rocketmq-namesrv rocketmq-namesrv ${project.version} + + ${basedir}/.. + + - org.apache.rocketmq + ${project.groupId} + rocketmq-controller + + + ${project.groupId} rocketmq-client - org.apache.rocketmq + ${project.groupId} rocketmq-tools @@ -41,12 +49,20 @@ rocketmq-srvutil - ch.qos.logback - logback-classic + org.openjdk.jmh + jmh-core + 1.19 + test + + + org.openjdk.jmh + jmh-generator-annprocess + 1.19 + test - ch.qos.logback - logback-core + org.bouncycastle + bcpkix-jdk15on diff --git a/namesrv/src/main/java/org/apache/rocketmq/namesrv/NamesrvController.java b/namesrv/src/main/java/org/apache/rocketmq/namesrv/NamesrvController.java index 51b20b416dd..be327cffa51 100644 --- a/namesrv/src/main/java/org/apache/rocketmq/namesrv/NamesrvController.java +++ b/namesrv/src/main/java/org/apache/rocketmq/namesrv/NamesrvController.java @@ -16,107 +16,239 @@ */ package org.apache.rocketmq.namesrv; +import java.util.Collections; +import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import org.apache.rocketmq.common.Configuration; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.apache.rocketmq.common.ThreadFactoryImpl; import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.future.FutureTaskExt; import org.apache.rocketmq.common.namesrv.NamesrvConfig; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.namesrv.kvconfig.KVConfigManager; +import org.apache.rocketmq.namesrv.processor.ClientRequestProcessor; import org.apache.rocketmq.namesrv.processor.ClusterTestRequestProcessor; import org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor; +import org.apache.rocketmq.namesrv.route.ZoneRouteRPCHook; import org.apache.rocketmq.namesrv.routeinfo.BrokerHousekeepingService; import org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager; +import org.apache.rocketmq.remoting.Configuration; +import org.apache.rocketmq.remoting.RemotingClient; import org.apache.rocketmq.remoting.RemotingServer; +import org.apache.rocketmq.remoting.common.TlsMode; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyRemotingClient; import org.apache.rocketmq.remoting.netty.NettyRemotingServer; import org.apache.rocketmq.remoting.netty.NettyServerConfig; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.netty.RequestTask; +import org.apache.rocketmq.remoting.netty.TlsSystemConfig; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.srvutil.FileWatchService; public class NamesrvController { - private static final Logger log = LoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME); + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME); + private static final Logger WATER_MARK_LOG = LoggerFactory.getLogger(LoggerName.NAMESRV_WATER_MARK_LOGGER_NAME); private final NamesrvConfig namesrvConfig; private final NettyServerConfig nettyServerConfig; + private final NettyClientConfig nettyClientConfig; + + private final ScheduledExecutorService scheduledExecutorService = ThreadUtils.newScheduledThreadPool(1, + new BasicThreadFactory.Builder().namingPattern("NSScheduledThread").daemon(true).build()); + + private final ScheduledExecutorService scanExecutorService = ThreadUtils.newScheduledThreadPool(1, + new BasicThreadFactory.Builder().namingPattern("NSScanScheduledThread").daemon(true).build()); - private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl( - "NSScheduledThread")); private final KVConfigManager kvConfigManager; private final RouteInfoManager routeInfoManager; + private RemotingClient remotingClient; private RemotingServer remotingServer; - private BrokerHousekeepingService brokerHousekeepingService; + private final BrokerHousekeepingService brokerHousekeepingService; + + private ExecutorService defaultExecutor; + private ExecutorService clientRequestExecutor; - private ExecutorService remotingExecutor; + private BlockingQueue defaultThreadPoolQueue; + private BlockingQueue clientRequestThreadPoolQueue; - private Configuration configuration; + private final Configuration configuration; + private FileWatchService fileWatchService; public NamesrvController(NamesrvConfig namesrvConfig, NettyServerConfig nettyServerConfig) { + this(namesrvConfig, nettyServerConfig, new NettyClientConfig()); + } + + public NamesrvController(NamesrvConfig namesrvConfig, NettyServerConfig nettyServerConfig, NettyClientConfig nettyClientConfig) { this.namesrvConfig = namesrvConfig; this.nettyServerConfig = nettyServerConfig; + this.nettyClientConfig = nettyClientConfig; this.kvConfigManager = new KVConfigManager(this); - this.routeInfoManager = new RouteInfoManager(); this.brokerHousekeepingService = new BrokerHousekeepingService(this); - this.configuration = new Configuration( - log, - this.namesrvConfig, this.nettyServerConfig - ); + this.routeInfoManager = new RouteInfoManager(namesrvConfig, this); + this.configuration = new Configuration(LOGGER, this.namesrvConfig, this.nettyServerConfig); this.configuration.setStorePathFromConfig(this.namesrvConfig, "configStorePath"); } public boolean initialize() { + loadConfig(); + initiateNetworkComponents(); + initiateThreadExecutors(); + registerProcessor(); + startScheduleService(); + initiateSslContext(); + initiateRpcHooks(); + return true; + } + private void loadConfig() { this.kvConfigManager.load(); + } + + private void startScheduleService() { + this.scanExecutorService.scheduleAtFixedRate(NamesrvController.this.routeInfoManager::scanNotActiveBroker, + 5, this.namesrvConfig.getScanNotActiveBrokerInterval(), TimeUnit.MILLISECONDS); + + this.scheduledExecutorService.scheduleAtFixedRate(NamesrvController.this.kvConfigManager::printAllPeriodically, + 1, 10, TimeUnit.MINUTES); + this.scheduledExecutorService.scheduleAtFixedRate(() -> { + try { + NamesrvController.this.printWaterMark(); + } catch (Throwable e) { + LOGGER.error("printWaterMark error.", e); + } + }, 10, 1, TimeUnit.SECONDS); + } + + private void initiateNetworkComponents() { this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService); + this.remotingClient = new NettyRemotingClient(this.nettyClientConfig); + } + + private void initiateThreadExecutors() { + this.defaultThreadPoolQueue = new LinkedBlockingQueue<>(this.namesrvConfig.getDefaultThreadPoolQueueCapacity()); + this.defaultExecutor = ThreadUtils.newThreadPoolExecutor(this.namesrvConfig.getDefaultThreadPoolNums(), this.namesrvConfig.getDefaultThreadPoolNums(), 1000 * 60, TimeUnit.MILLISECONDS, this.defaultThreadPoolQueue, new ThreadFactoryImpl("RemotingExecutorThread_")); - this.remotingExecutor = - Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_")); + this.clientRequestThreadPoolQueue = new LinkedBlockingQueue<>(this.namesrvConfig.getClientRequestThreadPoolQueueCapacity()); + this.clientRequestExecutor = ThreadUtils.newThreadPoolExecutor(this.namesrvConfig.getClientRequestThreadPoolNums(), this.namesrvConfig.getClientRequestThreadPoolNums(), 1000 * 60, TimeUnit.MILLISECONDS, this.clientRequestThreadPoolQueue, new ThreadFactoryImpl("ClientRequestExecutorThread_")); + } + + private void initiateSslContext() { + if (TlsSystemConfig.tlsMode == TlsMode.DISABLED) { + return; + } - this.registerProcessor(); + String[] watchFiles = {TlsSystemConfig.tlsServerCertPath, TlsSystemConfig.tlsServerKeyPath, TlsSystemConfig.tlsServerTrustCertPath}; - this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + FileWatchService.Listener listener = new FileWatchService.Listener() { + boolean certChanged, keyChanged = false; @Override - public void run() { - NamesrvController.this.routeInfoManager.scanNotActiveBroker(); + public void onChanged(String path) { + if (path.equals(TlsSystemConfig.tlsServerTrustCertPath)) { + LOGGER.info("The trust certificate changed, reload the ssl context"); + ((NettyRemotingServer) remotingServer).loadSslContext(); + } + if (path.equals(TlsSystemConfig.tlsServerCertPath)) { + certChanged = true; + } + if (path.equals(TlsSystemConfig.tlsServerKeyPath)) { + keyChanged = true; + } + if (certChanged && keyChanged) { + LOGGER.info("The certificate and private key changed, reload the ssl context"); + certChanged = keyChanged = false; + ((NettyRemotingServer) remotingServer).loadSslContext(); + } } - }, 5, 10, TimeUnit.SECONDS); + }; + + try { + fileWatchService = new FileWatchService(watchFiles, listener); + } catch (Exception e) { + LOGGER.warn("FileWatchService created error, can't load the certificate dynamically"); + } + } - this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + private void printWaterMark() { + WATER_MARK_LOG.info("[WATERMARK] ClientQueueSize:{} ClientQueueSlowTime:{} " + "DefaultQueueSize:{} DefaultQueueSlowTime:{}", this.clientRequestThreadPoolQueue.size(), headSlowTimeMills(this.clientRequestThreadPoolQueue), this.defaultThreadPoolQueue.size(), headSlowTimeMills(this.defaultThreadPoolQueue)); + } - @Override - public void run() { - NamesrvController.this.kvConfigManager.printAllPeriodically(); + private long headSlowTimeMills(BlockingQueue q) { + long slowTimeMills = 0; + final Runnable firstRunnable = q.peek(); + + if (firstRunnable instanceof FutureTaskExt) { + final Runnable inner = ((FutureTaskExt) firstRunnable).getRunnable(); + if (inner instanceof RequestTask) { + slowTimeMills = System.currentTimeMillis() - ((RequestTask) inner).getCreateTimestamp(); } - }, 1, 10, TimeUnit.MINUTES); + } - return true; + if (slowTimeMills < 0) { + slowTimeMills = 0; + } + + return slowTimeMills; } private void registerProcessor() { if (namesrvConfig.isClusterTest()) { - this.remotingServer.registerDefaultProcessor(new ClusterTestRequestProcessor(this, namesrvConfig.getProductEnvName()), - this.remotingExecutor); + this.remotingServer.registerDefaultProcessor(new ClusterTestRequestProcessor(this, namesrvConfig.getProductEnvName()), this.defaultExecutor); } else { + // Support get route info only temporarily + ClientRequestProcessor clientRequestProcessor = new ClientRequestProcessor(this); + this.remotingServer.registerProcessor(RequestCode.GET_ROUTEINFO_BY_TOPIC, clientRequestProcessor, this.clientRequestExecutor); - this.remotingServer.registerDefaultProcessor(new DefaultRequestProcessor(this), this.remotingExecutor); + this.remotingServer.registerDefaultProcessor(new DefaultRequestProcessor(this), this.defaultExecutor); } } + private void initiateRpcHooks() { + this.remotingServer.registerRPCHook(new ZoneRouteRPCHook()); + } + public void start() throws Exception { this.remotingServer.start(); + + // In test scenarios where it is up to OS to pick up an available port, set the listening port back to config + if (0 == nettyServerConfig.getListenPort()) { + nettyServerConfig.setListenPort(this.remotingServer.localListenPort()); + } + + this.remotingClient.updateNameServerAddressList(Collections.singletonList(NetworkUtil.getLocalAddress() + + ":" + nettyServerConfig.getListenPort())); + this.remotingClient.start(); + + if (this.fileWatchService != null) { + this.fileWatchService.start(); + } + + this.routeInfoManager.start(); } public void shutdown() { + this.remotingClient.shutdown(); this.remotingServer.shutdown(); - this.remotingExecutor.shutdown(); + this.defaultExecutor.shutdown(); + this.clientRequestExecutor.shutdown(); this.scheduledExecutorService.shutdown(); + this.scanExecutorService.shutdown(); + this.routeInfoManager.shutdown(); + + if (this.fileWatchService != null) { + this.fileWatchService.shutdown(); + } } public NamesrvConfig getNamesrvConfig() { @@ -139,6 +271,10 @@ public RemotingServer getRemotingServer() { return remotingServer; } + public RemotingClient getRemotingClient() { + return remotingClient; + } + public void setRemotingServer(RemotingServer remotingServer) { this.remotingServer = remotingServer; } diff --git a/namesrv/src/main/java/org/apache/rocketmq/namesrv/NamesrvStartup.java b/namesrv/src/main/java/org/apache/rocketmq/namesrv/NamesrvStartup.java index 0a0c6562d6f..179d3bb0d63 100644 --- a/namesrv/src/main/java/org/apache/rocketmq/namesrv/NamesrvStartup.java +++ b/namesrv/src/main/java/org/apache/rocketmq/namesrv/NamesrvStartup.java @@ -16,122 +16,214 @@ */ package org.apache.rocketmq.namesrv; -import ch.qos.logback.classic.LoggerContext; -import ch.qos.logback.classic.joran.JoranConfigurator; import java.io.BufferedInputStream; -import java.io.FileInputStream; import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.Properties; import java.util.concurrent.Callable; import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; -import org.apache.commons.cli.PosixParser; +import org.apache.rocketmq.common.ControllerConfig; +import org.apache.rocketmq.common.JraftConfig; import org.apache.rocketmq.common.MQVersion; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.constant.LoggerName; import org.apache.rocketmq.common.namesrv.NamesrvConfig; +import org.apache.rocketmq.controller.ControllerManager; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; import org.apache.rocketmq.remoting.netty.NettyServerConfig; import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.srvutil.ServerUtil; import org.apache.rocketmq.srvutil.ShutdownHookThread; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class NamesrvStartup { - public static Properties properties = null; - public static CommandLine commandLine = null; + + private final static Logger log = LoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME); + private final static Logger logConsole = LoggerFactory.getLogger(LoggerName.NAMESRV_CONSOLE_LOGGER_NAME); + private static Properties properties = null; + private static NamesrvConfig namesrvConfig = null; + private static NettyServerConfig nettyServerConfig = null; + private static NettyClientConfig nettyClientConfig = null; + private static ControllerConfig controllerConfig = null; public static void main(String[] args) { main0(args); + controllerManagerMain(); } public static NamesrvController main0(String[] args) { - System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION)); try { - //PackageConflictDetect.detectFastjson(); + parseCommandlineAndConfigFile(args); + NamesrvController controller = createAndStartNamesrvController(); + return controller; + } catch (Throwable e) { + e.printStackTrace(); + System.exit(-1); + } - Options options = ServerUtil.buildCommandlineOptions(new Options()); - commandLine = ServerUtil.parseCmdLine("mqnamesrv", args, buildCommandlineOptions(options), new PosixParser()); - if (null == commandLine) { - System.exit(-1); - return null; + return null; + } + + public static ControllerManager controllerManagerMain() { + try { + if (namesrvConfig.isEnableControllerInNamesrv()) { + return createAndStartControllerManager(); } + } catch (Throwable e) { + e.printStackTrace(); + System.exit(-1); + } + return null; + } + + public static void parseCommandlineAndConfigFile(String[] args) throws Exception { + System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION)); + + Options options = ServerUtil.buildCommandlineOptions(new Options()); + CommandLine commandLine = ServerUtil.parseCmdLine("mqnamesrv", args, buildCommandlineOptions(options), new DefaultParser()); + if (null == commandLine) { + System.exit(-1); + return; + } - final NamesrvConfig namesrvConfig = new NamesrvConfig(); - final NettyServerConfig nettyServerConfig = new NettyServerConfig(); - nettyServerConfig.setListenPort(9876); - if (commandLine.hasOption('c')) { - String file = commandLine.getOptionValue('c'); - if (file != null) { - InputStream in = new BufferedInputStream(new FileInputStream(file)); - properties = new Properties(); - properties.load(in); - MixAll.properties2Object(properties, namesrvConfig); - MixAll.properties2Object(properties, nettyServerConfig); - - namesrvConfig.setConfigStorePath(file); - - System.out.printf("load config properties file OK, " + file + "%n"); - in.close(); + namesrvConfig = new NamesrvConfig(); + nettyServerConfig = new NettyServerConfig(); + nettyClientConfig = new NettyClientConfig(); + nettyServerConfig.setListenPort(9876); + if (commandLine.hasOption('c')) { + String file = commandLine.getOptionValue('c'); + if (file != null) { + InputStream in = new BufferedInputStream(Files.newInputStream(Paths.get(file))); + properties = new Properties(); + properties.load(in); + MixAll.properties2Object(properties, namesrvConfig); + MixAll.properties2Object(properties, nettyServerConfig); + MixAll.properties2Object(properties, nettyClientConfig); + if (namesrvConfig.isEnableControllerInNamesrv()) { + controllerConfig = new ControllerConfig(); + JraftConfig jraftConfig = new JraftConfig(); + controllerConfig.setJraftConfig(jraftConfig); + MixAll.properties2Object(properties, controllerConfig); + MixAll.properties2Object(properties, jraftConfig); } + namesrvConfig.setConfigStorePath(file); + + System.out.printf("load config properties file OK, %s%n", file); + in.close(); } + } - if (commandLine.hasOption('p')) { - MixAll.printObjectProperties(null, namesrvConfig); - MixAll.printObjectProperties(null, nettyServerConfig); - System.exit(0); + MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig); + if (commandLine.hasOption('p')) { + MixAll.printObjectProperties(logConsole, namesrvConfig); + MixAll.printObjectProperties(logConsole, nettyServerConfig); + MixAll.printObjectProperties(logConsole, nettyClientConfig); + if (namesrvConfig.isEnableControllerInNamesrv()) { + MixAll.printObjectProperties(logConsole, controllerConfig); } + System.exit(0); + } - MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig); + if (null == namesrvConfig.getRocketmqHome()) { + System.out.printf("Please set the %s variable in your environment to match the location of the RocketMQ installation%n", MixAll.ROCKETMQ_HOME_ENV); + System.exit(-2); + } + MixAll.printObjectProperties(log, namesrvConfig); + MixAll.printObjectProperties(log, nettyServerConfig); - if (null == namesrvConfig.getRocketmqHome()) { - System.out.printf("Please set the %s variable in your environment to match the location of the RocketMQ installation%n", MixAll.ROCKETMQ_HOME_ENV); - System.exit(-2); - } + } + + public static NamesrvController createAndStartNamesrvController() throws Exception { - LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); - JoranConfigurator configurator = new JoranConfigurator(); - configurator.setContext(lc); - lc.reset(); - configurator.doConfigure(namesrvConfig.getRocketmqHome() + "/conf/logback_namesrv.xml"); - final Logger log = LoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME); + NamesrvController controller = createNamesrvController(); + start(controller); + NettyServerConfig serverConfig = controller.getNettyServerConfig(); + String tip = String.format("The Name Server boot success. serializeType=%s, address %s:%d", RemotingCommand.getSerializeTypeConfigInThisServer(), serverConfig.getBindAddress(), serverConfig.getListenPort()); + log.info(tip); + System.out.printf("%s%n", tip); + return controller; + } - MixAll.printObjectProperties(log, namesrvConfig); - MixAll.printObjectProperties(log, nettyServerConfig); + public static NamesrvController createNamesrvController() { - final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig); + final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig, nettyClientConfig); + // remember all configs to prevent discard + controller.getConfiguration().registerConfig(properties); + return controller; + } - // remember all configs to prevent discard - controller.getConfiguration().registerConfig(properties); + public static NamesrvController start(final NamesrvController controller) throws Exception { - boolean initResult = controller.initialize(); - if (!initResult) { - controller.shutdown(); - System.exit(-3); - } + if (null == controller) { + throw new IllegalArgumentException("NamesrvController is null"); + } - Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable() { - @Override - public Void call() throws Exception { - controller.shutdown(); - return null; - } - })); + boolean initResult = controller.initialize(); + if (!initResult) { + controller.shutdown(); + System.exit(-3); + } - controller.start(); + Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, (Callable) () -> { + controller.shutdown(); + return null; + })); - String tip = "The Name Server boot success. serializeType=" + RemotingCommand.getSerializeTypeConfigInThisServer(); - log.info(tip); - System.out.printf(tip + "%n"); + controller.start(); - return controller; - } catch (Throwable e) { - e.printStackTrace(); - System.exit(-1); + return controller; + } + + public static ControllerManager createAndStartControllerManager() throws Exception { + ControllerManager controllerManager = createControllerManager(); + start(controllerManager); + String tip = "The ControllerManager boot success. serializeType=" + RemotingCommand.getSerializeTypeConfigInThisServer(); + log.info(tip); + System.out.printf("%s%n", tip); + return controllerManager; + } + + public static ControllerManager createControllerManager() throws Exception { + NettyServerConfig controllerNettyServerConfig = (NettyServerConfig) nettyServerConfig.clone(); + ControllerManager controllerManager = new ControllerManager(controllerConfig, controllerNettyServerConfig, nettyClientConfig); + // remember all configs to prevent discard + controllerManager.getConfiguration().registerConfig(properties); + return controllerManager; + } + + public static ControllerManager start(final ControllerManager controllerManager) throws Exception { + + if (null == controllerManager) { + throw new IllegalArgumentException("ControllerManager is null"); } - return null; + boolean initResult = controllerManager.initialize(); + if (!initResult) { + controllerManager.shutdown(); + System.exit(-3); + } + + Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, (Callable) () -> { + controllerManager.shutdown(); + return null; + })); + + controllerManager.start(); + + return controllerManager; + } + + public static void shutdown(final NamesrvController controller) { + controller.shutdown(); + } + + public static void shutdown(final ControllerManager controllerManager) { + controllerManager.shutdown(); } public static Options buildCommandlineOptions(final Options options) { @@ -139,10 +231,13 @@ public static Options buildCommandlineOptions(final Options options) { opt.setRequired(false); options.addOption(opt); - opt = new Option("p", "printConfigItem", false, "Print all config item"); + opt = new Option("p", "printConfigItem", false, "Print all config items"); opt.setRequired(false); options.addOption(opt); - return options; } + + public static Properties getProperties() { + return properties; + } } diff --git a/namesrv/src/main/java/org/apache/rocketmq/namesrv/kvconfig/KVConfigManager.java b/namesrv/src/main/java/org/apache/rocketmq/namesrv/kvconfig/KVConfigManager.java index 376a8143094..5c8a3d15027 100644 --- a/namesrv/src/main/java/org/apache/rocketmq/namesrv/kvconfig/KVConfigManager.java +++ b/namesrv/src/main/java/org/apache/rocketmq/namesrv/kvconfig/KVConfigManager.java @@ -18,16 +18,15 @@ import java.io.IOException; import java.util.HashMap; -import java.util.Iterator; import java.util.Map.Entry; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.common.protocol.body.KVTable; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.namesrv.NamesrvController; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.body.KVTable; public class KVConfigManager { private static final Logger log = LoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME); @@ -36,7 +35,7 @@ public class KVConfigManager { private final ReadWriteLock lock = new ReentrantReadWriteLock(); private final HashMap> configTable = - new HashMap>(); + new HashMap<>(); public KVConfigManager(NamesrvController namesrvController) { this.namesrvController = namesrvController; @@ -65,7 +64,7 @@ public void putKVConfig(final String namespace, final String key, final String v try { HashMap kvTable = this.configTable.get(namespace); if (null == kvTable) { - kvTable = new HashMap(); + kvTable = new HashMap<>(); this.configTable.put(namespace, kvTable); log.info("putKVConfig create new Namespace {}", namespace); } @@ -178,15 +177,10 @@ public void printAllPeriodically() { { log.info("configTable SIZE: {}", this.configTable.size()); - Iterator>> it = - this.configTable.entrySet().iterator(); - while (it.hasNext()) { - Entry> next = it.next(); - Iterator> itSub = next.getValue().entrySet().iterator(); - while (itSub.hasNext()) { - Entry nextSub = itSub.next(); + for (Entry> next : this.configTable.entrySet()) { + for (Entry nextSub : next.getValue().entrySet()) { log.info("configTable NS: {} Key: {} Value: {}", next.getKey(), nextSub.getKey(), - nextSub.getValue()); + nextSub.getValue()); } } } diff --git a/namesrv/src/main/java/org/apache/rocketmq/namesrv/processor/ClientRequestProcessor.java b/namesrv/src/main/java/org/apache/rocketmq/namesrv/processor/ClientRequestProcessor.java new file mode 100644 index 00000000000..17a070c7f07 --- /dev/null +++ b/namesrv/src/main/java/org/apache/rocketmq/namesrv/processor/ClientRequestProcessor.java @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.namesrv.processor; + +import com.alibaba.fastjson.serializer.SerializerFeature; +import io.netty.channel.ChannelHandlerContext; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.help.FAQUrl; +import org.apache.rocketmq.common.namesrv.NamesrvUtil; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.namesrv.NamesrvController; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.namesrv.GetRouteInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; + +public class ClientRequestProcessor implements NettyRequestProcessor { + + private static Logger log = LoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME); + + protected NamesrvController namesrvController; + private long startupTimeMillis; + + private AtomicBoolean needCheckNamesrvReady = new AtomicBoolean(true); + + public ClientRequestProcessor(final NamesrvController namesrvController) { + this.namesrvController = namesrvController; + this.startupTimeMillis = System.currentTimeMillis(); + } + + @Override + public RemotingCommand processRequest(final ChannelHandlerContext ctx, + final RemotingCommand request) throws Exception { + return this.getRouteInfoByTopic(ctx, request); + } + + public RemotingCommand getRouteInfoByTopic(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + final GetRouteInfoRequestHeader requestHeader = + (GetRouteInfoRequestHeader) request.decodeCommandCustomHeader(GetRouteInfoRequestHeader.class); + + boolean namesrvReady = needCheckNamesrvReady.get() && System.currentTimeMillis() - startupTimeMillis >= TimeUnit.SECONDS.toMillis(namesrvController.getNamesrvConfig().getWaitSecondsForService()); + + if (namesrvController.getNamesrvConfig().isNeedWaitForService() && !namesrvReady) { + log.warn("name server not ready. request code {} ", request.getCode()); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("name server not ready"); + return response; + } + + TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic()); + + if (topicRouteData != null) { + //topic route info register success ,so disable namesrvReady check + if (needCheckNamesrvReady.get()) { + needCheckNamesrvReady.set(false); + } + + if (this.namesrvController.getNamesrvConfig().isOrderMessageEnable()) { + String orderTopicConf = + this.namesrvController.getKvConfigManager().getKVConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG, + requestHeader.getTopic()); + topicRouteData.setOrderTopicConf(orderTopicConf); + } + + byte[] content; + Boolean standardJsonOnly = Optional.ofNullable(requestHeader.getAcceptStandardJsonOnly()).orElse(false); + if (request.getVersion() >= MQVersion.Version.V4_9_4.ordinal() || standardJsonOnly) { + content = topicRouteData.encode(SerializerFeature.BrowserCompatible, + SerializerFeature.QuoteFieldNames, SerializerFeature.SkipTransientField, + SerializerFeature.MapSortField); + } else { + content = topicRouteData.encode(); + } + + response.setBody(content); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + response.setCode(ResponseCode.TOPIC_NOT_EXIST); + response.setRemark("No topic route info in name server for the topic: " + requestHeader.getTopic() + + FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL)); + return response; + } + + @Override + public boolean rejectRequest() { + return false; + } +} diff --git a/namesrv/src/main/java/org/apache/rocketmq/namesrv/processor/ClusterTestRequestProcessor.java b/namesrv/src/main/java/org/apache/rocketmq/namesrv/processor/ClusterTestRequestProcessor.java index f6611b6837a..725c5e633eb 100644 --- a/namesrv/src/main/java/org/apache/rocketmq/namesrv/processor/ClusterTestRequestProcessor.java +++ b/namesrv/src/main/java/org/apache/rocketmq/namesrv/processor/ClusterTestRequestProcessor.java @@ -21,17 +21,17 @@ import org.apache.rocketmq.common.constant.LoggerName; import org.apache.rocketmq.common.help.FAQUrl; import org.apache.rocketmq.common.namesrv.NamesrvUtil; -import org.apache.rocketmq.common.protocol.ResponseCode; -import org.apache.rocketmq.common.protocol.header.namesrv.GetRouteInfoRequestHeader; -import org.apache.rocketmq.common.protocol.route.TopicRouteData; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.namesrv.NamesrvController; import org.apache.rocketmq.remoting.exception.RemotingCommandException; import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.namesrv.GetRouteInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -public class ClusterTestRequestProcessor extends DefaultRequestProcessor { +public class ClusterTestRequestProcessor extends ClientRequestProcessor { private static final Logger log = LoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME); private final DefaultMQAdminExt adminExt; private final String productEnvName; diff --git a/namesrv/src/main/java/org/apache/rocketmq/namesrv/processor/DefaultRequestProcessor.java b/namesrv/src/main/java/org/apache/rocketmq/namesrv/processor/DefaultRequestProcessor.java index ed5b20b1667..deb51e4fc11 100644 --- a/namesrv/src/main/java/org/apache/rocketmq/namesrv/processor/DefaultRequestProcessor.java +++ b/namesrv/src/main/java/org/apache/rocketmq/namesrv/processor/DefaultRequestProcessor.java @@ -18,54 +18,83 @@ import io.netty.channel.ChannelHandlerContext; import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; import java.util.Properties; +import java.util.Set; import java.util.concurrent.atomic.AtomicLong; +import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.common.MQVersion; import org.apache.rocketmq.common.MQVersion.Version; import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.UtilAll; import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.common.help.FAQUrl; import org.apache.rocketmq.common.namesrv.NamesrvUtil; -import org.apache.rocketmq.common.namesrv.RegisterBrokerResult; -import org.apache.rocketmq.common.protocol.RequestCode; -import org.apache.rocketmq.common.protocol.ResponseCode; -import org.apache.rocketmq.common.protocol.body.RegisterBrokerBody; -import org.apache.rocketmq.common.protocol.body.TopicConfigSerializeWrapper; -import org.apache.rocketmq.common.protocol.header.GetTopicsByClusterRequestHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.DeleteKVConfigRequestHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.DeleteTopicInNamesrvRequestHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.GetKVConfigRequestHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.GetKVConfigResponseHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.GetKVListByNamespaceRequestHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.GetRouteInfoRequestHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.PutKVConfigRequestHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.RegisterBrokerRequestHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.RegisterBrokerResponseHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.UnRegisterBrokerRequestHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.WipeWritePermOfBrokerRequestHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.WipeWritePermOfBrokerResponseHeader; -import org.apache.rocketmq.common.protocol.route.TopicRouteData; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.namesrv.NamesrvController; import org.apache.rocketmq.remoting.common.RemotingHelper; import org.apache.rocketmq.remoting.exception.RemotingCommandException; import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; +import org.apache.rocketmq.remoting.protocol.DataVersion; import org.apache.rocketmq.remoting.protocol.RemotingCommand; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.RemotingSysResponseCode; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.BrokerMemberGroup; +import org.apache.rocketmq.remoting.protocol.body.GetBrokerMemberGroupResponseBody; +import org.apache.rocketmq.remoting.protocol.body.RegisterBrokerBody; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.body.TopicList; +import org.apache.rocketmq.remoting.protocol.header.GetBrokerMemberGroupRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetTopicsByClusterRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.AddWritePermOfBrokerRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.AddWritePermOfBrokerResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.BrokerHeartbeatRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.DeleteKVConfigRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.DeleteTopicFromNamesrvRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.GetKVConfigRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.GetKVConfigResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.GetKVListByNamespaceRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.PutKVConfigRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.QueryDataVersionRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.QueryDataVersionResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.RegisterBrokerRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.RegisterBrokerResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.RegisterTopicRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.UnRegisterBrokerRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.WipeWritePermOfBrokerRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.WipeWritePermOfBrokerResponseHeader; +import org.apache.rocketmq.remoting.protocol.namesrv.RegisterBrokerResult; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; public class DefaultRequestProcessor implements NettyRequestProcessor { - private static final Logger log = LoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME); + private static Logger log = LoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME); protected final NamesrvController namesrvController; + protected Set configBlackList = new HashSet<>(); + public DefaultRequestProcessor(NamesrvController namesrvController) { this.namesrvController = namesrvController; + initConfigBlackList(); + } + + private void initConfigBlackList() { + configBlackList.add("configBlackList"); + configBlackList.add("configStorePath"); + configBlackList.add("kvConfigPath"); + configBlackList.add("rocketmqHome"); + String[] configArray = namesrvController.getNamesrvConfig().getConfigBlackList().split(";"); + configBlackList.addAll(Arrays.asList(configArray)); } @Override public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException { - if (log.isDebugEnabled()) { + + if (ctx != null) { log.debug("receive request, {} {} {}", request.getCode(), RemotingHelper.parseChannelRemoteAddr(ctx.channel()), @@ -79,25 +108,28 @@ public RemotingCommand processRequest(ChannelHandlerContext ctx, return this.getKVConfig(ctx, request); case RequestCode.DELETE_KV_CONFIG: return this.deleteKVConfig(ctx, request); + case RequestCode.QUERY_DATA_VERSION: + return this.queryBrokerTopicConfig(ctx, request); case RequestCode.REGISTER_BROKER: - Version brokerVersion = MQVersion.value2Version(request.getVersion()); - if (brokerVersion.ordinal() >= MQVersion.Version.V3_0_11.ordinal()) { - return this.registerBrokerWithFilterServer(ctx, request); - } else { - return this.registerBroker(ctx, request); - } + return this.registerBroker(ctx, request); case RequestCode.UNREGISTER_BROKER: return this.unregisterBroker(ctx, request); - case RequestCode.GET_ROUTEINTO_BY_TOPIC: - return this.getRouteInfoByTopic(ctx, request); + case RequestCode.BROKER_HEARTBEAT: + return this.brokerHeartbeat(ctx, request); + case RequestCode.GET_BROKER_MEMBER_GROUP: + return this.getBrokerMemberGroup(ctx, request); case RequestCode.GET_BROKER_CLUSTER_INFO: return this.getBrokerClusterInfo(ctx, request); case RequestCode.WIPE_WRITE_PERM_OF_BROKER: return this.wipeWritePermOfBroker(ctx, request); + case RequestCode.ADD_WRITE_PERM_OF_BROKER: + return this.addWritePermOfBroker(ctx, request); case RequestCode.GET_ALL_TOPIC_LIST_FROM_NAMESERVER: - return getAllTopicListFromNameserver(ctx, request); + return this.getAllTopicListFromNameserver(ctx, request); case RequestCode.DELETE_TOPIC_IN_NAMESRV: - return deleteTopicInNamesrv(ctx, request); + return this.deleteTopicInNamesrv(ctx, request); + case RequestCode.REGISTER_TOPIC_IN_NAMESRV: + return this.registerTopicToNamesrv(ctx, request); case RequestCode.GET_KVLIST_BY_NAMESPACE: return this.getKVListByNamespace(ctx, request); case RequestCode.GET_TOPICS_BY_CLUSTER: @@ -115,9 +147,9 @@ public RemotingCommand processRequest(ChannelHandlerContext ctx, case RequestCode.GET_NAMESRV_CONFIG: return this.getConfig(ctx, request); default: - break; + String error = " request type " + request.getCode() + " not supported"; + return RemotingCommand.createResponseCommand(RemotingSysResponseCode.REQUEST_CODE_NOT_SUPPORTED, error); } - return null; } @Override @@ -131,6 +163,12 @@ public RemotingCommand putKVConfig(ChannelHandlerContext ctx, final PutKVConfigRequestHeader requestHeader = (PutKVConfigRequestHeader) request.decodeCommandCustomHeader(PutKVConfigRequestHeader.class); + if (requestHeader.getNamespace() == null || requestHeader.getKey() == null) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("namespace or key is null"); + return response; + } + this.namesrvController.getKvConfigManager().putKVConfig( requestHeader.getNamespace(), requestHeader.getKey(), @@ -182,20 +220,30 @@ public RemotingCommand deleteKVConfig(ChannelHandlerContext ctx, return response; } - public RemotingCommand registerBrokerWithFilterServer(ChannelHandlerContext ctx, RemotingCommand request) - throws RemotingCommandException { + public RemotingCommand registerBroker(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { final RemotingCommand response = RemotingCommand.createResponseCommand(RegisterBrokerResponseHeader.class); final RegisterBrokerResponseHeader responseHeader = (RegisterBrokerResponseHeader) response.readCustomHeader(); final RegisterBrokerRequestHeader requestHeader = (RegisterBrokerRequestHeader) request.decodeCommandCustomHeader(RegisterBrokerRequestHeader.class); - RegisterBrokerBody registerBrokerBody = new RegisterBrokerBody(); + if (!checksum(ctx, request, requestHeader)) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("crc32 not match"); + return response; + } - if (request.getBody() != null) { - registerBrokerBody = RegisterBrokerBody.decode(request.getBody(), RegisterBrokerBody.class); + TopicConfigSerializeWrapper topicConfigWrapper = null; + List filterServerList = null; + + Version brokerVersion = MQVersion.value2Version(request.getVersion()); + if (brokerVersion.ordinal() >= MQVersion.Version.V3_0_11.ordinal()) { + final RegisterBrokerBody registerBrokerBody = extractRegisterBrokerBodyFromRequest(request, requestHeader); + topicConfigWrapper = registerBrokerBody.getTopicConfigSerializeWrapper(); + filterServerList = registerBrokerBody.getFilterServerList(); } else { - registerBrokerBody.getTopicConfigSerializeWrapper().getDataVersion().setCounter(new AtomicLong(0)); - registerBrokerBody.getTopicConfigSerializeWrapper().getDataVersion().setTimestamp(0); + // RegisterBrokerBody of old version only contains TopicConfig. + topicConfigWrapper = extractRegisterTopicConfigFromRequest(request); } RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker( @@ -204,108 +252,152 @@ public RemotingCommand registerBrokerWithFilterServer(ChannelHandlerContext ctx, requestHeader.getBrokerName(), requestHeader.getBrokerId(), requestHeader.getHaServerAddr(), - registerBrokerBody.getTopicConfigSerializeWrapper(), - registerBrokerBody.getFilterServerList(), - ctx.channel()); + request.getExtFields().get(MixAll.ZONE_NAME), + requestHeader.getHeartbeatTimeoutMillis(), + requestHeader.getEnableActingMaster(), + topicConfigWrapper, + filterServerList, + ctx.channel() + ); + + if (result == null) { + // Register single topic route info should be after the broker completes the first registration. + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("register broker failed"); + return response; + } responseHeader.setHaServerAddr(result.getHaServerAddr()); responseHeader.setMasterAddr(result.getMasterAddr()); - byte[] jsonValue = this.namesrvController.getKvConfigManager().getKVListByNamespace(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG); - response.setBody(jsonValue); + if (this.namesrvController.getNamesrvConfig().isReturnOrderTopicConfigToBroker()) { + byte[] jsonValue = this.namesrvController.getKvConfigManager().getKVListByNamespace(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG); + response.setBody(jsonValue); + } response.setCode(ResponseCode.SUCCESS); response.setRemark(null); return response; } - public RemotingCommand registerBroker(ChannelHandlerContext ctx, - RemotingCommand request) throws RemotingCommandException { - final RemotingCommand response = RemotingCommand.createResponseCommand(RegisterBrokerResponseHeader.class); - final RegisterBrokerResponseHeader responseHeader = (RegisterBrokerResponseHeader) response.readCustomHeader(); - final RegisterBrokerRequestHeader requestHeader = - (RegisterBrokerRequestHeader) request.decodeCommandCustomHeader(RegisterBrokerRequestHeader.class); - + private TopicConfigSerializeWrapper extractRegisterTopicConfigFromRequest(final RemotingCommand request) { TopicConfigSerializeWrapper topicConfigWrapper; if (request.getBody() != null) { topicConfigWrapper = TopicConfigSerializeWrapper.decode(request.getBody(), TopicConfigSerializeWrapper.class); } else { topicConfigWrapper = new TopicConfigSerializeWrapper(); topicConfigWrapper.getDataVersion().setCounter(new AtomicLong(0)); - topicConfigWrapper.getDataVersion().setTimestamp(0); + topicConfigWrapper.getDataVersion().setTimestamp(0L); + topicConfigWrapper.getDataVersion().setStateVersion(0L); } + return topicConfigWrapper; + } - RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker( - requestHeader.getClusterName(), - requestHeader.getBrokerAddr(), - requestHeader.getBrokerName(), - requestHeader.getBrokerId(), - requestHeader.getHaServerAddr(), - topicConfigWrapper, - null, - ctx.channel() - ); + private RegisterBrokerBody extractRegisterBrokerBodyFromRequest(RemotingCommand request, + RegisterBrokerRequestHeader requestHeader) throws RemotingCommandException { + RegisterBrokerBody registerBrokerBody = new RegisterBrokerBody(); - responseHeader.setHaServerAddr(result.getHaServerAddr()); - responseHeader.setMasterAddr(result.getMasterAddr()); + if (request.getBody() != null) { + try { + Version brokerVersion = MQVersion.value2Version(request.getVersion()); + registerBrokerBody = RegisterBrokerBody.decode(request.getBody(), requestHeader.isCompressed(), brokerVersion); + } catch (Exception e) { + throw new RemotingCommandException("Failed to decode RegisterBrokerBody", e); + } + } else { + registerBrokerBody.getTopicConfigSerializeWrapper().getDataVersion().setCounter(new AtomicLong(0)); + registerBrokerBody.getTopicConfigSerializeWrapper().getDataVersion().setTimestamp(0L); + registerBrokerBody.getTopicConfigSerializeWrapper().getDataVersion().setStateVersion(0L); + } + return registerBrokerBody; + } + + private RemotingCommand getBrokerMemberGroup(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + GetBrokerMemberGroupRequestHeader requestHeader = (GetBrokerMemberGroupRequestHeader) request.decodeCommandCustomHeader(GetBrokerMemberGroupRequestHeader.class); + + BrokerMemberGroup memberGroup = this.namesrvController.getRouteInfoManager() + .getBrokerMemberGroup(requestHeader.getClusterName(), requestHeader.getBrokerName()); + + GetBrokerMemberGroupResponseBody responseBody = new GetBrokerMemberGroupResponseBody(); + responseBody.setBrokerMemberGroup(memberGroup); + + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + response.setBody(responseBody.encode()); + return response; + } + + private boolean checksum(ChannelHandlerContext ctx, RemotingCommand request, + RegisterBrokerRequestHeader requestHeader) { + if (requestHeader.getBodyCrc32() != 0) { + final int crc32 = UtilAll.crc32(request.getBody()); + if (crc32 != requestHeader.getBodyCrc32()) { + log.warn(String.format("receive registerBroker request,crc32 not match,from %s", + RemotingHelper.parseChannelRemoteAddr(ctx.channel()))); + return false; + } + } + return true; + } - byte[] jsonValue = this.namesrvController.getKvConfigManager().getKVListByNamespace(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG); - response.setBody(jsonValue); + public RemotingCommand queryBrokerTopicConfig(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(QueryDataVersionResponseHeader.class); + final QueryDataVersionResponseHeader responseHeader = (QueryDataVersionResponseHeader) response.readCustomHeader(); + final QueryDataVersionRequestHeader requestHeader = + (QueryDataVersionRequestHeader) request.decodeCommandCustomHeader(QueryDataVersionRequestHeader.class); + DataVersion dataVersion = DataVersion.decode(request.getBody(), DataVersion.class); + String clusterName = requestHeader.getClusterName(); + String brokerAddr = requestHeader.getBrokerAddr(); + + Boolean changed = this.namesrvController.getRouteInfoManager().isBrokerTopicConfigChanged(clusterName, brokerAddr, dataVersion); + this.namesrvController.getRouteInfoManager().updateBrokerInfoUpdateTimestamp(clusterName, brokerAddr); + + DataVersion nameSeverDataVersion = this.namesrvController.getRouteInfoManager().queryBrokerTopicConfig(clusterName, brokerAddr); response.setCode(ResponseCode.SUCCESS); response.setRemark(null); + + if (nameSeverDataVersion != null) { + response.setBody(nameSeverDataVersion.encode()); + } + responseHeader.setChanged(changed); return response; } public RemotingCommand unregisterBroker(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException { final RemotingCommand response = RemotingCommand.createResponseCommand(null); - final UnRegisterBrokerRequestHeader requestHeader = - (UnRegisterBrokerRequestHeader) request.decodeCommandCustomHeader(UnRegisterBrokerRequestHeader.class); - - this.namesrvController.getRouteInfoManager().unregisterBroker( - requestHeader.getClusterName(), - requestHeader.getBrokerAddr(), - requestHeader.getBrokerName(), - requestHeader.getBrokerId()); + final UnRegisterBrokerRequestHeader requestHeader = (UnRegisterBrokerRequestHeader) request.decodeCommandCustomHeader(UnRegisterBrokerRequestHeader.class); + if (!this.namesrvController.getRouteInfoManager().submitUnRegisterBrokerRequest(requestHeader)) { + log.warn("Couldn't submit the unregister broker request to handler, broker info: {}", requestHeader); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark(null); + return response; + } response.setCode(ResponseCode.SUCCESS); response.setRemark(null); return response; } - public RemotingCommand getRouteInfoByTopic(ChannelHandlerContext ctx, + public RemotingCommand brokerHeartbeat(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException { final RemotingCommand response = RemotingCommand.createResponseCommand(null); - final GetRouteInfoRequestHeader requestHeader = - (GetRouteInfoRequestHeader) request.decodeCommandCustomHeader(GetRouteInfoRequestHeader.class); - - TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic()); - - if (topicRouteData != null) { - if (this.namesrvController.getNamesrvConfig().isOrderMessageEnable()) { - String orderTopicConf = - this.namesrvController.getKvConfigManager().getKVConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG, - requestHeader.getTopic()); - topicRouteData.setOrderTopicConf(orderTopicConf); - } + final BrokerHeartbeatRequestHeader requestHeader = + (BrokerHeartbeatRequestHeader) request.decodeCommandCustomHeader(BrokerHeartbeatRequestHeader.class); - byte[] content = topicRouteData.encode(); - response.setBody(content); - response.setCode(ResponseCode.SUCCESS); - response.setRemark(null); - return response; - } + this.namesrvController.getRouteInfoManager().updateBrokerInfoUpdateTimestamp(requestHeader.getClusterName(), requestHeader.getBrokerAddr()); - response.setCode(ResponseCode.TOPIC_NOT_EXIST); - response.setRemark("No topic route info in name server for the topic: " + requestHeader.getTopic() - + FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL)); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); return response; } private RemotingCommand getBrokerClusterInfo(ChannelHandlerContext ctx, RemotingCommand request) { final RemotingCommand response = RemotingCommand.createResponseCommand(null); - byte[] content = this.namesrvController.getRouteInfoManager().getAllClusterInfo(); + byte[] content = this.namesrvController.getRouteInfoManager().getAllClusterInfo().encode(); response.setBody(content); response.setCode(ResponseCode.SUCCESS); @@ -322,12 +414,33 @@ private RemotingCommand wipeWritePermOfBroker(ChannelHandlerContext ctx, int wipeTopicCnt = this.namesrvController.getRouteInfoManager().wipeWritePermOfBrokerByLock(requestHeader.getBrokerName()); - log.info("wipe write perm of broker[{}], client: {}, {}", + if (ctx != null) { + log.info("wipe write perm of broker[{}], client: {}, {}", + requestHeader.getBrokerName(), + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), + wipeTopicCnt); + } + + responseHeader.setWipeTopicCount(wipeTopicCnt); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } + + private RemotingCommand addWritePermOfBroker(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(AddWritePermOfBrokerResponseHeader.class); + final AddWritePermOfBrokerResponseHeader responseHeader = (AddWritePermOfBrokerResponseHeader) response.readCustomHeader(); + final AddWritePermOfBrokerRequestHeader requestHeader = (AddWritePermOfBrokerRequestHeader) request.decodeCommandCustomHeader(AddWritePermOfBrokerRequestHeader.class); + + int addTopicCnt = this.namesrvController.getRouteInfoManager().addWritePermOfBrokerByLock(requestHeader.getBrokerName()); + + log.info("add write perm of broker[{}], client: {}, {}", requestHeader.getBrokerName(), RemotingHelper.parseChannelRemoteAddr(ctx.channel()), - wipeTopicCnt); + addTopicCnt); - responseHeader.setWipeTopicCount(wipeTopicCnt); + responseHeader.setAddTopicCount(addTopicCnt); response.setCode(ResponseCode.SUCCESS); response.setRemark(null); return response; @@ -335,10 +448,33 @@ private RemotingCommand wipeWritePermOfBroker(ChannelHandlerContext ctx, private RemotingCommand getAllTopicListFromNameserver(ChannelHandlerContext ctx, RemotingCommand request) { final RemotingCommand response = RemotingCommand.createResponseCommand(null); + boolean enableAllTopicList = namesrvController.getNamesrvConfig().isEnableAllTopicList(); + log.warn("getAllTopicListFromNameserver {} enable {}", ctx.channel().remoteAddress(), enableAllTopicList); + if (enableAllTopicList) { + byte[] body = this.namesrvController.getRouteInfoManager().getAllTopicList().encode(); + response.setBody(body); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + } else { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("disable"); + } + + return response; + } - byte[] body = this.namesrvController.getRouteInfoManager().getAllTopicList(); + private RemotingCommand registerTopicToNamesrv(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + + final RegisterTopicRequestHeader requestHeader = + (RegisterTopicRequestHeader) request.decodeCommandCustomHeader(RegisterTopicRequestHeader.class); + + TopicRouteData topicRouteData = TopicRouteData.decode(request.getBody(), TopicRouteData.class); + if (topicRouteData != null && topicRouteData.getQueueDatas() != null && !topicRouteData.getQueueDatas().isEmpty()) { + this.namesrvController.getRouteInfoManager().registerTopic(requestHeader.getTopic(), topicRouteData.getQueueDatas()); + } - response.setBody(body); response.setCode(ResponseCode.SUCCESS); response.setRemark(null); return response; @@ -347,10 +483,15 @@ private RemotingCommand getAllTopicListFromNameserver(ChannelHandlerContext ctx, private RemotingCommand deleteTopicInNamesrv(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException { final RemotingCommand response = RemotingCommand.createResponseCommand(null); - final DeleteTopicInNamesrvRequestHeader requestHeader = - (DeleteTopicInNamesrvRequestHeader) request.decodeCommandCustomHeader(DeleteTopicInNamesrvRequestHeader.class); + final DeleteTopicFromNamesrvRequestHeader requestHeader = + (DeleteTopicFromNamesrvRequestHeader) request.decodeCommandCustomHeader(DeleteTopicFromNamesrvRequestHeader.class); - this.namesrvController.getRouteInfoManager().deleteTopic(requestHeader.getTopic()); + if (requestHeader.getClusterName() != null + && !requestHeader.getClusterName().isEmpty()) { + this.namesrvController.getRouteInfoManager().deleteTopic(requestHeader.getTopic(), requestHeader.getClusterName()); + } else { + this.namesrvController.getRouteInfoManager().deleteTopic(requestHeader.getTopic()); + } response.setCode(ResponseCode.SUCCESS); response.setRemark(null); @@ -380,22 +521,31 @@ private RemotingCommand getKVListByNamespace(ChannelHandlerContext ctx, private RemotingCommand getTopicsByCluster(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException { final RemotingCommand response = RemotingCommand.createResponseCommand(null); + boolean enableTopicList = namesrvController.getNamesrvConfig().isEnableTopicList(); + if (!enableTopicList) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("disable"); + return response; + } final GetTopicsByClusterRequestHeader requestHeader = (GetTopicsByClusterRequestHeader) request.decodeCommandCustomHeader(GetTopicsByClusterRequestHeader.class); - byte[] body = this.namesrvController.getRouteInfoManager().getTopicsByCluster(requestHeader.getCluster()); + TopicList topicsByCluster = this.namesrvController.getRouteInfoManager().getTopicsByCluster(requestHeader.getCluster()); + byte[] body = topicsByCluster.encode(); response.setBody(body); response.setCode(ResponseCode.SUCCESS); response.setRemark(null); + return response; } private RemotingCommand getSystemTopicListFromNs(ChannelHandlerContext ctx, - RemotingCommand request) throws RemotingCommandException { + RemotingCommand request) { final RemotingCommand response = RemotingCommand.createResponseCommand(null); - byte[] body = this.namesrvController.getRouteInfoManager().getSystemTopicList(); + TopicList systemTopicList = this.namesrvController.getRouteInfoManager().getSystemTopicList(); + byte[] body = systemTopicList.encode(); response.setBody(body); response.setCode(ResponseCode.SUCCESS); @@ -404,43 +554,68 @@ private RemotingCommand getSystemTopicListFromNs(ChannelHandlerContext ctx, } private RemotingCommand getUnitTopicList(ChannelHandlerContext ctx, - RemotingCommand request) throws RemotingCommandException { + RemotingCommand request) { final RemotingCommand response = RemotingCommand.createResponseCommand(null); - byte[] body = this.namesrvController.getRouteInfoManager().getUnitTopics(); + boolean enableTopicList = namesrvController.getNamesrvConfig().isEnableTopicList(); + + if (enableTopicList) { + TopicList unitTopicList = this.namesrvController.getRouteInfoManager().getUnitTopics(); + byte[] body = unitTopicList.encode(); + response.setBody(body); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + } else { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("disable"); + } - response.setBody(body); - response.setCode(ResponseCode.SUCCESS); - response.setRemark(null); return response; } private RemotingCommand getHasUnitSubTopicList(ChannelHandlerContext ctx, - RemotingCommand request) throws RemotingCommandException { + RemotingCommand request) { final RemotingCommand response = RemotingCommand.createResponseCommand(null); - byte[] body = this.namesrvController.getRouteInfoManager().getHasUnitSubTopicList(); + boolean enableTopicList = namesrvController.getNamesrvConfig().isEnableTopicList(); + + if (enableTopicList) { + TopicList hasUnitSubTopicList = this.namesrvController.getRouteInfoManager().getHasUnitSubTopicList(); + byte[] body = hasUnitSubTopicList.encode(); + response.setBody(body); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + } else { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("disable"); + } - response.setBody(body); - response.setCode(ResponseCode.SUCCESS); - response.setRemark(null); return response; } - private RemotingCommand getHasUnitSubUnUnitTopicList(ChannelHandlerContext ctx, RemotingCommand request) - throws RemotingCommandException { + private RemotingCommand getHasUnitSubUnUnitTopicList(ChannelHandlerContext ctx, RemotingCommand request) { final RemotingCommand response = RemotingCommand.createResponseCommand(null); - byte[] body = this.namesrvController.getRouteInfoManager().getHasUnitSubUnUnitTopicList(); + boolean enableTopicList = namesrvController.getNamesrvConfig().isEnableTopicList(); + + if (enableTopicList) { + TopicList hasUnitSubUnUnitTopicList = this.namesrvController.getRouteInfoManager().getHasUnitSubUnUnitTopicList(); + byte[] body = hasUnitSubUnUnitTopicList.encode(); + response.setBody(body); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + } else { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("disable"); + } - response.setBody(body); - response.setCode(ResponseCode.SUCCESS); - response.setRemark(null); return response; } private RemotingCommand updateConfig(ChannelHandlerContext ctx, RemotingCommand request) { - log.info("updateConfig called by {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + if (ctx != null) { + log.info("updateConfig called by {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + } final RemotingCommand response = RemotingCommand.createResponseCommand(null); @@ -456,13 +631,6 @@ private RemotingCommand updateConfig(ChannelHandlerContext ctx, RemotingCommand return response; } - if (bodyStr == null) { - log.error("updateConfig get null body!"); - response.setCode(ResponseCode.SYSTEM_ERROR); - response.setRemark("string2Properties error"); - return response; - } - Properties properties = MixAll.string2Properties(bodyStr); if (properties == null) { log.error("updateConfig MixAll.string2Properties error {}", bodyStr); @@ -470,6 +638,11 @@ private RemotingCommand updateConfig(ChannelHandlerContext ctx, RemotingCommand response.setRemark("string2Properties error"); return response; } + if (validateBlackListConfigExist(properties)) { + response.setCode(ResponseCode.NO_PERMISSION); + response.setRemark("Can not update config in black list."); + return response; + } this.namesrvController.getConfiguration().update(properties); } @@ -483,7 +656,7 @@ private RemotingCommand getConfig(ChannelHandlerContext ctx, RemotingCommand req final RemotingCommand response = RemotingCommand.createResponseCommand(null); String content = this.namesrvController.getConfiguration().getAllConfigsFormatString(); - if (content != null && content.length() > 0) { + if (StringUtils.isNotBlank(content)) { try { response.setBody(content.getBytes(MixAll.DEFAULT_CHARSET)); } catch (UnsupportedEncodingException e) { @@ -499,4 +672,13 @@ private RemotingCommand getConfig(ChannelHandlerContext ctx, RemotingCommand req return response; } + private boolean validateBlackListConfigExist(Properties properties) { + for (String blackConfig : configBlackList) { + if (properties.containsKey(blackConfig)) { + return true; + } + } + return false; + } + } diff --git a/namesrv/src/main/java/org/apache/rocketmq/namesrv/route/ZoneRouteRPCHook.java b/namesrv/src/main/java/org/apache/rocketmq/namesrv/route/ZoneRouteRPCHook.java new file mode 100644 index 00000000000..4983c88c8af --- /dev/null +++ b/namesrv/src/main/java/org/apache/rocketmq/namesrv/route/ZoneRouteRPCHook.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.namesrv.route; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; + +public class ZoneRouteRPCHook implements RPCHook { + + @Override + public void doBeforeRequest(String remoteAddr, RemotingCommand request) { + + } + + @Override + public void doAfterResponse(String remoteAddr, RemotingCommand request, RemotingCommand response) { + if (RequestCode.GET_ROUTEINFO_BY_TOPIC != request.getCode()) { + return; + } + if (response == null || response.getBody() == null || ResponseCode.SUCCESS != response.getCode()) { + return; + } + boolean zoneMode = Boolean.parseBoolean(request.getExtFields().get(MixAll.ZONE_MODE)); + if (!zoneMode) { + return; + } + String zoneName = request.getExtFields().get(MixAll.ZONE_NAME); + if (StringUtils.isBlank(zoneName)) { + return; + } + TopicRouteData topicRouteData = RemotingSerializable.decode(response.getBody(), TopicRouteData.class); + + response.setBody(filterByZoneName(topicRouteData, zoneName).encode()); + } + + private TopicRouteData filterByZoneName(TopicRouteData topicRouteData, String zoneName) { + List brokerDataReserved = new ArrayList<>(); + Map brokerDataRemoved = new HashMap<>(); + for (BrokerData brokerData : topicRouteData.getBrokerDatas()) { + //master down, consume from slave. break nearby route rule. + if (brokerData.getBrokerAddrs().get(MixAll.MASTER_ID) == null + || StringUtils.equalsIgnoreCase(brokerData.getZoneName(), zoneName)) { + brokerDataReserved.add(brokerData); + } else { + brokerDataRemoved.put(brokerData.getBrokerName(), brokerData); + } + } + topicRouteData.setBrokerDatas(brokerDataReserved); + + List queueDataReserved = new ArrayList<>(); + for (QueueData queueData : topicRouteData.getQueueDatas()) { + if (!brokerDataRemoved.containsKey(queueData.getBrokerName())) { + queueDataReserved.add(queueData); + } + } + topicRouteData.setQueueDatas(queueDataReserved); + // remove filter server table by broker address + if (topicRouteData.getFilterServerTable() != null && !topicRouteData.getFilterServerTable().isEmpty()) { + for (Entry entry : brokerDataRemoved.entrySet()) { + BrokerData brokerData = entry.getValue(); + if (brokerData.getBrokerAddrs() == null) { + continue; + } + brokerData.getBrokerAddrs().values() + .forEach(brokerAddr -> topicRouteData.getFilterServerTable().remove(brokerAddr)); + } + } + return topicRouteData; + } +} diff --git a/namesrv/src/main/java/org/apache/rocketmq/namesrv/routeinfo/BatchUnregistrationService.java b/namesrv/src/main/java/org/apache/rocketmq/namesrv/routeinfo/BatchUnregistrationService.java new file mode 100644 index 00000000000..02cc722a159 --- /dev/null +++ b/namesrv/src/main/java/org/apache/rocketmq/namesrv/routeinfo/BatchUnregistrationService.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.namesrv.routeinfo; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.namesrv.NamesrvConfig; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.header.namesrv.UnRegisterBrokerRequestHeader; + +/** + * BatchUnregistrationService provides a mechanism to unregister brokers in batch manner, which speeds up broker-offline + * process. + */ +public class BatchUnregistrationService extends ServiceThread { + private final RouteInfoManager routeInfoManager; + private BlockingQueue unregistrationQueue; + private static final Logger log = LoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME); + + public BatchUnregistrationService(RouteInfoManager routeInfoManager, NamesrvConfig namesrvConfig) { + this.routeInfoManager = routeInfoManager; + this.unregistrationQueue = new LinkedBlockingQueue<>(namesrvConfig.getUnRegisterBrokerQueueCapacity()); + } + + /** + * Submits an unregister request to this queue. + * + * @param unRegisterRequest the request to submit + * @return {@code true} if the request was added to this queue, else {@code false} + */ + public boolean submit(UnRegisterBrokerRequestHeader unRegisterRequest) { + return unregistrationQueue.offer(unRegisterRequest); + } + + @Override + public String getServiceName() { + return BatchUnregistrationService.class.getName(); + } + + @Override + public void run() { + while (!this.isStopped()) { + try { + final UnRegisterBrokerRequestHeader request = unregistrationQueue.take(); + Set unregistrationRequests = new HashSet<>(); + unregistrationQueue.drainTo(unregistrationRequests); + + // Add polled request + unregistrationRequests.add(request); + + this.routeInfoManager.unRegisterBroker(unregistrationRequests); + } catch (Throwable e) { + log.error("Handle unregister broker request failed", e); + } + } + } + + // For test only + int queueLength() { + return this.unregistrationQueue.size(); + } +} diff --git a/namesrv/src/main/java/org/apache/rocketmq/namesrv/routeinfo/BrokerHousekeepingService.java b/namesrv/src/main/java/org/apache/rocketmq/namesrv/routeinfo/BrokerHousekeepingService.java index d3a320776ba..b527429f77d 100644 --- a/namesrv/src/main/java/org/apache/rocketmq/namesrv/routeinfo/BrokerHousekeepingService.java +++ b/namesrv/src/main/java/org/apache/rocketmq/namesrv/routeinfo/BrokerHousekeepingService.java @@ -17,14 +17,11 @@ package org.apache.rocketmq.namesrv.routeinfo; import io.netty.channel.Channel; -import org.apache.rocketmq.common.constant.LoggerName; import org.apache.rocketmq.namesrv.NamesrvController; import org.apache.rocketmq.remoting.ChannelEventListener; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class BrokerHousekeepingService implements ChannelEventListener { - private static final Logger log = LoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME); + private final NamesrvController namesrvController; public BrokerHousekeepingService(NamesrvController namesrvController) { @@ -37,16 +34,21 @@ public void onChannelConnect(String remoteAddr, Channel channel) { @Override public void onChannelClose(String remoteAddr, Channel channel) { - this.namesrvController.getRouteInfoManager().onChannelDestroy(remoteAddr, channel); + this.namesrvController.getRouteInfoManager().onChannelDestroy(channel); } @Override public void onChannelException(String remoteAddr, Channel channel) { - this.namesrvController.getRouteInfoManager().onChannelDestroy(remoteAddr, channel); + this.namesrvController.getRouteInfoManager().onChannelDestroy(channel); } @Override public void onChannelIdle(String remoteAddr, Channel channel) { - this.namesrvController.getRouteInfoManager().onChannelDestroy(remoteAddr, channel); + this.namesrvController.getRouteInfoManager().onChannelDestroy(channel); + } + + @Override + public void onChannelActive(String remoteAddr, Channel channel) { + } } diff --git a/namesrv/src/main/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManager.java b/namesrv/src/main/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManager.java index 109d3e810ec..ce311d392aa 100644 --- a/namesrv/src/main/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManager.java +++ b/namesrv/src/main/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManager.java @@ -16,7 +16,10 @@ */ package org.apache.rocketmq.namesrv.routeinfo; +import com.google.common.collect.Sets; import io.netty.channel.Channel; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -25,78 +28,185 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; -import org.apache.rocketmq.common.DataVersion; +import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.TopicConfig; import org.apache.rocketmq.common.constant.LoggerName; import org.apache.rocketmq.common.constant.PermName; -import org.apache.rocketmq.common.namesrv.RegisterBrokerResult; -import org.apache.rocketmq.common.protocol.body.ClusterInfo; -import org.apache.rocketmq.common.protocol.body.TopicConfigSerializeWrapper; -import org.apache.rocketmq.common.protocol.body.TopicList; -import org.apache.rocketmq.common.protocol.route.BrokerData; -import org.apache.rocketmq.common.protocol.route.QueueData; -import org.apache.rocketmq.common.protocol.route.TopicRouteData; +import org.apache.rocketmq.common.namesrv.NamesrvConfig; import org.apache.rocketmq.common.sysflag.TopicSysFlag; -import org.apache.rocketmq.remoting.common.RemotingUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.common.utils.ConcurrentHashMapUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.namesrv.NamesrvController; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.exception.RemotingConnectException; +import org.apache.rocketmq.remoting.exception.RemotingSendRequestException; +import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; +import org.apache.rocketmq.remoting.exception.RemotingTooMuchRequestException; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.body.BrokerMemberGroup; +import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigAndMappingSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.body.TopicList; +import org.apache.rocketmq.remoting.protocol.header.NotifyMinBrokerIdChangeRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.UnRegisterBrokerRequestHeader; +import org.apache.rocketmq.remoting.protocol.namesrv.RegisterBrokerResult; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingInfo; public class RouteInfoManager { private static final Logger log = LoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME); - private final static long BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2; + private final static long DEFAULT_BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2; private final ReadWriteLock lock = new ReentrantReadWriteLock(); - private final HashMap> topicQueueTable; - private final HashMap brokerAddrTable; - private final HashMap> clusterAddrTable; - private final HashMap brokerLiveTable; - private final HashMap/* Filter Server */> filterServerTable; + private final Map> topicQueueTable; + private final Map brokerAddrTable; + private final Map> clusterAddrTable; + private final Map brokerLiveTable; + private final Map/* Filter Server */> filterServerTable; + private final Map> topicQueueMappingInfoTable; + + private final BatchUnregistrationService unRegisterService; + + private final NamesrvController namesrvController; + private final NamesrvConfig namesrvConfig; + + public RouteInfoManager(final NamesrvConfig namesrvConfig, NamesrvController namesrvController) { + this.topicQueueTable = new ConcurrentHashMap<>(1024); + this.brokerAddrTable = new ConcurrentHashMap<>(128); + this.clusterAddrTable = new ConcurrentHashMap<>(32); + this.brokerLiveTable = new ConcurrentHashMap<>(256); + this.filterServerTable = new ConcurrentHashMap<>(256); + this.topicQueueMappingInfoTable = new ConcurrentHashMap<>(1024); + this.unRegisterService = new BatchUnregistrationService(this, namesrvConfig); + this.namesrvConfig = namesrvConfig; + this.namesrvController = namesrvController; + } + + public void start() { + this.unRegisterService.start(); + } - public RouteInfoManager() { - this.topicQueueTable = new HashMap>(1024); - this.brokerAddrTable = new HashMap(128); - this.clusterAddrTable = new HashMap>(32); - this.brokerLiveTable = new HashMap(256); - this.filterServerTable = new HashMap>(256); + public void shutdown() { + this.unRegisterService.shutdown(true); } - public byte[] getAllClusterInfo() { + public boolean submitUnRegisterBrokerRequest(UnRegisterBrokerRequestHeader unRegisterRequest) { + return this.unRegisterService.submit(unRegisterRequest); + } + + // For test only + int blockedUnRegisterRequests() { + return this.unRegisterService.queueLength(); + } + + public ClusterInfo getAllClusterInfo() { ClusterInfo clusterInfoSerializeWrapper = new ClusterInfo(); clusterInfoSerializeWrapper.setBrokerAddrTable(this.brokerAddrTable); clusterInfoSerializeWrapper.setClusterAddrTable(this.clusterAddrTable); - return clusterInfoSerializeWrapper.encode(); + return clusterInfoSerializeWrapper; + } + + public void registerTopic(final String topic, List queueDatas) { + if (queueDatas == null || queueDatas.isEmpty()) { + return; + } + + try { + this.lock.writeLock().lockInterruptibly(); + if (this.topicQueueTable.containsKey(topic)) { + Map queueDataMap = this.topicQueueTable.get(topic); + for (QueueData queueData : queueDatas) { + if (!this.brokerAddrTable.containsKey(queueData.getBrokerName())) { + log.warn("Register topic contains illegal broker, {}, {}", topic, queueData); + return; + } + queueDataMap.put(queueData.getBrokerName(), queueData); + } + log.info("Topic route already exist.{}, {}", topic, this.topicQueueTable.get(topic)); + } else { + // check and construct queue data map + Map queueDataMap = new HashMap<>(); + for (QueueData queueData : queueDatas) { + if (!this.brokerAddrTable.containsKey(queueData.getBrokerName())) { + log.warn("Register topic contains illegal broker, {}, {}", topic, queueData); + return; + } + queueDataMap.put(queueData.getBrokerName(), queueData); + } + + this.topicQueueTable.put(topic, queueDataMap); + log.info("Register topic route:{}, {}", topic, queueDatas); + } + } catch (Exception e) { + log.error("registerTopic Exception", e); + } finally { + this.lock.writeLock().unlock(); + } } public void deleteTopic(final String topic) { try { - try { - this.lock.writeLock().lockInterruptibly(); - this.topicQueueTable.remove(topic); - } finally { - this.lock.writeLock().unlock(); + this.lock.writeLock().lockInterruptibly(); + this.topicQueueTable.remove(topic); + } catch (Exception e) { + log.error("deleteTopic Exception", e); + } finally { + this.lock.writeLock().unlock(); + } + } + + public void deleteTopic(final String topic, final String clusterName) { + try { + this.lock.writeLock().lockInterruptibly(); + //get all the brokerNames fot the specified cluster + Set brokerNames = this.clusterAddrTable.get(clusterName); + if (brokerNames == null || brokerNames.isEmpty()) { + return; + } + //get the store information for single topic + Map queueDataMap = this.topicQueueTable.get(topic); + if (queueDataMap != null) { + for (String brokerName : brokerNames) { + final QueueData removedQD = queueDataMap.remove(brokerName); + if (removedQD != null) { + log.info("deleteTopic, remove one broker's topic {} {} {}", brokerName, topic, removedQD); + } + } + if (queueDataMap.isEmpty()) { + log.info("deleteTopic, remove the topic all queue {} {}", clusterName, topic); + this.topicQueueTable.remove(topic); + } } } catch (Exception e) { log.error("deleteTopic Exception", e); + } finally { + this.lock.writeLock().unlock(); } } - public byte[] getAllTopicList() { + public TopicList getAllTopicList() { TopicList topicList = new TopicList(); try { - try { - this.lock.readLock().lockInterruptibly(); - topicList.getTopicList().addAll(this.topicQueueTable.keySet()); - } finally { - this.lock.readLock().unlock(); - } + this.lock.readLock().lockInterruptibly(); + topicList.getTopicList().addAll(this.topicQueueTable.keySet()); } catch (Exception e) { log.error("getAllTopicList Exception", e); + } finally { + this.lock.readLock().unlock(); } - return topicList.encode(); + return topicList; } public RegisterBrokerResult registerBroker( @@ -105,91 +215,263 @@ public RegisterBrokerResult registerBroker( final String brokerName, final long brokerId, final String haServerAddr, + final String zoneName, + final Long timeoutMillis, + final TopicConfigSerializeWrapper topicConfigWrapper, + final List filterServerList, + final Channel channel) { + return registerBroker(clusterName, brokerAddr, brokerName, brokerId, haServerAddr, zoneName, timeoutMillis, false, topicConfigWrapper, filterServerList, channel); + } + + public RegisterBrokerResult registerBroker( + final String clusterName, + final String brokerAddr, + final String brokerName, + final long brokerId, + final String haServerAddr, + final String zoneName, + final Long timeoutMillis, + final Boolean enableActingMaster, final TopicConfigSerializeWrapper topicConfigWrapper, final List filterServerList, final Channel channel) { RegisterBrokerResult result = new RegisterBrokerResult(); try { - try { - this.lock.writeLock().lockInterruptibly(); + this.lock.writeLock().lockInterruptibly(); - Set brokerNames = this.clusterAddrTable.get(clusterName); - if (null == brokerNames) { - brokerNames = new HashSet(); - this.clusterAddrTable.put(clusterName, brokerNames); - } - brokerNames.add(brokerName); + //init or update the cluster info + Set brokerNames = ConcurrentHashMapUtils.computeIfAbsent((ConcurrentHashMap>) this.clusterAddrTable, clusterName, k -> new HashSet<>()); + brokerNames.add(brokerName); - boolean registerFirst = false; + boolean registerFirst = false; - BrokerData brokerData = this.brokerAddrTable.get(brokerName); - if (null == brokerData) { - registerFirst = true; - brokerData = new BrokerData(clusterName, brokerName, new HashMap()); - this.brokerAddrTable.put(brokerName, brokerData); + BrokerData brokerData = this.brokerAddrTable.get(brokerName); + if (null == brokerData) { + registerFirst = true; + brokerData = new BrokerData(clusterName, brokerName, new HashMap<>()); + this.brokerAddrTable.put(brokerName, brokerData); + } + + boolean isOldVersionBroker = enableActingMaster == null; + brokerData.setEnableActingMaster(!isOldVersionBroker && enableActingMaster); + brokerData.setZoneName(zoneName); + + Map brokerAddrsMap = brokerData.getBrokerAddrs(); + + boolean isMinBrokerIdChanged = false; + long prevMinBrokerId = 0; + if (!brokerAddrsMap.isEmpty()) { + prevMinBrokerId = Collections.min(brokerAddrsMap.keySet()); + } + + if (brokerId < prevMinBrokerId) { + isMinBrokerIdChanged = true; + } + + //Switch slave to master: first remove <1, IP:PORT> in namesrv, then add <0, IP:PORT> + //The same IP:PORT must only have one record in brokerAddrTable + brokerAddrsMap.entrySet().removeIf(item -> null != brokerAddr && brokerAddr.equals(item.getValue()) && brokerId != item.getKey()); + + //If Local brokerId stateVersion bigger than the registering one, + String oldBrokerAddr = brokerAddrsMap.get(brokerId); + if (null != oldBrokerAddr && !oldBrokerAddr.equals(brokerAddr)) { + BrokerLiveInfo oldBrokerInfo = brokerLiveTable.get(new BrokerAddrInfo(clusterName, oldBrokerAddr)); + + if (null != oldBrokerInfo) { + long oldStateVersion = oldBrokerInfo.getDataVersion().getStateVersion(); + long newStateVersion = topicConfigWrapper.getDataVersion().getStateVersion(); + if (oldStateVersion > newStateVersion) { + log.warn("Registered Broker conflicts with the existed one, just ignore.: Cluster:{}, BrokerName:{}, BrokerId:{}, " + + "Old BrokerAddr:{}, Old Version:{}, New BrokerAddr:{}, New Version:{}.", + clusterName, brokerName, brokerId, oldBrokerAddr, oldStateVersion, brokerAddr, newStateVersion); + //Remove the rejected brokerAddr from brokerLiveTable. + brokerLiveTable.remove(new BrokerAddrInfo(clusterName, brokerAddr)); + return result; + } } - String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr); - registerFirst = registerFirst || (null == oldAddr); - - if (null != topicConfigWrapper - && MixAll.MASTER_ID == brokerId) { - if (this.isBrokerTopicConfigChanged(brokerAddr, topicConfigWrapper.getDataVersion()) - || registerFirst) { - ConcurrentMap tcTable = - topicConfigWrapper.getTopicConfigTable(); - if (tcTable != null) { - for (Map.Entry entry : tcTable.entrySet()) { - this.createAndUpdateQueueData(brokerName, entry.getValue()); + } + + if (!brokerAddrsMap.containsKey(brokerId) && topicConfigWrapper.getTopicConfigTable().size() == 1) { + log.warn("Can't register topicConfigWrapper={} because broker[{}]={} has not registered.", + topicConfigWrapper.getTopicConfigTable(), brokerId, brokerAddr); + return null; + } + + String oldAddr = brokerAddrsMap.put(brokerId, brokerAddr); + registerFirst = registerFirst || (StringUtils.isEmpty(oldAddr)); + + boolean isMaster = MixAll.MASTER_ID == brokerId; + + boolean isPrimeSlave = !isOldVersionBroker && !isMaster + && brokerId == Collections.min(brokerAddrsMap.keySet()); + + if (null != topicConfigWrapper && (isMaster || isPrimeSlave)) { + + ConcurrentMap tcTable = + topicConfigWrapper.getTopicConfigTable(); + + if (tcTable != null) { + + TopicConfigAndMappingSerializeWrapper mappingSerializeWrapper = TopicConfigAndMappingSerializeWrapper.from(topicConfigWrapper); + Map topicQueueMappingInfoMap = mappingSerializeWrapper.getTopicQueueMappingInfoMap(); + + // Delete the topics that don't exist in tcTable from the current broker + // Static topic is not supported currently + if (namesrvConfig.isDeleteTopicWithBrokerRegistration() && topicQueueMappingInfoMap.isEmpty()) { + final Set oldTopicSet = topicSetOfBrokerName(brokerName); + final Set newTopicSet = tcTable.keySet(); + final Sets.SetView toDeleteTopics = Sets.difference(oldTopicSet, newTopicSet); + for (final String toDeleteTopic : toDeleteTopics) { + Map queueDataMap = topicQueueTable.get(toDeleteTopic); + final QueueData removedQD = queueDataMap.remove(brokerName); + if (removedQD != null) { + log.info("deleteTopic, remove one broker's topic {} {} {}", brokerName, toDeleteTopic, removedQD); + } + + if (queueDataMap.isEmpty()) { + log.info("deleteTopic, remove the topic all queue {}", toDeleteTopic); + topicQueueTable.remove(toDeleteTopic); } } } - } - BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr, - new BrokerLiveInfo( - System.currentTimeMillis(), - topicConfigWrapper.getDataVersion(), - channel, - haServerAddr)); - if (null == prevBrokerLiveInfo) { - log.info("new broker registered, {} HAServer: {}", brokerAddr, haServerAddr); - } + for (Map.Entry entry : tcTable.entrySet()) { + if (registerFirst || this.isTopicConfigChanged(clusterName, brokerAddr, + topicConfigWrapper.getDataVersion(), brokerName, + entry.getValue().getTopicName())) { + final TopicConfig topicConfig = entry.getValue(); + // In Slave Acting Master mode, Namesrv will regard the surviving Slave with the smallest brokerId as the "agent" Master, and modify the brokerPermission to read-only. + if (isPrimeSlave && brokerData.isEnableActingMaster()) { + // Wipe write perm for prime slave + topicConfig.setPerm(topicConfig.getPerm() & (~PermName.PERM_WRITE)); + } + this.createAndUpdateQueueData(brokerName, topicConfig); + } + } - if (filterServerList != null) { - if (filterServerList.isEmpty()) { - this.filterServerTable.remove(brokerAddr); - } else { - this.filterServerTable.put(brokerAddr, filterServerList); + if (this.isBrokerTopicConfigChanged(clusterName, brokerAddr, topicConfigWrapper.getDataVersion()) || registerFirst) { + //the topicQueueMappingInfoMap should never be null, but can be empty + for (Map.Entry entry : topicQueueMappingInfoMap.entrySet()) { + if (!topicQueueMappingInfoTable.containsKey(entry.getKey())) { + topicQueueMappingInfoTable.put(entry.getKey(), new HashMap<>()); + } + //Note asset brokerName equal entry.getValue().getBname() + //here use the mappingDetail.bname + topicQueueMappingInfoTable.get(entry.getKey()).put(entry.getValue().getBname(), entry.getValue()); + } } } + } - if (MixAll.MASTER_ID != brokerId) { - String masterAddr = brokerData.getBrokerAddrs().get(MixAll.MASTER_ID); - if (masterAddr != null) { - BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.get(masterAddr); - if (brokerLiveInfo != null) { - result.setHaServerAddr(brokerLiveInfo.getHaServerAddr()); - result.setMasterAddr(masterAddr); - } + BrokerAddrInfo brokerAddrInfo = new BrokerAddrInfo(clusterName, brokerAddr); + BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddrInfo, + new BrokerLiveInfo( + System.currentTimeMillis(), + timeoutMillis == null ? DEFAULT_BROKER_CHANNEL_EXPIRED_TIME : timeoutMillis, + topicConfigWrapper == null ? new DataVersion() : topicConfigWrapper.getDataVersion(), + channel, + haServerAddr)); + if (null == prevBrokerLiveInfo) { + log.info("new broker registered, {} HAService: {}", brokerAddrInfo, haServerAddr); + } + + if (filterServerList != null) { + if (filterServerList.isEmpty()) { + this.filterServerTable.remove(brokerAddrInfo); + } else { + this.filterServerTable.put(brokerAddrInfo, filterServerList); + } + } + + if (MixAll.MASTER_ID != brokerId) { + String masterAddr = brokerData.getBrokerAddrs().get(MixAll.MASTER_ID); + if (masterAddr != null) { + BrokerAddrInfo masterAddrInfo = new BrokerAddrInfo(clusterName, masterAddr); + BrokerLiveInfo masterLiveInfo = this.brokerLiveTable.get(masterAddrInfo); + if (masterLiveInfo != null) { + result.setHaServerAddr(masterLiveInfo.getHaServerAddr()); + result.setMasterAddr(masterAddr); } } - } finally { - this.lock.writeLock().unlock(); + } + + if (isMinBrokerIdChanged && namesrvConfig.isNotifyMinBrokerIdChanged()) { + notifyMinBrokerIdChanged(brokerAddrsMap, null, + this.brokerLiveTable.get(brokerAddrInfo).getHaServerAddr()); } } catch (Exception e) { log.error("registerBroker Exception", e); + } finally { + this.lock.writeLock().unlock(); } return result; } - private boolean isBrokerTopicConfigChanged(final String brokerAddr, final DataVersion dataVersion) { - BrokerLiveInfo prev = this.brokerLiveTable.get(brokerAddr); - if (null == prev || !prev.getDataVersion().equals(dataVersion)) { + private Set topicSetOfBrokerName(final String brokerName) { + Set topicOfBroker = new HashSet<>(); + for (final Entry> entry : this.topicQueueTable.entrySet()) { + if (entry.getValue().containsKey(brokerName)) { + topicOfBroker.add(entry.getKey()); + } + } + return topicOfBroker; + } + + public BrokerMemberGroup getBrokerMemberGroup(String clusterName, String brokerName) { + BrokerMemberGroup groupMember = new BrokerMemberGroup(clusterName, brokerName); + try { + try { + this.lock.readLock().lockInterruptibly(); + final BrokerData brokerData = this.brokerAddrTable.get(brokerName); + if (brokerData != null) { + groupMember.getBrokerAddrs().putAll(brokerData.getBrokerAddrs()); + } + } finally { + this.lock.readLock().unlock(); + } + } catch (Exception e) { + log.error("Get broker member group exception", e); + } + return groupMember; + } + + public boolean isBrokerTopicConfigChanged(final String clusterName, final String brokerAddr, + final DataVersion dataVersion) { + DataVersion prev = queryBrokerTopicConfig(clusterName, brokerAddr); + return null == prev || !prev.equals(dataVersion); + } + + public boolean isTopicConfigChanged(final String clusterName, final String brokerAddr, + final DataVersion dataVersion, String brokerName, String topic) { + boolean isChange = isBrokerTopicConfigChanged(clusterName, brokerAddr, dataVersion); + if (isChange) { + return true; + } + final Map queueDataMap = this.topicQueueTable.get(topic); + if (queueDataMap == null || queueDataMap.isEmpty()) { return true; } - return false; + // The topicQueueTable already contains the broker + return !queueDataMap.containsKey(brokerName); + } + + public DataVersion queryBrokerTopicConfig(final String clusterName, final String brokerAddr) { + BrokerAddrInfo addrInfo = new BrokerAddrInfo(clusterName, brokerAddr); + BrokerLiveInfo prev = this.brokerLiveTable.get(addrInfo); + if (prev != null) { + return prev.getDataVersion(); + } + return null; + } + + public void updateBrokerInfoUpdateTimestamp(final String clusterName, final String brokerAddr) { + BrokerAddrInfo addrInfo = new BrokerAddrInfo(clusterName, brokerAddr); + BrokerLiveInfo prev = this.brokerLiveTable.get(addrInfo); + if (prev != null) { + prev.setLastUpdateTimestamp(System.currentTimeMillis()); + } } private void createAndUpdateQueueData(final String brokerName, final TopicConfig topicConfig) { @@ -198,33 +480,22 @@ private void createAndUpdateQueueData(final String brokerName, final TopicConfig queueData.setWriteQueueNums(topicConfig.getWriteQueueNums()); queueData.setReadQueueNums(topicConfig.getReadQueueNums()); queueData.setPerm(topicConfig.getPerm()); - queueData.setTopicSynFlag(topicConfig.getTopicSysFlag()); + queueData.setTopicSysFlag(topicConfig.getTopicSysFlag()); - List queueDataList = this.topicQueueTable.get(topicConfig.getTopicName()); - if (null == queueDataList) { - queueDataList = new LinkedList(); - queueDataList.add(queueData); - this.topicQueueTable.put(topicConfig.getTopicName(), queueDataList); + Map queueDataMap = this.topicQueueTable.get(topicConfig.getTopicName()); + if (null == queueDataMap) { + queueDataMap = new HashMap<>(); + queueDataMap.put(brokerName, queueData); + this.topicQueueTable.put(topicConfig.getTopicName(), queueDataMap); log.info("new topic registered, {} {}", topicConfig.getTopicName(), queueData); } else { - boolean addNewOne = true; - - Iterator it = queueDataList.iterator(); - while (it.hasNext()) { - QueueData qd = it.next(); - if (qd.getBrokerName().equals(brokerName)) { - if (qd.equals(queueData)) { - addNewOne = false; - } else { - log.info("topic changed, {} OLD: {} NEW: {}", topicConfig.getTopicName(), qd, - queueData); - it.remove(); - } - } - } - - if (addNewOne) { - queueDataList.add(queueData); + final QueueData existedQD = queueDataMap.get(brokerName); + if (existedQD == null) { + queueDataMap.put(brokerName, queueData); + } else if (!existedQD.equals(queueData)) { + log.info("topic changed, {} OLD: {} NEW: {}", topicConfig.getTopicName(), existedQD, + queueData); + queueDataMap.put(brokerName, queueData); } } } @@ -233,7 +504,7 @@ public int wipeWritePermOfBrokerByLock(final String brokerName) { try { try { this.lock.writeLock().lockInterruptibly(); - return wipeWritePermOfBroker(brokerName); + return operateWritePermOfBroker(brokerName, RequestCode.WIPE_WRITE_PERM_OF_BROKER); } finally { this.lock.writeLock().unlock(); } @@ -244,26 +515,43 @@ public int wipeWritePermOfBrokerByLock(final String brokerName) { return 0; } - private int wipeWritePermOfBroker(final String brokerName) { - int wipeTopicCnt = 0; - Iterator>> itTopic = this.topicQueueTable.entrySet().iterator(); - while (itTopic.hasNext()) { - Entry> entry = itTopic.next(); - List qdList = entry.getValue(); + public int addWritePermOfBrokerByLock(final String brokerName) { + try { + try { + this.lock.writeLock().lockInterruptibly(); + return operateWritePermOfBroker(brokerName, RequestCode.ADD_WRITE_PERM_OF_BROKER); + } finally { + this.lock.writeLock().unlock(); + } + } catch (Exception e) { + log.error("addWritePermOfBrokerByLock Exception", e); + } + return 0; + } + + private int operateWritePermOfBroker(final String brokerName, final int requestCode) { + int topicCnt = 0; - Iterator it = qdList.iterator(); - while (it.hasNext()) { - QueueData qd = it.next(); - if (qd.getBrokerName().equals(brokerName)) { - int perm = qd.getPerm(); + for (Entry> entry : this.topicQueueTable.entrySet()) { + Map qdMap = entry.getValue(); + + final QueueData qd = qdMap.get(brokerName); + if (qd == null) { + continue; + } + int perm = qd.getPerm(); + switch (requestCode) { + case RequestCode.WIPE_WRITE_PERM_OF_BROKER: perm &= ~PermName.PERM_WRITE; - qd.setPerm(perm); - wipeTopicCnt++; - } + break; + case RequestCode.ADD_WRITE_PERM_OF_BROKER: + perm = PermName.PERM_READ | PermName.PERM_WRITE; + break; } + qd.setPerm(perm); + topicCnt++; } - - return wipeTopicCnt; + return topicCnt; } public void unregisterBroker( @@ -271,26 +559,50 @@ public void unregisterBroker( final String brokerAddr, final String brokerName, final long brokerId) { + UnRegisterBrokerRequestHeader unRegisterBrokerRequest = new UnRegisterBrokerRequestHeader(); + unRegisterBrokerRequest.setClusterName(clusterName); + unRegisterBrokerRequest.setBrokerAddr(brokerAddr); + unRegisterBrokerRequest.setBrokerName(brokerName); + unRegisterBrokerRequest.setBrokerId(brokerId); + + unRegisterBroker(Sets.newHashSet(unRegisterBrokerRequest)); + } + + public void unRegisterBroker(Set unRegisterRequests) { try { - try { - this.lock.writeLock().lockInterruptibly(); - BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.remove(brokerAddr); + Set removedBroker = new HashSet<>(); + Set reducedBroker = new HashSet<>(); + Map needNotifyBrokerMap = new HashMap<>(); + + this.lock.writeLock().lockInterruptibly(); + for (final UnRegisterBrokerRequestHeader unRegisterRequest : unRegisterRequests) { + final String brokerName = unRegisterRequest.getBrokerName(); + final String clusterName = unRegisterRequest.getClusterName(); + final String brokerAddr = unRegisterRequest.getBrokerAddr(); + + BrokerAddrInfo brokerAddrInfo = new BrokerAddrInfo(clusterName, brokerAddr); + + BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.remove(brokerAddrInfo); log.info("unregisterBroker, remove from brokerLiveTable {}, {}", brokerLiveInfo != null ? "OK" : "Failed", - brokerAddr + brokerAddrInfo ); - this.filterServerTable.remove(brokerAddr); + this.filterServerTable.remove(brokerAddrInfo); boolean removeBrokerName = false; + boolean isMinBrokerIdChanged = false; BrokerData brokerData = this.brokerAddrTable.get(brokerName); if (null != brokerData) { - String addr = brokerData.getBrokerAddrs().remove(brokerId); + if (!brokerData.getBrokerAddrs().isEmpty() && + unRegisterRequest.getBrokerId().equals(Collections.min(brokerData.getBrokerAddrs().keySet()))) { + isMinBrokerIdChanged = true; + } + boolean removed = brokerData.getBrokerAddrs().entrySet().removeIf(item -> item.getValue().equals(brokerAddr)); log.info("unregisterBroker, remove addr from brokerAddrTable {}, {}", - addr != null ? "OK" : "Failed", - brokerAddr + removed ? "OK" : "Failed", + brokerAddrInfo ); - if (brokerData.getBrokerAddrs().isEmpty()) { this.brokerAddrTable.remove(brokerName); log.info("unregisterBroker, remove name from brokerAddrTable OK, {}", @@ -298,6 +610,9 @@ public void unregisterBroker( ); removeBrokerName = true; + } else if (isMinBrokerIdChanged) { + needNotifyBrokerMap.put(brokerName, new BrokerStatusChangeInfo( + brokerData.getBrokerAddrs(), brokerAddr, null)); } } @@ -316,90 +631,169 @@ public void unregisterBroker( ); } } - this.removeTopicByBrokerName(brokerName); + removedBroker.add(brokerName); + } else { + reducedBroker.add(brokerName); } - } finally { - this.lock.writeLock().unlock(); + } + + cleanTopicByUnRegisterRequests(removedBroker, reducedBroker); + + if (!needNotifyBrokerMap.isEmpty() && namesrvConfig.isNotifyMinBrokerIdChanged()) { + notifyMinBrokerIdChanged(needNotifyBrokerMap); } } catch (Exception e) { log.error("unregisterBroker Exception", e); + } finally { + this.lock.writeLock().unlock(); } } - private void removeTopicByBrokerName(final String brokerName) { - Iterator>> itMap = this.topicQueueTable.entrySet().iterator(); + private void cleanTopicByUnRegisterRequests(Set removedBroker, Set reducedBroker) { + Iterator>> itMap = this.topicQueueTable.entrySet().iterator(); while (itMap.hasNext()) { - Entry> entry = itMap.next(); + Entry> entry = itMap.next(); String topic = entry.getKey(); - List queueDataList = entry.getValue(); - Iterator it = queueDataList.iterator(); - while (it.hasNext()) { - QueueData qd = it.next(); - if (qd.getBrokerName().equals(brokerName)) { - log.info("removeTopicByBrokerName, remove one broker's topic {} {}", topic, qd); - it.remove(); + Map queueDataMap = entry.getValue(); + + for (final String brokerName : removedBroker) { + final QueueData removedQD = queueDataMap.remove(brokerName); + if (removedQD != null) { + log.debug("removeTopicByBrokerName, remove one broker's topic {} {}", topic, removedQD); } } - if (queueDataList.isEmpty()) { - log.info("removeTopicByBrokerName, remove the topic all queue {}", topic); + if (queueDataMap.isEmpty()) { + log.debug("removeTopicByBrokerName, remove the topic all queue {}", topic); itMap.remove(); } + + for (final String brokerName : reducedBroker) { + final QueueData queueData = queueDataMap.get(brokerName); + + if (queueData != null) { + if (this.brokerAddrTable.get(brokerName).isEnableActingMaster()) { + // Master has been unregistered, wipe the write perm + if (isNoMasterExists(brokerName)) { + queueData.setPerm(queueData.getPerm() & (~PermName.PERM_WRITE)); + } + } + } + } + } + } + + private boolean isNoMasterExists(String brokerName) { + final BrokerData brokerData = this.brokerAddrTable.get(brokerName); + if (brokerData == null) { + return true; + } + + if (brokerData.getBrokerAddrs().size() == 0) { + return true; } + + return Collections.min(brokerData.getBrokerAddrs().keySet()) > 0; } public TopicRouteData pickupTopicRouteData(final String topic) { TopicRouteData topicRouteData = new TopicRouteData(); boolean foundQueueData = false; boolean foundBrokerData = false; - Set brokerNameSet = new HashSet(); - List brokerDataList = new LinkedList(); + List brokerDataList = new LinkedList<>(); topicRouteData.setBrokerDatas(brokerDataList); - HashMap> filterServerMap = new HashMap>(); + HashMap> filterServerMap = new HashMap<>(); topicRouteData.setFilterServerTable(filterServerMap); try { - try { - this.lock.readLock().lockInterruptibly(); - List queueDataList = this.topicQueueTable.get(topic); - if (queueDataList != null) { - topicRouteData.setQueueDatas(queueDataList); - foundQueueData = true; - - Iterator it = queueDataList.iterator(); - while (it.hasNext()) { - QueueData qd = it.next(); - brokerNameSet.add(qd.getBrokerName()); + this.lock.readLock().lockInterruptibly(); + Map queueDataMap = this.topicQueueTable.get(topic); + if (queueDataMap != null) { + topicRouteData.setQueueDatas(new ArrayList<>(queueDataMap.values())); + foundQueueData = true; + + Set brokerNameSet = new HashSet<>(queueDataMap.keySet()); + + for (String brokerName : brokerNameSet) { + BrokerData brokerData = this.brokerAddrTable.get(brokerName); + if (null == brokerData) { + continue; } + BrokerData brokerDataClone = new BrokerData(brokerData); - for (String brokerName : brokerNameSet) { - BrokerData brokerData = this.brokerAddrTable.get(brokerName); - if (null != brokerData) { - BrokerData brokerDataClone = new BrokerData(brokerData.getCluster(), brokerData.getBrokerName(), (HashMap) brokerData - .getBrokerAddrs().clone()); - brokerDataList.add(brokerDataClone); - foundBrokerData = true; - for (final String brokerAddr : brokerDataClone.getBrokerAddrs().values()) { - List filterServerList = this.filterServerTable.get(brokerAddr); - filterServerMap.put(brokerAddr, filterServerList); - } - } + brokerDataList.add(brokerDataClone); + foundBrokerData = true; + if (filterServerTable.isEmpty()) { + continue; + } + for (final String brokerAddr : brokerDataClone.getBrokerAddrs().values()) { + BrokerAddrInfo brokerAddrInfo = new BrokerAddrInfo(brokerDataClone.getCluster(), brokerAddr); + List filterServerList = this.filterServerTable.get(brokerAddrInfo); + filterServerMap.put(brokerAddr, filterServerList); } + } - } finally { - this.lock.readLock().unlock(); } } catch (Exception e) { log.error("pickupTopicRouteData Exception", e); + } finally { + this.lock.readLock().unlock(); } - if (log.isDebugEnabled()) { - log.debug("pickupTopicRouteData {} {}", topic, topicRouteData); - } + log.debug("pickupTopicRouteData {} {}", topic, topicRouteData); if (foundBrokerData && foundQueueData) { + + topicRouteData.setTopicQueueMappingByBroker(this.topicQueueMappingInfoTable.get(topic)); + + if (!namesrvConfig.isSupportActingMaster()) { + return topicRouteData; + } + + if (topic.startsWith(TopicValidator.SYNC_BROKER_MEMBER_GROUP_PREFIX)) { + return topicRouteData; + } + + if (topicRouteData.getBrokerDatas().size() == 0 || topicRouteData.getQueueDatas().size() == 0) { + return topicRouteData; + } + + boolean needActingMaster = false; + + for (final BrokerData brokerData : topicRouteData.getBrokerDatas()) { + if (brokerData.getBrokerAddrs().size() != 0 + && !brokerData.getBrokerAddrs().containsKey(MixAll.MASTER_ID)) { + needActingMaster = true; + break; + } + } + + if (!needActingMaster) { + return topicRouteData; + } + + for (final BrokerData brokerData : topicRouteData.getBrokerDatas()) { + final HashMap brokerAddrs = brokerData.getBrokerAddrs(); + if (brokerAddrs.size() == 0 || brokerAddrs.containsKey(MixAll.MASTER_ID) || !brokerData.isEnableActingMaster()) { + continue; + } + + // No master + for (final QueueData queueData : topicRouteData.getQueueDatas()) { + if (queueData.getBrokerName().equals(brokerData.getBrokerName())) { + if (!PermName.isWriteable(queueData.getPerm())) { + final Long minBrokerId = Collections.min(brokerAddrs.keySet()); + final String actingMasterAddr = brokerAddrs.remove(minBrokerId); + brokerAddrs.put(MixAll.MASTER_ID, actingMasterAddr); + } + break; + } + } + + } + return topicRouteData; } @@ -407,34 +801,63 @@ public TopicRouteData pickupTopicRouteData(final String topic) { } public void scanNotActiveBroker() { - Iterator> it = this.brokerLiveTable.entrySet().iterator(); - while (it.hasNext()) { - Entry next = it.next(); - long last = next.getValue().getLastUpdateTimestamp(); - if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) { - RemotingUtil.closeChannel(next.getValue().getChannel()); - it.remove(); - log.warn("The broker channel expired, {} {}ms", next.getKey(), BROKER_CHANNEL_EXPIRED_TIME); - this.onChannelDestroy(next.getKey(), next.getValue().getChannel()); + try { + log.info("start scanNotActiveBroker"); + for (Entry next : this.brokerLiveTable.entrySet()) { + long last = next.getValue().getLastUpdateTimestamp(); + long timeoutMillis = next.getValue().getHeartbeatTimeoutMillis(); + if ((last + timeoutMillis) < System.currentTimeMillis()) { + RemotingHelper.closeChannel(next.getValue().getChannel()); + log.warn("The broker channel expired, {} {}ms", next.getKey(), timeoutMillis); + this.onChannelDestroy(next.getKey()); + } } + } catch (Exception e) { + log.error("scanNotActiveBroker exception", e); } } - public void onChannelDestroy(String remoteAddr, Channel channel) { - String brokerAddrFound = null; + public void onChannelDestroy(BrokerAddrInfo brokerAddrInfo) { + UnRegisterBrokerRequestHeader unRegisterRequest = new UnRegisterBrokerRequestHeader(); + boolean needUnRegister = false; + if (brokerAddrInfo != null) { + try { + try { + this.lock.readLock().lockInterruptibly(); + needUnRegister = setupUnRegisterRequest(unRegisterRequest, brokerAddrInfo); + } finally { + this.lock.readLock().unlock(); + } + } catch (Exception e) { + log.error("onChannelDestroy Exception", e); + } + } + + if (needUnRegister) { + boolean result = this.submitUnRegisterBrokerRequest(unRegisterRequest); + log.info("the broker's channel destroyed, submit the unregister request at once, " + + "broker info: {}, submit result: {}", unRegisterRequest, result); + } + } + + public void onChannelDestroy(Channel channel) { + UnRegisterBrokerRequestHeader unRegisterRequest = new UnRegisterBrokerRequestHeader(); + BrokerAddrInfo brokerAddrFound = null; + boolean needUnRegister = false; if (channel != null) { try { try { this.lock.readLock().lockInterruptibly(); - Iterator> itBrokerLiveTable = - this.brokerLiveTable.entrySet().iterator(); - while (itBrokerLiveTable.hasNext()) { - Entry entry = itBrokerLiveTable.next(); + for (Entry entry : this.brokerLiveTable.entrySet()) { if (entry.getValue().getChannel() == channel) { brokerAddrFound = entry.getKey(); break; } } + + if (brokerAddrFound != null) { + needUnRegister = setupUnRegisterRequest(unRegisterRequest, brokerAddrFound); + } } finally { this.lock.readLock().unlock(); } @@ -443,104 +866,93 @@ public void onChannelDestroy(String remoteAddr, Channel channel) { } } - if (null == brokerAddrFound) { - brokerAddrFound = remoteAddr; - } else { - log.info("the broker's channel destroyed, {}, clean it's data structure at once", brokerAddrFound); + if (needUnRegister) { + boolean result = this.submitUnRegisterBrokerRequest(unRegisterRequest); + log.info("the broker's channel destroyed, submit the unregister request at once, " + + "broker info: {}, submit result: {}", unRegisterRequest, result); } + } - if (brokerAddrFound != null && brokerAddrFound.length() > 0) { + private boolean setupUnRegisterRequest(UnRegisterBrokerRequestHeader unRegisterRequest, + BrokerAddrInfo brokerAddrInfo) { + unRegisterRequest.setClusterName(brokerAddrInfo.getClusterName()); + unRegisterRequest.setBrokerAddr(brokerAddrInfo.getBrokerAddr()); - try { - try { - this.lock.writeLock().lockInterruptibly(); - this.brokerLiveTable.remove(brokerAddrFound); - this.filterServerTable.remove(brokerAddrFound); - String brokerNameFound = null; - boolean removeBrokerName = false; - Iterator> itBrokerAddrTable = - this.brokerAddrTable.entrySet().iterator(); - while (itBrokerAddrTable.hasNext() && (null == brokerNameFound)) { - BrokerData brokerData = itBrokerAddrTable.next().getValue(); - - Iterator> it = brokerData.getBrokerAddrs().entrySet().iterator(); - while (it.hasNext()) { - Entry entry = it.next(); - Long brokerId = entry.getKey(); - String brokerAddr = entry.getValue(); - if (brokerAddr.equals(brokerAddrFound)) { - brokerNameFound = brokerData.getBrokerName(); - it.remove(); - log.info("remove brokerAddr[{}, {}] from brokerAddrTable, because channel destroyed", - brokerId, brokerAddr); - break; - } - } + for (Entry stringBrokerDataEntry : this.brokerAddrTable.entrySet()) { + BrokerData brokerData = stringBrokerDataEntry.getValue(); + if (!brokerAddrInfo.getClusterName().equals(brokerData.getCluster())) { + continue; + } - if (brokerData.getBrokerAddrs().isEmpty()) { - removeBrokerName = true; - itBrokerAddrTable.remove(); - log.info("remove brokerName[{}] from brokerAddrTable, because channel destroyed", - brokerData.getBrokerName()); - } - } + for (Entry entry : brokerData.getBrokerAddrs().entrySet()) { + Long brokerId = entry.getKey(); + String brokerAddr = entry.getValue(); + if (brokerAddr.equals(brokerAddrInfo.getBrokerAddr())) { + unRegisterRequest.setBrokerName(brokerData.getBrokerName()); + unRegisterRequest.setBrokerId(brokerId); + return true; + } + } + } - if (brokerNameFound != null && removeBrokerName) { - Iterator>> it = this.clusterAddrTable.entrySet().iterator(); - while (it.hasNext()) { - Entry> entry = it.next(); - String clusterName = entry.getKey(); - Set brokerNames = entry.getValue(); - boolean removed = brokerNames.remove(brokerNameFound); - if (removed) { - log.info("remove brokerName[{}], clusterName[{}] from clusterAddrTable, because channel destroyed", - brokerNameFound, clusterName); - - if (brokerNames.isEmpty()) { - log.info("remove the clusterName[{}] from clusterAddrTable, because channel destroyed and no broker in this cluster", - clusterName); - it.remove(); - } - - break; - } - } - } + return false; + } - if (removeBrokerName) { - Iterator>> itTopicQueueTable = - this.topicQueueTable.entrySet().iterator(); - while (itTopicQueueTable.hasNext()) { - Entry> entry = itTopicQueueTable.next(); - String topic = entry.getKey(); - List queueDataList = entry.getValue(); - - Iterator itQueueData = queueDataList.iterator(); - while (itQueueData.hasNext()) { - QueueData queueData = itQueueData.next(); - if (queueData.getBrokerName().equals(brokerNameFound)) { - itQueueData.remove(); - log.info("remove topic[{} {}], from topicQueueTable, because channel destroyed", - topic, queueData); - } - } + private void notifyMinBrokerIdChanged(Map needNotifyBrokerMap) + throws InterruptedException, RemotingConnectException, RemotingTimeoutException, RemotingSendRequestException, + RemotingTooMuchRequestException { + for (String brokerName : needNotifyBrokerMap.keySet()) { + BrokerStatusChangeInfo brokerStatusChangeInfo = needNotifyBrokerMap.get(brokerName); + BrokerData brokerData = brokerAddrTable.get(brokerName); + if (brokerData != null && brokerData.isEnableActingMaster()) { + notifyMinBrokerIdChanged(brokerStatusChangeInfo.getBrokerAddrs(), + brokerStatusChangeInfo.getOfflineBrokerAddr(), brokerStatusChangeInfo.getHaBrokerAddr()); + } + } + } - if (queueDataList.isEmpty()) { - itTopicQueueTable.remove(); - log.info("remove topic[{}] all queue, from topicQueueTable, because channel destroyed", - topic); - } - } - } - } finally { - this.lock.writeLock().unlock(); - } - } catch (Exception e) { - log.error("onChannelDestroy Exception", e); + private void notifyMinBrokerIdChanged(Map brokerAddrMap, String offlineBrokerAddr, + String haBrokerAddr) + throws InterruptedException, RemotingSendRequestException, RemotingTimeoutException, + RemotingTooMuchRequestException, RemotingConnectException { + if (brokerAddrMap == null || brokerAddrMap.isEmpty() || this.namesrvController == null) { + return; + } + + NotifyMinBrokerIdChangeRequestHeader requestHeader = new NotifyMinBrokerIdChangeRequestHeader(); + long minBrokerId = Collections.min(brokerAddrMap.keySet()); + requestHeader.setMinBrokerId(minBrokerId); + requestHeader.setMinBrokerAddr(brokerAddrMap.get(minBrokerId)); + requestHeader.setOfflineBrokerAddr(offlineBrokerAddr); + requestHeader.setHaBrokerAddr(haBrokerAddr); + + List brokerAddrsNotify = chooseBrokerAddrsToNotify(brokerAddrMap, offlineBrokerAddr); + log.info("min broker id changed to {}, notify {}, offline broker addr {}", minBrokerId, brokerAddrsNotify, offlineBrokerAddr); + RemotingCommand request = + RemotingCommand.createRequestCommand(RequestCode.NOTIFY_MIN_BROKER_ID_CHANGE, requestHeader); + for (String brokerAddr : brokerAddrsNotify) { + this.namesrvController.getRemotingClient().invokeOneway(brokerAddr, request, 300); + } + } + + private List chooseBrokerAddrsToNotify(Map brokerAddrMap, String offlineBrokerAddr) { + if (offlineBrokerAddr != null || brokerAddrMap.size() == 1) { + // notify the reset brokers. + return new ArrayList<>(brokerAddrMap.values()); + } + + // new broker registered, notify previous brokers. + long minBrokerId = Collections.min(brokerAddrMap.keySet()); + List brokerAddrList = new ArrayList<>(); + for (Long brokerId : brokerAddrMap.keySet()) { + if (brokerId != minBrokerId) { + brokerAddrList.add(brokerAddrMap.get(brokerId)); } } + return brokerAddrList; } + // For test only public void printAllPeriodically() { try { try { @@ -548,36 +960,28 @@ public void printAllPeriodically() { log.info("--------------------------------------------------------"); { log.info("topicQueueTable SIZE: {}", this.topicQueueTable.size()); - Iterator>> it = this.topicQueueTable.entrySet().iterator(); - while (it.hasNext()) { - Entry> next = it.next(); + for (Entry> next : this.topicQueueTable.entrySet()) { log.info("topicQueueTable Topic: {} {}", next.getKey(), next.getValue()); } } { log.info("brokerAddrTable SIZE: {}", this.brokerAddrTable.size()); - Iterator> it = this.brokerAddrTable.entrySet().iterator(); - while (it.hasNext()) { - Entry next = it.next(); + for (Entry next : this.brokerAddrTable.entrySet()) { log.info("brokerAddrTable brokerName: {} {}", next.getKey(), next.getValue()); } } { log.info("brokerLiveTable SIZE: {}", this.brokerLiveTable.size()); - Iterator> it = this.brokerLiveTable.entrySet().iterator(); - while (it.hasNext()) { - Entry next = it.next(); + for (Entry next : this.brokerLiveTable.entrySet()) { log.info("brokerLiveTable brokerAddr: {} {}", next.getKey(), next.getValue()); } } { log.info("clusterAddrTable SIZE: {}", this.clusterAddrTable.size()); - Iterator>> it = this.clusterAddrTable.entrySet().iterator(); - while (it.hasNext()) { - Entry> next = it.next(); + for (Entry> next : this.clusterAddrTable.entrySet()) { log.info("clusterAddrTable clusterName: {} {}", next.getKey(), next.getValue()); } } @@ -589,56 +993,48 @@ public void printAllPeriodically() { } } - public byte[] getSystemTopicList() { + public TopicList getSystemTopicList() { TopicList topicList = new TopicList(); try { - try { - this.lock.readLock().lockInterruptibly(); - for (Map.Entry> entry : clusterAddrTable.entrySet()) { - topicList.getTopicList().add(entry.getKey()); - topicList.getTopicList().addAll(entry.getValue()); - } + this.lock.readLock().lockInterruptibly(); + for (Map.Entry> entry : clusterAddrTable.entrySet()) { + topicList.getTopicList().add(entry.getKey()); + topicList.getTopicList().addAll(entry.getValue()); + } - if (brokerAddrTable != null && !brokerAddrTable.isEmpty()) { - Iterator it = brokerAddrTable.keySet().iterator(); - while (it.hasNext()) { - BrokerData bd = brokerAddrTable.get(it.next()); - HashMap brokerAddrs = bd.getBrokerAddrs(); - if (brokerAddrs != null && !brokerAddrs.isEmpty()) { - Iterator it2 = brokerAddrs.keySet().iterator(); - topicList.setBrokerAddr(brokerAddrs.get(it2.next())); - break; - } + if (!brokerAddrTable.isEmpty()) { + for (String s : brokerAddrTable.keySet()) { + BrokerData bd = brokerAddrTable.get(s); + HashMap brokerAddrs = bd.getBrokerAddrs(); + if (brokerAddrs != null && !brokerAddrs.isEmpty()) { + Iterator it2 = brokerAddrs.keySet().iterator(); + topicList.setBrokerAddr(brokerAddrs.get(it2.next())); + break; } } - } finally { - this.lock.readLock().unlock(); } } catch (Exception e) { - log.error("getAllTopicList Exception", e); + log.error("getSystemTopicList Exception", e); + } finally { + this.lock.readLock().unlock(); } - return topicList.encode(); + return topicList; } - public byte[] getTopicsByCluster(String cluster) { + public TopicList getTopicsByCluster(String cluster) { TopicList topicList = new TopicList(); try { try { this.lock.readLock().lockInterruptibly(); Set brokerNameSet = this.clusterAddrTable.get(cluster); for (String brokerName : brokerNameSet) { - Iterator>> topicTableIt = - this.topicQueueTable.entrySet().iterator(); - while (topicTableIt.hasNext()) { - Entry> topicEntry = topicTableIt.next(); + for (Entry> topicEntry : this.topicQueueTable.entrySet()) { String topic = topicEntry.getKey(); - List queueDatas = topicEntry.getValue(); - for (QueueData queueData : queueDatas) { - if (brokerName.equals(queueData.getBrokerName())) { - topicList.getTopicList().add(topic); - break; - } + Map queueDataMap = topicEntry.getValue(); + final QueueData qd = queueDataMap.get(brokerName); + if (qd != null) { + topicList.getTopicList().add(topic); } } } @@ -646,101 +1042,153 @@ public byte[] getTopicsByCluster(String cluster) { this.lock.readLock().unlock(); } } catch (Exception e) { - log.error("getAllTopicList Exception", e); + log.error("getTopicsByCluster Exception", e); } - return topicList.encode(); + return topicList; } - public byte[] getUnitTopics() { + public TopicList getUnitTopics() { TopicList topicList = new TopicList(); try { - try { - this.lock.readLock().lockInterruptibly(); - Iterator>> topicTableIt = - this.topicQueueTable.entrySet().iterator(); - while (topicTableIt.hasNext()) { - Entry> topicEntry = topicTableIt.next(); - String topic = topicEntry.getKey(); - List queueDatas = topicEntry.getValue(); - if (queueDatas != null && queueDatas.size() > 0 - && TopicSysFlag.hasUnitFlag(queueDatas.get(0).getTopicSynFlag())) { - topicList.getTopicList().add(topic); - } + this.lock.readLock().lockInterruptibly(); + for (Entry> topicEntry : this.topicQueueTable.entrySet()) { + String topic = topicEntry.getKey(); + Map queueDatas = topicEntry.getValue(); + if (queueDatas != null && queueDatas.size() > 0 + && TopicSysFlag.hasUnitFlag(queueDatas.values().iterator().next().getTopicSysFlag())) { + topicList.getTopicList().add(topic); } - } finally { - this.lock.readLock().unlock(); } } catch (Exception e) { - log.error("getAllTopicList Exception", e); + log.error("getUnitTopics Exception", e); + } finally { + this.lock.readLock().unlock(); } - return topicList.encode(); + return topicList; } - public byte[] getHasUnitSubTopicList() { + public TopicList getHasUnitSubTopicList() { TopicList topicList = new TopicList(); try { - try { - this.lock.readLock().lockInterruptibly(); - Iterator>> topicTableIt = - this.topicQueueTable.entrySet().iterator(); - while (topicTableIt.hasNext()) { - Entry> topicEntry = topicTableIt.next(); - String topic = topicEntry.getKey(); - List queueDatas = topicEntry.getValue(); - if (queueDatas != null && queueDatas.size() > 0 - && TopicSysFlag.hasUnitSubFlag(queueDatas.get(0).getTopicSynFlag())) { - topicList.getTopicList().add(topic); - } + this.lock.readLock().lockInterruptibly(); + for (Entry> topicEntry : this.topicQueueTable.entrySet()) { + String topic = topicEntry.getKey(); + Map queueDatas = topicEntry.getValue(); + if (queueDatas != null && queueDatas.size() > 0 + && TopicSysFlag.hasUnitSubFlag(queueDatas.values().iterator().next().getTopicSysFlag())) { + topicList.getTopicList().add(topic); } - } finally { - this.lock.readLock().unlock(); } } catch (Exception e) { - log.error("getAllTopicList Exception", e); + log.error("getHasUnitSubTopicList Exception", e); + } finally { + this.lock.readLock().unlock(); } - return topicList.encode(); + return topicList; } - public byte[] getHasUnitSubUnUnitTopicList() { + public TopicList getHasUnitSubUnUnitTopicList() { TopicList topicList = new TopicList(); try { - try { - this.lock.readLock().lockInterruptibly(); - Iterator>> topicTableIt = - this.topicQueueTable.entrySet().iterator(); - while (topicTableIt.hasNext()) { - Entry> topicEntry = topicTableIt.next(); - String topic = topicEntry.getKey(); - List queueDatas = topicEntry.getValue(); - if (queueDatas != null && queueDatas.size() > 0 - && !TopicSysFlag.hasUnitFlag(queueDatas.get(0).getTopicSynFlag()) - && TopicSysFlag.hasUnitSubFlag(queueDatas.get(0).getTopicSynFlag())) { - topicList.getTopicList().add(topic); - } + this.lock.readLock().lockInterruptibly(); + for (Entry> topicEntry : this.topicQueueTable.entrySet()) { + String topic = topicEntry.getKey(); + Map queueDatas = topicEntry.getValue(); + if (queueDatas != null && queueDatas.size() > 0 + && !TopicSysFlag.hasUnitFlag(queueDatas.values().iterator().next().getTopicSysFlag()) + && TopicSysFlag.hasUnitSubFlag(queueDatas.values().iterator().next().getTopicSysFlag())) { + topicList.getTopicList().add(topic); } - } finally { - this.lock.readLock().unlock(); } } catch (Exception e) { - log.error("getAllTopicList Exception", e); + log.error("getHasUnitSubUnUnitTopicList Exception", e); + } finally { + this.lock.readLock().unlock(); + } + + return topicList; + } +} + +/** + * broker address information + */ +class BrokerAddrInfo { + private String clusterName; + private String brokerAddr; + + private int hash; + + public BrokerAddrInfo(String clusterName, String brokerAddr) { + this.clusterName = clusterName; + this.brokerAddr = brokerAddr; + } + + public String getClusterName() { + return clusterName; + } + + public String getBrokerAddr() { + return brokerAddr; + } + + public boolean isEmpty() { + return clusterName.isEmpty() && brokerAddr.isEmpty(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; } - return topicList.encode(); + if (obj instanceof BrokerAddrInfo) { + BrokerAddrInfo addr = (BrokerAddrInfo) obj; + return clusterName.equals(addr.clusterName) && brokerAddr.equals(addr.brokerAddr); + } + return false; + } + + @Override + public int hashCode() { + int h = hash; + if (h == 0 && clusterName.length() + brokerAddr.length() > 0) { + for (int i = 0; i < clusterName.length(); i++) { + h = 31 * h + clusterName.charAt(i); + } + h = 31 * h + '_'; + for (int i = 0; i < brokerAddr.length(); i++) { + h = 31 * h + brokerAddr.charAt(i); + } + hash = h; + } + return h; + } + + @Override + public String toString() { + return "BrokerIdentityInfo [clusterName=" + clusterName + ", brokerAddr=" + brokerAddr + "]"; } } class BrokerLiveInfo { private long lastUpdateTimestamp; + private long heartbeatTimeoutMillis; private DataVersion dataVersion; private Channel channel; private String haServerAddr; - public BrokerLiveInfo(long lastUpdateTimestamp, DataVersion dataVersion, Channel channel, + public BrokerLiveInfo(long lastUpdateTimestamp, long heartbeatTimeoutMillis, DataVersion dataVersion, + Channel channel, String haServerAddr) { this.lastUpdateTimestamp = lastUpdateTimestamp; + this.heartbeatTimeoutMillis = heartbeatTimeoutMillis; this.dataVersion = dataVersion; this.channel = channel; this.haServerAddr = haServerAddr; @@ -754,6 +1202,14 @@ public void setLastUpdateTimestamp(long lastUpdateTimestamp) { this.lastUpdateTimestamp = lastUpdateTimestamp; } + public long getHeartbeatTimeoutMillis() { + return heartbeatTimeoutMillis; + } + + public void setHeartbeatTimeoutMillis(long heartbeatTimeoutMillis) { + this.heartbeatTimeoutMillis = heartbeatTimeoutMillis; + } + public DataVersion getDataVersion() { return dataVersion; } @@ -784,3 +1240,39 @@ public String toString() { + ", channel=" + channel + ", haServerAddr=" + haServerAddr + "]"; } } + +class BrokerStatusChangeInfo { + Map brokerAddrs; + String offlineBrokerAddr; + String haBrokerAddr; + + public BrokerStatusChangeInfo(Map brokerAddrs, String offlineBrokerAddr, String haBrokerAddr) { + this.brokerAddrs = brokerAddrs; + this.offlineBrokerAddr = offlineBrokerAddr; + this.haBrokerAddr = haBrokerAddr; + } + + public Map getBrokerAddrs() { + return brokerAddrs; + } + + public void setBrokerAddrs(Map brokerAddrs) { + this.brokerAddrs = brokerAddrs; + } + + public String getOfflineBrokerAddr() { + return offlineBrokerAddr; + } + + public void setOfflineBrokerAddr(String offlineBrokerAddr) { + this.offlineBrokerAddr = offlineBrokerAddr; + } + + public String getHaBrokerAddr() { + return haBrokerAddr; + } + + public void setHaBrokerAddr(String haBrokerAddr) { + this.haBrokerAddr = haBrokerAddr; + } +} diff --git a/namesrv/src/main/resources/rmq.namesrv.logback.xml b/namesrv/src/main/resources/rmq.namesrv.logback.xml new file mode 100644 index 00000000000..2a3c95722a5 --- /dev/null +++ b/namesrv/src/main/resources/rmq.namesrv.logback.xml @@ -0,0 +1,117 @@ + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}namesrv_default.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}namesrv_default.%i.log.gz + 1 + 5 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}namesrv.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}namesrv.%i.log.gz + 1 + 5 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + 0 + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}namesrv_traffic.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}namesrv_traffic.%i.log.gz + 1 + 10 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/namesrv/src/test/java/org/apache/rocketmq/namesrv/NameServerInstanceTest.java b/namesrv/src/test/java/org/apache/rocketmq/namesrv/NameServerInstanceTest.java index 83ab103f66d..7768272f312 100644 --- a/namesrv/src/test/java/org/apache/rocketmq/namesrv/NameServerInstanceTest.java +++ b/namesrv/src/test/java/org/apache/rocketmq/namesrv/NameServerInstanceTest.java @@ -20,6 +20,7 @@ import org.apache.rocketmq.remoting.netty.NettyServerConfig; import org.junit.After; import org.junit.Before; +import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -37,6 +38,14 @@ public void startup() throws Exception { nameSrvController.start(); } + /** + * Add a placeholder to make Bazel happy. + */ + @Test + public void itWorks() { + + } + @After public void shutdown() throws Exception { if (nameSrvController != null) { diff --git a/namesrv/src/test/java/org/apache/rocketmq/namesrv/NamesrvControllerTest.java b/namesrv/src/test/java/org/apache/rocketmq/namesrv/NamesrvControllerTest.java new file mode 100644 index 00000000000..49d7103aa3d --- /dev/null +++ b/namesrv/src/test/java/org/apache/rocketmq/namesrv/NamesrvControllerTest.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.namesrv; + +import org.apache.rocketmq.common.namesrv.NamesrvConfig; +import org.apache.rocketmq.namesrv.kvconfig.KVConfigManager; +import org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager; +import org.apache.rocketmq.remoting.Configuration; +import org.apache.rocketmq.remoting.RemotingServer; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class NamesrvControllerTest { + + @Mock + private NettyServerConfig nettyServerConfig; + @Mock + private RemotingServer remotingServer; + + private NamesrvController namesrvController; + + @Before + public void setUp() throws Exception { + NamesrvConfig namesrvConfig = new NamesrvConfig(); + namesrvController = new NamesrvController(namesrvConfig, nettyServerConfig); + } + + @Test + public void getNamesrvConfig() { + NamesrvConfig namesrvConfig = namesrvController.getNamesrvConfig(); + Assert.assertNotNull(namesrvConfig); + } + + @Test + public void getNettyServerConfig() { + NettyServerConfig nettyServerConfig = namesrvController.getNettyServerConfig(); + Assert.assertNotNull(nettyServerConfig); + } + + @Test + public void getKvConfigManager() { + KVConfigManager manager = namesrvController.getKvConfigManager(); + Assert.assertNotNull(manager); + } + + @Test + public void getRouteInfoManager() { + RouteInfoManager manager = namesrvController.getRouteInfoManager(); + Assert.assertNotNull(manager); + } + + @Test + public void getRemotingServer() { + RemotingServer server = namesrvController.getRemotingServer(); + Assert.assertNull(server); + } + + @Test + public void setRemotingServer() { + namesrvController.setRemotingServer(remotingServer); + RemotingServer server = namesrvController.getRemotingServer(); + Assert.assertEquals(remotingServer, server); + } + + @Test + public void getConfiguration() { + Configuration configuration = namesrvController.getConfiguration(); + Assert.assertNotNull(configuration); + } +} diff --git a/namesrv/src/test/java/org/apache/rocketmq/namesrv/NamesrvStartupTest.java b/namesrv/src/test/java/org/apache/rocketmq/namesrv/NamesrvStartupTest.java new file mode 100644 index 00000000000..957406dac06 --- /dev/null +++ b/namesrv/src/test/java/org/apache/rocketmq/namesrv/NamesrvStartupTest.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.namesrv; + +import java.util.Properties; +import org.apache.commons.cli.Options; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class NamesrvStartupTest { + + @Mock + private NamesrvController namesrvController; + @Mock + private Options options; + + @Before + public void setUp() throws Exception { + Mockito.when(namesrvController.initialize()).thenReturn(true); + } + + @Test + public void testStart() throws Exception { + NamesrvController controller = NamesrvStartup.start(namesrvController); + Assert.assertNotNull(controller); + } + + @Test + public void testShutdown() { + NamesrvStartup.shutdown(namesrvController); + Mockito.verify(namesrvController).shutdown(); + } + + @Test + public void testBuildCommandlineOptions() { + Options options = NamesrvStartup.buildCommandlineOptions(this.options); + Assert.assertNotNull(options); + } + + @Test + public void testGetProperties() { + Properties properties = NamesrvStartup.getProperties(); + Assert.assertNull(properties); + } +} \ No newline at end of file diff --git a/namesrv/src/test/java/org/apache/rocketmq/namesrv/processor/ClusterTestRequestProcessorTest.java b/namesrv/src/test/java/org/apache/rocketmq/namesrv/processor/ClusterTestRequestProcessorTest.java index a0e8137524a..283f9033021 100644 --- a/namesrv/src/test/java/org/apache/rocketmq/namesrv/processor/ClusterTestRequestProcessorTest.java +++ b/namesrv/src/test/java/org/apache/rocketmq/namesrv/processor/ClusterTestRequestProcessorTest.java @@ -22,21 +22,25 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.concurrent.TimeUnit; import org.apache.rocketmq.client.ClientConfig; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.impl.MQClientAPIImpl; import org.apache.rocketmq.client.impl.MQClientManager; import org.apache.rocketmq.client.impl.factory.MQClientInstance; import org.apache.rocketmq.common.namesrv.NamesrvConfig; -import org.apache.rocketmq.common.protocol.ResponseCode; -import org.apache.rocketmq.common.protocol.route.BrokerData; -import org.apache.rocketmq.common.protocol.route.TopicRouteData; import org.apache.rocketmq.namesrv.NamesrvController; +import org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.exception.RemotingCommandException; import org.apache.rocketmq.remoting.exception.RemotingException; import org.apache.rocketmq.remoting.netty.NettyServerConfig; import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.namesrv.GetRouteInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; import org.apache.rocketmq.tools.admin.DefaultMQAdminExtImpl; import org.junit.After; @@ -44,6 +48,7 @@ import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; @@ -52,16 +57,17 @@ public class ClusterTestRequestProcessorTest { private ClusterTestRequestProcessor clusterTestProcessor; private DefaultMQAdminExtImpl defaultMQAdminExtImpl; - private MQClientInstance mqClientInstance = MQClientManager.getInstance().getAndCreateMQClientInstance(new ClientConfig()); + private MQClientInstance mqClientInstance = MQClientManager.getInstance() + .getOrCreateMQClientInstance(new ClientConfig()); private MQClientAPIImpl mQClientAPIImpl; private ChannelHandlerContext ctx; @Before - public void init() throws NoSuchFieldException, IllegalAccessException, RemotingException, MQClientException, InterruptedException { + public void init() throws NoSuchFieldException, IllegalAccessException, RemotingException, MQClientException, + InterruptedException { NamesrvController namesrvController = new NamesrvController( - new NamesrvConfig(), - new NettyServerConfig() - ); + new NamesrvConfig(), + new NettyServerConfig()); clusterTestProcessor = new ClusterTestRequestProcessor(namesrvController, "default-producer"); mQClientAPIImpl = mock(MQClientAPIImpl.class); @@ -82,7 +88,7 @@ public void init() throws NoSuchFieldException, IllegalAccessException, Remoting TopicRouteData topicRouteData = new TopicRouteData(); List brokerDatas = new ArrayList<>(); HashMap brokerAddrs = new HashMap<>(); - brokerAddrs.put(1234l, "127.0.0.1:10911"); + brokerAddrs.put(1234L, "127.0.0.1:10911"); BrokerData brokerData = new BrokerData(); brokerData.setCluster("default-cluster"); brokerData.setBrokerName("default-broker"); @@ -110,4 +116,94 @@ public void checkFields() throws RemotingCommandException { assertThat(remoting.getRemark()).isNotNull(); } -} \ No newline at end of file + @Test + public void testNamesrvReady() throws Exception { + String topicName = "rocketmq-topic-test-ready"; + RouteInfoManager routeInfoManager = mockRouteInfoManager(); + NamesrvController namesrvController = mockNamesrvController(routeInfoManager, true, -1,true); + ClientRequestProcessor clientRequestProcessor = new ClientRequestProcessor(namesrvController); + GetRouteInfoRequestHeader routeInfoRequestHeader = mockRouteInfoRequestHeader(topicName); + RemotingCommand remotingCommand = mockTopicRouteCommand(routeInfoRequestHeader); + RemotingCommand response = clientRequestProcessor.processRequest(mock(ChannelHandlerContext.class), + remotingCommand); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testNamesrvNoNeedWaitForService() throws Exception { + String topicName = "rocketmq-topic-test-ready"; + RouteInfoManager routeInfoManager = mockRouteInfoManager(); + NamesrvController namesrvController = mockNamesrvController(routeInfoManager, true, 45,false); + ClientRequestProcessor clientRequestProcessor = new ClientRequestProcessor(namesrvController); + GetRouteInfoRequestHeader routeInfoRequestHeader = mockRouteInfoRequestHeader(topicName); + RemotingCommand remotingCommand = mockTopicRouteCommand(routeInfoRequestHeader); + RemotingCommand response = clientRequestProcessor.processRequest(mock(ChannelHandlerContext.class), + remotingCommand); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testNamesrvNotReady() throws Exception { + String topicName = "rocketmq-topic-test"; + RouteInfoManager routeInfoManager = mockRouteInfoManager(); + NamesrvController namesrvController = mockNamesrvController(routeInfoManager, false, 45,true); + GetRouteInfoRequestHeader routeInfoRequestHeader = mockRouteInfoRequestHeader(topicName); + RemotingCommand remotingCommand = mockTopicRouteCommand(routeInfoRequestHeader); + ClientRequestProcessor clientRequestProcessor = new ClientRequestProcessor(namesrvController); + RemotingCommand response = clientRequestProcessor.processRequest(mock(ChannelHandlerContext.class), + remotingCommand); + assertThat(response.getCode()).isEqualTo(ResponseCode.SYSTEM_ERROR); + } + + @Test + public void testNamesrv() throws Exception { + int waitSecondsForService = 3; + String topicName = "rocketmq-topic-test"; + RouteInfoManager routeInfoManager = mockRouteInfoManager(); + NamesrvController namesrvController = mockNamesrvController(routeInfoManager, false, waitSecondsForService,true); + GetRouteInfoRequestHeader routeInfoRequestHeader = mockRouteInfoRequestHeader(topicName); + RemotingCommand remotingCommand = mockTopicRouteCommand(routeInfoRequestHeader); + ClientRequestProcessor clientRequestProcessor = new ClientRequestProcessor(namesrvController); + RemotingCommand response = clientRequestProcessor.processRequest(mock(ChannelHandlerContext.class), + remotingCommand); + assertThat(response.getCode()).isEqualTo(ResponseCode.SYSTEM_ERROR); + TimeUnit.SECONDS.sleep(waitSecondsForService + 1); + response = clientRequestProcessor.processRequest(mock(ChannelHandlerContext.class), remotingCommand); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + private RemotingCommand mockTopicRouteCommand( + GetRouteInfoRequestHeader routeInfoRequestHeader) throws RemotingCommandException { + RemotingCommand remotingCommand = mock(RemotingCommand.class); + when(remotingCommand.decodeCommandCustomHeader(any())).thenReturn(routeInfoRequestHeader); + when(remotingCommand.getCode()).thenReturn(RequestCode.GET_ROUTEINFO_BY_TOPIC); + return remotingCommand; + } + + public NamesrvController mockNamesrvController(RouteInfoManager routeInfoManager, boolean ready, + int waitSecondsForService,boolean needWaitForService) { + NamesrvConfig namesrvConfig = mock(NamesrvConfig.class); + when(namesrvConfig.isNeedWaitForService()).thenReturn(needWaitForService); + when(namesrvConfig.getUnRegisterBrokerQueueCapacity()).thenReturn(10); + when(namesrvConfig.getWaitSecondsForService()).thenReturn(ready ? 0 : waitSecondsForService); + NamesrvController namesrvController = mock(NamesrvController.class); + when(namesrvController.getNamesrvConfig()).thenReturn(namesrvConfig); + when(namesrvController.getRouteInfoManager()).thenReturn(routeInfoManager); + + return namesrvController; + } + + public RouteInfoManager mockRouteInfoManager() { + RouteInfoManager routeInfoManager = mock(RouteInfoManager.class); + TopicRouteData topicRouteData = mock(TopicRouteData.class); + when(routeInfoManager.pickupTopicRouteData(any())).thenReturn(topicRouteData); + return routeInfoManager; + } + + public GetRouteInfoRequestHeader mockRouteInfoRequestHeader(String topicName) { + GetRouteInfoRequestHeader routeInfoRequestHeader = mock(GetRouteInfoRequestHeader.class); + when(routeInfoRequestHeader.getTopic()).thenReturn(topicName); + return routeInfoRequestHeader; + } + +} diff --git a/namesrv/src/test/java/org/apache/rocketmq/namesrv/processor/DefaultRequestProcessorTest.java b/namesrv/src/test/java/org/apache/rocketmq/namesrv/processor/DefaultRequestProcessorTest.java deleted file mode 100644 index 97aa9ac016c..00000000000 --- a/namesrv/src/test/java/org/apache/rocketmq/namesrv/processor/DefaultRequestProcessorTest.java +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.namesrv.processor; - -import io.netty.channel.Channel; -import io.netty.channel.ChannelHandlerContext; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import org.apache.rocketmq.common.TopicConfig; -import org.apache.rocketmq.common.namesrv.NamesrvConfig; -import org.apache.rocketmq.common.namesrv.RegisterBrokerResult; -import org.apache.rocketmq.common.protocol.RequestCode; -import org.apache.rocketmq.common.protocol.ResponseCode; -import org.apache.rocketmq.common.protocol.body.TopicConfigSerializeWrapper; -import org.apache.rocketmq.common.protocol.header.namesrv.DeleteKVConfigRequestHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.GetKVConfigRequestHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.GetKVConfigResponseHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.PutKVConfigRequestHeader; -import org.apache.rocketmq.common.protocol.header.namesrv.RegisterBrokerRequestHeader; -import org.apache.rocketmq.common.protocol.route.BrokerData; -import org.apache.rocketmq.namesrv.NamesrvController; -import org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager; -import org.apache.rocketmq.remoting.exception.RemotingCommandException; -import org.apache.rocketmq.remoting.netty.NettyServerConfig; -import org.apache.rocketmq.remoting.protocol.RemotingCommand; -import org.assertj.core.util.Maps; -import org.junit.Before; -import org.junit.Test; -import org.slf4j.Logger; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class DefaultRequestProcessorTest { - private DefaultRequestProcessor defaultRequestProcessor; - - private NamesrvController namesrvController; - - private NamesrvConfig namesrvConfig; - - private NettyServerConfig nettyServerConfig; - - private RouteInfoManager routeInfoManager; - - private Logger logger; - - @Before - public void init() throws Exception { - namesrvConfig = new NamesrvConfig(); - nettyServerConfig = new NettyServerConfig(); - routeInfoManager = new RouteInfoManager(); - - namesrvController = new NamesrvController(namesrvConfig, nettyServerConfig); - - Field field = NamesrvController.class.getDeclaredField("routeInfoManager"); - field.setAccessible(true); - field.set(namesrvController, routeInfoManager); - defaultRequestProcessor = new DefaultRequestProcessor(namesrvController); - - registerRouteInfoManager(); - - logger = mock(Logger.class); - when(logger.isInfoEnabled()).thenReturn(false); - setFinalStatic(DefaultRequestProcessor.class.getDeclaredField("log"), logger); - } - - @Test - public void testProcessRequest_PutKVConfig() throws RemotingCommandException { - PutKVConfigRequestHeader header = new PutKVConfigRequestHeader(); - RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PUT_KV_CONFIG, - header); - request.addExtField("namespace", "namespace"); - request.addExtField("key", "key"); - request.addExtField("value", "value"); - - RemotingCommand response = defaultRequestProcessor.processRequest(null, request); - - assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); - assertThat(response.getRemark()).isNull(); - - assertThat(namesrvController.getKvConfigManager().getKVConfig("namespace", "key")) - .isEqualTo("value"); - } - - @Test - public void testProcessRequest_GetKVConfigReturnNotNull() throws RemotingCommandException { - namesrvController.getKvConfigManager().putKVConfig("namespace", "key", "value"); - - GetKVConfigRequestHeader header = new GetKVConfigRequestHeader(); - RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_KV_CONFIG, - header); - request.addExtField("namespace", "namespace"); - request.addExtField("key", "key"); - - RemotingCommand response = defaultRequestProcessor.processRequest(null, request); - - assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); - assertThat(response.getRemark()).isNull(); - - GetKVConfigResponseHeader responseHeader = (GetKVConfigResponseHeader) response - .readCustomHeader(); - - assertThat(responseHeader.getValue()).isEqualTo("value"); - } - - @Test - public void testProcessRequest_GetKVConfigReturnNull() throws RemotingCommandException { - GetKVConfigRequestHeader header = new GetKVConfigRequestHeader(); - RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_KV_CONFIG, - header); - request.addExtField("namespace", "namespace"); - request.addExtField("key", "key"); - - RemotingCommand response = defaultRequestProcessor.processRequest(null, request); - - assertThat(response.getCode()).isEqualTo(ResponseCode.QUERY_NOT_FOUND); - assertThat(response.getRemark()).isEqualTo("No config item, Namespace: namespace Key: key"); - - GetKVConfigResponseHeader responseHeader = (GetKVConfigResponseHeader) response - .readCustomHeader(); - - assertThat(responseHeader.getValue()).isNull(); - } - - @Test - public void testProcessRequest_DeleteKVConfig() throws RemotingCommandException { - namesrvController.getKvConfigManager().putKVConfig("namespace", "key", "value"); - - DeleteKVConfigRequestHeader header = new DeleteKVConfigRequestHeader(); - RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.DELETE_KV_CONFIG, - header); - request.addExtField("namespace", "namespace"); - request.addExtField("key", "key"); - - RemotingCommand response = defaultRequestProcessor.processRequest(null, request); - - assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); - assertThat(response.getRemark()).isNull(); - - assertThat(namesrvController.getKvConfigManager().getKVConfig("namespace", "key")) - .isNull(); - } - - @Test - public void testProcessRequest_RegisterBroker() throws RemotingCommandException, - NoSuchFieldException, IllegalAccessException { - RemotingCommand request = genSampleRegisterCmd(true); - - ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); - when(ctx.channel()).thenReturn(null); - - RemotingCommand response = defaultRequestProcessor.processRequest(ctx, request); - - assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); - assertThat(response.getRemark()).isNull(); - - RouteInfoManager routes = namesrvController.getRouteInfoManager(); - Field brokerAddrTable = RouteInfoManager.class.getDeclaredField("brokerAddrTable"); - brokerAddrTable.setAccessible(true); - - BrokerData broker = new BrokerData(); - broker.setBrokerName("broker"); - broker.setBrokerAddrs((HashMap) Maps.newHashMap(new Long(2333), "10.10.1.1")); - - assertThat((Map) brokerAddrTable.get(routes)) - .contains(new HashMap.SimpleEntry("broker", broker)); - } - - @Test - public void testProcessRequest_RegisterBrokerWithFilterServer() throws RemotingCommandException, - NoSuchFieldException, IllegalAccessException { - RemotingCommand request = genSampleRegisterCmd(true); - - // version >= MQVersion.Version.V3_0_11.ordinal() to register with filter server - request.setVersion(100); - - ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); - when(ctx.channel()).thenReturn(null); - - RemotingCommand response = defaultRequestProcessor.processRequest(ctx, request); - - assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); - assertThat(response.getRemark()).isNull(); - - RouteInfoManager routes = namesrvController.getRouteInfoManager(); - Field brokerAddrTable = RouteInfoManager.class.getDeclaredField("brokerAddrTable"); - brokerAddrTable.setAccessible(true); - - BrokerData broker = new BrokerData(); - broker.setBrokerName("broker"); - broker.setBrokerAddrs((HashMap) Maps.newHashMap(new Long(2333), "10.10.1.1")); - - assertThat((Map) brokerAddrTable.get(routes)) - .contains(new HashMap.SimpleEntry("broker", broker)); - } - - @Test - public void testProcessRequest_UnregisterBroker() throws RemotingCommandException, NoSuchFieldException, IllegalAccessException { - ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); - when(ctx.channel()).thenReturn(null); - - //Register broker - RemotingCommand regRequest = genSampleRegisterCmd(true); - defaultRequestProcessor.processRequest(ctx, regRequest); - - //Unregister broker - RemotingCommand unregRequest = genSampleRegisterCmd(false); - RemotingCommand unregResponse = defaultRequestProcessor.processRequest(ctx, unregRequest); - - assertThat(unregResponse.getCode()).isEqualTo(ResponseCode.SUCCESS); - assertThat(unregResponse.getRemark()).isNull(); - - RouteInfoManager routes = namesrvController.getRouteInfoManager(); - Field brokerAddrTable = RouteInfoManager.class.getDeclaredField("brokerAddrTable"); - brokerAddrTable.setAccessible(true); - - assertThat((Map) brokerAddrTable.get(routes)).isNotEmpty(); - } - - private static RemotingCommand genSampleRegisterCmd(boolean reg) { - RegisterBrokerRequestHeader header = new RegisterBrokerRequestHeader(); - header.setBrokerName("broker"); - RemotingCommand request = RemotingCommand.createRequestCommand( - reg ? RequestCode.REGISTER_BROKER : RequestCode.UNREGISTER_BROKER, header); - request.addExtField("brokerName", "broker"); - request.addExtField("brokerAddr", "10.10.1.1"); - request.addExtField("clusterName", "cluster"); - request.addExtField("haServerAddr", "10.10.2.1"); - request.addExtField("brokerId", "2333"); - return request; - } - - private static void setFinalStatic(Field field, Object newValue) throws Exception { - field.setAccessible(true); - Field modifiersField = Field.class.getDeclaredField("modifiers"); - modifiersField.setAccessible(true); - modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); - field.set(null, newValue); - } - - private void registerRouteInfoManager() { - TopicConfigSerializeWrapper topicConfigSerializeWrapper = new TopicConfigSerializeWrapper(); - ConcurrentHashMap topicConfigConcurrentHashMap = new ConcurrentHashMap<>(); - TopicConfig topicConfig = new TopicConfig(); - topicConfig.setWriteQueueNums(8); - topicConfig.setTopicName("unit-test"); - topicConfig.setPerm(6); - topicConfig.setReadQueueNums(8); - topicConfig.setOrder(false); - topicConfigConcurrentHashMap.put("unit-test", topicConfig); - topicConfigSerializeWrapper.setTopicConfigTable(topicConfigConcurrentHashMap); - Channel channel = mock(Channel.class); - RegisterBrokerResult registerBrokerResult = routeInfoManager.registerBroker("default-cluster", "127.0.0.1:10911", "default-broker", 1234, "127.0.0.1:1001", - topicConfigSerializeWrapper, new ArrayList(), channel); - - } -} \ No newline at end of file diff --git a/namesrv/src/test/java/org/apache/rocketmq/namesrv/processor/RequestProcessorTest.java b/namesrv/src/test/java/org/apache/rocketmq/namesrv/processor/RequestProcessorTest.java new file mode 100644 index 00000000000..2b2cf629494 --- /dev/null +++ b/namesrv/src/test/java/org/apache/rocketmq/namesrv/processor/RequestProcessorTest.java @@ -0,0 +1,630 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.namesrv.processor; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.namesrv.NamesrvConfig; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.namesrv.NamesrvController; +import org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.RegisterBrokerBody; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigAndMappingSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.header.namesrv.DeleteKVConfigRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.GetKVConfigRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.GetKVConfigResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.PutKVConfigRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.namesrv.RegisterBrokerRequestHeader; +import org.apache.rocketmq.remoting.protocol.namesrv.RegisterBrokerResult; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.assertj.core.util.Maps; +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class RequestProcessorTest { + private DefaultRequestProcessor defaultRequestProcessor; + + private ClientRequestProcessor clientRequestProcessor; + + private NamesrvController namesrvController; + + private NamesrvConfig namesrvConfig; + + private NettyServerConfig nettyServerConfig; + + private RouteInfoManager routeInfoManager; + + private Logger logger; + + @Before + public void init() throws Exception { + namesrvConfig = new NamesrvConfig(); + namesrvConfig.setEnableAllTopicList(true); + nettyServerConfig = new NettyServerConfig(); + routeInfoManager = new RouteInfoManager(new NamesrvConfig(), null); + + namesrvController = new NamesrvController(namesrvConfig, nettyServerConfig); + + Field field = NamesrvController.class.getDeclaredField("routeInfoManager"); + field.setAccessible(true); + field.set(namesrvController, routeInfoManager); + defaultRequestProcessor = new DefaultRequestProcessor(namesrvController); + + clientRequestProcessor = new ClientRequestProcessor(namesrvController); + + registerRouteInfoManager(); + + logger = mock(Logger.class); + setFinalStatic(DefaultRequestProcessor.class.getDeclaredField("log"), logger); + } + + @Test + public void testProcessRequest_PutKVConfig() throws RemotingCommandException { + PutKVConfigRequestHeader header = new PutKVConfigRequestHeader(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PUT_KV_CONFIG, + header); + request.addExtField("namespace", "namespace"); + request.addExtField("key", "key"); + request.addExtField("value", "value"); + + RemotingCommand response = defaultRequestProcessor.processRequest(null, request); + + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(response.getRemark()).isNull(); + + assertThat(namesrvController.getKvConfigManager().getKVConfig("namespace", "key")) + .isEqualTo("value"); + } + + @Test + public void testProcessRequest_GetKVConfigReturnNotNull() throws RemotingCommandException { + namesrvController.getKvConfigManager().putKVConfig("namespace", "key", "value"); + + GetKVConfigRequestHeader header = new GetKVConfigRequestHeader(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_KV_CONFIG, + header); + request.addExtField("namespace", "namespace"); + request.addExtField("key", "key"); + + RemotingCommand response = defaultRequestProcessor.processRequest(null, request); + + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(response.getRemark()).isNull(); + + GetKVConfigResponseHeader responseHeader = (GetKVConfigResponseHeader) response + .readCustomHeader(); + + assertThat(responseHeader.getValue()).isEqualTo("value"); + } + + @Test + public void testProcessRequest_GetKVConfigReturnNull() throws RemotingCommandException { + GetKVConfigRequestHeader header = new GetKVConfigRequestHeader(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_KV_CONFIG, + header); + request.addExtField("namespace", "namespace"); + request.addExtField("key", "key"); + + RemotingCommand response = defaultRequestProcessor.processRequest(null, request); + + assertThat(response.getCode()).isEqualTo(ResponseCode.QUERY_NOT_FOUND); + assertThat(response.getRemark()).isEqualTo("No config item, Namespace: namespace Key: key"); + + GetKVConfigResponseHeader responseHeader = (GetKVConfigResponseHeader) response + .readCustomHeader(); + + assertThat(responseHeader.getValue()).isNull(); + } + + @Test + public void testProcessRequest_DeleteKVConfig() throws RemotingCommandException { + namesrvController.getKvConfigManager().putKVConfig("namespace", "key", "value"); + + DeleteKVConfigRequestHeader header = new DeleteKVConfigRequestHeader(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.DELETE_KV_CONFIG, + header); + request.addExtField("namespace", "namespace"); + request.addExtField("key", "key"); + + RemotingCommand response = defaultRequestProcessor.processRequest(null, request); + + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(response.getRemark()).isNull(); + + assertThat(namesrvController.getKvConfigManager().getKVConfig("namespace", "key")) + .isNull(); + } + + @Test + public void testProcessRequest_UnSupportedRequest() throws RemotingCommandException { + final RemotingCommand unSupportedRequest = RemotingCommand.createRequestCommand(99999, null); + final RemotingCommand response = defaultRequestProcessor.processRequest(null, unSupportedRequest); + + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.REQUEST_CODE_NOT_SUPPORTED); + } + + @Test + public void testProcessRequest_UpdateConfigPath() throws RemotingCommandException { + final RemotingCommand updateConfigRequest = RemotingCommand.createRequestCommand(RequestCode.UPDATE_NAMESRV_CONFIG, null); + Properties properties = new Properties(); + + // Update allowed value + properties.setProperty("enableTopicList", "true"); + updateConfigRequest.setBody(MixAll.properties2String(properties).getBytes(StandardCharsets.UTF_8)); + + RemotingCommand response = defaultRequestProcessor.processRequest(null, updateConfigRequest); + + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + //update disallowed value + properties.clear(); + properties.setProperty("configStorePath", "test/path"); + updateConfigRequest.setBody(MixAll.properties2String(properties).getBytes(StandardCharsets.UTF_8)); + + response = defaultRequestProcessor.processRequest(null, updateConfigRequest); + + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.NO_PERMISSION); + assertThat(response.getRemark()).contains("Can not update config in black list."); + + //update disallowed values + properties.clear(); + properties.setProperty("kvConfigPath", "test/path"); + updateConfigRequest.setBody(MixAll.properties2String(properties).getBytes(StandardCharsets.UTF_8)); + + response = defaultRequestProcessor.processRequest(null, updateConfigRequest); + + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.NO_PERMISSION); + assertThat(response.getRemark()).contains("Can not update config in black list"); + + //update disallowed values + properties.clear(); + properties.setProperty("configBlackList", "test;path"); + updateConfigRequest.setBody(MixAll.properties2String(properties).getBytes(StandardCharsets.UTF_8)); + + response = defaultRequestProcessor.processRequest(null, updateConfigRequest); + + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(ResponseCode.NO_PERMISSION); + assertThat(response.getRemark()).contains("Can not update config in black list"); + } + + @Test + public void testProcessRequest_RegisterBroker() throws RemotingCommandException, + NoSuchFieldException, IllegalAccessException { + RemotingCommand request = genSampleRegisterCmd(true); + + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + when(ctx.channel()).thenReturn(null); + + RemotingCommand response = defaultRequestProcessor.processRequest(ctx, request); + + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(response.getRemark()).isNull(); + + RouteInfoManager routes = namesrvController.getRouteInfoManager(); + Field brokerAddrTable = RouteInfoManager.class.getDeclaredField("brokerAddrTable"); + brokerAddrTable.setAccessible(true); + + BrokerData broker = new BrokerData(); + broker.setBrokerName("broker"); + broker.setBrokerAddrs((HashMap) Maps.newHashMap(new Long(2333), "10.10.1.1")); + + assertThat((Map) brokerAddrTable.get(routes)) + .contains(new HashMap.SimpleEntry("broker", broker)); + } + + /*@Test + public void testProcessRequest_RegisterBrokerLogicalQueue() throws Exception { + String cluster = "cluster"; + String broker1Name = "broker1"; + String broker1Addr = "10.10.1.1"; + String broker2Name = "broker2"; + String broker2Addr = "10.10.1.2"; + String topic = "foobar"; + + LogicalQueueRouteData queueRouteData1 = new LogicalQueueRouteData(0, 0, new MessageQueue(topic, broker1Name, 0), MessageQueueRouteState.ReadOnly, 0, 10, 100, 100, broker1Addr); + { + RegisterBrokerRequestHeader header = new RegisterBrokerRequestHeader(); + header.setBrokerName(broker1Name); + RemotingCommand request = RemotingCommand.createRequestCommand( + RequestCode.REGISTER_BROKER, header); + request.addExtField("brokerName", broker1Name); + request.addExtField("brokerAddr", broker1Addr); + request.addExtField("clusterName", cluster); + request.addExtField("haServerAddr", "10.10.2.1"); + request.addExtField("brokerId", String.valueOf(MixAll.MASTER_ID)); + request.setVersion(MQVersion.CURRENT_VERSION); + TopicConfigSerializeWrapper topicConfigSerializeWrapper = new TopicConfigSerializeWrapper(); + topicConfigSerializeWrapper.setTopicConfigTable(new ConcurrentHashMap<>(Collections.singletonMap(topic, new TopicConfig(topic)))); + topicConfigSerializeWrapper.setLogicalQueuesInfoMap(Maps.newHashMap(topic, new LogicalQueuesInfo(Collections.singletonMap(0, Lists.newArrayList( + queueRouteData1 + ))))); + topicConfigSerializeWrapper.setDataVersion(new DataVersion()); + RegisterBrokerBody requestBody = new RegisterBrokerBody(); + requestBody.setTopicConfigSerializeWrapper(topicConfigSerializeWrapper); + requestBody.setFilterServerList(Lists.newArrayList()); + request.setBody(requestBody.encode()); + + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + when(ctx.channel()).thenReturn(null); + + RemotingCommand response = defaultRequestProcessor.processRequest(ctx, request); + + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(response.getRemark()).isNull(); + } + LogicalQueueRouteData queueRouteData2 = new LogicalQueueRouteData(0, 100, new MessageQueue(topic, broker2Name, 0), MessageQueueRouteState.Normal, 0, -1, -1, -1, broker2Addr); + LogicalQueueRouteData queueRouteData3 = new LogicalQueueRouteData(1, 100, new MessageQueue(topic, broker2Name, 0), MessageQueueRouteState.Normal, 0, -1, -1, -1, broker2Addr); + { + RegisterBrokerRequestHeader header = new RegisterBrokerRequestHeader(); + header.setBrokerName(broker2Name); + RemotingCommand request = RemotingCommand.createRequestCommand( + RequestCode.REGISTER_BROKER, header); + request.addExtField("brokerName", broker2Name); + request.addExtField("brokerAddr", broker2Addr); + request.addExtField("clusterName", cluster); + request.addExtField("haServerAddr", "10.10.2.1"); + request.addExtField("brokerId", String.valueOf(MixAll.MASTER_ID)); + request.setVersion(MQVersion.CURRENT_VERSION); + TopicConfigSerializeWrapper topicConfigSerializeWrapper = new TopicConfigSerializeWrapper(); + topicConfigSerializeWrapper.setTopicConfigTable(new ConcurrentHashMap<>(Collections.singletonMap(topic, new TopicConfig(topic)))); + topicConfigSerializeWrapper.setLogicalQueuesInfoMap(Maps.newHashMap(topic, new LogicalQueuesInfo(ImmutableMap.of( + 0, Collections.singletonList(queueRouteData2), + 1, Collections.singletonList(queueRouteData3) + )))); + topicConfigSerializeWrapper.setDataVersion(new DataVersion()); + RegisterBrokerBody requestBody = new RegisterBrokerBody(); + requestBody.setTopicConfigSerializeWrapper(topicConfigSerializeWrapper); + requestBody.setFilterServerList(Lists.newArrayList()); + request.setBody(requestBody.encode()); + + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + when(ctx.channel()).thenReturn(null); + + RemotingCommand response = defaultRequestProcessor.processRequest(ctx, request); + + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(response.getRemark()).isNull(); + } + + { + GetRouteInfoRequestHeader header = new GetRouteInfoRequestHeader(); + header.setTopic(topic); + header.setSysFlag(MessageSysFlag.LOGICAL_QUEUE_FLAG); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ROUTEINFO_BY_TOPIC, header); + request.makeCustomHeaderToNet(); + + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + when(ctx.channel()).thenReturn(null); + + RemotingCommand response = defaultRequestProcessor.processRequest(ctx, request); + + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + + TopicRouteDataNameSrv topicRouteDataNameSrv = JSON.parseObject(response.getBody(), TopicRouteDataNameSrv.class); + assertThat(topicRouteDataNameSrv).isNotNull(); + LogicalQueuesInfoUnordered logicalQueuesInfoUnordered = new LogicalQueuesInfoUnordered(); + logicalQueuesInfoUnordered.put(0, ImmutableMap.of( + new LogicalQueuesInfoUnordered.Key(queueRouteData1.getBrokerName(), queueRouteData1.getQueueId(), queueRouteData1.getOffsetDelta()), queueRouteData1, + new LogicalQueuesInfoUnordered.Key(queueRouteData2.getBrokerName(), queueRouteData2.getQueueId(), queueRouteData2.getOffsetDelta()), queueRouteData2 + )); + logicalQueuesInfoUnordered.put(1, ImmutableMap.of(new LogicalQueuesInfoUnordered.Key(queueRouteData3.getBrokerName(), queueRouteData3.getQueueId(), queueRouteData3.getOffsetDelta()), queueRouteData3)); + assertThat(topicRouteDataNameSrv.getLogicalQueuesInfoUnordered()).isEqualTo(logicalQueuesInfoUnordered); + } + } +*/ + @Test + public void testProcessRequest_RegisterBrokerWithFilterServer() throws RemotingCommandException, + NoSuchFieldException, IllegalAccessException { + RemotingCommand request = genSampleRegisterCmd(true); + + // version >= MQVersion.Version.V3_0_11.ordinal() to register with filter server + request.setVersion(100); + + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + when(ctx.channel()).thenReturn(null); + + RemotingCommand response = defaultRequestProcessor.processRequest(ctx, request); + + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(response.getRemark()).isNull(); + + RouteInfoManager routes = namesrvController.getRouteInfoManager(); + Field brokerAddrTable = RouteInfoManager.class.getDeclaredField("brokerAddrTable"); + brokerAddrTable.setAccessible(true); + + BrokerData broker = new BrokerData(); + broker.setBrokerName("broker"); + broker.setBrokerAddrs((HashMap) Maps.newHashMap(new Long(2333), "10.10.1.1")); + + assertThat((Map) brokerAddrTable.get(routes)) + .contains(new HashMap.SimpleEntry("broker", broker)); + } + + @Test + public void testProcessRequest_UnregisterBroker() throws RemotingCommandException, NoSuchFieldException, IllegalAccessException { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + when(ctx.channel()).thenReturn(null); + + //Register broker + RemotingCommand regRequest = genSampleRegisterCmd(true); + defaultRequestProcessor.processRequest(ctx, regRequest); + + //Unregister broker + RemotingCommand unregRequest = genSampleRegisterCmd(false); + RemotingCommand unregResponse = defaultRequestProcessor.processRequest(ctx, unregRequest); + + assertThat(unregResponse.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(unregResponse.getRemark()).isNull(); + + RouteInfoManager routes = namesrvController.getRouteInfoManager(); + Field brokerAddrTable = RouteInfoManager.class.getDeclaredField("brokerAddrTable"); + brokerAddrTable.setAccessible(true); + + assertThat((Map) brokerAddrTable.get(routes)).isNotEmpty(); + } + + @Test + public void testGetAllTopicList() throws RemotingCommandException { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + Channel channel = mock(Channel.class); + when(channel.remoteAddress()).thenReturn(null); + when(ctx.channel()).thenReturn(channel); + + namesrvController.getNamesrvConfig().setEnableAllTopicList(true); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ALL_TOPIC_LIST_FROM_NAMESERVER, null); + + RemotingCommand response = defaultRequestProcessor.processRequest(ctx, request); + + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(response.getRemark()).isNull(); + + namesrvController.getNamesrvConfig().setEnableAllTopicList(false); + + response = defaultRequestProcessor.processRequest(ctx, request); + + assertThat(response.getCode()).isEqualTo(ResponseCode.SYSTEM_ERROR); + } + + @Test + public void testGetRouteInfoByTopic() throws Exception { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + when(ctx.channel()).thenReturn(null); + RemotingCommand request = getRemotingCommand(RequestCode.GET_ROUTEINFO_BY_TOPIC); + RemotingCommand remotingCommandSuccess = clientRequestProcessor.processRequest(ctx, request); + assertThat(remotingCommandSuccess.getCode()).isEqualTo(ResponseCode.SUCCESS); + request.getExtFields().put("topic", "test"); + RemotingCommand remotingCommandNoTopicRouteInfo = clientRequestProcessor.processRequest(ctx, request); + assertThat(remotingCommandNoTopicRouteInfo.getCode()).isEqualTo(ResponseCode.TOPIC_NOT_EXIST); + } + + @Test + public void testGetBrokerClusterInfo() throws RemotingCommandException { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + when(ctx.channel()).thenReturn(null); + RemotingCommand request = getRemotingCommand(RequestCode.GET_BROKER_CLUSTER_INFO); + RemotingCommand remotingCommand = defaultRequestProcessor.processRequest(ctx, request); + assertThat(remotingCommand.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testWipeWritePermOfBroker() throws RemotingCommandException { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + when(ctx.channel()).thenReturn(null); + RemotingCommand request = getRemotingCommand(RequestCode.WIPE_WRITE_PERM_OF_BROKER); + RemotingCommand remotingCommand = defaultRequestProcessor.processRequest(ctx, request); + assertThat(remotingCommand.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetAllTopicListFromNameserver() throws RemotingCommandException { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + when(ctx.channel()).thenReturn(mock(Channel.class)); + when(ctx.channel().remoteAddress()).thenReturn(new InetSocketAddress(123)); + RemotingCommand request = getRemotingCommand(RequestCode.GET_ALL_TOPIC_LIST_FROM_NAMESERVER); + RemotingCommand remotingCommand = defaultRequestProcessor.processRequest(ctx, request); + assertThat(remotingCommand.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testDeleteTopicInNamesrv() throws RemotingCommandException { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + when(ctx.channel()).thenReturn(null); + RemotingCommand request = getRemotingCommand(RequestCode.DELETE_TOPIC_IN_NAMESRV); + RemotingCommand remotingCommand = defaultRequestProcessor.processRequest(ctx, request); + assertThat(remotingCommand.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetKVListByNamespace() throws RemotingCommandException { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + when(ctx.channel()).thenReturn(null); + RemotingCommand request = getRemotingCommand(RequestCode.GET_KVLIST_BY_NAMESPACE); + request.addExtField("namespace", "default-namespace-1"); + RemotingCommand remotingCommand = defaultRequestProcessor.processRequest(ctx, request); + assertThat(remotingCommand.getCode()).isEqualTo(ResponseCode.QUERY_NOT_FOUND); + namesrvController.getKvConfigManager().putKVConfig("default-namespace-1", "key", "value"); + RemotingCommand remotingCommandSuccess = defaultRequestProcessor.processRequest(ctx, request); + assertThat(remotingCommandSuccess.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetTopicsByCluster() throws RemotingCommandException { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + when(ctx.channel()).thenReturn(null); + RemotingCommand request = getRemotingCommand(RequestCode.GET_TOPICS_BY_CLUSTER); + request.addExtField("cluster", "default-cluster"); + RemotingCommand remotingCommand = defaultRequestProcessor.processRequest(ctx, request); + assertThat(remotingCommand.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetSystemTopicListFromNs() throws RemotingCommandException { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + when(ctx.channel()).thenReturn(null); + RemotingCommand request = getRemotingCommand(RequestCode.GET_SYSTEM_TOPIC_LIST_FROM_NS); + request.addExtField("cluster", "default-cluster"); + RemotingCommand remotingCommand = defaultRequestProcessor.processRequest(ctx, request); + assertThat(remotingCommand.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetUnitTopicList() throws RemotingCommandException { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + when(ctx.channel()).thenReturn(null); + RemotingCommand request = getRemotingCommand(RequestCode.GET_UNIT_TOPIC_LIST); + request.addExtField("cluster", "default-cluster"); + RemotingCommand remotingCommand = defaultRequestProcessor.processRequest(ctx, request); + assertThat(remotingCommand.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetHasUnitSubTopicList() throws RemotingCommandException { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + when(ctx.channel()).thenReturn(null); + RemotingCommand request = getRemotingCommand(RequestCode.GET_HAS_UNIT_SUB_TOPIC_LIST); + request.addExtField("cluster", "default-cluster"); + RemotingCommand remotingCommand = defaultRequestProcessor.processRequest(ctx, request); + assertThat(remotingCommand.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetHasUnitSubUnUnitTopicList() throws RemotingCommandException { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + when(ctx.channel()).thenReturn(null); + RemotingCommand request = getRemotingCommand(RequestCode.GET_HAS_UNIT_SUB_UNUNIT_TOPIC_LIST); + request.addExtField("cluster", "default-cluster"); + RemotingCommand remotingCommand = defaultRequestProcessor.processRequest(ctx, request); + assertThat(remotingCommand.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testUpdateConfig() throws RemotingCommandException { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + when(ctx.channel()).thenReturn(null); + RemotingCommand request = getRemotingCommand(RequestCode.UPDATE_NAMESRV_CONFIG); + request.addExtField("cluster", "default-cluster"); + Map propertiesMap = new HashMap<>(); + propertiesMap.put("key", "value"); + request.setBody(propertiesMap.toString().getBytes()); + RemotingCommand remotingCommand = defaultRequestProcessor.processRequest(ctx, request); + assertThat(remotingCommand.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testGetConfig() throws RemotingCommandException { + ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); + when(ctx.channel()).thenReturn(null); + RemotingCommand request = getRemotingCommand(RequestCode.GET_NAMESRV_CONFIG); + request.addExtField("cluster", "default-cluster"); + RemotingCommand remotingCommand = defaultRequestProcessor.processRequest(ctx, request); + assertThat(remotingCommand.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + private RemotingCommand getRemotingCommand(int code) { + RegisterBrokerRequestHeader header = new RegisterBrokerRequestHeader(); + header.setBrokerName("broker"); + RemotingCommand request = RemotingCommand.createRequestCommand(code, header); + request.addExtField("brokerName", "broker"); + request.addExtField("brokerAddr", "10.10.1.1"); + request.addExtField("clusterName", "cluster"); + request.addExtField("haServerAddr", "10.10.2.1"); + request.addExtField("brokerId", "2333"); + request.addExtField("topic", "unit-test0"); + return request; + } + + private static RemotingCommand genSampleRegisterCmd(boolean reg) { + RegisterBrokerRequestHeader header = new RegisterBrokerRequestHeader(); + byte[] body = null; + if (reg) { + TopicConfigAndMappingSerializeWrapper topicConfigWrapper = new TopicConfigAndMappingSerializeWrapper(); + topicConfigWrapper.getTopicConfigTable().put("unit-test1", new TopicConfig()); + topicConfigWrapper.getTopicConfigTable().put("unit-test2", new TopicConfig()); + RegisterBrokerBody requestBody = new RegisterBrokerBody(); + requestBody.setTopicConfigSerializeWrapper(topicConfigWrapper); + body = requestBody.encode(false); + final int bodyCrc32 = UtilAll.crc32(body); + header.setBodyCrc32(bodyCrc32); + } + header.setBrokerName("broker"); + RemotingCommand request = RemotingCommand.createRequestCommand( + reg ? RequestCode.REGISTER_BROKER : RequestCode.UNREGISTER_BROKER, header); + request.addExtField("brokerName", "broker"); + request.addExtField("brokerAddr", "10.10.1.1"); + request.addExtField("clusterName", "cluster"); + request.addExtField("haServerAddr", "10.10.2.1"); + request.addExtField("brokerId", "2333"); + request.setBody(body); + return request; + } + + private static void setFinalStatic(Field field, Object newValue) throws Exception { + field.setAccessible(true); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + field.set(null, newValue); + } + + private void registerRouteInfoManager() { + TopicConfigSerializeWrapper topicConfigSerializeWrapper = new TopicConfigSerializeWrapper(); + ConcurrentHashMap topicConfigConcurrentHashMap = new ConcurrentHashMap<>(); + for (int i = 0; i < 2; i++) { + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setWriteQueueNums(8); + topicConfig.setTopicName("unit-test" + i); + topicConfig.setPerm(6); + topicConfig.setReadQueueNums(8); + topicConfig.setOrder(false); + topicConfigConcurrentHashMap.put(topicConfig.getTopicName(), topicConfig); + } + topicConfigSerializeWrapper.setTopicConfigTable(topicConfigConcurrentHashMap); + Channel channel = mock(Channel.class); + RegisterBrokerResult registerBrokerResult = routeInfoManager.registerBroker("default-cluster", "127.0.0.1:10911", "default-broker", 1234, "127.0.0.1:1001", "", + null, topicConfigSerializeWrapper, new ArrayList<>(), channel); + + } + +} diff --git a/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/GetRouteInfoBenchmark.java b/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/GetRouteInfoBenchmark.java new file mode 100644 index 00000000000..b453f4ded74 --- /dev/null +++ b/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/GetRouteInfoBenchmark.java @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.namesrv.routeinfo; + +import io.netty.channel.Channel; +import java.util.ArrayList; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.namesrv.NamesrvConfig; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigSerializeWrapper; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +import static org.mockito.Mockito.mock; + +@BenchmarkMode(Mode.SampleTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@State(Scope.Benchmark) +public class GetRouteInfoBenchmark { + private RouteInfoManager routeInfoManager; + private String[] topicList = new String[40000]; + private ExecutorService es = Executors.newCachedThreadPool(); + + @Setup + public void setup() throws InterruptedException { + + routeInfoManager = new RouteInfoManager(new NamesrvConfig(), null); + + // Init 4 clusters and 8 brokers in each cluster + // Each cluster has 10000 topics + + for (int i = 0; i < 40000; i++) { + final String topic = RandomStringUtils.randomAlphabetic(32) + i; + topicList[i] = topic; + } + + for (int i = 0; i < 4; i++) { + // Cluster iteration + final String clusterName = "Default-Cluster-" + i; + for (int j = 0; j < 8; j++) { + // broker iteration + final int startTopicIndex = i * 10000; + final String brokerName = "Default-Broker-" + j; + final String brokerAddr = "127.0.0.1:500" + i * j; + es.submit(new Runnable() { + @Override + public void run() { + DataVersion dataVersion = new DataVersion(); + dataVersion.setCounter(new AtomicLong(10L)); + dataVersion.setTimestamp(100L); + + ConcurrentHashMap topicConfigConcurrentHashMap = new ConcurrentHashMap<>(); + + for (int k = startTopicIndex; k < startTopicIndex + 10000; k++) { + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setWriteQueueNums(8); + topicConfig.setTopicName(topicList[k]); + topicConfig.setPerm(6); + topicConfig.setReadQueueNums(8); + topicConfig.setOrder(false); + topicConfigConcurrentHashMap.put(topicList[k], topicConfig); + } + + while (true) { + try { + TimeUnit.MILLISECONDS.sleep(new Random().nextInt(100)); + } catch (InterruptedException ignored) { + } + + dataVersion.nextVersion(); + TopicConfigSerializeWrapper topicConfigSerializeWrapper = new TopicConfigSerializeWrapper(); + topicConfigSerializeWrapper.setDataVersion(dataVersion); + topicConfigSerializeWrapper.setTopicConfigTable(topicConfigConcurrentHashMap); + Channel channel = mock(Channel.class); + + routeInfoManager.registerBroker(clusterName, brokerAddr, brokerName, 0, brokerAddr, "", + null, topicConfigSerializeWrapper, new ArrayList<>(), channel); + } + } + }); + } + } + + // Wait threads startup + TimeUnit.SECONDS.sleep(3); + } + + @TearDown + public void tearDown() { + ThreadUtils.shutdownGracefully(es, 3, TimeUnit.SECONDS); + } + + @Benchmark + @Fork(value = 2) + @Measurement(iterations = 10, time = 10) + @Warmup(iterations = 10, time = 1) + @Threads(4) // Assume we have 128 clients try to pick up route data concurrently + public void pickupTopicRouteData() { + routeInfoManager.pickupTopicRouteData(topicList[new Random().nextInt(40000)]); + } + + public static void main(String[] args) throws Exception { + org.openjdk.jmh.Main.main(args); + } +} diff --git a/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/RegisterBrokerBenchmark.java b/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/RegisterBrokerBenchmark.java new file mode 100644 index 00000000000..0e9cf67f565 --- /dev/null +++ b/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/RegisterBrokerBenchmark.java @@ -0,0 +1,168 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.namesrv.routeinfo; + +import io.netty.channel.Channel; +import java.util.ArrayList; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.namesrv.NamesrvConfig; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigSerializeWrapper; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +import static org.mockito.Mockito.mock; + +@BenchmarkMode(Mode.SampleTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@State(Scope.Benchmark) +public class RegisterBrokerBenchmark { + private RouteInfoManager routeInfoManager; + private String[] topicList = new String[40000]; + private ConcurrentHashMap[] topicConfigMaps = new ConcurrentHashMap[32]; + private DataVersion[] dataVersions = new DataVersion[32]; + private ExecutorService es = Executors.newCachedThreadPool(); + private AtomicLong brokerIndex = new AtomicLong(0); + + @Setup + public void setup() throws InterruptedException { + routeInfoManager = new RouteInfoManager(new NamesrvConfig(), null); + + // Init 4 clusters and 8 brokers in each cluster + // Each cluster has 10000 topics + + for (int i = 0; i < 40000; i++) { + final String topic = RandomStringUtils.randomAlphabetic(32) + i; + topicList[i] = topic; + } + + for (int i = 0; i < 4; i++) { + // Cluster iteration + final String clusterName = "Default-Cluster-" + i; + for (int j = 0; j < 8; j++) { + // broker iteration + final int startTopicIndex = i * 10000; + final String brokerName = "Default-Broker-" + j; + final String brokerAddr = "127.0.0.1:500" + (i * 8 + j); + + topicConfigMaps[i * 8 + j] = new ConcurrentHashMap<>(); + for (int k = startTopicIndex; k < startTopicIndex + 10000; k++) { + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setWriteQueueNums(8); + topicConfig.setTopicName(topicList[k]); + topicConfig.setPerm(6); + topicConfig.setReadQueueNums(8); + topicConfig.setOrder(false); + topicConfigMaps[i * 8 + j].put(topicList[k], topicConfig); + } + + DataVersion dataVersion = new DataVersion(); + dataVersion.setCounter(new AtomicLong(10L)); + dataVersion.setTimestamp(100L); + + dataVersions[i * 8 + j] = dataVersion; + } + } + + // Init 32 threads to pick up route info + for (int i = 0; i < 32; i++) { + es.submit(new Runnable() { + @Override + public void run() { + routeInfoManager.pickupTopicRouteData(topicList[new Random().nextInt(40000)]); + try { + TimeUnit.MILLISECONDS.sleep(new Random().nextInt(10)); + } catch (InterruptedException ignored) { + } + } + }); + } + } + + @TearDown + public void tearDown() { + ThreadUtils.shutdownGracefully(es, 3, TimeUnit.SECONDS); + } + + @Benchmark + @Fork(value = 2) + @Measurement(iterations = 10, time = 10) + @Warmup(iterations = 10, time = 1) + @Threads(32) // Assume we have 128 clients try to pick up route data concurrently + public void registerBroker() { + final long index = Math.abs(brokerIndex.getAndIncrement() % 32); + dataVersions[(int) index].nextVersion(); + + TopicConfigSerializeWrapper topicConfigSerializeWrapper = new TopicConfigSerializeWrapper(); + topicConfigSerializeWrapper.setDataVersion(dataVersions[(int) index]); + topicConfigSerializeWrapper.setTopicConfigTable(topicConfigMaps[(int) index]); + Channel channel = mock(Channel.class); + + routeInfoManager.registerBroker("DefaultCluster" + index, + "127.0.0.1:500" + index, + "DefaultBroker" + index, 0, "127.0.0.1:400" + index, + "", + null, + topicConfigSerializeWrapper, new ArrayList<>(), channel); + } + + @Benchmark + @Fork(value = 2) + @Measurement(iterations = 10, time = 10) + @Warmup(iterations = 10, time = 1) + @Threads(32) // Assume we have 128 clients try to pick up route data concurrently + @BenchmarkMode(Mode.Throughput) + public void registerBroker_Throughput() { + final long index = Math.abs(brokerIndex.getAndIncrement() % 32); + dataVersions[(int) index].nextVersion(); + + TopicConfigSerializeWrapper topicConfigSerializeWrapper = new TopicConfigSerializeWrapper(); + topicConfigSerializeWrapper.setDataVersion(dataVersions[(int) index]); + topicConfigSerializeWrapper.setTopicConfigTable(topicConfigMaps[(int) index]); + Channel channel = mock(Channel.class); + + routeInfoManager.registerBroker("DefaultCluster" + index, + "127.0.0.1:500" + index, + "DefaultBroker" + index, 0, "127.0.0.1:400" + index, + "", + null, + topicConfigSerializeWrapper, new ArrayList<>(), channel); + } + + public static void main(String[] args) throws Exception { + org.openjdk.jmh.Main.main(args); + } +} diff --git a/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManagerBrokerPermTest.java b/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManagerBrokerPermTest.java new file mode 100644 index 00000000000..5a2fab8f0eb --- /dev/null +++ b/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManagerBrokerPermTest.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.namesrv.routeinfo; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; +import org.apache.rocketmq.common.constant.PermName; +import org.apache.rocketmq.common.namesrv.NamesrvConfig; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RouteInfoManagerBrokerPermTest extends RouteInfoManagerTestBase { + private static RouteInfoManager routeInfoManager; + public static String clusterName = "cluster"; + public static String brokerPrefix = "broker"; + public static String topicPrefix = "topic"; + + public static RouteInfoManagerTestBase.Cluster cluster; + + @Before + public void setup() { + routeInfoManager = new RouteInfoManager(new NamesrvConfig(), null); + cluster = registerCluster(routeInfoManager, + clusterName, + brokerPrefix, + 3, + 3, + topicPrefix, + 10); + } + + @After + public void terminate() { + routeInfoManager.printAllPeriodically(); + + for (BrokerData bd : cluster.brokerDataMap.values()) { + unregisterBrokerAll(routeInfoManager, bd); + } + } + + @Test + public void testAddWritePermOfBrokerByLock() throws Exception { + String brokerName = getBrokerName(brokerPrefix, 0); + String topicName = getTopicName(topicPrefix, 0); + + QueueData qd = new QueueData(); + qd.setPerm(PermName.PERM_READ); + qd.setBrokerName(brokerName); + + HashMap> topicQueueTable = new HashMap<>(); + + Map queueDataMap = new HashMap<>(); + queueDataMap.put(brokerName, qd); + topicQueueTable.put(topicName, queueDataMap); + + Field filed = RouteInfoManager.class.getDeclaredField("topicQueueTable"); + filed.setAccessible(true); + filed.set(routeInfoManager, topicQueueTable); + + int addTopicCnt = routeInfoManager.addWritePermOfBrokerByLock(brokerName); + assertThat(addTopicCnt).isEqualTo(1); + assertThat(qd.getPerm()).isEqualTo(PermName.PERM_READ | PermName.PERM_WRITE); + + } + + @Test + public void testWipeWritePermOfBrokerByLock() throws Exception { + String brokerName = getBrokerName(brokerPrefix, 0); + String topicName = getTopicName(topicPrefix, 0); + + QueueData qd = new QueueData(); + qd.setPerm(PermName.PERM_READ); + qd.setBrokerName(brokerName); + + HashMap> topicQueueTable = new HashMap<>(); + + Map queueDataMap = new HashMap<>(); + queueDataMap.put(brokerName, qd); + topicQueueTable.put(topicName, queueDataMap); + + Field filed = RouteInfoManager.class.getDeclaredField("topicQueueTable"); + filed.setAccessible(true); + filed.set(routeInfoManager, topicQueueTable); + + int addTopicCnt = routeInfoManager.wipeWritePermOfBrokerByLock(brokerName); + assertThat(addTopicCnt).isEqualTo(1); + assertThat(qd.getPerm()).isEqualTo(PermName.PERM_READ); + + } +} diff --git a/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManagerBrokerRegisterTest.java b/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManagerBrokerRegisterTest.java new file mode 100644 index 00000000000..aa616e64da9 --- /dev/null +++ b/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManagerBrokerRegisterTest.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.namesrv.routeinfo; + +import java.util.ArrayList; +import java.util.HashMap; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.namesrv.NamesrvConfig; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class RouteInfoManagerBrokerRegisterTest extends RouteInfoManagerTestBase { + private static RouteInfoManager routeInfoManager; + public static String clusterName = "cluster"; + public static String brokerPrefix = "broker"; + public static String topicPrefix = "topic"; + public static int brokerPerName = 3; + public static int brokerNameNumber = 3; + + public static RouteInfoManagerTestBase.Cluster cluster; + + @Before + public void setup() { + routeInfoManager = new RouteInfoManager(new NamesrvConfig(), null); + cluster = registerCluster(routeInfoManager, + clusterName, + brokerPrefix, + brokerNameNumber, + brokerPerName, + topicPrefix, + 10); + } + + @After + public void terminate() { + routeInfoManager.printAllPeriodically(); + + for (BrokerData bd : cluster.brokerDataMap.values()) { + unregisterBrokerAll(routeInfoManager, bd); + } + } + +// @Test +// public void testScanNotActiveBroker() { +// for (int j = 0; j < brokerNameNumber; j++) { +// String brokerName = getBrokerName(brokerPrefix, j); +// +// for (int i = 0; i < brokerPerName; i++) { +// String brokerAddr = getBrokerAddr(clusterName, brokerName, i); +// +// // set not active +// routeInfoManager.updateBrokerInfoUpdateTimestamp(brokerAddr, 0); +// +// assertEquals(1, routeInfoManager.scanNotActiveBroker()); +// } +// } +// +// } + + @Test + public void testMasterChangeFromSlave() { + String topicName = getTopicName(topicPrefix, 0); + String brokerName = getBrokerName(brokerPrefix, 0); + + String originMasterAddr = getBrokerAddr(clusterName, brokerName, MixAll.MASTER_ID); + TopicRouteData topicRouteData = routeInfoManager.pickupTopicRouteData(topicName); + BrokerData brokerDataOrigin = findBrokerDataByBrokerName(topicRouteData.getBrokerDatas(), brokerName); + + // check origin master address + Assert.assertEquals(brokerDataOrigin.getBrokerAddrs().get(MixAll.MASTER_ID), originMasterAddr); + + // master changed + String newMasterAddr = getBrokerAddr(clusterName, brokerName, 1); + registerBrokerWithTopicConfig(routeInfoManager, + clusterName, + newMasterAddr, + brokerName, + MixAll.MASTER_ID, + newMasterAddr, + cluster.topicConfig, + new ArrayList<>()); + + topicRouteData = routeInfoManager.pickupTopicRouteData(topicName); + brokerDataOrigin = findBrokerDataByBrokerName(topicRouteData.getBrokerDatas(), brokerName); + + // check new master address + assertEquals(brokerDataOrigin.getBrokerAddrs().get(MixAll.MASTER_ID), newMasterAddr); + } + + @Test + public void testUnregisterBroker() { + String topicName = getTopicName(topicPrefix, 0); + String brokerName = getBrokerName(brokerPrefix, 0); + long unregisterBrokerId = 2; + + unregisterBroker(routeInfoManager, cluster.brokerDataMap.get(brokerName), unregisterBrokerId); + + TopicRouteData topicRouteData = routeInfoManager.pickupTopicRouteData(topicName); + HashMap brokerAddrs = findBrokerDataByBrokerName(topicRouteData.getBrokerDatas(), brokerName).getBrokerAddrs(); + + assertFalse(brokerAddrs.containsKey(unregisterBrokerId)); + } +} diff --git a/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManagerNewTest.java b/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManagerNewTest.java new file mode 100644 index 00000000000..b52cf50740a --- /dev/null +++ b/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManagerNewTest.java @@ -0,0 +1,886 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.namesrv.routeinfo; + +import com.google.common.collect.Sets; +import io.netty.channel.Channel; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.constant.PermName; +import org.apache.rocketmq.common.namesrv.NamesrvConfig; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.body.TopicList; +import org.apache.rocketmq.remoting.protocol.header.namesrv.UnRegisterBrokerRequestHeader; +import org.apache.rocketmq.remoting.protocol.namesrv.RegisterBrokerResult; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Spy; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + +public class RouteInfoManagerNewTest { + private RouteInfoManager routeInfoManager; + private static final String DEFAULT_CLUSTER = "Default_Cluster"; + private static final String DEFAULT_BROKER = "Default_Broker"; + private static final String DEFAULT_ADDR_PREFIX = "127.0.0.1:"; + private static final String DEFAULT_ADDR = DEFAULT_ADDR_PREFIX + "10911"; + + // Synced from RouteInfoManager + private static final int BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2; + + @Spy + private static NamesrvConfig config = spy(new NamesrvConfig()); + + @Before + public void setup() { + config.setSupportActingMaster(true); + routeInfoManager = new RouteInfoManager(config, null); + routeInfoManager.start(); + } + + @After + public void tearDown() throws Exception { + routeInfoManager.shutdown(); + } + + @Test + public void getAllClusterInfo() { + registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker(), "TestTopic"); + + registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker() + .cluster("AnotherCluster") + .name("AnotherBroker") + .addr(DEFAULT_ADDR_PREFIX + 30911), "TestTopic1"); + + final byte[] content = routeInfoManager.getAllClusterInfo().encode(); + + final ClusterInfo clusterInfo = ClusterInfo.decode(content, ClusterInfo.class); + + assertThat(clusterInfo.retrieveAllClusterNames()).contains(DEFAULT_CLUSTER, "AnotherCluster"); + assertThat(clusterInfo.getBrokerAddrTable().keySet()).contains(DEFAULT_BROKER, "AnotherBroker"); + + final List addrList = Arrays.asList(clusterInfo.getBrokerAddrTable().get(DEFAULT_BROKER).getBrokerAddrs().get(0L), + clusterInfo.getBrokerAddrTable().get("AnotherBroker").getBrokerAddrs().get(0L)); + assertThat(addrList).contains(DEFAULT_ADDR, DEFAULT_ADDR_PREFIX + 30911); + } + + @Test + public void deleteTopic() { + String testTopic = "TestTopic"; + registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker(), testTopic); + + routeInfoManager.deleteTopic(testTopic); + + assertThat(routeInfoManager.pickupTopicRouteData(testTopic)).isNull(); + + registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker(), testTopic); + registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker().cluster("AnotherCluster").name("AnotherBroker"), + testTopic); + + assertThat(routeInfoManager.pickupTopicRouteData(testTopic).getBrokerDatas().size()).isEqualTo(2); + routeInfoManager.deleteTopic(testTopic, DEFAULT_CLUSTER); + + assertThat(routeInfoManager.pickupTopicRouteData(testTopic).getBrokerDatas().size()).isEqualTo(1); + assertThat(routeInfoManager.pickupTopicRouteData(testTopic).getBrokerDatas().get(0).getBrokerName()).isEqualTo("AnotherBroker"); + } + + @Test + public void getAllTopicList() { + byte[] content = routeInfoManager.getAllTopicList().encode(); + + TopicList topicList = TopicList.decode(content, TopicList.class); + assertThat(topicList.getTopicList()).isEmpty(); + + registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker(), "TestTopic", "TestTopic1", "TestTopic2"); + + content = routeInfoManager.getAllTopicList().encode(); + + topicList = TopicList.decode(content, TopicList.class); + assertThat(topicList.getTopicList()).contains("TestTopic", "TestTopic1", "TestTopic2"); + } + @Test + public void registerBroker() { + // Register master broker + final RegisterBrokerResult masterResult = registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker(), "TestTopic"); + + assertThat(masterResult).isNotNull(); + assertThat(masterResult.getHaServerAddr()).isNull(); + assertThat(masterResult.getMasterAddr()).isNull(); + + // Register slave broker + + final BrokerBasicInfo slaveBroker = BrokerBasicInfo.defaultBroker() + .id(1).addr(DEFAULT_ADDR_PREFIX + 30911).haAddr(DEFAULT_ADDR_PREFIX + 40911); + + final RegisterBrokerResult slaveResult = registerBrokerWithNormalTopic(slaveBroker, "TestTopic"); + + assertThat(slaveResult).isNotNull(); + assertThat(slaveResult.getHaServerAddr()).isEqualTo(DEFAULT_ADDR_PREFIX + 20911); + assertThat(slaveResult.getMasterAddr()).isEqualTo(DEFAULT_ADDR); + } + + @Test + public void unregisterBroker() { + registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker(), "TestTopic", "TestTopic1", "TestTopic2"); + routeInfoManager.unregisterBroker(DEFAULT_CLUSTER, DEFAULT_ADDR, DEFAULT_BROKER, 0); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic")).isNull(); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic1")).isNull(); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic2")).isNull(); + + registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker(), "TestTopic", "TestTopic1", "TestTopic2"); + + routeInfoManager.submitUnRegisterBrokerRequest(BrokerBasicInfo.defaultBroker().unRegisterRequest()); + await().atMost(Duration.ofSeconds(5)).until(() -> routeInfoManager.blockedUnRegisterRequests() == 0); + + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic")).isNull(); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic1")).isNull(); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic2")).isNull(); + } + + @Test + public void registerSlaveBroker() { + registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker(), "TestTopic"); + + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().get(0).getBrokerAddrs()).containsKeys(0L); + + registerBrokerWithNormalTopic(BrokerBasicInfo.slaveBroker(), "TestTopic"); + + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().get(0).getBrokerAddrs()).containsKeys(0L, 1L); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().get(0).getBrokerAddrs()) + .containsValues(BrokerBasicInfo.defaultBroker().brokerAddr, BrokerBasicInfo.slaveBroker().brokerAddr); + } + + @Test + public void createNewTopic() { + registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker(), "TestTopic"); + registerBrokerWithNormalTopic(BrokerBasicInfo.slaveBroker(), "TestTopic"); + + registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker(), "TestTopic", "TestTopic1"); + + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic1").getBrokerDatas().get(0).getBrokerAddrs()).containsKeys(0L, 1L); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic1").getBrokerDatas().get(0).getBrokerAddrs()) + .containsValues(BrokerBasicInfo.defaultBroker().brokerAddr, BrokerBasicInfo.slaveBroker().brokerAddr); + } + + @Test + public void switchBrokerRole() { + final BrokerBasicInfo masterBroker = BrokerBasicInfo.defaultBroker().enableActingMaster(false); + final BrokerBasicInfo slaveBroker = BrokerBasicInfo.slaveBroker().enableActingMaster(false); + registerBrokerWithNormalTopic(masterBroker, "TestTopic"); + registerBrokerWithNormalTopic(slaveBroker, "TestTopic"); + + // Master Down + routeInfoManager.unregisterBroker(masterBroker.clusterName, masterBroker.brokerAddr, masterBroker.brokerName, 0); + + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().get(0).getBrokerAddrs()).containsOnlyKeys(1L); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().get(0).getBrokerAddrs()) + .containsValues(slaveBroker.brokerAddr); + + // Switch slave to master + slaveBroker.id(0).dataVersion.nextVersion(); + registerBrokerWithNormalTopic(slaveBroker, "TestTopic"); + + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().get(0).getBrokerAddrs()).containsOnlyKeys(0L); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().get(0).getBrokerAddrs()) + .containsValues(slaveBroker.brokerAddr); + + // Old master switch to slave + masterBroker.id(1).dataVersion.nextVersion(); + registerBrokerWithNormalTopic(masterBroker, "TestTopic"); + + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().get(0).getBrokerAddrs()).containsKeys(0L, 1L); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().get(0).getBrokerAddrs()) + .containsValues(BrokerBasicInfo.defaultBroker().brokerAddr, BrokerBasicInfo.slaveBroker().brokerAddr); + } + + @Test + public void unRegisterSlaveBroker() { + registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker(), "TestTopic"); + final BrokerBasicInfo slaveBroker = BrokerBasicInfo.slaveBroker(); + registerBrokerWithNormalTopic(slaveBroker, "TestTopic"); + + routeInfoManager.unregisterBroker(slaveBroker.clusterName, slaveBroker.brokerAddr, slaveBroker.brokerName, 1); + + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().size()).isEqualTo(1); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().get(0) + .getBrokerAddrs().get(0L)).isEqualTo(BrokerBasicInfo.defaultBroker().brokerAddr); + + registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker(), "TestTopic"); + registerBrokerWithNormalTopic(slaveBroker, "TestTopic"); + + routeInfoManager.submitUnRegisterBrokerRequest(slaveBroker.unRegisterRequest()); + await().atMost(Duration.ofSeconds(5)).until(() -> routeInfoManager.blockedUnRegisterRequests() == 0); + + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().size()).isEqualTo(1); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().get(0) + .getBrokerAddrs().get(0L)).isEqualTo(BrokerBasicInfo.defaultBroker().brokerAddr); + } + + @Test + public void unRegisterMasterBroker() { + final BrokerBasicInfo masterBroker = BrokerBasicInfo.defaultBroker(); + masterBroker.enableActingMaster = true; + registerBrokerWithNormalTopic(masterBroker, "TestTopic"); + final BrokerBasicInfo slaveBroker = BrokerBasicInfo.slaveBroker(); + registerBrokerWithNormalTopic(slaveBroker, "TestTopic"); + + routeInfoManager.unregisterBroker(masterBroker.clusterName, masterBroker.brokerAddr, masterBroker.brokerName, 0); + + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().size()).isEqualTo(1); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().get(0) + .getBrokerAddrs().get(0L)).isEqualTo(slaveBroker.brokerAddr); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getQueueDatas().size()).isEqualTo(1); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getQueueDatas().get(0).getPerm()).isEqualTo(PermName.PERM_READ); + } + + @Test + public void unRegisterMasterBrokerOldVersion() { + final BrokerBasicInfo masterBroker = BrokerBasicInfo.defaultBroker(); + masterBroker.enableActingMaster = false; + registerBrokerWithNormalTopic(masterBroker, "TestTopic"); + final BrokerBasicInfo slaveBroker = BrokerBasicInfo.slaveBroker(); + slaveBroker.enableActingMaster = false; + registerBrokerWithNormalTopic(slaveBroker, "TestTopic"); + + routeInfoManager.unregisterBroker(masterBroker.clusterName, masterBroker.brokerAddr, masterBroker.brokerName, 0); + + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().size()).isEqualTo(1); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().get(0) + .getBrokerAddrs().get(0L)).isNull(); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getQueueDatas().size()).isEqualTo(1); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getQueueDatas().get(0).getPerm()).isEqualTo(PermName.PERM_READ | PermName.PERM_WRITE); + } + + @Test + public void submitMultiUnRegisterRequests() { + final BrokerBasicInfo master1 = BrokerBasicInfo.defaultBroker(); + final BrokerBasicInfo master2 = BrokerBasicInfo.defaultBroker().name(DEFAULT_BROKER + 1).addr(DEFAULT_ADDR + 9); + registerBrokerWithNormalTopic(master1, "TestTopic1"); + registerBrokerWithNormalTopic(master2, "TestTopic2"); + + routeInfoManager.submitUnRegisterBrokerRequest(master1.unRegisterRequest()); + routeInfoManager.submitUnRegisterBrokerRequest(master2.unRegisterRequest()); + + await().atMost(Duration.ofSeconds(5)).until(() -> routeInfoManager.blockedUnRegisterRequests() == 0); + + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic1")).isNull(); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic2")).isNull(); + } + + @Test + public void isBrokerTopicConfigChanged() { + registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker(), "TestTopic"); + + final DataVersion dataVersion = routeInfoManager.queryBrokerTopicConfig(DEFAULT_CLUSTER, DEFAULT_ADDR); + + assertThat(routeInfoManager.isBrokerTopicConfigChanged(DEFAULT_CLUSTER, DEFAULT_ADDR, dataVersion)).isFalse(); + + DataVersion newVersion = new DataVersion(); + newVersion.setTimestamp(System.currentTimeMillis() + 1000); + newVersion.setCounter(new AtomicLong(dataVersion.getCounter().get())); + + assertThat(routeInfoManager.isBrokerTopicConfigChanged(DEFAULT_CLUSTER, DEFAULT_ADDR, newVersion)).isTrue(); + + newVersion = new DataVersion(); + newVersion.setTimestamp(dataVersion.getTimestamp()); + newVersion.setCounter(new AtomicLong(dataVersion.getCounter().get() + 1)); + + assertThat(routeInfoManager.isBrokerTopicConfigChanged(DEFAULT_CLUSTER, DEFAULT_ADDR, newVersion)).isTrue(); + } + + @Test + public void isTopicConfigChanged() { + final BrokerBasicInfo brokerInfo = BrokerBasicInfo.defaultBroker(); + assertThat(routeInfoManager.isTopicConfigChanged(DEFAULT_CLUSTER, + DEFAULT_ADDR, + brokerInfo.dataVersion, + DEFAULT_BROKER, "TestTopic")).isTrue(); + + registerBrokerWithNormalTopic(brokerInfo, "TestTopic"); + + assertThat(routeInfoManager.isTopicConfigChanged(DEFAULT_CLUSTER, + DEFAULT_ADDR, + brokerInfo.dataVersion, + DEFAULT_BROKER, "TestTopic")).isFalse(); + + assertThat(routeInfoManager.isTopicConfigChanged(DEFAULT_CLUSTER, + DEFAULT_ADDR, + brokerInfo.dataVersion, + DEFAULT_BROKER, "TestTopic1")).isTrue(); + } + + @Test + public void queryBrokerTopicConfig() { + final BrokerBasicInfo basicInfo = BrokerBasicInfo.defaultBroker(); + registerBrokerWithNormalTopic(basicInfo, "TestTopic"); + + final DataVersion dataVersion = routeInfoManager.queryBrokerTopicConfig(DEFAULT_CLUSTER, DEFAULT_ADDR); + + assertThat(basicInfo.dataVersion.equals(dataVersion)).isTrue(); + } + + @Test + public void wipeWritePermOfBrokerByLock() { + registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker(), "TestTopic"); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getQueueDatas().get(0).getPerm()).isEqualTo(6); + + routeInfoManager.wipeWritePermOfBrokerByLock(DEFAULT_BROKER); + + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getQueueDatas().get(0).getPerm()).isEqualTo(4); + } + + @Test + public void pickupTopicRouteData() { + String testTopic = "TestTopic"; + registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker(), testTopic); + + TopicRouteData data = routeInfoManager.pickupTopicRouteData(testTopic); + assertThat(data.getBrokerDatas().size()).isEqualTo(1); + assertThat(data.getBrokerDatas().get(0).getBrokerName()).isEqualTo(DEFAULT_BROKER); + assertThat(data.getBrokerDatas().get(0).getBrokerAddrs().get(0L)).isEqualTo(DEFAULT_ADDR); + assertThat(data.getQueueDatas().size()).isEqualTo(1); + assertThat(data.getQueueDatas().get(0).getBrokerName()).isEqualTo(DEFAULT_BROKER); + assertThat(data.getQueueDatas().get(0).getReadQueueNums()).isEqualTo(8); + assertThat(data.getQueueDatas().get(0).getWriteQueueNums()).isEqualTo(8); + assertThat(data.getQueueDatas().get(0).getPerm()).isEqualTo(6); + + registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker().name("AnotherBroker"), testTopic); + data = routeInfoManager.pickupTopicRouteData(testTopic); + + assertThat(data.getBrokerDatas().size()).isEqualTo(2); + assertThat(data.getQueueDatas().size()).isEqualTo(2); + + List brokerList = + Arrays.asList(data.getBrokerDatas().get(0).getBrokerName(), data.getBrokerDatas().get(1).getBrokerName()); + assertThat(brokerList).contains(DEFAULT_BROKER, "AnotherBroker"); + + brokerList = + Arrays.asList(data.getQueueDatas().get(0).getBrokerName(), data.getQueueDatas().get(1).getBrokerName()); + assertThat(brokerList).contains(DEFAULT_BROKER, "AnotherBroker"); + } + + @Test + public void pickupTopicRouteDataWithSlave() { + String testTopic = "TestTopic"; + registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker(), testTopic); + registerBrokerWithNormalTopic(BrokerBasicInfo.slaveBroker(), testTopic); + + TopicRouteData routeData = routeInfoManager.pickupTopicRouteData(testTopic); + + assertThat(routeData.getBrokerDatas().get(0).getBrokerAddrs()).hasSize(2); + assertThat(PermName.isWriteable(routeData.getQueueDatas().get(0).getPerm())).isTrue(); + + routeInfoManager.unRegisterBroker(Sets.newHashSet(BrokerBasicInfo.defaultBroker().unRegisterRequest())); + routeData = routeInfoManager.pickupTopicRouteData(testTopic); + + assertThat(routeData.getBrokerDatas().get(0).getBrokerAddrs()).hasSize(1); + assertThat(PermName.isWriteable(routeData.getQueueDatas().get(0).getPerm())).isFalse(); + + registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker(), testTopic); + routeData = routeInfoManager.pickupTopicRouteData(testTopic); + + assertThat(routeData.getBrokerDatas().get(0).getBrokerAddrs()).hasSize(2); + assertThat(PermName.isWriteable(routeData.getQueueDatas().get(0).getPerm())).isTrue(); + } + + @Test + public void scanNotActiveBroker() { + registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker(), "TestTopic"); + routeInfoManager.scanNotActiveBroker(); + } + + @Test + public void pickupPartitionOrderTopicRouteData() { + String orderTopic = "PartitionOrderTopicTest"; + + // Case 1: Register global order topic with slave + registerBrokerWithOrderTopic(BrokerBasicInfo.slaveBroker(), orderTopic); + + TopicRouteData orderRoute = routeInfoManager.pickupTopicRouteData(orderTopic); + + // Acting master check + assertThat(orderRoute.getBrokerDatas().get(0).getBrokerAddrs()) + .containsOnlyKeys(MixAll.MASTER_ID); + assertThat(orderRoute.getBrokerDatas().get(0).getBrokerAddrs()) + .containsValue(BrokerBasicInfo.slaveBroker().brokerAddr); + assertThat(PermName.isWriteable(orderRoute.getQueueDatas().get(0).getPerm())).isFalse(); + + routeInfoManager.unRegisterBroker(Sets.newHashSet(BrokerBasicInfo.slaveBroker().unRegisterRequest())); + + // Case 2: Register global order topic with master and slave, then unregister master + + registerBrokerWithOrderTopic(BrokerBasicInfo.slaveBroker(), orderTopic); + registerBrokerWithOrderTopic(BrokerBasicInfo.defaultBroker(), orderTopic); + routeInfoManager.unRegisterBroker(Sets.newHashSet(BrokerBasicInfo.defaultBroker().unRegisterRequest())); + + orderRoute = routeInfoManager.pickupTopicRouteData(orderTopic); + + // Acting master check + assertThat(orderRoute.getBrokerDatas().get(0).getBrokerAddrs()) + .containsOnlyKeys(MixAll.MASTER_ID); + assertThat(orderRoute.getBrokerDatas().get(0).getBrokerAddrs()) + .containsValue(BrokerBasicInfo.slaveBroker().brokerAddr); + assertThat(PermName.isWriteable(orderRoute.getQueueDatas().get(0).getPerm())).isFalse(); + + routeInfoManager.unRegisterBroker(Sets.newHashSet(BrokerBasicInfo.slaveBroker().unRegisterRequest())); + + // Case 3: Register two broker groups, only one group enable acting master + registerBrokerWithOrderTopic(BrokerBasicInfo.slaveBroker(), orderTopic); + registerBrokerWithOrderTopic(BrokerBasicInfo.defaultBroker(), orderTopic); + + final BrokerBasicInfo master1 = BrokerBasicInfo.defaultBroker().name(DEFAULT_BROKER + "_ANOTHER"); + final BrokerBasicInfo slave1 = BrokerBasicInfo.slaveBroker().name(DEFAULT_BROKER + "_ANOTHER"); + + registerBrokerWithOrderTopic(master1, orderTopic); + registerBrokerWithOrderTopic(slave1, orderTopic); + + orderRoute = routeInfoManager.pickupTopicRouteData(orderTopic); + + assertThat(orderRoute.getBrokerDatas()).hasSize(2); + assertThat(orderRoute.getQueueDatas()).hasSize(2); + + routeInfoManager.unRegisterBroker(Sets.newHashSet(BrokerBasicInfo.defaultBroker().unRegisterRequest())); + orderRoute = routeInfoManager.pickupTopicRouteData(orderTopic); + + assertThat(orderRoute.getBrokerDatas()).hasSize(2); + assertThat(orderRoute.getQueueDatas()).hasSize(2); + + for (final BrokerData brokerData : orderRoute.getBrokerDatas()) { + if (brokerData.getBrokerAddrs().size() == 1) { + assertThat(brokerData.getBrokerAddrs()).containsOnlyKeys(MixAll.MASTER_ID); + assertThat(brokerData.getBrokerAddrs()).containsValue(BrokerBasicInfo.slaveBroker().brokerAddr); + } else if (brokerData.getBrokerAddrs().size() == 2) { + assertThat(brokerData.getBrokerAddrs()).containsKeys(MixAll.MASTER_ID, (long) slave1.brokerId); + assertThat(brokerData.getBrokerAddrs()).containsValues(master1.brokerAddr, slave1.brokerAddr); + } else { + throw new RuntimeException("Shouldn't reach here"); + } + } + } + + @Test + public void pickupGlobalOrderTopicRouteData() { + String orderTopic = "GlobalOrderTopicTest"; + + // Case 1: Register global order topic with slave + registerBrokerWithGlobalOrderTopic(BrokerBasicInfo.slaveBroker(), orderTopic); + + TopicRouteData orderRoute = routeInfoManager.pickupTopicRouteData(orderTopic); + + // Acting master check + assertThat(orderRoute.getBrokerDatas().get(0).getBrokerAddrs()) + .containsOnlyKeys(MixAll.MASTER_ID); + assertThat(orderRoute.getBrokerDatas().get(0).getBrokerAddrs()) + .containsValue(BrokerBasicInfo.slaveBroker().brokerAddr); + assertThat(PermName.isWriteable(orderRoute.getQueueDatas().get(0).getPerm())).isFalse(); + + routeInfoManager.unRegisterBroker(Sets.newHashSet(BrokerBasicInfo.slaveBroker().unRegisterRequest())); + + // Case 2: Register global order topic with master and slave, then unregister master + + registerBrokerWithGlobalOrderTopic(BrokerBasicInfo.slaveBroker(), orderTopic); + registerBrokerWithGlobalOrderTopic(BrokerBasicInfo.defaultBroker(), orderTopic); + routeInfoManager.unRegisterBroker(Sets.newHashSet(BrokerBasicInfo.defaultBroker().unRegisterRequest())); + + // Acting master check + assertThat(orderRoute.getBrokerDatas().get(0).getBrokerAddrs()) + .containsOnlyKeys(MixAll.MASTER_ID); + assertThat(orderRoute.getBrokerDatas().get(0).getBrokerAddrs()) + .containsValue(BrokerBasicInfo.slaveBroker().brokerAddr); + assertThat(PermName.isWriteable(orderRoute.getQueueDatas().get(0).getPerm())).isFalse(); + } + + @Test + public void registerOnlySlaveBroker() { + final String testTopic = "TestTopic"; + + // Case 1: Only slave broker + registerBrokerWithNormalTopic(BrokerBasicInfo.slaveBroker(), testTopic); + assertThat(routeInfoManager.pickupTopicRouteData(testTopic)).isNotNull(); + int topicPerm = routeInfoManager.pickupTopicRouteData(testTopic).getQueueDatas().get(0).getPerm(); + assertThat(PermName.isWriteable(topicPerm)).isFalse(); + routeInfoManager.unRegisterBroker(Sets.newHashSet(BrokerBasicInfo.slaveBroker().unRegisterRequest())); + + // Case 2: Register master, and slave, then unregister master, finally recover master + registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker(), testTopic); + registerBrokerWithNormalTopic(BrokerBasicInfo.slaveBroker(), testTopic); + + assertThat(routeInfoManager.pickupTopicRouteData(testTopic)).isNotNull(); + topicPerm = routeInfoManager.pickupTopicRouteData(testTopic).getQueueDatas().get(0).getPerm(); + assertThat(PermName.isWriteable(topicPerm)).isTrue(); + + routeInfoManager.unRegisterBroker(Sets.newHashSet(BrokerBasicInfo.defaultBroker().unRegisterRequest())); + assertThat(routeInfoManager.pickupTopicRouteData(testTopic)).isNotNull(); + topicPerm = routeInfoManager.pickupTopicRouteData(testTopic).getQueueDatas().get(0).getPerm(); + assertThat(PermName.isWriteable(topicPerm)).isFalse(); + + registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker(), testTopic); + assertThat(routeInfoManager.pickupTopicRouteData(testTopic)).isNotNull(); + topicPerm = routeInfoManager.pickupTopicRouteData(testTopic).getQueueDatas().get(0).getPerm(); + assertThat(PermName.isWriteable(topicPerm)).isTrue(); + } + + @Test + public void onChannelDestroy() { + Channel channel = mock(Channel.class); + + registerBroker(BrokerBasicInfo.defaultBroker(), channel, null, "TestTopic", "TestTopic1"); + routeInfoManager.onChannelDestroy(channel); + await().atMost(Duration.ofSeconds(5)).until(() -> routeInfoManager.blockedUnRegisterRequests() == 0); + + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic")).isNull(); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic1")).isNull(); + + final BrokerBasicInfo masterBroker = BrokerBasicInfo.defaultBroker().enableActingMaster(false); + final BrokerBasicInfo slaveBroker = BrokerBasicInfo.slaveBroker().enableActingMaster(false); + + Channel masterChannel = mock(Channel.class); + Channel slaveChannel = mock(Channel.class); + + registerBroker(masterBroker, masterChannel, null, "TestTopic"); + registerBroker(slaveBroker, slaveChannel, null, "TestTopic"); + + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().get(0).getBrokerAddrs()).containsKeys(0L, 1L); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().get(0).getBrokerAddrs()) + .containsValues(masterBroker.brokerAddr, slaveBroker.brokerAddr); + + routeInfoManager.onChannelDestroy(masterChannel); + await().atMost(Duration.ofSeconds(5)).until(() -> routeInfoManager.blockedUnRegisterRequests() == 0); + + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().get(0).getBrokerAddrs()).containsOnlyKeys(1L); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().get(0).getBrokerAddrs()) + .containsValues(slaveBroker.brokerAddr); + + routeInfoManager.onChannelDestroy(slaveChannel); + await().atMost(Duration.ofSeconds(5)).until(() -> routeInfoManager.blockedUnRegisterRequests() == 0); + + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic")).isNull(); + } + + @Test + public void switchBrokerRole_ChannelDestroy() { + final BrokerBasicInfo masterBroker = BrokerBasicInfo.defaultBroker().enableActingMaster(false); + final BrokerBasicInfo slaveBroker = BrokerBasicInfo.slaveBroker().enableActingMaster(false); + + Channel masterChannel = mock(Channel.class); + Channel slaveChannel = mock(Channel.class); + + registerBroker(masterBroker, masterChannel, null, "TestTopic"); + registerBroker(slaveBroker, slaveChannel, null, "TestTopic"); + + // Master Down + routeInfoManager.onChannelDestroy(masterChannel); + await().atMost(Duration.ofSeconds(5)).until(() -> routeInfoManager.blockedUnRegisterRequests() == 0); + + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().get(0).getBrokerAddrs()).containsOnlyKeys(1L); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().get(0).getBrokerAddrs()) + .containsValues(slaveBroker.brokerAddr); + + // Switch slave to master + slaveBroker.id(0).dataVersion.nextVersion(); + registerBrokerWithNormalTopic(slaveBroker, "TestTopic"); + + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().get(0).getBrokerAddrs()).containsOnlyKeys(0L); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().get(0).getBrokerAddrs()) + .containsValues(slaveBroker.brokerAddr); + + // Old master switch to slave + masterBroker.id(1).dataVersion.nextVersion(); + registerBrokerWithNormalTopic(masterBroker, "TestTopic"); + + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().get(0).getBrokerAddrs()).containsKeys(0L, 1L); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().get(0).getBrokerAddrs()) + .containsValues(BrokerBasicInfo.defaultBroker().brokerAddr, BrokerBasicInfo.slaveBroker().brokerAddr); + } + + @Test + public void keepTopicWithBrokerRegistration() { + RegisterBrokerResult masterResult = registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker(), "TestTopic", "TestTopic1"); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic")).isNotNull(); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic1")).isNotNull(); + + masterResult = registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker(), "TestTopic1"); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic")).isNotNull(); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic1")).isNotNull(); + } + + @Test + public void deleteTopicWithBrokerRegistration() { + config.setDeleteTopicWithBrokerRegistration(true); + registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker(), "TestTopic", "TestTopic1"); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic")).isNotNull(); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic1")).isNotNull(); + + registerBrokerWithNormalTopic(BrokerBasicInfo.defaultBroker(), "TestTopic1"); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic")).isNull(); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic1")).isNotNull(); + } + + @Test + public void deleteTopicWithBrokerRegistration2() { + // Register two brokers and delete a specific one by one + config.setDeleteTopicWithBrokerRegistration(true); + final BrokerBasicInfo master1 = BrokerBasicInfo.defaultBroker(); + final BrokerBasicInfo master2 = BrokerBasicInfo.defaultBroker().name(DEFAULT_BROKER + 1).addr(DEFAULT_ADDR + 9); + + registerBrokerWithNormalTopic(master1, "TestTopic", "TestTopic1"); + registerBrokerWithNormalTopic(master2, "TestTopic", "TestTopic1"); + + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas()).hasSize(2); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic1").getBrokerDatas()).hasSize(2); + + + registerBrokerWithNormalTopic(master1, "TestTopic1"); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas()).hasSize(1); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic").getBrokerDatas().get(0).getBrokerName()) + .isEqualTo(master2.brokerName); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic1").getBrokerDatas()).hasSize(2); + + registerBrokerWithNormalTopic(master2, "TestTopic1"); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic")).isNull(); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic1").getBrokerDatas()).hasSize(2); + } + + @Test + public void registerSingleTopicWithBrokerRegistration() { + config.setDeleteTopicWithBrokerRegistration(true); + final BrokerBasicInfo master1 = BrokerBasicInfo.defaultBroker(); + + registerSingleTopicWithBrokerName(master1.brokerName, "TestTopic"); + + // Single topic registration failed because there is no broker connection exists + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic")).isNull(); + + // Register broker with TestTopic first and then register single topic TestTopic1 + registerBrokerWithNormalTopic(master1, "TestTopic"); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic")).isNotNull(); + + registerSingleTopicWithBrokerName(master1.brokerName, "TestTopic1"); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic1")).isNotNull(); + + // Register the two topics to keep the route info + registerBrokerWithNormalTopic(master1, "TestTopic", "TestTopic1"); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic")).isNotNull(); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic1")).isNotNull(); + + // Cancel the TestTopic1 with broker registration + registerBrokerWithNormalTopic(master1, "TestTopic"); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic")).isNotNull(); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic1")).isNull(); + + // Add TestTopic1 and cancel all the topics with broker un-registration + registerSingleTopicWithBrokerName(master1.brokerName, "TestTopic1"); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic1")).isNotNull(); + + routeInfoManager.unregisterBroker(master1.clusterName, master1.brokerAddr, master1.brokerName, 0); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic")).isNull(); + assertThat(routeInfoManager.pickupTopicRouteData("TestTopic1")).isNull(); + + + } + + private RegisterBrokerResult registerBrokerWithNormalTopic(BrokerBasicInfo brokerInfo, String... topics) { + ConcurrentHashMap topicConfigConcurrentHashMap = new ConcurrentHashMap<>(); + TopicConfig baseTopic = new TopicConfig("baseTopic"); + topicConfigConcurrentHashMap.put(baseTopic.getTopicName(), baseTopic); + for (final String topic : topics) { + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setWriteQueueNums(8); + topicConfig.setTopicName(topic); + topicConfig.setPerm(6); + topicConfig.setReadQueueNums(8); + topicConfig.setOrder(false); + topicConfigConcurrentHashMap.put(topic, topicConfig); + } + + return registerBroker(brokerInfo, mock(Channel.class), topicConfigConcurrentHashMap, topics); + } + + private RegisterBrokerResult registerBrokerWithOrderTopic(BrokerBasicInfo brokerBasicInfo, String... topics) { + ConcurrentHashMap topicConfigConcurrentHashMap = new ConcurrentHashMap<>(); + + TopicConfig baseTopic = new TopicConfig("baseTopic"); + baseTopic.setOrder(true); + topicConfigConcurrentHashMap.put(baseTopic.getTopicName(), baseTopic); + for (final String topic : topics) { + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setWriteQueueNums(8); + topicConfig.setTopicName(topic); + topicConfig.setPerm(6); + topicConfig.setReadQueueNums(8); + topicConfig.setOrder(true); + topicConfigConcurrentHashMap.put(topic, topicConfig); + } + return registerBroker(brokerBasicInfo, mock(Channel.class), topicConfigConcurrentHashMap, topics); + } + + private RegisterBrokerResult registerBrokerWithGlobalOrderTopic(BrokerBasicInfo brokerBasicInfo, String... topics) { + ConcurrentHashMap topicConfigConcurrentHashMap = new ConcurrentHashMap<>(); + TopicConfig baseTopic = new TopicConfig("baseTopic", 1, 1); + baseTopic.setOrder(true); + topicConfigConcurrentHashMap.put(baseTopic.getTopicName(), baseTopic); + for (final String topic : topics) { + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setWriteQueueNums(1); + topicConfig.setTopicName(topic); + topicConfig.setPerm(6); + topicConfig.setReadQueueNums(1); + topicConfig.setOrder(true); + topicConfigConcurrentHashMap.put(topic, topicConfig); + } + return registerBroker(brokerBasicInfo, mock(Channel.class), topicConfigConcurrentHashMap, topics); + } + + private RegisterBrokerResult registerBroker(BrokerBasicInfo brokerInfo, Channel channel, + ConcurrentMap topicConfigConcurrentHashMap, String... topics) { + + if (topicConfigConcurrentHashMap == null) { + topicConfigConcurrentHashMap = new ConcurrentHashMap<>(); + TopicConfig baseTopic = new TopicConfig("baseTopic"); + topicConfigConcurrentHashMap.put(baseTopic.getTopicName(), baseTopic); + for (final String topic : topics) { + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setWriteQueueNums(8); + topicConfig.setTopicName(topic); + topicConfig.setPerm(6); + topicConfig.setReadQueueNums(8); + topicConfig.setOrder(false); + topicConfigConcurrentHashMap.put(topic, topicConfig); + } + } + + TopicConfigSerializeWrapper topicConfigSerializeWrapper = new TopicConfigSerializeWrapper(); + topicConfigSerializeWrapper.setDataVersion(brokerInfo.dataVersion); + topicConfigSerializeWrapper.setTopicConfigTable(topicConfigConcurrentHashMap); + + RegisterBrokerResult registerBrokerResult = routeInfoManager.registerBroker( + brokerInfo.clusterName, + brokerInfo.brokerAddr, + brokerInfo.brokerName, + brokerInfo.brokerId, + brokerInfo.haAddr, + "", + null, + brokerInfo.enableActingMaster, + topicConfigSerializeWrapper, new ArrayList<>(), channel); + return registerBrokerResult; + } + + private void registerSingleTopicWithBrokerName(String brokerName, String... topics) { + for (final String topic : topics) { + QueueData queueData = new QueueData(); + queueData.setBrokerName(brokerName); + queueData.setReadQueueNums(8); + queueData.setWriteQueueNums(8); + queueData.setPerm(6); + routeInfoManager.registerTopic(topic, Collections.singletonList(queueData)); + } + } + + static class BrokerBasicInfo { + String clusterName; + String brokerName; + String brokerAddr; + String haAddr; + int brokerId; + boolean enableActingMaster; + + DataVersion dataVersion; + + static BrokerBasicInfo defaultBroker() { + BrokerBasicInfo basicInfo = new BrokerBasicInfo(); + DataVersion dataVersion = new DataVersion(); + dataVersion.setCounter(new AtomicLong(1)); + dataVersion.setTimestamp(System.currentTimeMillis()); + basicInfo.dataVersion = dataVersion; + + return basicInfo.id(0) + .name(DEFAULT_BROKER) + .cluster(DEFAULT_CLUSTER) + .addr(DEFAULT_ADDR) + .haAddr(DEFAULT_ADDR_PREFIX + "20911") + .enableActingMaster(true); + } + + UnRegisterBrokerRequestHeader unRegisterRequest() { + UnRegisterBrokerRequestHeader unRegisterBrokerRequest = new UnRegisterBrokerRequestHeader(); + unRegisterBrokerRequest.setBrokerAddr(brokerAddr); + unRegisterBrokerRequest.setBrokerName(brokerName); + unRegisterBrokerRequest.setClusterName(clusterName); + unRegisterBrokerRequest.setBrokerId((long) brokerId); + return unRegisterBrokerRequest; + } + + static BrokerBasicInfo slaveBroker() { + final BrokerBasicInfo slaveBroker = defaultBroker(); + return slaveBroker + .id(1) + .addr(DEFAULT_ADDR_PREFIX + "30911") + .haAddr(DEFAULT_ADDR_PREFIX + "40911") + .enableActingMaster(true); + } + + BrokerBasicInfo name(String name) { + this.brokerName = name; + return this; + } + + BrokerBasicInfo cluster(String name) { + this.clusterName = name; + return this; + } + + BrokerBasicInfo addr(String addr) { + this.brokerAddr = addr; + return this; + } + + BrokerBasicInfo id(int id) { + this.brokerId = id; + return this; + } + + BrokerBasicInfo haAddr(String addr) { + this.haAddr = addr; + return this; + } + + BrokerBasicInfo enableActingMaster(boolean enableActingMaster) { + this.enableActingMaster = enableActingMaster; + return this; + } + } + +} diff --git a/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManagerStaticRegisterTest.java b/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManagerStaticRegisterTest.java new file mode 100644 index 00000000000..6c90e7b710b --- /dev/null +++ b/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManagerStaticRegisterTest.java @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.namesrv.routeinfo; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.namesrv.NamesrvConfig; +import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.body.TopicList; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class RouteInfoManagerStaticRegisterTest extends RouteInfoManagerTestBase { + private static RouteInfoManager routeInfoManager; + public static String clusterName = "cluster"; + public static String brokerPrefix = "broker"; + public static String topicPrefix = "topic"; + + public static RouteInfoManagerTestBase.Cluster cluster; + + @Before + public void setup() { + routeInfoManager = new RouteInfoManager(new NamesrvConfig(), null); + cluster = registerCluster(routeInfoManager, + clusterName, + brokerPrefix, + 3, + 3, + topicPrefix, + 10); + } + + @After + public void terminate() { + routeInfoManager.printAllPeriodically(); + + for (BrokerData bd : cluster.brokerDataMap.values()) { + unregisterBrokerAll(routeInfoManager, bd); + } + } + + @Test + public void testGetAllClusterInfo() { + ClusterInfo clusterInfo = routeInfoManager.getAllClusterInfo(); + Map> clusterAddrTable = clusterInfo.getClusterAddrTable(); + + assertEquals(1, clusterAddrTable.size()); + assertEquals(cluster.getAllBrokerName(), clusterAddrTable.get(clusterName)); + } + + @Test + public void testGetAllTopicList() { + TopicList topicInfo = routeInfoManager.getAllTopicList(); + + assertEquals(cluster.getAllTopicName(), topicInfo.getTopicList()); + } + + @Test + public void testGetTopicsByCluster() { + TopicList topicList = routeInfoManager.getTopicsByCluster(clusterName); + assertEquals(cluster.getAllTopicName(), topicList.getTopicList()); + } + + @Test + public void testPickupTopicRouteData() { + String topic = getTopicName(topicPrefix, 0); + + TopicRouteData topicRouteData = routeInfoManager.pickupTopicRouteData(topic); + + TopicConfig topicConfig = cluster.topicConfig.get(topic); + + // check broker data + Collections.sort(topicRouteData.getBrokerDatas()); + List ans = new ArrayList<>(cluster.brokerDataMap.values()); + Collections.sort(ans); + + assertEquals(topicRouteData.getBrokerDatas(), ans); + + // check queue data + HashSet allBrokerNameInQueueData = new HashSet<>(); + + for (QueueData queueData : topicRouteData.getQueueDatas()) { + allBrokerNameInQueueData.add(queueData.getBrokerName()); + + assertEquals(queueData.getWriteQueueNums(), topicConfig.getWriteQueueNums()); + assertEquals(queueData.getReadQueueNums(), topicConfig.getReadQueueNums()); + assertEquals(queueData.getPerm(), topicConfig.getPerm()); + assertEquals(queueData.getTopicSysFlag(), topicConfig.getTopicSysFlag()); + } + + assertEquals(allBrokerNameInQueueData, new HashSet<>(cluster.getAllBrokerName())); + } + + @Test + public void testDeleteTopic() { + String topic = getTopicName(topicPrefix, 0); + routeInfoManager.deleteTopic(topic); + + assertNull(routeInfoManager.pickupTopicRouteData(topic)); + } + + @Test + public void testGetSystemTopicList() { + TopicList topicList = routeInfoManager.getSystemTopicList(); + assertThat(topicList).isNotNull(); + } + + @Test + public void testGetUnitTopics() { + TopicList topicList = routeInfoManager.getUnitTopics(); + assertThat(topicList).isNotNull(); + } + + @Test + public void testGetHasUnitSubTopicList() { + TopicList topicList = routeInfoManager.getHasUnitSubTopicList(); + assertThat(topicList).isNotNull(); + } + + @Test + public void testGetHasUnitSubUnUnitTopicList() { + TopicList topicList = routeInfoManager.getHasUnitSubUnUnitTopicList(); + assertThat(topicList).isNotNull(); + } + +} diff --git a/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManagerTest.java b/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManagerTest.java index 5ab77be1ebe..d9ac9e49461 100644 --- a/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManagerTest.java +++ b/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManagerTest.java @@ -17,12 +17,20 @@ package org.apache.rocketmq.namesrv.routeinfo; import io.netty.channel.Channel; +import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; import org.apache.rocketmq.common.TopicConfig; -import org.apache.rocketmq.common.namesrv.RegisterBrokerResult; -import org.apache.rocketmq.common.protocol.body.TopicConfigSerializeWrapper; -import org.apache.rocketmq.common.protocol.route.TopicRouteData; +import org.apache.rocketmq.common.constant.PermName; +import org.apache.rocketmq.common.namesrv.NamesrvConfig; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.namesrv.RegisterBrokerResult; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -37,51 +45,113 @@ public class RouteInfoManagerTest { @Before public void setup() { - routeInfoManager = new RouteInfoManager(); + routeInfoManager = new RouteInfoManager(new NamesrvConfig(), null); + routeInfoManager.start(); testRegisterBroker(); } @After public void terminate() { + routeInfoManager.shutdown(); routeInfoManager.printAllPeriodically(); routeInfoManager.unregisterBroker("default-cluster", "127.0.0.1:10911", "default-broker", 1234); } @Test public void testGetAllClusterInfo() { - byte[] clusterInfo = routeInfoManager.getAllClusterInfo(); + byte[] clusterInfo = routeInfoManager.getAllClusterInfo().encode(); assertThat(clusterInfo).isNotNull(); } + @Test + public void testQueryBrokerTopicConfig() { + { + DataVersion targetVersion = new DataVersion(); + targetVersion.setCounter(new AtomicLong(10L)); + targetVersion.setTimestamp(100L); + + DataVersion dataVersion = routeInfoManager.queryBrokerTopicConfig("default-cluster", "127.0.0.1:10911"); + assertThat(dataVersion.equals(targetVersion)).isTrue(); + } + + { + // register broker default-cluster-1 with the same addr, then test + DataVersion targetVersion = new DataVersion(); + targetVersion.setCounter(new AtomicLong(20L)); + targetVersion.setTimestamp(200L); + + ConcurrentHashMap topicConfigConcurrentHashMap = new ConcurrentHashMap<>(); + topicConfigConcurrentHashMap.put("unit-test-0", new TopicConfig("unit-test-0")); + topicConfigConcurrentHashMap.put("unit-test-1", new TopicConfig("unit-test-1")); + + TopicConfigSerializeWrapper topicConfigSerializeWrapper = new TopicConfigSerializeWrapper(); + topicConfigSerializeWrapper.setDataVersion(targetVersion); + topicConfigSerializeWrapper.setTopicConfigTable(topicConfigConcurrentHashMap); + Channel channel = mock(Channel.class); + RegisterBrokerResult registerBrokerResult = routeInfoManager.registerBroker("default-cluster-1", "127.0.0.1:10911", "default-broker-1", 1234, "127.0.0.1:1001", "", + null, topicConfigSerializeWrapper, new ArrayList<>(), channel); + assertThat(registerBrokerResult).isNotNull(); + + DataVersion dataVersion0 = routeInfoManager.queryBrokerTopicConfig("default-cluster", "127.0.0.1:10911"); + assertThat(targetVersion.equals(dataVersion0)).isFalse(); + + DataVersion dataVersion1 = routeInfoManager.queryBrokerTopicConfig("default-cluster-1", "127.0.0.1:10911"); + assertThat(targetVersion.equals(dataVersion1)).isTrue(); + } + + // unregister broker default-cluster-1, then test + { + routeInfoManager.unregisterBroker("default-cluster-1", "127.0.0.1:10911", "default-broker-1", 1234); + assertThat(null != routeInfoManager.queryBrokerTopicConfig("default-cluster", "127.0.0.1:10911")).isTrue(); + assertThat(null == routeInfoManager.queryBrokerTopicConfig("default-cluster-1", "127.0.0.1:10911")).isTrue(); + } + } + @Test public void testGetAllTopicList() { - byte[] topicInfo = routeInfoManager.getAllTopicList(); + byte[] topicInfo = routeInfoManager.getAllTopicList().encode(); Assert.assertTrue(topicInfo != null); assertThat(topicInfo).isNotNull(); } @Test public void testRegisterBroker() { - TopicConfigSerializeWrapper topicConfigSerializeWrapper = new TopicConfigSerializeWrapper(); + DataVersion dataVersion = new DataVersion(); + dataVersion.setCounter(new AtomicLong(10L)); + dataVersion.setTimestamp(100L); + ConcurrentHashMap topicConfigConcurrentHashMap = new ConcurrentHashMap<>(); - TopicConfig topicConfig = new TopicConfig(); - topicConfig.setWriteQueueNums(8); - topicConfig.setTopicName("unit-test"); - topicConfig.setPerm(6); - topicConfig.setReadQueueNums(8); - topicConfig.setOrder(false); - topicConfigConcurrentHashMap.put("unit-test", topicConfig); + topicConfigConcurrentHashMap.put("unit-test0", new TopicConfig("unit-test0")); + topicConfigConcurrentHashMap.put("unit-test1", new TopicConfig("unit-test1")); + + TopicConfigSerializeWrapper topicConfigSerializeWrapper = new TopicConfigSerializeWrapper(); + topicConfigSerializeWrapper.setDataVersion(dataVersion); topicConfigSerializeWrapper.setTopicConfigTable(topicConfigConcurrentHashMap); Channel channel = mock(Channel.class); - RegisterBrokerResult registerBrokerResult = routeInfoManager.registerBroker("default-cluster", "127.0.0.1:10911", "default-broker", 1234, "127.0.0.1:1001", - topicConfigSerializeWrapper, new ArrayList(), channel); + RegisterBrokerResult registerBrokerResult = routeInfoManager.registerBroker("default-cluster", "127.0.0.1:10911", "default-broker", 1234, "127.0.0.1:1001", "", + null, topicConfigSerializeWrapper, new ArrayList<>(), channel); assertThat(registerBrokerResult).isNotNull(); } @Test - public void testWipeWritePermOfBrokerByLock() { - int result = routeInfoManager.wipeWritePermOfBrokerByLock("default-broker"); - assertThat(result).isEqualTo(0); + public void testWipeWritePermOfBrokerByLock() throws Exception { + Map qdMap = new HashMap<>(); + + QueueData qd = new QueueData(); + qd.setPerm(PermName.PERM_READ | PermName.PERM_WRITE); + qd.setBrokerName("broker-a"); + qdMap.put("broker-a",qd); + HashMap> topicQueueTable = new HashMap<>(); + topicQueueTable.put("topic-a", qdMap); + + Field filed = RouteInfoManager.class.getDeclaredField("topicQueueTable"); + filed.setAccessible(true); + filed.set(routeInfoManager, topicQueueTable); + + int addTopicCnt = routeInfoManager.wipeWritePermOfBrokerByLock("broker-a"); + assertThat(addTopicCnt).isEqualTo(1); + assertThat(qd.getPerm()).isEqualTo(PermName.PERM_READ); + } @Test @@ -92,31 +162,51 @@ public void testPickupTopicRouteData() { @Test public void testGetSystemTopicList() { - byte[] topicList = routeInfoManager.getSystemTopicList(); + byte[] topicList = routeInfoManager.getSystemTopicList().encode(); assertThat(topicList).isNotNull(); } @Test public void testGetTopicsByCluster() { - byte[] topicList = routeInfoManager.getTopicsByCluster("default-cluster"); + byte[] topicList = routeInfoManager.getTopicsByCluster("default-cluster").encode(); assertThat(topicList).isNotNull(); } @Test public void testGetUnitTopics() { - byte[] topicList = routeInfoManager.getUnitTopics(); + byte[] topicList = routeInfoManager.getUnitTopics().encode(); assertThat(topicList).isNotNull(); } @Test public void testGetHasUnitSubTopicList() { - byte[] topicList = routeInfoManager.getHasUnitSubTopicList(); + byte[] topicList = routeInfoManager.getHasUnitSubTopicList().encode(); assertThat(topicList).isNotNull(); } @Test public void testGetHasUnitSubUnUnitTopicList() { - byte[] topicList = routeInfoManager.getHasUnitSubUnUnitTopicList(); + byte[] topicList = routeInfoManager.getHasUnitSubUnUnitTopicList().encode(); assertThat(topicList).isNotNull(); } -} \ No newline at end of file + + @Test + public void testAddWritePermOfBrokerByLock() throws Exception { + Map qdMap = new HashMap<>(); + QueueData qd = new QueueData(); + qd.setPerm(PermName.PERM_READ); + qd.setBrokerName("broker-a"); + qdMap.put("broker-a",qd); + HashMap> topicQueueTable = new HashMap<>(); + topicQueueTable.put("topic-a", qdMap); + + Field filed = RouteInfoManager.class.getDeclaredField("topicQueueTable"); + filed.setAccessible(true); + filed.set(routeInfoManager, topicQueueTable); + + int addTopicCnt = routeInfoManager.addWritePermOfBrokerByLock("broker-a"); + assertThat(addTopicCnt).isEqualTo(1); + assertThat(qd.getPerm()).isEqualTo(PermName.PERM_READ | PermName.PERM_WRITE); + + } +} diff --git a/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManagerTestBase.java b/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManagerTestBase.java new file mode 100644 index 00000000000..a5faeea9dda --- /dev/null +++ b/namesrv/src/test/java/org/apache/rocketmq/namesrv/routeinfo/RouteInfoManagerTestBase.java @@ -0,0 +1,189 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.namesrv.routeinfo; + +import io.netty.channel.Channel; +import io.netty.channel.embedded.EmbeddedChannel; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigSerializeWrapper; +import org.apache.rocketmq.remoting.protocol.namesrv.RegisterBrokerResult; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; + +public class RouteInfoManagerTestBase { + + protected static class Cluster { + ConcurrentMap topicConfig; + Map brokerDataMap; + + public Cluster(ConcurrentMap topicConfig, Map brokerData) { + this.topicConfig = topicConfig; + this.brokerDataMap = brokerData; + } + + public Set getAllBrokerName() { + return brokerDataMap.keySet(); + } + + public Set getAllTopicName() { + return topicConfig.keySet(); + } + } + + protected Cluster registerCluster(RouteInfoManager routeInfoManager, String cluster, + String brokerNamePrefix, + int brokerNameNumber, + int brokerPerName, + String topicPrefix, + int topicNumber) { + + Map brokerDataMap = new HashMap<>(); + + // no filterServer address + List filterServerAddr = new ArrayList<>(); + + ConcurrentMap topicConfig = genTopicConfig(topicPrefix, topicNumber); + + for (int i = 0; i < brokerNameNumber; i++) { + String brokerName = getBrokerName(brokerNamePrefix, i); + + BrokerData brokerData = genBrokerData(cluster, brokerName, brokerPerName, true); + + // avoid object reference copy + ConcurrentMap topicConfigForBroker = genTopicConfig(topicPrefix, topicNumber); + + registerBrokerWithTopicConfig(routeInfoManager, brokerData, topicConfigForBroker, filterServerAddr); + + // avoid object reference copy + brokerDataMap.put(brokerData.getBrokerName(), genBrokerData(cluster, brokerName, brokerPerName, true)); + } + + return new Cluster(topicConfig, brokerDataMap); + } + + protected String getBrokerAddr(String cluster, String brokerName, long brokerNumber) { + return cluster + "-" + brokerName + ":" + brokerNumber; + } + + protected BrokerData genBrokerData(String clusterName, String brokerName, long totalBrokerNumber, boolean hasMaster) { + HashMap brokerAddrMap = new HashMap<>(); + + long startId = 0; + if (hasMaster) { + brokerAddrMap.put(MixAll.MASTER_ID, getBrokerAddr(clusterName, brokerName, MixAll.MASTER_ID)); + startId = 1; + } + + for (long i = startId; i < totalBrokerNumber; i++) { + brokerAddrMap.put(i, getBrokerAddr(clusterName, brokerName, i)); + } + + return new BrokerData(clusterName, brokerName, brokerAddrMap); + } + + protected void registerBrokerWithTopicConfig(RouteInfoManager routeInfoManager, BrokerData brokerData, + ConcurrentMap topicConfigTable, + List filterServerAddr) { + + brokerData.getBrokerAddrs().forEach((brokerId, brokerAddr) -> { + registerBrokerWithTopicConfig(routeInfoManager, brokerData.getCluster(), + brokerAddr, + brokerData.getBrokerName(), + brokerId, + brokerAddr, // set ha server address the same as brokerAddr + new ConcurrentHashMap<>(topicConfigTable), + new ArrayList<>(filterServerAddr)); + }); + } + + protected void unregisterBrokerAll(RouteInfoManager routeInfoManager, BrokerData brokerData) { + for (Map.Entry entry : brokerData.getBrokerAddrs().entrySet()) { + routeInfoManager.unregisterBroker(brokerData.getCluster(), entry.getValue(), brokerData.getBrokerName(), entry.getKey()); + } + } + + protected void unregisterBroker(RouteInfoManager routeInfoManager, BrokerData brokerData, long brokerId) { + HashMap brokerAddrs = brokerData.getBrokerAddrs(); + if (brokerAddrs.containsKey(brokerId)) { + String address = brokerAddrs.remove(brokerId); + routeInfoManager.unregisterBroker(brokerData.getCluster(), address, brokerData.getBrokerName(), brokerId); + } + } + + protected RegisterBrokerResult registerBrokerWithTopicConfig(RouteInfoManager routeInfoManager, String clusterName, + String brokerAddr, + String brokerName, + long brokerId, + String haServerAddr, + ConcurrentMap topicConfigTable, + List filterServerAddr) { + + TopicConfigSerializeWrapper topicConfigSerializeWrapper = new TopicConfigSerializeWrapper(); + topicConfigSerializeWrapper.setTopicConfigTable(topicConfigTable); + + Channel channel = new EmbeddedChannel(); + return routeInfoManager.registerBroker(clusterName, + brokerAddr, + brokerName, + brokerId, + "", + haServerAddr, + null, + topicConfigSerializeWrapper, + filterServerAddr, + channel); + } + + + protected String getTopicName(String topicPrefix, int topicNumber) { + return topicPrefix + "-" + topicNumber; + } + + protected ConcurrentMap genTopicConfig(String topicPrefix, int topicNumber) { + ConcurrentMap topicConfigMap = new ConcurrentHashMap<>(); + + for (int i = 0; i < topicNumber; i++) { + String topicName = getTopicName(topicPrefix, i); + + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setWriteQueueNums(8); + topicConfig.setTopicName(topicName); + topicConfig.setPerm(6); + topicConfig.setReadQueueNums(8); + topicConfig.setOrder(false); + topicConfigMap.put(topicName, topicConfig); + } + + return topicConfigMap; + } + + protected String getBrokerName(String brokerNamePrefix, long brokerNameNumber) { + return brokerNamePrefix + "-" + brokerNameNumber; + } + + protected BrokerData findBrokerDataByBrokerName(List data, String brokerName) { + return data.stream().filter(bd -> bd.getBrokerName().equals(brokerName)).findFirst().orElse(null); + } + +} diff --git a/namesrv/src/test/resources/rmq.logback-test.xml b/namesrv/src/test/resources/rmq.logback-test.xml new file mode 100644 index 00000000000..8695d52d57c --- /dev/null +++ b/namesrv/src/test/resources/rmq.logback-test.xml @@ -0,0 +1,36 @@ + + + + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + \ No newline at end of file diff --git a/openmessaging/pom.xml b/openmessaging/pom.xml index 56246f8665b..fee41bfaa23 100644 --- a/openmessaging/pom.xml +++ b/openmessaging/pom.xml @@ -20,21 +20,26 @@ rocketmq-all org.apache.rocketmq - 4.2.0 + 5.2.0 - 4.0.0 + 4.0.0 rocketmq-openmessaging rocketmq-openmessaging ${project.version} + + ${basedir}/.. + + io.openmessaging openmessaging-api - org.apache.rocketmq + ${project.groupId} rocketmq-client + ${project.version} \ No newline at end of file diff --git a/openmessaging/src/main/java/io/openmessaging/rocketmq/MessagingAccessPointImpl.java b/openmessaging/src/main/java/io/openmessaging/rocketmq/MessagingAccessPointImpl.java index 65caf84084c..51388f9c610 100644 --- a/openmessaging/src/main/java/io/openmessaging/rocketmq/MessagingAccessPointImpl.java +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/MessagingAccessPointImpl.java @@ -16,24 +16,21 @@ */ package io.openmessaging.rocketmq; -import io.openmessaging.IterableConsumer; import io.openmessaging.KeyValue; import io.openmessaging.MessagingAccessPoint; -import io.openmessaging.Producer; -import io.openmessaging.PullConsumer; -import io.openmessaging.PushConsumer; import io.openmessaging.ResourceManager; -import io.openmessaging.SequenceProducer; -import io.openmessaging.ServiceEndPoint; +import io.openmessaging.consumer.PullConsumer; +import io.openmessaging.consumer.PushConsumer; +import io.openmessaging.consumer.StreamingConsumer; import io.openmessaging.exception.OMSNotSupportedException; -import io.openmessaging.observer.Observer; +import io.openmessaging.producer.Producer; import io.openmessaging.rocketmq.consumer.PullConsumerImpl; import io.openmessaging.rocketmq.consumer.PushConsumerImpl; import io.openmessaging.rocketmq.producer.ProducerImpl; -import io.openmessaging.rocketmq.producer.SequenceProducerImpl; import io.openmessaging.rocketmq.utils.OMSUtil; public class MessagingAccessPointImpl implements MessagingAccessPoint { + private final KeyValue accessPointProperties; public MessagingAccessPointImpl(final KeyValue accessPointProperties) { @@ -41,10 +38,15 @@ public MessagingAccessPointImpl(final KeyValue accessPointProperties) { } @Override - public KeyValue properties() { + public KeyValue attributes() { return accessPointProperties; } + @Override + public String implVersion() { + return "0.3.0"; + } + @Override public Producer createProducer() { return new ProducerImpl(this.accessPointProperties); @@ -55,16 +57,6 @@ public Producer createProducer(KeyValue properties) { return new ProducerImpl(OMSUtil.buildKeyValue(this.accessPointProperties, properties)); } - @Override - public SequenceProducer createSequenceProducer() { - return new SequenceProducerImpl(this.accessPointProperties); - } - - @Override - public SequenceProducer createSequenceProducer(KeyValue properties) { - return new SequenceProducerImpl(OMSUtil.buildKeyValue(this.accessPointProperties, properties)); - } - @Override public PushConsumer createPushConsumer() { return new PushConsumerImpl(accessPointProperties); @@ -76,50 +68,30 @@ public PushConsumer createPushConsumer(KeyValue properties) { } @Override - public PullConsumer createPullConsumer(String queueName) { - return new PullConsumerImpl(queueName, accessPointProperties); + public PullConsumer createPullConsumer() { + return new PullConsumerImpl(accessPointProperties); } @Override - public PullConsumer createPullConsumer(String queueName, KeyValue properties) { - return new PullConsumerImpl(queueName, OMSUtil.buildKeyValue(this.accessPointProperties, properties)); + public PullConsumer createPullConsumer(KeyValue attributes) { + return new PullConsumerImpl(OMSUtil.buildKeyValue(this.accessPointProperties, attributes)); } @Override - public IterableConsumer createIterableConsumer(String queueName) { - throw new OMSNotSupportedException("-1", "IterableConsumer is not supported in current version"); + public StreamingConsumer createStreamingConsumer() { + return null; } @Override - public IterableConsumer createIterableConsumer(String queueName, KeyValue properties) { - throw new OMSNotSupportedException("-1", "IterableConsumer is not supported in current version"); + public StreamingConsumer createStreamingConsumer(KeyValue attributes) { + return null; } @Override - public ResourceManager getResourceManager() { + public ResourceManager resourceManager() { throw new OMSNotSupportedException("-1", "ResourceManager is not supported in current version."); } - @Override - public ServiceEndPoint createServiceEndPoint() { - throw new OMSNotSupportedException("-1", "ServiceEndPoint is not supported in current version."); - } - - @Override - public ServiceEndPoint createServiceEndPoint(KeyValue properties) { - throw new OMSNotSupportedException("-1", "ServiceEndPoint is not supported in current version."); - } - - @Override - public void addObserver(Observer observer) { - //Ignore - } - - @Override - public void deleteObserver(Observer observer) { - //Ignore - } - @Override public void startup() { //Ignore diff --git a/openmessaging/src/main/java/io/openmessaging/rocketmq/config/ClientConfig.java b/openmessaging/src/main/java/io/openmessaging/rocketmq/config/ClientConfig.java index 7077c6dc997..a5dfe49484a 100644 --- a/openmessaging/src/main/java/io/openmessaging/rocketmq/config/ClientConfig.java +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/config/ClientConfig.java @@ -16,20 +16,20 @@ */ package io.openmessaging.rocketmq.config; -import io.openmessaging.PropertyKeys; +import io.openmessaging.OMSBuiltinKeys; import io.openmessaging.rocketmq.domain.NonStandardKeys; -public class ClientConfig implements PropertyKeys, NonStandardKeys { - private String omsDriverImpl; - private String omsAccessPoints; - private String omsNamespace; - private String omsProducerId; - private String omsConsumerId; - private int omsOperationTimeout = 5000; - private String omsRoutingName; - private String omsOperatorName; - private String omsDstQueue; - private String omsSrcTopic; +public class ClientConfig implements OMSBuiltinKeys, NonStandardKeys { + private String driverImpl; + private String accessPoints; + private String namespace; + private String producerId; + private String consumerId; + private int operationTimeout = 5000; + private String region; + private String routingSource; + private String routingDestination; + private String routingExpression; private String rmqConsumerGroup; private String rmqProducerGroup = "__OMS_PRODUCER_DEFAULT_GROUP"; private int rmqMaxRedeliveryTimes = 16; @@ -40,84 +40,60 @@ public class ClientConfig implements PropertyKeys, NonStandardKeys { private int rmqPullMessageBatchNums = 32; private int rmqPullMessageCacheCapacity = 1000; - public String getOmsDriverImpl() { - return omsDriverImpl; + public String getDriverImpl() { + return driverImpl; } - public void setOmsDriverImpl(final String omsDriverImpl) { - this.omsDriverImpl = omsDriverImpl; + public void setDriverImpl(final String driverImpl) { + this.driverImpl = driverImpl; } - public String getOmsAccessPoints() { - return omsAccessPoints; + public String getAccessPoints() { + return accessPoints; } - public void setOmsAccessPoints(final String omsAccessPoints) { - this.omsAccessPoints = omsAccessPoints; + public void setAccessPoints(final String accessPoints) { + this.accessPoints = accessPoints; } - public String getOmsNamespace() { - return omsNamespace; + public String getNamespace() { + return namespace; } - public void setOmsNamespace(final String omsNamespace) { - this.omsNamespace = omsNamespace; + public void setNamespace(final String namespace) { + this.namespace = namespace; } - public String getOmsProducerId() { - return omsProducerId; + public String getProducerId() { + return producerId; } - public void setOmsProducerId(final String omsProducerId) { - this.omsProducerId = omsProducerId; + public void setProducerId(final String producerId) { + this.producerId = producerId; } - public String getOmsConsumerId() { - return omsConsumerId; + public String getConsumerId() { + return consumerId; } - public void setOmsConsumerId(final String omsConsumerId) { - this.omsConsumerId = omsConsumerId; + public void setConsumerId(final String consumerId) { + this.consumerId = consumerId; } - public int getOmsOperationTimeout() { - return omsOperationTimeout; + public int getOperationTimeout() { + return operationTimeout; } - public void setOmsOperationTimeout(final int omsOperationTimeout) { - this.omsOperationTimeout = omsOperationTimeout; + public void setOperationTimeout(final int operationTimeout) { + this.operationTimeout = operationTimeout; } - public String getOmsRoutingName() { - return omsRoutingName; + public String getRoutingSource() { + return routingSource; } - public void setOmsRoutingName(final String omsRoutingName) { - this.omsRoutingName = omsRoutingName; - } - - public String getOmsOperatorName() { - return omsOperatorName; - } - - public void setOmsOperatorName(final String omsOperatorName) { - this.omsOperatorName = omsOperatorName; - } - - public String getOmsDstQueue() { - return omsDstQueue; - } - - public void setOmsDstQueue(final String omsDstQueue) { - this.omsDstQueue = omsDstQueue; - } - - public String getOmsSrcTopic() { - return omsSrcTopic; - } - - public void setOmsSrcTopic(final String omsSrcTopic) { - this.omsSrcTopic = omsSrcTopic; + public void setRoutingSource(final String routingSource) { + this.routingSource = routingSource; } public String getRmqConsumerGroup() { @@ -191,4 +167,28 @@ public int getRmqPullMessageCacheCapacity() { public void setRmqPullMessageCacheCapacity(final int rmqPullMessageCacheCapacity) { this.rmqPullMessageCacheCapacity = rmqPullMessageCacheCapacity; } + + public String getRegion() { + return region; + } + + public void setRegion(String region) { + this.region = region; + } + + public String getRoutingDestination() { + return routingDestination; + } + + public void setRoutingDestination(String routingDestination) { + this.routingDestination = routingDestination; + } + + public String getRoutingExpression() { + return routingExpression; + } + + public void setRoutingExpression(String routingExpression) { + this.routingExpression = routingExpression; + } } diff --git a/openmessaging/src/main/java/io/openmessaging/rocketmq/consumer/LocalMessageCache.java b/openmessaging/src/main/java/io/openmessaging/rocketmq/consumer/LocalMessageCache.java index 90f9e03ed3d..3b2d0141c0b 100644 --- a/openmessaging/src/main/java/io/openmessaging/rocketmq/consumer/LocalMessageCache.java +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/consumer/LocalMessageCache.java @@ -17,7 +17,7 @@ package io.openmessaging.rocketmq.consumer; import io.openmessaging.KeyValue; -import io.openmessaging.PropertyKeys; +import io.openmessaging.Message; import io.openmessaging.ServiceLifecycle; import io.openmessaging.rocketmq.config.ClientConfig; import io.openmessaging.rocketmq.domain.ConsumeRequest; @@ -35,15 +35,17 @@ import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.impl.consumer.ProcessQueue; -import org.apache.rocketmq.client.log.ClientLogger; import org.apache.rocketmq.common.ThreadFactoryImpl; import org.apache.rocketmq.common.message.MessageAccessor; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageQueue; import org.apache.rocketmq.common.utils.ThreadUtils; -import org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; class LocalMessageCache implements ServiceLifecycle { + private static final Logger log = LoggerFactory.getLogger(LocalMessageCache.class); + private final BlockingQueue consumeRequestCache; private final Map consumedRequest; private final ConcurrentHashMap pullOffsetTable; @@ -51,8 +53,6 @@ class LocalMessageCache implements ServiceLifecycle { private final ClientConfig clientConfig; private final ScheduledExecutorService cleanExpireMsgExecutors; - private final static Logger log = ClientLogger.getLog(); - LocalMessageCache(final DefaultMQPullConsumer rocketmqPullConsumer, final ClientConfig clientConfig) { consumeRequestCache = new LinkedBlockingQueue<>(clientConfig.getRmqPullMessageCacheCapacity()); this.consumedRequest = new ConcurrentHashMap<>(); @@ -73,7 +73,7 @@ long nextPullOffset(MessageQueue remoteQueue) { pullOffsetTable.putIfAbsent(remoteQueue, rocketmqPullConsumer.fetchConsumeOffset(remoteQueue, false)); } catch (MQClientException e) { - log.error("A error occurred in fetch consume offset process.", e); + log.error("An error occurred in fetch consume offset process.", e); } } return pullOffsetTable.get(remoteQueue); @@ -91,13 +91,13 @@ void submitConsumeRequest(ConsumeRequest consumeRequest) { } MessageExt poll() { - return poll(clientConfig.getOmsOperationTimeout()); + return poll(clientConfig.getOperationTimeout()); } MessageExt poll(final KeyValue properties) { - int currentPollTimeout = clientConfig.getOmsOperationTimeout(); - if (properties.containsKey(PropertyKeys.OPERATION_TIMEOUT)) { - currentPollTimeout = properties.getInt(PropertyKeys.OPERATION_TIMEOUT); + int currentPollTimeout = clientConfig.getOperationTimeout(); + if (properties.containsKey(Message.BuiltinKeys.TIMEOUT)) { + currentPollTimeout = properties.getInt(Message.BuiltinKeys.TIMEOUT); } return poll(currentPollTimeout); } @@ -124,7 +124,7 @@ void ack(final String messageId) { try { rocketmqPullConsumer.updateConsumeOffset(consumeRequest.getMessageQueue(), offset); } catch (MQClientException e) { - log.error("A error occurred in update consume offset process.", e); + log.error("An error occurred in update consume offset process.", e); } } } @@ -135,7 +135,7 @@ void ack(final MessageQueue messageQueue, final ProcessQueue processQueue, final try { rocketmqPullConsumer.updateConsumeOffset(messageQueue, offset); } catch (MQClientException e) { - log.error("A error occurred in update consume offset process.", e); + log.error("An error occurred in update consume offset process.", e); } } diff --git a/openmessaging/src/main/java/io/openmessaging/rocketmq/consumer/PullConsumerImpl.java b/openmessaging/src/main/java/io/openmessaging/rocketmq/consumer/PullConsumerImpl.java index 8d396d43c5f..670d1abaac4 100644 --- a/openmessaging/src/main/java/io/openmessaging/rocketmq/consumer/PullConsumerImpl.java +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/consumer/PullConsumerImpl.java @@ -18,8 +18,8 @@ import io.openmessaging.KeyValue; import io.openmessaging.Message; -import io.openmessaging.PropertyKeys; -import io.openmessaging.PullConsumer; +import io.openmessaging.OMSBuiltinKeys; +import io.openmessaging.consumer.PullConsumer; import io.openmessaging.exception.OMSRuntimeException; import io.openmessaging.rocketmq.config.ClientConfig; import io.openmessaging.rocketmq.domain.ConsumeRequest; @@ -33,29 +33,27 @@ import org.apache.rocketmq.client.consumer.PullTaskContext; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.impl.consumer.ProcessQueue; -import org.apache.rocketmq.client.log.ClientLogger; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageQueue; -import org.slf4j.Logger; +import org.apache.rocketmq.remoting.protocol.LanguageCode; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; public class PullConsumerImpl implements PullConsumer { + private static final Logger log = LoggerFactory.getLogger(PullConsumerImpl.class); + private final DefaultMQPullConsumer rocketmqPullConsumer; private final KeyValue properties; private boolean started = false; - private String targetQueueName; private final MQPullConsumerScheduleService pullConsumerScheduleService; private final LocalMessageCache localMessageCache; private final ClientConfig clientConfig; - final static Logger log = ClientLogger.getLog(); - - public PullConsumerImpl(final String queueName, final KeyValue properties) { + public PullConsumerImpl(final KeyValue properties) { this.properties = properties; - this.targetQueueName = queueName; - this.clientConfig = BeanUtils.populate(properties, ClientConfig.class); - String consumerGroup = clientConfig.getRmqConsumerGroup(); + String consumerGroup = clientConfig.getConsumerId(); if (null == consumerGroup || consumerGroup.isEmpty()) { throw new OMSRuntimeException("-1", "Consumer Group is necessary for RocketMQ, please set it."); } @@ -63,11 +61,13 @@ public PullConsumerImpl(final String queueName, final KeyValue properties) { this.rocketmqPullConsumer = pullConsumerScheduleService.getDefaultMQPullConsumer(); - String accessPoints = clientConfig.getOmsAccessPoints(); - if (accessPoints == null || accessPoints.isEmpty()) { - throw new OMSRuntimeException("-1", "OMS AccessPoints is null or empty."); + if ("true".equalsIgnoreCase(System.getenv("OMS_RMQ_DIRECT_NAME_SRV"))) { + String accessPoints = clientConfig.getAccessPoints(); + if (accessPoints == null || accessPoints.isEmpty()) { + throw new OMSRuntimeException("-1", "OMS AccessPoints is null or empty."); + } + this.rocketmqPullConsumer.setNamesrvAddr(accessPoints.replace(',', ';')); } - this.rocketmqPullConsumer.setNamesrvAddr(accessPoints.replace(',', ';')); this.rocketmqPullConsumer.setConsumerGroup(consumerGroup); @@ -76,24 +76,44 @@ public PullConsumerImpl(final String queueName, final KeyValue properties) { String consumerId = OMSUtil.buildInstanceName(); this.rocketmqPullConsumer.setInstanceName(consumerId); - properties.put(PropertyKeys.CONSUMER_ID, consumerId); + properties.put(OMSBuiltinKeys.CONSUMER_ID, consumerId); + + this.rocketmqPullConsumer.setLanguage(LanguageCode.OMS); this.localMessageCache = new LocalMessageCache(this.rocketmqPullConsumer, clientConfig); } @Override - public KeyValue properties() { + public KeyValue attributes() { return properties; } @Override - public Message poll() { + public PullConsumer attachQueue(String queueName) { + registerPullTaskCallback(queueName); + return this; + } + + @Override + public PullConsumer attachQueue(String queueName, KeyValue attributes) { + registerPullTaskCallback(queueName); + return this; + } + + @Override + public PullConsumer detachQueue(String queueName) { + this.rocketmqPullConsumer.getRegisterTopics().remove(queueName); + return this; + } + + @Override + public Message receive() { MessageExt rmqMsg = localMessageCache.poll(); return rmqMsg == null ? null : OMSUtil.msgConvert(rmqMsg); } @Override - public Message poll(final KeyValue properties) { + public Message receive(final KeyValue properties) { MessageExt rmqMsg = localMessageCache.poll(properties); return rmqMsg == null ? null : OMSUtil.msgConvert(rmqMsg); } @@ -112,7 +132,6 @@ public void ack(final String messageId, final KeyValue properties) { public synchronized void startup() { if (!started) { try { - registerPullTaskCallback(); this.pullConsumerScheduleService.start(); this.localMessageCache.startup(); } catch (MQClientException e) { @@ -122,7 +141,7 @@ public synchronized void startup() { this.started = true; } - private void registerPullTaskCallback() { + private void registerPullTaskCallback(final String targetQueueName) { this.pullConsumerScheduleService.registerPullTaskCallback(targetQueueName, new PullTaskCallback() { @Override public void doPullTask(final MessageQueue mq, final PullTaskContext context) { @@ -148,7 +167,7 @@ public void doPullTask(final MessageQueue mq, final PullTaskContext context) { } localMessageCache.updatePullOffset(mq, pullResult.getNextBeginOffset()); } catch (Exception e) { - log.error("A error occurred in pull message process.", e); + log.error("An error occurred in pull message process.", e); } } }); diff --git a/openmessaging/src/main/java/io/openmessaging/rocketmq/consumer/PushConsumerImpl.java b/openmessaging/src/main/java/io/openmessaging/rocketmq/consumer/PushConsumerImpl.java index f9b8058e04a..1675a16f1ba 100644 --- a/openmessaging/src/main/java/io/openmessaging/rocketmq/consumer/PushConsumerImpl.java +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/consumer/PushConsumerImpl.java @@ -18,12 +18,12 @@ import io.openmessaging.BytesMessage; import io.openmessaging.KeyValue; -import io.openmessaging.MessageListener; import io.openmessaging.OMS; -import io.openmessaging.PropertyKeys; -import io.openmessaging.PushConsumer; -import io.openmessaging.ReceivedMessageContext; +import io.openmessaging.OMSBuiltinKeys; +import io.openmessaging.consumer.MessageListener; +import io.openmessaging.consumer.PushConsumer; import io.openmessaging.exception.OMSRuntimeException; +import io.openmessaging.interceptor.ConsumerInterceptor; import io.openmessaging.rocketmq.config.ClientConfig; import io.openmessaging.rocketmq.domain.NonStandardKeys; import io.openmessaging.rocketmq.utils.BeanUtils; @@ -39,6 +39,7 @@ import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.remoting.protocol.LanguageCode; public class PushConsumerImpl implements PushConsumer { private final DefaultMQPushConsumer rocketmqPushConsumer; @@ -52,13 +53,15 @@ public PushConsumerImpl(final KeyValue properties) { this.properties = properties; this.clientConfig = BeanUtils.populate(properties, ClientConfig.class); - String accessPoints = clientConfig.getOmsAccessPoints(); - if (accessPoints == null || accessPoints.isEmpty()) { - throw new OMSRuntimeException("-1", "OMS AccessPoints is null or empty."); + if ("true".equalsIgnoreCase(System.getenv("OMS_RMQ_DIRECT_NAME_SRV"))) { + String accessPoints = clientConfig.getAccessPoints(); + if (accessPoints == null || accessPoints.isEmpty()) { + throw new OMSRuntimeException("-1", "OMS AccessPoints is null or empty."); + } + this.rocketmqPushConsumer.setNamesrvAddr(accessPoints.replace(',', ';')); } - this.rocketmqPushConsumer.setNamesrvAddr(accessPoints.replace(',', ';')); - String consumerGroup = clientConfig.getRmqConsumerGroup(); + String consumerGroup = clientConfig.getConsumerId(); if (null == consumerGroup || consumerGroup.isEmpty()) { throw new OMSRuntimeException("-1", "Consumer Group is necessary for RocketMQ, please set it."); } @@ -70,13 +73,14 @@ public PushConsumerImpl(final KeyValue properties) { String consumerId = OMSUtil.buildInstanceName(); this.rocketmqPushConsumer.setInstanceName(consumerId); - properties.put(PropertyKeys.CONSUMER_ID, consumerId); + properties.put(OMSBuiltinKeys.CONSUMER_ID, consumerId); + this.rocketmqPushConsumer.setLanguage(LanguageCode.OMS); this.rocketmqPushConsumer.registerMessageListener(new MessageListenerImpl()); } @Override - public KeyValue properties() { + public KeyValue attributes() { return properties; } @@ -90,9 +94,14 @@ public void suspend() { this.rocketmqPushConsumer.suspend(); } + @Override + public void suspend(long timeout) { + + } + @Override public boolean isSuspended() { - return this.rocketmqPushConsumer.getDefaultMQPushConsumerImpl().isPause(); + return this.rocketmqPushConsumer.isPause(); } @Override @@ -106,6 +115,32 @@ public PushConsumer attachQueue(final String queueName, final MessageListener li return this; } + @Override + public PushConsumer attachQueue(String queueName, MessageListener listener, KeyValue attributes) { + return this.attachQueue(queueName, listener); + } + + @Override + public PushConsumer detachQueue(String queueName) { + this.subscribeTable.remove(queueName); + try { + this.rocketmqPushConsumer.unsubscribe(queueName); + } catch (Exception e) { + throw new OMSRuntimeException("-1", String.format("RocketMQ push consumer fails to unsubscribe topic: %s", queueName)); + } + return null; + } + + @Override + public void addInterceptor(ConsumerInterceptor interceptor) { + + } + + @Override + public void removeInterceptor(ConsumerInterceptor interceptor) { + + } + @Override public synchronized void startup() { if (!started) { @@ -146,9 +181,9 @@ public ConsumeConcurrentlyStatus consumeMessage(List rmqMsgList, contextProperties.put(NonStandardKeys.MESSAGE_CONSUME_STATUS, ConsumeConcurrentlyStatus.RECONSUME_LATER.name()); - ReceivedMessageContext context = new ReceivedMessageContext() { + MessageListener.Context context = new MessageListener.Context() { @Override - public KeyValue properties() { + public KeyValue attributes() { return contextProperties; } @@ -158,16 +193,9 @@ public void ack() { contextProperties.put(NonStandardKeys.MESSAGE_CONSUME_STATUS, ConsumeConcurrentlyStatus.CONSUME_SUCCESS.name()); } - - @Override - public void ack(final KeyValue properties) { - sync.countDown(); - contextProperties.put(NonStandardKeys.MESSAGE_CONSUME_STATUS, - properties.getString(NonStandardKeys.MESSAGE_CONSUME_STATUS)); - } }; long begin = System.currentTimeMillis(); - listener.onMessage(omsMsg, context); + listener.onReceived(omsMsg, context); long costs = System.currentTimeMillis() - begin; long timeoutMills = clientConfig.getRmqMessageConsumeTimeout() * 60 * 1000; try { diff --git a/openmessaging/src/main/java/io/openmessaging/rocketmq/domain/BytesMessageImpl.java b/openmessaging/src/main/java/io/openmessaging/rocketmq/domain/BytesMessageImpl.java index 43f80ae5be4..6d8995a1564 100644 --- a/openmessaging/src/main/java/io/openmessaging/rocketmq/domain/BytesMessageImpl.java +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/domain/BytesMessageImpl.java @@ -20,21 +20,26 @@ import io.openmessaging.KeyValue; import io.openmessaging.Message; import io.openmessaging.OMS; +import io.openmessaging.exception.OMSMessageFormatException; import org.apache.commons.lang3.builder.ToStringBuilder; public class BytesMessageImpl implements BytesMessage { - private KeyValue headers; - private KeyValue properties; + private KeyValue sysHeaders; + private KeyValue userHeaders; private byte[] body; public BytesMessageImpl() { - this.headers = OMS.newKeyValue(); - this.properties = OMS.newKeyValue(); + this.sysHeaders = OMS.newKeyValue(); + this.userHeaders = OMS.newKeyValue(); } @Override - public byte[] getBody() { - return body; + public T getBody(Class type) throws OMSMessageFormatException { + if (type == byte[].class) { + return (T)body; + } + + throw new OMSMessageFormatException("", "Cannot assign byte[] to " + type.getName()); } @Override @@ -44,60 +49,60 @@ public BytesMessage setBody(final byte[] body) { } @Override - public KeyValue headers() { - return headers; + public KeyValue sysHeaders() { + return sysHeaders; } @Override - public KeyValue properties() { - return properties; + public KeyValue userHeaders() { + return userHeaders; } @Override - public Message putHeaders(final String key, final int value) { - headers.put(key, value); + public Message putSysHeaders(String key, int value) { + sysHeaders.put(key, value); return this; } @Override - public Message putHeaders(final String key, final long value) { - headers.put(key, value); + public Message putSysHeaders(String key, long value) { + sysHeaders.put(key, value); return this; } @Override - public Message putHeaders(final String key, final double value) { - headers.put(key, value); + public Message putSysHeaders(String key, double value) { + sysHeaders.put(key, value); return this; } @Override - public Message putHeaders(final String key, final String value) { - headers.put(key, value); + public Message putSysHeaders(String key, String value) { + sysHeaders.put(key, value); return this; } @Override - public Message putProperties(final String key, final int value) { - properties.put(key, value); + public Message putUserHeaders(String key, int value) { + userHeaders.put(key, value); return this; } @Override - public Message putProperties(final String key, final long value) { - properties.put(key, value); + public Message putUserHeaders(String key, long value) { + userHeaders.put(key, value); return this; } @Override - public Message putProperties(final String key, final double value) { - properties.put(key, value); + public Message putUserHeaders(String key, double value) { + userHeaders.put(key, value); return this; } @Override - public Message putProperties(final String key, final String value) { - properties.put(key, value); + public Message putUserHeaders(String key, String value) { + userHeaders.put(key, value); return this; } diff --git a/openmessaging/src/main/java/io/openmessaging/rocketmq/domain/RocketMQConstants.java b/openmessaging/src/main/java/io/openmessaging/rocketmq/domain/RocketMQConstants.java new file mode 100644 index 00000000000..2bebc8abfab --- /dev/null +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/domain/RocketMQConstants.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.openmessaging.rocketmq.domain; + +public interface RocketMQConstants { + + /** + * Key of scheduled message delivery time + */ + String START_DELIVER_TIME = "__STARTDELIVERTIME"; + +} diff --git a/openmessaging/src/main/java/io/openmessaging/rocketmq/domain/SendResultImpl.java b/openmessaging/src/main/java/io/openmessaging/rocketmq/domain/SendResultImpl.java index 228a9f0b595..85bcd685493 100644 --- a/openmessaging/src/main/java/io/openmessaging/rocketmq/domain/SendResultImpl.java +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/domain/SendResultImpl.java @@ -17,7 +17,7 @@ package io.openmessaging.rocketmq.domain; import io.openmessaging.KeyValue; -import io.openmessaging.SendResult; +import io.openmessaging.producer.SendResult; public class SendResultImpl implements SendResult { private String messageId; @@ -33,7 +33,6 @@ public String messageId() { return messageId; } - @Override public KeyValue properties() { return properties; } diff --git a/openmessaging/src/main/java/io/openmessaging/rocketmq/producer/AbstractOMSProducer.java b/openmessaging/src/main/java/io/openmessaging/rocketmq/producer/AbstractOMSProducer.java index 8246bcd294a..e03246142c9 100644 --- a/openmessaging/src/main/java/io/openmessaging/rocketmq/producer/AbstractOMSProducer.java +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/producer/AbstractOMSProducer.java @@ -20,8 +20,7 @@ import io.openmessaging.KeyValue; import io.openmessaging.Message; import io.openmessaging.MessageFactory; -import io.openmessaging.MessageHeader; -import io.openmessaging.PropertyKeys; +import io.openmessaging.OMSBuiltinKeys; import io.openmessaging.ServiceLifecycle; import io.openmessaging.exception.OMSMessageFormatException; import io.openmessaging.exception.OMSNotSupportedException; @@ -32,39 +31,42 @@ import io.openmessaging.rocketmq.utils.BeanUtils; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; -import org.apache.rocketmq.client.log.ClientLogger; import org.apache.rocketmq.client.producer.DefaultMQProducer; -import org.apache.rocketmq.common.protocol.ResponseCode; import org.apache.rocketmq.remoting.exception.RemotingConnectException; import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; -import org.slf4j.Logger; +import org.apache.rocketmq.remoting.protocol.LanguageCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; import static io.openmessaging.rocketmq.utils.OMSUtil.buildInstanceName; abstract class AbstractOMSProducer implements ServiceLifecycle, MessageFactory { - final static Logger log = ClientLogger.getLog(); final KeyValue properties; final DefaultMQProducer rocketmqProducer; private boolean started = false; - final ClientConfig clientConfig; + private final ClientConfig clientConfig; AbstractOMSProducer(final KeyValue properties) { this.properties = properties; this.rocketmqProducer = new DefaultMQProducer(); this.clientConfig = BeanUtils.populate(properties, ClientConfig.class); - String accessPoints = clientConfig.getOmsAccessPoints(); - if (accessPoints == null || accessPoints.isEmpty()) { - throw new OMSRuntimeException("-1", "OMS AccessPoints is null or empty."); + if ("true".equalsIgnoreCase(System.getenv("OMS_RMQ_DIRECT_NAME_SRV"))) { + String accessPoints = clientConfig.getAccessPoints(); + if (accessPoints == null || accessPoints.isEmpty()) { + throw new OMSRuntimeException("-1", "OMS AccessPoints is null or empty."); + } + + this.rocketmqProducer.setNamesrvAddr(accessPoints.replace(',', ';')); } - this.rocketmqProducer.setNamesrvAddr(accessPoints.replace(',', ';')); + this.rocketmqProducer.setProducerGroup(clientConfig.getRmqProducerGroup()); String producerId = buildInstanceName(); - this.rocketmqProducer.setSendMsgTimeout(clientConfig.getOmsOperationTimeout()); + this.rocketmqProducer.setSendMsgTimeout(clientConfig.getOperationTimeout()); this.rocketmqProducer.setInstanceName(producerId); this.rocketmqProducer.setMaxMessageSize(1024 * 1024 * 4); - properties.put(PropertyKeys.PRODUCER_ID, producerId); + this.rocketmqProducer.setLanguage(LanguageCode.OMS); + properties.put(OMSBuiltinKeys.PRODUCER_ID, producerId); } @Override @@ -94,9 +96,19 @@ OMSRuntimeException checkProducerException(String topic, String msgId, Throwable return new OMSTimeOutException("-1", String.format("Send message to broker timeout, %dms, Topic=%s, msgId=%s", this.rocketmqProducer.getSendMsgTimeout(), topic, msgId), e); } else if (e.getCause() instanceof MQBrokerException || e.getCause() instanceof RemotingConnectException) { - MQBrokerException brokerException = (MQBrokerException) e.getCause(); - return new OMSRuntimeException("-1", String.format("Received a broker exception, Topic=%s, msgId=%s, %s", - topic, msgId, brokerException.getErrorMessage()), e); + if (e.getCause() instanceof MQBrokerException) { + MQBrokerException brokerException = (MQBrokerException) e.getCause(); + return new OMSRuntimeException("-1", String.format("Received a broker exception, Topic=%s, msgId=%s, %s", + topic, msgId, brokerException.getErrorMessage()), e); + } + + if (e.getCause() instanceof RemotingConnectException) { + RemotingConnectException connectException = (RemotingConnectException)e.getCause(); + return new OMSRuntimeException("-1", + String.format("Network connection experiences failures. Topic=%s, msgId=%s, %s", + topic, msgId, connectException.getMessage()), + e); + } } } // Exception thrown by local. @@ -121,18 +133,10 @@ protected void checkMessageType(Message message) { } @Override - public BytesMessage createBytesMessageToTopic(final String topic, final byte[] body) { - BytesMessage bytesMessage = new BytesMessageImpl(); - bytesMessage.setBody(body); - bytesMessage.headers().put(MessageHeader.TOPIC, topic); - return bytesMessage; - } - - @Override - public BytesMessage createBytesMessageToQueue(final String queue, final byte[] body) { - BytesMessage bytesMessage = new BytesMessageImpl(); - bytesMessage.setBody(body); - bytesMessage.headers().put(MessageHeader.QUEUE, queue); - return bytesMessage; + public BytesMessage createBytesMessage(String queue, byte[] body) { + BytesMessage message = new BytesMessageImpl(); + message.setBody(body); + message.sysHeaders().put(Message.BuiltinKeys.DESTINATION, queue); + return message; } } diff --git a/openmessaging/src/main/java/io/openmessaging/rocketmq/producer/ProducerImpl.java b/openmessaging/src/main/java/io/openmessaging/rocketmq/producer/ProducerImpl.java index 2c00c60ebc4..af712cac31f 100644 --- a/openmessaging/src/main/java/io/openmessaging/rocketmq/producer/ProducerImpl.java +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/producer/ProducerImpl.java @@ -19,27 +19,32 @@ import io.openmessaging.BytesMessage; import io.openmessaging.KeyValue; import io.openmessaging.Message; -import io.openmessaging.MessageHeader; -import io.openmessaging.Producer; import io.openmessaging.Promise; -import io.openmessaging.PropertyKeys; -import io.openmessaging.SendResult; import io.openmessaging.exception.OMSRuntimeException; +import io.openmessaging.interceptor.ProducerInterceptor; +import io.openmessaging.producer.BatchMessageSender; +import io.openmessaging.producer.LocalTransactionExecutor; +import io.openmessaging.producer.Producer; +import io.openmessaging.producer.SendResult; import io.openmessaging.rocketmq.promise.DefaultPromise; import io.openmessaging.rocketmq.utils.OMSUtil; import org.apache.rocketmq.client.producer.SendCallback; import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import static io.openmessaging.rocketmq.utils.OMSUtil.msgConvert; public class ProducerImpl extends AbstractOMSProducer implements Producer { + private static final Logger log = LoggerFactory.getLogger(ProducerImpl.class); + public ProducerImpl(final KeyValue properties) { super(properties); } @Override - public KeyValue properties() { + public KeyValue attributes() { return properties; } @@ -50,11 +55,16 @@ public SendResult send(final Message message) { @Override public SendResult send(final Message message, final KeyValue properties) { - long timeout = properties.containsKey(PropertyKeys.OPERATION_TIMEOUT) - ? properties.getInt(PropertyKeys.OPERATION_TIMEOUT) : this.rocketmqProducer.getSendMsgTimeout(); + long timeout = properties.containsKey(Message.BuiltinKeys.TIMEOUT) + ? properties.getInt(Message.BuiltinKeys.TIMEOUT) : this.rocketmqProducer.getSendMsgTimeout(); return send(message, timeout); } + @Override + public SendResult send(Message message, LocalTransactionExecutor branchExecutor, KeyValue attributes) { + return null; + } + private SendResult send(final Message message, long timeout) { checkMessageType(message); org.apache.rocketmq.common.message.Message rmqMessage = msgConvert((BytesMessage) message); @@ -64,11 +74,11 @@ private SendResult send(final Message message, long timeout) { log.error(String.format("Send message to RocketMQ failed, %s", message)); throw new OMSRuntimeException("-1", "Send message to RocketMQ broker failed."); } - message.headers().put(MessageHeader.MESSAGE_ID, rmqResult.getMsgId()); + message.sysHeaders().put(Message.BuiltinKeys.MESSAGE_ID, rmqResult.getMsgId()); return OMSUtil.sendResultConvert(rmqResult); } catch (Exception e) { log.error(String.format("Send message to RocketMQ failed, %s", message), e); - throw checkProducerException(rmqMessage.getTopic(), message.headers().getString(MessageHeader.MESSAGE_ID), e); + throw checkProducerException(rmqMessage.getTopic(), message.sysHeaders().getString(Message.BuiltinKeys.MESSAGE_ID), e); } } @@ -79,8 +89,8 @@ public Promise sendAsync(final Message message) { @Override public Promise sendAsync(final Message message, final KeyValue properties) { - long timeout = properties.containsKey(PropertyKeys.OPERATION_TIMEOUT) - ? properties.getInt(PropertyKeys.OPERATION_TIMEOUT) : this.rocketmqProducer.getSendMsgTimeout(); + long timeout = properties.containsKey(Message.BuiltinKeys.TIMEOUT) + ? properties.getInt(Message.BuiltinKeys.TIMEOUT) : this.rocketmqProducer.getSendMsgTimeout(); return sendAsync(message, timeout); } @@ -92,7 +102,7 @@ private Promise sendAsync(final Message message, long timeout) { this.rocketmqProducer.send(rmqMessage, new SendCallback() { @Override public void onSuccess(final org.apache.rocketmq.client.producer.SendResult rmqResult) { - message.headers().put(MessageHeader.MESSAGE_ID, rmqResult.getMsgId()); + message.sysHeaders().put(Message.BuiltinKeys.MESSAGE_ID, rmqResult.getMsgId()); promise.set(OMSUtil.sendResultConvert(rmqResult)); } @@ -121,4 +131,19 @@ public void sendOneway(final Message message) { public void sendOneway(final Message message, final KeyValue properties) { sendOneway(message); } + + @Override + public BatchMessageSender createBatchMessageSender() { + return null; + } + + @Override + public void addInterceptor(ProducerInterceptor interceptor) { + + } + + @Override + public void removeInterceptor(ProducerInterceptor interceptor) { + + } } diff --git a/openmessaging/src/main/java/io/openmessaging/rocketmq/producer/SequenceProducerImpl.java b/openmessaging/src/main/java/io/openmessaging/rocketmq/producer/SequenceProducerImpl.java deleted file mode 100644 index 05225cc5058..00000000000 --- a/openmessaging/src/main/java/io/openmessaging/rocketmq/producer/SequenceProducerImpl.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.openmessaging.rocketmq.producer; - -import io.openmessaging.BytesMessage; -import io.openmessaging.KeyValue; -import io.openmessaging.Message; -import io.openmessaging.MessageHeader; -import io.openmessaging.SequenceProducer; -import io.openmessaging.rocketmq.utils.OMSUtil; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import org.apache.rocketmq.client.Validators; -import org.apache.rocketmq.client.exception.MQClientException; -import org.apache.rocketmq.client.producer.SendResult; - -public class SequenceProducerImpl extends AbstractOMSProducer implements SequenceProducer { - - private BlockingQueue msgCacheQueue; - - public SequenceProducerImpl(final KeyValue properties) { - super(properties); - this.msgCacheQueue = new LinkedBlockingQueue<>(); - } - - @Override - public KeyValue properties() { - return properties; - } - - @Override - public void send(final Message message) { - checkMessageType(message); - org.apache.rocketmq.common.message.Message rmqMessage = OMSUtil.msgConvert((BytesMessage) message); - try { - Validators.checkMessage(rmqMessage, this.rocketmqProducer); - } catch (MQClientException e) { - throw checkProducerException(rmqMessage.getTopic(), message.headers().getString(MessageHeader.MESSAGE_ID), e); - } - msgCacheQueue.add(message); - } - - @Override - public void send(final Message message, final KeyValue properties) { - send(message); - } - - @Override - public synchronized void commit() { - List messages = new ArrayList<>(); - msgCacheQueue.drainTo(messages); - - List rmqMessages = new ArrayList<>(); - - for (Message message : messages) { - rmqMessages.add(OMSUtil.msgConvert((BytesMessage) message)); - } - - if (rmqMessages.size() == 0) { - return; - } - - try { - SendResult sendResult = this.rocketmqProducer.send(rmqMessages); - String[] msgIdArray = sendResult.getMsgId().split(","); - for (int i = 0; i < messages.size(); i++) { - Message message = messages.get(i); - message.headers().put(MessageHeader.MESSAGE_ID, msgIdArray[i]); - } - } catch (Exception e) { - throw checkProducerException("", "", e); - } - } - - @Override - public synchronized void rollback() { - msgCacheQueue.clear(); - } -} diff --git a/openmessaging/src/main/java/io/openmessaging/rocketmq/promise/DefaultPromise.java b/openmessaging/src/main/java/io/openmessaging/rocketmq/promise/DefaultPromise.java index 3e4bd266ce1..36ac27f417a 100644 --- a/openmessaging/src/main/java/io/openmessaging/rocketmq/promise/DefaultPromise.java +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/promise/DefaultPromise.java @@ -17,12 +17,13 @@ package io.openmessaging.rocketmq.promise; import io.openmessaging.Promise; -import io.openmessaging.PromiseListener; +import io.openmessaging.FutureListener; import io.openmessaging.exception.OMSRuntimeException; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + import java.util.ArrayList; import java.util.List; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class DefaultPromise implements Promise { private static final Logger LOG = LoggerFactory.getLogger(DefaultPromise.class); @@ -32,7 +33,7 @@ public class DefaultPromise implements Promise { private long timeout; private long createTime; private Throwable exception = null; - private List> promiseListenerList; + private List> promiseListenerList; public DefaultPromise() { createTime = System.currentTimeMillis(); @@ -120,7 +121,7 @@ public boolean setFailure(final Throwable cause) { } @Override - public void addListener(final PromiseListener listener) { + public void addListener(final FutureListener listener) { if (listener == null) { throw new NullPointerException("FutureListener is null"); } @@ -149,14 +150,14 @@ public Throwable getThrowable() { private void notifyListeners() { if (promiseListenerList != null) { - for (PromiseListener listener : promiseListenerList) { + for (FutureListener listener : promiseListenerList) { notifyListener(listener); } } } private boolean isSuccess() { - return isDone() && (exception == null); + return isDone() && exception == null; } private void timeoutSoCancel() { @@ -198,12 +199,9 @@ private boolean done() { return true; } - private void notifyListener(final PromiseListener listener) { + private void notifyListener(final FutureListener listener) { try { - if (exception != null) - listener.operationFailed(this); - else - listener.operationCompleted(this); + listener.operationComplete(this); } catch (Throwable t) { LOG.error("notifyListener {} Error:{}", listener.getClass().getSimpleName(), t); } diff --git a/openmessaging/src/main/java/io/openmessaging/rocketmq/utils/BeanUtils.java b/openmessaging/src/main/java/io/openmessaging/rocketmq/utils/BeanUtils.java index 104d3d964fa..de91374c052 100644 --- a/openmessaging/src/main/java/io/openmessaging/rocketmq/utils/BeanUtils.java +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/utils/BeanUtils.java @@ -21,19 +21,20 @@ import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; +import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import org.apache.commons.lang3.StringUtils; -import org.apache.rocketmq.client.log.ClientLogger; -import org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; public final class BeanUtils { - final static Logger log = ClientLogger.getLog(); + private static final Logger log = LoggerFactory.getLogger(BeanUtils.class); /** * Maps primitive {@code Class}es to their corresponding wrapper {@code Class}. */ - private static Map, Class> primitiveWrapperMap = new HashMap, Class>(); + private static Map, Class> primitiveWrapperMap = new HashMap<>(); static { primitiveWrapperMap.put(Boolean.TYPE, Boolean.class); @@ -47,13 +48,13 @@ public final class BeanUtils { primitiveWrapperMap.put(Void.TYPE, Void.TYPE); } - private static Map, Class> wrapperMap = new HashMap, Class>(); + private static Map, Class> wrapperMap = new HashMap<>(); static { - for (final Class primitiveClass : primitiveWrapperMap.keySet()) { - final Class wrapperClass = primitiveWrapperMap.get(primitiveClass); - if (!primitiveClass.equals(wrapperClass)) { - wrapperMap.put(wrapperClass, primitiveClass); + for (Entry, Class> primitiveClass : primitiveWrapperMap.entrySet()) { + final Class wrapperClass = primitiveClass.getValue(); + if (!primitiveClass.getKey().equals(wrapperClass)) { + wrapperMap.put(wrapperClass, primitiveClass.getKey()); } } wrapperMap.put(String.class, String.class); @@ -86,7 +87,7 @@ public final class BeanUtils { public static T populate(final Properties properties, final Class clazz) { T obj = null; try { - obj = clazz.newInstance(); + obj = clazz.getDeclaredConstructor().newInstance(); return populate(properties, obj); } catch (Throwable e) { log.warn("Error occurs !", e); @@ -97,7 +98,7 @@ public static T populate(final Properties properties, final Class clazz) public static T populate(final KeyValue properties, final Class clazz) { T obj = null; try { - obj = clazz.newInstance(); + obj = clazz.getDeclaredConstructor().newInstance(); return populate(properties, obj); } catch (Throwable e) { log.warn("Error occurs !", e); @@ -164,7 +165,7 @@ public static T populate(final KeyValue properties, final T obj) { final Set keySet = properties.keySet(); for (String key : keySet) { - String[] keyGroup = key.split("\\."); + String[] keyGroup = key.split("[\\._]"); for (int i = 0; i < keyGroup.length; i++) { keyGroup[i] = keyGroup[i].toLowerCase(); keyGroup[i] = StringUtils.capitalize(keyGroup[i]); diff --git a/openmessaging/src/main/java/io/openmessaging/rocketmq/utils/OMSUtil.java b/openmessaging/src/main/java/io/openmessaging/rocketmq/utils/OMSUtil.java index 60c840813cf..66af8cebb91 100644 --- a/openmessaging/src/main/java/io/openmessaging/rocketmq/utils/OMSUtil.java +++ b/openmessaging/src/main/java/io/openmessaging/rocketmq/utils/OMSUtil.java @@ -18,11 +18,11 @@ import io.openmessaging.BytesMessage; import io.openmessaging.KeyValue; -import io.openmessaging.MessageHeader; +import io.openmessaging.Message.BuiltinKeys; import io.openmessaging.OMS; -import io.openmessaging.SendResult; +import io.openmessaging.producer.SendResult; import io.openmessaging.rocketmq.domain.BytesMessageImpl; -import io.openmessaging.rocketmq.domain.NonStandardKeys; +import io.openmessaging.rocketmq.domain.RocketMQConstants; import io.openmessaging.rocketmq.domain.SendResultImpl; import java.lang.reflect.Field; import java.util.Iterator; @@ -46,27 +46,28 @@ public static String buildInstanceName() { public static org.apache.rocketmq.common.message.Message msgConvert(BytesMessage omsMessage) { org.apache.rocketmq.common.message.Message rmqMessage = new org.apache.rocketmq.common.message.Message(); - rmqMessage.setBody(omsMessage.getBody()); + rmqMessage.setBody(omsMessage.getBody(byte[].class)); - KeyValue headers = omsMessage.headers(); - KeyValue properties = omsMessage.properties(); + KeyValue sysHeaders = omsMessage.sysHeaders(); + KeyValue userHeaders = omsMessage.userHeaders(); //All destinations in RocketMQ use Topic - if (headers.containsKey(MessageHeader.TOPIC)) { - rmqMessage.setTopic(headers.getString(MessageHeader.TOPIC)); - rmqMessage.putUserProperty(NonStandardKeys.MESSAGE_DESTINATION, "TOPIC"); - } else { - rmqMessage.setTopic(headers.getString(MessageHeader.QUEUE)); - rmqMessage.putUserProperty(NonStandardKeys.MESSAGE_DESTINATION, "QUEUE"); + rmqMessage.setTopic(sysHeaders.getString(BuiltinKeys.DESTINATION)); + + if (sysHeaders.containsKey(BuiltinKeys.START_TIME)) { + long deliverTime = sysHeaders.getLong(BuiltinKeys.START_TIME, 0); + if (deliverTime > 0) { + rmqMessage.putUserProperty(RocketMQConstants.START_DELIVER_TIME, String.valueOf(deliverTime)); + } } - for (String key : properties.keySet()) { - MessageAccessor.putProperty(rmqMessage, key, properties.getString(key)); + for (String key : userHeaders.keySet()) { + MessageAccessor.putProperty(rmqMessage, key, userHeaders.getString(key)); } - //Headers has a high priority - for (String key : headers.keySet()) { - MessageAccessor.putProperty(rmqMessage, key, headers.getString(key)); + //System headers has a high priority + for (String key : sysHeaders.keySet()) { + MessageAccessor.putProperty(rmqMessage, key, sysHeaders.getString(key)); } return rmqMessage; @@ -76,8 +77,8 @@ public static BytesMessage msgConvert(org.apache.rocketmq.common.message.Message BytesMessage omsMsg = new BytesMessageImpl(); omsMsg.setBody(rmqMsg.getBody()); - KeyValue headers = omsMsg.headers(); - KeyValue properties = omsMsg.properties(); + KeyValue headers = omsMsg.sysHeaders(); + KeyValue properties = omsMsg.userHeaders(); final Set> entries = rmqMsg.getProperties().entrySet(); @@ -89,25 +90,22 @@ public static BytesMessage msgConvert(org.apache.rocketmq.common.message.Message } } - omsMsg.putHeaders(MessageHeader.MESSAGE_ID, rmqMsg.getMsgId()); - if (!rmqMsg.getProperties().containsKey(NonStandardKeys.MESSAGE_DESTINATION) || - rmqMsg.getProperties().get(NonStandardKeys.MESSAGE_DESTINATION).equals("TOPIC")) { - omsMsg.putHeaders(MessageHeader.TOPIC, rmqMsg.getTopic()); - } else { - omsMsg.putHeaders(MessageHeader.QUEUE, rmqMsg.getTopic()); - } - omsMsg.putHeaders(MessageHeader.SEARCH_KEY, rmqMsg.getKeys()); - omsMsg.putHeaders(MessageHeader.BORN_HOST, String.valueOf(rmqMsg.getBornHost())); - omsMsg.putHeaders(MessageHeader.BORN_TIMESTAMP, rmqMsg.getBornTimestamp()); - omsMsg.putHeaders(MessageHeader.STORE_HOST, String.valueOf(rmqMsg.getStoreHost())); - omsMsg.putHeaders(MessageHeader.STORE_TIMESTAMP, rmqMsg.getStoreTimestamp()); + omsMsg.putSysHeaders(BuiltinKeys.MESSAGE_ID, rmqMsg.getMsgId()); + + omsMsg.putSysHeaders(BuiltinKeys.DESTINATION, rmqMsg.getTopic()); + + omsMsg.putSysHeaders(BuiltinKeys.SEARCH_KEYS, rmqMsg.getKeys()); + omsMsg.putSysHeaders(BuiltinKeys.BORN_HOST, String.valueOf(rmqMsg.getBornHost())); + omsMsg.putSysHeaders(BuiltinKeys.BORN_TIMESTAMP, rmqMsg.getBornTimestamp()); + omsMsg.putSysHeaders(BuiltinKeys.STORE_HOST, String.valueOf(rmqMsg.getStoreHost())); + omsMsg.putSysHeaders(BuiltinKeys.STORE_TIMESTAMP, rmqMsg.getStoreTimestamp()); return omsMsg; } public static boolean isOMSHeader(String value) { - for (Field field : MessageHeader.class.getDeclaredFields()) { + for (Field field : BuiltinKeys.class.getDeclaredFields()) { try { - if (field.get(MessageHeader.class).equals(value)) { + if (field.get(BuiltinKeys.class).equals(value)) { return true; } } catch (IllegalAccessException e) { diff --git a/openmessaging/src/test/java/io/openmessaging/rocketmq/consumer/PullConsumerImplTest.java b/openmessaging/src/test/java/io/openmessaging/rocketmq/consumer/PullConsumerImplTest.java index 843ddb78805..5a0fd9c8c38 100644 --- a/openmessaging/src/test/java/io/openmessaging/rocketmq/consumer/PullConsumerImplTest.java +++ b/openmessaging/src/test/java/io/openmessaging/rocketmq/consumer/PullConsumerImplTest.java @@ -18,12 +18,10 @@ import io.openmessaging.BytesMessage; import io.openmessaging.Message; -import io.openmessaging.MessageHeader; import io.openmessaging.MessagingAccessPoint; -import io.openmessaging.MessagingAccessPointFactory; import io.openmessaging.OMS; -import io.openmessaging.PropertyKeys; -import io.openmessaging.PullConsumer; +import io.openmessaging.OMSBuiltinKeys; +import io.openmessaging.consumer.PullConsumer; import io.openmessaging.rocketmq.config.ClientConfig; import io.openmessaging.rocketmq.domain.NonStandardKeys; import java.lang.reflect.Field; @@ -50,18 +48,18 @@ public class PullConsumerImplTest { @Before public void init() throws NoSuchFieldException, IllegalAccessException { - final MessagingAccessPoint messagingAccessPoint = MessagingAccessPointFactory - .getMessagingAccessPoint("openmessaging:rocketmq://IP1:9876,IP2:9876/namespace"); + final MessagingAccessPoint messagingAccessPoint = OMS + .getMessagingAccessPoint("oms:rocketmq://IP1:9876,IP2:9876/namespace"); - consumer = messagingAccessPoint.createPullConsumer(queueName, - OMS.newKeyValue().put(NonStandardKeys.CONSUMER_GROUP, "TestGroup")); + consumer = messagingAccessPoint.createPullConsumer(OMS.newKeyValue().put(OMSBuiltinKeys.CONSUMER_ID, "TestGroup")); + consumer.attachQueue(queueName); Field field = PullConsumerImpl.class.getDeclaredField("rocketmqPullConsumer"); field.setAccessible(true); field.set(consumer, rocketmqPullConsumer); //Replace ClientConfig clientConfig = new ClientConfig(); - clientConfig.setOmsOperationTimeout(200); + clientConfig.setOperationTimeout(200); localMessageCache = spy(new LocalMessageCache(rocketmqPullConsumer, clientConfig)); field = PullConsumerImpl.class.getDeclaredField("localMessageCache"); @@ -83,18 +81,18 @@ public void testPoll() { when(localMessageCache.poll()).thenReturn(consumedMsg); - Message message = consumer.poll(); - assertThat(message.headers().getString(MessageHeader.MESSAGE_ID)).isEqualTo("NewMsgId"); - assertThat(((BytesMessage) message).getBody()).isEqualTo(testBody); + Message message = consumer.receive(); + assertThat(message.sysHeaders().getString(Message.BuiltinKeys.MESSAGE_ID)).isEqualTo("NewMsgId"); + assertThat(((BytesMessage) message).getBody(byte[].class)).isEqualTo(testBody); } @Test public void testPoll_WithTimeout() { //There is a default timeout value, @see ClientConfig#omsOperationTimeout. - Message message = consumer.poll(); + Message message = consumer.receive(); assertThat(message).isNull(); - message = consumer.poll(OMS.newKeyValue().put(PropertyKeys.OPERATION_TIMEOUT, 100)); + message = consumer.receive(OMS.newKeyValue().put(Message.BuiltinKeys.TIMEOUT, 100)); assertThat(message).isNull(); } } \ No newline at end of file diff --git a/openmessaging/src/test/java/io/openmessaging/rocketmq/consumer/PushConsumerImplTest.java b/openmessaging/src/test/java/io/openmessaging/rocketmq/consumer/PushConsumerImplTest.java index 882e57ea5ee..d80e02622db 100644 --- a/openmessaging/src/test/java/io/openmessaging/rocketmq/consumer/PushConsumerImplTest.java +++ b/openmessaging/src/test/java/io/openmessaging/rocketmq/consumer/PushConsumerImplTest.java @@ -18,13 +18,11 @@ import io.openmessaging.BytesMessage; import io.openmessaging.Message; -import io.openmessaging.MessageHeader; -import io.openmessaging.MessageListener; +import io.openmessaging.OMSBuiltinKeys; +import io.openmessaging.consumer.MessageListener; import io.openmessaging.MessagingAccessPoint; -import io.openmessaging.MessagingAccessPointFactory; import io.openmessaging.OMS; -import io.openmessaging.PushConsumer; -import io.openmessaging.ReceivedMessageContext; +import io.openmessaging.consumer.PushConsumer; import io.openmessaging.rocketmq.domain.NonStandardKeys; import java.lang.reflect.Field; import java.util.Collections; @@ -49,10 +47,10 @@ public class PushConsumerImplTest { @Before public void init() throws NoSuchFieldException, IllegalAccessException { - final MessagingAccessPoint messagingAccessPoint = MessagingAccessPointFactory - .getMessagingAccessPoint("openmessaging:rocketmq://IP1:9876,IP2:9876/namespace"); + final MessagingAccessPoint messagingAccessPoint = OMS + .getMessagingAccessPoint("oms:rocketmq://IP1:9876,IP2:9876/namespace"); consumer = messagingAccessPoint.createPushConsumer( - OMS.newKeyValue().put(NonStandardKeys.CONSUMER_GROUP, "TestGroup")); + OMS.newKeyValue().put(OMSBuiltinKeys.CONSUMER_ID, "TestGroup")); Field field = PushConsumerImpl.class.getDeclaredField("rocketmqPushConsumer"); field.setAccessible(true); @@ -75,9 +73,9 @@ public void testConsumeMessage() { consumedMsg.setTopic("HELLO_QUEUE"); consumer.attachQueue("HELLO_QUEUE", new MessageListener() { @Override - public void onMessage(final Message message, final ReceivedMessageContext context) { - assertThat(message.headers().getString(MessageHeader.MESSAGE_ID)).isEqualTo("NewMsgId"); - assertThat(((BytesMessage) message).getBody()).isEqualTo(testBody); + public void onReceived(Message message, Context context) { + assertThat(message.sysHeaders().getString(Message.BuiltinKeys.MESSAGE_ID)).isEqualTo("NewMsgId"); + assertThat(((BytesMessage) message).getBody(byte[].class)).isEqualTo(testBody); context.ack(); } }); diff --git a/openmessaging/src/test/java/io/openmessaging/rocketmq/producer/ProducerImplTest.java b/openmessaging/src/test/java/io/openmessaging/rocketmq/producer/ProducerImplTest.java index 1db80c3efac..fc6515ea8d1 100644 --- a/openmessaging/src/test/java/io/openmessaging/rocketmq/producer/ProducerImplTest.java +++ b/openmessaging/src/test/java/io/openmessaging/rocketmq/producer/ProducerImplTest.java @@ -17,9 +17,9 @@ package io.openmessaging.rocketmq.producer; import io.openmessaging.MessagingAccessPoint; -import io.openmessaging.MessagingAccessPointFactory; -import io.openmessaging.Producer; +import io.openmessaging.OMS; import io.openmessaging.exception.OMSRuntimeException; +import io.openmessaging.producer.Producer; import java.lang.reflect.Field; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; @@ -49,8 +49,8 @@ public class ProducerImplTest { @Before public void init() throws NoSuchFieldException, IllegalAccessException { - final MessagingAccessPoint messagingAccessPoint = MessagingAccessPointFactory - .getMessagingAccessPoint("openmessaging:rocketmq://IP1:9876,IP2:9876/namespace"); + final MessagingAccessPoint messagingAccessPoint = OMS + .getMessagingAccessPoint("oms:rocketmq://IP1:9876,IP2:9876/namespace"); producer = messagingAccessPoint.createProducer(); Field field = AbstractOMSProducer.class.getDeclaredField("rocketmqProducer"); @@ -67,8 +67,8 @@ public void testSend_OK() throws InterruptedException, RemotingException, MQClie sendResult.setMsgId("TestMsgID"); sendResult.setSendStatus(SendStatus.SEND_OK); when(rocketmqProducer.send(any(Message.class), anyLong())).thenReturn(sendResult); - io.openmessaging.SendResult omsResult = - producer.send(producer.createBytesMessageToTopic("HELLO_TOPIC", new byte[] {'a'})); + io.openmessaging.producer.SendResult omsResult = + producer.send(producer.createBytesMessage("HELLO_TOPIC", new byte[] {'a'})); assertThat(omsResult.messageId()).isEqualTo("TestMsgID"); } @@ -80,7 +80,7 @@ public void testSend_Not_OK() throws InterruptedException, RemotingException, MQ when(rocketmqProducer.send(any(Message.class), anyLong())).thenReturn(sendResult); try { - producer.send(producer.createBytesMessageToTopic("HELLO_TOPIC", new byte[] {'a'})); + producer.send(producer.createBytesMessage("HELLO_TOPIC", new byte[] {'a'})); failBecauseExceptionWasNotThrown(OMSRuntimeException.class); } catch (Exception e) { assertThat(e).hasMessageContaining("Send message to RocketMQ broker failed."); @@ -91,7 +91,7 @@ public void testSend_Not_OK() throws InterruptedException, RemotingException, MQ public void testSend_WithException() throws InterruptedException, RemotingException, MQClientException, MQBrokerException { when(rocketmqProducer.send(any(Message.class), anyLong())).thenThrow(MQClientException.class); try { - producer.send(producer.createBytesMessageToTopic("HELLO_TOPIC", new byte[] {'a'})); + producer.send(producer.createBytesMessage("HELLO_TOPIC", new byte[] {'a'})); failBecauseExceptionWasNotThrown(OMSRuntimeException.class); } catch (Exception e) { assertThat(e).hasMessageContaining("Send message to RocketMQ broker failed."); diff --git a/openmessaging/src/test/java/io/openmessaging/rocketmq/producer/SequenceProducerImplTest.java b/openmessaging/src/test/java/io/openmessaging/rocketmq/producer/SequenceProducerImplTest.java deleted file mode 100644 index 823fe015cca..00000000000 --- a/openmessaging/src/test/java/io/openmessaging/rocketmq/producer/SequenceProducerImplTest.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.openmessaging.rocketmq.producer; - -import io.openmessaging.BytesMessage; -import io.openmessaging.MessageHeader; -import io.openmessaging.MessagingAccessPoint; -import io.openmessaging.MessagingAccessPointFactory; -import io.openmessaging.SequenceProducer; -import java.lang.reflect.Field; -import org.apache.rocketmq.client.exception.MQBrokerException; -import org.apache.rocketmq.client.exception.MQClientException; -import org.apache.rocketmq.client.producer.DefaultMQProducer; -import org.apache.rocketmq.client.producer.SendResult; -import org.apache.rocketmq.client.producer.SendStatus; -import org.apache.rocketmq.common.message.Message; -import org.apache.rocketmq.remoting.exception.RemotingException; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentMatchers; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.when; - -@RunWith(MockitoJUnitRunner.class) -public class SequenceProducerImplTest { - - private SequenceProducer producer; - - @Mock - private DefaultMQProducer rocketmqProducer; - - @Before - public void init() throws NoSuchFieldException, IllegalAccessException { - final MessagingAccessPoint messagingAccessPoint = MessagingAccessPointFactory - .getMessagingAccessPoint("openmessaging:rocketmq://IP1:9876,IP2:9876/namespace"); - producer = messagingAccessPoint.createSequenceProducer(); - - Field field = AbstractOMSProducer.class.getDeclaredField("rocketmqProducer"); - field.setAccessible(true); - field.set(producer, rocketmqProducer); - - messagingAccessPoint.startup(); - producer.startup(); - } - - @Test - public void testSend_WithCommit() throws InterruptedException, RemotingException, MQClientException, MQBrokerException { - SendResult sendResult = new SendResult(); - sendResult.setMsgId("TestMsgID"); - sendResult.setSendStatus(SendStatus.SEND_OK); - when(rocketmqProducer.send(ArgumentMatchers.anyList())).thenReturn(sendResult); - when(rocketmqProducer.getMaxMessageSize()).thenReturn(1024); - final BytesMessage message = producer.createBytesMessageToTopic("HELLO_TOPIC", new byte[] {'a'}); - producer.send(message); - producer.commit(); - assertThat(message.headers().getString(MessageHeader.MESSAGE_ID)).isEqualTo("TestMsgID"); - } - - @Test - public void testRollback() { - when(rocketmqProducer.getMaxMessageSize()).thenReturn(1024); - final BytesMessage message = producer.createBytesMessageToTopic("HELLO_TOPIC", new byte[] {'a'}); - producer.send(message); - producer.rollback(); - producer.commit(); //Commit nothing. - assertThat(message.headers().getString(MessageHeader.MESSAGE_ID)).isEqualTo(null); - } -} \ No newline at end of file diff --git a/openmessaging/src/test/java/io/openmessaging/rocketmq/promise/DefaultPromiseTest.java b/openmessaging/src/test/java/io/openmessaging/rocketmq/promise/DefaultPromiseTest.java index 2240ff2dd11..f226edef0a2 100644 --- a/openmessaging/src/test/java/io/openmessaging/rocketmq/promise/DefaultPromiseTest.java +++ b/openmessaging/src/test/java/io/openmessaging/rocketmq/promise/DefaultPromiseTest.java @@ -16,8 +16,9 @@ */ package io.openmessaging.rocketmq.promise; +import io.openmessaging.Future; +import io.openmessaging.FutureListener; import io.openmessaging.Promise; -import io.openmessaging.PromiseListener; import io.openmessaging.exception.OMSRuntimeException; import org.junit.Before; import org.junit.Test; @@ -63,14 +64,10 @@ public void testGet_WithTimeout() throws Exception { @Test public void testAddListener() throws Exception { - promise.addListener(new PromiseListener() { + promise.addListener(new FutureListener() { @Override - public void operationCompleted(final Promise promise) { + public void operationComplete(Future future) { assertThat(promise.get()).isEqualTo("Done"); - } - - @Override - public void operationFailed(final Promise promise) { } }); @@ -80,15 +77,10 @@ public void operationFailed(final Promise promise) { @Test public void testAddListener_ListenerAfterSet() throws Exception { promise.set("Done"); - promise.addListener(new PromiseListener() { - @Override - public void operationCompleted(final Promise promise) { - assertThat(promise.get()).isEqualTo("Done"); - } - + promise.addListener(new FutureListener() { @Override - public void operationFailed(final Promise promise) { - + public void operationComplete(Future future) { + assertThat(future.get()).isEqualTo("Done"); } }); } @@ -97,13 +89,9 @@ public void operationFailed(final Promise promise) { public void testAddListener_WithException_ListenerAfterSet() throws Exception { final Throwable exception = new OMSRuntimeException("-1", "Test Error"); promise.setFailure(exception); - promise.addListener(new PromiseListener() { - @Override - public void operationCompleted(final Promise promise) { - } - + promise.addListener(new FutureListener() { @Override - public void operationFailed(final Promise promise) { + public void operationComplete(Future future) { assertThat(promise.getThrowable()).isEqualTo(exception); } }); @@ -112,13 +100,9 @@ public void operationFailed(final Promise promise) { @Test public void testAddListener_WithException() throws Exception { final Throwable exception = new OMSRuntimeException("-1", "Test Error"); - promise.addListener(new PromiseListener() { - @Override - public void operationCompleted(final Promise promise) { - } - + promise.addListener(new FutureListener() { @Override - public void operationFailed(final Promise promise) { + public void operationComplete(Future future) { assertThat(promise.getThrowable()).isEqualTo(exception); } }); diff --git a/openmessaging/src/test/java/io/openmessaging/rocketmq/utils/BeanUtilsTest.java b/openmessaging/src/test/java/io/openmessaging/rocketmq/utils/BeanUtilsTest.java index 71ca11ccfdd..1a431d988fd 100644 --- a/openmessaging/src/test/java/io/openmessaging/rocketmq/utils/BeanUtilsTest.java +++ b/openmessaging/src/test/java/io/openmessaging/rocketmq/utils/BeanUtilsTest.java @@ -92,9 +92,9 @@ public void testPopulate() { @Test public void testPopulate_ExistObj() { CustomizedConfig config = new CustomizedConfig(); - config.setOmsConsumerId("NewConsumerId"); + config.setConsumerId("NewConsumerId"); - Assert.assertEquals(config.getOmsConsumerId(), "NewConsumerId"); + Assert.assertEquals(config.getConsumerId(), "NewConsumerId"); config = BeanUtils.populate(properties, config); diff --git a/openmessaging/src/test/resources/rmq.logback-test.xml b/openmessaging/src/test/resources/rmq.logback-test.xml new file mode 100644 index 00000000000..8695d52d57c --- /dev/null +++ b/openmessaging/src/test/resources/rmq.logback-test.xml @@ -0,0 +1,36 @@ + + + + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 079aecd5465..8d4f2ab132d 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. --> - @@ -29,20 +28,16 @@ 2012 org.apache.rocketmq rocketmq-all - 4.2.0 + 5.2.0 pom Apache RocketMQ ${project.version} http://rocketmq.apache.org/ - - 3.2.5 - - git@github.com:apache/rocketmq.git scm:git:git@github.com:apache/rocketmq.git scm:git:git@github.com:apache/rocketmq.git - rocketmq-all-4.2.0 + rocketmq-all-5.2.0 @@ -95,18 +90,92 @@ UTF-8 UTF-8 + ${basedir} false true - 1.7 - 1.7 + 1.8 + 1.8 + + 1.5.0 + 4.1.65.Final + 2.0.53.Final + 1.69 + 1.2.83 + 3.20.0-GA + 4.2.2 + 3.12.0 + 2.7 + 32.0.1-jre + 2.9.0 + 0.3.1-alpha + 2.0 + 1.13 + 1.0.1 + 2.0.3 + 1.0.0 + 1.7 + 1.5.2-2 + 1.8.0 + 0.33.0 + 1.6.0 + 0.3.1.2 + 6.0.53 + 1.0-beta-4 + 1.4.2 + 2.0.3 + 1.53.0 + 3.20.1 + 1.2.10 + 0.9.11 + 2.9.3 + 5.3.27 + 3.4.0 + 1.29.0 + 1.29.0-alpha + 2.0.6 + 2.20.29 + 1.0.2 + 2.13.4.2 + 1.3.14 + + + 4.13.2 + 3.22.0 + 3.10.0 + 2.0.9 + 4.1.0 + 0.30 + 2.11.0 + 5.0.5 + + + 2.2 + 1.0.2 + 2.7 + 1.4.1 + 3.5.1 + 3.0.1 + 2.2 + 3.2.0 + 0.12 + 3.0.2 + 4.3.0 + 0.8.5 + 2.19.1 + 3.0.2 + 4.2.2 + 3.4.2 + 2.10.4 + 2.19.1 + 3.2.4 + jacoco ${project.basedir}/../test/target/jacoco-it.exec file:**/generated-sources/**,**/test/** - @@ -117,14 +186,17 @@ store namesrv remoting - logappender - example - filtersrv srvutil filter test distribution openmessaging + acl + example + container + controller + proxy + tieredstore @@ -132,21 +204,21 @@ org.codehaus.mojo versions-maven-plugin - 2.2 + ${versions-maven-plugin.version} com.github.vongosling dependency-mediator-maven-plugin - 1.0.2 + ${dependency-mediator-maven-plugin.version} org.codehaus.mojo clirr-maven-plugin - 2.7 + ${clirr-maven-plugin.version} maven-enforcer-plugin - 1.4.1 + ${maven-enforcer-plugin.version} enforce-ban-circular-dependencies @@ -158,6 +230,7 @@ + true @@ -165,13 +238,13 @@ org.codehaus.mojo extra-enforcer-rules - 1.0-beta-4 + ${extra-enforcer-rules.version} maven-compiler-plugin - 3.5.1 + ${maven-compiler-plugin.version} ${maven.compiler.source} ${maven.compiler.target} @@ -180,24 +253,9 @@ true - - maven-javadoc-plugin - 2.10.4 - - UTF-8 - - - - attach-javadocs - - jar - - - - maven-source-plugin - 3.0.1 + ${maven-source-plugin.version} attach-sources @@ -209,14 +267,11 @@ maven-help-plugin - 2.2 + ${maven-help-plugin.version} generate-effective-dependencies-pom generate-resources - - effective-pom - ${project.build.directory}/effective-pom/effective-dependencies.xml @@ -225,17 +280,18 @@ maven-checkstyle-plugin - 2.17 + ${maven-checkstyle-plugin.version} - verify - verify + validate + validate style/rmq_checkstyle.xml - UTF-8 + UTF-8 true true - false + true + **/generated*/**/* check @@ -246,20 +302,31 @@ org.apache.rat apache-rat-plugin - 0.12 + ${apache-rat-plugin.version} + .gitignore .travis.yml + README.md CONTRIBUTING.md bin/README.md - .github/* + .github/** + src/test/resources/** src/test/resources/certs/* + src/test/**/*.log + src/test/resources/META-INF/service/* + src/main/resources/META-INF/service/* + */target/** + */*.iml + docs/** + localbin/** + conf/rmq-proxy.json maven-resources-plugin - 3.0.2 + ${maven-resources-plugin.version} ${project.build.sourceEncoding} @@ -268,12 +335,12 @@ org.eluder.coveralls coveralls-maven-plugin - 4.3.0 + ${coveralls-maven-plugin.version} org.jacoco jacoco-maven-plugin - 0.7.8 + ${jacoco-maven-plugin.version} default-prepare-agent @@ -284,6 +351,13 @@ ${project.build.directory}/jacoco.exec + + report + test + + report + + default-prepare-agent-integration pre-integration-test @@ -311,37 +385,42 @@ maven-surefire-plugin - 2.19.1 + ${maven-surefire-plugin.version} + 1 1 true + + **/IT*.java + - - org.codehaus.mojo - findbugs-maven-plugin - 3.0.4 - org.sonarsource.scanner.maven sonar-maven-plugin - 3.0.2 + ${sonar-maven-plugin.version} - maven-failsafe-plugin - 2.19.1 - - - **/NormalMsgDelayIT.java - - + com.github.spotbugs + spotbugs-maven-plugin + ${spotbugs-plugin.version} + check + compile - integration-test + check + + true + false + true + ${project.root}/style/spotbugs-suppressions.xml + High + Max + @@ -349,7 +428,7 @@ maven-assembly-plugin - 3.0.0 + ${maven-assembly-plugin.version} @@ -366,7 +445,7 @@ maven-javadoc-plugin - 2.10.4 + ${maven-javadoc-plugin.version} -Xdoclint:none @@ -377,7 +456,7 @@ maven-javadoc-plugin - 2.10.4 + ${maven-javadoc-plugin.version} -Xdoclint:none @@ -417,7 +496,7 @@ maven-failsafe-plugin - 2.19.1 + ${maven-failsafe-plugin.version} @{failsafeArgLine} @@ -445,159 +524,596 @@ - - - junit - junit - 4.11 - test - - - org.assertj - assertj-core - 2.6.0 - test - - - org.mockito - mockito-core - 2.6.3 - test - - - - ${project.groupId} - rocketmq-client + org.apache.rocketmq + rocketmq-acl ${project.version} - ${project.groupId} + org.apache.rocketmq rocketmq-broker ${project.version} - ${project.groupId} + org.apache.rocketmq + rocketmq-client + ${project.version} + + + org.apache.rocketmq rocketmq-common ${project.version} - ${project.groupId} - rocketmq-store + org.apache.rocketmq + rocketmq-container ${project.version} - ${project.groupId} - rocketmq-namesrv + org.apache.rocketmq + rocketmq-controller ${project.version} - ${project.groupId} - rocketmq-tools + org.apache.rocketmq + rocketmq-example ${project.version} - ${project.groupId} - rocketmq-remoting + org.apache.rocketmq + rocketmq-filter ${project.version} - ${project.groupId} - rocketmq-test + org.apache.rocketmq + rocketmq-namesrv ${project.version} - ${project.groupId} - rocketmq-filtersrv + org.apache.rocketmq + rocketmq-openmessaging ${project.version} - ${project.groupId} + org.apache.rocketmq + rocketmq-proxy + ${project.version} + + + org.apache.rocketmq + rocketmq-remoting + ${project.version} + + + org.apache.rocketmq rocketmq-srvutil ${project.version} org.apache.rocketmq - rocketmq-filter + rocketmq-store ${project.version} - ${project.groupId} - rocketmq-example + org.apache.rocketmq + rocketmq-tiered-store ${project.version} - org.slf4j - slf4j-api - 1.7.7 + org.apache.rocketmq + rocketmq-test + ${project.version} - ch.qos.logback - logback-classic - 1.0.13 + org.apache.rocketmq + rocketmq-tools + ${project.version} - ch.qos.logback - logback-core - 1.0.13 + ${project.groupId} + rocketmq-proto + ${rocketmq-proto.version} + + + io.grpc + grpc-protobuf + + + io.grpc + grpc-stub + + + io.grpc + grpc-netty-shaded + + commons-cli commons-cli - 1.2 + ${commons-cli.version} io.netty netty-all - 4.0.42.Final + ${netty.version} + + + org.bouncycastle + bcpkix-jdk15on + runtime + jar + ${bcpkix-jdk15on.version} com.alibaba fastjson - 1.2.29 + ${fastjson.version} org.javassist javassist - 3.20.0-GA + ${javassist.version} net.java.dev.jna jna - 4.2.2 + ${jna.version} org.apache.commons commons-lang3 - 3.4 + ${commons-lang3.version} + + + commons-io + commons-io + ${commons-io.version} com.google.guava guava - 19.0 + ${guava.version} + + + com.google.errorprone + error_prone_annotations + + + + + com.google.code + gson + ${gson.version} + + + com.googlecode.concurrentlinkedhashmap + concurrentlinkedhashmap-lru + ${concurrentlinkedhashmap-lru.version} io.openmessaging openmessaging-api - 0.1.0-alpha + ${openmessaging.version} + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + commons-codec + commons-codec + ${commons-codec.version} + + + org.slf4j + slf4j-api + ${slf4j-api.version} + + + org.apache.rocketmq + rocketmq-rocksdb + ${rocksdb.version} + + + io.github.aliyunmq + rocketmq-shaded-slf4j-api-bridge + ${rocketmq-shaded-slf4j-api-bridge.version} + + + io.github.aliyunmq + rocketmq-slf4j-api + ${rocketmq-logging.version} + + + io.github.aliyunmq + rocketmq-logback-classic + ${rocketmq-logging.version} + + + commons-validator + commons-validator + ${commons-validator.version} + + + com.github.luben + zstd-jni + ${zstd-jni.version} + + + org.lz4 + lz4-java + ${lz4-java.version} + + + io.opentracing + opentracing-api + ${opentracing.version} + provided + + + io.opentracing + opentracing-mock + ${opentracing.version} + test + + + io.jaegertracing + jaeger-core + ${jaeger.version} + + + com.google.code.gson + gson + + + + + io.jaegertracing + jaeger-thrift + ${jaeger.version} + + + com.squareup.okhttp3 + okhttp + + + + + io.jaegertracing + jaeger-client + ${jaeger.version} + + + org.jetbrains.kotlin + kotlin-stdlib + + + + + io.openmessaging.storage + dledger + ${dleger.version} + + + org.slf4j + slf4j-api + + + + + org.apache.tomcat + annotations-api + ${annotations-api.version} + + + junit + junit + ${junit.version} + test + + + org.assertj + assertj-core + ${assertj-core.version} + test + + + org.mockito + mockito-core + ${mockito-core.version} + test - log4j - log4j - 1.2.17 + org.awaitility + awaitility + ${awaitility.version} - org.apache.logging.log4j - log4j-core - 2.7 + com.google.truth + truth + ${truth.version} + + + com.google.errorprone + error_prone_annotations + + + - org.apache.logging.log4j - log4j-slf4j-impl - 2.7 + org.reflections + reflections + ${org.relection.version} + + + io.grpc + grpc-netty-shaded + ${grpc.version} + + + io.grpc + grpc-protobuf + ${grpc.version} + + + com.google.protobuf + protobuf-java + + + + + io.grpc + grpc-stub + ${grpc.version} + + + io.grpc + grpc-services + ${grpc.version} + + + io.grpc + grpc-testing + ${grpc.version} + test + + + com.google.protobuf + protobuf-java-util + ${protobuf.version} + + + com.google.errorprone + error_prone_annotations + + + com.google.code.gson + gson + + + com.google.j2objc + j2objc-annotations + + + + + io.github.aliyunmq + rocketmq-grpc-netty-codec-haproxy + 1.0.0 + + + com.conversantmedia + disruptor + ${disruptor.version} + + + org.slf4j + slf4j-api + + + + + com.github.ben-manes.caffeine + caffeine + ${caffeine.version} + + + com.google.errorprone + error_prone_annotations + + + + + io.netty + netty-tcnative-boringssl-static + ${netty.tcnative.version} + + + + org.springframework + spring-core + ${spring.version} + test + + + + com.squareup.okio + okio-jvm + ${okio-jvm.version} + + + org.jetbrains.kotlin + kotlin-stdlib + + + org.jetbrains.kotlin + kotlin-stdlib-common + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + + + + + io.opentelemetry + opentelemetry-exporter-otlp + ${opentelemetry.version} + + + io.opentelemetry + opentelemetry-exporter-prometheus + ${opentelemetry-exporter-prometheus.version} + + + io.opentelemetry + opentelemetry-exporter-logging + ${opentelemetry.version} + + + io.opentelemetry + opentelemetry-sdk + ${opentelemetry.version} + + + io.opentelemetry + opentelemetry-exporter-logging-otlp + ${opentelemetry.version} + + + org.slf4j + jul-to-slf4j + ${jul-to-slf4j.version} + + + software.amazon.awssdk + s3 + ${s3.version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson-databind.version} + + + com.alipay.sofa + jraft-core + ${sofa-jraft.version} + + + org.ow2.asm + asm + + + com.google.protobuf + protobuf-java + + + org.rocksdb + rocksdbjni + + + + + com.adobe.testing + s3mock-junit4 + ${s3mock-junit4.version} + test + + + annotations + software.amazon.awssdk + + + commons-logging + commons-logging + + + http-client-spi + software.amazon.awssdk + + + json-utils + software.amazon.awssdk + + + profiles + software.amazon.awssdk + + + regions + software.amazon.awssdk + + + sdk-core + software.amazon.awssdk + + + utils + software.amazon.awssdk + + + jackson-dataformat-cbor + com.fasterxml.jackson.dataformat + + + + + + junit + junit + ${junit.version} + test + + + org.assertj + assertj-core + ${assertj-core.version} + test + + + org.mockito + mockito-core + ${mockito-core.version} + test + + + org.awaitility + awaitility + ${awaitility.version} + + + org.powermock + powermock-module-junit4 + ${powermock-version} + test + + + org.objenesis + objenesis + + + net.bytebuddy + byte-buddy + + + net.bytebuddy + byte-buddy-agent + + + + + org.powermock + powermock-api-mockito2 + ${powermock-version} + test + + diff --git a/proxy/BUILD.bazel b/proxy/BUILD.bazel new file mode 100644 index 00000000000..eb069528ea2 --- /dev/null +++ b/proxy/BUILD.bazel @@ -0,0 +1,124 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +load("//bazel:GenTestRules.bzl", "GenTestRules") + +java_library( + name = "proxy", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//acl", + "//broker", + "//client", + "//common", + "//remoting", + "//srvutil", + "@maven//:ch_qos_logback_logback_classic", + "@maven//:ch_qos_logback_logback_core", + "@maven//:com_alibaba_fastjson", + "@maven//:com_github_ben_manes_caffeine_caffeine", + "@maven//:com_github_luben_zstd_jni", + "@maven//:com_google_code_findbugs_jsr305", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:com_google_protobuf_protobuf_java_util", + "@maven//:commons_cli_commons_cli", + "@maven//:commons_codec_commons_codec", + "@maven//:commons_collections_commons_collections", + "@maven//:commons_validator_commons_validator", + "@maven//:io_grpc_grpc_api", + "@maven//:io_grpc_grpc_context", + "@maven//:io_grpc_grpc_netty_shaded", + "@maven//:io_grpc_grpc_services", + "@maven//:io_grpc_grpc_stub", + "@maven//:io_netty_netty_all", + "@maven//:io_github_aliyunmq_rocketmq_grpc_netty_codec_haproxy", + "@maven//:io_openmessaging_storage_dledger", + "@maven//:io_opentelemetry_opentelemetry_api", + "@maven//:io_opentelemetry_opentelemetry_exporter_otlp", + "@maven//:io_opentelemetry_opentelemetry_exporter_prometheus", + "@maven//:io_opentelemetry_opentelemetry_exporter_logging", + "@maven//:io_opentelemetry_opentelemetry_exporter_logging_otlp", + "@maven//:io_opentelemetry_opentelemetry_sdk", + "@maven//:io_opentelemetry_opentelemetry_sdk_common", + "@maven//:io_opentelemetry_opentelemetry_sdk_metrics", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:org_apache_rocketmq_rocketmq_proto", + "@maven//:org_checkerframework_checker_qual", + "@maven//:org_lz4_lz4_java", + "@maven//:org_slf4j_slf4j_api", + "@maven//:io_github_aliyunmq_rocketmq_slf4j_api", + "@maven//:org_slf4j_jul_to_slf4j", + "@maven//:io_github_aliyunmq_rocketmq_logback_classic", + ], +) + +java_library( + name = "tests", + srcs = glob(["src/test/java/**/*.java"]), + resources = [ + "src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker", + "src/test/resources/rmq-proxy-home/conf/broker.conf", + "src/test/resources/rmq-proxy-home/conf/logback_proxy.xml", + "src/test/resources/rmq-proxy-home/conf/rmq-proxy.json", + ], + visibility = ["//visibility:public"], + deps = [ + "//acl", + ":proxy", + "//:test_deps", + "//broker", + "//client", + "//common", + "//remoting", + "@maven//:ch_qos_logback_logback_core", + "@maven//:com_alibaba_fastjson", + "@maven//:com_github_ben_manes_caffeine_caffeine", + "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", + "@maven//:com_google_protobuf_protobuf_java_util", + "@maven//:io_grpc_grpc_api", + "@maven//:io_grpc_grpc_context", + "@maven//:io_grpc_grpc_netty_shaded", + "@maven//:io_grpc_grpc_stub", + "@maven//:io_netty_netty_all", + "@maven//:io_github_aliyunmq_rocketmq_grpc_netty_codec_haproxy", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:io_opentelemetry_opentelemetry_exporter_otlp", + "@maven//:io_opentelemetry_opentelemetry_exporter_prometheus", + "@maven//:io_opentelemetry_opentelemetry_sdk", + "@maven//:org_apache_rocketmq_rocketmq_proto", + "@maven//:org_checkerframework_checker_qual", + "@maven//:org_slf4j_slf4j_api", + "@maven//:org_springframework_spring_core", + "@maven//:org_jetbrains_annotations", + "@maven//:org_slf4j_jul_to_slf4j", + "@maven//:io_netty_netty_tcnative_boringssl_static", + "@maven//:commons_codec_commons_codec", + ], +) + +GenTestRules( + name = "GeneratedTestRules", + exclude_tests = [ + "src/test/java/org/apache/rocketmq/proxy/config/InitConfigTest", + ], + test_files = glob(["src/test/java/**/*Test.java"]), + deps = [ + ":tests", + ], +) diff --git a/proxy/README.md b/proxy/README.md new file mode 100644 index 00000000000..936bd024b0b --- /dev/null +++ b/proxy/README.md @@ -0,0 +1,60 @@ +rocketmq-proxy +-------- + +## Introduction + +`RocketMQ Proxy` is a stateless component that makes full use of the newly introduced `pop` consumption mechanism to +achieve stateless consumption behavior. `gRPC` protocol is supported by `Proxy` now and all the message types +including `normal`, `fifo`, `transaction` and `delay` are supported via `pop` consumption mode. `Proxy` will translate +incoming traffic into customized `Remoting` protocol to access `Broker` and `Namesrv`. + +`Proxy` also handles SSL, authorization/authentication and logging/tracing/metrics and is in charge of connection +management and traffic governance. + +### Multi-language support. + +`gRPC` combined with `Protocol Buffer` makes it easy to implement clients with both `java` and other programming +languages while the server side doesn't need extra work to support different programming languages. +See [rocketmq-clients](https://github.com/apache/rocketmq-clients) for more information. + +### Multi-protocol support. + +With `Proxy` served as a traffic interface, it's convenient to implement multiple protocols upon proxy. `gRPC` protocol +is implemented first and the customized `Remoting` protocol will be implemented later. HTTP/1.1 will also be taken into +consideration. + +## Architecture + +`RocketMQ Proxy` has two deployment modes: `Cluster` mode and `Local` mode. With both modes, `Pop` mode is natively +supported in `Proxy`. + +### `Cluster` mode + +While in `Cluster` mode, `Proxy` is an independent cluster that communicates with `Broker` with remote procedure call. +In this scenario, `Proxy` acts as a stateless computing component while `Broker` is a stateful component with local +storage. This form of deployment introduces the architecture of separation of computing and storage for RocketMQ. + +Due to the separation of computing and storage, `RocketMQ Proxy` can be scaled out indefinitely in `Cluster` mode to +handle traffic peak while `Broker` can focus on storage engine and high availability. + +![](../docs/en/images/rocketmq_proxy_cluster_mode.png) + +### `Local` mode + +`Proxy` in `Local` mode has more similarity with `RocketMQ` 4.x version, which is easily deployed or upgraded for +current RocketMQ users. With `Local` mode, `Proxy` deployed with `Broker` in the same process with inter-process +communication so the network overhead is reduced compared to `Cluster` mode. + +![](../docs/en/images/rocketmq_proxy_local_mode.png) + +## Deploy guide + +See [Proxy Deployment](../docs/en/proxy/deploy_guide.md) + +## Related + +* [rocketmq-apis](https://github.com/apache/rocketmq-apis): Common communication protocol between server and client. +* [rocketmq-clients](https://github.com/apache/rocketmq-clients): Collection of Polyglot Clients for Apache RocketMQ. +* [RIP-37: New and Unified APIs](https://shimo.im/docs/m5kv92OeRRU8olqX): RocketMQ proposal of new and unified APIs + crossing different languages. +* [RIP-39: Support gRPC protocol](https://shimo.im/docs/gXqmeEPYgdUw5bqo): RocketMQ proposal of gRPC protocol support. \ No newline at end of file diff --git a/proxy/pom.xml b/proxy/pom.xml new file mode 100644 index 00000000000..5bbe4122835 --- /dev/null +++ b/proxy/pom.xml @@ -0,0 +1,118 @@ + + + + + + rocketmq-all + org.apache.rocketmq + 5.2.0 + + + 4.0.0 + jar + rocketmq-proxy + rocketmq-proxy ${project.version} + + + 8 + 8 + ${basedir}/.. + + + + + org.apache.rocketmq + rocketmq-proto + + + org.apache.rocketmq + rocketmq-broker + + + org.apache.rocketmq + rocketmq-common + + + org.apache.rocketmq + rocketmq-client + + + org.apache.rocketmq + rocketmq-acl + + + io.grpc + grpc-netty-shaded + + + io.grpc + grpc-protobuf + + + io.grpc + grpc-stub + + + io.grpc + grpc-services + + + com.google.protobuf + protobuf-java-util + + + io.github.aliyunmq + rocketmq-grpc-netty-codec-haproxy + + + org.apache.commons + commons-lang3 + + + io.github.aliyunmq + rocketmq-slf4j-api + + + io.github.aliyunmq + rocketmq-logback-classic + + + com.github.ben-manes.caffeine + caffeine + + + org.checkerframework + checker-qual + + + + + io.netty + netty-tcnative-boringssl-static + + + org.springframework + spring-core + test + + + org.slf4j + jul-to-slf4j + + + \ No newline at end of file diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/CommandLineArgument.java b/proxy/src/main/java/org/apache/rocketmq/proxy/CommandLineArgument.java new file mode 100644 index 00000000000..0499f2659bb --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/CommandLineArgument.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy; + +public class CommandLineArgument { + private String namesrvAddr; + private String brokerConfigPath; + private String proxyConfigPath; + private String proxyMode; + + public String getNamesrvAddr() { + return namesrvAddr; + } + + public void setNamesrvAddr(String namesrvAddr) { + this.namesrvAddr = namesrvAddr; + } + + public String getBrokerConfigPath() { + return brokerConfigPath; + } + + public void setBrokerConfigPath(String brokerConfigPath) { + this.brokerConfigPath = brokerConfigPath; + } + + public String getProxyConfigPath() { + return proxyConfigPath; + } + + public void setProxyConfigPath(String proxyConfigPath) { + this.proxyConfigPath = proxyConfigPath; + } + + public String getProxyMode() { + return proxyMode; + } + + public void setProxyMode(String proxyMode) { + this.proxyMode = proxyMode; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/ProxyMode.java b/proxy/src/main/java/org/apache/rocketmq/proxy/ProxyMode.java new file mode 100644 index 00000000000..3cc36425b06 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/ProxyMode.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy; + +public enum ProxyMode { + LOCAL("LOCAL"), + CLUSTER("CLUSTER"); + + private final String mode; + + ProxyMode(String mode) { + this.mode = mode; + } + + public static boolean isClusterMode(String mode) { + if (mode == null) { + return false; + } + return CLUSTER.mode.equals(mode.toUpperCase()); + } + + public static boolean isClusterMode(ProxyMode mode) { + if (mode == null) { + return false; + } + return CLUSTER.equals(mode); + } + + public static boolean isLocalMode(String mode) { + if (mode == null) { + return false; + } + return LOCAL.mode.equals(mode.toUpperCase()); + } + + public static boolean isLocalMode(ProxyMode mode) { + if (mode == null) { + return false; + } + return LOCAL.equals(mode); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/ProxyStartup.java b/proxy/src/main/java/org/apache/rocketmq/proxy/ProxyStartup.java new file mode 100644 index 00000000000..3b2ca99bfd0 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/ProxyStartup.java @@ -0,0 +1,257 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy; + +import com.google.common.collect.Lists; +import io.grpc.protobuf.services.ChannelzService; +import io.grpc.protobuf.services.ProtoReflectionService; +import java.util.Date; +import java.util.List; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.acl.AccessValidator; +import org.apache.rocketmq.acl.plain.PlainAccessValidator; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.BrokerStartup; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.thread.ThreadPoolMonitor; +import org.apache.rocketmq.common.utils.ServiceProvider; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.common.utils.AbstractStartAndShutdown; +import org.apache.rocketmq.common.utils.StartAndShutdown; +import org.apache.rocketmq.proxy.config.Configuration; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.ProxyConfig; +import org.apache.rocketmq.proxy.grpc.GrpcServer; +import org.apache.rocketmq.proxy.grpc.GrpcServerBuilder; +import org.apache.rocketmq.proxy.grpc.v2.GrpcMessagingApplication; +import org.apache.rocketmq.proxy.metrics.ProxyMetricsManager; +import org.apache.rocketmq.proxy.processor.DefaultMessagingProcessor; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.proxy.remoting.RemotingProtocolServer; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.srvutil.ServerUtil; + +public class ProxyStartup { + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + private static final ProxyStartAndShutdown PROXY_START_AND_SHUTDOWN = new ProxyStartAndShutdown(); + + private static class ProxyStartAndShutdown extends AbstractStartAndShutdown { + @Override + public void appendStartAndShutdown(StartAndShutdown startAndShutdown) { + super.appendStartAndShutdown(startAndShutdown); + } + } + + public static void main(String[] args) { + try { + // parse argument from command line + CommandLineArgument commandLineArgument = parseCommandLineArgument(args); + initConfiguration(commandLineArgument); + + // init thread pool monitor for proxy. + initThreadPoolMonitor(); + + ThreadPoolExecutor executor = createServerExecutor(); + + MessagingProcessor messagingProcessor = createMessagingProcessor(); + + List accessValidators = loadAccessValidators(); + // create grpcServer + GrpcServer grpcServer = GrpcServerBuilder.newBuilder(executor, ConfigurationManager.getProxyConfig().getGrpcServerPort()) + .addService(createServiceProcessor(messagingProcessor)) + .addService(ChannelzService.newInstance(100)) + .addService(ProtoReflectionService.newInstance()) + .configInterceptor(accessValidators) + .shutdownTime(ConfigurationManager.getProxyConfig().getGrpcShutdownTimeSeconds(), TimeUnit.SECONDS) + .build(); + PROXY_START_AND_SHUTDOWN.appendStartAndShutdown(grpcServer); + + RemotingProtocolServer remotingServer = new RemotingProtocolServer(messagingProcessor, accessValidators); + PROXY_START_AND_SHUTDOWN.appendStartAndShutdown(remotingServer); + + // start servers one by one. + PROXY_START_AND_SHUTDOWN.start(); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + log.info("try to shutdown server"); + try { + PROXY_START_AND_SHUTDOWN.preShutdown(); + PROXY_START_AND_SHUTDOWN.shutdown(); + } catch (Exception e) { + log.error("err when shutdown rocketmq-proxy", e); + } + })); + } catch (Exception e) { + e.printStackTrace(); + log.error("find an unexpect err.", e); + System.exit(1); + } + + System.out.printf("%s%n", new Date() + " rocketmq-proxy startup successfully"); + log.info(new Date() + " rocketmq-proxy startup successfully"); + } + + protected static List loadAccessValidators() { + List accessValidators = ServiceProvider.load(AccessValidator.class); + if (accessValidators.isEmpty()) { + log.info("ServiceProvider loaded no AccessValidator, using default org.apache.rocketmq.acl.plain.PlainAccessValidator"); + accessValidators.add(new PlainAccessValidator()); + } + return accessValidators; + } + + protected static void initConfiguration(CommandLineArgument commandLineArgument) throws Exception { + if (StringUtils.isNotBlank(commandLineArgument.getProxyConfigPath())) { + System.setProperty(Configuration.CONFIG_PATH_PROPERTY, commandLineArgument.getProxyConfigPath()); + } + ConfigurationManager.initEnv(); + ConfigurationManager.intConfig(); + setConfigFromCommandLineArgument(commandLineArgument); + log.info("Current configuration: " + ConfigurationManager.formatProxyConfig()); + + } + + protected static CommandLineArgument parseCommandLineArgument(String[] args) { + CommandLine commandLine = ServerUtil.parseCmdLine("mqproxy", args, + buildCommandlineOptions(), new DefaultParser()); + if (commandLine == null) { + throw new RuntimeException("parse command line argument failed"); + } + + CommandLineArgument commandLineArgument = new CommandLineArgument(); + MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), commandLineArgument); + return commandLineArgument; + } + + private static Options buildCommandlineOptions() { + Options options = ServerUtil.buildCommandlineOptions(new Options()); + + Option opt = new Option("bc", "brokerConfigPath", true, "Broker config file path for local mode"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("pc", "proxyConfigPath", true, "Proxy config file path"); + opt.setRequired(false); + options.addOption(opt); + + opt = new Option("pm", "proxyMode", true, "Proxy run in local or cluster mode"); + opt.setRequired(false); + options.addOption(opt); + + return options; + } + + private static void setConfigFromCommandLineArgument(CommandLineArgument commandLineArgument) { + if (StringUtils.isNotBlank(commandLineArgument.getNamesrvAddr())) { + ConfigurationManager.getProxyConfig().setNamesrvAddr(commandLineArgument.getNamesrvAddr()); + } + if (StringUtils.isNotBlank(commandLineArgument.getBrokerConfigPath())) { + ConfigurationManager.getProxyConfig().setBrokerConfigPath(commandLineArgument.getBrokerConfigPath()); + } + if (StringUtils.isNotBlank(commandLineArgument.getProxyMode())) { + ConfigurationManager.getProxyConfig().setProxyMode(commandLineArgument.getProxyMode()); + } + } + + protected static MessagingProcessor createMessagingProcessor() { + String proxyModeStr = ConfigurationManager.getProxyConfig().getProxyMode(); + MessagingProcessor messagingProcessor; + + if (ProxyMode.isClusterMode(proxyModeStr)) { + messagingProcessor = DefaultMessagingProcessor.createForClusterMode(); + ProxyMetricsManager proxyMetricsManager = ProxyMetricsManager.initClusterMode(ConfigurationManager.getProxyConfig()); + PROXY_START_AND_SHUTDOWN.appendStartAndShutdown(proxyMetricsManager); + } else if (ProxyMode.isLocalMode(proxyModeStr)) { + BrokerController brokerController = createBrokerController(); + ProxyMetricsManager.initLocalMode(brokerController.getBrokerMetricsManager(), ConfigurationManager.getProxyConfig()); + StartAndShutdown brokerControllerWrapper = new StartAndShutdown() { + @Override + public void start() throws Exception { + brokerController.start(); + String tip = "The broker[" + brokerController.getBrokerConfig().getBrokerName() + ", " + + brokerController.getBrokerAddr() + "] boot success. serializeType=" + RemotingCommand.getSerializeTypeConfigInThisServer(); + if (null != brokerController.getBrokerConfig().getNamesrvAddr()) { + tip += " and name server is " + brokerController.getBrokerConfig().getNamesrvAddr(); + } + log.info(tip); + } + + @Override + public void shutdown() throws Exception { + brokerController.shutdown(); + } + }; + PROXY_START_AND_SHUTDOWN.appendStartAndShutdown(brokerControllerWrapper); + messagingProcessor = DefaultMessagingProcessor.createForLocalMode(brokerController); + } else { + throw new IllegalArgumentException("try to start grpc server with wrong mode, use 'local' or 'cluster'"); + } + PROXY_START_AND_SHUTDOWN.appendStartAndShutdown(messagingProcessor); + return messagingProcessor; + } + + private static GrpcMessagingApplication createServiceProcessor(MessagingProcessor messagingProcessor) { + GrpcMessagingApplication application = GrpcMessagingApplication.create(messagingProcessor); + PROXY_START_AND_SHUTDOWN.appendStartAndShutdown(application); + return application; + } + + protected static BrokerController createBrokerController() { + ProxyConfig config = ConfigurationManager.getProxyConfig(); + List brokerStartupArgList = Lists.newArrayList("-c", config.getBrokerConfigPath()); + if (StringUtils.isNotBlank(config.getNamesrvAddr())) { + brokerStartupArgList.add("-n"); + brokerStartupArgList.add(config.getNamesrvAddr()); + } + String[] brokerStartupArgs = brokerStartupArgList.toArray(new String[0]); + return BrokerStartup.createBrokerController(brokerStartupArgs); + } + + public static ThreadPoolExecutor createServerExecutor() { + ProxyConfig config = ConfigurationManager.getProxyConfig(); + int threadPoolNums = config.getGrpcThreadPoolNums(); + int threadPoolQueueCapacity = config.getGrpcThreadPoolQueueCapacity(); + ThreadPoolExecutor executor = ThreadPoolMonitor.createAndMonitor( + threadPoolNums, + threadPoolNums, + 1, TimeUnit.MINUTES, + "GrpcRequestExecutorThread", + threadPoolQueueCapacity + ); + PROXY_START_AND_SHUTDOWN.appendShutdown(executor::shutdown); + return executor; + } + + public static void initThreadPoolMonitor() { + ProxyConfig config = ConfigurationManager.getProxyConfig(); + ThreadPoolMonitor.config( + LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME), + LoggerFactory.getLogger(LoggerName.PROXY_WATER_MARK_LOGGER_NAME), + config.isEnablePrintJstack(), config.getPrintJstackInMillis(), + config.getPrintThreadPoolStatusInMillis()); + ThreadPoolMonitor.init(); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/common/AbstractCacheLoader.java b/proxy/src/main/java/org/apache/rocketmq/proxy/common/AbstractCacheLoader.java new file mode 100644 index 00000000000..581caffdbdd --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/common/AbstractCacheLoader.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.common; + +import com.google.common.cache.CacheLoader; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListenableFutureTask; +import java.util.concurrent.ThreadPoolExecutor; +import javax.annotation.Nonnull; + +public abstract class AbstractCacheLoader extends CacheLoader { + private final ThreadPoolExecutor cacheRefreshExecutor; + + public AbstractCacheLoader(ThreadPoolExecutor cacheRefreshExecutor) { + this.cacheRefreshExecutor = cacheRefreshExecutor; + } + + @Override + public ListenableFuture reload(@Nonnull K key, @Nonnull V oldValue) throws Exception { + ListenableFutureTask task = ListenableFutureTask.create(() -> { + try { + return getDirectly(key); + } catch (Exception e) { + onErr(key, e); + return oldValue; + } + }); + cacheRefreshExecutor.execute(task); + return task; + } + + @Override + public V load(@Nonnull K key) throws Exception { + return getDirectly(key); + } + + protected abstract V getDirectly(K key) throws Exception; + + protected abstract void onErr(K key, Exception e); +} \ No newline at end of file diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/common/Address.java b/proxy/src/main/java/org/apache/rocketmq/proxy/common/Address.java new file mode 100644 index 00000000000..2fc1dab40ed --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/common/Address.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.common; + +import com.google.common.net.HostAndPort; +import java.util.Objects; + +public class Address { + + public enum AddressScheme { + IPv4, + IPv6, + DOMAIN_NAME, + UNRECOGNIZED + } + + private AddressScheme addressScheme; + private HostAndPort hostAndPort; + + public Address(AddressScheme addressScheme, HostAndPort hostAndPort) { + this.addressScheme = addressScheme; + this.hostAndPort = hostAndPort; + } + + public AddressScheme getAddressScheme() { + return addressScheme; + } + + public void setAddressScheme(AddressScheme addressScheme) { + this.addressScheme = addressScheme; + } + + public HostAndPort getHostAndPort() { + return hostAndPort; + } + + public void setHostAndPort(HostAndPort hostAndPort) { + this.hostAndPort = hostAndPort; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Address address = (Address) o; + return addressScheme == address.addressScheme && Objects.equals(hostAndPort, address.hostAndPort); + } + + @Override + public int hashCode() { + return Objects.hash(addressScheme, hostAndPort); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/common/ContextVariable.java b/proxy/src/main/java/org/apache/rocketmq/proxy/common/ContextVariable.java new file mode 100644 index 00000000000..93b4eacd8ad --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/common/ContextVariable.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.common; + +public class ContextVariable { + public static final String REMOTE_ADDRESS = "remote-address"; + public static final String LOCAL_ADDRESS = "local-address"; + public static final String CLIENT_ID = "client-id"; + public static final String CHANNEL = "channel"; + public static final String LANGUAGE = "language"; + public static final String CLIENT_VERSION = "client-version"; + public static final String REMAINING_MS = "remaining-ms"; + public static final String ACTION = "action"; + public static final String PROTOCOL_TYPE = "protocol-type"; + public static final String NAMESPACE = "namespace"; +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/common/MessageReceiptHandle.java b/proxy/src/main/java/org/apache/rocketmq/proxy/common/MessageReceiptHandle.java new file mode 100644 index 00000000000..c015e9f53f3 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/common/MessageReceiptHandle.java @@ -0,0 +1,155 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.common; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.rocketmq.common.consumer.ReceiptHandle; + +public class MessageReceiptHandle { + private final String group; + private final String topic; + private final int queueId; + private final String messageId; + private final long queueOffset; + private final String originalReceiptHandleStr; + private final ReceiptHandle originalReceiptHandle; + private final int reconsumeTimes; + + private final AtomicInteger renewRetryTimes = new AtomicInteger(0); + private final AtomicInteger renewTimes = new AtomicInteger(0); + private final long consumeTimestamp; + private volatile String receiptHandleStr; + + public MessageReceiptHandle(String group, String topic, int queueId, String receiptHandleStr, String messageId, + long queueOffset, int reconsumeTimes) { + this.originalReceiptHandle = ReceiptHandle.decode(receiptHandleStr); + this.group = group; + this.topic = topic; + this.queueId = queueId; + this.receiptHandleStr = receiptHandleStr; + this.originalReceiptHandleStr = receiptHandleStr; + this.messageId = messageId; + this.queueOffset = queueOffset; + this.reconsumeTimes = reconsumeTimes; + this.consumeTimestamp = originalReceiptHandle.getRetrieveTime(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MessageReceiptHandle handle = (MessageReceiptHandle) o; + return queueId == handle.queueId && queueOffset == handle.queueOffset && consumeTimestamp == handle.consumeTimestamp + && reconsumeTimes == handle.reconsumeTimes + && Objects.equal(group, handle.group) && Objects.equal(topic, handle.topic) + && Objects.equal(messageId, handle.messageId) && Objects.equal(originalReceiptHandleStr, handle.originalReceiptHandleStr) + && Objects.equal(receiptHandleStr, handle.receiptHandleStr); + } + + @Override + public int hashCode() { + return Objects.hashCode(group, topic, queueId, messageId, queueOffset, originalReceiptHandleStr, consumeTimestamp, + reconsumeTimes, receiptHandleStr); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("group", group) + .add("topic", topic) + .add("queueId", queueId) + .add("messageId", messageId) + .add("queueOffset", queueOffset) + .add("originalReceiptHandleStr", originalReceiptHandleStr) + .add("reconsumeTimes", reconsumeTimes) + .add("renewRetryTimes", renewRetryTimes) + .add("firstConsumeTimestamp", consumeTimestamp) + .add("receiptHandleStr", receiptHandleStr) + .toString(); + } + + public String getGroup() { + return group; + } + + public String getTopic() { + return topic; + } + + public int getQueueId() { + return queueId; + } + + public String getReceiptHandleStr() { + return receiptHandleStr; + } + + public String getOriginalReceiptHandleStr() { + return originalReceiptHandleStr; + } + + public String getMessageId() { + return messageId; + } + + public long getQueueOffset() { + return queueOffset; + } + + public int getReconsumeTimes() { + return reconsumeTimes; + } + + public long getConsumeTimestamp() { + return consumeTimestamp; + } + + public void updateReceiptHandle(String receiptHandleStr) { + this.receiptHandleStr = receiptHandleStr; + } + + public int incrementAndGetRenewRetryTimes() { + return this.renewRetryTimes.incrementAndGet(); + } + + public int incrementRenewTimes() { + return this.renewTimes.incrementAndGet(); + } + + public int getRenewTimes() { + return this.renewTimes.get(); + } + + public void resetRenewRetryTimes() { + this.renewRetryTimes.set(0); + } + + public int getRenewRetryTimes() { + return this.renewRetryTimes.get(); + } + + public ReceiptHandle getOriginalReceiptHandle() { + return originalReceiptHandle; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/common/ProxyContext.java b/proxy/src/main/java/org/apache/rocketmq/proxy/common/ProxyContext.java new file mode 100644 index 00000000000..e6fc989fccb --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/common/ProxyContext.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.common; + +import io.netty.channel.Channel; +import java.util.HashMap; +import java.util.Map; + +public class ProxyContext { + public static final String INNER_ACTION_PREFIX = "Inner"; + private final Map value = new HashMap<>(); + + public static ProxyContext create() { + return new ProxyContext(); + } + + public static ProxyContext createForInner(String actionName) { + return create().setAction(INNER_ACTION_PREFIX + actionName); + } + + public static ProxyContext createForInner(Class clazz) { + return createForInner(clazz.getSimpleName()); + } + + public Map getValue() { + return this.value; + } + + public ProxyContext withVal(String key, Object val) { + this.value.put(key, val); + return this; + } + + public T getVal(String key) { + return (T) this.value.get(key); + } + + public ProxyContext setLocalAddress(String localAddress) { + this.withVal(ContextVariable.LOCAL_ADDRESS, localAddress); + return this; + } + + public String getLocalAddress() { + return this.getVal(ContextVariable.LOCAL_ADDRESS); + } + + public ProxyContext setRemoteAddress(String remoteAddress) { + this.withVal(ContextVariable.REMOTE_ADDRESS, remoteAddress); + return this; + } + + public String getRemoteAddress() { + return this.getVal(ContextVariable.REMOTE_ADDRESS); + } + + public ProxyContext setClientID(String clientID) { + this.withVal(ContextVariable.CLIENT_ID, clientID); + return this; + } + + public String getClientID() { + return this.getVal(ContextVariable.CLIENT_ID); + } + + public ProxyContext setChannel(Channel channel) { + this.withVal(ContextVariable.CHANNEL, channel); + return this; + } + + public Channel getChannel() { + return this.getVal(ContextVariable.CHANNEL); + } + + public ProxyContext setLanguage(String language) { + this.withVal(ContextVariable.LANGUAGE, language); + return this; + } + + public String getLanguage() { + return this.getVal(ContextVariable.LANGUAGE); + } + + public ProxyContext setClientVersion(String clientVersion) { + this.withVal(ContextVariable.CLIENT_VERSION, clientVersion); + return this; + } + + public String getClientVersion() { + return this.getVal(ContextVariable.CLIENT_VERSION); + } + + public ProxyContext setRemainingMs(Long remainingMs) { + this.withVal(ContextVariable.REMAINING_MS, remainingMs); + return this; + } + + public Long getRemainingMs() { + return this.getVal(ContextVariable.REMAINING_MS); + } + + public ProxyContext setAction(String action) { + this.withVal(ContextVariable.ACTION, action); + return this; + } + + public String getAction() { + return this.getVal(ContextVariable.ACTION); + } + + public ProxyContext setProtocolType(String protocol) { + this.withVal(ContextVariable.PROTOCOL_TYPE, protocol); + return this; + } + + public String getProtocolType() { + return this.getVal(ContextVariable.PROTOCOL_TYPE); + } + + public ProxyContext setNamespace(String namespace) { + this.withVal(ContextVariable.NAMESPACE, namespace); + return this; + } + + public String getNamespace() { + return this.getVal(ContextVariable.NAMESPACE); + } + +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/common/ProxyException.java b/proxy/src/main/java/org/apache/rocketmq/proxy/common/ProxyException.java new file mode 100644 index 00000000000..af528329fd5 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/common/ProxyException.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.common; + +public class ProxyException extends RuntimeException { + + private final ProxyExceptionCode code; + + public ProxyException(ProxyExceptionCode code, String message) { + super(message); + this.code = code; + } + + public ProxyException(ProxyExceptionCode code, String message, Throwable cause) { + super(message, cause); + this.code = code; + } + + public ProxyExceptionCode getCode() { + return code; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/common/ProxyExceptionCode.java b/proxy/src/main/java/org/apache/rocketmq/proxy/common/ProxyExceptionCode.java new file mode 100644 index 00000000000..4f91388215c --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/common/ProxyExceptionCode.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.common; + +public enum ProxyExceptionCode { + INVALID_BROKER_NAME, + TRANSACTION_DATA_NOT_FOUND, + FORBIDDEN, + MESSAGE_PROPERTY_CONFLICT_WITH_TYPE, + INVALID_RECEIPT_HANDLE, + INTERNAL_SERVER_ERROR, +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/common/ReceiptHandleGroup.java b/proxy/src/main/java/org/apache/rocketmq/proxy/common/ReceiptHandleGroup.java new file mode 100644 index 00000000000..6fee38d117b --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/common/ReceiptHandleGroup.java @@ -0,0 +1,296 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.common; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.rocketmq.common.consumer.ReceiptHandle; +import org.apache.rocketmq.common.utils.ConcurrentHashMapUtils; +import org.apache.rocketmq.proxy.config.ConfigurationManager; + +public class ReceiptHandleGroup { + + // The messages having the same messageId will be deduplicated based on the parameters of broker, queueId, and offset + protected final Map> receiptHandleMap = new ConcurrentHashMap<>(); + + public static class HandleKey { + private final String originalHandle; + private final String broker; + private final int queueId; + private final long offset; + + public HandleKey(String handle) { + this(ReceiptHandle.decode(handle)); + } + + public HandleKey(ReceiptHandle receiptHandle) { + this.originalHandle = receiptHandle.getReceiptHandle(); + this.broker = receiptHandle.getBrokerName(); + this.queueId = receiptHandle.getQueueId(); + this.offset = receiptHandle.getOffset(); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + HandleKey key = (HandleKey) o; + return queueId == key.queueId && offset == key.offset && Objects.equal(broker, key.broker); + } + + @Override + public int hashCode() { + return Objects.hashCode(broker, queueId, offset); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .append("originalHandle", originalHandle) + .append("broker", broker) + .append("queueId", queueId) + .append("offset", offset) + .toString(); + } + + public String getOriginalHandle() { + return originalHandle; + } + + public String getBroker() { + return broker; + } + + public int getQueueId() { + return queueId; + } + + public long getOffset() { + return offset; + } + } + + public static class HandleData { + private final Semaphore semaphore = new Semaphore(1); + private volatile boolean needRemove = false; + private volatile MessageReceiptHandle messageReceiptHandle; + + public HandleData(MessageReceiptHandle messageReceiptHandle) { + this.messageReceiptHandle = messageReceiptHandle; + } + + public boolean lock(long timeoutMs) { + try { + return this.semaphore.tryAcquire(timeoutMs, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + return false; + } + } + + public void unlock() { + this.semaphore.release(); + } + + public MessageReceiptHandle getMessageReceiptHandle() { + return messageReceiptHandle; + } + + @Override + public boolean equals(Object o) { + return this == o; + } + + @Override + public int hashCode() { + return Objects.hashCode(semaphore, needRemove, messageReceiptHandle); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("semaphore", semaphore) + .add("needRemove", needRemove) + .add("messageReceiptHandle", messageReceiptHandle) + .toString(); + } + } + + public void put(String msgID, MessageReceiptHandle value) { + long timeout = ConfigurationManager.getProxyConfig().getLockTimeoutMsInHandleGroup(); + Map handleMap = ConcurrentHashMapUtils.computeIfAbsent((ConcurrentHashMap>) this.receiptHandleMap, + msgID, msgIDKey -> new ConcurrentHashMap<>()); + handleMap.compute(new HandleKey(value.getOriginalReceiptHandle()), (handleKey, handleData) -> { + if (handleData == null || handleData.needRemove) { + return new HandleData(value); + } + if (!handleData.lock(timeout)) { + throw new ProxyException(ProxyExceptionCode.INTERNAL_SERVER_ERROR, "try to put handle failed"); + } + try { + if (handleData.needRemove) { + return new HandleData(value); + } + handleData.messageReceiptHandle = value; + } finally { + handleData.unlock(); + } + return handleData; + }); + } + + public boolean isEmpty() { + return this.receiptHandleMap.isEmpty(); + } + + public MessageReceiptHandle get(String msgID, String handle) { + Map handleMap = this.receiptHandleMap.get(msgID); + if (handleMap == null) { + return null; + } + long timeout = ConfigurationManager.getProxyConfig().getLockTimeoutMsInHandleGroup(); + AtomicReference res = new AtomicReference<>(); + handleMap.computeIfPresent(new HandleKey(handle), (handleKey, handleData) -> { + if (!handleData.lock(timeout)) { + throw new ProxyException(ProxyExceptionCode.INTERNAL_SERVER_ERROR, "try to get handle failed"); + } + try { + if (handleData.needRemove) { + return null; + } + res.set(handleData.messageReceiptHandle); + } finally { + handleData.unlock(); + } + return handleData; + }); + return res.get(); + } + + public MessageReceiptHandle remove(String msgID, String handle) { + Map handleMap = this.receiptHandleMap.get(msgID); + if (handleMap == null) { + return null; + } + long timeout = ConfigurationManager.getProxyConfig().getLockTimeoutMsInHandleGroup(); + AtomicReference res = new AtomicReference<>(); + handleMap.computeIfPresent(new HandleKey(handle), (handleKey, handleData) -> { + if (!handleData.lock(timeout)) { + throw new ProxyException(ProxyExceptionCode.INTERNAL_SERVER_ERROR, "try to remove and get handle failed"); + } + try { + if (!handleData.needRemove) { + handleData.needRemove = true; + res.set(handleData.messageReceiptHandle); + } + return null; + } finally { + handleData.unlock(); + } + }); + removeHandleMapKeyIfNeed(msgID); + return res.get(); + } + + public MessageReceiptHandle removeOne(String msgID) { + Map handleMap = this.receiptHandleMap.get(msgID); + if (handleMap == null) { + return null; + } + Set keys = handleMap.keySet(); + for (HandleKey key : keys) { + MessageReceiptHandle res = this.remove(msgID, key.originalHandle); + if (res != null) { + return res; + } + } + return null; + } + + public void computeIfPresent(String msgID, String handle, + Function> function) { + Map handleMap = this.receiptHandleMap.get(msgID); + if (handleMap == null) { + return; + } + long timeout = ConfigurationManager.getProxyConfig().getLockTimeoutMsInHandleGroup(); + handleMap.computeIfPresent(new HandleKey(handle), (handleKey, handleData) -> { + if (!handleData.lock(timeout)) { + throw new ProxyException(ProxyExceptionCode.INTERNAL_SERVER_ERROR, "try to compute failed"); + } + CompletableFuture future = function.apply(handleData.messageReceiptHandle); + future.whenComplete((messageReceiptHandle, throwable) -> { + try { + if (throwable != null) { + return; + } + if (messageReceiptHandle == null) { + handleData.needRemove = true; + } else { + handleData.messageReceiptHandle = messageReceiptHandle; + } + } finally { + handleData.unlock(); + } + if (handleData.needRemove) { + handleMap.remove(handleKey, handleData); + } + removeHandleMapKeyIfNeed(msgID); + }); + return handleData; + }); + } + + protected void removeHandleMapKeyIfNeed(String msgID) { + this.receiptHandleMap.computeIfPresent(msgID, (msgIDKey, handleMap) -> { + if (handleMap.isEmpty()) { + return null; + } + return handleMap; + }); + } + + public interface DataScanner { + void onData(String msgID, String handle, MessageReceiptHandle receiptHandle); + } + + public void scan(DataScanner scanner) { + this.receiptHandleMap.forEach((msgID, handleMap) -> { + handleMap.forEach((handleKey, v) -> { + scanner.onData(msgID, handleKey.originalHandle, v.messageReceiptHandle); + }); + }); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("receiptHandleMap", receiptHandleMap) + .toString(); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/common/ReceiptHandleGroupKey.java b/proxy/src/main/java/org/apache/rocketmq/proxy/common/ReceiptHandleGroupKey.java new file mode 100644 index 00000000000..bd28393e5ef --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/common/ReceiptHandleGroupKey.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.common; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import io.netty.channel.Channel; + +public class ReceiptHandleGroupKey { + protected final Channel channel; + protected final String group; + + public ReceiptHandleGroupKey(Channel channel, String group) { + this.channel = channel; + this.group = group; + } + + protected String getChannelId() { + return channel.id().asLongText(); + } + + public String getGroup() { + return group; + } + + public Channel getChannel() { + return channel; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ReceiptHandleGroupKey key = (ReceiptHandleGroupKey) o; + return Objects.equal(getChannelId(), key.getChannelId()) && Objects.equal(group, key.group); + } + + @Override + public int hashCode() { + return Objects.hashCode(getChannelId(), group); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("channelId", getChannelId()) + .add("group", group) + .toString(); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/common/RenewEvent.java b/proxy/src/main/java/org/apache/rocketmq/proxy/common/RenewEvent.java new file mode 100644 index 00000000000..8d591560a7d --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/common/RenewEvent.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.common; + +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.client.consumer.AckResult; + +public class RenewEvent { + protected ReceiptHandleGroupKey key; + protected MessageReceiptHandle messageReceiptHandle; + protected long renewTime; + protected EventType eventType; + protected CompletableFuture future; + + public enum EventType { + RENEW, + STOP_RENEW, + CLEAR_GROUP + } + + public RenewEvent(ReceiptHandleGroupKey key, MessageReceiptHandle messageReceiptHandle, long renewTime, + EventType eventType, CompletableFuture future) { + this.key = key; + this.messageReceiptHandle = messageReceiptHandle; + this.renewTime = renewTime; + this.eventType = eventType; + this.future = future; + } + + public ReceiptHandleGroupKey getKey() { + return key; + } + + public MessageReceiptHandle getMessageReceiptHandle() { + return messageReceiptHandle; + } + + public long getRenewTime() { + return renewTime; + } + + public EventType getEventType() { + return eventType; + } + + public CompletableFuture getFuture() { + return future; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/common/RenewStrategyPolicy.java b/proxy/src/main/java/org/apache/rocketmq/proxy/common/RenewStrategyPolicy.java new file mode 100644 index 00000000000..ce33619b4d2 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/common/RenewStrategyPolicy.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.common; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.remoting.protocol.subscription.RetryPolicy; + +import java.util.concurrent.TimeUnit; + + +public class RenewStrategyPolicy implements RetryPolicy { + // 1m 3m 5m 6m 10m 30m 1h + private long[] next = new long[]{ + TimeUnit.MINUTES.toMillis(1), + TimeUnit.MINUTES.toMillis(3), + TimeUnit.MINUTES.toMillis(5), + TimeUnit.MINUTES.toMillis(10), + TimeUnit.MINUTES.toMillis(30), + TimeUnit.HOURS.toMillis(1) + }; + + public RenewStrategyPolicy() { + } + + public RenewStrategyPolicy(long[] next) { + this.next = next; + } + + public long[] getNext() { + return next; + } + + public void setNext(long[] next) { + this.next = next; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("next", next) + .toString(); + } + + @Override + public long nextDelayDuration(int renewTimes) { + if (renewTimes < 0) { + renewTimes = 0; + } + int index = renewTimes; + if (index >= next.length) { + index = next.length - 1; + } + return next[index]; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/common/channel/ChannelHelper.java b/proxy/src/main/java/org/apache/rocketmq/proxy/common/channel/ChannelHelper.java new file mode 100644 index 00000000000..dd15c85fb2b --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/common/channel/ChannelHelper.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.common.channel; + +import io.netty.channel.Channel; +import org.apache.rocketmq.proxy.grpc.v2.channel.GrpcClientChannel; +import org.apache.rocketmq.proxy.processor.channel.ChannelProtocolType; +import org.apache.rocketmq.proxy.processor.channel.RemoteChannel; +import org.apache.rocketmq.proxy.remoting.channel.RemotingChannel; + +public class ChannelHelper { + + /** + * judge channel is sync from other proxy or not + * + * @param channel channel + * @return true if is sync from other proxy + */ + public static boolean isRemote(Channel channel) { + return channel instanceof RemoteChannel; + } + + public static ChannelProtocolType getChannelProtocolType(Channel channel) { + if (channel instanceof GrpcClientChannel) { + return ChannelProtocolType.GRPC_V2; + } else if (channel instanceof RemotingChannel) { + return ChannelProtocolType.REMOTING; + } else if (channel instanceof RemoteChannel) { + RemoteChannel remoteChannel = (RemoteChannel) channel; + return remoteChannel.getType(); + } + return ChannelProtocolType.UNKNOWN; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/common/utils/ExceptionUtils.java b/proxy/src/main/java/org/apache/rocketmq/proxy/common/utils/ExceptionUtils.java new file mode 100644 index 00000000000..e85360a5daf --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/common/utils/ExceptionUtils.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.common.utils; + +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; + +public class ExceptionUtils { + + public static Throwable getRealException(Throwable throwable) { + if (throwable instanceof CompletionException || throwable instanceof ExecutionException) { + if (throwable.getCause() != null) { + throwable = throwable.getCause(); + } + } + return throwable; + } + + public static String getErrorDetailMessage(Throwable t) { + if (t == null) { + return null; + } + StringBuilder sb = new StringBuilder(); + sb.append(t.getMessage()).append(". ").append(t.getClass().getSimpleName()); + + if (t.getStackTrace().length > 0) { + sb.append(". ").append(t.getStackTrace()[0]); + } + return sb.toString(); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/common/utils/FilterUtils.java b/proxy/src/main/java/org/apache/rocketmq/proxy/common/utils/FilterUtils.java new file mode 100644 index 00000000000..9e44aceaec0 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/common/utils/FilterUtils.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.common.utils; + +import java.util.Set; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; + +public class FilterUtils { + /** + * Whether the message's tag matches consumerGroup's SubscriptionData + * + * @param tagsSet, tagSet in {@link SubscriptionData}, tagSet empty means SubscriptionData.SUB_ALL(*) + * @param tags, message's tags, null means not tag attached to the message. + */ + public static boolean isTagMatched(Set tagsSet, String tags) { + if (tagsSet.isEmpty()) { + return true; + } + + if (tags == null) { + return false; + } + + return tagsSet.contains(tags); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/common/utils/FutureUtils.java b/proxy/src/main/java/org/apache/rocketmq/proxy/common/utils/FutureUtils.java new file mode 100644 index 00000000000..ea50e64eeaa --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/common/utils/FutureUtils.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.common.utils; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; + +public class FutureUtils { + + public static CompletableFuture appendNextFuture(CompletableFuture future, + CompletableFuture nextFuture, ExecutorService executor) { + future.whenCompleteAsync((t, throwable) -> { + if (throwable != null) { + nextFuture.completeExceptionally(throwable); + } else { + nextFuture.complete(t); + } + }, executor); + return nextFuture; + } + + public static CompletableFuture addExecutor(CompletableFuture future, ExecutorService executor) { + return appendNextFuture(future, new CompletableFuture<>(), executor); + } + + public static CompletableFuture completeExceptionally(Throwable t) { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(t); + return future; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/common/utils/ProxyUtils.java b/proxy/src/main/java/org/apache/rocketmq/proxy/common/utils/ProxyUtils.java new file mode 100644 index 00000000000..7e82a49613b --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/common/utils/ProxyUtils.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.common.utils; + +public class ProxyUtils { + + public static final int MAX_MSG_NUMS_FOR_POP_REQUEST = 32; + + public static final String BROKER_ADDR = "brokerAddr"; +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/config/ConfigFile.java b/proxy/src/main/java/org/apache/rocketmq/proxy/config/ConfigFile.java new file mode 100644 index 00000000000..37757f8d636 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/config/ConfigFile.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.config; + +public interface ConfigFile { + + void initData(); +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/config/Configuration.java b/proxy/src/main/java/org/apache/rocketmq/proxy/config/Configuration.java new file mode 100644 index 00000000000..2561d44190e --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/config/Configuration.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.config; + +import com.alibaba.fastjson.JSON; +import com.google.common.base.Charsets; +import com.google.common.io.CharStreams; +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class Configuration { + private final static Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + private final AtomicReference proxyConfigReference = new AtomicReference<>(); + public static final String CONFIG_PATH_PROPERTY = "com.rocketmq.proxy.configPath"; + + public void init() throws Exception { + String proxyConfigData = loadJsonConfig(); + + ProxyConfig proxyConfig = JSON.parseObject(proxyConfigData, ProxyConfig.class); + proxyConfig.initData(); + setProxyConfig(proxyConfig); + } + + public static String loadJsonConfig() throws Exception { + String configFileName = ProxyConfig.DEFAULT_CONFIG_FILE_NAME; + String filePath = System.getProperty(CONFIG_PATH_PROPERTY); + if (StringUtils.isBlank(filePath)) { + final String testResource = "rmq-proxy-home/conf/" + configFileName; + try (InputStream inputStream = Configuration.class.getClassLoader().getResourceAsStream(testResource)) { + if (null != inputStream) { + return CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8)); + } + } + filePath = new File(ConfigurationManager.getProxyHome() + File.separator + "conf", configFileName).toString(); + } + + File file = new File(filePath); + log.info("The current configuration file path is {}", filePath); + if (!file.exists()) { + log.warn("the config file {} not exist", filePath); + throw new RuntimeException(String.format("the config file %s not exist", filePath)); + } + long fileLength = file.length(); + if (fileLength <= 0) { + log.warn("the config file {} length is zero", filePath); + throw new RuntimeException(String.format("the config file %s length is zero", filePath)); + } + + return new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + } + + public ProxyConfig getProxyConfig() { + return proxyConfigReference.get(); + } + + public void setProxyConfig(ProxyConfig proxyConfig) { + proxyConfigReference.set(proxyConfig); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/config/ConfigurationManager.java b/proxy/src/main/java/org/apache/rocketmq/proxy/config/ConfigurationManager.java new file mode 100644 index 00000000000..911e1f28f2d --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/config/ConfigurationManager.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.config; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.serializer.SerializerFeature; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.MixAll; + +public class ConfigurationManager { + public static final String RMQ_PROXY_HOME = "RMQ_PROXY_HOME"; + protected static final String DEFAULT_RMQ_PROXY_HOME = System.getenv(MixAll.ROCKETMQ_HOME_ENV); + protected static String proxyHome; + protected static Configuration configuration; + + public static void initEnv() { + proxyHome = System.getenv(RMQ_PROXY_HOME); + if (StringUtils.isEmpty(proxyHome)) { + proxyHome = System.getProperty(RMQ_PROXY_HOME, DEFAULT_RMQ_PROXY_HOME); + } + + if (proxyHome == null) { + proxyHome = "./"; + } + } + + public static void intConfig() throws Exception { + configuration = new Configuration(); + configuration.init(); + } + + public static String getProxyHome() { + return proxyHome; + } + + public static ProxyConfig getProxyConfig() { + return configuration.getProxyConfig(); + } + + public static String formatProxyConfig() { + return JSON.toJSONString(ConfigurationManager.getProxyConfig(), + SerializerFeature.PrettyFormat, SerializerFeature.WriteMapNullValue, SerializerFeature.WriteDateUseDateFormat, SerializerFeature.WriteNullListAsEmpty); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/config/MetricCollectorMode.java b/proxy/src/main/java/org/apache/rocketmq/proxy/config/MetricCollectorMode.java new file mode 100644 index 00000000000..fb5329012db --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/config/MetricCollectorMode.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.config; + +public enum MetricCollectorMode { + /** + * Do not collect the metric from clients. + */ + OFF("off"), + /** + * Collect the metric from clients to the given address. + */ + ON("on"), + /** + * Collect the metric by the proxy itself. + */ + PROXY("proxy"); + + private final String modeString; + + MetricCollectorMode(String modeString) { + this.modeString = modeString; + } + + public String getModeString() { + return modeString; + } + + public static MetricCollectorMode getEnumByString(String modeString) { + for (MetricCollectorMode mode : MetricCollectorMode.values()) { + if (mode.modeString.equals(modeString.toLowerCase())) { + return mode; + } + } + return OFF; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/config/ProxyConfig.java b/proxy/src/main/java/org/apache/rocketmq/proxy/config/ProxyConfig.java new file mode 100644 index 00000000000..e907a1ccc3f --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/config/ProxyConfig.java @@ -0,0 +1,1474 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.config; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.time.Duration; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.metrics.MetricsExporterType; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.ProxyMode; +import org.apache.rocketmq.proxy.common.ProxyException; +import org.apache.rocketmq.proxy.common.ProxyExceptionCode; + +public class ProxyConfig implements ConfigFile { + private final static Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + public final static String DEFAULT_CONFIG_FILE_NAME = "rmq-proxy.json"; + private static final int PROCESSOR_NUMBER = Runtime.getRuntime().availableProcessors(); + private static final String DEFAULT_CLUSTER_NAME = "DefaultCluster"; + + private static String localHostName; + + static { + try { + localHostName = InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + log.error("Failed to obtain the host name", e); + } + } + + private String rocketMQClusterName = DEFAULT_CLUSTER_NAME; + private String proxyClusterName = DEFAULT_CLUSTER_NAME; + private String proxyName = StringUtils.isEmpty(localHostName) ? "DEFAULT_PROXY" : localHostName; + + private String localServeAddr = ""; + + private String heartbeatSyncerTopicClusterName = ""; + private int heartbeatSyncerThreadPoolNums = 4; + private int heartbeatSyncerThreadPoolQueueCapacity = 100; + + private String heartbeatSyncerTopicName = "DefaultHeartBeatSyncerTopic"; + + /** + * configuration for ThreadPoolMonitor + */ + private boolean enablePrintJstack = true; + private long printJstackInMillis = Duration.ofSeconds(60).toMillis(); + private long printThreadPoolStatusInMillis = Duration.ofSeconds(3).toMillis(); + + private String namesrvAddr = System.getProperty(MixAll.NAMESRV_ADDR_PROPERTY, System.getenv(MixAll.NAMESRV_ADDR_ENV)); + private String namesrvDomain = ""; + private String namesrvDomainSubgroup = ""; + /** + * TLS + */ + private boolean tlsTestModeEnable = true; + private String tlsKeyPath = ConfigurationManager.getProxyHome() + "/conf/tls/rocketmq.key"; + private String tlsCertPath = ConfigurationManager.getProxyHome() + "/conf/tls/rocketmq.crt"; + /** + * gRPC + */ + private String proxyMode = ProxyMode.CLUSTER.name(); + private Integer grpcServerPort = 8081; + private long grpcShutdownTimeSeconds = 30; + private int grpcBossLoopNum = 1; + private int grpcWorkerLoopNum = PROCESSOR_NUMBER * 2; + private boolean enableGrpcEpoll = false; + private int grpcThreadPoolNums = 16 + PROCESSOR_NUMBER * 2; + private int grpcThreadPoolQueueCapacity = 100000; + private String brokerConfigPath = ConfigurationManager.getProxyHome() + "/conf/broker.conf"; + /** + * gRPC max message size + * 130M = 4M * 32 messages + 2M attributes + */ + private int grpcMaxInboundMessageSize = 130 * 1024 * 1024; + /** + * max message body size, 0 or negative number means no limit for proxy + */ + private int maxMessageSize = 4 * 1024 * 1024; + /** + * max user property size, 0 or negative number means no limit for proxy + */ + private int maxUserPropertySize = 16 * 1024; + private int userPropertyMaxNum = 128; + + /** + * max message group size, 0 or negative number means no limit for proxy + */ + private int maxMessageGroupSize = 64; + + /** + * When a message pops, the message is invisible by default + */ + private long defaultInvisibleTimeMills = Duration.ofSeconds(60).toMillis(); + private long minInvisibleTimeMillsForRecv = Duration.ofSeconds(10).toMillis(); + private long maxInvisibleTimeMills = Duration.ofHours(12).toMillis(); + private long maxDelayTimeMills = Duration.ofDays(1).toMillis(); + private long maxTransactionRecoverySecond = Duration.ofHours(1).getSeconds(); + private boolean enableTopicMessageTypeCheck = true; + + private int grpcClientProducerMaxAttempts = 3; + private long grpcClientProducerBackoffInitialMillis = 10; + private long grpcClientProducerBackoffMaxMillis = 1000; + private int grpcClientProducerBackoffMultiplier = 2; + private long grpcClientConsumerMinLongPollingTimeoutMillis = Duration.ofSeconds(5).toMillis(); + private long grpcClientConsumerMaxLongPollingTimeoutMillis = Duration.ofSeconds(20).toMillis(); + private int grpcClientConsumerLongPollingBatchSize = 32; + private long grpcClientIdleTimeMills = Duration.ofSeconds(120).toMillis(); + + private int channelExpiredInSeconds = 60; + private int contextExpiredInSeconds = 30; + + private int rocketmqMQClientNum = 6; + + private long grpcProxyRelayRequestTimeoutInSeconds = 5; + private int grpcProducerThreadPoolNums = PROCESSOR_NUMBER; + private int grpcProducerThreadQueueCapacity = 10000; + private int grpcConsumerThreadPoolNums = PROCESSOR_NUMBER; + private int grpcConsumerThreadQueueCapacity = 10000; + private int grpcRouteThreadPoolNums = PROCESSOR_NUMBER; + private int grpcRouteThreadQueueCapacity = 10000; + private int grpcClientManagerThreadPoolNums = PROCESSOR_NUMBER; + private int grpcClientManagerThreadQueueCapacity = 10000; + private int grpcTransactionThreadPoolNums = PROCESSOR_NUMBER; + private int grpcTransactionThreadQueueCapacity = 10000; + + private int producerProcessorThreadPoolNums = PROCESSOR_NUMBER; + private int producerProcessorThreadPoolQueueCapacity = 10000; + private int consumerProcessorThreadPoolNums = PROCESSOR_NUMBER; + private int consumerProcessorThreadPoolQueueCapacity = 10000; + + private boolean useEndpointPortFromRequest = false; + + private int topicRouteServiceCacheExpiredSeconds = 300; + private int topicRouteServiceCacheRefreshSeconds = 20; + private int topicRouteServiceCacheMaxNum = 20000; + private int topicRouteServiceThreadPoolNums = PROCESSOR_NUMBER; + private int topicRouteServiceThreadPoolQueueCapacity = 5000; + private int topicConfigCacheExpiredSeconds = 300; + private int topicConfigCacheRefreshSeconds = 20; + private int topicConfigCacheMaxNum = 20000; + private int subscriptionGroupConfigCacheExpiredSeconds = 300; + private int subscriptionGroupConfigCacheRefreshSeconds = 20; + private int subscriptionGroupConfigCacheMaxNum = 20000; + private int metadataThreadPoolNums = 3; + private int metadataThreadPoolQueueCapacity = 100000; + + private int transactionHeartbeatThreadPoolNums = 20; + private int transactionHeartbeatThreadPoolQueueCapacity = 200; + private int transactionHeartbeatPeriodSecond = 20; + private int transactionHeartbeatBatchNum = 100; + private long transactionDataExpireScanPeriodMillis = Duration.ofSeconds(10).toMillis(); + private long transactionDataMaxWaitClearMillis = Duration.ofSeconds(30).toMillis(); + private long transactionDataExpireMillis = Duration.ofSeconds(30).toMillis(); + private int transactionDataMaxNum = 15; + + private long longPollingReserveTimeInMillis = 100; + + private long invisibleTimeMillisWhenClear = 1000L; + private boolean enableProxyAutoRenew = true; + private int maxRenewRetryTimes = 3; + private int renewThreadPoolNums = 2; + private int renewMaxThreadPoolNums = 4; + private int renewThreadPoolQueueCapacity = 300; + private long lockTimeoutMsInHandleGroup = TimeUnit.SECONDS.toMillis(3); + private long renewAheadTimeMillis = TimeUnit.SECONDS.toMillis(10); + private long renewMaxTimeMillis = TimeUnit.HOURS.toMillis(3); + private long renewSchedulePeriodMillis = TimeUnit.SECONDS.toMillis(5); + + private boolean enableACL = false; + + private boolean enableAclRpcHookForClusterMode = false; + + private boolean useDelayLevel = false; + private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"; + private transient ConcurrentSkipListMap delayLevelTable = new ConcurrentSkipListMap<>(); + + private String metricCollectorMode = MetricCollectorMode.OFF.getModeString(); + // Example address: 127.0.0.1:1234 + private String metricCollectorAddress = ""; + + private String regionId = ""; + + private boolean traceOn = false; + + private MetricsExporterType metricsExporterType = MetricsExporterType.DISABLE; + + private String metricsGrpcExporterTarget = ""; + private String metricsGrpcExporterHeader = ""; + private long metricGrpcExporterTimeOutInMills = 3 * 1000; + private long metricGrpcExporterIntervalInMills = 60 * 1000; + private long metricLoggingExporterIntervalInMills = 10 * 1000; + + private int metricsPromExporterPort = 5557; + private String metricsPromExporterHost = ""; + + // Label pairs in CSV. Each label follows pattern of Key:Value. eg: instance_id:xxx,uid:xxx + private String metricsLabel = ""; + + private boolean metricsInDelta = false; + + private long channelExpiredTimeout = 1000 * 120; + + // remoting + private boolean enableRemotingLocalProxyGrpc = true; + private int localProxyConnectTimeoutMs = 3000; + private String remotingAccessAddr = ""; + private int remotingListenPort = 8080; + + // related to proxy's send strategy in cluster mode. + private boolean sendLatencyEnable = false; + private boolean startDetectorEnable = false; + private int detectTimeout = 200; + private int detectInterval = 2 * 1000; + + private int remotingHeartbeatThreadPoolNums = 2 * PROCESSOR_NUMBER; + private int remotingTopicRouteThreadPoolNums = 2 * PROCESSOR_NUMBER; + private int remotingSendMessageThreadPoolNums = 4 * PROCESSOR_NUMBER; + private int remotingPullMessageThreadPoolNums = 4 * PROCESSOR_NUMBER; + private int remotingUpdateOffsetThreadPoolNums = 4 * PROCESSOR_NUMBER; + private int remotingDefaultThreadPoolNums = 4 * PROCESSOR_NUMBER; + + private int remotingHeartbeatThreadPoolQueueCapacity = 50000; + private int remotingTopicRouteThreadPoolQueueCapacity = 50000; + private int remotingSendThreadPoolQueueCapacity = 10000; + private int remotingPullThreadPoolQueueCapacity = 50000; + private int remotingUpdateOffsetThreadPoolQueueCapacity = 10000; + private int remotingDefaultThreadPoolQueueCapacity = 50000; + + private long remotingWaitTimeMillsInSendQueue = 3 * 1000; + private long remotingWaitTimeMillsInPullQueue = 5 * 1000; + private long remotingWaitTimeMillsInHeartbeatQueue = 31 * 1000; + private long remotingWaitTimeMillsInUpdateOffsetQueue = 3 * 1000; + private long remotingWaitTimeMillsInTopicRouteQueue = 3 * 1000; + private long remotingWaitTimeMillsInDefaultQueue = 3 * 1000; + + private boolean enableBatchAck = false; + + @Override + public void initData() { + parseDelayLevel(); + if (StringUtils.isEmpty(localServeAddr)) { + this.localServeAddr = NetworkUtil.getLocalAddress(); + } + if (StringUtils.isBlank(localServeAddr)) { + throw new ProxyException(ProxyExceptionCode.INTERNAL_SERVER_ERROR, "get local serve ip failed"); + } + if (StringUtils.isBlank(remotingAccessAddr)) { + this.remotingAccessAddr = this.localServeAddr; + } + if (StringUtils.isBlank(heartbeatSyncerTopicClusterName)) { + this.heartbeatSyncerTopicClusterName = this.rocketMQClusterName; + } + } + + public int computeDelayLevel(long timeMillis) { + long intervalMillis = timeMillis - System.currentTimeMillis(); + List> sortedLevels = delayLevelTable.entrySet().stream().sorted(Comparator.comparingLong(Map.Entry::getValue)).collect(Collectors.toList()); + for (Map.Entry entry : sortedLevels) { + if (entry.getValue() > intervalMillis) { + return entry.getKey(); + } + } + return sortedLevels.get(sortedLevels.size() - 1).getKey(); + } + + public void parseDelayLevel() { + this.delayLevelTable = new ConcurrentSkipListMap<>(); + Map timeUnitTable = new HashMap<>(); + timeUnitTable.put("s", 1000L); + timeUnitTable.put("m", 1000L * 60); + timeUnitTable.put("h", 1000L * 60 * 60); + timeUnitTable.put("d", 1000L * 60 * 60 * 24); + + String levelString = this.getMessageDelayLevel(); + try { + String[] levelArray = levelString.split(" "); + for (int i = 0; i < levelArray.length; i++) { + String value = levelArray[i]; + String ch = value.substring(value.length() - 1); + Long tu = timeUnitTable.get(ch); + + int level = i + 1; + long num = Long.parseLong(value.substring(0, value.length() - 1)); + long delayTimeMillis = tu * num; + this.delayLevelTable.put(level, delayTimeMillis); + } + } catch (Exception e) { + log.error("parse delay level failed. messageDelayLevel:{}", messageDelayLevel, e); + } + } + + public String getRocketMQClusterName() { + return rocketMQClusterName; + } + + public void setRocketMQClusterName(String rocketMQClusterName) { + this.rocketMQClusterName = rocketMQClusterName; + } + + public String getProxyClusterName() { + return proxyClusterName; + } + + public void setProxyClusterName(String proxyClusterName) { + this.proxyClusterName = proxyClusterName; + } + + public String getProxyName() { + return proxyName; + } + + public void setProxyName(String proxyName) { + this.proxyName = proxyName; + } + + public String getLocalServeAddr() { + return localServeAddr; + } + + public void setLocalServeAddr(String localServeAddr) { + this.localServeAddr = localServeAddr; + } + + public String getHeartbeatSyncerTopicClusterName() { + return heartbeatSyncerTopicClusterName; + } + + public void setHeartbeatSyncerTopicClusterName(String heartbeatSyncerTopicClusterName) { + this.heartbeatSyncerTopicClusterName = heartbeatSyncerTopicClusterName; + } + + public int getHeartbeatSyncerThreadPoolNums() { + return heartbeatSyncerThreadPoolNums; + } + + public void setHeartbeatSyncerThreadPoolNums(int heartbeatSyncerThreadPoolNums) { + this.heartbeatSyncerThreadPoolNums = heartbeatSyncerThreadPoolNums; + } + + public int getHeartbeatSyncerThreadPoolQueueCapacity() { + return heartbeatSyncerThreadPoolQueueCapacity; + } + + public void setHeartbeatSyncerThreadPoolQueueCapacity(int heartbeatSyncerThreadPoolQueueCapacity) { + this.heartbeatSyncerThreadPoolQueueCapacity = heartbeatSyncerThreadPoolQueueCapacity; + } + + public String getHeartbeatSyncerTopicName() { + return heartbeatSyncerTopicName; + } + + public void setHeartbeatSyncerTopicName(String heartbeatSyncerTopicName) { + this.heartbeatSyncerTopicName = heartbeatSyncerTopicName; + } + + public boolean isEnablePrintJstack() { + return enablePrintJstack; + } + + public void setEnablePrintJstack(boolean enablePrintJstack) { + this.enablePrintJstack = enablePrintJstack; + } + + public long getPrintJstackInMillis() { + return printJstackInMillis; + } + + public void setPrintJstackInMillis(long printJstackInMillis) { + this.printJstackInMillis = printJstackInMillis; + } + + public long getPrintThreadPoolStatusInMillis() { + return printThreadPoolStatusInMillis; + } + + public void setPrintThreadPoolStatusInMillis(long printThreadPoolStatusInMillis) { + this.printThreadPoolStatusInMillis = printThreadPoolStatusInMillis; + } + + public String getNamesrvAddr() { + return namesrvAddr; + } + + public void setNamesrvAddr(String namesrvAddr) { + this.namesrvAddr = namesrvAddr; + } + + public String getNamesrvDomain() { + return namesrvDomain; + } + + public void setNamesrvDomain(String namesrvDomain) { + this.namesrvDomain = namesrvDomain; + } + + public String getNamesrvDomainSubgroup() { + return namesrvDomainSubgroup; + } + + public void setNamesrvDomainSubgroup(String namesrvDomainSubgroup) { + this.namesrvDomainSubgroup = namesrvDomainSubgroup; + } + + public String getProxyMode() { + return proxyMode; + } + + public void setProxyMode(String proxyMode) { + this.proxyMode = proxyMode; + } + + public Integer getGrpcServerPort() { + return grpcServerPort; + } + + public void setGrpcServerPort(Integer grpcServerPort) { + this.grpcServerPort = grpcServerPort; + } + + public long getGrpcShutdownTimeSeconds() { + return grpcShutdownTimeSeconds; + } + + public void setGrpcShutdownTimeSeconds(long grpcShutdownTimeSeconds) { + this.grpcShutdownTimeSeconds = grpcShutdownTimeSeconds; + } + + public boolean isUseEndpointPortFromRequest() { + return useEndpointPortFromRequest; + } + + public void setUseEndpointPortFromRequest(boolean useEndpointPortFromRequest) { + this.useEndpointPortFromRequest = useEndpointPortFromRequest; + } + + public boolean isTlsTestModeEnable() { + return tlsTestModeEnable; + } + + public void setTlsTestModeEnable(boolean tlsTestModeEnable) { + this.tlsTestModeEnable = tlsTestModeEnable; + } + + public String getTlsKeyPath() { + return tlsKeyPath; + } + + public void setTlsKeyPath(String tlsKeyPath) { + this.tlsKeyPath = tlsKeyPath; + } + + public String getTlsCertPath() { + return tlsCertPath; + } + + public void setTlsCertPath(String tlsCertPath) { + this.tlsCertPath = tlsCertPath; + } + + public int getGrpcBossLoopNum() { + return grpcBossLoopNum; + } + + public void setGrpcBossLoopNum(int grpcBossLoopNum) { + this.grpcBossLoopNum = grpcBossLoopNum; + } + + public int getGrpcWorkerLoopNum() { + return grpcWorkerLoopNum; + } + + public void setGrpcWorkerLoopNum(int grpcWorkerLoopNum) { + this.grpcWorkerLoopNum = grpcWorkerLoopNum; + } + + public boolean isEnableGrpcEpoll() { + return enableGrpcEpoll; + } + + public void setEnableGrpcEpoll(boolean enableGrpcEpoll) { + this.enableGrpcEpoll = enableGrpcEpoll; + } + + public int getGrpcThreadPoolNums() { + return grpcThreadPoolNums; + } + + public void setGrpcThreadPoolNums(int grpcThreadPoolNums) { + this.grpcThreadPoolNums = grpcThreadPoolNums; + } + + public int getGrpcThreadPoolQueueCapacity() { + return grpcThreadPoolQueueCapacity; + } + + public void setGrpcThreadPoolQueueCapacity(int grpcThreadPoolQueueCapacity) { + this.grpcThreadPoolQueueCapacity = grpcThreadPoolQueueCapacity; + } + + public String getBrokerConfigPath() { + return brokerConfigPath; + } + + public void setBrokerConfigPath(String brokerConfigPath) { + this.brokerConfigPath = brokerConfigPath; + } + + public int getGrpcMaxInboundMessageSize() { + return grpcMaxInboundMessageSize; + } + + public void setGrpcMaxInboundMessageSize(int grpcMaxInboundMessageSize) { + this.grpcMaxInboundMessageSize = grpcMaxInboundMessageSize; + } + + public int getMaxMessageSize() { + return maxMessageSize; + } + + public void setMaxMessageSize(int maxMessageSize) { + this.maxMessageSize = maxMessageSize; + } + + public int getMaxUserPropertySize() { + return maxUserPropertySize; + } + + public void setMaxUserPropertySize(int maxUserPropertySize) { + this.maxUserPropertySize = maxUserPropertySize; + } + + public int getUserPropertyMaxNum() { + return userPropertyMaxNum; + } + + public void setUserPropertyMaxNum(int userPropertyMaxNum) { + this.userPropertyMaxNum = userPropertyMaxNum; + } + + public int getMaxMessageGroupSize() { + return maxMessageGroupSize; + } + + public void setMaxMessageGroupSize(int maxMessageGroupSize) { + this.maxMessageGroupSize = maxMessageGroupSize; + } + + public long getMinInvisibleTimeMillsForRecv() { + return minInvisibleTimeMillsForRecv; + } + + public void setMinInvisibleTimeMillsForRecv(long minInvisibleTimeMillsForRecv) { + this.minInvisibleTimeMillsForRecv = minInvisibleTimeMillsForRecv; + } + + public long getDefaultInvisibleTimeMills() { + return defaultInvisibleTimeMills; + } + + public void setDefaultInvisibleTimeMills(long defaultInvisibleTimeMills) { + this.defaultInvisibleTimeMills = defaultInvisibleTimeMills; + } + + public long getMaxInvisibleTimeMills() { + return maxInvisibleTimeMills; + } + + public void setMaxInvisibleTimeMills(long maxInvisibleTimeMills) { + this.maxInvisibleTimeMills = maxInvisibleTimeMills; + } + + public long getMaxDelayTimeMills() { + return maxDelayTimeMills; + } + + public void setMaxDelayTimeMills(long maxDelayTimeMills) { + this.maxDelayTimeMills = maxDelayTimeMills; + } + + public long getMaxTransactionRecoverySecond() { + return maxTransactionRecoverySecond; + } + + public void setMaxTransactionRecoverySecond(long maxTransactionRecoverySecond) { + this.maxTransactionRecoverySecond = maxTransactionRecoverySecond; + } + + public int getGrpcClientProducerMaxAttempts() { + return grpcClientProducerMaxAttempts; + } + + public void setGrpcClientProducerMaxAttempts(int grpcClientProducerMaxAttempts) { + this.grpcClientProducerMaxAttempts = grpcClientProducerMaxAttempts; + } + + public long getGrpcClientProducerBackoffInitialMillis() { + return grpcClientProducerBackoffInitialMillis; + } + + public void setGrpcClientProducerBackoffInitialMillis(long grpcClientProducerBackoffInitialMillis) { + this.grpcClientProducerBackoffInitialMillis = grpcClientProducerBackoffInitialMillis; + } + + public long getGrpcClientProducerBackoffMaxMillis() { + return grpcClientProducerBackoffMaxMillis; + } + + public void setGrpcClientProducerBackoffMaxMillis(long grpcClientProducerBackoffMaxMillis) { + this.grpcClientProducerBackoffMaxMillis = grpcClientProducerBackoffMaxMillis; + } + + public int getGrpcClientProducerBackoffMultiplier() { + return grpcClientProducerBackoffMultiplier; + } + + public void setGrpcClientProducerBackoffMultiplier(int grpcClientProducerBackoffMultiplier) { + this.grpcClientProducerBackoffMultiplier = grpcClientProducerBackoffMultiplier; + } + + public long getGrpcClientConsumerMinLongPollingTimeoutMillis() { + return grpcClientConsumerMinLongPollingTimeoutMillis; + } + + public void setGrpcClientConsumerMinLongPollingTimeoutMillis(long grpcClientConsumerMinLongPollingTimeoutMillis) { + this.grpcClientConsumerMinLongPollingTimeoutMillis = grpcClientConsumerMinLongPollingTimeoutMillis; + } + + public long getGrpcClientConsumerMaxLongPollingTimeoutMillis() { + return grpcClientConsumerMaxLongPollingTimeoutMillis; + } + + public void setGrpcClientConsumerMaxLongPollingTimeoutMillis(long grpcClientConsumerMaxLongPollingTimeoutMillis) { + this.grpcClientConsumerMaxLongPollingTimeoutMillis = grpcClientConsumerMaxLongPollingTimeoutMillis; + } + + public int getGrpcClientConsumerLongPollingBatchSize() { + return grpcClientConsumerLongPollingBatchSize; + } + + public void setGrpcClientConsumerLongPollingBatchSize(int grpcClientConsumerLongPollingBatchSize) { + this.grpcClientConsumerLongPollingBatchSize = grpcClientConsumerLongPollingBatchSize; + } + + public int getChannelExpiredInSeconds() { + return channelExpiredInSeconds; + } + + public void setChannelExpiredInSeconds(int channelExpiredInSeconds) { + this.channelExpiredInSeconds = channelExpiredInSeconds; + } + + public int getContextExpiredInSeconds() { + return contextExpiredInSeconds; + } + + public void setContextExpiredInSeconds(int contextExpiredInSeconds) { + this.contextExpiredInSeconds = contextExpiredInSeconds; + } + + public int getRocketmqMQClientNum() { + return rocketmqMQClientNum; + } + + public void setRocketmqMQClientNum(int rocketmqMQClientNum) { + this.rocketmqMQClientNum = rocketmqMQClientNum; + } + + public long getGrpcProxyRelayRequestTimeoutInSeconds() { + return grpcProxyRelayRequestTimeoutInSeconds; + } + + public void setGrpcProxyRelayRequestTimeoutInSeconds(long grpcProxyRelayRequestTimeoutInSeconds) { + this.grpcProxyRelayRequestTimeoutInSeconds = grpcProxyRelayRequestTimeoutInSeconds; + } + + public int getGrpcProducerThreadPoolNums() { + return grpcProducerThreadPoolNums; + } + + public void setGrpcProducerThreadPoolNums(int grpcProducerThreadPoolNums) { + this.grpcProducerThreadPoolNums = grpcProducerThreadPoolNums; + } + + public int getGrpcProducerThreadQueueCapacity() { + return grpcProducerThreadQueueCapacity; + } + + public void setGrpcProducerThreadQueueCapacity(int grpcProducerThreadQueueCapacity) { + this.grpcProducerThreadQueueCapacity = grpcProducerThreadQueueCapacity; + } + + public int getGrpcConsumerThreadPoolNums() { + return grpcConsumerThreadPoolNums; + } + + public void setGrpcConsumerThreadPoolNums(int grpcConsumerThreadPoolNums) { + this.grpcConsumerThreadPoolNums = grpcConsumerThreadPoolNums; + } + + public int getGrpcConsumerThreadQueueCapacity() { + return grpcConsumerThreadQueueCapacity; + } + + public void setGrpcConsumerThreadQueueCapacity(int grpcConsumerThreadQueueCapacity) { + this.grpcConsumerThreadQueueCapacity = grpcConsumerThreadQueueCapacity; + } + + public int getGrpcRouteThreadPoolNums() { + return grpcRouteThreadPoolNums; + } + + public void setGrpcRouteThreadPoolNums(int grpcRouteThreadPoolNums) { + this.grpcRouteThreadPoolNums = grpcRouteThreadPoolNums; + } + + public int getGrpcRouteThreadQueueCapacity() { + return grpcRouteThreadQueueCapacity; + } + + public void setGrpcRouteThreadQueueCapacity(int grpcRouteThreadQueueCapacity) { + this.grpcRouteThreadQueueCapacity = grpcRouteThreadQueueCapacity; + } + + public int getGrpcClientManagerThreadPoolNums() { + return grpcClientManagerThreadPoolNums; + } + + public void setGrpcClientManagerThreadPoolNums(int grpcClientManagerThreadPoolNums) { + this.grpcClientManagerThreadPoolNums = grpcClientManagerThreadPoolNums; + } + + public int getGrpcClientManagerThreadQueueCapacity() { + return grpcClientManagerThreadQueueCapacity; + } + + public void setGrpcClientManagerThreadQueueCapacity(int grpcClientManagerThreadQueueCapacity) { + this.grpcClientManagerThreadQueueCapacity = grpcClientManagerThreadQueueCapacity; + } + + public int getGrpcTransactionThreadPoolNums() { + return grpcTransactionThreadPoolNums; + } + + public void setGrpcTransactionThreadPoolNums(int grpcTransactionThreadPoolNums) { + this.grpcTransactionThreadPoolNums = grpcTransactionThreadPoolNums; + } + + public int getGrpcTransactionThreadQueueCapacity() { + return grpcTransactionThreadQueueCapacity; + } + + public void setGrpcTransactionThreadQueueCapacity(int grpcTransactionThreadQueueCapacity) { + this.grpcTransactionThreadQueueCapacity = grpcTransactionThreadQueueCapacity; + } + + public int getProducerProcessorThreadPoolNums() { + return producerProcessorThreadPoolNums; + } + + public void setProducerProcessorThreadPoolNums(int producerProcessorThreadPoolNums) { + this.producerProcessorThreadPoolNums = producerProcessorThreadPoolNums; + } + + public int getProducerProcessorThreadPoolQueueCapacity() { + return producerProcessorThreadPoolQueueCapacity; + } + + public void setProducerProcessorThreadPoolQueueCapacity(int producerProcessorThreadPoolQueueCapacity) { + this.producerProcessorThreadPoolQueueCapacity = producerProcessorThreadPoolQueueCapacity; + } + + public int getConsumerProcessorThreadPoolNums() { + return consumerProcessorThreadPoolNums; + } + + public void setConsumerProcessorThreadPoolNums(int consumerProcessorThreadPoolNums) { + this.consumerProcessorThreadPoolNums = consumerProcessorThreadPoolNums; + } + + public int getConsumerProcessorThreadPoolQueueCapacity() { + return consumerProcessorThreadPoolQueueCapacity; + } + + public void setConsumerProcessorThreadPoolQueueCapacity(int consumerProcessorThreadPoolQueueCapacity) { + this.consumerProcessorThreadPoolQueueCapacity = consumerProcessorThreadPoolQueueCapacity; + } + + public int getTopicRouteServiceCacheExpiredSeconds() { + return topicRouteServiceCacheExpiredSeconds; + } + + public void setTopicRouteServiceCacheExpiredSeconds(int topicRouteServiceCacheExpiredSeconds) { + this.topicRouteServiceCacheExpiredSeconds = topicRouteServiceCacheExpiredSeconds; + } + + public int getTopicRouteServiceCacheRefreshSeconds() { + return topicRouteServiceCacheRefreshSeconds; + } + + public void setTopicRouteServiceCacheRefreshSeconds(int topicRouteServiceCacheRefreshSeconds) { + this.topicRouteServiceCacheRefreshSeconds = topicRouteServiceCacheRefreshSeconds; + } + + public int getTopicRouteServiceCacheMaxNum() { + return topicRouteServiceCacheMaxNum; + } + + public void setTopicRouteServiceCacheMaxNum(int topicRouteServiceCacheMaxNum) { + this.topicRouteServiceCacheMaxNum = topicRouteServiceCacheMaxNum; + } + + public int getTopicRouteServiceThreadPoolNums() { + return topicRouteServiceThreadPoolNums; + } + + public void setTopicRouteServiceThreadPoolNums(int topicRouteServiceThreadPoolNums) { + this.topicRouteServiceThreadPoolNums = topicRouteServiceThreadPoolNums; + } + + public int getTopicRouteServiceThreadPoolQueueCapacity() { + return topicRouteServiceThreadPoolQueueCapacity; + } + + public void setTopicRouteServiceThreadPoolQueueCapacity(int topicRouteServiceThreadPoolQueueCapacity) { + this.topicRouteServiceThreadPoolQueueCapacity = topicRouteServiceThreadPoolQueueCapacity; + } + + public int getTopicConfigCacheRefreshSeconds() { + return topicConfigCacheRefreshSeconds; + } + + public void setTopicConfigCacheRefreshSeconds(int topicConfigCacheRefreshSeconds) { + this.topicConfigCacheRefreshSeconds = topicConfigCacheRefreshSeconds; + } + + public int getTopicConfigCacheExpiredSeconds() { + return topicConfigCacheExpiredSeconds; + } + + public void setTopicConfigCacheExpiredSeconds(int topicConfigCacheExpiredSeconds) { + this.topicConfigCacheExpiredSeconds = topicConfigCacheExpiredSeconds; + } + + public int getTopicConfigCacheMaxNum() { + return topicConfigCacheMaxNum; + } + + public void setTopicConfigCacheMaxNum(int topicConfigCacheMaxNum) { + this.topicConfigCacheMaxNum = topicConfigCacheMaxNum; + } + + public int getSubscriptionGroupConfigCacheRefreshSeconds() { + return subscriptionGroupConfigCacheRefreshSeconds; + } + + public void setSubscriptionGroupConfigCacheRefreshSeconds(int subscriptionGroupConfigCacheRefreshSeconds) { + this.subscriptionGroupConfigCacheRefreshSeconds = subscriptionGroupConfigCacheRefreshSeconds; + } + + public int getSubscriptionGroupConfigCacheExpiredSeconds() { + return subscriptionGroupConfigCacheExpiredSeconds; + } + + public void setSubscriptionGroupConfigCacheExpiredSeconds(int subscriptionGroupConfigCacheExpiredSeconds) { + this.subscriptionGroupConfigCacheExpiredSeconds = subscriptionGroupConfigCacheExpiredSeconds; + } + + public int getSubscriptionGroupConfigCacheMaxNum() { + return subscriptionGroupConfigCacheMaxNum; + } + + public void setSubscriptionGroupConfigCacheMaxNum(int subscriptionGroupConfigCacheMaxNum) { + this.subscriptionGroupConfigCacheMaxNum = subscriptionGroupConfigCacheMaxNum; + } + + public int getMetadataThreadPoolNums() { + return metadataThreadPoolNums; + } + + public void setMetadataThreadPoolNums(int metadataThreadPoolNums) { + this.metadataThreadPoolNums = metadataThreadPoolNums; + } + + public int getMetadataThreadPoolQueueCapacity() { + return metadataThreadPoolQueueCapacity; + } + + public void setMetadataThreadPoolQueueCapacity(int metadataThreadPoolQueueCapacity) { + this.metadataThreadPoolQueueCapacity = metadataThreadPoolQueueCapacity; + } + + public int getTransactionHeartbeatThreadPoolNums() { + return transactionHeartbeatThreadPoolNums; + } + + public void setTransactionHeartbeatThreadPoolNums(int transactionHeartbeatThreadPoolNums) { + this.transactionHeartbeatThreadPoolNums = transactionHeartbeatThreadPoolNums; + } + + public int getTransactionHeartbeatThreadPoolQueueCapacity() { + return transactionHeartbeatThreadPoolQueueCapacity; + } + + public void setTransactionHeartbeatThreadPoolQueueCapacity(int transactionHeartbeatThreadPoolQueueCapacity) { + this.transactionHeartbeatThreadPoolQueueCapacity = transactionHeartbeatThreadPoolQueueCapacity; + } + + public int getTransactionHeartbeatPeriodSecond() { + return transactionHeartbeatPeriodSecond; + } + + public void setTransactionHeartbeatPeriodSecond(int transactionHeartbeatPeriodSecond) { + this.transactionHeartbeatPeriodSecond = transactionHeartbeatPeriodSecond; + } + + public int getTransactionHeartbeatBatchNum() { + return transactionHeartbeatBatchNum; + } + + public void setTransactionHeartbeatBatchNum(int transactionHeartbeatBatchNum) { + this.transactionHeartbeatBatchNum = transactionHeartbeatBatchNum; + } + + public long getTransactionDataExpireScanPeriodMillis() { + return transactionDataExpireScanPeriodMillis; + } + + public void setTransactionDataExpireScanPeriodMillis(long transactionDataExpireScanPeriodMillis) { + this.transactionDataExpireScanPeriodMillis = transactionDataExpireScanPeriodMillis; + } + + public long getTransactionDataMaxWaitClearMillis() { + return transactionDataMaxWaitClearMillis; + } + + public void setTransactionDataMaxWaitClearMillis(long transactionDataMaxWaitClearMillis) { + this.transactionDataMaxWaitClearMillis = transactionDataMaxWaitClearMillis; + } + + public long getTransactionDataExpireMillis() { + return transactionDataExpireMillis; + } + + public void setTransactionDataExpireMillis(long transactionDataExpireMillis) { + this.transactionDataExpireMillis = transactionDataExpireMillis; + } + + public int getTransactionDataMaxNum() { + return transactionDataMaxNum; + } + + public void setTransactionDataMaxNum(int transactionDataMaxNum) { + this.transactionDataMaxNum = transactionDataMaxNum; + } + + public long getLongPollingReserveTimeInMillis() { + return longPollingReserveTimeInMillis; + } + + public void setLongPollingReserveTimeInMillis(long longPollingReserveTimeInMillis) { + this.longPollingReserveTimeInMillis = longPollingReserveTimeInMillis; + } + + public boolean isEnableACL() { + return enableACL; + } + + public void setEnableACL(boolean enableACL) { + this.enableACL = enableACL; + } + + public boolean isEnableAclRpcHookForClusterMode() { + return enableAclRpcHookForClusterMode; + } + + public void setEnableAclRpcHookForClusterMode(boolean enableAclRpcHookForClusterMode) { + this.enableAclRpcHookForClusterMode = enableAclRpcHookForClusterMode; + } + + public boolean isEnableTopicMessageTypeCheck() { + return enableTopicMessageTypeCheck; + } + + public void setEnableTopicMessageTypeCheck(boolean enableTopicMessageTypeCheck) { + this.enableTopicMessageTypeCheck = enableTopicMessageTypeCheck; + } + + public long getInvisibleTimeMillisWhenClear() { + return invisibleTimeMillisWhenClear; + } + + public void setInvisibleTimeMillisWhenClear(long invisibleTimeMillisWhenClear) { + this.invisibleTimeMillisWhenClear = invisibleTimeMillisWhenClear; + } + + public boolean isEnableProxyAutoRenew() { + return enableProxyAutoRenew; + } + + public void setEnableProxyAutoRenew(boolean enableProxyAutoRenew) { + this.enableProxyAutoRenew = enableProxyAutoRenew; + } + + public int getMaxRenewRetryTimes() { + return maxRenewRetryTimes; + } + + public void setMaxRenewRetryTimes(int maxRenewRetryTimes) { + this.maxRenewRetryTimes = maxRenewRetryTimes; + } + + public int getRenewThreadPoolNums() { + return renewThreadPoolNums; + } + + public void setRenewThreadPoolNums(int renewThreadPoolNums) { + this.renewThreadPoolNums = renewThreadPoolNums; + } + + public int getRenewMaxThreadPoolNums() { + return renewMaxThreadPoolNums; + } + + public void setRenewMaxThreadPoolNums(int renewMaxThreadPoolNums) { + this.renewMaxThreadPoolNums = renewMaxThreadPoolNums; + } + + public int getRenewThreadPoolQueueCapacity() { + return renewThreadPoolQueueCapacity; + } + + public void setRenewThreadPoolQueueCapacity(int renewThreadPoolQueueCapacity) { + this.renewThreadPoolQueueCapacity = renewThreadPoolQueueCapacity; + } + + public long getLockTimeoutMsInHandleGroup() { + return lockTimeoutMsInHandleGroup; + } + + public void setLockTimeoutMsInHandleGroup(long lockTimeoutMsInHandleGroup) { + this.lockTimeoutMsInHandleGroup = lockTimeoutMsInHandleGroup; + } + + public long getRenewAheadTimeMillis() { + return renewAheadTimeMillis; + } + + public void setRenewAheadTimeMillis(long renewAheadTimeMillis) { + this.renewAheadTimeMillis = renewAheadTimeMillis; + } + + public long getRenewMaxTimeMillis() { + return renewMaxTimeMillis; + } + + public void setRenewMaxTimeMillis(long renewMaxTimeMillis) { + this.renewMaxTimeMillis = renewMaxTimeMillis; + } + + public long getRenewSchedulePeriodMillis() { + return renewSchedulePeriodMillis; + } + + public void setRenewSchedulePeriodMillis(long renewSchedulePeriodMillis) { + this.renewSchedulePeriodMillis = renewSchedulePeriodMillis; + } + + public String getMetricCollectorMode() { + return metricCollectorMode; + } + + public void setMetricCollectorMode(String metricCollectorMode) { + this.metricCollectorMode = metricCollectorMode; + } + + public String getMetricCollectorAddress() { + return metricCollectorAddress; + } + + public void setMetricCollectorAddress(String metricCollectorAddress) { + this.metricCollectorAddress = metricCollectorAddress; + } + + public boolean isUseDelayLevel() { + return useDelayLevel; + } + + public void setUseDelayLevel(boolean useDelayLevel) { + this.useDelayLevel = useDelayLevel; + } + + public String getMessageDelayLevel() { + return messageDelayLevel; + } + + public void setMessageDelayLevel(String messageDelayLevel) { + this.messageDelayLevel = messageDelayLevel; + } + + public ConcurrentSkipListMap getDelayLevelTable() { + return delayLevelTable; + } + + public long getGrpcClientIdleTimeMills() { + return grpcClientIdleTimeMills; + } + + public void setGrpcClientIdleTimeMills(final long grpcClientIdleTimeMills) { + this.grpcClientIdleTimeMills = grpcClientIdleTimeMills; + } + + public String getRegionId() { + return regionId; + } + + public void setRegionId(String regionId) { + this.regionId = regionId; + } + + public boolean isTraceOn() { + return traceOn; + } + + public void setTraceOn(boolean traceOn) { + this.traceOn = traceOn; + } + + public String getRemotingAccessAddr() { + return remotingAccessAddr; + } + + public void setRemotingAccessAddr(String remotingAccessAddr) { + this.remotingAccessAddr = remotingAccessAddr; + } + + public MetricsExporterType getMetricsExporterType() { + return metricsExporterType; + } + + public void setMetricsExporterType(MetricsExporterType metricsExporterType) { + this.metricsExporterType = metricsExporterType; + } + + public void setMetricsExporterType(int metricsExporterType) { + this.metricsExporterType = MetricsExporterType.valueOf(metricsExporterType); + } + + public void setMetricsExporterType(String metricsExporterType) { + this.metricsExporterType = MetricsExporterType.valueOf(metricsExporterType); + } + + public String getMetricsGrpcExporterTarget() { + return metricsGrpcExporterTarget; + } + + public void setMetricsGrpcExporterTarget(String metricsGrpcExporterTarget) { + this.metricsGrpcExporterTarget = metricsGrpcExporterTarget; + } + + public String getMetricsGrpcExporterHeader() { + return metricsGrpcExporterHeader; + } + + public void setMetricsGrpcExporterHeader(String metricsGrpcExporterHeader) { + this.metricsGrpcExporterHeader = metricsGrpcExporterHeader; + } + + public long getMetricGrpcExporterTimeOutInMills() { + return metricGrpcExporterTimeOutInMills; + } + + public void setMetricGrpcExporterTimeOutInMills(long metricGrpcExporterTimeOutInMills) { + this.metricGrpcExporterTimeOutInMills = metricGrpcExporterTimeOutInMills; + } + + public long getMetricGrpcExporterIntervalInMills() { + return metricGrpcExporterIntervalInMills; + } + + public void setMetricGrpcExporterIntervalInMills(long metricGrpcExporterIntervalInMills) { + this.metricGrpcExporterIntervalInMills = metricGrpcExporterIntervalInMills; + } + + public long getMetricLoggingExporterIntervalInMills() { + return metricLoggingExporterIntervalInMills; + } + + public void setMetricLoggingExporterIntervalInMills(long metricLoggingExporterIntervalInMills) { + this.metricLoggingExporterIntervalInMills = metricLoggingExporterIntervalInMills; + } + + public int getMetricsPromExporterPort() { + return metricsPromExporterPort; + } + + public void setMetricsPromExporterPort(int metricsPromExporterPort) { + this.metricsPromExporterPort = metricsPromExporterPort; + } + + public String getMetricsPromExporterHost() { + return metricsPromExporterHost; + } + + public void setMetricsPromExporterHost(String metricsPromExporterHost) { + this.metricsPromExporterHost = metricsPromExporterHost; + } + + public String getMetricsLabel() { + return metricsLabel; + } + + public void setMetricsLabel(String metricsLabel) { + this.metricsLabel = metricsLabel; + } + + public boolean isMetricsInDelta() { + return metricsInDelta; + } + + public void setMetricsInDelta(boolean metricsInDelta) { + this.metricsInDelta = metricsInDelta; + } + + public long getChannelExpiredTimeout() { + return channelExpiredTimeout; + } + + public boolean isEnableRemotingLocalProxyGrpc() { + return enableRemotingLocalProxyGrpc; + } + + public void setChannelExpiredTimeout(long channelExpiredTimeout) { + this.channelExpiredTimeout = channelExpiredTimeout; + } + + public void setEnableRemotingLocalProxyGrpc(boolean enableRemotingLocalProxyGrpc) { + this.enableRemotingLocalProxyGrpc = enableRemotingLocalProxyGrpc; + } + + public int getLocalProxyConnectTimeoutMs() { + return localProxyConnectTimeoutMs; + } + + public void setLocalProxyConnectTimeoutMs(int localProxyConnectTimeoutMs) { + this.localProxyConnectTimeoutMs = localProxyConnectTimeoutMs; + } + + public int getRemotingListenPort() { + return remotingListenPort; + } + + public void setRemotingListenPort(int remotingListenPort) { + this.remotingListenPort = remotingListenPort; + } + + public int getRemotingHeartbeatThreadPoolNums() { + return remotingHeartbeatThreadPoolNums; + } + + public void setRemotingHeartbeatThreadPoolNums(int remotingHeartbeatThreadPoolNums) { + this.remotingHeartbeatThreadPoolNums = remotingHeartbeatThreadPoolNums; + } + + public int getRemotingTopicRouteThreadPoolNums() { + return remotingTopicRouteThreadPoolNums; + } + + public void setRemotingTopicRouteThreadPoolNums(int remotingTopicRouteThreadPoolNums) { + this.remotingTopicRouteThreadPoolNums = remotingTopicRouteThreadPoolNums; + } + + public int getRemotingSendMessageThreadPoolNums() { + return remotingSendMessageThreadPoolNums; + } + + public void setRemotingSendMessageThreadPoolNums(int remotingSendMessageThreadPoolNums) { + this.remotingSendMessageThreadPoolNums = remotingSendMessageThreadPoolNums; + } + + public int getRemotingPullMessageThreadPoolNums() { + return remotingPullMessageThreadPoolNums; + } + + public void setRemotingPullMessageThreadPoolNums(int remotingPullMessageThreadPoolNums) { + this.remotingPullMessageThreadPoolNums = remotingPullMessageThreadPoolNums; + } + + public int getRemotingUpdateOffsetThreadPoolNums() { + return remotingUpdateOffsetThreadPoolNums; + } + + public void setRemotingUpdateOffsetThreadPoolNums(int remotingUpdateOffsetThreadPoolNums) { + this.remotingUpdateOffsetThreadPoolNums = remotingUpdateOffsetThreadPoolNums; + } + + public int getRemotingDefaultThreadPoolNums() { + return remotingDefaultThreadPoolNums; + } + + public void setRemotingDefaultThreadPoolNums(int remotingDefaultThreadPoolNums) { + this.remotingDefaultThreadPoolNums = remotingDefaultThreadPoolNums; + } + + public int getRemotingHeartbeatThreadPoolQueueCapacity() { + return remotingHeartbeatThreadPoolQueueCapacity; + } + + public void setRemotingHeartbeatThreadPoolQueueCapacity(int remotingHeartbeatThreadPoolQueueCapacity) { + this.remotingHeartbeatThreadPoolQueueCapacity = remotingHeartbeatThreadPoolQueueCapacity; + } + + public int getRemotingTopicRouteThreadPoolQueueCapacity() { + return remotingTopicRouteThreadPoolQueueCapacity; + } + + public void setRemotingTopicRouteThreadPoolQueueCapacity(int remotingTopicRouteThreadPoolQueueCapacity) { + this.remotingTopicRouteThreadPoolQueueCapacity = remotingTopicRouteThreadPoolQueueCapacity; + } + + public int getRemotingSendThreadPoolQueueCapacity() { + return remotingSendThreadPoolQueueCapacity; + } + + public void setRemotingSendThreadPoolQueueCapacity(int remotingSendThreadPoolQueueCapacity) { + this.remotingSendThreadPoolQueueCapacity = remotingSendThreadPoolQueueCapacity; + } + + public int getRemotingPullThreadPoolQueueCapacity() { + return remotingPullThreadPoolQueueCapacity; + } + + public void setRemotingPullThreadPoolQueueCapacity(int remotingPullThreadPoolQueueCapacity) { + this.remotingPullThreadPoolQueueCapacity = remotingPullThreadPoolQueueCapacity; + } + + public int getRemotingUpdateOffsetThreadPoolQueueCapacity() { + return remotingUpdateOffsetThreadPoolQueueCapacity; + } + + public void setRemotingUpdateOffsetThreadPoolQueueCapacity(int remotingUpdateOffsetThreadPoolQueueCapacity) { + this.remotingUpdateOffsetThreadPoolQueueCapacity = remotingUpdateOffsetThreadPoolQueueCapacity; + } + + public int getRemotingDefaultThreadPoolQueueCapacity() { + return remotingDefaultThreadPoolQueueCapacity; + } + + public void setRemotingDefaultThreadPoolQueueCapacity(int remotingDefaultThreadPoolQueueCapacity) { + this.remotingDefaultThreadPoolQueueCapacity = remotingDefaultThreadPoolQueueCapacity; + } + + public long getRemotingWaitTimeMillsInSendQueue() { + return remotingWaitTimeMillsInSendQueue; + } + + public void setRemotingWaitTimeMillsInSendQueue(long remotingWaitTimeMillsInSendQueue) { + this.remotingWaitTimeMillsInSendQueue = remotingWaitTimeMillsInSendQueue; + } + + public long getRemotingWaitTimeMillsInPullQueue() { + return remotingWaitTimeMillsInPullQueue; + } + + public void setRemotingWaitTimeMillsInPullQueue(long remotingWaitTimeMillsInPullQueue) { + this.remotingWaitTimeMillsInPullQueue = remotingWaitTimeMillsInPullQueue; + } + + public long getRemotingWaitTimeMillsInHeartbeatQueue() { + return remotingWaitTimeMillsInHeartbeatQueue; + } + + public void setRemotingWaitTimeMillsInHeartbeatQueue(long remotingWaitTimeMillsInHeartbeatQueue) { + this.remotingWaitTimeMillsInHeartbeatQueue = remotingWaitTimeMillsInHeartbeatQueue; + } + + public long getRemotingWaitTimeMillsInUpdateOffsetQueue() { + return remotingWaitTimeMillsInUpdateOffsetQueue; + } + + public void setRemotingWaitTimeMillsInUpdateOffsetQueue(long remotingWaitTimeMillsInUpdateOffsetQueue) { + this.remotingWaitTimeMillsInUpdateOffsetQueue = remotingWaitTimeMillsInUpdateOffsetQueue; + } + + public long getRemotingWaitTimeMillsInTopicRouteQueue() { + return remotingWaitTimeMillsInTopicRouteQueue; + } + + public void setRemotingWaitTimeMillsInTopicRouteQueue(long remotingWaitTimeMillsInTopicRouteQueue) { + this.remotingWaitTimeMillsInTopicRouteQueue = remotingWaitTimeMillsInTopicRouteQueue; + } + + public long getRemotingWaitTimeMillsInDefaultQueue() { + return remotingWaitTimeMillsInDefaultQueue; + } + + public void setRemotingWaitTimeMillsInDefaultQueue(long remotingWaitTimeMillsInDefaultQueue) { + this.remotingWaitTimeMillsInDefaultQueue = remotingWaitTimeMillsInDefaultQueue; + } + + public boolean isSendLatencyEnable() { + return sendLatencyEnable; + } + + public boolean isStartDetectorEnable() { + return startDetectorEnable; + } + + public void setStartDetectorEnable(boolean startDetectorEnable) { + this.startDetectorEnable = startDetectorEnable; + } + + public void setSendLatencyEnable(boolean sendLatencyEnable) { + this.sendLatencyEnable = sendLatencyEnable; + } + + public boolean getStartDetectorEnable() { + return this.startDetectorEnable; + } + + public boolean getSendLatencyEnable() { + return this.sendLatencyEnable; + } + + public int getDetectTimeout() { + return detectTimeout; + } + + public void setDetectTimeout(int detectTimeout) { + this.detectTimeout = detectTimeout; + } + + public int getDetectInterval() { + return detectInterval; + } + + public void setDetectInterval(int detectInterval) { + this.detectInterval = detectInterval; + } + + public boolean isEnableBatchAck() { + return enableBatchAck; + } + + public void setEnableBatchAck(boolean enableBatchAck) { + this.enableBatchAck = enableBatchAck; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/GrpcServer.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/GrpcServer.java new file mode 100644 index 00000000000..d5b896fe144 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/GrpcServer.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc; + +import java.util.concurrent.TimeUnit; +import io.grpc.Server; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.common.utils.StartAndShutdown; + +public class GrpcServer implements StartAndShutdown { + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + + private final Server server; + + private final long timeout; + + private final TimeUnit unit; + + protected GrpcServer(Server server, long timeout, TimeUnit unit) { + this.server = server; + this.timeout = timeout; + this.unit = unit; + } + + public void start() throws Exception { + this.server.start(); + log.info("grpc server start successfully."); + } + + public void shutdown() { + try { + this.server.shutdown().awaitTermination(timeout, unit); + log.info("grpc server shutdown successfully."); + } catch (Exception e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/GrpcServerBuilder.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/GrpcServerBuilder.java new file mode 100644 index 00000000000..0e79006f6b3 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/GrpcServerBuilder.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.grpc; + +import io.grpc.BindableService; +import io.grpc.ServerInterceptor; +import io.grpc.ServerServiceDefinition; +import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; +import io.grpc.netty.shaded.io.netty.channel.epoll.EpollEventLoopGroup; +import io.grpc.netty.shaded.io.netty.channel.epoll.EpollServerSocketChannel; +import io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoopGroup; +import io.grpc.netty.shaded.io.netty.channel.socket.nio.NioServerSocketChannel; +import java.util.List; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.acl.AccessValidator; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.grpc.interceptor.AuthenticationInterceptor; +import org.apache.rocketmq.proxy.grpc.interceptor.ContextInterceptor; +import org.apache.rocketmq.proxy.grpc.interceptor.GlobalExceptionInterceptor; +import org.apache.rocketmq.proxy.grpc.interceptor.HeaderInterceptor; + +public class GrpcServerBuilder { + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + protected NettyServerBuilder serverBuilder; + + protected long time = 30; + + protected TimeUnit unit = TimeUnit.SECONDS; + + public static GrpcServerBuilder newBuilder(ThreadPoolExecutor executor, int port) { + return new GrpcServerBuilder(executor, port); + } + + protected GrpcServerBuilder(ThreadPoolExecutor executor, int port) { + serverBuilder = NettyServerBuilder.forPort(port); + + serverBuilder.protocolNegotiator(new ProxyAndTlsProtocolNegotiator()); + + // build server + int bossLoopNum = ConfigurationManager.getProxyConfig().getGrpcBossLoopNum(); + int workerLoopNum = ConfigurationManager.getProxyConfig().getGrpcWorkerLoopNum(); + int maxInboundMessageSize = ConfigurationManager.getProxyConfig().getGrpcMaxInboundMessageSize(); + long idleTimeMills = ConfigurationManager.getProxyConfig().getGrpcClientIdleTimeMills(); + + if (ConfigurationManager.getProxyConfig().isEnableGrpcEpoll()) { + serverBuilder.bossEventLoopGroup(new EpollEventLoopGroup(bossLoopNum)) + .workerEventLoopGroup(new EpollEventLoopGroup(workerLoopNum)) + .channelType(EpollServerSocketChannel.class) + .executor(executor); + } else { + serverBuilder.bossEventLoopGroup(new NioEventLoopGroup(bossLoopNum)) + .workerEventLoopGroup(new NioEventLoopGroup(workerLoopNum)) + .channelType(NioServerSocketChannel.class) + .executor(executor); + } + + serverBuilder.maxInboundMessageSize(maxInboundMessageSize) + .maxConnectionIdle(idleTimeMills, TimeUnit.MILLISECONDS); + + log.info( + "grpc server has built. port: {}, tlsKeyPath: {}, tlsCertPath: {}, threadPool: {}, queueCapacity: {}, " + + "boosLoop: {}, workerLoop: {}, maxInboundMessageSize: {}", + port, bossLoopNum, workerLoopNum, maxInboundMessageSize); + } + + public GrpcServerBuilder shutdownTime(long time, TimeUnit unit) { + this.time = time; + this.unit = unit; + return this; + } + + public GrpcServerBuilder addService(BindableService service) { + this.serverBuilder.addService(service); + return this; + } + + public GrpcServerBuilder addService(ServerServiceDefinition service) { + this.serverBuilder.addService(service); + return this; + } + + public GrpcServerBuilder appendInterceptor(ServerInterceptor interceptor) { + this.serverBuilder.intercept(interceptor); + return this; + } + + public GrpcServer build() { + return new GrpcServer(this.serverBuilder.build(), time, unit); + } + + public GrpcServerBuilder configInterceptor(List accessValidators) { + // grpc interceptors, including acl, logging etc. + this.serverBuilder.intercept(new AuthenticationInterceptor(accessValidators)); + + this.serverBuilder + .intercept(new GlobalExceptionInterceptor()) + .intercept(new ContextInterceptor()) + .intercept(new HeaderInterceptor()); + + return this; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/ProxyAndTlsProtocolNegotiator.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/ProxyAndTlsProtocolNegotiator.java new file mode 100644 index 00000000000..7c92866803f --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/ProxyAndTlsProtocolNegotiator.java @@ -0,0 +1,265 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.grpc; + +import io.grpc.Attributes; +import io.grpc.netty.shaded.io.grpc.netty.GrpcHttp2ConnectionHandler; +import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.shaded.io.grpc.netty.InternalProtocolNegotiationEvent; +import io.grpc.netty.shaded.io.grpc.netty.InternalProtocolNegotiator; +import io.grpc.netty.shaded.io.grpc.netty.InternalProtocolNegotiators; +import io.grpc.netty.shaded.io.grpc.netty.ProtocolNegotiationEvent; +import io.grpc.netty.shaded.io.netty.buffer.ByteBuf; +import io.grpc.netty.shaded.io.netty.buffer.ByteBufUtil; +import io.grpc.netty.shaded.io.netty.channel.ChannelHandler; +import io.grpc.netty.shaded.io.netty.channel.ChannelHandlerContext; +import io.grpc.netty.shaded.io.netty.channel.ChannelInboundHandlerAdapter; +import io.grpc.netty.shaded.io.netty.handler.codec.ByteToMessageDecoder; +import io.grpc.netty.shaded.io.netty.handler.codec.ProtocolDetectionResult; +import io.grpc.netty.shaded.io.netty.handler.codec.ProtocolDetectionState; +import io.grpc.netty.shaded.io.netty.handler.codec.haproxy.HAProxyMessage; +import io.grpc.netty.shaded.io.netty.handler.codec.haproxy.HAProxyMessageDecoder; +import io.grpc.netty.shaded.io.netty.handler.codec.haproxy.HAProxyProtocolVersion; +import io.grpc.netty.shaded.io.netty.handler.codec.haproxy.HAProxyTLV; +import io.grpc.netty.shaded.io.netty.handler.ssl.ClientAuth; +import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext; +import io.grpc.netty.shaded.io.netty.handler.ssl.SslHandler; +import io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.grpc.netty.shaded.io.netty.handler.ssl.util.SelfSignedCertificate; +import io.grpc.netty.shaded.io.netty.util.AsciiString; +import io.grpc.netty.shaded.io.netty.util.CharsetUtil; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.constant.HAProxyConstants; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.BinaryUtil; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.ProxyConfig; +import org.apache.rocketmq.proxy.grpc.constant.AttributeKeys; +import org.apache.rocketmq.remoting.common.TlsMode; +import org.apache.rocketmq.remoting.netty.TlsSystemConfig; + +public class ProxyAndTlsProtocolNegotiator implements InternalProtocolNegotiator.ProtocolNegotiator { + protected static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + + private static final String HA_PROXY_DECODER = "HAProxyDecoder"; + private static final String HA_PROXY_HANDLER = "HAProxyHandler"; + private static final String TLS_MODE_HANDLER = "TlsModeHandler"; + /** + * the length of the ssl record header (in bytes) + */ + private static final int SSL_RECORD_HEADER_LENGTH = 5; + + private static SslContext sslContext; + + public ProxyAndTlsProtocolNegotiator() { + sslContext = loadSslContext(); + } + + @Override + public AsciiString scheme() { + return AsciiString.of("https"); + } + + @Override + public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) { + return new ProxyAndTlsProtocolHandler(grpcHandler); + } + + @Override + public void close() { + } + + private static SslContext loadSslContext() { + try { + ProxyConfig proxyConfig = ConfigurationManager.getProxyConfig(); + if (proxyConfig.isTlsTestModeEnable()) { + SelfSignedCertificate selfSignedCertificate = new SelfSignedCertificate(); + return GrpcSslContexts.forServer(selfSignedCertificate.certificate(), + selfSignedCertificate.privateKey()) + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .clientAuth(ClientAuth.NONE) + .build(); + } else { + String tlsKeyPath = ConfigurationManager.getProxyConfig().getTlsKeyPath(); + String tlsCertPath = ConfigurationManager.getProxyConfig().getTlsCertPath(); + try (InputStream serverKeyInputStream = Files.newInputStream( + Paths.get(tlsKeyPath)); + InputStream serverCertificateStream = Files.newInputStream( + Paths.get(tlsCertPath))) { + SslContext res = GrpcSslContexts.forServer(serverCertificateStream, + serverKeyInputStream) + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .clientAuth(ClientAuth.NONE) + .build(); + log.info("grpc load TLS configured OK"); + return res; + } + } + } catch (Exception e) { + log.error("grpc tls set failed. msg: {}, e:", e.getMessage(), e); + throw new RuntimeException("grpc tls set failed: " + e.getMessage()); + } + } + + private class ProxyAndTlsProtocolHandler extends ByteToMessageDecoder { + + private final GrpcHttp2ConnectionHandler grpcHandler; + + public ProxyAndTlsProtocolHandler(GrpcHttp2ConnectionHandler grpcHandler) { + this.grpcHandler = grpcHandler; + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) { + try { + ProtocolDetectionResult ha = HAProxyMessageDecoder.detectProtocol( + in); + if (ha.state() == ProtocolDetectionState.NEEDS_MORE_DATA) { + return; + } + if (ha.state() == ProtocolDetectionState.DETECTED) { + ctx.pipeline().addAfter(ctx.name(), HA_PROXY_DECODER, new HAProxyMessageDecoder()) + .addAfter(HA_PROXY_DECODER, HA_PROXY_HANDLER, new HAProxyMessageHandler()) + .addAfter(HA_PROXY_HANDLER, TLS_MODE_HANDLER, new TlsModeHandler(grpcHandler)); + } else { + ctx.pipeline().addAfter(ctx.name(), TLS_MODE_HANDLER, new TlsModeHandler(grpcHandler)); + } + + ctx.fireUserEventTriggered(InternalProtocolNegotiationEvent.getDefault()); + ctx.pipeline().remove(this); + } catch (Exception e) { + log.error("process proxy protocol negotiator failed.", e); + throw e; + } + } + } + + private class HAProxyMessageHandler extends ChannelInboundHandlerAdapter { + + private ProtocolNegotiationEvent pne = InternalProtocolNegotiationEvent.getDefault(); + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof HAProxyMessage) { + handleWithMessage((HAProxyMessage) msg); + ctx.fireUserEventTriggered(pne); + } else { + super.channelRead(ctx, msg); + } + ctx.pipeline().remove(this); + } + + /** + * The definition of key refers to the implementation of nginx + * ngx_http_core_module + * + * @param msg + */ + private void handleWithMessage(HAProxyMessage msg) { + try { + Attributes.Builder builder = InternalProtocolNegotiationEvent.getAttributes(pne).toBuilder(); + if (StringUtils.isNotBlank(msg.sourceAddress())) { + builder.set(AttributeKeys.PROXY_PROTOCOL_ADDR, msg.sourceAddress()); + } + if (msg.sourcePort() > 0) { + builder.set(AttributeKeys.PROXY_PROTOCOL_PORT, String.valueOf(msg.sourcePort())); + } + if (StringUtils.isNotBlank(msg.destinationAddress())) { + builder.set(AttributeKeys.PROXY_PROTOCOL_SERVER_ADDR, msg.destinationAddress()); + } + if (msg.destinationPort() > 0) { + builder.set(AttributeKeys.PROXY_PROTOCOL_SERVER_PORT, String.valueOf(msg.destinationPort())); + } + if (CollectionUtils.isNotEmpty(msg.tlvs())) { + msg.tlvs().forEach(tlv -> handleHAProxyTLV(tlv, builder)); + } + pne = InternalProtocolNegotiationEvent + .withAttributes(InternalProtocolNegotiationEvent.getDefault(), builder.build()); + } finally { + msg.release(); + } + } + } + + protected void handleHAProxyTLV(HAProxyTLV tlv, Attributes.Builder builder) { + byte[] valueBytes = ByteBufUtil.getBytes(tlv.content()); + if (!BinaryUtil.isAscii(valueBytes)) { + return; + } + Attributes.Key key = AttributeKeys.valueOf( + HAProxyConstants.PROXY_PROTOCOL_TLV_PREFIX + String.format("%02x", tlv.typeByteValue())); + builder.set(key, new String(valueBytes, CharsetUtil.UTF_8)); + } + + private class TlsModeHandler extends ByteToMessageDecoder { + + private ProtocolNegotiationEvent pne = InternalProtocolNegotiationEvent.getDefault(); + + private final ChannelHandler ssl; + private final ChannelHandler plaintext; + + public TlsModeHandler(GrpcHttp2ConnectionHandler grpcHandler) { + this.ssl = InternalProtocolNegotiators.serverTls(sslContext) + .newHandler(grpcHandler); + this.plaintext = InternalProtocolNegotiators.serverPlaintext() + .newHandler(grpcHandler); + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) { + try { + TlsMode tlsMode = TlsSystemConfig.tlsMode; + if (TlsMode.ENFORCING.equals(tlsMode)) { + ctx.pipeline().addAfter(ctx.name(), null, this.ssl); + } else if (TlsMode.DISABLED.equals(tlsMode)) { + ctx.pipeline().addAfter(ctx.name(), null, this.plaintext); + } else { + // in SslHandler.isEncrypted, it need at least 5 bytes to judge is encrypted or not + if (in.readableBytes() < SSL_RECORD_HEADER_LENGTH) { + return; + } + if (SslHandler.isEncrypted(in)) { + ctx.pipeline().addAfter(ctx.name(), null, this.ssl); + } else { + ctx.pipeline().addAfter(ctx.name(), null, this.plaintext); + } + } + ctx.fireUserEventTriggered(pne); + ctx.pipeline().remove(this); + } catch (Exception e) { + log.error("process ssl protocol negotiator failed.", e); + throw e; + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof ProtocolNegotiationEvent) { + pne = (ProtocolNegotiationEvent) evt; + } else { + super.userEventTriggered(ctx, evt); + } + } + } +} \ No newline at end of file diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/constant/AttributeKeys.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/constant/AttributeKeys.java new file mode 100644 index 00000000000..096a5ba3d3d --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/constant/AttributeKeys.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.grpc.constant; + +import io.grpc.Attributes; +import org.apache.rocketmq.common.constant.HAProxyConstants; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class AttributeKeys { + + public static final Attributes.Key PROXY_PROTOCOL_ADDR = + Attributes.Key.create(HAProxyConstants.PROXY_PROTOCOL_ADDR); + + public static final Attributes.Key PROXY_PROTOCOL_PORT = + Attributes.Key.create(HAProxyConstants.PROXY_PROTOCOL_PORT); + + public static final Attributes.Key PROXY_PROTOCOL_SERVER_ADDR = + Attributes.Key.create(HAProxyConstants.PROXY_PROTOCOL_SERVER_ADDR); + + public static final Attributes.Key PROXY_PROTOCOL_SERVER_PORT = + Attributes.Key.create(HAProxyConstants.PROXY_PROTOCOL_SERVER_PORT); + + private static final Map> ATTRIBUTES_KEY_MAP = new ConcurrentHashMap<>(); + + public static Attributes.Key valueOf(String name) { + return ATTRIBUTES_KEY_MAP.computeIfAbsent(name, key -> Attributes.Key.create(name)); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/interceptor/AuthenticationInterceptor.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/interceptor/AuthenticationInterceptor.java new file mode 100644 index 00000000000..951ebf006b5 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/interceptor/AuthenticationInterceptor.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.interceptor; + +import com.google.protobuf.GeneratedMessageV3; +import io.grpc.Context; +import io.grpc.ForwardingServerCallListener; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import java.util.List; +import org.apache.rocketmq.acl.AccessResource; +import org.apache.rocketmq.acl.AccessValidator; +import org.apache.rocketmq.acl.common.AclException; +import org.apache.rocketmq.acl.common.AuthenticationHeader; +import org.apache.rocketmq.acl.plain.PlainAccessResource; +import org.apache.rocketmq.proxy.config.ConfigurationManager; + +public class AuthenticationInterceptor implements ServerInterceptor { + protected final List accessValidatorList; + + public AuthenticationInterceptor(List accessValidatorList) { + this.accessValidatorList = accessValidatorList; + } + + @Override + public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, + ServerCallHandler next) { + return new ForwardingServerCallListener.SimpleForwardingServerCallListener(next.startCall(call, headers)) { + @Override + public void onMessage(R message) { + GeneratedMessageV3 messageV3 = (GeneratedMessageV3) message; + headers.put(InterceptorConstants.RPC_NAME, messageV3.getDescriptorForType().getFullName()); + headers.put(InterceptorConstants.SIMPLE_RPC_NAME, messageV3.getDescriptorForType().getName()); + if (ConfigurationManager.getProxyConfig().isEnableACL()) { + try { + AuthenticationHeader authenticationHeader = AuthenticationHeader.builder() + .remoteAddress(InterceptorConstants.METADATA.get(Context.current()).get(InterceptorConstants.REMOTE_ADDRESS)) + .namespace(InterceptorConstants.METADATA.get(Context.current()).get(InterceptorConstants.NAMESPACE_ID)) + .authorization(InterceptorConstants.METADATA.get(Context.current()).get(InterceptorConstants.AUTHORIZATION)) + .datetime(InterceptorConstants.METADATA.get(Context.current()).get(InterceptorConstants.DATE_TIME)) + .sessionToken(InterceptorConstants.METADATA.get(Context.current()).get(InterceptorConstants.SESSION_TOKEN)) + .requestId(InterceptorConstants.METADATA.get(Context.current()).get(InterceptorConstants.REQUEST_ID)) + .language(InterceptorConstants.METADATA.get(Context.current()).get(InterceptorConstants.LANGUAGE)) + .clientVersion(InterceptorConstants.METADATA.get(Context.current()).get(InterceptorConstants.CLIENT_VERSION)) + .protocol(InterceptorConstants.METADATA.get(Context.current()).get(InterceptorConstants.PROTOCOL_VERSION)) + .requestCode(RequestMapping.map(messageV3.getDescriptorForType().getFullName())) + .build(); + + validate(authenticationHeader, headers, messageV3); + super.onMessage(message); + } catch (AclException aclException) { + throw new StatusRuntimeException(Status.PERMISSION_DENIED, headers); + } + } else { + super.onMessage(message); + } + } + }; + } + + protected void validate(AuthenticationHeader authenticationHeader, Metadata headers, GeneratedMessageV3 messageV3) { + for (AccessValidator accessValidator : accessValidatorList) { + AccessResource accessResource = accessValidator.parse(messageV3, authenticationHeader); + accessValidator.validate(accessResource); + + if (accessResource instanceof PlainAccessResource) { + PlainAccessResource plainAccessResource = (PlainAccessResource) accessResource; + headers.put(InterceptorConstants.AUTHORIZATION_AK, plainAccessResource.getAccessKey()); + } + } + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/interceptor/ContextInterceptor.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/interceptor/ContextInterceptor.java new file mode 100644 index 00000000000..07d7ab9bf38 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/interceptor/ContextInterceptor.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.interceptor; + +import io.grpc.Context; +import io.grpc.Contexts; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; + +public class ContextInterceptor implements ServerInterceptor { + + @Override + public ServerCall.Listener interceptCall( + ServerCall call, + Metadata headers, + ServerCallHandler next + ) { + Context context = Context.current().withValue(InterceptorConstants.METADATA, headers); + return Contexts.interceptCall(context, call, headers, next); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/interceptor/GlobalExceptionInterceptor.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/interceptor/GlobalExceptionInterceptor.java new file mode 100644 index 00000000000..3ce00b4ce87 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/interceptor/GlobalExceptionInterceptor.java @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.interceptor; + +import io.grpc.ForwardingServerCall; +import io.grpc.ForwardingServerCallListener; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class GlobalExceptionInterceptor implements ServerInterceptor { + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + + @Override + public ServerCall.Listener interceptCall( + ServerCall call, + Metadata headers, + ServerCallHandler next + ) { + final ServerCall serverCall = new ClosableServerCall<>(call); + ServerCall.Listener delegate = next.startCall(serverCall, headers); + return new ForwardingServerCallListener.SimpleForwardingServerCallListener(delegate) { + @Override + public void onMessage(R message) { + try { + super.onMessage(message); + } catch (Throwable e) { + closeWithException(e); + } + } + + @Override + public void onHalfClose() { + try { + super.onHalfClose(); + } catch (Throwable e) { + closeWithException(e); + } + } + + @Override + public void onCancel() { + try { + super.onCancel(); + } catch (Throwable e) { + closeWithException(e); + } + } + + @Override + public void onComplete() { + try { + super.onComplete(); + } catch (Throwable e) { + closeWithException(e); + } + } + + @Override + public void onReady() { + try { + super.onReady(); + } catch (Throwable e) { + closeWithException(e); + } + } + + private void closeWithException(Throwable t) { + Metadata trailers = new Metadata(); + Status status = Status.INTERNAL.withDescription(t.getMessage()); + boolean printLog = true; + + if (t instanceof StatusRuntimeException) { + trailers = ((StatusRuntimeException) t).getTrailers(); + status = ((StatusRuntimeException) t).getStatus(); + // no error stack for permission denied. + if (status.getCode().value() == Status.PERMISSION_DENIED.getCode().value()) { + printLog = false; + } + } + + if (printLog) { + log.error("grpc server has exception. errorMsg:{}, e:", t.getMessage(), t); + } + + serverCall.close(status, trailers); + } + }; + } + + private static class ClosableServerCall extends + ForwardingServerCall.SimpleForwardingServerCall { + private boolean closeCalled = false; + + ClosableServerCall(ServerCall delegate) { + super(delegate); + } + + @Override + public synchronized void close(final Status status, final Metadata trailers) { + if (!closeCalled) { + closeCalled = true; + ClosableServerCall.super.close(status, trailers); + } + } + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/interceptor/HeaderInterceptor.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/interceptor/HeaderInterceptor.java new file mode 100644 index 00000000000..13893e5eda3 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/interceptor/HeaderInterceptor.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.interceptor; + +import com.google.common.net.HostAndPort; +import io.grpc.Attributes; +import io.grpc.Grpc; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.constant.HAProxyConstants; +import org.apache.rocketmq.proxy.grpc.constant.AttributeKeys; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +public class HeaderInterceptor implements ServerInterceptor { + @Override + public ServerCall.Listener interceptCall( + ServerCall call, + Metadata headers, + ServerCallHandler next + ) { + String remoteAddress = getProxyProtocolAddress(call.getAttributes()); + if (StringUtils.isBlank(remoteAddress)) { + SocketAddress remoteSocketAddress = call.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR); + remoteAddress = parseSocketAddress(remoteSocketAddress); + } + headers.put(InterceptorConstants.REMOTE_ADDRESS, remoteAddress); + + SocketAddress localSocketAddress = call.getAttributes().get(Grpc.TRANSPORT_ATTR_LOCAL_ADDR); + String localAddress = parseSocketAddress(localSocketAddress); + headers.put(InterceptorConstants.LOCAL_ADDRESS, localAddress); + + for (Attributes.Key key : call.getAttributes().keys()) { + if (!StringUtils.startsWith(key.toString(), HAProxyConstants.PROXY_PROTOCOL_PREFIX)) { + continue; + } + Metadata.Key headerKey + = Metadata.Key.of(key.toString(), Metadata.ASCII_STRING_MARSHALLER); + String headerValue = String.valueOf(call.getAttributes().get(key)); + headers.put(headerKey, headerValue); + } + + return next.startCall(call, headers); + } + + private String parseSocketAddress(SocketAddress socketAddress) { + if (socketAddress instanceof InetSocketAddress) { + InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress; + return HostAndPort.fromParts( + inetSocketAddress.getAddress() + .getHostAddress(), + inetSocketAddress.getPort() + ).toString(); + } + + return ""; + } + + private String getProxyProtocolAddress(Attributes attributes) { + String proxyProtocolAddr = attributes.get(AttributeKeys.PROXY_PROTOCOL_ADDR); + String proxyProtocolPort = attributes.get(AttributeKeys.PROXY_PROTOCOL_PORT); + if (StringUtils.isBlank(proxyProtocolAddr) || StringUtils.isBlank(proxyProtocolPort)) { + return null; + } + return proxyProtocolAddr + ":" + proxyProtocolPort; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/interceptor/InterceptorConstants.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/interceptor/InterceptorConstants.java new file mode 100644 index 00000000000..768f3d96abc --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/interceptor/InterceptorConstants.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.interceptor; + +import io.grpc.Context; +import io.grpc.Metadata; + +public class InterceptorConstants { + public static final Context.Key METADATA = Context.key("rpc-metadata"); + + /** + * Remote address key in attributes of call + */ + public static final Metadata.Key REMOTE_ADDRESS + = Metadata.Key.of("rpc-remote-address", Metadata.ASCII_STRING_MARSHALLER); + + /** + * Local address key in attributes of call + */ + public static final Metadata.Key LOCAL_ADDRESS + = Metadata.Key.of("rpc-local-address", Metadata.ASCII_STRING_MARSHALLER); + + public static final Metadata.Key AUTHORIZATION + = Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER); + + public static final Metadata.Key NAMESPACE_ID + = Metadata.Key.of("x-mq-namespace", Metadata.ASCII_STRING_MARSHALLER); + + public static final Metadata.Key DATE_TIME + = Metadata.Key.of("x-mq-date-time", Metadata.ASCII_STRING_MARSHALLER); + + public static final Metadata.Key REQUEST_ID + = Metadata.Key.of("x-mq-request-id", Metadata.ASCII_STRING_MARSHALLER); + + public static final Metadata.Key LANGUAGE + = Metadata.Key.of("x-mq-language", Metadata.ASCII_STRING_MARSHALLER); + + public static final Metadata.Key CLIENT_VERSION + = Metadata.Key.of("x-mq-client-version", Metadata.ASCII_STRING_MARSHALLER); + + public static final Metadata.Key PROTOCOL_VERSION + = Metadata.Key.of("x-mq-protocol", Metadata.ASCII_STRING_MARSHALLER); + + public static final Metadata.Key RPC_NAME + = Metadata.Key.of("x-mq-rpc-name", Metadata.ASCII_STRING_MARSHALLER); + + public static final Metadata.Key SIMPLE_RPC_NAME + = Metadata.Key.of("x-mq-simple-rpc-name", Metadata.ASCII_STRING_MARSHALLER); + + public static final Metadata.Key SESSION_TOKEN + = Metadata.Key.of("x-mq-session-token", Metadata.ASCII_STRING_MARSHALLER); + + public static final Metadata.Key CLIENT_ID + = Metadata.Key.of("x-mq-client-id", Metadata.ASCII_STRING_MARSHALLER); + + public static final Metadata.Key AUTHORIZATION_AK + = Metadata.Key.of("x-mq-authorization-ak", Metadata.ASCII_STRING_MARSHALLER); +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/interceptor/RequestMapping.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/interceptor/RequestMapping.java new file mode 100644 index 00000000000..866124d747c --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/interceptor/RequestMapping.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.interceptor; + +import apache.rocketmq.v2.AckMessageRequest; +import apache.rocketmq.v2.ChangeInvisibleDurationRequest; +import apache.rocketmq.v2.EndTransactionRequest; +import apache.rocketmq.v2.ForwardMessageToDeadLetterQueueResponse; +import apache.rocketmq.v2.HeartbeatRequest; +import apache.rocketmq.v2.NotifyClientTerminationRequest; +import apache.rocketmq.v2.QueryAssignmentRequest; +import apache.rocketmq.v2.QueryRouteRequest; +import apache.rocketmq.v2.ReceiveMessageRequest; +import apache.rocketmq.v2.SendMessageRequest; +import java.util.HashMap; +import java.util.Map; +import org.apache.rocketmq.remoting.protocol.RequestCode; + +public class RequestMapping { + private final static Map REQUEST_MAP = new HashMap() { + { + // v2 + put(QueryRouteRequest.getDescriptor().getFullName(), RequestCode.GET_ROUTEINFO_BY_TOPIC); + put(HeartbeatRequest.getDescriptor().getFullName(), RequestCode.HEART_BEAT); + put(SendMessageRequest.getDescriptor().getFullName(), RequestCode.SEND_MESSAGE_V2); + put(QueryAssignmentRequest.getDescriptor().getFullName(), RequestCode.GET_ROUTEINFO_BY_TOPIC); + put(ReceiveMessageRequest.getDescriptor().getFullName(), RequestCode.PULL_MESSAGE); + put(AckMessageRequest.getDescriptor().getFullName(), RequestCode.UPDATE_CONSUMER_OFFSET); + put(ForwardMessageToDeadLetterQueueResponse.getDescriptor().getFullName(), RequestCode.CONSUMER_SEND_MSG_BACK); + put(EndTransactionRequest.getDescriptor().getFullName(), RequestCode.END_TRANSACTION); + put(NotifyClientTerminationRequest.getDescriptor().getFullName(), RequestCode.UNREGISTER_CLIENT); + put(ChangeInvisibleDurationRequest.getDescriptor().getFullName(), RequestCode.CONSUMER_SEND_MSG_BACK); + } + }; + + public static int map(String rpcFullName) { + if (REQUEST_MAP.containsKey(rpcFullName)) { + return REQUEST_MAP.get(rpcFullName); + } + return RequestCode.HEART_BEAT; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/AbstractMessingActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/AbstractMessingActivity.java new file mode 100644 index 00000000000..6598b9e7e65 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/AbstractMessingActivity.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.grpc.v2; + +import apache.rocketmq.v2.Resource; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.grpc.v2.channel.GrpcChannelManager; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcClientSettingsManager; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcValidator; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; + +public abstract class AbstractMessingActivity { + protected static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + protected final MessagingProcessor messagingProcessor; + protected final GrpcClientSettingsManager grpcClientSettingsManager; + protected final GrpcChannelManager grpcChannelManager; + + public AbstractMessingActivity(MessagingProcessor messagingProcessor, + GrpcClientSettingsManager grpcClientSettingsManager, GrpcChannelManager grpcChannelManager) { + this.messagingProcessor = messagingProcessor; + this.grpcClientSettingsManager = grpcClientSettingsManager; + this.grpcChannelManager = grpcChannelManager; + } + + protected void validateTopic(Resource topic) { + GrpcValidator.getInstance().validateTopic(topic); + } + + protected void validateConsumerGroup(Resource consumerGroup) { + GrpcValidator.getInstance().validateConsumerGroup(consumerGroup); + } + + protected void validateTopicAndConsumerGroup(Resource topic, Resource consumerGroup) { + GrpcValidator.getInstance().validateTopicAndConsumerGroup(topic, consumerGroup); + } + + protected void validateInvisibleTime(long invisibleTime) { + GrpcValidator.getInstance().validateInvisibleTime(invisibleTime); + } + + protected void validateInvisibleTime(long invisibleTime, long minInvisibleTime) { + GrpcValidator.getInstance().validateInvisibleTime(invisibleTime, minInvisibleTime); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/ContextStreamObserver.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/ContextStreamObserver.java new file mode 100644 index 00000000000..c186bfb61cd --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/ContextStreamObserver.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.v2; + +import org.apache.rocketmq.proxy.common.ProxyContext; + +public interface ContextStreamObserver { + + void onNext(ProxyContext ctx, V value); + + void onError(Throwable t); + + void onCompleted(); +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/DefaultGrpcMessingActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/DefaultGrpcMessingActivity.java new file mode 100644 index 00000000000..091e9086ecc --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/DefaultGrpcMessingActivity.java @@ -0,0 +1,152 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.grpc.v2; + +import apache.rocketmq.v2.AckMessageRequest; +import apache.rocketmq.v2.AckMessageResponse; +import apache.rocketmq.v2.ChangeInvisibleDurationRequest; +import apache.rocketmq.v2.ChangeInvisibleDurationResponse; +import apache.rocketmq.v2.EndTransactionRequest; +import apache.rocketmq.v2.EndTransactionResponse; +import apache.rocketmq.v2.ForwardMessageToDeadLetterQueueRequest; +import apache.rocketmq.v2.ForwardMessageToDeadLetterQueueResponse; +import apache.rocketmq.v2.HeartbeatRequest; +import apache.rocketmq.v2.HeartbeatResponse; +import apache.rocketmq.v2.NotifyClientTerminationRequest; +import apache.rocketmq.v2.NotifyClientTerminationResponse; +import apache.rocketmq.v2.QueryAssignmentRequest; +import apache.rocketmq.v2.QueryAssignmentResponse; +import apache.rocketmq.v2.QueryRouteRequest; +import apache.rocketmq.v2.QueryRouteResponse; +import apache.rocketmq.v2.ReceiveMessageRequest; +import apache.rocketmq.v2.ReceiveMessageResponse; +import apache.rocketmq.v2.SendMessageRequest; +import apache.rocketmq.v2.SendMessageResponse; +import apache.rocketmq.v2.TelemetryCommand; +import io.grpc.stub.StreamObserver; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.common.utils.AbstractStartAndShutdown; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.grpc.v2.channel.GrpcChannelManager; +import org.apache.rocketmq.proxy.grpc.v2.client.ClientActivity; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcClientSettingsManager; +import org.apache.rocketmq.proxy.grpc.v2.consumer.AckMessageActivity; +import org.apache.rocketmq.proxy.grpc.v2.consumer.ChangeInvisibleDurationActivity; +import org.apache.rocketmq.proxy.grpc.v2.consumer.ReceiveMessageActivity; +import org.apache.rocketmq.proxy.grpc.v2.producer.ForwardMessageToDLQActivity; +import org.apache.rocketmq.proxy.grpc.v2.producer.SendMessageActivity; +import org.apache.rocketmq.proxy.grpc.v2.route.RouteActivity; +import org.apache.rocketmq.proxy.grpc.v2.transaction.EndTransactionActivity; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; + +public class DefaultGrpcMessingActivity extends AbstractStartAndShutdown implements GrpcMessingActivity { + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + + protected GrpcClientSettingsManager grpcClientSettingsManager; + protected GrpcChannelManager grpcChannelManager; + protected ReceiveMessageActivity receiveMessageActivity; + protected AckMessageActivity ackMessageActivity; + protected ChangeInvisibleDurationActivity changeInvisibleDurationActivity; + protected SendMessageActivity sendMessageActivity; + protected ForwardMessageToDLQActivity forwardMessageToDLQActivity; + protected EndTransactionActivity endTransactionActivity; + protected RouteActivity routeActivity; + protected ClientActivity clientActivity; + + protected DefaultGrpcMessingActivity(MessagingProcessor messagingProcessor) { + this.init(messagingProcessor); + } + + protected void init(MessagingProcessor messagingProcessor) { + this.grpcClientSettingsManager = new GrpcClientSettingsManager(messagingProcessor); + this.grpcChannelManager = new GrpcChannelManager(messagingProcessor.getProxyRelayService(), this.grpcClientSettingsManager); + + this.receiveMessageActivity = new ReceiveMessageActivity(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); + this.ackMessageActivity = new AckMessageActivity(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); + this.changeInvisibleDurationActivity = new ChangeInvisibleDurationActivity(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); + this.sendMessageActivity = new SendMessageActivity(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); + this.forwardMessageToDLQActivity = new ForwardMessageToDLQActivity(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); + this.endTransactionActivity = new EndTransactionActivity(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); + this.routeActivity = new RouteActivity(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); + this.clientActivity = new ClientActivity(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); + + this.appendStartAndShutdown(this.grpcClientSettingsManager); + } + + @Override + public CompletableFuture queryRoute(ProxyContext ctx, QueryRouteRequest request) { + return this.routeActivity.queryRoute(ctx, request); + } + + @Override + public CompletableFuture heartbeat(ProxyContext ctx, HeartbeatRequest request) { + return this.clientActivity.heartbeat(ctx, request); + } + + @Override + public CompletableFuture sendMessage(ProxyContext ctx, SendMessageRequest request) { + return this.sendMessageActivity.sendMessage(ctx, request); + } + + @Override + public CompletableFuture queryAssignment(ProxyContext ctx, + QueryAssignmentRequest request) { + return this.routeActivity.queryAssignment(ctx, request); + } + + @Override + public void receiveMessage(ProxyContext ctx, ReceiveMessageRequest request, + StreamObserver responseObserver) { + this.receiveMessageActivity.receiveMessage(ctx, request, responseObserver); + } + + @Override + public CompletableFuture ackMessage(ProxyContext ctx, AckMessageRequest request) { + return this.ackMessageActivity.ackMessage(ctx, request); + } + + @Override + public CompletableFuture forwardMessageToDeadLetterQueue(ProxyContext ctx, + ForwardMessageToDeadLetterQueueRequest request) { + return this.forwardMessageToDLQActivity.forwardMessageToDeadLetterQueue(ctx, request); + } + + @Override + public CompletableFuture endTransaction(ProxyContext ctx, EndTransactionRequest request) { + return this.endTransactionActivity.endTransaction(ctx, request); + } + + @Override + public CompletableFuture notifyClientTermination(ProxyContext ctx, + NotifyClientTerminationRequest request) { + return this.clientActivity.notifyClientTermination(ctx, request); + } + + @Override + public CompletableFuture changeInvisibleDuration(ProxyContext ctx, + ChangeInvisibleDurationRequest request) { + return this.changeInvisibleDurationActivity.changeInvisibleDuration(ctx, request); + } + + @Override + public ContextStreamObserver telemetry(StreamObserver responseObserver) { + return this.clientActivity.telemetry(responseObserver); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/GrpcMessagingApplication.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/GrpcMessagingApplication.java new file mode 100644 index 00000000000..a344b0590c4 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/GrpcMessagingApplication.java @@ -0,0 +1,470 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.v2; + +import apache.rocketmq.v2.AckMessageRequest; +import apache.rocketmq.v2.AckMessageResponse; +import apache.rocketmq.v2.ChangeInvisibleDurationRequest; +import apache.rocketmq.v2.ChangeInvisibleDurationResponse; +import apache.rocketmq.v2.Code; +import apache.rocketmq.v2.EndTransactionRequest; +import apache.rocketmq.v2.EndTransactionResponse; +import apache.rocketmq.v2.ForwardMessageToDeadLetterQueueRequest; +import apache.rocketmq.v2.ForwardMessageToDeadLetterQueueResponse; +import apache.rocketmq.v2.HeartbeatRequest; +import apache.rocketmq.v2.HeartbeatResponse; +import apache.rocketmq.v2.MessagingServiceGrpc; +import apache.rocketmq.v2.NotifyClientTerminationRequest; +import apache.rocketmq.v2.NotifyClientTerminationResponse; +import apache.rocketmq.v2.QueryAssignmentRequest; +import apache.rocketmq.v2.QueryAssignmentResponse; +import apache.rocketmq.v2.QueryRouteRequest; +import apache.rocketmq.v2.QueryRouteResponse; +import apache.rocketmq.v2.ReceiveMessageRequest; +import apache.rocketmq.v2.ReceiveMessageResponse; +import apache.rocketmq.v2.SendMessageRequest; +import apache.rocketmq.v2.SendMessageResponse; +import apache.rocketmq.v2.Status; +import apache.rocketmq.v2.TelemetryCommand; +import io.grpc.Context; +import io.grpc.Metadata; +import io.grpc.stub.StreamObserver; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.thread.ThreadPoolMonitor; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.common.utils.StartAndShutdown; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.ProxyConfig; +import org.apache.rocketmq.proxy.grpc.interceptor.InterceptorConstants; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcProxyException; +import org.apache.rocketmq.proxy.grpc.v2.common.ResponseBuilder; +import org.apache.rocketmq.proxy.grpc.v2.common.ResponseWriter; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.processor.channel.ChannelProtocolType; + +public class GrpcMessagingApplication extends MessagingServiceGrpc.MessagingServiceImplBase implements StartAndShutdown { + private final static Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + + private final GrpcMessingActivity grpcMessingActivity; + + protected ThreadPoolExecutor routeThreadPoolExecutor; + protected ThreadPoolExecutor producerThreadPoolExecutor; + protected ThreadPoolExecutor consumerThreadPoolExecutor; + protected ThreadPoolExecutor clientManagerThreadPoolExecutor; + protected ThreadPoolExecutor transactionThreadPoolExecutor; + + protected GrpcMessagingApplication(GrpcMessingActivity grpcMessingActivity) { + this.grpcMessingActivity = grpcMessingActivity; + + ProxyConfig config = ConfigurationManager.getProxyConfig(); + this.routeThreadPoolExecutor = ThreadPoolMonitor.createAndMonitor( + config.getGrpcRouteThreadPoolNums(), + config.getGrpcRouteThreadPoolNums(), + 1, + TimeUnit.MINUTES, + "GrpcRouteThreadPool", + config.getGrpcRouteThreadQueueCapacity() + ); + this.producerThreadPoolExecutor = ThreadPoolMonitor.createAndMonitor( + config.getGrpcProducerThreadPoolNums(), + config.getGrpcProducerThreadPoolNums(), + 1, + TimeUnit.MINUTES, + "GrpcProducerThreadPool", + config.getGrpcProducerThreadQueueCapacity() + ); + this.consumerThreadPoolExecutor = ThreadPoolMonitor.createAndMonitor( + config.getGrpcConsumerThreadPoolNums(), + config.getGrpcConsumerThreadPoolNums(), + 1, + TimeUnit.MINUTES, + "GrpcConsumerThreadPool", + config.getGrpcConsumerThreadQueueCapacity() + ); + this.clientManagerThreadPoolExecutor = ThreadPoolMonitor.createAndMonitor( + config.getGrpcClientManagerThreadPoolNums(), + config.getGrpcClientManagerThreadPoolNums(), + 1, + TimeUnit.MINUTES, + "GrpcClientManagerThreadPool", + config.getGrpcClientManagerThreadQueueCapacity() + ); + this.transactionThreadPoolExecutor = ThreadPoolMonitor.createAndMonitor( + config.getGrpcTransactionThreadPoolNums(), + config.getGrpcTransactionThreadPoolNums(), + 1, + TimeUnit.MINUTES, + "GrpcTransactionThreadPool", + config.getGrpcTransactionThreadQueueCapacity() + ); + + this.init(); + } + + protected void init() { + GrpcTaskRejectedExecutionHandler rejectedExecutionHandler = new GrpcTaskRejectedExecutionHandler(); + this.routeThreadPoolExecutor.setRejectedExecutionHandler(rejectedExecutionHandler); + this.routeThreadPoolExecutor.setRejectedExecutionHandler(rejectedExecutionHandler); + this.producerThreadPoolExecutor.setRejectedExecutionHandler(rejectedExecutionHandler); + this.consumerThreadPoolExecutor.setRejectedExecutionHandler(rejectedExecutionHandler); + this.clientManagerThreadPoolExecutor.setRejectedExecutionHandler(rejectedExecutionHandler); + this.transactionThreadPoolExecutor.setRejectedExecutionHandler(rejectedExecutionHandler); + } + + public static GrpcMessagingApplication create(MessagingProcessor messagingProcessor) { + return new GrpcMessagingApplication(new DefaultGrpcMessingActivity( + messagingProcessor + )); + } + + protected Status flowLimitStatus() { + return ResponseBuilder.getInstance().buildStatus(Code.TOO_MANY_REQUESTS, "flow limit"); + } + + protected Status convertExceptionToStatus(Throwable t) { + return ResponseBuilder.getInstance().buildStatus(t); + } + + protected void addExecutor(ExecutorService executor, ProxyContext context, V request, Runnable runnable, + StreamObserver responseObserver, Function statusResponseCreator) { + executor.submit(new GrpcTask<>(runnable, context, request, responseObserver, statusResponseCreator.apply(flowLimitStatus()))); + } + + protected void writeResponse(ProxyContext context, V request, T response, StreamObserver responseObserver, + Throwable t, Function errorResponseCreator) { + if (t != null) { + ResponseWriter.getInstance().write( + responseObserver, + errorResponseCreator.apply(convertExceptionToStatus(t)) + ); + } else { + ResponseWriter.getInstance().write(responseObserver, response); + } + } + + protected ProxyContext createContext() { + Context ctx = Context.current(); + Metadata headers = InterceptorConstants.METADATA.get(ctx); + ProxyContext context = ProxyContext.create() + .setLocalAddress(getDefaultStringMetadataInfo(headers, InterceptorConstants.LOCAL_ADDRESS)) + .setRemoteAddress(getDefaultStringMetadataInfo(headers, InterceptorConstants.REMOTE_ADDRESS)) + .setClientID(getDefaultStringMetadataInfo(headers, InterceptorConstants.CLIENT_ID)) + .setProtocolType(ChannelProtocolType.GRPC_V2.getName()) + .setLanguage(getDefaultStringMetadataInfo(headers, InterceptorConstants.LANGUAGE)) + .setClientVersion(getDefaultStringMetadataInfo(headers, InterceptorConstants.CLIENT_VERSION)) + .setAction(getDefaultStringMetadataInfo(headers, InterceptorConstants.SIMPLE_RPC_NAME)) + .setNamespace(getDefaultStringMetadataInfo(headers, InterceptorConstants.NAMESPACE_ID)); + if (ctx.getDeadline() != null) { + context.setRemainingMs(ctx.getDeadline().timeRemaining(TimeUnit.MILLISECONDS)); + } + return context; + } + + protected void validateContext(ProxyContext context) { + if (StringUtils.isBlank(context.getClientID())) { + throw new GrpcProxyException(Code.CLIENT_ID_REQUIRED, "client id cannot be empty"); + } + } + + protected String getDefaultStringMetadataInfo(Metadata headers, Metadata.Key key) { + return StringUtils.defaultString(headers.get(key)); + } + + @Override + public void queryRoute(QueryRouteRequest request, StreamObserver responseObserver) { + Function statusResponseCreator = status -> QueryRouteResponse.newBuilder().setStatus(status).build(); + ProxyContext context = createContext(); + try { + validateContext(context); + this.addExecutor(this.routeThreadPoolExecutor, + context, + request, + () -> grpcMessingActivity.queryRoute(context, request) + .whenComplete((response, throwable) -> writeResponse(context, request, response, responseObserver, throwable, statusResponseCreator)), + responseObserver, + statusResponseCreator); + } catch (Throwable t) { + writeResponse(context, request, null, responseObserver, t, statusResponseCreator); + } + } + + @Override + public void heartbeat(HeartbeatRequest request, StreamObserver responseObserver) { + Function statusResponseCreator = status -> HeartbeatResponse.newBuilder().setStatus(status).build(); + ProxyContext context = createContext(); + try { + validateContext(context); + this.addExecutor(this.clientManagerThreadPoolExecutor, + context, + request, + () -> grpcMessingActivity.heartbeat(context, request) + .whenComplete((response, throwable) -> writeResponse(context, request, response, responseObserver, throwable, statusResponseCreator)), + responseObserver, + statusResponseCreator); + } catch (Throwable t) { + writeResponse(context, request, null, responseObserver, t, statusResponseCreator); + } + } + + @Override + public void sendMessage(SendMessageRequest request, StreamObserver responseObserver) { + Function statusResponseCreator = status -> SendMessageResponse.newBuilder().setStatus(status).build(); + ProxyContext context = createContext(); + try { + validateContext(context); + this.addExecutor(this.producerThreadPoolExecutor, + context, + request, + () -> grpcMessingActivity.sendMessage(context, request) + .whenComplete((response, throwable) -> writeResponse(context, request, response, responseObserver, throwable, statusResponseCreator)), + responseObserver, + statusResponseCreator); + } catch (Throwable t) { + writeResponse(context, request, null, responseObserver, t, statusResponseCreator); + } + } + + @Override + public void queryAssignment(QueryAssignmentRequest request, + StreamObserver responseObserver) { + Function statusResponseCreator = status -> QueryAssignmentResponse.newBuilder().setStatus(status).build(); + ProxyContext context = createContext(); + try { + validateContext(context); + this.addExecutor(this.routeThreadPoolExecutor, + context, + request, + () -> grpcMessingActivity.queryAssignment(context, request) + .whenComplete((response, throwable) -> writeResponse(context, request, response, responseObserver, throwable, statusResponseCreator)), + responseObserver, + statusResponseCreator); + } catch (Throwable t) { + writeResponse(context, request, null, responseObserver, t, statusResponseCreator); + } + } + + @Override + public void receiveMessage(ReceiveMessageRequest request, StreamObserver responseObserver) { + Function statusResponseCreator = status -> ReceiveMessageResponse.newBuilder().setStatus(status).build(); + ProxyContext context = createContext(); + try { + validateContext(context); + this.addExecutor(this.consumerThreadPoolExecutor, + context, + request, + () -> grpcMessingActivity.receiveMessage(context, request, responseObserver), + responseObserver, + statusResponseCreator); + } catch (Throwable t) { + writeResponse(context, request, null, responseObserver, t, statusResponseCreator); + } + } + + @Override + public void ackMessage(AckMessageRequest request, StreamObserver responseObserver) { + Function statusResponseCreator = status -> AckMessageResponse.newBuilder().setStatus(status).build(); + ProxyContext context = createContext(); + try { + validateContext(context); + this.addExecutor(this.consumerThreadPoolExecutor, + context, + request, + () -> grpcMessingActivity.ackMessage(context, request) + .whenComplete((response, throwable) -> writeResponse(context, request, response, responseObserver, throwable, statusResponseCreator)), + responseObserver, + statusResponseCreator); + } catch (Throwable t) { + writeResponse(context, request, null, responseObserver, t, statusResponseCreator); + } + } + + @Override + public void forwardMessageToDeadLetterQueue(ForwardMessageToDeadLetterQueueRequest request, + StreamObserver responseObserver) { + Function statusResponseCreator = status -> ForwardMessageToDeadLetterQueueResponse.newBuilder().setStatus(status).build(); + ProxyContext context = createContext(); + try { + validateContext(context); + this.addExecutor(this.producerThreadPoolExecutor, + context, + request, + () -> grpcMessingActivity.forwardMessageToDeadLetterQueue(context, request) + .whenComplete((response, throwable) -> writeResponse(context, request, response, responseObserver, throwable, statusResponseCreator)), + responseObserver, + statusResponseCreator); + } catch (Throwable t) { + writeResponse(context, request, null, responseObserver, t, statusResponseCreator); + } + } + + @Override + public void endTransaction(EndTransactionRequest request, StreamObserver responseObserver) { + Function statusResponseCreator = status -> EndTransactionResponse.newBuilder().setStatus(status).build(); + ProxyContext context = createContext(); + try { + validateContext(context); + this.addExecutor(this.transactionThreadPoolExecutor, + context, + request, + () -> grpcMessingActivity.endTransaction(context, request) + .whenComplete((response, throwable) -> writeResponse(context, request, response, responseObserver, throwable, statusResponseCreator)), + responseObserver, + statusResponseCreator); + } catch (Throwable t) { + writeResponse(context, request, null, responseObserver, t, statusResponseCreator); + } + } + + @Override + public void notifyClientTermination(NotifyClientTerminationRequest request, + StreamObserver responseObserver) { + Function statusResponseCreator = status -> NotifyClientTerminationResponse.newBuilder().setStatus(status).build(); + ProxyContext context = createContext(); + try { + validateContext(context); + this.addExecutor(this.clientManagerThreadPoolExecutor, + context, + request, + () -> grpcMessingActivity.notifyClientTermination(context, request) + .whenComplete((response, throwable) -> writeResponse(context, request, response, responseObserver, throwable, statusResponseCreator)), + responseObserver, + statusResponseCreator); + } catch (Throwable t) { + writeResponse(context, request, null, responseObserver, t, statusResponseCreator); + } + } + + @Override + public void changeInvisibleDuration(ChangeInvisibleDurationRequest request, + StreamObserver responseObserver) { + Function statusResponseCreator = status -> ChangeInvisibleDurationResponse.newBuilder().setStatus(status).build(); + ProxyContext context = createContext(); + try { + validateContext(context); + this.addExecutor(this.consumerThreadPoolExecutor, + context, + request, + () -> grpcMessingActivity.changeInvisibleDuration(context, request) + .whenComplete((response, throwable) -> writeResponse(context, request, response, responseObserver, throwable, statusResponseCreator)), + responseObserver, + statusResponseCreator); + } catch (Throwable t) { + writeResponse(context, request, null, responseObserver, t, statusResponseCreator); + } + } + + @Override + public StreamObserver telemetry(StreamObserver responseObserver) { + Function statusResponseCreator = status -> TelemetryCommand.newBuilder().setStatus(status).build(); + ContextStreamObserver responseTelemetryCommand = grpcMessingActivity.telemetry(responseObserver); + return new StreamObserver() { + @Override + public void onNext(TelemetryCommand value) { + ProxyContext context = createContext(); + try { + validateContext(context); + addExecutor(clientManagerThreadPoolExecutor, + context, + value, + () -> responseTelemetryCommand.onNext(context, value), + responseObserver, + statusResponseCreator); + } catch (Throwable t) { + writeResponse(context, value, null, responseObserver, t, statusResponseCreator); + } + } + + @Override + public void onError(Throwable t) { + responseTelemetryCommand.onError(t); + } + + @Override + public void onCompleted() { + responseTelemetryCommand.onCompleted(); + } + }; + } + + @Override + public void shutdown() throws Exception { + this.grpcMessingActivity.shutdown(); + + this.routeThreadPoolExecutor.shutdown(); + this.routeThreadPoolExecutor.shutdown(); + this.producerThreadPoolExecutor.shutdown(); + this.consumerThreadPoolExecutor.shutdown(); + this.clientManagerThreadPoolExecutor.shutdown(); + this.transactionThreadPoolExecutor.shutdown(); + } + + @Override + public void start() throws Exception { + this.grpcMessingActivity.start(); + } + + protected static class GrpcTask implements Runnable { + + protected final Runnable runnable; + protected final ProxyContext context; + protected final V request; + protected final T executeRejectResponse; + protected final StreamObserver streamObserver; + + public GrpcTask(Runnable runnable, ProxyContext context, V request, StreamObserver streamObserver, + T executeRejectResponse) { + this.runnable = runnable; + this.context = context; + this.streamObserver = streamObserver; + this.request = request; + this.executeRejectResponse = executeRejectResponse; + } + + @Override + public void run() { + this.runnable.run(); + } + } + + protected class GrpcTaskRejectedExecutionHandler implements RejectedExecutionHandler { + + public GrpcTaskRejectedExecutionHandler() { + + } + + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + if (r instanceof GrpcTask) { + try { + GrpcTask grpcTask = (GrpcTask) r; + writeResponse(grpcTask.context, grpcTask.request, grpcTask.executeRejectResponse, grpcTask.streamObserver, null, null); + } catch (Throwable t) { + log.warn("write rejected error response failed", t); + } + } + } + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/GrpcMessingActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/GrpcMessingActivity.java new file mode 100644 index 00000000000..77bd3a88f9d --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/GrpcMessingActivity.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.v2; + +import apache.rocketmq.v2.AckMessageRequest; +import apache.rocketmq.v2.AckMessageResponse; +import apache.rocketmq.v2.ChangeInvisibleDurationRequest; +import apache.rocketmq.v2.ChangeInvisibleDurationResponse; +import apache.rocketmq.v2.EndTransactionRequest; +import apache.rocketmq.v2.EndTransactionResponse; +import apache.rocketmq.v2.ForwardMessageToDeadLetterQueueRequest; +import apache.rocketmq.v2.ForwardMessageToDeadLetterQueueResponse; +import apache.rocketmq.v2.HeartbeatRequest; +import apache.rocketmq.v2.HeartbeatResponse; +import apache.rocketmq.v2.NotifyClientTerminationRequest; +import apache.rocketmq.v2.NotifyClientTerminationResponse; +import apache.rocketmq.v2.QueryAssignmentRequest; +import apache.rocketmq.v2.QueryAssignmentResponse; +import apache.rocketmq.v2.QueryRouteRequest; +import apache.rocketmq.v2.QueryRouteResponse; +import apache.rocketmq.v2.ReceiveMessageRequest; +import apache.rocketmq.v2.ReceiveMessageResponse; +import apache.rocketmq.v2.SendMessageRequest; +import apache.rocketmq.v2.SendMessageResponse; +import apache.rocketmq.v2.TelemetryCommand; +import io.grpc.stub.StreamObserver; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.common.utils.StartAndShutdown; + +public interface GrpcMessingActivity extends StartAndShutdown { + + CompletableFuture queryRoute(ProxyContext ctx, QueryRouteRequest request); + + CompletableFuture heartbeat(ProxyContext ctx, HeartbeatRequest request); + + CompletableFuture sendMessage(ProxyContext ctx, SendMessageRequest request); + + CompletableFuture queryAssignment(ProxyContext ctx, QueryAssignmentRequest request); + + void receiveMessage(ProxyContext ctx, ReceiveMessageRequest request, + StreamObserver responseObserver); + + CompletableFuture ackMessage(ProxyContext ctx, AckMessageRequest request); + + CompletableFuture forwardMessageToDeadLetterQueue(ProxyContext ctx, + ForwardMessageToDeadLetterQueueRequest request); + + CompletableFuture endTransaction(ProxyContext ctx, EndTransactionRequest request); + + CompletableFuture notifyClientTermination(ProxyContext ctx, + NotifyClientTerminationRequest request); + + CompletableFuture changeInvisibleDuration(ProxyContext ctx, + ChangeInvisibleDurationRequest request); + + ContextStreamObserver telemetry(StreamObserver responseObserver); +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/channel/GrpcChannelManager.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/channel/GrpcChannelManager.java new file mode 100644 index 00000000000..a18cf7600c1 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/channel/GrpcChannelManager.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.v2.channel; + +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.utils.StartAndShutdown; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.ProxyConfig; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcClientSettingsManager; +import org.apache.rocketmq.proxy.service.relay.ProxyRelayResult; +import org.apache.rocketmq.proxy.service.relay.ProxyRelayService; +import org.apache.rocketmq.remoting.protocol.ResponseCode; + +public class GrpcChannelManager implements StartAndShutdown { + private final ProxyRelayService proxyRelayService; + private final GrpcClientSettingsManager grpcClientSettingsManager; + protected final ConcurrentMap clientIdChannelMap = new ConcurrentHashMap<>(); + + protected final AtomicLong nonceIdGenerator = new AtomicLong(0); + protected final ConcurrentMap resultNonceFutureMap = new ConcurrentHashMap<>(); + + protected final ScheduledExecutorService scheduledExecutorService = ThreadUtils.newSingleThreadScheduledExecutor( + new ThreadFactoryImpl("GrpcChannelManager_") + ); + + public GrpcChannelManager(ProxyRelayService proxyRelayService, GrpcClientSettingsManager grpcClientSettingsManager) { + this.proxyRelayService = proxyRelayService; + this.grpcClientSettingsManager = grpcClientSettingsManager; + this.init(); + } + + protected void init() { + this.scheduledExecutorService.scheduleAtFixedRate( + this::scanExpireResultFuture, + 10, 1, TimeUnit.SECONDS + ); + } + + public GrpcClientChannel createChannel(ProxyContext ctx, String clientId) { + return this.clientIdChannelMap.computeIfAbsent(clientId, + k -> new GrpcClientChannel(proxyRelayService, grpcClientSettingsManager, this, ctx, clientId)); + } + + public GrpcClientChannel getChannel(String clientId) { + return clientIdChannelMap.get(clientId); + } + + public GrpcClientChannel removeChannel(String clientId) { + return this.clientIdChannelMap.remove(clientId); + } + + public String addResponseFuture(CompletableFuture> responseFuture) { + String nonce = this.nextNonce(); + this.resultNonceFutureMap.put(nonce, new ResultFuture<>(responseFuture)); + return nonce; + } + + public CompletableFuture> getAndRemoveResponseFuture(String nonce) { + ResultFuture resultFuture = this.resultNonceFutureMap.remove(nonce); + if (resultFuture != null) { + return resultFuture.future; + } + return null; + } + + protected String nextNonce() { + return String.valueOf(this.nonceIdGenerator.getAndIncrement()); + } + + protected void scanExpireResultFuture() { + ProxyConfig proxyConfig = ConfigurationManager.getProxyConfig(); + long timeOutMs = TimeUnit.SECONDS.toMillis(proxyConfig.getGrpcProxyRelayRequestTimeoutInSeconds()); + + Set nonceSet = this.resultNonceFutureMap.keySet(); + for (String nonce : nonceSet) { + ResultFuture resultFuture = this.resultNonceFutureMap.get(nonce); + if (resultFuture == null) { + continue; + } + if (System.currentTimeMillis() - resultFuture.createTime > timeOutMs) { + resultFuture = this.resultNonceFutureMap.remove(nonce); + if (resultFuture != null) { + resultFuture.future.complete(new ProxyRelayResult<>(ResponseCode.SYSTEM_BUSY, "call remote timeout", null)); + } + } + } + } + + @Override + public void shutdown() throws Exception { + this.scheduledExecutorService.shutdown(); + } + + @Override + public void start() throws Exception { + + } + + protected static class ResultFuture { + public CompletableFuture> future; + public long createTime = System.currentTimeMillis(); + + public ResultFuture(CompletableFuture> future) { + this.future = future; + } + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/channel/GrpcClientChannel.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/channel/GrpcClientChannel.java new file mode 100644 index 00000000000..714d0bf019e --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/channel/GrpcClientChannel.java @@ -0,0 +1,270 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.grpc.v2.channel; + +import apache.rocketmq.v2.PrintThreadStackTraceCommand; +import apache.rocketmq.v2.RecoverOrphanedTransactionCommand; +import apache.rocketmq.v2.Settings; +import apache.rocketmq.v2.TelemetryCommand; +import apache.rocketmq.v2.VerifyMessageCommand; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ComparisonChain; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.TextFormat; +import com.google.protobuf.util.JsonFormat; +import io.grpc.StatusRuntimeException; +import io.grpc.stub.StreamObserver; +import io.netty.channel.Channel; +import io.netty.channel.ChannelId; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.common.channel.ChannelHelper; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcClientSettingsManager; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcConverter; +import org.apache.rocketmq.proxy.processor.channel.ChannelExtendAttributeGetter; +import org.apache.rocketmq.proxy.processor.channel.ChannelProtocolType; +import org.apache.rocketmq.proxy.processor.channel.RemoteChannel; +import org.apache.rocketmq.proxy.processor.channel.RemoteChannelConverter; +import org.apache.rocketmq.proxy.service.relay.ProxyChannel; +import org.apache.rocketmq.proxy.service.relay.ProxyRelayResult; +import org.apache.rocketmq.proxy.service.relay.ProxyRelayService; +import org.apache.rocketmq.proxy.service.transaction.TransactionData; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.header.CheckTransactionStateRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ConsumeMessageDirectlyResultRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerRunningInfoRequestHeader; + +public class GrpcClientChannel extends ProxyChannel implements ChannelExtendAttributeGetter, RemoteChannelConverter { + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + + private final GrpcChannelManager grpcChannelManager; + private final GrpcClientSettingsManager grpcClientSettingsManager; + + private final AtomicReference> telemetryCommandRef = new AtomicReference<>(); + private final Object telemetryWriteLock = new Object(); + private final String clientId; + + public GrpcClientChannel(ProxyRelayService proxyRelayService, GrpcClientSettingsManager grpcClientSettingsManager, + GrpcChannelManager grpcChannelManager, ProxyContext ctx, String clientId) { + super(proxyRelayService, null, new GrpcChannelId(clientId), + ctx.getRemoteAddress(), + ctx.getLocalAddress()); + this.grpcChannelManager = grpcChannelManager; + this.grpcClientSettingsManager = grpcClientSettingsManager; + this.clientId = clientId; + } + + @Override + public String getChannelExtendAttribute() { + Settings settings = this.grpcClientSettingsManager.getRawClientSettings(this.clientId); + if (settings == null) { + return null; + } + try { + return JsonFormat.printer().print(settings); + } catch (InvalidProtocolBufferException e) { + log.error("convert settings to json data failed. settings:{}", settings, e); + } + return null; + } + + public static Settings parseChannelExtendAttribute(Channel channel) { + if (ChannelHelper.getChannelProtocolType(channel).equals(ChannelProtocolType.GRPC_V2) && + channel instanceof ChannelExtendAttributeGetter) { + String attr = ((ChannelExtendAttributeGetter) channel).getChannelExtendAttribute(); + if (attr == null) { + return null; + } + + Settings.Builder builder = Settings.newBuilder(); + try { + JsonFormat.parser().merge(attr, builder); + return builder.build(); + } catch (InvalidProtocolBufferException e) { + log.error("convert settings json data to settings failed. data:{}", attr, e); + return null; + } + } + return null; + } + + @Override + public RemoteChannel toRemoteChannel() { + return new RemoteChannel( + ConfigurationManager.getProxyConfig().getLocalServeAddr(), + this.getRemoteAddress(), + this.getLocalAddress(), + ChannelProtocolType.GRPC_V2, + this.getChannelExtendAttribute()); + } + + protected static class GrpcChannelId implements ChannelId { + + private final String clientId; + + public GrpcChannelId(String clientId) { + this.clientId = clientId; + } + + @Override + public String asShortText() { + return this.clientId; + } + + @Override + public String asLongText() { + return this.clientId; + } + + @Override + public int compareTo(ChannelId o) { + if (this == o) { + return 0; + } + if (o instanceof GrpcChannelId) { + GrpcChannelId other = (GrpcChannelId) o; + return ComparisonChain.start() + .compare(this.clientId, other.clientId) + .result(); + } + + return asLongText().compareTo(o.asLongText()); + } + } + + public void setClientObserver(StreamObserver future) { + this.telemetryCommandRef.set(future); + } + + protected void clearClientObserver(StreamObserver future) { + this.telemetryCommandRef.compareAndSet(future, null); + } + + @Override + public boolean isOpen() { + return this.telemetryCommandRef.get() != null; + } + + @Override + public boolean isActive() { + return this.telemetryCommandRef.get() != null; + } + + @Override + public boolean isWritable() { + return this.telemetryCommandRef.get() != null; + } + + @Override + protected CompletableFuture processOtherMessage(Object msg) { + if (msg instanceof TelemetryCommand) { + TelemetryCommand response = (TelemetryCommand) msg; + this.writeTelemetryCommand(response); + } + return CompletableFuture.completedFuture(null); + } + + @Override + protected CompletableFuture processCheckTransaction(CheckTransactionStateRequestHeader header, + MessageExt messageExt, TransactionData transactionData, CompletableFuture> responseFuture) { + CompletableFuture writeFuture = new CompletableFuture<>(); + try { + this.writeTelemetryCommand(TelemetryCommand.newBuilder() + .setRecoverOrphanedTransactionCommand(RecoverOrphanedTransactionCommand.newBuilder() + .setTransactionId(transactionData.getTransactionId()) + .setMessage(GrpcConverter.getInstance().buildMessage(messageExt)) + .build()) + .build()); + responseFuture.complete(null); + writeFuture.complete(null); + } catch (Throwable t) { + responseFuture.completeExceptionally(t); + writeFuture.completeExceptionally(t); + } + return writeFuture; + } + + @Override + protected CompletableFuture processGetConsumerRunningInfo(RemotingCommand command, + GetConsumerRunningInfoRequestHeader header, + CompletableFuture> responseFuture) { + if (!header.isJstackEnable()) { + return CompletableFuture.completedFuture(null); + } + this.writeTelemetryCommand(TelemetryCommand.newBuilder() + .setPrintThreadStackTraceCommand(PrintThreadStackTraceCommand.newBuilder() + .setNonce(this.grpcChannelManager.addResponseFuture(responseFuture)) + .build()) + .build()); + return CompletableFuture.completedFuture(null); + } + + @Override + protected CompletableFuture processConsumeMessageDirectly(RemotingCommand command, + ConsumeMessageDirectlyResultRequestHeader header, + MessageExt messageExt, CompletableFuture> responseFuture) { + this.writeTelemetryCommand(TelemetryCommand.newBuilder() + .setVerifyMessageCommand(VerifyMessageCommand.newBuilder() + .setNonce(this.grpcChannelManager.addResponseFuture(responseFuture)) + .setMessage(GrpcConverter.getInstance().buildMessage(messageExt)) + .build()) + .build()); + return CompletableFuture.completedFuture(null); + } + + public String getClientId() { + return clientId; + } + + public void writeTelemetryCommand(TelemetryCommand command) { + StreamObserver observer = this.telemetryCommandRef.get(); + if (observer == null) { + log.warn("telemetry command observer is null when try to write data. command:{}, channel:{}", TextFormat.shortDebugString(command), this); + return; + } + synchronized (this.telemetryWriteLock) { + observer = this.telemetryCommandRef.get(); + if (observer == null) { + log.warn("telemetry command observer is null when try to write data. command:{}, channel:{}", TextFormat.shortDebugString(command), this); + return; + } + try { + observer.onNext(command); + } catch (StatusRuntimeException | IllegalStateException exception) { + log.warn("write telemetry failed. command:{}", command, exception); + this.clearClientObserver(observer); + } + } + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("clientId", clientId) + .add("remoteAddress", getRemoteAddress()) + .add("localAddress", getLocalAddress()) + .toString(); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/client/ClientActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/client/ClientActivity.java new file mode 100644 index 00000000000..59d8432ae88 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/client/ClientActivity.java @@ -0,0 +1,486 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.grpc.v2.client; + +import apache.rocketmq.v2.ClientType; +import apache.rocketmq.v2.Code; +import apache.rocketmq.v2.FilterExpression; +import apache.rocketmq.v2.HeartbeatRequest; +import apache.rocketmq.v2.HeartbeatResponse; +import apache.rocketmq.v2.NotifyClientTerminationRequest; +import apache.rocketmq.v2.NotifyClientTerminationResponse; +import apache.rocketmq.v2.Resource; +import apache.rocketmq.v2.Settings; +import apache.rocketmq.v2.Status; +import apache.rocketmq.v2.SubscriptionEntry; +import apache.rocketmq.v2.TelemetryCommand; +import apache.rocketmq.v2.ThreadStackTrace; +import apache.rocketmq.v2.VerifyMessageResult; +import io.grpc.StatusRuntimeException; +import io.grpc.stub.StreamObserver; +import io.netty.channel.Channel; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.broker.client.ClientChannelInfo; +import org.apache.rocketmq.broker.client.ConsumerGroupEvent; +import org.apache.rocketmq.broker.client.ConsumerIdsChangeListener; +import org.apache.rocketmq.broker.client.ProducerChangeListener; +import org.apache.rocketmq.broker.client.ProducerGroupEvent; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.common.attribute.TopicMessageType; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.common.channel.ChannelHelper; +import org.apache.rocketmq.proxy.grpc.v2.AbstractMessingActivity; +import org.apache.rocketmq.proxy.grpc.v2.ContextStreamObserver; +import org.apache.rocketmq.proxy.grpc.v2.channel.GrpcChannelManager; +import org.apache.rocketmq.proxy.grpc.v2.channel.GrpcClientChannel; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcClientSettingsManager; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcConverter; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcProxyException; +import org.apache.rocketmq.proxy.grpc.v2.common.ResponseBuilder; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.proxy.service.relay.ProxyRelayResult; +import org.apache.rocketmq.remoting.protocol.LanguageCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.CMResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.filter.FilterAPI; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; + +public class ClientActivity extends AbstractMessingActivity { + + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + + public ClientActivity(MessagingProcessor messagingProcessor, + GrpcClientSettingsManager grpcClientSettingsManager, + GrpcChannelManager grpcChannelManager) { + super(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); + this.init(); + } + + protected void init() { + this.messagingProcessor.registerConsumerListener(new ConsumerIdsChangeListenerImpl()); + this.messagingProcessor.registerProducerListener(new ProducerChangeListenerImpl()); + } + + public CompletableFuture heartbeat(ProxyContext ctx, HeartbeatRequest request) { + CompletableFuture future = new CompletableFuture<>(); + + try { + Settings clientSettings = grpcClientSettingsManager.getClientSettings(ctx); + if (clientSettings == null) { + future.complete(HeartbeatResponse.newBuilder() + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.UNRECOGNIZED_CLIENT_TYPE, "cannot find client settings for this client")) + .build()); + return future; + } + switch (clientSettings.getClientType()) { + case PRODUCER: { + for (Resource topic : clientSettings.getPublishing().getTopicsList()) { + String topicName = topic.getName(); + this.registerProducer(ctx, topicName); + } + break; + } + case PUSH_CONSUMER: + case SIMPLE_CONSUMER: { + validateConsumerGroup(request.getGroup()); + String consumerGroup = request.getGroup().getName(); + this.registerConsumer(ctx, consumerGroup, clientSettings.getClientType(), clientSettings.getSubscription().getSubscriptionsList(), false); + break; + } + default: { + future.complete(HeartbeatResponse.newBuilder() + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.UNRECOGNIZED_CLIENT_TYPE, clientSettings.getClientType().name())) + .build()); + return future; + } + } + future.complete(HeartbeatResponse.newBuilder() + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.OK, Code.OK.name())) + .build()); + return future; + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + public CompletableFuture notifyClientTermination(ProxyContext ctx, + NotifyClientTerminationRequest request) { + CompletableFuture future = new CompletableFuture<>(); + + try { + String clientId = ctx.getClientID(); + LanguageCode languageCode = LanguageCode.valueOf(ctx.getLanguage()); + Settings clientSettings = grpcClientSettingsManager.removeAndGetClientSettings(ctx); + + switch (clientSettings.getClientType()) { + case PRODUCER: + for (Resource topic : clientSettings.getPublishing().getTopicsList()) { + String topicName = topic.getName(); + GrpcClientChannel channel = this.grpcChannelManager.removeChannel(clientId); + if (channel != null) { + ClientChannelInfo clientChannelInfo = new ClientChannelInfo(channel, clientId, languageCode, MQVersion.Version.V5_0_0.ordinal()); + this.messagingProcessor.unRegisterProducer(ctx, topicName, clientChannelInfo); + } + } + break; + case PUSH_CONSUMER: + case SIMPLE_CONSUMER: + validateConsumerGroup(request.getGroup()); + String consumerGroup = request.getGroup().getName(); + GrpcClientChannel channel = this.grpcChannelManager.removeChannel(clientId); + if (channel != null) { + ClientChannelInfo clientChannelInfo = new ClientChannelInfo(channel, clientId, languageCode, MQVersion.Version.V5_0_0.ordinal()); + this.messagingProcessor.unRegisterConsumer(ctx, consumerGroup, clientChannelInfo); + } + break; + default: + future.complete(NotifyClientTerminationResponse.newBuilder() + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.UNRECOGNIZED_CLIENT_TYPE, clientSettings.getClientType().name())) + .build()); + return future; + } + future.complete(NotifyClientTerminationResponse.newBuilder() + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.OK, Code.OK.name())) + .build()); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + public ContextStreamObserver telemetry(StreamObserver responseObserver) { + return new ContextStreamObserver() { + @Override + public void onNext(ProxyContext ctx, TelemetryCommand request) { + try { + switch (request.getCommandCase()) { + case SETTINGS: { + processAndWriteClientSettings(ctx, request, responseObserver); + break; + } + case THREAD_STACK_TRACE: { + reportThreadStackTrace(ctx, request.getStatus(), request.getThreadStackTrace()); + break; + } + case VERIFY_MESSAGE_RESULT: { + reportVerifyMessageResult(ctx, request.getStatus(), request.getVerifyMessageResult()); + break; + } + } + } catch (Throwable t) { + processTelemetryException(request, t, responseObserver); + } + } + + @Override + public void onError(Throwable t) { + log.error("telemetry on error", t); + } + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + + protected void processTelemetryException(TelemetryCommand request, Throwable t, + StreamObserver responseObserver) { + StatusRuntimeException exception = io.grpc.Status.INTERNAL + .withDescription("process client telemetryCommand failed. " + t.getMessage()) + .withCause(t) + .asRuntimeException(); + if (t instanceof GrpcProxyException) { + GrpcProxyException proxyException = (GrpcProxyException) t; + if (proxyException.getCode().getNumber() < Code.INTERNAL_ERROR_VALUE && + proxyException.getCode().getNumber() >= Code.BAD_REQUEST_VALUE) { + exception = io.grpc.Status.INVALID_ARGUMENT + .withDescription("process client telemetryCommand failed. " + t.getMessage()) + .withCause(t) + .asRuntimeException(); + } + } + if (exception.getStatus().getCode().equals(io.grpc.Status.Code.INTERNAL)) { + log.warn("process client telemetryCommand failed. request:{}", request, t); + } + responseObserver.onError(exception); + } + + protected void processAndWriteClientSettings(ProxyContext ctx, TelemetryCommand request, + StreamObserver responseObserver) { + GrpcClientChannel grpcClientChannel = null; + Settings settings = request.getSettings(); + switch (settings.getPubSubCase()) { + case PUBLISHING: + for (Resource topic : settings.getPublishing().getTopicsList()) { + validateTopic(topic); + String topicName = topic.getName(); + grpcClientChannel = registerProducer(ctx, topicName); + grpcClientChannel.setClientObserver(responseObserver); + } + break; + case SUBSCRIPTION: + validateConsumerGroup(settings.getSubscription().getGroup()); + String groupName = settings.getSubscription().getGroup().getName(); + grpcClientChannel = registerConsumer(ctx, groupName, settings.getClientType(), settings.getSubscription().getSubscriptionsList(), true); + grpcClientChannel.setClientObserver(responseObserver); + break; + default: + break; + } + if (Settings.PubSubCase.PUBSUB_NOT_SET.equals(settings.getPubSubCase())) { + responseObserver.onError(io.grpc.Status.INVALID_ARGUMENT + .withDescription("there is no publishing or subscription data in settings") + .asRuntimeException()); + return; + } + TelemetryCommand command = processClientSettings(ctx, request); + if (grpcClientChannel != null) { + grpcClientChannel.writeTelemetryCommand(command); + } else { + responseObserver.onNext(command); + } + } + + protected TelemetryCommand processClientSettings(ProxyContext ctx, TelemetryCommand request) { + String clientId = ctx.getClientID(); + grpcClientSettingsManager.updateClientSettings(ctx, clientId, request.getSettings()); + Settings settings = grpcClientSettingsManager.getClientSettings(ctx); + return TelemetryCommand.newBuilder() + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.OK, Code.OK.name())) + .setSettings(settings) + .build(); + } + + protected GrpcClientChannel registerProducer(ProxyContext ctx, String topicName) { + String clientId = ctx.getClientID(); + LanguageCode languageCode = LanguageCode.valueOf(ctx.getLanguage()); + + GrpcClientChannel channel = this.grpcChannelManager.createChannel(ctx, clientId); + // use topic name as producer group + ClientChannelInfo clientChannelInfo = new ClientChannelInfo(channel, clientId, languageCode, parseClientVersion(ctx.getClientVersion())); + this.messagingProcessor.registerProducer(ctx, topicName, clientChannelInfo); + TopicMessageType topicMessageType = this.messagingProcessor.getMetadataService().getTopicMessageType(ctx, topicName); + if (TopicMessageType.TRANSACTION.equals(topicMessageType)) { + this.messagingProcessor.addTransactionSubscription(ctx, topicName, topicName); + } + return channel; + } + + protected GrpcClientChannel registerConsumer(ProxyContext ctx, String consumerGroup, ClientType clientType, + List subscriptionEntryList, boolean updateSubscription) { + String clientId = ctx.getClientID(); + LanguageCode languageCode = LanguageCode.valueOf(ctx.getLanguage()); + + GrpcClientChannel channel = this.grpcChannelManager.createChannel(ctx, clientId); + ClientChannelInfo clientChannelInfo = new ClientChannelInfo(channel, clientId, languageCode, parseClientVersion(ctx.getClientVersion())); + + this.messagingProcessor.registerConsumer( + ctx, + consumerGroup, + clientChannelInfo, + this.buildConsumeType(clientType), + MessageModel.CLUSTERING, + ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET, + this.buildSubscriptionDataSet(subscriptionEntryList), + updateSubscription + ); + return channel; + } + + private int parseClientVersion(String clientVersionStr) { + int clientVersion = MQVersion.CURRENT_VERSION; + if (!StringUtils.isEmpty(clientVersionStr)) { + try { + String tmp = StringUtils.upperCase(clientVersionStr); + clientVersion = MQVersion.Version.valueOf(tmp).ordinal(); + } catch (Exception ignored) { + } + } + return clientVersion; + } + + protected void reportThreadStackTrace(ProxyContext ctx, Status status, ThreadStackTrace request) { + String nonce = request.getNonce(); + String threadStack = request.getThreadStackTrace(); + CompletableFuture> responseFuture = this.grpcChannelManager.getAndRemoveResponseFuture(nonce); + if (responseFuture != null) { + try { + if (status.getCode().equals(Code.OK)) { + ConsumerRunningInfo runningInfo = new ConsumerRunningInfo(); + runningInfo.setJstack(threadStack); + responseFuture.complete(new ProxyRelayResult<>(ResponseCode.SUCCESS, "", runningInfo)); + } else if (status.getCode().equals(Code.VERIFY_FIFO_MESSAGE_UNSUPPORTED)) { + responseFuture.complete(new ProxyRelayResult<>(ResponseCode.NO_PERMISSION, "forbidden to verify message", null)); + } else { + responseFuture.complete(new ProxyRelayResult<>(ResponseCode.SYSTEM_ERROR, "verify message failed", null)); + } + } catch (Throwable t) { + responseFuture.completeExceptionally(t); + } + } + } + + protected void reportVerifyMessageResult(ProxyContext ctx, Status status, VerifyMessageResult request) { + String nonce = request.getNonce(); + CompletableFuture> responseFuture = this.grpcChannelManager.getAndRemoveResponseFuture(nonce); + if (responseFuture != null) { + try { + ConsumeMessageDirectlyResult result = this.buildConsumeMessageDirectlyResult(status, request); + responseFuture.complete(new ProxyRelayResult<>(ResponseCode.SUCCESS, "", result)); + } catch (Throwable t) { + responseFuture.completeExceptionally(t); + } + } + } + + protected ConsumeMessageDirectlyResult buildConsumeMessageDirectlyResult(Status status, + VerifyMessageResult request) { + ConsumeMessageDirectlyResult consumeMessageDirectlyResult = new ConsumeMessageDirectlyResult(); + switch (status.getCode().getNumber()) { + case Code.OK_VALUE: { + consumeMessageDirectlyResult.setConsumeResult(CMResult.CR_SUCCESS); + break; + } + case Code.FAILED_TO_CONSUME_MESSAGE_VALUE: { + consumeMessageDirectlyResult.setConsumeResult(CMResult.CR_LATER); + break; + } + case Code.MESSAGE_CORRUPTED_VALUE: { + consumeMessageDirectlyResult.setConsumeResult(CMResult.CR_RETURN_NULL); + break; + } + } + consumeMessageDirectlyResult.setRemark("from gRPC client"); + return consumeMessageDirectlyResult; + } + + protected ConsumeType buildConsumeType(ClientType clientType) { + switch (clientType) { + case SIMPLE_CONSUMER: + return ConsumeType.CONSUME_ACTIVELY; + case PUSH_CONSUMER: + return ConsumeType.CONSUME_PASSIVELY; + default: + throw new IllegalArgumentException("Client type is not consumer, type: " + clientType); + } + } + + protected Set buildSubscriptionDataSet(List subscriptionEntryList) { + Set subscriptionDataSet = new HashSet<>(); + for (SubscriptionEntry sub : subscriptionEntryList) { + String topicName = sub.getTopic().getName(); + FilterExpression filterExpression = sub.getExpression(); + subscriptionDataSet.add(buildSubscriptionData(topicName, filterExpression)); + } + return subscriptionDataSet; + } + + protected SubscriptionData buildSubscriptionData(String topicName, FilterExpression filterExpression) { + String expression = filterExpression.getExpression(); + String expressionType = GrpcConverter.getInstance().buildExpressionType(filterExpression.getType()); + try { + return FilterAPI.build(topicName, expression, expressionType); + } catch (Exception e) { + throw new GrpcProxyException(Code.ILLEGAL_FILTER_EXPRESSION, "expression format is not correct", e); + } + } + + protected class ConsumerIdsChangeListenerImpl implements ConsumerIdsChangeListener { + + @Override + public void handle(ConsumerGroupEvent event, String group, Object... args) { + switch (event) { + case CLIENT_UNREGISTER: + processClientUnregister(group, args); + break; + case REGISTER: + processClientRegister(group, args); + break; + default: + break; + } + } + + protected void processClientUnregister(String group, Object... args) { + if (args == null || args.length < 1) { + return; + } + if (args[0] instanceof ClientChannelInfo) { + ClientChannelInfo clientChannelInfo = (ClientChannelInfo) args[0]; + if (ChannelHelper.isRemote(clientChannelInfo.getChannel())) { + return; + } + GrpcClientChannel removedChannel = grpcChannelManager.removeChannel(clientChannelInfo.getClientId()); + log.info("remove grpc channel when client unregister. group:{}, clientChannelInfo:{}, removed:{}", + group, clientChannelInfo, removedChannel != null); + } + } + + protected void processClientRegister(String group, Object... args) { + if (args == null || args.length < 2) { + return; + } + if (args[1] instanceof ClientChannelInfo) { + ClientChannelInfo clientChannelInfo = (ClientChannelInfo) args[1]; + Channel channel = clientChannelInfo.getChannel(); + if (ChannelHelper.isRemote(channel)) { + // save settings from channel sync from other proxy + Settings settings = GrpcClientChannel.parseChannelExtendAttribute(channel); + log.debug("save client settings sync from other proxy. group:{}, channelInfo:{}, settings:{}", group, clientChannelInfo, settings); + if (settings == null) { + return; + } + grpcClientSettingsManager.updateClientSettings( + ProxyContext.createForInner(this.getClass()), + clientChannelInfo.getClientId(), + settings + ); + } + } + } + + @Override + public void shutdown() { + + } + } + + protected class ProducerChangeListenerImpl implements ProducerChangeListener { + + @Override + public void handle(ProducerGroupEvent event, String group, ClientChannelInfo clientChannelInfo) { + if (event == ProducerGroupEvent.CLIENT_UNREGISTER) { + grpcChannelManager.removeChannel(clientChannelInfo.getClientId()); + grpcClientSettingsManager.removeAndGetRawClientSettings(clientChannelInfo.getClientId()); + } + } + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/common/GrpcClientSettingsManager.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/common/GrpcClientSettingsManager.java new file mode 100644 index 00000000000..e741bd389d7 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/common/GrpcClientSettingsManager.java @@ -0,0 +1,248 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.v2.common; + +import apache.rocketmq.v2.Address; +import apache.rocketmq.v2.AddressScheme; +import apache.rocketmq.v2.ClientType; +import apache.rocketmq.v2.CustomizedBackoff; +import apache.rocketmq.v2.Endpoints; +import apache.rocketmq.v2.ExponentialBackoff; +import apache.rocketmq.v2.Metric; +import apache.rocketmq.v2.Settings; +import com.google.protobuf.Duration; +import com.google.protobuf.util.Durations; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import org.apache.rocketmq.broker.client.ConsumerGroupInfo; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.StartAndShutdown; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.MetricCollectorMode; +import org.apache.rocketmq.proxy.config.ProxyConfig; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.remoting.protocol.subscription.CustomizedRetryPolicy; +import org.apache.rocketmq.remoting.protocol.subscription.ExponentialRetryPolicy; +import org.apache.rocketmq.remoting.protocol.subscription.GroupRetryPolicy; +import org.apache.rocketmq.remoting.protocol.subscription.GroupRetryPolicyType; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; + +public class GrpcClientSettingsManager extends ServiceThread implements StartAndShutdown { + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + protected static final Map CLIENT_SETTINGS_MAP = new ConcurrentHashMap<>(); + + private final MessagingProcessor messagingProcessor; + + public GrpcClientSettingsManager(MessagingProcessor messagingProcessor) { + this.messagingProcessor = messagingProcessor; + } + + public Settings getRawClientSettings(String clientId) { + return CLIENT_SETTINGS_MAP.get(clientId); + } + + public Settings getClientSettings(ProxyContext ctx) { + String clientId = ctx.getClientID(); + Settings settings = getRawClientSettings(clientId); + if (settings == null) { + return null; + } + if (settings.hasPublishing()) { + settings = mergeProducerData(settings); + } else if (settings.hasSubscription()) { + settings = mergeSubscriptionData(ctx, settings, settings.getSubscription().getGroup().getName()); + } + return mergeMetric(settings); + } + + protected static Settings mergeProducerData(Settings settings) { + ProxyConfig config = ConfigurationManager.getProxyConfig(); + Settings.Builder builder = settings.toBuilder(); + + builder.getBackoffPolicyBuilder() + .setMaxAttempts(config.getGrpcClientProducerMaxAttempts()) + .setExponentialBackoff(ExponentialBackoff.newBuilder() + .setInitial(Durations.fromMillis(config.getGrpcClientProducerBackoffInitialMillis())) + .setMax(Durations.fromMillis(config.getGrpcClientProducerBackoffMaxMillis())) + .setMultiplier(config.getGrpcClientProducerBackoffMultiplier()) + .build()); + + builder.getPublishingBuilder() + .setValidateMessageType(config.isEnableTopicMessageTypeCheck()) + .setMaxBodySize(config.getMaxMessageSize()); + return builder.build(); + } + + protected Settings mergeSubscriptionData(ProxyContext ctx, Settings settings, String consumerGroup) { + SubscriptionGroupConfig config = this.messagingProcessor.getSubscriptionGroupConfig(ctx, consumerGroup); + if (config == null) { + return settings; + } + + return mergeSubscriptionData(settings, config); + } + + protected Settings mergeMetric(Settings settings) { + // Construct metric according to the proxy config + final ProxyConfig proxyConfig = ConfigurationManager.getProxyConfig(); + final MetricCollectorMode metricCollectorMode = + MetricCollectorMode.getEnumByString(proxyConfig.getMetricCollectorMode()); + final String metricCollectorAddress = proxyConfig.getMetricCollectorAddress(); + final Metric.Builder metricBuilder = Metric.newBuilder(); + switch (metricCollectorMode) { + case ON: + final String[] split = metricCollectorAddress.split(":"); + final String host = split[0]; + final int port = Integer.parseInt(split[1]); + Address address = Address.newBuilder().setHost(host).setPort(port).build(); + final Endpoints endpoints = Endpoints.newBuilder().setScheme(AddressScheme.IPv4) + .addAddresses(address).build(); + metricBuilder.setOn(true).setEndpoints(endpoints); + break; + case PROXY: + metricBuilder.setOn(true).setEndpoints(settings.getAccessPoint()); + break; + case OFF: + default: + metricBuilder.setOn(false); + break; + } + Metric metric = metricBuilder.build(); + return settings.toBuilder().setMetric(metric).build(); + } + + protected static Settings mergeSubscriptionData(Settings settings, SubscriptionGroupConfig groupConfig) { + Settings.Builder resultSettingsBuilder = settings.toBuilder(); + ProxyConfig config = ConfigurationManager.getProxyConfig(); + + resultSettingsBuilder.getSubscriptionBuilder() + .setReceiveBatchSize(config.getGrpcClientConsumerLongPollingBatchSize()) + .setLongPollingTimeout(Durations.fromMillis(config.getGrpcClientConsumerMaxLongPollingTimeoutMillis())) + .setFifo(groupConfig.isConsumeMessageOrderly()); + + resultSettingsBuilder.getBackoffPolicyBuilder().setMaxAttempts(groupConfig.getRetryMaxTimes() + 1); + + GroupRetryPolicy groupRetryPolicy = groupConfig.getGroupRetryPolicy(); + if (groupRetryPolicy.getType().equals(GroupRetryPolicyType.EXPONENTIAL)) { + ExponentialRetryPolicy exponentialRetryPolicy = groupRetryPolicy.getExponentialRetryPolicy(); + if (exponentialRetryPolicy == null) { + exponentialRetryPolicy = new ExponentialRetryPolicy(); + } + resultSettingsBuilder.getBackoffPolicyBuilder().setExponentialBackoff(convertToExponentialBackoff(exponentialRetryPolicy)); + } else { + CustomizedRetryPolicy customizedRetryPolicy = groupRetryPolicy.getCustomizedRetryPolicy(); + if (customizedRetryPolicy == null) { + customizedRetryPolicy = new CustomizedRetryPolicy(); + } + resultSettingsBuilder.getBackoffPolicyBuilder().setCustomizedBackoff(convertToCustomizedRetryPolicy(customizedRetryPolicy)); + } + + return resultSettingsBuilder.build(); + } + + protected static ExponentialBackoff convertToExponentialBackoff(ExponentialRetryPolicy retryPolicy) { + return ExponentialBackoff.newBuilder() + .setInitial(Durations.fromMillis(retryPolicy.getInitial())) + .setMax(Durations.fromMillis(retryPolicy.getMax())) + .setMultiplier(retryPolicy.getMultiplier()) + .build(); + } + + protected static CustomizedBackoff convertToCustomizedRetryPolicy(CustomizedRetryPolicy retryPolicy) { + List durationList = Arrays.stream(retryPolicy.getNext()) + .mapToObj(Durations::fromMillis).collect(Collectors.toList()); + return CustomizedBackoff.newBuilder() + .addAllNext(durationList) + .build(); + } + + public void updateClientSettings(ProxyContext ctx, String clientId, Settings settings) { + if (settings.hasSubscription()) { + settings = createDefaultConsumerSettingsBuilder().mergeFrom(settings).build(); + } + CLIENT_SETTINGS_MAP.put(clientId, settings); + } + + protected Settings.Builder createDefaultConsumerSettingsBuilder() { + return mergeSubscriptionData(Settings.newBuilder().getDefaultInstanceForType(), new SubscriptionGroupConfig()) + .toBuilder(); + } + + public Settings removeAndGetRawClientSettings(String clientId) { + return CLIENT_SETTINGS_MAP.remove(clientId); + } + + public Settings removeAndGetClientSettings(ProxyContext ctx) { + String clientId = ctx.getClientID(); + Settings settings = this.removeAndGetRawClientSettings(clientId); + if (settings == null) { + return null; + } + if (settings.hasSubscription()) { + settings = mergeSubscriptionData(ctx, settings, settings.getSubscription().getGroup().getName()); + } + return mergeMetric(settings); + } + + @Override + public String getServiceName() { + return "GrpcClientSettingsManagerCleaner"; + } + + @Override + public void run() { + while (!this.isStopped()) { + this.waitForRunning(TimeUnit.SECONDS.toMillis(5)); + } + } + + @Override + protected void onWaitEnd() { + Set clientIdSet = CLIENT_SETTINGS_MAP.keySet(); + for (String clientId : clientIdSet) { + try { + CLIENT_SETTINGS_MAP.computeIfPresent(clientId, (clientIdKey, settings) -> { + if (!settings.getClientType().equals(ClientType.PUSH_CONSUMER) && !settings.getClientType().equals(ClientType.SIMPLE_CONSUMER)) { + return settings; + } + String consumerGroup = settings.getSubscription().getGroup().getName(); + ConsumerGroupInfo consumerGroupInfo = this.messagingProcessor.getConsumerGroupInfo( + ProxyContext.createForInner(this.getClass()), + consumerGroup + ); + if (consumerGroupInfo == null || consumerGroupInfo.findChannel(clientId) == null) { + log.info("remove unused grpc client settings. group:{}, settings:{}", consumerGroupInfo, settings); + return null; + } + return settings; + }); + } catch (Throwable t) { + log.error("check expired grpc client settings failed. clientId:{}", clientId, t); + } + } + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/common/GrpcConverter.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/common/GrpcConverter.java new file mode 100644 index 00000000000..33a4e1312f8 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/common/GrpcConverter.java @@ -0,0 +1,260 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.v2.common; + +import apache.rocketmq.v2.Broker; +import apache.rocketmq.v2.DeadLetterQueue; +import apache.rocketmq.v2.Digest; +import apache.rocketmq.v2.DigestType; +import apache.rocketmq.v2.Encoding; +import apache.rocketmq.v2.FilterType; +import apache.rocketmq.v2.Message; +import apache.rocketmq.v2.MessageQueue; +import apache.rocketmq.v2.MessageType; +import apache.rocketmq.v2.Resource; +import apache.rocketmq.v2.SystemProperties; +import com.google.protobuf.ByteString; +import com.google.protobuf.util.Timestamps; +import java.net.SocketAddress; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.filter.ExpressionType; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.common.utils.BinaryUtil; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; + +public class GrpcConverter { + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + + protected static final Object INSTANCE_CREATE_LOCK = new Object(); + protected static volatile GrpcConverter instance; + + public static GrpcConverter getInstance() { + if (instance == null) { + synchronized (INSTANCE_CREATE_LOCK) { + if (instance == null) { + instance = new GrpcConverter(); + } + } + } + return instance; + } + + public MessageQueue buildMessageQueue(MessageExt messageExt, String brokerName) { + Broker broker = Broker.getDefaultInstance(); + if (!StringUtils.isEmpty(brokerName)) { + broker = Broker.newBuilder() + .setName(brokerName) + .setId(0) + .build(); + } + return MessageQueue.newBuilder() + .setId(messageExt.getQueueId()) + .setTopic(Resource.newBuilder() + .setName(NamespaceUtil.withoutNamespace(messageExt.getTopic())) + .setResourceNamespace(NamespaceUtil.getNamespaceFromResource(messageExt.getTopic())) + .build()) + .setBroker(broker) + .build(); + } + + public String buildExpressionType(FilterType filterType) { + switch (filterType) { + case SQL: + return ExpressionType.SQL92; + case TAG: + default: + return ExpressionType.TAG; + } + } + + public Message buildMessage(MessageExt messageExt) { + Map userProperties = buildUserAttributes(messageExt); + SystemProperties systemProperties = buildSystemProperties(messageExt); + Resource topic = buildResource(messageExt.getTopic()); + + return Message.newBuilder() + .setTopic(topic) + .putAllUserProperties(userProperties) + .setSystemProperties(systemProperties) + .setBody(ByteString.copyFrom(messageExt.getBody())) + .build(); + } + + protected Map buildUserAttributes(MessageExt messageExt) { + Map userAttributes = new HashMap<>(); + Map properties = messageExt.getProperties(); + + for (Map.Entry property : properties.entrySet()) { + if (!MessageConst.STRING_HASH_SET.contains(property.getKey())) { + userAttributes.put(property.getKey(), property.getValue()); + } + } + + return userAttributes; + } + + protected SystemProperties buildSystemProperties(MessageExt messageExt) { + SystemProperties.Builder systemPropertiesBuilder = SystemProperties.newBuilder(); + + // tag + String tag = messageExt.getUserProperty(MessageConst.PROPERTY_TAGS); + if (tag != null) { + systemPropertiesBuilder.setTag(tag); + } + + // keys + String keys = messageExt.getKeys(); + if (keys != null) { + String[] keysArray = keys.split(MessageConst.KEY_SEPARATOR); + systemPropertiesBuilder.addAllKeys(Arrays.asList(keysArray)); + } + + // message_id + String uniqKey = messageExt.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX); + + if (uniqKey == null) { + uniqKey = messageExt.getMsgId(); + } + + if (uniqKey != null) { + systemPropertiesBuilder.setMessageId(uniqKey); + } + + // body_digest & body_encoding + String md5Result = BinaryUtil.generateMd5(messageExt.getBody()); + Digest digest = Digest.newBuilder() + .setType(DigestType.MD5) + .setChecksum(md5Result) + .build(); + systemPropertiesBuilder.setBodyDigest(digest); + + if ((messageExt.getSysFlag() & MessageSysFlag.COMPRESSED_FLAG) == MessageSysFlag.COMPRESSED_FLAG) { + systemPropertiesBuilder.setBodyEncoding(Encoding.GZIP); + } else { + systemPropertiesBuilder.setBodyEncoding(Encoding.IDENTITY); + } + + // message_type + String isTrans = messageExt.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED); + String isTransValue = "true"; + if (isTransValue.equals(isTrans)) { + systemPropertiesBuilder.setMessageType(MessageType.TRANSACTION); + } else if (messageExt.getProperty(MessageConst.PROPERTY_DELAY_TIME_LEVEL) != null + || messageExt.getProperty(MessageConst.PROPERTY_TIMER_DELIVER_MS) != null + || messageExt.getProperty(MessageConst.PROPERTY_TIMER_DELAY_SEC) != null) { + systemPropertiesBuilder.setMessageType(MessageType.DELAY); + } else if (messageExt.getProperty(MessageConst.PROPERTY_SHARDING_KEY) != null) { + systemPropertiesBuilder.setMessageType(MessageType.FIFO); + } else { + systemPropertiesBuilder.setMessageType(MessageType.NORMAL); + } + + // born_timestamp (millis) + long bornTimestamp = messageExt.getBornTimestamp(); + systemPropertiesBuilder.setBornTimestamp(Timestamps.fromMillis(bornTimestamp)); + + // born_host + String bornHostString = messageExt.getProperty(MessageConst.PROPERTY_BORN_HOST); + if (StringUtils.isBlank(bornHostString)) { + bornHostString = messageExt.getBornHostString(); + } + if (StringUtils.isNotBlank(bornHostString)) { + systemPropertiesBuilder.setBornHost(bornHostString); + } + + // store_timestamp (millis) + long storeTimestamp = messageExt.getStoreTimestamp(); + systemPropertiesBuilder.setStoreTimestamp(Timestamps.fromMillis(storeTimestamp)); + + // store_host + SocketAddress storeHost = messageExt.getStoreHost(); + if (storeHost != null) { + systemPropertiesBuilder.setStoreHost(NetworkUtil.socketAddress2String(storeHost)); + } + + // delivery_timestamp + String deliverMsString; + long deliverMs; + if (messageExt.getProperty(MessageConst.PROPERTY_TIMER_DELAY_SEC) != null) { + long delayMs = TimeUnit.SECONDS.toMillis(Long.parseLong(messageExt.getProperty(MessageConst.PROPERTY_TIMER_DELAY_SEC))); + deliverMs = System.currentTimeMillis() + delayMs; + systemPropertiesBuilder.setDeliveryTimestamp(Timestamps.fromMillis(deliverMs)); + } else { + deliverMsString = messageExt.getProperty(MessageConst.PROPERTY_TIMER_DELIVER_MS); + if (deliverMsString != null) { + deliverMs = Long.parseLong(deliverMsString); + systemPropertiesBuilder.setDeliveryTimestamp(Timestamps.fromMillis(deliverMs)); + } + } + + // sharding key + String shardingKey = messageExt.getProperty(MessageConst.PROPERTY_SHARDING_KEY); + if (shardingKey != null) { + systemPropertiesBuilder.setMessageGroup(shardingKey); + } + + // receipt_handle && invisible_period + String handle = messageExt.getProperty(MessageConst.PROPERTY_POP_CK); + if (handle != null) { + systemPropertiesBuilder.setReceiptHandle(handle); + } + + // partition_id + systemPropertiesBuilder.setQueueId(messageExt.getQueueId()); + + // partition_offset + systemPropertiesBuilder.setQueueOffset(messageExt.getQueueOffset()); + + // delivery_attempt + systemPropertiesBuilder.setDeliveryAttempt(messageExt.getReconsumeTimes() + 1); + + // trace context + String traceContext = messageExt.getProperty(MessageConst.PROPERTY_TRACE_CONTEXT); + if (traceContext != null) { + systemPropertiesBuilder.setTraceContext(traceContext); + } + + String dlqOriginTopic = messageExt.getProperty(MessageConst.PROPERTY_DLQ_ORIGIN_TOPIC); + String dlqOriginMessageId = messageExt.getProperty(MessageConst.PROPERTY_DLQ_ORIGIN_MESSAGE_ID); + if (dlqOriginTopic != null && dlqOriginMessageId != null) { + DeadLetterQueue dlq = DeadLetterQueue.newBuilder() + .setTopic(dlqOriginTopic) + .setMessageId(dlqOriginMessageId) + .build(); + systemPropertiesBuilder.setDeadLetterQueue(dlq); + } + return systemPropertiesBuilder.build(); + } + + public Resource buildResource(String resourceNameWithNamespace) { + return Resource.newBuilder() + .setResourceNamespace(NamespaceUtil.getNamespaceFromResource(resourceNameWithNamespace)) + .setName(NamespaceUtil.withoutNamespace(resourceNameWithNamespace)) + .build(); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/common/GrpcProxyException.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/common/GrpcProxyException.java new file mode 100644 index 00000000000..74e499b4d72 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/common/GrpcProxyException.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.grpc.v2.common; + +import apache.rocketmq.v2.Code; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.proxy.common.ProxyException; +import org.apache.rocketmq.proxy.common.ProxyExceptionCode; + +public class GrpcProxyException extends RuntimeException { + + private ProxyException proxyException; + private Code code; + + protected static final Map CODE_MAPPING = new ConcurrentHashMap<>(); + + static { + CODE_MAPPING.put(ProxyExceptionCode.INVALID_BROKER_NAME, Code.BAD_REQUEST); + CODE_MAPPING.put(ProxyExceptionCode.INVALID_RECEIPT_HANDLE, Code.INVALID_RECEIPT_HANDLE); + CODE_MAPPING.put(ProxyExceptionCode.FORBIDDEN, Code.FORBIDDEN); + CODE_MAPPING.put(ProxyExceptionCode.INTERNAL_SERVER_ERROR, Code.INTERNAL_SERVER_ERROR); + CODE_MAPPING.put(ProxyExceptionCode.MESSAGE_PROPERTY_CONFLICT_WITH_TYPE, Code.MESSAGE_PROPERTY_CONFLICT_WITH_TYPE); + } + + public GrpcProxyException(Code code, String message) { + super(message); + this.code = code; + } + + public GrpcProxyException(Code code, String message, Throwable t) { + super(message, t); + this.code = code; + } + + public GrpcProxyException(ProxyException proxyException) { + super(proxyException); + this.proxyException = proxyException; + } + + public Code getCode() { + if (this.code != null) { + return this.code; + } + if (this.proxyException != null) { + return CODE_MAPPING.getOrDefault(this.proxyException.getCode(), Code.INTERNAL_SERVER_ERROR); + } + return Code.INTERNAL_SERVER_ERROR; + } + + public ProxyException getProxyException() { + return proxyException; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/common/GrpcValidator.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/common/GrpcValidator.java new file mode 100644 index 00000000000..a556bfe2710 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/common/GrpcValidator.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.v2.common; + +import apache.rocketmq.v2.Code; +import apache.rocketmq.v2.Resource; +import com.google.common.base.CharMatcher; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.Validators; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.config.ConfigurationManager; + +public class GrpcValidator { + protected static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + + protected static final Object INSTANCE_CREATE_LOCK = new Object(); + protected static volatile GrpcValidator instance; + + public static GrpcValidator getInstance() { + if (instance == null) { + synchronized (INSTANCE_CREATE_LOCK) { + if (instance == null) { + instance = new GrpcValidator(); + } + } + } + return instance; + } + + public void validateTopic(Resource topic) { + validateTopic(topic.getName()); + } + + public void validateTopic(String topicName) { + try { + Validators.checkTopic(topicName); + } catch (MQClientException mqClientException) { + throw new GrpcProxyException(Code.ILLEGAL_TOPIC, mqClientException.getErrorMessage()); + } + if (TopicValidator.isSystemTopic(topicName)) { + throw new GrpcProxyException(Code.ILLEGAL_TOPIC, "cannot access system topic"); + } + } + + public void validateConsumerGroup(Resource consumerGroup) { + validateConsumerGroup(consumerGroup.getName()); + } + + public void validateConsumerGroup(String consumerGroupName) { + try { + Validators.checkGroup(consumerGroupName); + } catch (MQClientException mqClientException) { + throw new GrpcProxyException(Code.ILLEGAL_CONSUMER_GROUP, mqClientException.getErrorMessage()); + } + if (MixAll.isSysConsumerGroup(consumerGroupName)) { + throw new GrpcProxyException(Code.ILLEGAL_CONSUMER_GROUP, "cannot use system consumer group"); + } + } + + public void validateTopicAndConsumerGroup(Resource topic, Resource consumerGroup) { + validateTopic(topic); + validateConsumerGroup(consumerGroup); + } + + public void validateInvisibleTime(long invisibleTime) { + validateInvisibleTime(invisibleTime, 0); + } + + public void validateInvisibleTime(long invisibleTime, long minInvisibleTime) { + if (invisibleTime < minInvisibleTime) { + throw new GrpcProxyException(Code.ILLEGAL_INVISIBLE_TIME, "the invisibleTime is too small. min is " + minInvisibleTime); + } + long maxInvisibleTime = ConfigurationManager.getProxyConfig().getMaxInvisibleTimeMills(); + if (maxInvisibleTime <= 0) { + return; + } + if (invisibleTime > maxInvisibleTime) { + throw new GrpcProxyException(Code.ILLEGAL_INVISIBLE_TIME, "the invisibleTime is too large. max is " + maxInvisibleTime); + } + } + + public void validateTag(String tag) { + if (StringUtils.isNotEmpty(tag)) { + if (StringUtils.isBlank(tag)) { + throw new GrpcProxyException(Code.ILLEGAL_MESSAGE_TAG, "tag cannot be the char sequence of whitespace"); + } + if (tag.contains("|")) { + throw new GrpcProxyException(Code.ILLEGAL_MESSAGE_TAG, "tag cannot contain '|'"); + } + if (containControlCharacter(tag)) { + throw new GrpcProxyException(Code.ILLEGAL_MESSAGE_TAG, "tag cannot contain control character"); + } + } + } + + public boolean containControlCharacter(String data) { + for (int i = 0; i < data.length(); i++) { + if (CharMatcher.javaIsoControl().matches(data.charAt(i))) { + return true; + } + } + return false; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/common/ResponseBuilder.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/common/ResponseBuilder.java new file mode 100644 index 00000000000..efa879a9cbd --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/common/ResponseBuilder.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.v2.common; + +import apache.rocketmq.v2.Code; +import apache.rocketmq.v2.Status; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.client.common.ClientErrorCode; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.common.ProxyException; +import org.apache.rocketmq.proxy.common.utils.ExceptionUtils; +import org.apache.rocketmq.proxy.service.route.TopicRouteHelper; +import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; +import org.apache.rocketmq.remoting.protocol.ResponseCode; + +public class ResponseBuilder { + + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + protected static final Map RESPONSE_CODE_MAPPING = new ConcurrentHashMap<>(); + + protected static final Object INSTANCE_CREATE_LOCK = new Object(); + protected static volatile ResponseBuilder instance; + + static { + RESPONSE_CODE_MAPPING.put(ResponseCode.SUCCESS, Code.OK); + RESPONSE_CODE_MAPPING.put(ResponseCode.SYSTEM_BUSY, Code.TOO_MANY_REQUESTS); + RESPONSE_CODE_MAPPING.put(ResponseCode.REQUEST_CODE_NOT_SUPPORTED, Code.NOT_IMPLEMENTED); + RESPONSE_CODE_MAPPING.put(ResponseCode.SUBSCRIPTION_GROUP_NOT_EXIST, Code.CONSUMER_GROUP_NOT_FOUND); + RESPONSE_CODE_MAPPING.put(ClientErrorCode.ACCESS_BROKER_TIMEOUT, Code.PROXY_TIMEOUT); + } + + public static ResponseBuilder getInstance() { + if (instance == null) { + synchronized (INSTANCE_CREATE_LOCK) { + if (instance == null) { + instance = new ResponseBuilder(); + } + } + } + return instance; + } + + public Status buildStatus(Throwable t) { + t = ExceptionUtils.getRealException(t); + + if (t instanceof ProxyException) { + t = new GrpcProxyException((ProxyException) t); + } + if (t instanceof GrpcProxyException) { + GrpcProxyException grpcProxyException = (GrpcProxyException) t; + return buildStatus(grpcProxyException.getCode(), grpcProxyException.getMessage()); + } + if (TopicRouteHelper.isTopicNotExistError(t)) { + return buildStatus(Code.TOPIC_NOT_FOUND, t.getMessage()); + } + if (t instanceof MQBrokerException) { + MQBrokerException mqBrokerException = (MQBrokerException) t; + return buildStatus(buildCode(mqBrokerException.getResponseCode()), mqBrokerException.getErrorMessage()); + } + if (t instanceof MQClientException) { + MQClientException mqClientException = (MQClientException) t; + return buildStatus(buildCode(mqClientException.getResponseCode()), mqClientException.getErrorMessage()); + } + if (t instanceof RemotingTimeoutException) { + return buildStatus(Code.PROXY_TIMEOUT, t.getMessage()); + } + + log.error("internal server error", t); + return buildStatus(Code.INTERNAL_SERVER_ERROR, ExceptionUtils.getErrorDetailMessage(t)); + } + + public Status buildStatus(Code code, String message) { + return Status.newBuilder() + .setCode(code) + .setMessage(message != null ? message : code.name()) + .build(); + } + + public Status buildStatus(int remotingResponseCode, String remark) { + String message = remark; + if (message == null) { + message = String.valueOf(remotingResponseCode); + } + return Status.newBuilder() + .setCode(buildCode(remotingResponseCode)) + .setMessage(message) + .build(); + } + + public Code buildCode(int remotingResponseCode) { + return RESPONSE_CODE_MAPPING.getOrDefault(remotingResponseCode, Code.INTERNAL_SERVER_ERROR); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/common/ResponseWriter.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/common/ResponseWriter.java new file mode 100644 index 00000000000..3ac3d48410e --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/common/ResponseWriter.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.v2.common; + +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.stub.ServerCallStreamObserver; +import io.grpc.stub.StreamObserver; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class ResponseWriter { + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + + protected static final Object INSTANCE_CREATE_LOCK = new Object(); + protected static volatile ResponseWriter instance; + + public static ResponseWriter getInstance() { + if (instance == null) { + synchronized (INSTANCE_CREATE_LOCK) { + if (instance == null) { + instance = new ResponseWriter(); + } + } + } + return instance; + } + + public void write(StreamObserver observer, final T response) { + if (writeResponse(observer, response)) { + observer.onCompleted(); + } + } + + public boolean writeResponse(StreamObserver observer, final T response) { + if (null == response) { + return false; + } + log.debug("start to write response. response: {}", response); + if (isCancelled(observer)) { + log.warn("client has cancelled the request. response to write: {}", response); + return false; + } + try { + observer.onNext(response); + } catch (StatusRuntimeException statusRuntimeException) { + if (Status.CANCELLED.equals(statusRuntimeException.getStatus())) { + log.warn("client has cancelled the request. response to write: {}", response); + return false; + } + throw statusRuntimeException; + } + return true; + } + + public boolean isCancelled(StreamObserver observer) { + if (observer instanceof ServerCallStreamObserver) { + final ServerCallStreamObserver serverCallStreamObserver = (ServerCallStreamObserver) observer; + return serverCallStreamObserver.isCancelled(); + } + return false; + } +} + diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/AckMessageActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/AckMessageActivity.java new file mode 100644 index 00000000000..4a5b9cfcd62 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/AckMessageActivity.java @@ -0,0 +1,203 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.grpc.v2.consumer; + +import apache.rocketmq.v2.AckMessageEntry; +import apache.rocketmq.v2.AckMessageRequest; +import apache.rocketmq.v2.AckMessageResponse; +import apache.rocketmq.v2.AckMessageResultEntry; +import apache.rocketmq.v2.Code; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.client.consumer.AckResult; +import org.apache.rocketmq.client.consumer.AckStatus; +import org.apache.rocketmq.common.consumer.ReceiptHandle; +import org.apache.rocketmq.proxy.common.MessageReceiptHandle; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.grpc.v2.AbstractMessingActivity; +import org.apache.rocketmq.proxy.grpc.v2.channel.GrpcChannelManager; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcClientSettingsManager; +import org.apache.rocketmq.proxy.grpc.v2.common.ResponseBuilder; +import org.apache.rocketmq.proxy.processor.BatchAckResult; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.proxy.service.message.ReceiptHandleMessage; + +public class AckMessageActivity extends AbstractMessingActivity { + + public AckMessageActivity(MessagingProcessor messagingProcessor, GrpcClientSettingsManager grpcClientSettingsManager, + GrpcChannelManager grpcChannelManager) { + super(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); + } + + public CompletableFuture ackMessage(ProxyContext ctx, AckMessageRequest request) { + CompletableFuture future = new CompletableFuture<>(); + + try { + validateTopicAndConsumerGroup(request.getTopic(), request.getGroup()); + String group = request.getGroup().getName(); + String topic = request.getTopic().getName(); + if (ConfigurationManager.getProxyConfig().isEnableBatchAck()) { + future = ackMessageInBatch(ctx, group, topic, request); + } else { + future = ackMessageOneByOne(ctx, group, topic, request); + } + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + protected CompletableFuture ackMessageInBatch(ProxyContext ctx, String group, String topic, AckMessageRequest request) { + List handleMessageList = new ArrayList<>(request.getEntriesCount()); + + for (AckMessageEntry ackMessageEntry : request.getEntriesList()) { + String handleString = getHandleString(ctx, group, request, ackMessageEntry); + handleMessageList.add(new ReceiptHandleMessage(ReceiptHandle.decode(handleString), ackMessageEntry.getMessageId())); + } + return this.messagingProcessor.batchAckMessage(ctx, handleMessageList, group, topic) + .thenApply(batchAckResultList -> { + AckMessageResponse.Builder responseBuilder = AckMessageResponse.newBuilder(); + Set responseCodes = new HashSet<>(); + for (BatchAckResult batchAckResult : batchAckResultList) { + AckMessageResultEntry entry = convertToAckMessageResultEntry(batchAckResult); + responseBuilder.addEntries(entry); + responseCodes.add(entry.getStatus().getCode()); + } + setAckResponseStatus(responseBuilder, responseCodes); + return responseBuilder.build(); + }); + } + + protected AckMessageResultEntry convertToAckMessageResultEntry(BatchAckResult batchAckResult) { + ReceiptHandleMessage handleMessage = batchAckResult.getReceiptHandleMessage(); + AckMessageResultEntry.Builder resultBuilder = AckMessageResultEntry.newBuilder() + .setMessageId(handleMessage.getMessageId()) + .setReceiptHandle(handleMessage.getReceiptHandle().getReceiptHandle()); + if (batchAckResult.getProxyException() != null) { + resultBuilder.setStatus(ResponseBuilder.getInstance().buildStatus(batchAckResult.getProxyException())); + } else { + AckResult ackResult = batchAckResult.getAckResult(); + if (AckStatus.OK.equals(ackResult.getStatus())) { + resultBuilder.setStatus(ResponseBuilder.getInstance().buildStatus(Code.OK, Code.OK.name())); + } else { + resultBuilder.setStatus(ResponseBuilder.getInstance().buildStatus(Code.INTERNAL_SERVER_ERROR, "ack failed: status is abnormal")); + } + } + return resultBuilder.build(); + } + + protected CompletableFuture ackMessageOneByOne(ProxyContext ctx, String group, String topic, AckMessageRequest request) { + CompletableFuture resultFuture = new CompletableFuture<>(); + CompletableFuture[] futures = new CompletableFuture[request.getEntriesCount()]; + for (int i = 0; i < request.getEntriesCount(); i++) { + futures[i] = processAckMessage(ctx, group, topic, request, request.getEntries(i)); + } + CompletableFuture.allOf(futures).whenComplete((val, throwable) -> { + if (throwable != null) { + resultFuture.completeExceptionally(throwable); + return; + } + + Set responseCodes = new HashSet<>(); + List entryList = new ArrayList<>(); + for (CompletableFuture entryFuture : futures) { + AckMessageResultEntry entryResult = entryFuture.join(); + responseCodes.add(entryResult.getStatus().getCode()); + entryList.add(entryResult); + } + AckMessageResponse.Builder responseBuilder = AckMessageResponse.newBuilder() + .addAllEntries(entryList); + setAckResponseStatus(responseBuilder, responseCodes); + resultFuture.complete(responseBuilder.build()); + }); + return resultFuture; + } + + protected CompletableFuture processAckMessage(ProxyContext ctx, String group, String topic, AckMessageRequest request, + AckMessageEntry ackMessageEntry) { + CompletableFuture future = new CompletableFuture<>(); + + try { + String handleString = this.getHandleString(ctx, group, request, ackMessageEntry); + CompletableFuture ackResultFuture = this.messagingProcessor.ackMessage( + ctx, + ReceiptHandle.decode(handleString), + ackMessageEntry.getMessageId(), + group, + topic + ); + ackResultFuture.thenAccept(result -> { + future.complete(convertToAckMessageResultEntry(ctx, ackMessageEntry, result)); + }).exceptionally(t -> { + future.complete(convertToAckMessageResultEntry(ctx, ackMessageEntry, t)); + return null; + }); + } catch (Throwable t) { + future.complete(convertToAckMessageResultEntry(ctx, ackMessageEntry, t)); + } + return future; + } + + protected AckMessageResultEntry convertToAckMessageResultEntry(ProxyContext ctx, AckMessageEntry ackMessageEntry, Throwable throwable) { + return AckMessageResultEntry.newBuilder() + .setStatus(ResponseBuilder.getInstance().buildStatus(throwable)) + .setMessageId(ackMessageEntry.getMessageId()) + .setReceiptHandle(ackMessageEntry.getReceiptHandle()) + .build(); + } + + protected AckMessageResultEntry convertToAckMessageResultEntry(ProxyContext ctx, AckMessageEntry ackMessageEntry, + AckResult ackResult) { + if (AckStatus.OK.equals(ackResult.getStatus())) { + return AckMessageResultEntry.newBuilder() + .setMessageId(ackMessageEntry.getMessageId()) + .setReceiptHandle(ackMessageEntry.getReceiptHandle()) + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.OK, Code.OK.name())) + .build(); + } + return AckMessageResultEntry.newBuilder() + .setMessageId(ackMessageEntry.getMessageId()) + .setReceiptHandle(ackMessageEntry.getReceiptHandle()) + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.INTERNAL_SERVER_ERROR, "ack failed: status is abnormal")) + .build(); + } + + protected void setAckResponseStatus(AckMessageResponse.Builder responseBuilder, Set responseCodes) { + if (responseCodes.size() > 1) { + responseBuilder.setStatus(ResponseBuilder.getInstance().buildStatus(Code.MULTIPLE_RESULTS, Code.MULTIPLE_RESULTS.name())); + } else if (responseCodes.size() == 1) { + Code code = responseCodes.stream().findAny().get(); + responseBuilder.setStatus(ResponseBuilder.getInstance().buildStatus(code, code.name())); + } else { + responseBuilder.setStatus(ResponseBuilder.getInstance().buildStatus(Code.INTERNAL_SERVER_ERROR, "ack message result is empty")); + } + } + + protected String getHandleString(ProxyContext ctx, String group, AckMessageRequest request, AckMessageEntry ackMessageEntry) { + String handleString = ackMessageEntry.getReceiptHandle(); + + MessageReceiptHandle messageReceiptHandle = messagingProcessor.removeReceiptHandle(ctx, grpcChannelManager.getChannel(ctx.getClientID()), group, ackMessageEntry.getMessageId(), ackMessageEntry.getReceiptHandle()); + if (messageReceiptHandle != null) { + handleString = messageReceiptHandle.getReceiptHandleStr(); + } + return handleString; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ChangeInvisibleDurationActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ChangeInvisibleDurationActivity.java new file mode 100644 index 00000000000..b7d63a33c43 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ChangeInvisibleDurationActivity.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.grpc.v2.consumer; + +import apache.rocketmq.v2.ChangeInvisibleDurationRequest; +import apache.rocketmq.v2.ChangeInvisibleDurationResponse; +import apache.rocketmq.v2.Code; +import com.google.protobuf.util.Durations; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.client.consumer.AckResult; +import org.apache.rocketmq.client.consumer.AckStatus; +import org.apache.rocketmq.common.consumer.ReceiptHandle; +import org.apache.rocketmq.proxy.common.MessageReceiptHandle; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.grpc.v2.AbstractMessingActivity; +import org.apache.rocketmq.proxy.grpc.v2.channel.GrpcChannelManager; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcClientSettingsManager; +import org.apache.rocketmq.proxy.grpc.v2.common.ResponseBuilder; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; + +public class ChangeInvisibleDurationActivity extends AbstractMessingActivity { + + public ChangeInvisibleDurationActivity(MessagingProcessor messagingProcessor, + GrpcClientSettingsManager grpcClientSettingsManager, GrpcChannelManager grpcChannelManager) { + super(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); + } + + public CompletableFuture changeInvisibleDuration(ProxyContext ctx, + ChangeInvisibleDurationRequest request) { + CompletableFuture future = new CompletableFuture<>(); + + try { + validateTopicAndConsumerGroup(request.getTopic(), request.getGroup()); + validateInvisibleTime(Durations.toMillis(request.getInvisibleDuration())); + + ReceiptHandle receiptHandle = ReceiptHandle.decode(request.getReceiptHandle()); + String group = request.getGroup().getName(); + + MessageReceiptHandle messageReceiptHandle = messagingProcessor.removeReceiptHandle(ctx, grpcChannelManager.getChannel(ctx.getClientID()), group, request.getMessageId(), receiptHandle.getReceiptHandle()); + if (messageReceiptHandle != null) { + receiptHandle = ReceiptHandle.decode(messageReceiptHandle.getReceiptHandleStr()); + } + return this.messagingProcessor.changeInvisibleTime( + ctx, + receiptHandle, + request.getMessageId(), + group, + request.getTopic().getName(), + Durations.toMillis(request.getInvisibleDuration()) + ).thenApply(ackResult -> convertToChangeInvisibleDurationResponse(ctx, request, ackResult)); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + protected ChangeInvisibleDurationResponse convertToChangeInvisibleDurationResponse(ProxyContext ctx, + ChangeInvisibleDurationRequest request, AckResult ackResult) { + if (AckStatus.OK.equals(ackResult.getStatus())) { + return ChangeInvisibleDurationResponse.newBuilder() + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.OK, Code.OK.name())) + .setReceiptHandle(ackResult.getExtraInfo()) + .build(); + } + return ChangeInvisibleDurationResponse.newBuilder() + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.INTERNAL_SERVER_ERROR, "changeInvisibleDuration failed: status is abnormal")) + .build(); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/PopMessageResultFilterImpl.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/PopMessageResultFilterImpl.java new file mode 100644 index 00000000000..b31106164e8 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/PopMessageResultFilterImpl.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.grpc.v2.consumer; + +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.common.utils.FilterUtils; +import org.apache.rocketmq.proxy.processor.PopMessageResultFilter; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; + +public class PopMessageResultFilterImpl implements PopMessageResultFilter { + + private final int maxAttempts; + + public PopMessageResultFilterImpl(int maxAttempts) { + this.maxAttempts = maxAttempts; + } + + @Override + public FilterResult filterMessage(ProxyContext ctx, String consumerGroup, SubscriptionData subscriptionData, + MessageExt messageExt) { + if (!FilterUtils.isTagMatched(subscriptionData.getTagsSet(), messageExt.getTags())) { + return FilterResult.NO_MATCH; + } + if (messageExt.getReconsumeTimes() >= maxAttempts) { + return FilterResult.TO_DLQ; + } + return FilterResult.MATCH; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivity.java new file mode 100644 index 00000000000..b3550eb4f37 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivity.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.grpc.v2.consumer; + +import apache.rocketmq.v2.Code; +import apache.rocketmq.v2.FilterExpression; +import apache.rocketmq.v2.ReceiveMessageRequest; +import apache.rocketmq.v2.ReceiveMessageResponse; +import apache.rocketmq.v2.Settings; +import apache.rocketmq.v2.Subscription; +import com.google.protobuf.util.Durations; +import io.grpc.stub.StreamObserver; +import java.util.List; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.consumer.PopStatus; +import org.apache.rocketmq.common.constant.ConsumeInitMode; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.proxy.common.MessageReceiptHandle; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.ProxyConfig; +import org.apache.rocketmq.proxy.grpc.v2.AbstractMessingActivity; +import org.apache.rocketmq.proxy.grpc.v2.channel.GrpcChannelManager; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcClientSettingsManager; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcConverter; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.proxy.processor.QueueSelector; +import org.apache.rocketmq.proxy.service.route.AddressableMessageQueue; +import org.apache.rocketmq.proxy.service.route.MessageQueueSelector; +import org.apache.rocketmq.proxy.service.route.MessageQueueView; +import org.apache.rocketmq.remoting.protocol.filter.FilterAPI; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; + +public class ReceiveMessageActivity extends AbstractMessingActivity { + private static final String ILLEGAL_POLLING_TIME_INTRODUCED_CLIENT_VERSION = "5.0.3"; + + public ReceiveMessageActivity(MessagingProcessor messagingProcessor, + GrpcClientSettingsManager grpcClientSettingsManager, GrpcChannelManager grpcChannelManager) { + super(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); + } + + public void receiveMessage(ProxyContext ctx, ReceiveMessageRequest request, + StreamObserver responseObserver) { + ReceiveMessageResponseStreamWriter writer = createWriter(ctx, responseObserver); + + try { + Settings settings = this.grpcClientSettingsManager.getClientSettings(ctx); + Subscription subscription = settings.getSubscription(); + boolean fifo = subscription.getFifo(); + int maxAttempts = settings.getBackoffPolicy().getMaxAttempts(); + ProxyConfig config = ConfigurationManager.getProxyConfig(); + + Long timeRemaining = ctx.getRemainingMs(); + long pollingTime; + if (request.hasLongPollingTimeout()) { + pollingTime = Durations.toMillis(request.getLongPollingTimeout()); + } else { + pollingTime = timeRemaining - Durations.toMillis(settings.getRequestTimeout()) / 2; + } + if (pollingTime < config.getGrpcClientConsumerMinLongPollingTimeoutMillis()) { + pollingTime = config.getGrpcClientConsumerMinLongPollingTimeoutMillis(); + } + if (pollingTime > config.getGrpcClientConsumerMaxLongPollingTimeoutMillis()) { + pollingTime = config.getGrpcClientConsumerMaxLongPollingTimeoutMillis(); + } + + if (pollingTime > timeRemaining) { + if (timeRemaining >= config.getGrpcClientConsumerMinLongPollingTimeoutMillis()) { + pollingTime = timeRemaining; + } else { + final String clientVersion = ctx.getClientVersion(); + Code code = + null == clientVersion || ILLEGAL_POLLING_TIME_INTRODUCED_CLIENT_VERSION.compareTo(clientVersion) > 0 ? + Code.BAD_REQUEST : Code.ILLEGAL_POLLING_TIME; + writer.writeAndComplete(ctx, code, "The deadline time remaining is not enough" + + " for polling, please check network condition"); + return; + } + } + + validateTopicAndConsumerGroup(request.getMessageQueue().getTopic(), request.getGroup()); + String topic = request.getMessageQueue().getTopic().getName(); + String group = request.getGroup().getName(); + + long actualInvisibleTime = Durations.toMillis(request.getInvisibleDuration()); + ProxyConfig proxyConfig = ConfigurationManager.getProxyConfig(); + if (proxyConfig.isEnableProxyAutoRenew() && request.getAutoRenew()) { + actualInvisibleTime = proxyConfig.getDefaultInvisibleTimeMills(); + } else { + validateInvisibleTime(actualInvisibleTime, + ConfigurationManager.getProxyConfig().getMinInvisibleTimeMillsForRecv()); + } + + FilterExpression filterExpression = request.getFilterExpression(); + SubscriptionData subscriptionData; + try { + subscriptionData = FilterAPI.build(topic, filterExpression.getExpression(), + GrpcConverter.getInstance().buildExpressionType(filterExpression.getType())); + } catch (Exception e) { + writer.writeAndComplete(ctx, Code.ILLEGAL_FILTER_EXPRESSION, e.getMessage()); + return; + } + + this.messagingProcessor.popMessage( + ctx, + new ReceiveMessageQueueSelector( + request.getMessageQueue().getBroker().getName() + ), + group, + topic, + request.getBatchSize(), + actualInvisibleTime, + pollingTime, + ConsumeInitMode.MAX, + subscriptionData, + fifo, + new PopMessageResultFilterImpl(maxAttempts), + request.hasAttemptId() ? request.getAttemptId() : null, + timeRemaining + ).thenAccept(popResult -> { + if (proxyConfig.isEnableProxyAutoRenew() && request.getAutoRenew()) { + if (PopStatus.FOUND.equals(popResult.getPopStatus())) { + List messageExtList = popResult.getMsgFoundList(); + for (MessageExt messageExt : messageExtList) { + String receiptHandle = messageExt.getProperty(MessageConst.PROPERTY_POP_CK); + if (receiptHandle != null) { + MessageReceiptHandle messageReceiptHandle = + new MessageReceiptHandle(group, topic, messageExt.getQueueId(), receiptHandle, messageExt.getMsgId(), + messageExt.getQueueOffset(), messageExt.getReconsumeTimes()); + messagingProcessor.addReceiptHandle(ctx, grpcChannelManager.getChannel(ctx.getClientID()), group, messageExt.getMsgId(), messageReceiptHandle); + } + } + } + } + writer.writeAndComplete(ctx, request, popResult); + }) + .exceptionally(t -> { + writer.writeAndComplete(ctx, request, t); + return null; + }); + } catch (Throwable t) { + writer.writeAndComplete(ctx, request, t); + } + } + + protected ReceiveMessageResponseStreamWriter createWriter(ProxyContext ctx, + StreamObserver responseObserver) { + return new ReceiveMessageResponseStreamWriter( + this.messagingProcessor, + responseObserver + ); + } + + protected static class ReceiveMessageQueueSelector implements QueueSelector { + + private final String brokerName; + + public ReceiveMessageQueueSelector(String brokerName) { + this.brokerName = brokerName; + } + + @Override + public AddressableMessageQueue select(ProxyContext ctx, MessageQueueView messageQueueView) { + try { + AddressableMessageQueue addressableMessageQueue = null; + MessageQueueSelector messageQueueSelector = messageQueueView.getReadSelector(); + + if (StringUtils.isNotBlank(brokerName)) { + addressableMessageQueue = messageQueueSelector.getQueueByBrokerName(brokerName); + } + + if (addressableMessageQueue == null) { + addressableMessageQueue = messageQueueSelector.selectOne(true); + } + return addressableMessageQueue; + } catch (Throwable t) { + return null; + } + } + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageResponseStreamWriter.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageResponseStreamWriter.java new file mode 100644 index 00000000000..d0f94e8613a --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageResponseStreamWriter.java @@ -0,0 +1,160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.grpc.v2.consumer; + +import apache.rocketmq.v2.Code; +import apache.rocketmq.v2.Message; +import apache.rocketmq.v2.ReceiveMessageRequest; +import apache.rocketmq.v2.ReceiveMessageResponse; +import com.google.protobuf.util.Timestamps; +import io.grpc.stub.StreamObserver; +import java.time.Duration; +import java.util.Iterator; +import java.util.List; +import org.apache.rocketmq.client.consumer.PopResult; +import org.apache.rocketmq.client.consumer.PopStatus; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.consumer.ReceiptHandle; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcConverter; +import org.apache.rocketmq.proxy.grpc.v2.common.ResponseBuilder; +import org.apache.rocketmq.proxy.grpc.v2.common.ResponseWriter; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; + +public class ReceiveMessageResponseStreamWriter { + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + protected static final long NACK_INVISIBLE_TIME = Duration.ofSeconds(1).toMillis(); + + protected final MessagingProcessor messagingProcessor; + protected final StreamObserver streamObserver; + + public ReceiveMessageResponseStreamWriter( + MessagingProcessor messagingProcessor, + StreamObserver observer) { + this.messagingProcessor = messagingProcessor; + this.streamObserver = observer; + } + + public void writeAndComplete(ProxyContext ctx, ReceiveMessageRequest request, PopResult popResult) { + PopStatus status = popResult.getPopStatus(); + List messageFoundList = popResult.getMsgFoundList(); + try { + switch (status) { + case FOUND: + if (messageFoundList.isEmpty()) { + streamObserver.onNext(ReceiveMessageResponse.newBuilder() + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.MESSAGE_NOT_FOUND, "no match message")) + .build()); + } else { + streamObserver.onNext(ReceiveMessageResponse.newBuilder() + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.OK, Code.OK.name())) + .build()); + Iterator messageIterator = messageFoundList.iterator(); + while (messageIterator.hasNext()) { + MessageExt curMessageExt = messageIterator.next(); + Message curMessage = convertToMessage(curMessageExt); + try { + streamObserver.onNext(ReceiveMessageResponse.newBuilder() + .setMessage(curMessage) + .build()); + } catch (Throwable t) { + this.processThrowableWhenWriteMessage(t, ctx, request, curMessageExt); + messageIterator.forEachRemaining(messageExt -> + this.processThrowableWhenWriteMessage(t, ctx, request, messageExt)); + return; + } + } + } + break; + case POLLING_FULL: + streamObserver.onNext(ReceiveMessageResponse.newBuilder() + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.TOO_MANY_REQUESTS, "polling full")) + .build()); + break; + case NO_NEW_MSG: + case POLLING_NOT_FOUND: + default: + streamObserver.onNext(ReceiveMessageResponse.newBuilder() + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.MESSAGE_NOT_FOUND, "no new message")) + .build()); + break; + } + } catch (Throwable t) { + writeResponseWithErrorIgnore( + ReceiveMessageResponse.newBuilder().setStatus(ResponseBuilder.getInstance().buildStatus(t)).build()); + } finally { + onComplete(); + } + } + + protected Message convertToMessage(MessageExt messageExt) { + return GrpcConverter.getInstance().buildMessage(messageExt); + } + + protected void processThrowableWhenWriteMessage(Throwable throwable, + ProxyContext ctx, ReceiveMessageRequest request, MessageExt messageExt) { + + String handle = messageExt.getProperty(MessageConst.PROPERTY_POP_CK); + if (handle == null) { + return; + } + + this.messagingProcessor.changeInvisibleTime( + ctx, + ReceiptHandle.decode(handle), + messageExt.getMsgId(), + request.getGroup().getName(), + request.getMessageQueue().getTopic().getName(), + NACK_INVISIBLE_TIME + ); + } + + public void writeAndComplete(ProxyContext ctx, Code code, String message) { + writeResponseWithErrorIgnore( + ReceiveMessageResponse.newBuilder().setStatus(ResponseBuilder.getInstance().buildStatus(code, message)).build()); + onComplete(); + } + + public void writeAndComplete(ProxyContext ctx, ReceiveMessageRequest request, Throwable throwable) { + writeResponseWithErrorIgnore( + ReceiveMessageResponse.newBuilder().setStatus(ResponseBuilder.getInstance().buildStatus(throwable)).build()); + onComplete(); + } + + protected void writeResponseWithErrorIgnore(ReceiveMessageResponse response) { + try { + ResponseWriter.getInstance().writeResponse(streamObserver, response); + } catch (Exception e) { + log.error("err when write receive message response", e); + } + } + + protected void onComplete() { + writeResponseWithErrorIgnore(ReceiveMessageResponse.newBuilder() + .setDeliveryTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .build()); + try { + streamObserver.onCompleted(); + } catch (Exception e) { + log.error("err when complete receive message response", e); + } + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/producer/ForwardMessageToDLQActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/producer/ForwardMessageToDLQActivity.java new file mode 100644 index 00000000000..d0cfc14ce00 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/producer/ForwardMessageToDLQActivity.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.grpc.v2.producer; + +import apache.rocketmq.v2.ForwardMessageToDeadLetterQueueRequest; +import apache.rocketmq.v2.ForwardMessageToDeadLetterQueueResponse; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.common.consumer.ReceiptHandle; +import org.apache.rocketmq.proxy.common.MessageReceiptHandle; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.grpc.v2.AbstractMessingActivity; +import org.apache.rocketmq.proxy.grpc.v2.channel.GrpcChannelManager; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcClientSettingsManager; +import org.apache.rocketmq.proxy.grpc.v2.common.ResponseBuilder; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public class ForwardMessageToDLQActivity extends AbstractMessingActivity { + + public ForwardMessageToDLQActivity(MessagingProcessor messagingProcessor, + GrpcClientSettingsManager grpcClientSettingsManager, GrpcChannelManager grpcChannelManager) { + super(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); + } + + public CompletableFuture forwardMessageToDeadLetterQueue(ProxyContext ctx, + ForwardMessageToDeadLetterQueueRequest request) { + CompletableFuture future = new CompletableFuture<>(); + try { + validateTopicAndConsumerGroup(request.getTopic(), request.getGroup()); + + String group = request.getGroup().getName(); + String handleString = request.getReceiptHandle(); + MessageReceiptHandle messageReceiptHandle = messagingProcessor.removeReceiptHandle(ctx, grpcChannelManager.getChannel(ctx.getClientID()), group, request.getMessageId(), request.getReceiptHandle()); + if (messageReceiptHandle != null) { + handleString = messageReceiptHandle.getReceiptHandleStr(); + } + ReceiptHandle receiptHandle = ReceiptHandle.decode(handleString); + + return this.messagingProcessor.forwardMessageToDeadLetterQueue( + ctx, + receiptHandle, + request.getMessageId(), + request.getGroup().getName(), + request.getTopic().getName() + ).thenApply(result -> convertToForwardMessageToDeadLetterQueueResponse(ctx, result)); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + protected ForwardMessageToDeadLetterQueueResponse convertToForwardMessageToDeadLetterQueueResponse(ProxyContext ctx, + RemotingCommand result) { + return ForwardMessageToDeadLetterQueueResponse.newBuilder() + .setStatus(ResponseBuilder.getInstance().buildStatus(result.getCode(), result.getRemark())) + .build(); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/producer/SendMessageActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/producer/SendMessageActivity.java new file mode 100644 index 00000000000..8679bfbe388 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/producer/SendMessageActivity.java @@ -0,0 +1,393 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.grpc.v2.producer; + +import apache.rocketmq.v2.Code; +import apache.rocketmq.v2.Encoding; +import apache.rocketmq.v2.MessageType; +import apache.rocketmq.v2.Resource; +import apache.rocketmq.v2.SendMessageRequest; +import apache.rocketmq.v2.SendMessageResponse; +import apache.rocketmq.v2.SendResultEntry; +import com.google.common.collect.Maps; +import com.google.common.hash.Hashing; +import com.google.protobuf.ByteString; +import com.google.protobuf.Timestamp; +import com.google.protobuf.util.Durations; +import com.google.protobuf.util.Timestamps; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.ProxyConfig; +import org.apache.rocketmq.proxy.grpc.v2.AbstractMessingActivity; +import org.apache.rocketmq.proxy.grpc.v2.channel.GrpcChannelManager; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcClientSettingsManager; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcProxyException; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcValidator; +import org.apache.rocketmq.proxy.grpc.v2.common.ResponseBuilder; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.proxy.processor.QueueSelector; +import org.apache.rocketmq.proxy.service.route.AddressableMessageQueue; +import org.apache.rocketmq.proxy.service.route.MessageQueueView; + +public class SendMessageActivity extends AbstractMessingActivity { + + public SendMessageActivity(MessagingProcessor messagingProcessor, + GrpcClientSettingsManager grpcClientSettingsManager, GrpcChannelManager grpcChannelManager) { + super(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); + } + + public CompletableFuture sendMessage(ProxyContext ctx, SendMessageRequest request) { + CompletableFuture future = new CompletableFuture<>(); + + try { + if (request.getMessagesCount() <= 0) { + throw new GrpcProxyException(Code.MESSAGE_CORRUPTED, "no message to send"); + } + + List messageList = request.getMessagesList(); + apache.rocketmq.v2.Message message = messageList.get(0); + Resource topic = message.getTopic(); + validateTopic(topic); + + future = this.messagingProcessor.sendMessage( + ctx, + new SendMessageQueueSelector(request), + topic.getName(), + buildSysFlag(message), + buildMessage(ctx, request.getMessagesList(), topic) + ).thenApply(result -> convertToSendMessageResponse(ctx, request, result)); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + protected List buildMessage(ProxyContext context, List protoMessageList, + Resource topic) { + String topicName = topic.getName(); + List messageExtList = new ArrayList<>(); + for (apache.rocketmq.v2.Message protoMessage : protoMessageList) { + if (!protoMessage.getTopic().equals(topic)) { + throw new GrpcProxyException(Code.MESSAGE_CORRUPTED, "topic in message is not same"); + } + // here use topicName as producerGroup for transactional checker. + messageExtList.add(buildMessage(context, protoMessage, topicName)); + } + return messageExtList; + } + + protected Message buildMessage(ProxyContext context, apache.rocketmq.v2.Message protoMessage, String producerGroup) { + String topicName = protoMessage.getTopic().getName(); + + validateMessageBodySize(protoMessage.getBody()); + Message messageExt = new Message(); + messageExt.setTopic(topicName); + messageExt.setBody(protoMessage.getBody().toByteArray()); + Map messageProperty = this.buildMessageProperty(context, protoMessage, producerGroup); + + MessageAccessor.setProperties(messageExt, messageProperty); + return messageExt; + } + + protected int buildSysFlag(apache.rocketmq.v2.Message protoMessage) { + // sysFlag (body encoding & message type) + int sysFlag = 0; + Encoding bodyEncoding = protoMessage.getSystemProperties().getBodyEncoding(); + if (bodyEncoding.equals(Encoding.GZIP)) { + sysFlag |= MessageSysFlag.COMPRESSED_FLAG; + } + // transaction + MessageType messageType = protoMessage.getSystemProperties().getMessageType(); + if (messageType.equals(MessageType.TRANSACTION)) { + sysFlag |= MessageSysFlag.TRANSACTION_PREPARED_TYPE; + } + return sysFlag; + } + + protected void validateMessageBodySize(ByteString body) { + int max = ConfigurationManager.getProxyConfig().getMaxMessageSize(); + if (max <= 0) { + return; + } + if (body.size() > max) { + throw new GrpcProxyException(Code.MESSAGE_BODY_TOO_LARGE, "message body cannot exceed the max " + max); + } + } + + protected void validateMessageKey(String key) { + if (StringUtils.isNotEmpty(key)) { + if (StringUtils.isBlank(key)) { + throw new GrpcProxyException(Code.ILLEGAL_MESSAGE_KEY, "key cannot be the char sequence of whitespace"); + } + if (GrpcValidator.getInstance().containControlCharacter(key)) { + throw new GrpcProxyException(Code.ILLEGAL_MESSAGE_KEY, "key cannot contain control character"); + } + } + } + + protected void validateMessageGroup(String messageGroup) { + if (StringUtils.isNotEmpty(messageGroup)) { + if (StringUtils.isBlank(messageGroup)) { + throw new GrpcProxyException(Code.ILLEGAL_MESSAGE_GROUP, "message group cannot be the char sequence of whitespace"); + } + int maxSize = ConfigurationManager.getProxyConfig().getMaxMessageGroupSize(); + if (maxSize <= 0) { + return; + } + if (messageGroup.getBytes(StandardCharsets.UTF_8).length >= maxSize) { + throw new GrpcProxyException(Code.ILLEGAL_MESSAGE_GROUP, "message group exceed the max size " + maxSize); + } + if (GrpcValidator.getInstance().containControlCharacter(messageGroup)) { + throw new GrpcProxyException(Code.ILLEGAL_MESSAGE_GROUP, "message group cannot contain control character"); + } + } + } + + protected void validateDelayTime(long deliveryTimestampMs) { + long maxDelay = ConfigurationManager.getProxyConfig().getMaxDelayTimeMills(); + if (maxDelay <= 0) { + return; + } + if (deliveryTimestampMs - System.currentTimeMillis() > maxDelay) { + throw new GrpcProxyException(Code.ILLEGAL_DELIVERY_TIME, "the max delay time of message is too large, max is " + maxDelay); + } + } + + protected void validateTransactionRecoverySecond(long transactionRecoverySecond) { + long maxTransactionRecoverySecond = ConfigurationManager.getProxyConfig().getMaxTransactionRecoverySecond(); + if (maxTransactionRecoverySecond <= 0) { + return; + } + if (transactionRecoverySecond > maxTransactionRecoverySecond) { + throw new GrpcProxyException(Code.BAD_REQUEST, "the max transaction recovery time of message is too large, max is " + maxTransactionRecoverySecond); + } + } + + protected Map buildMessageProperty(ProxyContext context, apache.rocketmq.v2.Message message, String producerGroup) { + long userPropertySize = 0; + ProxyConfig config = ConfigurationManager.getProxyConfig(); + org.apache.rocketmq.common.message.Message messageWithHeader = new org.apache.rocketmq.common.message.Message(); + // set user properties + Map userProperties = message.getUserPropertiesMap(); + if (userProperties.size() > config.getUserPropertyMaxNum()) { + throw new GrpcProxyException(Code.MESSAGE_PROPERTIES_TOO_LARGE, "too many user properties, max is " + config.getUserPropertyMaxNum()); + } + for (Map.Entry userPropertiesEntry : userProperties.entrySet()) { + if (MessageConst.STRING_HASH_SET.contains(userPropertiesEntry.getKey())) { + throw new GrpcProxyException(Code.ILLEGAL_MESSAGE_PROPERTY_KEY, "property is used by system: " + userPropertiesEntry.getKey()); + } + if (GrpcValidator.getInstance().containControlCharacter(userPropertiesEntry.getKey())) { + throw new GrpcProxyException(Code.ILLEGAL_MESSAGE_PROPERTY_KEY, "the key of property cannot contain control character"); + } + if (GrpcValidator.getInstance().containControlCharacter(userPropertiesEntry.getValue())) { + throw new GrpcProxyException(Code.ILLEGAL_MESSAGE_PROPERTY_KEY, "the value of property cannot contain control character"); + } + userPropertySize += userPropertiesEntry.getKey().getBytes(StandardCharsets.UTF_8).length; + userPropertySize += userPropertiesEntry.getValue().getBytes(StandardCharsets.UTF_8).length; + } + MessageAccessor.setProperties(messageWithHeader, Maps.newHashMap(userProperties)); + + // set tag + String tag = message.getSystemProperties().getTag(); + GrpcValidator.getInstance().validateTag(tag); + messageWithHeader.setTags(tag); + userPropertySize += tag.getBytes(StandardCharsets.UTF_8).length; + + // set keys + List keysList = message.getSystemProperties().getKeysList(); + for (String key : keysList) { + validateMessageKey(key); + userPropertySize += key.getBytes(StandardCharsets.UTF_8).length; + } + if (keysList.size() > 0) { + messageWithHeader.setKeys(keysList); + } + + if (userPropertySize > config.getMaxUserPropertySize()) { + throw new GrpcProxyException(Code.MESSAGE_PROPERTIES_TOO_LARGE, "the total size of user property is too large, max is " + config.getMaxUserPropertySize()); + } + + // set message id + String messageId = message.getSystemProperties().getMessageId(); + if (StringUtils.isBlank(messageId)) { + throw new GrpcProxyException(Code.ILLEGAL_MESSAGE_ID, "message id cannot be empty"); + } + MessageAccessor.putProperty(messageWithHeader, MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, messageId); + + // set transaction property + MessageType messageType = message.getSystemProperties().getMessageType(); + if (messageType.equals(MessageType.TRANSACTION)) { + MessageAccessor.putProperty(messageWithHeader, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true"); + + if (message.getSystemProperties().hasOrphanedTransactionRecoveryDuration()) { + long transactionRecoverySecond = Durations.toSeconds(message.getSystemProperties().getOrphanedTransactionRecoveryDuration()); + validateTransactionRecoverySecond(transactionRecoverySecond); + MessageAccessor.putProperty(messageWithHeader, MessageConst.PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS, + String.valueOf(transactionRecoverySecond)); + } + } + + // set delay level or deliver timestamp + fillDelayMessageProperty(message, messageWithHeader); + + // set reconsume times + int reconsumeTimes = message.getSystemProperties().getDeliveryAttempt(); + MessageAccessor.setReconsumeTime(messageWithHeader, String.valueOf(reconsumeTimes)); + // set producer group + MessageAccessor.putProperty(messageWithHeader, MessageConst.PROPERTY_PRODUCER_GROUP, producerGroup); + // set message group + String messageGroup = message.getSystemProperties().getMessageGroup(); + if (StringUtils.isNotEmpty(messageGroup)) { + validateMessageGroup(messageGroup); + MessageAccessor.putProperty(messageWithHeader, MessageConst.PROPERTY_SHARDING_KEY, messageGroup); + } + // set trace context + String traceContext = message.getSystemProperties().getTraceContext(); + if (!traceContext.isEmpty()) { + MessageAccessor.putProperty(messageWithHeader, MessageConst.PROPERTY_TRACE_CONTEXT, traceContext); + } + + String bornHost = message.getSystemProperties().getBornHost(); + if (StringUtils.isBlank(bornHost)) { + bornHost = context.getRemoteAddress(); + } + if (StringUtils.isNotBlank(bornHost)) { + MessageAccessor.putProperty(messageWithHeader, MessageConst.PROPERTY_BORN_HOST, bornHost); + } + + Timestamp bornTimestamp = message.getSystemProperties().getBornTimestamp(); + if (Timestamps.isValid(bornTimestamp)) { + MessageAccessor.putProperty(messageWithHeader, MessageConst.PROPERTY_BORN_TIMESTAMP, String.valueOf(Timestamps.toMillis(bornTimestamp))); + } + + return messageWithHeader.getProperties(); + } + + protected void fillDelayMessageProperty(apache.rocketmq.v2.Message message, org.apache.rocketmq.common.message.Message messageWithHeader) { + if (message.getSystemProperties().hasDeliveryTimestamp()) { + Timestamp deliveryTimestamp = message.getSystemProperties().getDeliveryTimestamp(); + long deliveryTimestampMs = Timestamps.toMillis(deliveryTimestamp); + validateDelayTime(deliveryTimestampMs); + + ProxyConfig config = ConfigurationManager.getProxyConfig(); + if (config.isUseDelayLevel()) { + int delayLevel = config.computeDelayLevel(deliveryTimestampMs); + MessageAccessor.putProperty(messageWithHeader, MessageConst.PROPERTY_DELAY_TIME_LEVEL, String.valueOf(delayLevel)); + } + + String timestampString = String.valueOf(deliveryTimestampMs); + MessageAccessor.putProperty(messageWithHeader, MessageConst.PROPERTY_TIMER_DELIVER_MS, timestampString); + } + } + + protected SendMessageResponse convertToSendMessageResponse(ProxyContext ctx, SendMessageRequest request, + List resultList) { + SendMessageResponse.Builder builder = SendMessageResponse.newBuilder(); + + Set responseCodes = new HashSet<>(); + for (SendResult result : resultList) { + SendResultEntry resultEntry; + switch (result.getSendStatus()) { + case FLUSH_DISK_TIMEOUT: + resultEntry = SendResultEntry.newBuilder() + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.MASTER_PERSISTENCE_TIMEOUT, "send message failed, sendStatus=" + result.getSendStatus())) + .build(); + break; + case FLUSH_SLAVE_TIMEOUT: + resultEntry = SendResultEntry.newBuilder() + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.SLAVE_PERSISTENCE_TIMEOUT, "send message failed, sendStatus=" + result.getSendStatus())) + .build(); + break; + case SLAVE_NOT_AVAILABLE: + resultEntry = SendResultEntry.newBuilder() + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.HA_NOT_AVAILABLE, "send message failed, sendStatus=" + result.getSendStatus())) + .build(); + break; + case SEND_OK: + resultEntry = SendResultEntry.newBuilder() + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.OK, Code.OK.name())) + .setOffset(result.getQueueOffset()) + .setMessageId(StringUtils.defaultString(result.getMsgId())) + .setTransactionId(StringUtils.defaultString(result.getTransactionId())) + .build(); + break; + default: + resultEntry = SendResultEntry.newBuilder() + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.INTERNAL_SERVER_ERROR, "send message failed, sendStatus=" + result.getSendStatus())) + .build(); + break; + } + builder.addEntries(resultEntry); + responseCodes.add(resultEntry.getStatus().getCode()); + } + if (responseCodes.size() > 1) { + builder.setStatus(ResponseBuilder.getInstance().buildStatus(Code.MULTIPLE_RESULTS, Code.MULTIPLE_RESULTS.name())); + } else if (responseCodes.size() == 1) { + Code code = responseCodes.stream().findAny().get(); + builder.setStatus(ResponseBuilder.getInstance().buildStatus(code, code.name())); + } else { + builder.setStatus(ResponseBuilder.getInstance().buildStatus(Code.INTERNAL_SERVER_ERROR, "send status is empty")); + } + return builder.build(); + } + + protected static class SendMessageQueueSelector implements QueueSelector { + + private final SendMessageRequest request; + + public SendMessageQueueSelector(SendMessageRequest request) { + this.request = request; + } + + @Override + public AddressableMessageQueue select(ProxyContext ctx, MessageQueueView messageQueueView) { + try { + apache.rocketmq.v2.Message message = request.getMessages(0); + String shardingKey = null; + if (request.getMessagesCount() == 1) { + shardingKey = message.getSystemProperties().getMessageGroup(); + } + AddressableMessageQueue targetMessageQueue; + if (StringUtils.isNotEmpty(shardingKey)) { + // With shardingKey + List writeQueues = messageQueueView.getWriteSelector().getQueues(); + int bucket = Hashing.consistentHash(shardingKey.hashCode(), writeQueues.size()); + targetMessageQueue = writeQueues.get(bucket); + } else { + targetMessageQueue = messageQueueView.getWriteSelector().selectOneByPipeline(false); + } + return targetMessageQueue; + } catch (Exception e) { + return null; + } + } + } + +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/route/RouteActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/route/RouteActivity.java new file mode 100644 index 00000000000..fe14fe01c64 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/route/RouteActivity.java @@ -0,0 +1,305 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.grpc.v2.route; + +import apache.rocketmq.v2.Address; +import apache.rocketmq.v2.AddressScheme; +import apache.rocketmq.v2.Assignment; +import apache.rocketmq.v2.Broker; +import apache.rocketmq.v2.Code; +import apache.rocketmq.v2.Endpoints; +import apache.rocketmq.v2.MessageQueue; +import apache.rocketmq.v2.MessageType; +import apache.rocketmq.v2.Permission; +import apache.rocketmq.v2.QueryAssignmentRequest; +import apache.rocketmq.v2.QueryAssignmentResponse; +import apache.rocketmq.v2.QueryRouteRequest; +import apache.rocketmq.v2.QueryRouteResponse; +import apache.rocketmq.v2.Resource; +import com.google.common.net.HostAndPort; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.attribute.TopicMessageType; +import org.apache.rocketmq.common.constant.PermName; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.grpc.v2.AbstractMessingActivity; +import org.apache.rocketmq.proxy.grpc.v2.channel.GrpcChannelManager; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcClientSettingsManager; +import org.apache.rocketmq.proxy.grpc.v2.common.ResponseBuilder; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.proxy.service.route.ProxyTopicRouteData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; + +public class RouteActivity extends AbstractMessingActivity { + + public RouteActivity(MessagingProcessor messagingProcessor, + GrpcClientSettingsManager grpcClientSettingsManager, GrpcChannelManager grpcChannelManager) { + super(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); + } + + public CompletableFuture queryRoute(ProxyContext ctx, QueryRouteRequest request) { + CompletableFuture future = new CompletableFuture<>(); + try { + validateTopic(request.getTopic()); + List addressList = this.convertToAddressList(request.getEndpoints()); + + String topicName = request.getTopic().getName(); + ProxyTopicRouteData proxyTopicRouteData = this.messagingProcessor.getTopicRouteDataForProxy( + ctx, addressList, topicName); + + List messageQueueList = new ArrayList<>(); + Map> brokerMap = buildBrokerMap(proxyTopicRouteData.getBrokerDatas()); + + TopicMessageType topicMessageType = messagingProcessor.getMetadataService().getTopicMessageType(ctx, topicName); + for (QueueData queueData : proxyTopicRouteData.getQueueDatas()) { + String brokerName = queueData.getBrokerName(); + Map brokerIdMap = brokerMap.get(brokerName); + if (brokerIdMap == null) { + break; + } + for (Broker broker : brokerIdMap.values()) { + messageQueueList.addAll(this.genMessageQueueFromQueueData(queueData, request.getTopic(), topicMessageType, broker)); + } + } + + QueryRouteResponse response = QueryRouteResponse.newBuilder() + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.OK, Code.OK.name())) + .addAllMessageQueues(messageQueueList) + .build(); + future.complete(response); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + public CompletableFuture queryAssignment(ProxyContext ctx, + QueryAssignmentRequest request) { + CompletableFuture future = new CompletableFuture<>(); + + try { + validateTopicAndConsumerGroup(request.getTopic(), request.getGroup()); + List addressList = this.convertToAddressList(request.getEndpoints()); + + ProxyTopicRouteData proxyTopicRouteData = this.messagingProcessor.getTopicRouteDataForProxy( + ctx, + addressList, + request.getTopic().getName()); + + boolean fifo = false; + SubscriptionGroupConfig config = this.messagingProcessor.getSubscriptionGroupConfig(ctx, + request.getGroup().getName()); + if (config != null && config.isConsumeMessageOrderly()) { + fifo = true; + } + + List assignments = new ArrayList<>(); + Map> brokerMap = buildBrokerMap(proxyTopicRouteData.getBrokerDatas()); + for (QueueData queueData : proxyTopicRouteData.getQueueDatas()) { + if (PermName.isReadable(queueData.getPerm()) && queueData.getReadQueueNums() > 0) { + Map brokerIdMap = brokerMap.get(queueData.getBrokerName()); + if (brokerIdMap != null) { + Broker broker = brokerIdMap.get(MixAll.MASTER_ID); + Permission permission = this.convertToPermission(queueData.getPerm()); + if (fifo) { + for (int i = 0; i < queueData.getReadQueueNums(); i++) { + MessageQueue defaultMessageQueue = MessageQueue.newBuilder() + .setTopic(request.getTopic()) + .setId(i) + .setPermission(permission) + .setBroker(broker) + .build(); + assignments.add(Assignment.newBuilder() + .setMessageQueue(defaultMessageQueue) + .build()); + } + } else { + MessageQueue defaultMessageQueue = MessageQueue.newBuilder() + .setTopic(request.getTopic()) + .setId(-1) + .setPermission(permission) + .setBroker(broker) + .build(); + assignments.add(Assignment.newBuilder() + .setMessageQueue(defaultMessageQueue) + .build()); + } + + } + } + } + + QueryAssignmentResponse response; + if (assignments.isEmpty()) { + response = QueryAssignmentResponse.newBuilder() + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.FORBIDDEN, "no readable queue")) + .build(); + } else { + response = QueryAssignmentResponse.newBuilder() + .addAllAssignments(assignments) + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.OK, Code.OK.name())) + .build(); + } + future.complete(response); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + protected Permission convertToPermission(int perm) { + boolean isReadable = PermName.isReadable(perm); + boolean isWriteable = PermName.isWriteable(perm); + if (isReadable && isWriteable) { + return Permission.READ_WRITE; + } + if (isReadable) { + return Permission.READ; + } + if (isWriteable) { + return Permission.WRITE; + } + return Permission.NONE; + } + + protected List convertToAddressList(Endpoints endpoints) { + + boolean useEndpointPort = ConfigurationManager.getProxyConfig().isUseEndpointPortFromRequest(); + + List addressList = new ArrayList<>(); + for (Address address : endpoints.getAddressesList()) { + int port = ConfigurationManager.getProxyConfig().getGrpcServerPort(); + if (useEndpointPort) { + port = address.getPort(); + } + addressList.add(new org.apache.rocketmq.proxy.common.Address( + org.apache.rocketmq.proxy.common.Address.AddressScheme.valueOf(endpoints.getScheme().name()), + HostAndPort.fromParts(address.getHost(), port))); + } + + return addressList; + + } + + protected Map> buildBrokerMap( + List brokerDataList) { + Map> brokerMap = new HashMap<>(); + for (ProxyTopicRouteData.ProxyBrokerData brokerData : brokerDataList) { + Map brokerIdMap = new HashMap<>(); + String brokerName = brokerData.getBrokerName(); + for (Map.Entry> entry : brokerData.getBrokerAddrs().entrySet()) { + Long brokerId = entry.getKey(); + List
    addressList = new ArrayList<>(); + AddressScheme addressScheme = AddressScheme.IPv4; + for (org.apache.rocketmq.proxy.common.Address address : entry.getValue()) { + addressScheme = AddressScheme.valueOf(address.getAddressScheme().name()); + addressList.add(Address.newBuilder() + .setHost(address.getHostAndPort().getHost()) + .setPort(address.getHostAndPort().getPort()) + .build()); + } + + Broker broker = Broker.newBuilder() + .setName(brokerName) + .setId(Math.toIntExact(brokerId)) + .setEndpoints(Endpoints.newBuilder() + .setScheme(addressScheme) + .addAllAddresses(addressList) + .build()) + .build(); + + brokerIdMap.put(brokerId, broker); + } + brokerMap.put(brokerName, brokerIdMap); + } + return brokerMap; + } + + protected List genMessageQueueFromQueueData(QueueData queueData, Resource topic, + TopicMessageType topicMessageType, Broker broker) { + List messageQueueList = new ArrayList<>(); + + int r = 0; + int w = 0; + int rw = 0; + if (PermName.isWriteable(queueData.getPerm()) && PermName.isReadable(queueData.getPerm())) { + rw = Math.min(queueData.getWriteQueueNums(), queueData.getReadQueueNums()); + r = queueData.getReadQueueNums() - rw; + w = queueData.getWriteQueueNums() - rw; + } else if (PermName.isWriteable(queueData.getPerm())) { + w = queueData.getWriteQueueNums(); + } else if (PermName.isReadable(queueData.getPerm())) { + r = queueData.getReadQueueNums(); + } + + // r here means readOnly queue nums, w means writeOnly queue nums, while rw means both readable and writable queue nums. + int queueIdIndex = 0; + for (int i = 0; i < r; i++) { + MessageQueue messageQueue = MessageQueue.newBuilder().setBroker(broker).setTopic(topic) + .setId(queueIdIndex++) + .setPermission(Permission.READ) + .addAllAcceptMessageTypes(parseTopicMessageType(topicMessageType)) + .build(); + messageQueueList.add(messageQueue); + } + + for (int i = 0; i < w; i++) { + MessageQueue messageQueue = MessageQueue.newBuilder().setBroker(broker).setTopic(topic) + .setId(queueIdIndex++) + .setPermission(Permission.WRITE) + .addAllAcceptMessageTypes(parseTopicMessageType(topicMessageType)) + .build(); + messageQueueList.add(messageQueue); + } + + for (int i = 0; i < rw; i++) { + MessageQueue messageQueue = MessageQueue.newBuilder().setBroker(broker).setTopic(topic) + .setId(queueIdIndex++) + .setPermission(Permission.READ_WRITE) + .addAllAcceptMessageTypes(parseTopicMessageType(topicMessageType)) + .build(); + messageQueueList.add(messageQueue); + } + + return messageQueueList; + } + + private List parseTopicMessageType(TopicMessageType topicMessageType) { + switch (topicMessageType) { + case NORMAL: + return Collections.singletonList(MessageType.NORMAL); + case FIFO: + return Collections.singletonList(MessageType.FIFO); + case TRANSACTION: + return Collections.singletonList(MessageType.TRANSACTION); + case DELAY: + return Collections.singletonList(MessageType.DELAY); + case MIXED: + return Arrays.asList(MessageType.NORMAL, MessageType.FIFO, MessageType.DELAY, MessageType.TRANSACTION); + default: + return Collections.singletonList(MessageType.MESSAGE_TYPE_UNSPECIFIED); + } + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/transaction/EndTransactionActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/transaction/EndTransactionActivity.java new file mode 100644 index 00000000000..1e114865823 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/v2/transaction/EndTransactionActivity.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.grpc.v2.transaction; + +import apache.rocketmq.v2.Code; +import apache.rocketmq.v2.EndTransactionRequest; +import apache.rocketmq.v2.EndTransactionResponse; +import apache.rocketmq.v2.TransactionResolution; +import apache.rocketmq.v2.TransactionSource; +import java.util.concurrent.CompletableFuture; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.grpc.v2.AbstractMessingActivity; +import org.apache.rocketmq.proxy.grpc.v2.channel.GrpcChannelManager; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcClientSettingsManager; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcProxyException; +import org.apache.rocketmq.proxy.grpc.v2.common.ResponseBuilder; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.proxy.processor.TransactionStatus; + +public class EndTransactionActivity extends AbstractMessingActivity { + + public EndTransactionActivity(MessagingProcessor messagingProcessor, + GrpcClientSettingsManager grpcClientSettingsManager, GrpcChannelManager grpcChannelManager) { + super(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); + } + + public CompletableFuture endTransaction(ProxyContext ctx, EndTransactionRequest request) { + CompletableFuture future = new CompletableFuture<>(); + try { + validateTopic(request.getTopic()); + if (StringUtils.isBlank(request.getTransactionId())) { + throw new GrpcProxyException(Code.INVALID_TRANSACTION_ID, "transaction id cannot be empty"); + } + + TransactionStatus transactionStatus = TransactionStatus.UNKNOWN; + TransactionResolution transactionResolution = request.getResolution(); + switch (transactionResolution) { + case COMMIT: + transactionStatus = TransactionStatus.COMMIT; + break; + case ROLLBACK: + transactionStatus = TransactionStatus.ROLLBACK; + break; + default: + break; + } + future = this.messagingProcessor.endTransaction( + ctx, + request.getTransactionId(), + request.getMessageId(), + request.getTopic().getName(), + transactionStatus, + request.getSource().equals(TransactionSource.SOURCE_SERVER_CHECK)) + .thenApply(r -> EndTransactionResponse.newBuilder() + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.OK, Code.OK.name())) + .build()); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/metrics/ProxyMetricsConstant.java b/proxy/src/main/java/org/apache/rocketmq/proxy/metrics/ProxyMetricsConstant.java new file mode 100644 index 00000000000..a7682e6cf93 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/metrics/ProxyMetricsConstant.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.metrics; + +public class ProxyMetricsConstant { + public static final String GAUGE_PROXY_UP = "rocketmq_proxy_up"; + + public static final String LABEL_PROXY_MODE = "proxy_mode"; + public static final String NODE_TYPE_PROXY = "proxy"; +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/metrics/ProxyMetricsManager.java b/proxy/src/main/java/org/apache/rocketmq/proxy/metrics/ProxyMetricsManager.java new file mode 100644 index 00000000000..2b8dac5d8be --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/metrics/ProxyMetricsManager.java @@ -0,0 +1,258 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.metrics; + +import com.google.common.base.Splitter; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.ObservableLongGauge; +import io.opentelemetry.exporter.logging.otlp.OtlpJsonLoggingMetricExporter; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporterBuilder; +import io.opentelemetry.exporter.prometheus.PrometheusHttpServer; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; +import io.opentelemetry.sdk.resources.Resource; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.broker.metrics.BrokerMetricsManager; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.metrics.MetricsExporterType; +import org.apache.rocketmq.common.utils.StartAndShutdown; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.config.ProxyConfig; +import org.slf4j.bridge.SLF4JBridgeHandler; + +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.AGGREGATION_DELTA; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_AGGREGATION; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_CLUSTER_NAME; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_NODE_ID; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.LABEL_NODE_TYPE; +import static org.apache.rocketmq.broker.metrics.BrokerMetricsConstant.OPEN_TELEMETRY_METER_NAME; +import static org.apache.rocketmq.proxy.metrics.ProxyMetricsConstant.GAUGE_PROXY_UP; +import static org.apache.rocketmq.proxy.metrics.ProxyMetricsConstant.LABEL_PROXY_MODE; +import static org.apache.rocketmq.proxy.metrics.ProxyMetricsConstant.NODE_TYPE_PROXY; + +public class ProxyMetricsManager implements StartAndShutdown { + private final static Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + + private static ProxyConfig proxyConfig; + private final static Map LABEL_MAP = new HashMap<>(); + public static Supplier attributesBuilderSupplier; + + private OtlpGrpcMetricExporter metricExporter; + private PeriodicMetricReader periodicMetricReader; + private PrometheusHttpServer prometheusHttpServer; + private MetricExporter loggingMetricExporter; + + public static ObservableLongGauge proxyUp = null; + + public static void initLocalMode(BrokerMetricsManager brokerMetricsManager, ProxyConfig proxyConfig) { + if (proxyConfig.getMetricsExporterType() == MetricsExporterType.DISABLE) { + return; + } + ProxyMetricsManager.proxyConfig = proxyConfig; + LABEL_MAP.put(LABEL_NODE_TYPE, NODE_TYPE_PROXY); + LABEL_MAP.put(LABEL_CLUSTER_NAME, proxyConfig.getProxyClusterName()); + LABEL_MAP.put(LABEL_NODE_ID, proxyConfig.getProxyName()); + LABEL_MAP.put(LABEL_PROXY_MODE, proxyConfig.getProxyMode().toLowerCase()); + initMetrics(brokerMetricsManager.getBrokerMeter(), BrokerMetricsManager::newAttributesBuilder); + } + + public static ProxyMetricsManager initClusterMode(ProxyConfig proxyConfig) { + ProxyMetricsManager.proxyConfig = proxyConfig; + return new ProxyMetricsManager(); + } + + public static AttributesBuilder newAttributesBuilder() { + AttributesBuilder attributesBuilder; + if (attributesBuilderSupplier == null) { + attributesBuilder = Attributes.builder(); + LABEL_MAP.forEach(attributesBuilder::put); + return attributesBuilder; + } + attributesBuilder = attributesBuilderSupplier.get(); + LABEL_MAP.forEach(attributesBuilder::put); + return attributesBuilder; + } + + private static void initMetrics(Meter meter, Supplier attributesBuilderSupplier) { + ProxyMetricsManager.attributesBuilderSupplier = attributesBuilderSupplier; + + proxyUp = meter.gaugeBuilder(GAUGE_PROXY_UP) + .setDescription("proxy status") + .ofLongs() + .buildWithCallback(measurement -> measurement.record(1, newAttributesBuilder().build())); + } + + public ProxyMetricsManager() { + } + + private boolean checkConfig() { + if (proxyConfig == null) { + return false; + } + MetricsExporterType exporterType = proxyConfig.getMetricsExporterType(); + if (!exporterType.isEnable()) { + return false; + } + + switch (exporterType) { + case OTLP_GRPC: + return StringUtils.isNotBlank(proxyConfig.getMetricsGrpcExporterTarget()); + case PROM: + return true; + case LOG: + return true; + } + return false; + } + + @Override + public void start() throws Exception { + MetricsExporterType metricsExporterType = proxyConfig.getMetricsExporterType(); + if (metricsExporterType == MetricsExporterType.DISABLE) { + return; + } + if (!checkConfig()) { + log.error("check metrics config failed, will not export metrics"); + return; + } + + String labels = proxyConfig.getMetricsLabel(); + if (StringUtils.isNotBlank(labels)) { + List kvPairs = Splitter.on(',').omitEmptyStrings().splitToList(labels); + for (String item : kvPairs) { + String[] split = item.split(":"); + if (split.length != 2) { + log.warn("metricsLabel is not valid: {}", labels); + continue; + } + LABEL_MAP.put(split[0], split[1]); + } + } + if (proxyConfig.isMetricsInDelta()) { + LABEL_MAP.put(LABEL_AGGREGATION, AGGREGATION_DELTA); + } + LABEL_MAP.put(LABEL_NODE_TYPE, NODE_TYPE_PROXY); + LABEL_MAP.put(LABEL_CLUSTER_NAME, proxyConfig.getProxyClusterName()); + LABEL_MAP.put(LABEL_NODE_ID, proxyConfig.getProxyName()); + LABEL_MAP.put(LABEL_PROXY_MODE, proxyConfig.getProxyMode().toLowerCase()); + + SdkMeterProviderBuilder providerBuilder = SdkMeterProvider.builder() + .setResource(Resource.empty()); + + if (metricsExporterType == MetricsExporterType.OTLP_GRPC) { + String endpoint = proxyConfig.getMetricsGrpcExporterTarget(); + if (!endpoint.startsWith("http")) { + endpoint = "https://" + endpoint; + } + OtlpGrpcMetricExporterBuilder metricExporterBuilder = OtlpGrpcMetricExporter.builder() + .setEndpoint(endpoint) + .setTimeout(proxyConfig.getMetricGrpcExporterTimeOutInMills(), TimeUnit.MILLISECONDS) + .setAggregationTemporalitySelector(type -> { + if (proxyConfig.isMetricsInDelta() && + (type == InstrumentType.COUNTER || type == InstrumentType.OBSERVABLE_COUNTER || type == InstrumentType.HISTOGRAM)) { + return AggregationTemporality.DELTA; + } + return AggregationTemporality.CUMULATIVE; + }); + + String headers = proxyConfig.getMetricsGrpcExporterHeader(); + if (StringUtils.isNotBlank(headers)) { + Map headerMap = new HashMap<>(); + List kvPairs = Splitter.on(',').omitEmptyStrings().splitToList(headers); + for (String item : kvPairs) { + String[] split = item.split(":"); + if (split.length != 2) { + log.warn("metricsGrpcExporterHeader is not valid: {}", headers); + continue; + } + headerMap.put(split[0], split[1]); + } + headerMap.forEach(metricExporterBuilder::addHeader); + } + + metricExporter = metricExporterBuilder.build(); + + periodicMetricReader = PeriodicMetricReader.builder(metricExporter) + .setInterval(proxyConfig.getMetricGrpcExporterIntervalInMills(), TimeUnit.MILLISECONDS) + .build(); + + providerBuilder.registerMetricReader(periodicMetricReader); + } + + if (metricsExporterType == MetricsExporterType.PROM) { + String promExporterHost = proxyConfig.getMetricsPromExporterHost(); + if (StringUtils.isBlank(promExporterHost)) { + promExporterHost = "0.0.0.0"; + } + prometheusHttpServer = PrometheusHttpServer.builder() + .setHost(promExporterHost) + .setPort(proxyConfig.getMetricsPromExporterPort()) + .build(); + providerBuilder.registerMetricReader(prometheusHttpServer); + } + + if (metricsExporterType == MetricsExporterType.LOG) { + SLF4JBridgeHandler.removeHandlersForRootLogger(); + SLF4JBridgeHandler.install(); + loggingMetricExporter = OtlpJsonLoggingMetricExporter.create(proxyConfig.isMetricsInDelta() ? AggregationTemporality.DELTA : AggregationTemporality.CUMULATIVE); + java.util.logging.Logger.getLogger(OtlpJsonLoggingMetricExporter.class.getName()).setLevel(java.util.logging.Level.FINEST); + periodicMetricReader = PeriodicMetricReader.builder(loggingMetricExporter) + .setInterval(proxyConfig.getMetricLoggingExporterIntervalInMills(), TimeUnit.MILLISECONDS) + .build(); + providerBuilder.registerMetricReader(periodicMetricReader); + } + + Meter proxyMeter = OpenTelemetrySdk.builder() + .setMeterProvider(providerBuilder.build()) + .build() + .getMeter(OPEN_TELEMETRY_METER_NAME); + + initMetrics(proxyMeter, null); + } + + @Override + public void shutdown() throws Exception { + if (proxyConfig.getMetricsExporterType() == MetricsExporterType.OTLP_GRPC) { + periodicMetricReader.forceFlush(); + periodicMetricReader.shutdown(); + metricExporter.shutdown(); + } + if (proxyConfig.getMetricsExporterType() == MetricsExporterType.PROM) { + prometheusHttpServer.forceFlush(); + prometheusHttpServer.shutdown(); + } + if (proxyConfig.getMetricsExporterType() == MetricsExporterType.LOG) { + periodicMetricReader.forceFlush(); + periodicMetricReader.shutdown(); + loggingMetricExporter.shutdown(); + } + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/AbstractProcessor.java b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/AbstractProcessor.java new file mode 100644 index 00000000000..c63212c2314 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/AbstractProcessor.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.processor; + +import org.apache.rocketmq.common.consumer.ReceiptHandle; +import org.apache.rocketmq.common.utils.AbstractStartAndShutdown; +import org.apache.rocketmq.proxy.common.ProxyException; +import org.apache.rocketmq.proxy.common.ProxyExceptionCode; +import org.apache.rocketmq.proxy.service.ServiceManager; + +public abstract class AbstractProcessor extends AbstractStartAndShutdown { + + protected MessagingProcessor messagingProcessor; + protected ServiceManager serviceManager; + + protected static final ProxyException EXPIRED_HANDLE_PROXY_EXCEPTION = new ProxyException(ProxyExceptionCode.INVALID_RECEIPT_HANDLE, "receipt handle is expired"); + + public AbstractProcessor(MessagingProcessor messagingProcessor, + ServiceManager serviceManager) { + this.messagingProcessor = messagingProcessor; + this.serviceManager = serviceManager; + } + + protected void validateReceiptHandle(ReceiptHandle handle) { + if (handle.isExpired()) { + throw EXPIRED_HANDLE_PROXY_EXCEPTION; + } + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/BatchAckResult.java b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/BatchAckResult.java new file mode 100644 index 00000000000..dfb9c9b9e02 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/BatchAckResult.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.processor; + +import org.apache.rocketmq.client.consumer.AckResult; +import org.apache.rocketmq.proxy.common.ProxyException; +import org.apache.rocketmq.proxy.service.message.ReceiptHandleMessage; + +public class BatchAckResult { + + private final ReceiptHandleMessage receiptHandleMessage; + private AckResult ackResult; + private ProxyException proxyException; + + public BatchAckResult(ReceiptHandleMessage receiptHandleMessage, + AckResult ackResult) { + this.receiptHandleMessage = receiptHandleMessage; + this.ackResult = ackResult; + } + + public BatchAckResult(ReceiptHandleMessage receiptHandleMessage, + ProxyException proxyException) { + this.receiptHandleMessage = receiptHandleMessage; + this.proxyException = proxyException; + } + + public ReceiptHandleMessage getReceiptHandleMessage() { + return receiptHandleMessage; + } + + public AckResult getAckResult() { + return ackResult; + } + + public ProxyException getProxyException() { + return proxyException; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ClientProcessor.java b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ClientProcessor.java new file mode 100644 index 00000000000..eeb9bf87e67 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ClientProcessor.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.processor; + +import io.netty.channel.Channel; +import java.util.Set; +import org.apache.rocketmq.broker.client.ClientChannelInfo; +import org.apache.rocketmq.broker.client.ConsumerGroupInfo; +import org.apache.rocketmq.broker.client.ConsumerIdsChangeListener; +import org.apache.rocketmq.broker.client.ProducerChangeListener; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.service.ServiceManager; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; + +public class ClientProcessor extends AbstractProcessor { + + public ClientProcessor(MessagingProcessor messagingProcessor, + ServiceManager serviceManager) { + super(messagingProcessor, serviceManager); + } + + public void registerProducer( + ProxyContext ctx, + String producerGroup, + ClientChannelInfo clientChannelInfo + ) { + this.serviceManager.getProducerManager().registerProducer(producerGroup, clientChannelInfo); + } + + public void unRegisterProducer( + ProxyContext ctx, + String producerGroup, + ClientChannelInfo clientChannelInfo + ) { + this.serviceManager.getProducerManager().unregisterProducer(producerGroup, clientChannelInfo); + } + + public Channel findProducerChannel( + ProxyContext ctx, + String producerGroup, + String clientId + ) { + return this.serviceManager.getProducerManager().findChannel(clientId); + } + + public void registerProducerChangeListener(ProducerChangeListener listener) { + this.serviceManager.getProducerManager().appendProducerChangeListener(listener); + } + + public void registerConsumer( + ProxyContext ctx, + String consumerGroup, + ClientChannelInfo clientChannelInfo, + ConsumeType consumeType, + MessageModel messageModel, + ConsumeFromWhere consumeFromWhere, + Set subList, + boolean updateSubscription + ) { + this.serviceManager.getConsumerManager().registerConsumer( + consumerGroup, + clientChannelInfo, + consumeType, + messageModel, + consumeFromWhere, + subList, + false, + updateSubscription); + } + + public ClientChannelInfo findConsumerChannel( + ProxyContext ctx, + String consumerGroup, + Channel channel + ) { + return this.serviceManager.getConsumerManager().findChannel(consumerGroup, channel); + } + + public void unRegisterConsumer( + ProxyContext ctx, + String consumerGroup, + ClientChannelInfo clientChannelInfo + ) { + this.serviceManager.getConsumerManager().unregisterConsumer(consumerGroup, clientChannelInfo, false); + } + + public void doChannelCloseEvent(String remoteAddr, Channel channel) { + this.serviceManager.getConsumerManager().doChannelCloseEvent(remoteAddr, channel); + this.serviceManager.getProducerManager().doChannelCloseEvent(remoteAddr, channel); + } + + public void registerConsumerIdsChangeListener(ConsumerIdsChangeListener listener) { + this.serviceManager.getConsumerManager().appendConsumerIdsChangeListener(listener); + } + + public ConsumerGroupInfo getConsumerGroupInfo(ProxyContext ctx, String consumerGroup) { + return this.serviceManager.getConsumerManager().getConsumerGroupInfo(consumerGroup); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ConsumerProcessor.java b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ConsumerProcessor.java new file mode 100644 index 00000000000..3ff34237014 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ConsumerProcessor.java @@ -0,0 +1,530 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.processor; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.ExecutorService; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.consumer.AckResult; +import org.apache.rocketmq.client.consumer.PopResult; +import org.apache.rocketmq.client.consumer.PopStatus; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.consumer.ReceiptHandle; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageClientExt; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.common.ProxyException; +import org.apache.rocketmq.proxy.common.ProxyExceptionCode; +import org.apache.rocketmq.proxy.common.utils.FutureUtils; +import org.apache.rocketmq.proxy.common.utils.ProxyUtils; +import org.apache.rocketmq.proxy.service.ServiceManager; +import org.apache.rocketmq.proxy.service.message.ReceiptHandleMessage; +import org.apache.rocketmq.proxy.service.route.AddressableMessageQueue; +import org.apache.rocketmq.remoting.protocol.body.LockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.body.UnlockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.header.AckMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ChangeInvisibleTimeRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMaxOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMinOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PopMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumerOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateConsumerOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; + +public class ConsumerProcessor extends AbstractProcessor { + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + + private final ExecutorService executor; + + public ConsumerProcessor(MessagingProcessor messagingProcessor, ServiceManager serviceManager, + ExecutorService executor) { + super(messagingProcessor, serviceManager); + this.executor = executor; + } + + public CompletableFuture popMessage( + ProxyContext ctx, + QueueSelector queueSelector, + String consumerGroup, + String topic, + int maxMsgNums, + long invisibleTime, + long pollTime, + int initMode, + SubscriptionData subscriptionData, + boolean fifo, + PopMessageResultFilter popMessageResultFilter, + String attemptId, + long timeoutMillis + ) { + CompletableFuture future = new CompletableFuture<>(); + try { + AddressableMessageQueue messageQueue = queueSelector.select(ctx, this.serviceManager.getTopicRouteService().getCurrentMessageQueueView(ctx, topic)); + if (messageQueue == null) { + throw new ProxyException(ProxyExceptionCode.FORBIDDEN, "no readable queue"); + } + return popMessage(ctx, messageQueue, consumerGroup, topic, maxMsgNums, invisibleTime, pollTime, initMode, + subscriptionData, fifo, popMessageResultFilter, attemptId, timeoutMillis); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + public CompletableFuture popMessage( + ProxyContext ctx, + AddressableMessageQueue messageQueue, + String consumerGroup, + String topic, + int maxMsgNums, + long invisibleTime, + long pollTime, + int initMode, + SubscriptionData subscriptionData, + boolean fifo, + PopMessageResultFilter popMessageResultFilter, + String attemptId, + long timeoutMillis + ) { + CompletableFuture future = new CompletableFuture<>(); + try { + if (maxMsgNums > ProxyUtils.MAX_MSG_NUMS_FOR_POP_REQUEST) { + log.warn("change maxNums from {} to {} for pop request, with info: topic:{}, group:{}", + maxMsgNums, ProxyUtils.MAX_MSG_NUMS_FOR_POP_REQUEST, topic, consumerGroup); + maxMsgNums = ProxyUtils.MAX_MSG_NUMS_FOR_POP_REQUEST; + } + + PopMessageRequestHeader requestHeader = new PopMessageRequestHeader(); + requestHeader.setConsumerGroup(consumerGroup); + requestHeader.setTopic(topic); + requestHeader.setQueueId(messageQueue.getQueueId()); + requestHeader.setMaxMsgNums(maxMsgNums); + requestHeader.setInvisibleTime(invisibleTime); + requestHeader.setPollTime(pollTime); + requestHeader.setInitMode(initMode); + requestHeader.setExpType(subscriptionData.getExpressionType()); + requestHeader.setExp(subscriptionData.getSubString()); + requestHeader.setOrder(fifo); + requestHeader.setAttemptId(attemptId); + requestHeader.setBornTime(System.currentTimeMillis()); + + future = this.serviceManager.getMessageService().popMessage( + ctx, + messageQueue, + requestHeader, + timeoutMillis) + .thenApplyAsync(popResult -> { + if (PopStatus.FOUND.equals(popResult.getPopStatus()) && + popResult.getMsgFoundList() != null && + !popResult.getMsgFoundList().isEmpty() && + popMessageResultFilter != null) { + + List messageExtList = new ArrayList<>(); + for (MessageExt messageExt : popResult.getMsgFoundList()) { + try { + fillUniqIDIfNeed(messageExt); + String handleString = createHandle(messageExt.getProperty(MessageConst.PROPERTY_POP_CK), messageExt.getCommitLogOffset()); + if (handleString == null) { + log.error("[BUG] pop message from broker but handle is empty. requestHeader:{}, msg:{}", requestHeader, messageExt); + messageExtList.add(messageExt); + continue; + } + MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_POP_CK, handleString); + + PopMessageResultFilter.FilterResult filterResult = + popMessageResultFilter.filterMessage(ctx, consumerGroup, subscriptionData, messageExt); + switch (filterResult) { + case NO_MATCH: + this.messagingProcessor.ackMessage( + ctx, + ReceiptHandle.decode(handleString), + messageExt.getMsgId(), + consumerGroup, + topic, + MessagingProcessor.DEFAULT_TIMEOUT_MILLS); + break; + case TO_DLQ: + this.messagingProcessor.forwardMessageToDeadLetterQueue( + ctx, + ReceiptHandle.decode(handleString), + messageExt.getMsgId(), + consumerGroup, + topic, + MessagingProcessor.DEFAULT_TIMEOUT_MILLS); + break; + case MATCH: + default: + messageExtList.add(messageExt); + break; + } + } catch (Throwable t) { + log.error("process filterMessage failed. requestHeader:{}, msg:{}", requestHeader, messageExt, t); + messageExtList.add(messageExt); + } + } + popResult.setMsgFoundList(messageExtList); + } + return popResult; + }, this.executor); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return FutureUtils.addExecutor(future, this.executor); + } + + private void fillUniqIDIfNeed(MessageExt messageExt) { + if (StringUtils.isBlank(MessageClientIDSetter.getUniqID(messageExt))) { + if (messageExt instanceof MessageClientExt) { + MessageClientExt clientExt = (MessageClientExt) messageExt; + MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, clientExt.getOffsetMsgId()); + } + } + } + + public CompletableFuture ackMessage( + ProxyContext ctx, + ReceiptHandle handle, + String messageId, + String consumerGroup, + String topic, + long timeoutMillis + ) { + CompletableFuture future = new CompletableFuture<>(); + try { + this.validateReceiptHandle(handle); + + AckMessageRequestHeader ackMessageRequestHeader = new AckMessageRequestHeader(); + ackMessageRequestHeader.setConsumerGroup(consumerGroup); + ackMessageRequestHeader.setTopic(handle.getRealTopic(topic, consumerGroup)); + ackMessageRequestHeader.setQueueId(handle.getQueueId()); + ackMessageRequestHeader.setExtraInfo(handle.getReceiptHandle()); + ackMessageRequestHeader.setOffset(handle.getOffset()); + + future = this.serviceManager.getMessageService().ackMessage( + ctx, + handle, + messageId, + ackMessageRequestHeader, + timeoutMillis); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return FutureUtils.addExecutor(future, this.executor); + } + + public CompletableFuture> batchAckMessage( + ProxyContext ctx, + List handleMessageList, + String consumerGroup, + String topic, + long timeoutMillis + ) { + CompletableFuture> future = new CompletableFuture<>(); + try { + List batchAckResultList = new ArrayList<>(handleMessageList.size()); + Map> brokerHandleListMap = new HashMap<>(); + + for (ReceiptHandleMessage handleMessage : handleMessageList) { + if (handleMessage.getReceiptHandle().isExpired()) { + batchAckResultList.add(new BatchAckResult(handleMessage, EXPIRED_HANDLE_PROXY_EXCEPTION)); + continue; + } + List brokerHandleList = brokerHandleListMap.computeIfAbsent(handleMessage.getReceiptHandle().getBrokerName(), key -> new ArrayList<>()); + brokerHandleList.add(handleMessage); + } + + if (brokerHandleListMap.isEmpty()) { + return FutureUtils.addExecutor(CompletableFuture.completedFuture(batchAckResultList), this.executor); + } + Set>> brokerHandleListMapEntrySet = brokerHandleListMap.entrySet(); + CompletableFuture>[] futures = new CompletableFuture[brokerHandleListMapEntrySet.size()]; + int futureIndex = 0; + for (Map.Entry> entry : brokerHandleListMapEntrySet) { + futures[futureIndex++] = processBrokerHandle(ctx, consumerGroup, topic, entry.getValue(), timeoutMillis); + } + CompletableFuture.allOf(futures).whenComplete((val, throwable) -> { + if (throwable != null) { + future.completeExceptionally(throwable); + } + for (CompletableFuture> resultFuture : futures) { + batchAckResultList.addAll(resultFuture.join()); + } + future.complete(batchAckResultList); + }); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return FutureUtils.addExecutor(future, this.executor); + } + + protected CompletableFuture> processBrokerHandle(ProxyContext ctx, String consumerGroup, String topic, List handleMessageList, long timeoutMillis) { + return this.serviceManager.getMessageService().batchAckMessage(ctx, handleMessageList, consumerGroup, topic, timeoutMillis) + .thenApply(result -> { + List results = new ArrayList<>(); + for (ReceiptHandleMessage handleMessage : handleMessageList) { + results.add(new BatchAckResult(handleMessage, result)); + } + return results; + }) + .exceptionally(throwable -> { + List results = new ArrayList<>(); + for (ReceiptHandleMessage handleMessage : handleMessageList) { + results.add(new BatchAckResult(handleMessage, new ProxyException(ProxyExceptionCode.INTERNAL_SERVER_ERROR, throwable.getMessage(), throwable))); + } + return results; + }); + } + + public CompletableFuture changeInvisibleTime(ProxyContext ctx, ReceiptHandle handle, + String messageId, String groupName, String topicName, long invisibleTime, long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + try { + this.validateReceiptHandle(handle); + + ChangeInvisibleTimeRequestHeader changeInvisibleTimeRequestHeader = new ChangeInvisibleTimeRequestHeader(); + changeInvisibleTimeRequestHeader.setConsumerGroup(groupName); + changeInvisibleTimeRequestHeader.setTopic(handle.getRealTopic(topicName, groupName)); + changeInvisibleTimeRequestHeader.setQueueId(handle.getQueueId()); + changeInvisibleTimeRequestHeader.setExtraInfo(handle.getReceiptHandle()); + changeInvisibleTimeRequestHeader.setOffset(handle.getOffset()); + changeInvisibleTimeRequestHeader.setInvisibleTime(invisibleTime); + long commitLogOffset = handle.getCommitLogOffset(); + + future = this.serviceManager.getMessageService().changeInvisibleTime( + ctx, + handle, + messageId, + changeInvisibleTimeRequestHeader, + timeoutMillis) + .thenApplyAsync(ackResult -> { + if (StringUtils.isNotBlank(ackResult.getExtraInfo())) { + AckResult result = new AckResult(); + result.setStatus(ackResult.getStatus()); + result.setPopTime(result.getPopTime()); + result.setExtraInfo(createHandle(ackResult.getExtraInfo(), commitLogOffset)); + return result; + } else { + return ackResult; + } + }, this.executor); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return FutureUtils.addExecutor(future, this.executor); + } + + protected String createHandle(String handleString, long commitLogOffset) { + if (handleString == null) { + return null; + } + return handleString + MessageConst.KEY_SEPARATOR + commitLogOffset; + } + + public CompletableFuture pullMessage(ProxyContext ctx, MessageQueue messageQueue, String consumerGroup, + long queueOffset, int maxMsgNums, int sysFlag, long commitOffset, + long suspendTimeoutMillis, SubscriptionData subscriptionData, long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + try { + AddressableMessageQueue addressableMessageQueue = serviceManager.getTopicRouteService() + .buildAddressableMessageQueue(ctx, messageQueue); + PullMessageRequestHeader requestHeader = new PullMessageRequestHeader(); + requestHeader.setConsumerGroup(consumerGroup); + requestHeader.setTopic(addressableMessageQueue.getTopic()); + requestHeader.setQueueId(addressableMessageQueue.getQueueId()); + requestHeader.setQueueOffset(queueOffset); + requestHeader.setMaxMsgNums(maxMsgNums); + requestHeader.setSysFlag(sysFlag); + requestHeader.setCommitOffset(commitOffset); + requestHeader.setSuspendTimeoutMillis(suspendTimeoutMillis); + requestHeader.setSubscription(subscriptionData.getSubString()); + requestHeader.setExpressionType(subscriptionData.getExpressionType()); + future = serviceManager.getMessageService().pullMessage(ctx, addressableMessageQueue, requestHeader, timeoutMillis); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return FutureUtils.addExecutor(future, this.executor); + } + + public CompletableFuture updateConsumerOffset(ProxyContext ctx, MessageQueue messageQueue, + String consumerGroup, long commitOffset, long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + try { + AddressableMessageQueue addressableMessageQueue = serviceManager.getTopicRouteService() + .buildAddressableMessageQueue(ctx, messageQueue); + UpdateConsumerOffsetRequestHeader requestHeader = new UpdateConsumerOffsetRequestHeader(); + requestHeader.setConsumerGroup(consumerGroup); + requestHeader.setTopic(addressableMessageQueue.getTopic()); + requestHeader.setQueueId(addressableMessageQueue.getQueueId()); + requestHeader.setCommitOffset(commitOffset); + future = serviceManager.getMessageService().updateConsumerOffset(ctx, addressableMessageQueue, requestHeader, timeoutMillis); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return FutureUtils.addExecutor(future, this.executor); + } + + public CompletableFuture queryConsumerOffset(ProxyContext ctx, MessageQueue messageQueue, + String consumerGroup, long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + try { + AddressableMessageQueue addressableMessageQueue = serviceManager.getTopicRouteService() + .buildAddressableMessageQueue(ctx, messageQueue); + QueryConsumerOffsetRequestHeader requestHeader = new QueryConsumerOffsetRequestHeader(); + requestHeader.setConsumerGroup(consumerGroup); + requestHeader.setTopic(addressableMessageQueue.getTopic()); + requestHeader.setQueueId(addressableMessageQueue.getQueueId()); + future = serviceManager.getMessageService().queryConsumerOffset(ctx, addressableMessageQueue, requestHeader, timeoutMillis); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return FutureUtils.addExecutor(future, this.executor); + } + + public CompletableFuture> lockBatchMQ(ProxyContext ctx, Set mqSet, + String consumerGroup, String clientId, long timeoutMillis) { + CompletableFuture> future = new CompletableFuture<>(); + try { + Set successSet = new CopyOnWriteArraySet<>(); + Set addressableMessageQueueSet = buildAddressableSet(ctx, mqSet); + Map> messageQueueSetMap = buildAddressableMapByBrokerName(addressableMessageQueueSet); + List> futureList = new ArrayList<>(); + messageQueueSetMap.forEach((k, v) -> { + LockBatchRequestBody requestBody = new LockBatchRequestBody(); + requestBody.setConsumerGroup(consumerGroup); + requestBody.setClientId(clientId); + requestBody.setMqSet(v.stream().map(AddressableMessageQueue::getMessageQueue).collect(Collectors.toSet())); + CompletableFuture future0 = serviceManager.getMessageService() + .lockBatchMQ(ctx, v.get(0), requestBody, timeoutMillis) + .thenAccept(successSet::addAll); + futureList.add(FutureUtils.addExecutor(future0, this.executor)); + }); + CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).whenComplete((v, t) -> { + if (t != null) { + log.error("LockBatchMQ failed, group={}", consumerGroup, t); + } + future.complete(successSet); + }); + } catch (Throwable t) { + log.error("LockBatchMQ exception, group={}", consumerGroup, t); + future.completeExceptionally(t); + } + return FutureUtils.addExecutor(future, this.executor); + } + + public CompletableFuture unlockBatchMQ(ProxyContext ctx, Set mqSet, + String consumerGroup, String clientId, long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + try { + Set addressableMessageQueueSet = buildAddressableSet(ctx, mqSet); + Map> messageQueueSetMap = buildAddressableMapByBrokerName(addressableMessageQueueSet); + List> futureList = new ArrayList<>(); + messageQueueSetMap.forEach((k, v) -> { + UnlockBatchRequestBody requestBody = new UnlockBatchRequestBody(); + requestBody.setConsumerGroup(consumerGroup); + requestBody.setClientId(clientId); + requestBody.setMqSet(v.stream().map(AddressableMessageQueue::getMessageQueue).collect(Collectors.toSet())); + CompletableFuture future0 = serviceManager.getMessageService().unlockBatchMQ(ctx, v.get(0), requestBody, timeoutMillis); + futureList.add(FutureUtils.addExecutor(future0, this.executor)); + }); + CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).whenComplete((v, t) -> { + if (t != null) { + log.error("UnlockBatchMQ failed, group={}", consumerGroup, t); + } + future.complete(null); + }); + } catch (Throwable t) { + log.error("UnlockBatchMQ exception, group={}", consumerGroup, t); + future.completeExceptionally(t); + } + return FutureUtils.addExecutor(future, this.executor); + } + + public CompletableFuture getMaxOffset(ProxyContext ctx, MessageQueue messageQueue, long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + try { + AddressableMessageQueue addressableMessageQueue = serviceManager.getTopicRouteService() + .buildAddressableMessageQueue(ctx, messageQueue); + GetMaxOffsetRequestHeader requestHeader = new GetMaxOffsetRequestHeader(); + requestHeader.setTopic(addressableMessageQueue.getTopic()); + requestHeader.setQueueId(addressableMessageQueue.getQueueId()); + future = serviceManager.getMessageService().getMaxOffset(ctx, addressableMessageQueue, requestHeader, timeoutMillis); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return FutureUtils.addExecutor(future, this.executor); + } + + public CompletableFuture getMinOffset(ProxyContext ctx, MessageQueue messageQueue, long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + try { + AddressableMessageQueue addressableMessageQueue = serviceManager.getTopicRouteService() + .buildAddressableMessageQueue(ctx, messageQueue); + GetMinOffsetRequestHeader requestHeader = new GetMinOffsetRequestHeader(); + requestHeader.setTopic(addressableMessageQueue.getTopic()); + requestHeader.setQueueId(addressableMessageQueue.getQueueId()); + future = serviceManager.getMessageService().getMinOffset(ctx, addressableMessageQueue, requestHeader, timeoutMillis); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return FutureUtils.addExecutor(future, this.executor); + } + + protected Set buildAddressableSet(ProxyContext ctx, Set mqSet) { + Set addressableMessageQueueSet = new HashSet<>(mqSet.size()); + for (MessageQueue mq:mqSet) { + try { + addressableMessageQueueSet.add(serviceManager.getTopicRouteService().buildAddressableMessageQueue(ctx, mq)) ; + } catch (Exception e) { + log.error("build addressable message queue fail, messageQueue = {}", mq, e); + } + } + return addressableMessageQueueSet; + } + + protected HashMap> buildAddressableMapByBrokerName( + final Set mqSet) { + HashMap> result = new HashMap<>(); + if (mqSet == null) { + return result; + } + for (AddressableMessageQueue mq : mqSet) { + if (mq == null) { + continue; + } + List mqs = result.computeIfAbsent(mq.getBrokerName(), k -> new ArrayList<>()); + mqs.add(mq); + } + return result; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/DefaultMessagingProcessor.java b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/DefaultMessagingProcessor.java new file mode 100644 index 00000000000..ba150051bc0 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/DefaultMessagingProcessor.java @@ -0,0 +1,342 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.processor; + +import io.netty.channel.Channel; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.acl.common.AclUtils; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.client.ClientChannelInfo; +import org.apache.rocketmq.broker.client.ConsumerGroupInfo; +import org.apache.rocketmq.broker.client.ConsumerIdsChangeListener; +import org.apache.rocketmq.broker.client.ProducerChangeListener; +import org.apache.rocketmq.client.consumer.AckResult; +import org.apache.rocketmq.client.consumer.PopResult; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.consumer.ReceiptHandle; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.thread.ThreadPoolMonitor; +import org.apache.rocketmq.common.utils.AbstractStartAndShutdown; +import org.apache.rocketmq.proxy.common.Address; +import org.apache.rocketmq.proxy.common.MessageReceiptHandle; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.ProxyConfig; +import org.apache.rocketmq.proxy.service.ServiceManager; +import org.apache.rocketmq.proxy.service.ServiceManagerFactory; +import org.apache.rocketmq.proxy.service.message.ReceiptHandleMessage; +import org.apache.rocketmq.proxy.service.metadata.MetadataService; +import org.apache.rocketmq.proxy.service.relay.ProxyRelayService; +import org.apache.rocketmq.proxy.service.route.ProxyTopicRouteData; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; + +public class DefaultMessagingProcessor extends AbstractStartAndShutdown implements MessagingProcessor { + + protected ServiceManager serviceManager; + protected ProducerProcessor producerProcessor; + protected ConsumerProcessor consumerProcessor; + protected TransactionProcessor transactionProcessor; + protected ClientProcessor clientProcessor; + protected RequestBrokerProcessor requestBrokerProcessor; + protected ReceiptHandleProcessor receiptHandleProcessor; + + protected ThreadPoolExecutor producerProcessorExecutor; + protected ThreadPoolExecutor consumerProcessorExecutor; + protected static final String ROCKETMQ_HOME = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, + System.getenv(MixAll.ROCKETMQ_HOME_ENV)); + + protected DefaultMessagingProcessor(ServiceManager serviceManager) { + ProxyConfig proxyConfig = ConfigurationManager.getProxyConfig(); + this.producerProcessorExecutor = ThreadPoolMonitor.createAndMonitor( + proxyConfig.getProducerProcessorThreadPoolNums(), + proxyConfig.getProducerProcessorThreadPoolNums(), + 1, + TimeUnit.MINUTES, + "ProducerProcessorExecutor", + proxyConfig.getProducerProcessorThreadPoolQueueCapacity() + ); + this.consumerProcessorExecutor = ThreadPoolMonitor.createAndMonitor( + proxyConfig.getConsumerProcessorThreadPoolNums(), + proxyConfig.getConsumerProcessorThreadPoolNums(), + 1, + TimeUnit.MINUTES, + "ConsumerProcessorExecutor", + proxyConfig.getConsumerProcessorThreadPoolQueueCapacity() + ); + + this.serviceManager = serviceManager; + this.producerProcessor = new ProducerProcessor(this, serviceManager, this.producerProcessorExecutor); + this.consumerProcessor = new ConsumerProcessor(this, serviceManager, this.consumerProcessorExecutor); + this.transactionProcessor = new TransactionProcessor(this, serviceManager); + this.clientProcessor = new ClientProcessor(this, serviceManager); + this.requestBrokerProcessor = new RequestBrokerProcessor(this, serviceManager); + this.receiptHandleProcessor = new ReceiptHandleProcessor(this, serviceManager); + + this.init(); + } + + public static DefaultMessagingProcessor createForLocalMode(BrokerController brokerController) { + return createForLocalMode(brokerController, null); + } + + public static DefaultMessagingProcessor createForLocalMode(BrokerController brokerController, RPCHook rpcHook) { + return new DefaultMessagingProcessor(ServiceManagerFactory.createForLocalMode(brokerController, rpcHook)); + } + + public static DefaultMessagingProcessor createForClusterMode() { + RPCHook rpcHook = null; + if (ConfigurationManager.getProxyConfig().isEnableAclRpcHookForClusterMode()) { + rpcHook = AclUtils.getAclRPCHook(ROCKETMQ_HOME + MixAll.ACL_CONF_TOOLS_FILE); + } + return createForClusterMode(rpcHook); + } + + public static DefaultMessagingProcessor createForClusterMode(RPCHook rpcHook) { + return new DefaultMessagingProcessor(ServiceManagerFactory.createForClusterMode(rpcHook)); + } + + protected void init() { + this.appendStartAndShutdown(this.serviceManager); + this.appendShutdown(this.producerProcessorExecutor::shutdown); + this.appendShutdown(this.consumerProcessorExecutor::shutdown); + } + + @Override + public SubscriptionGroupConfig getSubscriptionGroupConfig(ProxyContext ctx, String consumerGroupName) { + return this.serviceManager.getMetadataService().getSubscriptionGroupConfig(ctx, consumerGroupName); + } + + @Override + public ProxyTopicRouteData getTopicRouteDataForProxy(ProxyContext ctx, List
    requestHostAndPortList, + String topicName) throws Exception { + return this.serviceManager.getTopicRouteService().getTopicRouteForProxy(ctx, requestHostAndPortList, topicName); + } + + @Override + public CompletableFuture> sendMessage(ProxyContext ctx, QueueSelector queueSelector, + String producerGroup, int sysFlag, List msg, long timeoutMillis) { + return this.producerProcessor.sendMessage(ctx, queueSelector, producerGroup, sysFlag, msg, timeoutMillis); + } + + @Override + public CompletableFuture forwardMessageToDeadLetterQueue(ProxyContext ctx, ReceiptHandle handle, + String messageId, String groupName, String topicName, long timeoutMillis) { + return this.producerProcessor.forwardMessageToDeadLetterQueue(ctx, handle, messageId, groupName, topicName, timeoutMillis); + } + + @Override + public CompletableFuture endTransaction(ProxyContext ctx, String transactionId, String messageId, String producerGroup, + TransactionStatus transactionStatus, boolean fromTransactionCheck, + long timeoutMillis) { + return this.transactionProcessor.endTransaction(ctx, transactionId, messageId, producerGroup, transactionStatus, fromTransactionCheck, timeoutMillis); + } + + @Override + public CompletableFuture popMessage( + ProxyContext ctx, + QueueSelector queueSelector, + String consumerGroup, + String topic, + int maxMsgNums, + long invisibleTime, + long pollTime, + int initMode, + SubscriptionData subscriptionData, + boolean fifo, + PopMessageResultFilter popMessageResultFilter, + String attemptId, + long timeoutMillis + ) { + return this.consumerProcessor.popMessage(ctx, queueSelector, consumerGroup, topic, maxMsgNums, + invisibleTime, pollTime, initMode, subscriptionData, fifo, popMessageResultFilter, attemptId, timeoutMillis); + } + + @Override + public CompletableFuture ackMessage(ProxyContext ctx, ReceiptHandle handle, String messageId, + String consumerGroup, String topic, long timeoutMillis) { + return this.consumerProcessor.ackMessage(ctx, handle, messageId, consumerGroup, topic, timeoutMillis); + } + + @Override + public CompletableFuture> batchAckMessage(ProxyContext ctx, + List handleMessageList, String consumerGroup, String topic, long timeoutMillis) { + return this.consumerProcessor.batchAckMessage(ctx, handleMessageList, consumerGroup, topic, timeoutMillis); + } + + @Override + public CompletableFuture changeInvisibleTime(ProxyContext ctx, ReceiptHandle handle, String messageId, + String groupName, String topicName, long invisibleTime, long timeoutMillis) { + return this.consumerProcessor.changeInvisibleTime(ctx, handle, messageId, groupName, topicName, invisibleTime, timeoutMillis); + } + + @Override + public CompletableFuture pullMessage(ProxyContext ctx, MessageQueue messageQueue, String consumerGroup, + long queueOffset, int maxMsgNums, int sysFlag, long commitOffset, long suspendTimeoutMillis, + SubscriptionData subscriptionData, long timeoutMillis) { + return this.consumerProcessor.pullMessage(ctx, messageQueue, consumerGroup, queueOffset, maxMsgNums, + sysFlag, commitOffset, suspendTimeoutMillis, subscriptionData, timeoutMillis); + } + + @Override + public CompletableFuture updateConsumerOffset(ProxyContext ctx, MessageQueue messageQueue, + String consumerGroup, long commitOffset, long timeoutMillis) { + return this.consumerProcessor.updateConsumerOffset(ctx, messageQueue, consumerGroup, commitOffset, timeoutMillis); + } + + @Override + public CompletableFuture queryConsumerOffset(ProxyContext ctx, MessageQueue messageQueue, + String consumerGroup, long timeoutMillis) { + return this.consumerProcessor.queryConsumerOffset(ctx, messageQueue, consumerGroup, timeoutMillis); + } + + @Override + public CompletableFuture> lockBatchMQ(ProxyContext ctx, Set mqSet, + String consumerGroup, String clientId, long timeoutMillis) { + return this.consumerProcessor.lockBatchMQ(ctx, mqSet, consumerGroup, clientId, timeoutMillis); + } + + @Override + public CompletableFuture unlockBatchMQ(ProxyContext ctx, Set mqSet, + String consumerGroup, + String clientId, long timeoutMillis) { + return this.consumerProcessor.unlockBatchMQ(ctx, mqSet, consumerGroup, clientId, timeoutMillis); + } + + @Override + public CompletableFuture getMaxOffset(ProxyContext ctx, MessageQueue messageQueue, long timeoutMillis) { + return this.consumerProcessor.getMaxOffset(ctx, messageQueue, timeoutMillis); + } + + @Override + public CompletableFuture getMinOffset(ProxyContext ctx, MessageQueue messageQueue, long timeoutMillis) { + return this.consumerProcessor.getMinOffset(ctx, messageQueue, timeoutMillis); + } + + @Override + public CompletableFuture request(ProxyContext ctx, String brokerName, RemotingCommand request, + long timeoutMillis) { + int originalRequestOpaque = request.getOpaque(); + request.setOpaque(RemotingCommand.createNewRequestId()); + return this.requestBrokerProcessor.request(ctx, brokerName, request, timeoutMillis).thenApply(r -> { + request.setOpaque(originalRequestOpaque); + return r; + }); + } + + @Override + public CompletableFuture requestOneway(ProxyContext ctx, String brokerName, RemotingCommand request, + long timeoutMillis) { + int originalRequestOpaque = request.getOpaque(); + request.setOpaque(RemotingCommand.createNewRequestId()); + return this.requestBrokerProcessor.requestOneway(ctx, brokerName, request, timeoutMillis).thenApply(r -> { + request.setOpaque(originalRequestOpaque); + return r; + }); + } + + @Override + public void registerProducer(ProxyContext ctx, String producerGroup, ClientChannelInfo clientChannelInfo) { + this.clientProcessor.registerProducer(ctx, producerGroup, clientChannelInfo); + } + + @Override + public void unRegisterProducer(ProxyContext ctx, String producerGroup, ClientChannelInfo clientChannelInfo) { + this.clientProcessor.unRegisterProducer(ctx, producerGroup, clientChannelInfo); + } + + @Override + public Channel findProducerChannel(ProxyContext ctx, String producerGroup, String clientId) { + return this.clientProcessor.findProducerChannel(ctx, producerGroup, clientId); + } + + @Override + public void registerProducerListener(ProducerChangeListener producerChangeListener) { + this.clientProcessor.registerProducerChangeListener(producerChangeListener); + } + + @Override + public void registerConsumer(ProxyContext ctx, String consumerGroup, ClientChannelInfo clientChannelInfo, + ConsumeType consumeType, MessageModel messageModel, ConsumeFromWhere consumeFromWhere, + Set subList, boolean updateSubscription) { + this.clientProcessor.registerConsumer(ctx, consumerGroup, clientChannelInfo, consumeType, messageModel, consumeFromWhere, subList, updateSubscription); + } + + @Override + public ClientChannelInfo findConsumerChannel(ProxyContext ctx, String consumerGroup, Channel channel) { + return this.clientProcessor.findConsumerChannel(ctx, consumerGroup, channel); + } + + @Override + public void unRegisterConsumer(ProxyContext ctx, String consumerGroup, ClientChannelInfo clientChannelInfo) { + this.clientProcessor.unRegisterConsumer(ctx, consumerGroup, clientChannelInfo); + } + + @Override + public void registerConsumerListener(ConsumerIdsChangeListener consumerIdsChangeListener) { + this.clientProcessor.registerConsumerIdsChangeListener(consumerIdsChangeListener); + } + + @Override + public void doChannelCloseEvent(String remoteAddr, Channel channel) { + this.clientProcessor.doChannelCloseEvent(remoteAddr, channel); + } + + @Override + public ConsumerGroupInfo getConsumerGroupInfo(ProxyContext ctx, String consumerGroup) { + return this.clientProcessor.getConsumerGroupInfo(ctx, consumerGroup); + } + + @Override + public void addTransactionSubscription(ProxyContext ctx, String producerGroup, String topic) { + this.transactionProcessor.addTransactionSubscription(ctx, producerGroup, topic); + } + + @Override + public ProxyRelayService getProxyRelayService() { + return this.serviceManager.getProxyRelayService(); + } + + @Override + public MetadataService getMetadataService() { + return this.serviceManager.getMetadataService(); + } + + @Override + public void addReceiptHandle(ProxyContext ctx, Channel channel, String group, String msgID, + MessageReceiptHandle messageReceiptHandle) { + receiptHandleProcessor.addReceiptHandle(ctx, channel, group, msgID, messageReceiptHandle); + } + + @Override + public MessageReceiptHandle removeReceiptHandle(ProxyContext ctx, Channel channel, String group, String msgID, + String receiptHandle) { + return receiptHandleProcessor.removeReceiptHandle(ctx, channel, group, msgID, receiptHandle); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/MessagingProcessor.java b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/MessagingProcessor.java new file mode 100644 index 00000000000..2ae7418ba72 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/MessagingProcessor.java @@ -0,0 +1,325 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.processor; + +import io.netty.channel.Channel; +import java.time.Duration; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.broker.client.ClientChannelInfo; +import org.apache.rocketmq.broker.client.ConsumerGroupInfo; +import org.apache.rocketmq.broker.client.ConsumerIdsChangeListener; +import org.apache.rocketmq.broker.client.ProducerChangeListener; +import org.apache.rocketmq.client.consumer.AckResult; +import org.apache.rocketmq.client.consumer.PopResult; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.consumer.ReceiptHandle; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.proxy.common.Address; +import org.apache.rocketmq.proxy.common.MessageReceiptHandle; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.common.utils.StartAndShutdown; +import org.apache.rocketmq.proxy.service.message.ReceiptHandleMessage; +import org.apache.rocketmq.proxy.service.metadata.MetadataService; +import org.apache.rocketmq.proxy.service.relay.ProxyRelayService; +import org.apache.rocketmq.proxy.service.route.ProxyTopicRouteData; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; + +public interface MessagingProcessor extends StartAndShutdown { + + long DEFAULT_TIMEOUT_MILLS = Duration.ofSeconds(2).toMillis(); + + SubscriptionGroupConfig getSubscriptionGroupConfig( + ProxyContext ctx, + String consumerGroupName + ); + + ProxyTopicRouteData getTopicRouteDataForProxy( + ProxyContext ctx, + List
    requestHostAndPortList, + String topicName + ) throws Exception; + + default CompletableFuture> sendMessage( + ProxyContext ctx, + QueueSelector queueSelector, + String producerGroup, + int sysFlag, + List msg + ) { + return sendMessage(ctx, queueSelector, producerGroup, sysFlag, msg, DEFAULT_TIMEOUT_MILLS); + } + + CompletableFuture> sendMessage( + ProxyContext ctx, + QueueSelector queueSelector, + String producerGroup, + int sysFlag, + List msg, + long timeoutMillis + ); + + default CompletableFuture forwardMessageToDeadLetterQueue( + ProxyContext ctx, + ReceiptHandle handle, + String messageId, + String groupName, + String topicName + ) { + return forwardMessageToDeadLetterQueue(ctx, handle, messageId, groupName, topicName, DEFAULT_TIMEOUT_MILLS); + } + + CompletableFuture forwardMessageToDeadLetterQueue( + ProxyContext ctx, + ReceiptHandle handle, + String messageId, + String groupName, + String topicName, + long timeoutMillis + ); + + default CompletableFuture endTransaction( + ProxyContext ctx, + String transactionId, + String messageId, + String producerGroup, + TransactionStatus transactionStatus, + boolean fromTransactionCheck + ) { + return endTransaction(ctx, transactionId, messageId, producerGroup, transactionStatus, fromTransactionCheck, DEFAULT_TIMEOUT_MILLS); + } + + CompletableFuture endTransaction( + ProxyContext ctx, + String transactionId, + String messageId, + String producerGroup, + TransactionStatus transactionStatus, + boolean fromTransactionCheck, + long timeoutMillis + ); + + CompletableFuture popMessage( + ProxyContext ctx, + QueueSelector queueSelector, + String consumerGroup, + String topic, + int maxMsgNums, + long invisibleTime, + long pollTime, + int initMode, + SubscriptionData subscriptionData, + boolean fifo, + PopMessageResultFilter popMessageResultFilter, + String attemptId, + long timeoutMillis + ); + + default CompletableFuture ackMessage( + ProxyContext ctx, + ReceiptHandle handle, + String messageId, + String consumerGroup, + String topic + ) { + return ackMessage(ctx, handle, messageId, consumerGroup, topic, DEFAULT_TIMEOUT_MILLS); + } + + CompletableFuture ackMessage( + ProxyContext ctx, + ReceiptHandle handle, + String messageId, + String consumerGroup, + String topic, + long timeoutMillis + ); + + default CompletableFuture> batchAckMessage( + ProxyContext ctx, + List handleMessageList, + String consumerGroup, + String topic + ) { + return batchAckMessage(ctx, handleMessageList, consumerGroup, topic, DEFAULT_TIMEOUT_MILLS); + } + + CompletableFuture> batchAckMessage( + ProxyContext ctx, + List handleMessageList, + String consumerGroup, + String topic, + long timeoutMillis + ); + + default CompletableFuture changeInvisibleTime( + ProxyContext ctx, + ReceiptHandle handle, + String messageId, + String groupName, + String topicName, + long invisibleTime + ) { + return changeInvisibleTime(ctx, handle, messageId, groupName, topicName, invisibleTime, DEFAULT_TIMEOUT_MILLS); + } + + CompletableFuture changeInvisibleTime( + ProxyContext ctx, + ReceiptHandle handle, + String messageId, + String groupName, + String topicName, + long invisibleTime, + long timeoutMillis + ); + + CompletableFuture pullMessage( + ProxyContext ctx, + MessageQueue messageQueue, + String consumerGroup, + long queueOffset, + int maxMsgNums, + int sysFlag, + long commitOffset, + long suspendTimeoutMillis, + SubscriptionData subscriptionData, + long timeoutMillis + ); + + CompletableFuture updateConsumerOffset( + ProxyContext ctx, + MessageQueue messageQueue, + String consumerGroup, + long commitOffset, + long timeoutMillis + ); + + CompletableFuture queryConsumerOffset( + ProxyContext ctx, + MessageQueue messageQueue, + String consumerGroup, + long timeoutMillis + ); + + CompletableFuture> lockBatchMQ( + ProxyContext ctx, + Set mqSet, + String consumerGroup, + String clientId, + long timeoutMillis + ); + + CompletableFuture unlockBatchMQ( + ProxyContext ctx, + Set mqSet, + String consumerGroup, + String clientId, + long timeoutMillis + ); + + CompletableFuture getMaxOffset( + ProxyContext ctx, + MessageQueue messageQueue, + long timeoutMillis + ); + + CompletableFuture getMinOffset( + ProxyContext ctx, + MessageQueue messageQueue, + long timeoutMillis + ); + + CompletableFuture request(ProxyContext ctx, String brokerName, RemotingCommand request, + long timeoutMillis); + + CompletableFuture requestOneway(ProxyContext ctx, String brokerName, RemotingCommand request, + long timeoutMillis); + + void registerProducer( + ProxyContext ctx, + String producerGroup, + ClientChannelInfo clientChannelInfo + ); + + void unRegisterProducer( + ProxyContext ctx, + String producerGroup, + ClientChannelInfo clientChannelInfo + ); + + Channel findProducerChannel( + ProxyContext ctx, + String producerGroup, + String clientId + ); + + void registerProducerListener( + ProducerChangeListener producerChangeListener + ); + + void registerConsumer( + ProxyContext ctx, + String consumerGroup, + ClientChannelInfo clientChannelInfo, + ConsumeType consumeType, + MessageModel messageModel, + ConsumeFromWhere consumeFromWhere, + Set subList, + boolean updateSubscription + ); + + ClientChannelInfo findConsumerChannel( + ProxyContext ctx, + String consumerGroup, + Channel channel + ); + + void unRegisterConsumer( + ProxyContext ctx, + String consumerGroup, + ClientChannelInfo clientChannelInfo + ); + + void registerConsumerListener( + ConsumerIdsChangeListener consumerIdsChangeListener + ); + + void doChannelCloseEvent(String remoteAddr, Channel channel); + + ConsumerGroupInfo getConsumerGroupInfo(ProxyContext ctx, String consumerGroup); + + void addTransactionSubscription( + ProxyContext ctx, + String producerGroup, + String topic + ); + + ProxyRelayService getProxyRelayService(); + + MetadataService getMetadataService(); + + void addReceiptHandle(ProxyContext ctx, Channel channel, String group, String msgID, MessageReceiptHandle messageReceiptHandle); + + MessageReceiptHandle removeReceiptHandle(ProxyContext ctx, Channel channel, String group, String msgID, String receiptHandle); +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/PopMessageResultFilter.java b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/PopMessageResultFilter.java new file mode 100644 index 00000000000..09c1a0bf1a6 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/PopMessageResultFilter.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.processor; + +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; + +public interface PopMessageResultFilter { + + enum FilterResult { + TO_DLQ, + NO_MATCH, + MATCH + } + + FilterResult filterMessage(ProxyContext ctx, String consumerGroup, SubscriptionData subscriptionData, + MessageExt messageExt); +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ProducerProcessor.java b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ProducerProcessor.java new file mode 100644 index 00000000000..a80f6df0b07 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ProducerProcessor.java @@ -0,0 +1,234 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.processor; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; + +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.attribute.TopicMessageType; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.consumer.ReceiptHandle; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageId; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.common.ProxyException; +import org.apache.rocketmq.proxy.common.ProxyExceptionCode; +import org.apache.rocketmq.proxy.common.utils.FutureUtils; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.processor.validator.DefaultTopicMessageTypeValidator; +import org.apache.rocketmq.proxy.processor.validator.TopicMessageTypeValidator; +import org.apache.rocketmq.proxy.service.ServiceManager; +import org.apache.rocketmq.proxy.service.route.AddressableMessageQueue; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.ConsumerSendMsgBackRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; + +public class ProducerProcessor extends AbstractProcessor { + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + private final ExecutorService executor; + private final TopicMessageTypeValidator topicMessageTypeValidator; + + public ProducerProcessor(MessagingProcessor messagingProcessor, + ServiceManager serviceManager, ExecutorService executor) { + super(messagingProcessor, serviceManager); + this.executor = executor; + this.topicMessageTypeValidator = new DefaultTopicMessageTypeValidator(); + } + + public CompletableFuture> sendMessage(ProxyContext ctx, QueueSelector queueSelector, + String producerGroup, int sysFlag, List messageList, long timeoutMillis) { + CompletableFuture> future = new CompletableFuture<>(); + long beginTimestampFirst = System.currentTimeMillis(); + AddressableMessageQueue messageQueue = null; + try { + Message message = messageList.get(0); + String topic = message.getTopic(); + if (ConfigurationManager.getProxyConfig().isEnableTopicMessageTypeCheck()) { + if (topicMessageTypeValidator != null) { + // Do not check retry or dlq topic + if (!NamespaceUtil.isRetryTopic(topic) && !NamespaceUtil.isDLQTopic(topic)) { + TopicMessageType topicMessageType = serviceManager.getMetadataService().getTopicMessageType(ctx, topic); + TopicMessageType messageType = TopicMessageType.parseFromMessageProperty(message.getProperties()); + topicMessageTypeValidator.validate(topicMessageType, messageType); + } + } + } + messageQueue = queueSelector.select(ctx, + this.serviceManager.getTopicRouteService().getCurrentMessageQueueView(ctx, topic)); + if (messageQueue == null) { + throw new ProxyException(ProxyExceptionCode.FORBIDDEN, "no writable queue"); + } + + for (Message msg : messageList) { + MessageClientIDSetter.setUniqID(msg); + } + SendMessageRequestHeader requestHeader = buildSendMessageRequestHeader(messageList, producerGroup, sysFlag, messageQueue.getQueueId()); + + AddressableMessageQueue finalMessageQueue = messageQueue; + future = this.serviceManager.getMessageService().sendMessage( + ctx, + messageQueue, + messageList, + requestHeader, + timeoutMillis) + .thenApplyAsync(sendResultList -> { + for (SendResult sendResult : sendResultList) { + int tranType = MessageSysFlag.getTransactionValue(requestHeader.getSysFlag()); + if (SendStatus.SEND_OK.equals(sendResult.getSendStatus()) && + tranType == MessageSysFlag.TRANSACTION_PREPARED_TYPE && + StringUtils.isNotBlank(sendResult.getTransactionId())) { + fillTransactionData(ctx, producerGroup, finalMessageQueue, sendResult, messageList); + } + } + return sendResultList; + }, this.executor) + .whenComplete((result, exception) -> { + long endTimestamp = System.currentTimeMillis(); + if (exception != null) { + this.serviceManager.getTopicRouteService().updateFaultItem(finalMessageQueue.getBrokerName(), endTimestamp - beginTimestampFirst, true, false); + } else { + this.serviceManager.getTopicRouteService().updateFaultItem(finalMessageQueue.getBrokerName(),endTimestamp - beginTimestampFirst, false, true); + } + }); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return FutureUtils.addExecutor(future, this.executor); + } + + protected void fillTransactionData(ProxyContext ctx, String producerGroup, AddressableMessageQueue messageQueue, SendResult sendResult, List messageList) { + try { + MessageId id; + if (sendResult.getOffsetMsgId() != null) { + id = MessageDecoder.decodeMessageId(sendResult.getOffsetMsgId()); + } else { + id = MessageDecoder.decodeMessageId(sendResult.getMsgId()); + } + this.serviceManager.getTransactionService().addTransactionDataByBrokerName( + ctx, + messageQueue.getBrokerName(), + producerGroup, + sendResult.getQueueOffset(), + id.getOffset(), + sendResult.getTransactionId(), + messageList.get(0) + ); + } catch (Throwable t) { + log.warn("fillTransactionData failed. messageQueue: {}, sendResult: {}", messageQueue, sendResult, t); + } + } + + protected SendMessageRequestHeader buildSendMessageRequestHeader(List messageList, + String producerGroup, int sysFlag, int queueId) { + SendMessageRequestHeader requestHeader = new SendMessageRequestHeader(); + + Message message = messageList.get(0); + + requestHeader.setProducerGroup(producerGroup); + requestHeader.setTopic(message.getTopic()); + requestHeader.setDefaultTopic(TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC); + requestHeader.setDefaultTopicQueueNums(4); + requestHeader.setQueueId(queueId); + requestHeader.setSysFlag(sysFlag); + /* + In RocketMQ 4.0, org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader.bornTimestamp + represents the timestamp when the message was born. In RocketMQ 5.0, the bornTimestamp of the message + is a message attribute, that is, the timestamp when message was constructed, and there is no + bornTimestamp in the SendMessageRequest of RocketMQ 5.0. + Note: When using grpc sendMessage to send multiple messages, the bornTimestamp in the requestHeader + is set to the bornTimestamp of the first message, which may not be accurate. When a bornTimestamp is + required, the bornTimestamp of the message property should be used. + * */ + try { + requestHeader.setBornTimestamp(Long.parseLong(message.getProperty(MessageConst.PROPERTY_BORN_TIMESTAMP))); + } catch (Exception e) { + log.warn("parse born time error, with value:{}", message.getProperty(MessageConst.PROPERTY_BORN_TIMESTAMP)); + requestHeader.setBornTimestamp(System.currentTimeMillis()); + } + requestHeader.setFlag(message.getFlag()); + requestHeader.setProperties(MessageDecoder.messageProperties2String(message.getProperties())); + requestHeader.setReconsumeTimes(0); + if (messageList.size() > 1) { + requestHeader.setBatch(true); + } + if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + String reconsumeTimes = MessageAccessor.getReconsumeTime(message); + if (reconsumeTimes != null) { + requestHeader.setReconsumeTimes(Integer.valueOf(reconsumeTimes)); + MessageAccessor.clearProperty(message, MessageConst.PROPERTY_RECONSUME_TIME); + } + + String maxReconsumeTimes = MessageAccessor.getMaxReconsumeTimes(message); + if (maxReconsumeTimes != null) { + requestHeader.setMaxReconsumeTimes(Integer.valueOf(maxReconsumeTimes)); + MessageAccessor.clearProperty(message, MessageConst.PROPERTY_MAX_RECONSUME_TIMES); + } + } + + return requestHeader; + } + + public CompletableFuture forwardMessageToDeadLetterQueue(ProxyContext ctx, ReceiptHandle handle, + String messageId, String groupName, String topicName, long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + try { + if (handle.getCommitLogOffset() < 0) { + throw new ProxyException(ProxyExceptionCode.INVALID_RECEIPT_HANDLE, "commit log offset is empty"); + } + + ConsumerSendMsgBackRequestHeader consumerSendMsgBackRequestHeader = new ConsumerSendMsgBackRequestHeader(); + consumerSendMsgBackRequestHeader.setOffset(handle.getCommitLogOffset()); + consumerSendMsgBackRequestHeader.setGroup(groupName); + consumerSendMsgBackRequestHeader.setDelayLevel(-1); + consumerSendMsgBackRequestHeader.setOriginMsgId(messageId); + consumerSendMsgBackRequestHeader.setOriginTopic(handle.getRealTopic(topicName, groupName)); + consumerSendMsgBackRequestHeader.setMaxReconsumeTimes(0); + + future = this.serviceManager.getMessageService().sendMessageBack( + ctx, + handle, + messageId, + consumerSendMsgBackRequestHeader, + timeoutMillis + ).whenCompleteAsync((remotingCommand, t) -> { + if (t == null && remotingCommand.getCode() == ResponseCode.SUCCESS) { + this.messagingProcessor.ackMessage(ctx, handle, messageId, + groupName, topicName, timeoutMillis); + } + }, this.executor); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return FutureUtils.addExecutor(future, this.executor); + } + +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/QueueSelector.java b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/QueueSelector.java new file mode 100644 index 00000000000..5fe0d1c38a2 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/QueueSelector.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.processor; + +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.service.route.AddressableMessageQueue; +import org.apache.rocketmq.proxy.service.route.MessageQueueView; + +public interface QueueSelector { + + AddressableMessageQueue select(ProxyContext ctx, MessageQueueView messageQueueView); +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ReceiptHandleProcessor.java b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ReceiptHandleProcessor.java new file mode 100644 index 00000000000..5e1be932183 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/ReceiptHandleProcessor.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.processor; + +import io.netty.channel.Channel; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.consumer.ReceiptHandle; +import org.apache.rocketmq.common.state.StateEventListener; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.common.MessageReceiptHandle; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.common.RenewEvent; +import org.apache.rocketmq.proxy.service.ServiceManager; +import org.apache.rocketmq.proxy.service.receipt.DefaultReceiptHandleManager; + +public class ReceiptHandleProcessor extends AbstractProcessor { + protected final static Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + protected DefaultReceiptHandleManager receiptHandleManager; + + public ReceiptHandleProcessor(MessagingProcessor messagingProcessor, ServiceManager serviceManager) { + super(messagingProcessor, serviceManager); + StateEventListener eventListener = event -> { + ProxyContext context = createContext(event.getEventType().name()) + .setChannel(event.getKey().getChannel()); + MessageReceiptHandle messageReceiptHandle = event.getMessageReceiptHandle(); + ReceiptHandle handle = ReceiptHandle.decode(messageReceiptHandle.getReceiptHandleStr()); + messagingProcessor.changeInvisibleTime(context, handle, messageReceiptHandle.getMessageId(), + messageReceiptHandle.getGroup(), messageReceiptHandle.getTopic(), event.getRenewTime()) + .whenComplete((v, t) -> { + if (t != null) { + event.getFuture().completeExceptionally(t); + return; + } + event.getFuture().complete(v); + }); + }; + this.receiptHandleManager = new DefaultReceiptHandleManager(serviceManager.getMetadataService(), serviceManager.getConsumerManager(), eventListener); + } + + protected ProxyContext createContext(String actionName) { + return ProxyContext.createForInner(this.getClass().getSimpleName() + actionName); + } + + public void addReceiptHandle(ProxyContext ctx, Channel channel, String group, String msgID, MessageReceiptHandle messageReceiptHandle) { + receiptHandleManager.addReceiptHandle(ctx, channel, group, msgID, messageReceiptHandle); + } + + public MessageReceiptHandle removeReceiptHandle(ProxyContext ctx, Channel channel, String group, String msgID, String receiptHandle) { + return receiptHandleManager.removeReceiptHandle(ctx, channel, group, msgID, receiptHandle); + } + +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/RequestBrokerProcessor.java b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/RequestBrokerProcessor.java new file mode 100644 index 00000000000..9f3187cde71 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/RequestBrokerProcessor.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.processor; + +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.service.ServiceManager; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public class RequestBrokerProcessor extends AbstractProcessor { + + public RequestBrokerProcessor(MessagingProcessor messagingProcessor, + ServiceManager serviceManager) { + super(messagingProcessor, serviceManager); + } + + CompletableFuture request(ProxyContext ctx, String brokerName, RemotingCommand request, long timeoutMillis) { + return serviceManager.getMessageService().request(ctx, brokerName, request, timeoutMillis); + } + + CompletableFuture requestOneway(ProxyContext ctx, String brokerName, RemotingCommand request, long timeoutMillis) { + return serviceManager.getMessageService().requestOneway(ctx, brokerName, request, timeoutMillis); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/TransactionProcessor.java b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/TransactionProcessor.java new file mode 100644 index 00000000000..c0ba255f544 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/TransactionProcessor.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.processor; + +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.common.ProxyException; +import org.apache.rocketmq.proxy.common.ProxyExceptionCode; +import org.apache.rocketmq.proxy.service.ServiceManager; +import org.apache.rocketmq.proxy.service.transaction.EndTransactionRequestData; + +public class TransactionProcessor extends AbstractProcessor { + + public TransactionProcessor(MessagingProcessor messagingProcessor, + ServiceManager serviceManager) { + super(messagingProcessor, serviceManager); + } + + public CompletableFuture endTransaction(ProxyContext ctx, String transactionId, String messageId, String producerGroup, + TransactionStatus transactionStatus, boolean fromTransactionCheck, long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + try { + EndTransactionRequestData headerData = serviceManager.getTransactionService().genEndTransactionRequestHeader( + ctx, + producerGroup, + buildCommitOrRollback(transactionStatus), + fromTransactionCheck, + messageId, + transactionId + ); + if (headerData == null) { + future.completeExceptionally(new ProxyException(ProxyExceptionCode.TRANSACTION_DATA_NOT_FOUND, "cannot found transaction data")); + return future; + } + return this.serviceManager.getMessageService().endTransactionOneway( + ctx, + headerData.getBrokerName(), + headerData.getRequestHeader(), + timeoutMillis + ); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + protected int buildCommitOrRollback(TransactionStatus transactionStatus) { + switch (transactionStatus) { + case COMMIT: + return MessageSysFlag.TRANSACTION_COMMIT_TYPE; + case ROLLBACK: + return MessageSysFlag.TRANSACTION_ROLLBACK_TYPE; + default: + return MessageSysFlag.TRANSACTION_NOT_TYPE; + } + } + + public void addTransactionSubscription(ProxyContext ctx, String producerGroup, String topic) { + this.serviceManager.getTransactionService().addTransactionSubscription(ctx, producerGroup, topic); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/TransactionStatus.java b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/TransactionStatus.java new file mode 100644 index 00000000000..e456a6061aa --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/TransactionStatus.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.processor; + +public enum TransactionStatus { + UNKNOWN, + COMMIT, + ROLLBACK +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/channel/ChannelExtendAttributeGetter.java b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/channel/ChannelExtendAttributeGetter.java new file mode 100644 index 00000000000..3538a9496c4 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/channel/ChannelExtendAttributeGetter.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.processor.channel; + +public interface ChannelExtendAttributeGetter { + + String getChannelExtendAttribute(); +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/channel/ChannelProtocolType.java b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/channel/ChannelProtocolType.java new file mode 100644 index 00000000000..d2eeb83536d --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/channel/ChannelProtocolType.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.processor.channel; + +public enum ChannelProtocolType { + UNKNOWN("unknown"), + GRPC_V2("grpc_v2"), + GRPC_V1("grpc_v1"), + REMOTING("remoting"); + + private final String name; + + ChannelProtocolType(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/channel/RemoteChannel.java b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/channel/RemoteChannel.java new file mode 100644 index 00000000000..fb9666afcc3 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/channel/RemoteChannel.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.processor.channel; + +import com.google.common.base.MoreObjects; +import io.netty.channel.Channel; +import io.netty.channel.ChannelId; +import org.apache.rocketmq.proxy.service.channel.SimpleChannel; + +public class RemoteChannel extends SimpleChannel implements ChannelExtendAttributeGetter { + protected final ChannelProtocolType type; + protected final String remoteProxyIp; + protected volatile String extendAttribute; + + public RemoteChannel(String remoteProxyIp, String remoteAddress, String localAddress, ChannelProtocolType type, String extendAttribute) { + super(null, + new RemoteChannelId(remoteProxyIp, remoteAddress, localAddress, type), + remoteAddress, localAddress); + this.type = type; + this.remoteProxyIp = remoteProxyIp; + this.extendAttribute = extendAttribute; + } + + public static class RemoteChannelId implements ChannelId { + + private final String id; + + public RemoteChannelId(String remoteProxyIp, String remoteAddress, String localAddress, ChannelProtocolType type) { + this.id = remoteProxyIp + "@" + remoteAddress + "@" + localAddress + "@" + type; + } + + @Override + public String asShortText() { + return this.id; + } + + @Override + public String asLongText() { + return this.id; + } + + @Override + public int compareTo(ChannelId o) { + return this.id.compareTo(o.asLongText()); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("id", id) + .toString(); + } + } + + @Override + public boolean isWritable() { + return false; + } + + public ChannelProtocolType getType() { + return type; + } + + public String encode() { + return RemoteChannelSerializer.toJson(this); + } + + public static RemoteChannel decode(String data) { + return RemoteChannelSerializer.decodeFromJson(data); + } + + public static RemoteChannel create(Channel channel) { + if (channel instanceof RemoteChannelConverter) { + return ((RemoteChannelConverter) channel).toRemoteChannel(); + } + return null; + } + + public String getRemoteProxyIp() { + return remoteProxyIp; + } + + public void setExtendAttribute(String extendAttribute) { + this.extendAttribute = extendAttribute; + } + + @Override + public String getChannelExtendAttribute() { + return this.extendAttribute; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("channelId", id()) + .add("type", type) + .add("remoteProxyIp", remoteProxyIp) + .add("extendAttribute", extendAttribute) + .toString(); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/channel/RemoteChannelConverter.java b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/channel/RemoteChannelConverter.java new file mode 100644 index 00000000000..9f886e85d23 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/channel/RemoteChannelConverter.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.processor.channel; + +public interface RemoteChannelConverter { + + RemoteChannel toRemoteChannel(); +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/channel/RemoteChannelSerializer.java b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/channel/RemoteChannelSerializer.java new file mode 100644 index 00000000000..a22401a5f32 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/channel/RemoteChannelSerializer.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.processor.channel; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import java.util.HashMap; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class RemoteChannelSerializer { + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + private static final String REMOTE_PROXY_IP_KEY = "remoteProxyIp"; + private static final String REMOTE_ADDRESS_KEY = "remoteAddress"; + private static final String LOCAL_ADDRESS_KEY = "localAddress"; + private static final String TYPE_KEY = "type"; + private static final String EXTEND_ATTRIBUTE_KEY = "extendAttribute"; + + public static String toJson(RemoteChannel remoteChannel) { + Map data = new HashMap<>(); + data.put(REMOTE_PROXY_IP_KEY, remoteChannel.getRemoteProxyIp()); + data.put(REMOTE_ADDRESS_KEY, remoteChannel.getRemoteAddress()); + data.put(LOCAL_ADDRESS_KEY, remoteChannel.getLocalAddress()); + data.put(TYPE_KEY, remoteChannel.getType()); + data.put(EXTEND_ATTRIBUTE_KEY, remoteChannel.getChannelExtendAttribute()); + return JSON.toJSONString(data); + } + + public static RemoteChannel decodeFromJson(String jsonData) { + if (StringUtils.isBlank(jsonData)) { + return null; + } + try { + JSONObject jsonObject = JSON.parseObject(jsonData); + return new RemoteChannel( + jsonObject.getString(REMOTE_PROXY_IP_KEY), + jsonObject.getString(REMOTE_ADDRESS_KEY), + jsonObject.getString(LOCAL_ADDRESS_KEY), + jsonObject.getObject(TYPE_KEY, ChannelProtocolType.class), + jsonObject.getObject(EXTEND_ATTRIBUTE_KEY, String.class) + ); + } catch (Throwable t) { + log.error("decode remote channel data failed. data:{}", jsonData, t); + } + return null; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/validator/DefaultTopicMessageTypeValidator.java b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/validator/DefaultTopicMessageTypeValidator.java new file mode 100644 index 00000000000..83588f110c0 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/validator/DefaultTopicMessageTypeValidator.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.processor.validator; + +import org.apache.rocketmq.common.attribute.TopicMessageType; +import org.apache.rocketmq.proxy.common.ProxyException; +import org.apache.rocketmq.proxy.common.ProxyExceptionCode; + +public class DefaultTopicMessageTypeValidator implements TopicMessageTypeValidator { + + public void validate(TopicMessageType expectedType, TopicMessageType actualType) { + if (actualType.equals(TopicMessageType.UNSPECIFIED) + || !actualType.equals(expectedType) && !expectedType.equals(TopicMessageType.MIXED)) { + String errorInfo = String.format("TopicMessageType validate failed, the expected type is %s, but actual type is %s", expectedType, actualType); + throw new ProxyException(ProxyExceptionCode.MESSAGE_PROPERTY_CONFLICT_WITH_TYPE, errorInfo); + } + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/processor/validator/TopicMessageTypeValidator.java b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/validator/TopicMessageTypeValidator.java new file mode 100644 index 00000000000..32758da5024 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/processor/validator/TopicMessageTypeValidator.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.processor.validator; + +import org.apache.rocketmq.common.attribute.TopicMessageType; + +public interface TopicMessageTypeValidator { + /** + * Will throw {@link org.apache.rocketmq.proxy.common.ProxyException} if validate failed. + * + * @param expectedType Target topic + * @param actualType Message's type + */ + void validate(TopicMessageType expectedType, TopicMessageType actualType); +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/ClientHousekeepingService.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/ClientHousekeepingService.java new file mode 100644 index 00000000000..74eb6f2db2f --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/ClientHousekeepingService.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting; + +import io.netty.channel.Channel; +import org.apache.rocketmq.proxy.remoting.activity.ClientManagerActivity; +import org.apache.rocketmq.remoting.ChannelEventListener; + +public class ClientHousekeepingService implements ChannelEventListener { + + private final ClientManagerActivity clientManagerActivity; + + public ClientHousekeepingService(ClientManagerActivity clientManagerActivity) { + this.clientManagerActivity = clientManagerActivity; + } + + @Override + public void onChannelConnect(String remoteAddr, Channel channel) { + + } + + @Override + public void onChannelClose(String remoteAddr, Channel channel) { + this.clientManagerActivity.doChannelCloseEvent(remoteAddr, channel); + } + + @Override + public void onChannelException(String remoteAddr, Channel channel) { + this.clientManagerActivity.doChannelCloseEvent(remoteAddr, channel); + } + + @Override + public void onChannelIdle(String remoteAddr, Channel channel) { + this.clientManagerActivity.doChannelCloseEvent(remoteAddr, channel); + } + + @Override + public void onChannelActive(String remoteAddr, Channel channel) { + + } +} + diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/MultiProtocolRemotingServer.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/MultiProtocolRemotingServer.java new file mode 100644 index 00000000000..d7c2820b27f --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/MultiProtocolRemotingServer.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting; + +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.timeout.IdleStateHandler; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.common.ProxyException; +import org.apache.rocketmq.proxy.common.ProxyExceptionCode; +import org.apache.rocketmq.proxy.remoting.protocol.ProtocolNegotiationHandler; +import org.apache.rocketmq.proxy.remoting.protocol.http2proxy.Http2ProtocolProxyHandler; +import org.apache.rocketmq.proxy.remoting.protocol.remoting.RemotingProtocolHandler; +import org.apache.rocketmq.remoting.ChannelEventListener; +import org.apache.rocketmq.remoting.common.TlsMode; +import org.apache.rocketmq.remoting.netty.NettyRemotingServer; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.netty.TlsSystemConfig; + +import java.io.IOException; +import java.security.cert.CertificateException; + +/** + * support remoting and http2 protocol at one port + */ +public class MultiProtocolRemotingServer extends NettyRemotingServer { + + private final static Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + private final NettyServerConfig nettyServerConfig; + + private final RemotingProtocolHandler remotingProtocolHandler; + protected Http2ProtocolProxyHandler http2ProtocolProxyHandler; + + public MultiProtocolRemotingServer(NettyServerConfig nettyServerConfig, ChannelEventListener channelEventListener) { + super(nettyServerConfig, channelEventListener); + this.nettyServerConfig = nettyServerConfig; + + this.remotingProtocolHandler = new RemotingProtocolHandler( + this::getEncoder, + this::getDistributionHandler, + this::getConnectionManageHandler, + this::getServerHandler); + this.http2ProtocolProxyHandler = new Http2ProtocolProxyHandler(); + } + + @Override + public void loadSslContext() { + TlsMode tlsMode = TlsSystemConfig.tlsMode; + log.info("Server is running in TLS {} mode", tlsMode.getName()); + + if (tlsMode != TlsMode.DISABLED) { + try { + sslContext = MultiProtocolTlsHelper.buildSslContext(); + log.info("SSLContext created for server"); + } catch (CertificateException | IOException e) { + throw new ProxyException(ProxyExceptionCode.INTERNAL_SERVER_ERROR, "Failed to create SSLContext for server", e); + } + } + } + + @Override + protected ChannelPipeline configChannel(SocketChannel ch) { + return ch.pipeline() + .addLast(this.getDefaultEventExecutorGroup(), HANDSHAKE_HANDLER_NAME, new HandshakeHandler()) + .addLast(this.getDefaultEventExecutorGroup(), + new IdleStateHandler(0, 0, nettyServerConfig.getServerChannelMaxIdleTimeSeconds()), + new ProtocolNegotiationHandler(this.remotingProtocolHandler) + .addProtocolHandler(this.http2ProtocolProxyHandler) + ); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/MultiProtocolTlsHelper.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/MultiProtocolTlsHelper.java new file mode 100644 index 00000000000..59342ca3cd2 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/MultiProtocolTlsHelper.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting; + +import io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.OpenSsl; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.cert.CertificateException; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.netty.TlsHelper; + +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.tlsServerAuthClient; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.tlsServerCertPath; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.tlsServerKeyPassword; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.tlsServerKeyPath; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.tlsServerNeedClientAuth; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.tlsServerTrustCertPath; +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.tlsTestModeEnable; + +public class MultiProtocolTlsHelper extends TlsHelper { + private final static Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + private static final DecryptionStrategy DECRYPTION_STRATEGY = (privateKeyEncryptPath, forClient) -> new FileInputStream(privateKeyEncryptPath); + + public static SslContext buildSslContext() throws IOException, CertificateException { + TlsHelper.buildSslContext(false); + SslProvider provider; + if (OpenSsl.isAvailable()) { + provider = SslProvider.OPENSSL; + log.info("Using OpenSSL provider"); + } else { + provider = SslProvider.JDK; + log.info("Using JDK SSL provider"); + } + + SslContextBuilder sslContextBuilder = null; + if (tlsTestModeEnable) { + SelfSignedCertificate selfSignedCertificate = new SelfSignedCertificate(); + sslContextBuilder = SslContextBuilder + .forServer(selfSignedCertificate.certificate(), selfSignedCertificate.privateKey()) + .sslProvider(SslProvider.OPENSSL) + .clientAuth(ClientAuth.OPTIONAL); + } else { + sslContextBuilder = SslContextBuilder.forServer( + !StringUtils.isBlank(tlsServerCertPath) ? Files.newInputStream(Paths.get(tlsServerCertPath)) : null, + !StringUtils.isBlank(tlsServerKeyPath) ? DECRYPTION_STRATEGY.decryptPrivateKey(tlsServerKeyPath, false) : null, + !StringUtils.isBlank(tlsServerKeyPassword) ? tlsServerKeyPassword : null) + .sslProvider(provider); + + if (!tlsServerAuthClient) { + sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE); + } else { + if (!StringUtils.isBlank(tlsServerTrustCertPath)) { + sslContextBuilder.trustManager(new File(tlsServerTrustCertPath)); + } + } + + sslContextBuilder.clientAuth(parseClientAuthMode(tlsServerNeedClientAuth)); + } + + sslContextBuilder.applicationProtocolConfig(new ApplicationProtocolConfig( + ApplicationProtocolConfig.Protocol.ALPN, + // NO_ADVERTISE is currently the only mode supported by both OpenSsl and JDK providers. + ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, + // ACCEPT is currently the only mode supported by both OpenSsl and JDK providers. + ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2)); + + return sslContextBuilder.build(); + } + + private static ClientAuth parseClientAuthMode(String authMode) { + if (null == authMode || authMode.trim().isEmpty()) { + return ClientAuth.NONE; + } + + for (ClientAuth clientAuth : ClientAuth.values()) { + if (clientAuth.name().equals(authMode.toUpperCase())) { + return clientAuth; + } + } + + return ClientAuth.NONE; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/RemotingProtocolServer.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/RemotingProtocolServer.java new file mode 100644 index 00000000000..3227d1e1c63 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/RemotingProtocolServer.java @@ -0,0 +1,373 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import io.netty.channel.Channel; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.acl.AccessValidator; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.future.FutureTaskExt; +import org.apache.rocketmq.common.thread.ThreadPoolMonitor; +import org.apache.rocketmq.common.thread.ThreadPoolStatusMonitor; +import org.apache.rocketmq.common.utils.StartAndShutdown; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.ProxyConfig; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.proxy.remoting.activity.AckMessageActivity; +import org.apache.rocketmq.proxy.remoting.activity.ChangeInvisibleTimeActivity; +import org.apache.rocketmq.proxy.remoting.activity.ClientManagerActivity; +import org.apache.rocketmq.proxy.remoting.activity.ConsumerManagerActivity; +import org.apache.rocketmq.proxy.remoting.activity.GetTopicRouteActivity; +import org.apache.rocketmq.proxy.remoting.activity.PopMessageActivity; +import org.apache.rocketmq.proxy.remoting.activity.PullMessageActivity; +import org.apache.rocketmq.proxy.remoting.activity.SendMessageActivity; +import org.apache.rocketmq.proxy.remoting.activity.TransactionActivity; +import org.apache.rocketmq.proxy.remoting.channel.RemotingChannelManager; +import org.apache.rocketmq.proxy.remoting.pipeline.AuthenticationPipeline; +import org.apache.rocketmq.proxy.remoting.pipeline.RequestPipeline; +import org.apache.rocketmq.remoting.ChannelEventListener; +import org.apache.rocketmq.remoting.InvokeCallback; +import org.apache.rocketmq.remoting.RemotingServer; +import org.apache.rocketmq.remoting.netty.NettyRemotingServer; +import org.apache.rocketmq.remoting.netty.NettyServerConfig; +import org.apache.rocketmq.remoting.netty.RequestTask; +import org.apache.rocketmq.remoting.netty.ResponseFuture; +import org.apache.rocketmq.remoting.netty.TlsSystemConfig; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; + +public class RemotingProtocolServer implements StartAndShutdown, RemotingProxyOutClient { + private final static Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + + protected final MessagingProcessor messagingProcessor; + protected final RemotingChannelManager remotingChannelManager; + protected final ChannelEventListener clientHousekeepingService; + protected final RemotingServer defaultRemotingServer; + protected final GetTopicRouteActivity getTopicRouteActivity; + protected final ClientManagerActivity clientManagerActivity; + protected final ConsumerManagerActivity consumerManagerActivity; + protected final SendMessageActivity sendMessageActivity; + protected final TransactionActivity transactionActivity; + protected final PullMessageActivity pullMessageActivity; + protected final PopMessageActivity popMessageActivity; + protected final AckMessageActivity ackMessageActivity; + protected final ChangeInvisibleTimeActivity changeInvisibleTimeActivity; + protected final ThreadPoolExecutor sendMessageExecutor; + protected final ThreadPoolExecutor pullMessageExecutor; + protected final ThreadPoolExecutor heartbeatExecutor; + protected final ThreadPoolExecutor updateOffsetExecutor; + protected final ThreadPoolExecutor topicRouteExecutor; + protected final ThreadPoolExecutor defaultExecutor; + protected final ScheduledExecutorService timerExecutor; + + public RemotingProtocolServer(MessagingProcessor messagingProcessor, List accessValidators) { + this.messagingProcessor = messagingProcessor; + this.remotingChannelManager = new RemotingChannelManager(this, messagingProcessor.getProxyRelayService()); + + RequestPipeline pipeline = createRequestPipeline(accessValidators); + this.getTopicRouteActivity = new GetTopicRouteActivity(pipeline, messagingProcessor); + this.clientManagerActivity = new ClientManagerActivity(pipeline, messagingProcessor, remotingChannelManager); + this.consumerManagerActivity = new ConsumerManagerActivity(pipeline, messagingProcessor); + this.sendMessageActivity = new SendMessageActivity(pipeline, messagingProcessor); + this.transactionActivity = new TransactionActivity(pipeline, messagingProcessor); + this.pullMessageActivity = new PullMessageActivity(pipeline, messagingProcessor); + this.popMessageActivity = new PopMessageActivity(pipeline, messagingProcessor); + this.ackMessageActivity = new AckMessageActivity(pipeline, messagingProcessor); + this.changeInvisibleTimeActivity = new ChangeInvisibleTimeActivity(pipeline, messagingProcessor); + + ProxyConfig config = ConfigurationManager.getProxyConfig(); + NettyServerConfig defaultServerConfig = new NettyServerConfig(); + defaultServerConfig.setListenPort(config.getRemotingListenPort()); + TlsSystemConfig.tlsTestModeEnable = config.isTlsTestModeEnable(); + System.setProperty(TlsSystemConfig.TLS_TEST_MODE_ENABLE, Boolean.toString(config.isTlsTestModeEnable())); + TlsSystemConfig.tlsServerCertPath = config.getTlsCertPath(); + System.setProperty(TlsSystemConfig.TLS_SERVER_CERTPATH, config.getTlsCertPath()); + TlsSystemConfig.tlsServerKeyPath = config.getTlsKeyPath(); + System.setProperty(TlsSystemConfig.TLS_SERVER_KEYPATH, config.getTlsKeyPath()); + + this.clientHousekeepingService = new ClientHousekeepingService(this.clientManagerActivity); + + if (config.isEnableRemotingLocalProxyGrpc()) { + this.defaultRemotingServer = new MultiProtocolRemotingServer(defaultServerConfig, this.clientHousekeepingService); + } else { + this.defaultRemotingServer = new NettyRemotingServer(defaultServerConfig, this.clientHousekeepingService); + } + this.registerRemotingServer(this.defaultRemotingServer); + + this.sendMessageExecutor = ThreadPoolMonitor.createAndMonitor( + config.getRemotingSendMessageThreadPoolNums(), + config.getRemotingSendMessageThreadPoolNums(), + 1000 * 60, + TimeUnit.MILLISECONDS, + "RemotingSendMessageThread", + config.getRemotingSendThreadPoolQueueCapacity(), + new ThreadPoolHeadSlowTimeMillsMonitor(config.getRemotingWaitTimeMillsInSendQueue()) + ); + + this.pullMessageExecutor = ThreadPoolMonitor.createAndMonitor( + config.getRemotingPullMessageThreadPoolNums(), + config.getRemotingPullMessageThreadPoolNums(), + 1000 * 60, + TimeUnit.MILLISECONDS, + "RemotingPullMessageThread", + config.getRemotingPullThreadPoolQueueCapacity(), + new ThreadPoolHeadSlowTimeMillsMonitor(config.getRemotingWaitTimeMillsInPullQueue()) + ); + + this.updateOffsetExecutor = ThreadPoolMonitor.createAndMonitor( + config.getRemotingUpdateOffsetThreadPoolNums(), + config.getRemotingUpdateOffsetThreadPoolNums(), + 1, + TimeUnit.MINUTES, + "RemotingUpdateOffsetThread", + config.getRemotingUpdateOffsetThreadPoolQueueCapacity(), + new ThreadPoolHeadSlowTimeMillsMonitor(config.getRemotingWaitTimeMillsInUpdateOffsetQueue()) + ); + + this.heartbeatExecutor = ThreadPoolMonitor.createAndMonitor( + config.getRemotingHeartbeatThreadPoolNums(), + config.getRemotingHeartbeatThreadPoolNums(), + 1000 * 60, + TimeUnit.MILLISECONDS, + "RemotingHeartbeatThread", + config.getRemotingHeartbeatThreadPoolQueueCapacity(), + new ThreadPoolHeadSlowTimeMillsMonitor(config.getRemotingWaitTimeMillsInHeartbeatQueue()) + ); + + this.topicRouteExecutor = ThreadPoolMonitor.createAndMonitor( + config.getRemotingTopicRouteThreadPoolNums(), + config.getRemotingTopicRouteThreadPoolNums(), + 1000 * 60, + TimeUnit.MILLISECONDS, + "RemotingTopicRouteThread", + config.getRemotingTopicRouteThreadPoolQueueCapacity(), + new ThreadPoolHeadSlowTimeMillsMonitor(config.getRemotingWaitTimeMillsInTopicRouteQueue()) + ); + + this.defaultExecutor = ThreadPoolMonitor.createAndMonitor( + config.getRemotingDefaultThreadPoolNums(), + config.getRemotingDefaultThreadPoolNums(), + 1000 * 60, + TimeUnit.MILLISECONDS, + "RemotingDefaultThread", + config.getRemotingDefaultThreadPoolQueueCapacity(), + new ThreadPoolHeadSlowTimeMillsMonitor(config.getRemotingWaitTimeMillsInDefaultQueue()) + ); + + this.timerExecutor = ThreadUtils.newSingleThreadScheduledExecutor( + new ThreadFactoryBuilder().setNameFormat("RemotingServerScheduler-%d").build() + ); + this.timerExecutor.scheduleAtFixedRate(this::cleanExpireRequest, 10, 10, TimeUnit.SECONDS); + } + + protected void registerRemotingServer(RemotingServer remotingServer) { + remotingServer.registerProcessor(RequestCode.SEND_MESSAGE, sendMessageActivity, this.sendMessageExecutor); + remotingServer.registerProcessor(RequestCode.SEND_MESSAGE_V2, sendMessageActivity, this.sendMessageExecutor); + remotingServer.registerProcessor(RequestCode.SEND_BATCH_MESSAGE, sendMessageActivity, this.sendMessageExecutor); + remotingServer.registerProcessor(RequestCode.CONSUMER_SEND_MSG_BACK, sendMessageActivity, sendMessageExecutor); + + remotingServer.registerProcessor(RequestCode.END_TRANSACTION, transactionActivity, sendMessageExecutor); + + remotingServer.registerProcessor(RequestCode.HEART_BEAT, clientManagerActivity, this.heartbeatExecutor); + remotingServer.registerProcessor(RequestCode.UNREGISTER_CLIENT, clientManagerActivity, this.defaultExecutor); + remotingServer.registerProcessor(RequestCode.CHECK_CLIENT_CONFIG, clientManagerActivity, this.defaultExecutor); + + remotingServer.registerProcessor(RequestCode.PULL_MESSAGE, pullMessageActivity, this.pullMessageExecutor); + remotingServer.registerProcessor(RequestCode.LITE_PULL_MESSAGE, pullMessageActivity, this.pullMessageExecutor); + remotingServer.registerProcessor(RequestCode.POP_MESSAGE, pullMessageActivity, this.pullMessageExecutor); + + remotingServer.registerProcessor(RequestCode.UPDATE_CONSUMER_OFFSET, consumerManagerActivity, this.updateOffsetExecutor); + remotingServer.registerProcessor(RequestCode.ACK_MESSAGE, consumerManagerActivity, this.updateOffsetExecutor); + remotingServer.registerProcessor(RequestCode.CHANGE_MESSAGE_INVISIBLETIME, consumerManagerActivity, this.updateOffsetExecutor); + remotingServer.registerProcessor(RequestCode.GET_CONSUMER_CONNECTION_LIST, consumerManagerActivity, this.updateOffsetExecutor); + + remotingServer.registerProcessor(RequestCode.GET_CONSUMER_LIST_BY_GROUP, consumerManagerActivity, this.defaultExecutor); + remotingServer.registerProcessor(RequestCode.GET_MAX_OFFSET, consumerManagerActivity, this.defaultExecutor); + remotingServer.registerProcessor(RequestCode.GET_MIN_OFFSET, consumerManagerActivity, this.defaultExecutor); + remotingServer.registerProcessor(RequestCode.QUERY_CONSUMER_OFFSET, consumerManagerActivity, this.defaultExecutor); + remotingServer.registerProcessor(RequestCode.SEARCH_OFFSET_BY_TIMESTAMP, consumerManagerActivity, this.defaultExecutor); + remotingServer.registerProcessor(RequestCode.LOCK_BATCH_MQ, consumerManagerActivity, this.defaultExecutor); + remotingServer.registerProcessor(RequestCode.UNLOCK_BATCH_MQ, consumerManagerActivity, this.defaultExecutor); + + remotingServer.registerProcessor(RequestCode.GET_ROUTEINFO_BY_TOPIC, getTopicRouteActivity, this.topicRouteExecutor); + } + + @Override + public void shutdown() throws Exception { + this.defaultRemotingServer.shutdown(); + this.remotingChannelManager.shutdown(); + this.sendMessageExecutor.shutdown(); + this.pullMessageExecutor.shutdown(); + this.heartbeatExecutor.shutdown(); + this.updateOffsetExecutor.shutdown(); + this.topicRouteExecutor.shutdown(); + this.defaultExecutor.shutdown(); + } + + @Override + public void start() throws Exception { + this.remotingChannelManager.start(); + this.defaultRemotingServer.start(); + } + + @Override + public CompletableFuture invokeToClient(Channel channel, RemotingCommand request, + long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + try { + this.defaultRemotingServer.invokeAsync(channel, request, timeoutMillis, new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { + + } + + @Override + public void operationSucceed(RemotingCommand response) { + future.complete(response); + } + + @Override + public void operationFail(Throwable throwable) { + future.completeExceptionally(throwable); + } + }); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + protected RequestPipeline createRequestPipeline(List accessValidators) { + RequestPipeline pipeline = (ctx, request, context) -> { + }; + // add pipeline + // the last pipe add will execute at the first + return pipeline.pipe(new AuthenticationPipeline(accessValidators)); + } + + protected class ThreadPoolHeadSlowTimeMillsMonitor implements ThreadPoolStatusMonitor { + + private final long maxWaitTimeMillsInQueue; + + public ThreadPoolHeadSlowTimeMillsMonitor(long maxWaitTimeMillsInQueue) { + this.maxWaitTimeMillsInQueue = maxWaitTimeMillsInQueue; + } + + @Override + public String describe() { + return "headSlow"; + } + + @Override + public double value(ThreadPoolExecutor executor) { + return headSlowTimeMills(executor.getQueue()); + } + + @Override + public boolean needPrintJstack(ThreadPoolExecutor executor, double value) { + return value > maxWaitTimeMillsInQueue; + } + } + + protected long headSlowTimeMills(BlockingQueue q) { + try { + long slowTimeMills = 0; + final Runnable peek = q.peek(); + if (peek != null) { + RequestTask rt = castRunnable(peek); + slowTimeMills = rt == null ? 0 : System.currentTimeMillis() - rt.getCreateTimestamp(); + } + + if (slowTimeMills < 0) { + slowTimeMills = 0; + } + + return slowTimeMills; + } catch (Exception e) { + log.error("error when headSlowTimeMills.", e); + } + return -1; + } + + protected void cleanExpireRequest() { + ProxyConfig config = ConfigurationManager.getProxyConfig(); + + cleanExpiredRequestInQueue(this.sendMessageExecutor, config.getRemotingWaitTimeMillsInSendQueue()); + cleanExpiredRequestInQueue(this.pullMessageExecutor, config.getRemotingWaitTimeMillsInPullQueue()); + cleanExpiredRequestInQueue(this.heartbeatExecutor, config.getRemotingWaitTimeMillsInHeartbeatQueue()); + cleanExpiredRequestInQueue(this.updateOffsetExecutor, config.getRemotingWaitTimeMillsInUpdateOffsetQueue()); + cleanExpiredRequestInQueue(this.topicRouteExecutor, config.getRemotingWaitTimeMillsInTopicRouteQueue()); + cleanExpiredRequestInQueue(this.defaultExecutor, config.getRemotingWaitTimeMillsInDefaultQueue()); + } + + protected void cleanExpiredRequestInQueue(ThreadPoolExecutor threadPoolExecutor, long maxWaitTimeMillsInQueue) { + while (true) { + try { + BlockingQueue blockingQueue = threadPoolExecutor.getQueue(); + if (!blockingQueue.isEmpty()) { + final Runnable runnable = blockingQueue.peek(); + if (null == runnable) { + break; + } + final RequestTask rt = castRunnable(runnable); + if (rt == null || rt.isStopRun()) { + break; + } + + final long behind = System.currentTimeMillis() - rt.getCreateTimestamp(); + if (behind >= maxWaitTimeMillsInQueue) { + if (blockingQueue.remove(runnable)) { + rt.setStopRun(true); + rt.returnResponse(ResponseCode.SYSTEM_BUSY, + String.format("[TIMEOUT_CLEAN_QUEUE]broker busy, start flow control for a while, period in queue: %sms, size of queue: %d", behind, blockingQueue.size())); + } + } else { + break; + } + } else { + break; + } + } catch (Throwable ignored) { + } + } + } + + private RequestTask castRunnable(final Runnable runnable) { + try { + if (runnable instanceof FutureTaskExt) { + FutureTaskExt futureTaskExt = (FutureTaskExt) runnable; + return (RequestTask) futureTaskExt.getRunnable(); + } + return null; + } catch (Throwable e) { + log.error("castRunnable exception. class:{}", runnable.getClass().getName(), e); + } + + return null; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/RemotingProxyOutClient.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/RemotingProxyOutClient.java new file mode 100644 index 00000000000..5a96c41c93c --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/RemotingProxyOutClient.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting; + +import io.netty.channel.Channel; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public interface RemotingProxyOutClient { + + CompletableFuture invokeToClient(Channel channel, RemotingCommand request, long timeoutMillis); +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/AbstractRemotingActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/AbstractRemotingActivity.java new file mode 100644 index 00000000000..ce4a633976f --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/AbstractRemotingActivity.java @@ -0,0 +1,193 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.activity; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.acl.common.AclException; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.common.ProxyException; +import org.apache.rocketmq.proxy.common.ProxyExceptionCode; +import org.apache.rocketmq.proxy.common.utils.ExceptionUtils; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.ProxyConfig; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.proxy.processor.channel.ChannelProtocolType; +import org.apache.rocketmq.proxy.remoting.pipeline.RequestPipeline; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.netty.AttributeKeys; +import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public abstract class AbstractRemotingActivity implements NettyRequestProcessor { + protected final static Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + protected final MessagingProcessor messagingProcessor; + protected static final String BROKER_NAME_FIELD = "bname"; + protected static final String BROKER_NAME_FIELD_FOR_SEND_MESSAGE_V2 = "n"; + private static final Map PROXY_EXCEPTION_RESPONSE_CODE_MAP = new HashMap() { + { + put(ProxyExceptionCode.FORBIDDEN, ResponseCode.NO_PERMISSION); + put(ProxyExceptionCode.MESSAGE_PROPERTY_CONFLICT_WITH_TYPE, ResponseCode.MESSAGE_ILLEGAL); + put(ProxyExceptionCode.INTERNAL_SERVER_ERROR, ResponseCode.SYSTEM_ERROR); + put(ProxyExceptionCode.TRANSACTION_DATA_NOT_FOUND, ResponseCode.SUCCESS); + } + }; + protected final RequestPipeline requestPipeline; + + public AbstractRemotingActivity(RequestPipeline requestPipeline, MessagingProcessor messagingProcessor) { + this.requestPipeline = requestPipeline; + this.messagingProcessor = messagingProcessor; + } + + protected RemotingCommand request(ChannelHandlerContext ctx, RemotingCommand request, + ProxyContext context, long timeoutMillis) throws Exception { + String brokerName; + if (request.getCode() == RequestCode.SEND_MESSAGE_V2) { + if (request.getExtFields().get(BROKER_NAME_FIELD_FOR_SEND_MESSAGE_V2) == null) { + return RemotingCommand.buildErrorResponse(ResponseCode.VERSION_NOT_SUPPORTED, + "Request doesn't have field bname"); + } + brokerName = request.getExtFields().get(BROKER_NAME_FIELD_FOR_SEND_MESSAGE_V2); + } else { + if (request.getExtFields().get(BROKER_NAME_FIELD) == null) { + return RemotingCommand.buildErrorResponse(ResponseCode.VERSION_NOT_SUPPORTED, + "Request doesn't have field bname"); + } + brokerName = request.getExtFields().get(BROKER_NAME_FIELD); + } + if (request.isOnewayRPC()) { + messagingProcessor.requestOneway(context, brokerName, request, timeoutMillis); + return null; + } + messagingProcessor.request(context, brokerName, request, timeoutMillis) + .thenAccept(r -> writeResponse(ctx, context, request, r)) + .exceptionally(t -> { + writeErrResponse(ctx, context, request, t); + return null; + }); + return null; + } + + @Override + public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) throws Exception { + ProxyContext context = createContext(ctx, request); + try { + this.requestPipeline.execute(ctx, request, context); + RemotingCommand response = this.processRequest0(ctx, request, context); + if (response != null) { + writeResponse(ctx, context, request, response); + } + return null; + } catch (Throwable t) { + writeErrResponse(ctx, context, request, t); + return null; + } + } + + @Override + public boolean rejectRequest() { + return false; + } + + protected abstract RemotingCommand processRequest0(ChannelHandlerContext ctx, RemotingCommand request, + ProxyContext context) throws Exception; + + protected ProxyContext createContext(ChannelHandlerContext ctx, RemotingCommand request) { + ProxyContext context = ProxyContext.create(); + Channel channel = ctx.channel(); + context.setAction(RemotingHelper.getRequestCodeDesc(request.getCode())) + .setProtocolType(ChannelProtocolType.REMOTING.getName()) + .setChannel(channel) + .setLocalAddress(NetworkUtil.socketAddress2String(ctx.channel().localAddress())) + .setRemoteAddress(RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + + Optional.ofNullable(RemotingHelper.getAttributeValue(AttributeKeys.LANGUAGE_CODE_KEY, channel)) + .ifPresent(language -> context.setLanguage(language.name())); + Optional.ofNullable(RemotingHelper.getAttributeValue(AttributeKeys.CLIENT_ID_KEY, channel)) + .ifPresent(context::setClientID); + Optional.ofNullable(RemotingHelper.getAttributeValue(AttributeKeys.VERSION_KEY, channel)) + .ifPresent(version -> context.setClientVersion(MQVersion.getVersionDesc(version))); + + return context; + } + + protected void writeErrResponse(ChannelHandlerContext ctx, final ProxyContext context, + final RemotingCommand request, Throwable t) { + t = ExceptionUtils.getRealException(t); + if (t instanceof ProxyException) { + ProxyException e = (ProxyException) t; + writeResponse(ctx, context, request, + RemotingCommand.createResponseCommand( + PROXY_EXCEPTION_RESPONSE_CODE_MAP.getOrDefault(e.getCode(), ResponseCode.SYSTEM_ERROR), + e.getMessage()), + t); + } else if (t instanceof MQClientException) { + MQClientException e = (MQClientException) t; + writeResponse(ctx, context, request, RemotingCommand.createResponseCommand(e.getResponseCode(), e.getErrorMessage()), t); + } else if (t instanceof MQBrokerException) { + MQBrokerException e = (MQBrokerException) t; + writeResponse(ctx, context, request, RemotingCommand.createResponseCommand(e.getResponseCode(), e.getErrorMessage()), t); + } else if (t instanceof AclException) { + writeResponse(ctx, context, request, RemotingCommand.createResponseCommand(ResponseCode.NO_PERMISSION, t.getMessage()), t); + } else { + writeResponse(ctx, context, request, + RemotingCommand.createResponseCommand(ResponseCode.SYSTEM_ERROR, t.getMessage()), t); + } + } + + protected void writeResponse(ChannelHandlerContext ctx, final ProxyContext context, + final RemotingCommand request, RemotingCommand response) { + writeResponse(ctx, context, request, response, null); + } + + protected void writeResponse(ChannelHandlerContext ctx, final ProxyContext context, + final RemotingCommand request, RemotingCommand response, Throwable t) { + if (request.isOnewayRPC()) { + return; + } + if (!ctx.channel().isWritable()) { + return; + } + + ProxyConfig config = ConfigurationManager.getProxyConfig(); + + response.setOpaque(request.getOpaque()); + response.markResponseType(); + response.addExtField(MessageConst.PROPERTY_MSG_REGION, config.getRegionId()); + response.addExtField(MessageConst.PROPERTY_TRACE_SWITCH, String.valueOf(config.isTraceOn())); + if (t != null) { + response.setRemark(t.getMessage()); + } + + ctx.writeAndFlush(response); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/AckMessageActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/AckMessageActivity.java new file mode 100644 index 00000000000..723b5918bb6 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/AckMessageActivity.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.activity; + +import io.netty.channel.ChannelHandlerContext; +import java.time.Duration; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.proxy.remoting.pipeline.RequestPipeline; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public class AckMessageActivity extends AbstractRemotingActivity { + public AckMessageActivity(RequestPipeline requestPipeline, + MessagingProcessor messagingProcessor) { + super(requestPipeline, messagingProcessor); + } + + @Override + protected RemotingCommand processRequest0(ChannelHandlerContext ctx, RemotingCommand request, + ProxyContext context) throws Exception { + return request(ctx, request, context, Duration.ofSeconds(3).toMillis()); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/ChangeInvisibleTimeActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/ChangeInvisibleTimeActivity.java new file mode 100644 index 00000000000..9f6de99e08c --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/ChangeInvisibleTimeActivity.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.activity; + +import io.netty.channel.ChannelHandlerContext; +import java.time.Duration; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.proxy.remoting.pipeline.RequestPipeline; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public class ChangeInvisibleTimeActivity extends AbstractRemotingActivity { + public ChangeInvisibleTimeActivity(RequestPipeline requestPipeline, + MessagingProcessor messagingProcessor) { + super(requestPipeline, messagingProcessor); + } + + @Override + protected RemotingCommand processRequest0(ChannelHandlerContext ctx, RemotingCommand request, + ProxyContext context) throws Exception { + return request(ctx, request, context, Duration.ofSeconds(3).toMillis()); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/ClientManagerActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/ClientManagerActivity.java new file mode 100644 index 00000000000..c671593a34b --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/ClientManagerActivity.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.activity; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.broker.client.ClientChannelInfo; +import org.apache.rocketmq.broker.client.ConsumerGroupEvent; +import org.apache.rocketmq.broker.client.ConsumerIdsChangeListener; +import org.apache.rocketmq.broker.client.ProducerChangeListener; +import org.apache.rocketmq.broker.client.ProducerGroupEvent; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.proxy.remoting.channel.RemotingChannel; +import org.apache.rocketmq.proxy.remoting.channel.RemotingChannelManager; +import org.apache.rocketmq.proxy.remoting.pipeline.RequestPipeline; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.netty.AttributeKeys; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.UnregisterClientRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UnregisterClientResponseHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumerData; +import org.apache.rocketmq.remoting.protocol.heartbeat.HeartbeatData; +import org.apache.rocketmq.remoting.protocol.heartbeat.ProducerData; + +import java.util.Set; + +public class ClientManagerActivity extends AbstractRemotingActivity { + + private final RemotingChannelManager remotingChannelManager; + + public ClientManagerActivity(RequestPipeline requestPipeline, MessagingProcessor messagingProcessor, + RemotingChannelManager manager) { + super(requestPipeline, messagingProcessor); + this.remotingChannelManager = manager; + this.init(); + } + + protected void init() { + this.messagingProcessor.registerConsumerListener(new ConsumerIdsChangeListenerImpl()); + this.messagingProcessor.registerProducerListener(new ProducerChangeListenerImpl()); + } + + @Override + protected RemotingCommand processRequest0(ChannelHandlerContext ctx, RemotingCommand request, + ProxyContext context) throws Exception { + switch (request.getCode()) { + case RequestCode.HEART_BEAT: + return this.heartBeat(ctx, request, context); + case RequestCode.UNREGISTER_CLIENT: + return this.unregisterClient(ctx, request, context); + case RequestCode.CHECK_CLIENT_CONFIG: + return this.checkClientConfig(ctx, request, context); + default: + break; + } + return null; + } + + protected RemotingCommand heartBeat(ChannelHandlerContext ctx, RemotingCommand request, + ProxyContext context) { + HeartbeatData heartbeatData = HeartbeatData.decode(request.getBody(), HeartbeatData.class); + String clientId = heartbeatData.getClientID(); + + for (ProducerData data : heartbeatData.getProducerDataSet()) { + ClientChannelInfo clientChannelInfo = new ClientChannelInfo( + this.remotingChannelManager.createProducerChannel(context, ctx.channel(), data.getGroupName(), clientId), + clientId, request.getLanguage(), + request.getVersion()); + setClientPropertiesToChannelAttr(clientChannelInfo); + messagingProcessor.registerProducer(context, data.getGroupName(), clientChannelInfo); + } + + for (ConsumerData data : heartbeatData.getConsumerDataSet()) { + ClientChannelInfo clientChannelInfo = new ClientChannelInfo( + this.remotingChannelManager.createConsumerChannel(context, ctx.channel(), data.getGroupName(), clientId, data.getSubscriptionDataSet()), + clientId, request.getLanguage(), + request.getVersion()); + setClientPropertiesToChannelAttr(clientChannelInfo); + messagingProcessor.registerConsumer(context, data.getGroupName(), clientChannelInfo, data.getConsumeType(), + data.getMessageModel(), data.getConsumeFromWhere(), data.getSubscriptionDataSet(), true); + } + + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(""); + return response; + } + + private void setClientPropertiesToChannelAttr(final ClientChannelInfo clientChannelInfo) { + Channel channel = clientChannelInfo.getChannel(); + if (channel instanceof RemotingChannel) { + RemotingChannel remotingChannel = (RemotingChannel) channel; + Channel parent = remotingChannel.parent(); + RemotingHelper.setPropertyToAttr(parent, AttributeKeys.CLIENT_ID_KEY, clientChannelInfo.getClientId()); + RemotingHelper.setPropertyToAttr(parent, AttributeKeys.LANGUAGE_CODE_KEY, clientChannelInfo.getLanguage()); + RemotingHelper.setPropertyToAttr(parent, AttributeKeys.VERSION_KEY, clientChannelInfo.getVersion()); + } + + } + + protected RemotingCommand unregisterClient(ChannelHandlerContext ctx, RemotingCommand request, + ProxyContext context) throws RemotingCommandException { + final RemotingCommand response = RemotingCommand.createResponseCommand(UnregisterClientResponseHeader.class); + final UnregisterClientRequestHeader requestHeader = + (UnregisterClientRequestHeader) request.decodeCommandCustomHeader(UnregisterClientRequestHeader.class); + final String producerGroup = requestHeader.getProducerGroup(); + if (producerGroup != null) { + RemotingChannel channel = this.remotingChannelManager.removeProducerChannel(context, producerGroup, ctx.channel()); + ClientChannelInfo clientChannelInfo = new ClientChannelInfo( + channel, + requestHeader.getClientID(), + request.getLanguage(), + request.getVersion()); + this.messagingProcessor.unRegisterProducer(context, producerGroup, clientChannelInfo); + } + final String consumerGroup = requestHeader.getConsumerGroup(); + if (consumerGroup != null) { + RemotingChannel channel = this.remotingChannelManager.removeConsumerChannel(context, consumerGroup, ctx.channel()); + ClientChannelInfo clientChannelInfo = new ClientChannelInfo( + channel, + requestHeader.getClientID(), + request.getLanguage(), + request.getVersion()); + this.messagingProcessor.unRegisterConsumer(context, consumerGroup, clientChannelInfo); + } + response.setCode(ResponseCode.SUCCESS); + response.setRemark(""); + return response; + } + + protected RemotingCommand checkClientConfig(ChannelHandlerContext ctx, RemotingCommand request, + ProxyContext context) { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(""); + return response; + } + + public void doChannelCloseEvent(String remoteAddr, Channel channel) { + Set remotingChannelSet = this.remotingChannelManager.removeChannel(channel); + for (RemotingChannel remotingChannel : remotingChannelSet) { + this.messagingProcessor.doChannelCloseEvent(remoteAddr, remotingChannel); + } + } + + protected class ConsumerIdsChangeListenerImpl implements ConsumerIdsChangeListener { + + @Override + public void handle(ConsumerGroupEvent event, String group, Object... args) { + if (event == ConsumerGroupEvent.CLIENT_UNREGISTER) { + if (args == null || args.length < 1) { + return; + } + if (args[0] instanceof ClientChannelInfo) { + ClientChannelInfo clientChannelInfo = (ClientChannelInfo) args[0]; + remotingChannelManager.removeConsumerChannel(ProxyContext.createForInner(this.getClass()), group, clientChannelInfo.getChannel()); + log.info("remove remoting channel when client unregister. clientChannelInfo:{}", clientChannelInfo); + } + } + } + + @Override + public void shutdown() { + + } + } + + protected class ProducerChangeListenerImpl implements ProducerChangeListener { + + @Override + public void handle(ProducerGroupEvent event, String group, ClientChannelInfo clientChannelInfo) { + if (event == ProducerGroupEvent.CLIENT_UNREGISTER) { + remotingChannelManager.removeProducerChannel(ProxyContext.createForInner(this.getClass()), group, clientChannelInfo.getChannel()); + } + } + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/ConsumerManagerActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/ConsumerManagerActivity.java new file mode 100644 index 00000000000..b21b4afa42d --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/ConsumerManagerActivity.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.activity; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.rocketmq.broker.client.ClientChannelInfo; +import org.apache.rocketmq.broker.client.ConsumerGroupInfo; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.Connection; +import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection; +import org.apache.rocketmq.remoting.protocol.body.LockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.body.UnlockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerConnectionListRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerListByGroupRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerListByGroupResponseBody; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerListByGroupResponseHeader; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.proxy.remoting.pipeline.RequestPipeline; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public class ConsumerManagerActivity extends AbstractRemotingActivity { + public ConsumerManagerActivity(RequestPipeline requestPipeline, MessagingProcessor messagingProcessor) { + super(requestPipeline, messagingProcessor); + } + + @Override + protected RemotingCommand processRequest0(ChannelHandlerContext ctx, RemotingCommand request, + ProxyContext context) throws Exception { + switch (request.getCode()) { + case RequestCode.GET_CONSUMER_LIST_BY_GROUP: { + return getConsumerListByGroup(ctx, request, context); + } + case RequestCode.LOCK_BATCH_MQ: { + return lockBatchMQ(ctx, request, context); + } + case RequestCode.UNLOCK_BATCH_MQ: { + return unlockBatchMQ(ctx, request, context); + } + case RequestCode.UPDATE_CONSUMER_OFFSET: + case RequestCode.QUERY_CONSUMER_OFFSET: + case RequestCode.SEARCH_OFFSET_BY_TIMESTAMP: + case RequestCode.GET_MIN_OFFSET: + case RequestCode.GET_MAX_OFFSET: + case RequestCode.GET_EARLIEST_MSG_STORETIME: { + return request(ctx, request, context, Duration.ofSeconds(3).toMillis()); + } + case RequestCode.GET_CONSUMER_CONNECTION_LIST: { + return getConsumerConnectionList(ctx, request, context); + } + default: + break; + } + return null; + } + + protected RemotingCommand getConsumerListByGroup(ChannelHandlerContext ctx, RemotingCommand request, + ProxyContext context) throws Exception { + RemotingCommand response = RemotingCommand.createResponseCommand(GetConsumerListByGroupResponseHeader.class); + GetConsumerListByGroupRequestHeader header = (GetConsumerListByGroupRequestHeader) request.decodeCommandCustomHeader(GetConsumerListByGroupRequestHeader.class); + ConsumerGroupInfo consumerGroupInfo = messagingProcessor.getConsumerGroupInfo(context, header.getConsumerGroup()); + List clientIds = consumerGroupInfo.getAllClientId(); + GetConsumerListByGroupResponseBody body = new GetConsumerListByGroupResponseBody(); + body.setConsumerIdList(clientIds); + response.setBody(body.encode()); + response.setCode(ResponseCode.SUCCESS); + return response; + } + + protected RemotingCommand getConsumerConnectionList(ChannelHandlerContext ctx, RemotingCommand request, + ProxyContext context) throws Exception { + RemotingCommand response = RemotingCommand.createResponseCommand(GetConsumerConnectionListRequestHeader.class); + GetConsumerConnectionListRequestHeader header = (GetConsumerConnectionListRequestHeader) request.decodeCommandCustomHeader(GetConsumerConnectionListRequestHeader.class); + ConsumerGroupInfo consumerGroupInfo = messagingProcessor.getConsumerGroupInfo(context, header.getConsumerGroup()); + if (consumerGroupInfo != null) { + ConsumerConnection bodydata = new ConsumerConnection(); + bodydata.setConsumeFromWhere(consumerGroupInfo.getConsumeFromWhere()); + bodydata.setConsumeType(consumerGroupInfo.getConsumeType()); + bodydata.setMessageModel(consumerGroupInfo.getMessageModel()); + bodydata.getSubscriptionTable().putAll(consumerGroupInfo.getSubscriptionTable()); + + Iterator> it = consumerGroupInfo.getChannelInfoTable().entrySet().iterator(); + while (it.hasNext()) { + ClientChannelInfo info = it.next().getValue(); + Connection connection = new Connection(); + connection.setClientId(info.getClientId()); + connection.setLanguage(info.getLanguage()); + connection.setVersion(info.getVersion()); + connection.setClientAddr(RemotingHelper.parseChannelRemoteAddr(info.getChannel())); + + bodydata.getConnectionSet().add(connection); + } + + byte[] body = bodydata.encode(); + response.setBody(body); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + + return response; + } + + response.setCode(ResponseCode.CONSUMER_NOT_ONLINE); + response.setRemark("the consumer group[" + header.getConsumerGroup() + "] not online"); + return response; + } + + protected RemotingCommand lockBatchMQ(ChannelHandlerContext ctx, RemotingCommand request, + ProxyContext context) throws Exception { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + LockBatchRequestBody requestBody = LockBatchRequestBody.decode(request.getBody(), LockBatchRequestBody.class); + Set mqSet = requestBody.getMqSet(); + if (mqSet.isEmpty()) { + response.setBody(requestBody.encode()); + response.setRemark("MessageQueue set is empty"); + return response; + } + + String brokerName = new ArrayList<>(mqSet).get(0).getBrokerName(); + messagingProcessor.request(context, brokerName, request, Duration.ofSeconds(3).toMillis()) + .thenAccept(r -> writeResponse(ctx, context, request, r)) + .exceptionally(t -> { + writeErrResponse(ctx, context, request, t); + return null; + }); + return null; + } + + protected RemotingCommand unlockBatchMQ(ChannelHandlerContext ctx, RemotingCommand request, + ProxyContext context) throws Exception { + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + UnlockBatchRequestBody requestBody = UnlockBatchRequestBody.decode(request.getBody(), UnlockBatchRequestBody.class); + Set mqSet = requestBody.getMqSet(); + if (mqSet.isEmpty()) { + response.setBody(requestBody.encode()); + response.setRemark("MessageQueue set is empty"); + return response; + } + + String brokerName = new ArrayList<>(mqSet).get(0).getBrokerName(); + messagingProcessor.request(context, brokerName, request, Duration.ofSeconds(3).toMillis()) + .thenAccept(r -> writeResponse(ctx, context, request, r)) + .exceptionally(t -> { + writeErrResponse(ctx, context, request, t); + return null; + }); + return null; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/GetTopicRouteActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/GetTopicRouteActivity.java new file mode 100644 index 00000000000..9972c26c991 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/GetTopicRouteActivity.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.activity; + +import com.alibaba.fastjson.serializer.SerializerFeature; +import com.google.common.net.HostAndPort; +import io.netty.channel.ChannelHandlerContext; +import java.util.ArrayList; +import java.util.List; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.namesrv.GetRouteInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.apache.rocketmq.proxy.common.Address; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.ProxyConfig; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.proxy.remoting.pipeline.RequestPipeline; +import org.apache.rocketmq.proxy.service.route.ProxyTopicRouteData; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public class GetTopicRouteActivity extends AbstractRemotingActivity { + public GetTopicRouteActivity(RequestPipeline requestPipeline, + MessagingProcessor messagingProcessor) { + super(requestPipeline, messagingProcessor); + } + + @Override + protected RemotingCommand processRequest0(ChannelHandlerContext ctx, RemotingCommand request, + ProxyContext context) throws Exception { + ProxyConfig proxyConfig = ConfigurationManager.getProxyConfig(); + final RemotingCommand response = RemotingCommand.createResponseCommand(null); + final GetRouteInfoRequestHeader requestHeader = + (GetRouteInfoRequestHeader) request.decodeCommandCustomHeader(GetRouteInfoRequestHeader.class); + List
    addressList = new ArrayList<>(); + // AddressScheme is just a placeholder and will not affect topic route result in this case. + addressList.add(new Address(Address.AddressScheme.IPv4, HostAndPort.fromParts(proxyConfig.getRemotingAccessAddr(), proxyConfig.getRemotingListenPort()))); + ProxyTopicRouteData proxyTopicRouteData = messagingProcessor.getTopicRouteDataForProxy(context, addressList, requestHeader.getTopic()); + TopicRouteData topicRouteData = proxyTopicRouteData.buildTopicRouteData(); + + byte[] content; + Boolean standardJsonOnly = requestHeader.getAcceptStandardJsonOnly(); + if (request.getVersion() >= MQVersion.Version.V4_9_4.ordinal() || null != standardJsonOnly && standardJsonOnly) { + content = topicRouteData.encode(SerializerFeature.BrowserCompatible, + SerializerFeature.QuoteFieldNames, SerializerFeature.SkipTransientField, + SerializerFeature.MapSortField); + } else { + content = topicRouteData.encode(); + } + + response.setBody(content); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + return response; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/PopMessageActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/PopMessageActivity.java new file mode 100644 index 00000000000..a635e55cc6c --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/PopMessageActivity.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.activity; + +import io.netty.channel.ChannelHandlerContext; +import java.time.Duration; +import org.apache.rocketmq.remoting.protocol.header.PopMessageRequestHeader; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.proxy.remoting.pipeline.RequestPipeline; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public class PopMessageActivity extends AbstractRemotingActivity { + public PopMessageActivity(RequestPipeline requestPipeline, + MessagingProcessor messagingProcessor) { + super(requestPipeline, messagingProcessor); + } + + @Override + protected RemotingCommand processRequest0(ChannelHandlerContext ctx, RemotingCommand request, + ProxyContext context) throws Exception { + PopMessageRequestHeader popMessageRequestHeader = (PopMessageRequestHeader) request.decodeCommandCustomHeader(PopMessageRequestHeader.class); + long timeoutMillis = popMessageRequestHeader.getPollTime(); + return request(ctx, request, context, timeoutMillis + Duration.ofSeconds(10).toMillis()); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/PullMessageActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/PullMessageActivity.java new file mode 100644 index 00000000000..3324c231ab4 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/PullMessageActivity.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.activity; + +import io.netty.channel.ChannelHandlerContext; +import java.time.Duration; +import org.apache.rocketmq.broker.client.ConsumerGroupInfo; +import org.apache.rocketmq.common.sysflag.PullSysFlag; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.proxy.remoting.pipeline.RequestPipeline; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; + +public class PullMessageActivity extends AbstractRemotingActivity { + public PullMessageActivity(RequestPipeline requestPipeline, + MessagingProcessor messagingProcessor) { + super(requestPipeline, messagingProcessor); + } + + @Override + protected RemotingCommand processRequest0(ChannelHandlerContext ctx, RemotingCommand request, + ProxyContext context) throws Exception { + PullMessageRequestHeader requestHeader = (PullMessageRequestHeader) request.decodeCommandCustomHeader(PullMessageRequestHeader.class); + int sysFlag = requestHeader.getSysFlag(); + if (!PullSysFlag.hasSubscriptionFlag(sysFlag)) { + ConsumerGroupInfo consumerInfo = messagingProcessor.getConsumerGroupInfo(context, requestHeader.getConsumerGroup()); + if (consumerInfo == null) { + return RemotingCommand.buildErrorResponse(ResponseCode.SUBSCRIPTION_NOT_LATEST, + "the consumer's subscription not latest"); + } + SubscriptionData subscriptionData = consumerInfo.findSubscriptionData(requestHeader.getTopic()); + if (subscriptionData == null) { + return RemotingCommand.buildErrorResponse(ResponseCode.SUBSCRIPTION_NOT_EXIST, + "the consumer's subscription not exist"); + } + requestHeader.setSysFlag(PullSysFlag.buildSysFlagWithSubscription(sysFlag)); + requestHeader.setSubscription(subscriptionData.getSubString()); + requestHeader.setExpressionType(subscriptionData.getExpressionType()); + request.writeCustomHeader(requestHeader); + request.makeCustomHeaderToNet(); + } + long timeoutMillis = requestHeader.getSuspendTimeoutMillis() + Duration.ofSeconds(10).toMillis(); + return request(ctx, request, context, timeoutMillis); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/SendMessageActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/SendMessageActivity.java new file mode 100644 index 00000000000..17af0fdcb37 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/SendMessageActivity.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.activity; + +import io.netty.channel.ChannelHandlerContext; +import java.time.Duration; +import java.util.Map; +import org.apache.rocketmq.common.attribute.TopicMessageType; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.remoting.protocol.NamespaceUtil; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.proxy.processor.validator.DefaultTopicMessageTypeValidator; +import org.apache.rocketmq.proxy.processor.validator.TopicMessageTypeValidator; +import org.apache.rocketmq.proxy.remoting.pipeline.RequestPipeline; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public class SendMessageActivity extends AbstractRemotingActivity { + TopicMessageTypeValidator topicMessageTypeValidator; + + public SendMessageActivity(RequestPipeline requestPipeline, + MessagingProcessor messagingProcessor) { + super(requestPipeline, messagingProcessor); + this.topicMessageTypeValidator = new DefaultTopicMessageTypeValidator(); + } + + @Override + protected RemotingCommand processRequest0(ChannelHandlerContext ctx, RemotingCommand request, + ProxyContext context) throws Exception { + switch (request.getCode()) { + case RequestCode.SEND_MESSAGE: + case RequestCode.SEND_MESSAGE_V2: + case RequestCode.SEND_BATCH_MESSAGE: { + return sendMessage(ctx, request, context); + } + case RequestCode.CONSUMER_SEND_MSG_BACK: { + return consumerSendMessage(ctx, request, context); + } + default: + break; + } + return null; + } + + protected RemotingCommand sendMessage(ChannelHandlerContext ctx, RemotingCommand request, + ProxyContext context) throws Exception { + SendMessageRequestHeader requestHeader = SendMessageRequestHeader.parseRequestHeader(request); + String topic = requestHeader.getTopic(); + Map property = MessageDecoder.string2messageProperties(requestHeader.getProperties()); + TopicMessageType messageType = TopicMessageType.parseFromMessageProperty(property); + if (ConfigurationManager.getProxyConfig().isEnableTopicMessageTypeCheck()) { + if (topicMessageTypeValidator != null) { + // Do not check retry or dlq topic + if (!NamespaceUtil.isRetryTopic(topic) && !NamespaceUtil.isDLQTopic(topic)) { + TopicMessageType topicMessageType = messagingProcessor.getMetadataService().getTopicMessageType(context, topic); + topicMessageTypeValidator.validate(topicMessageType, messageType); + } + } + } + if (!NamespaceUtil.isRetryTopic(topic) && !NamespaceUtil.isDLQTopic(topic)) { + if (TopicMessageType.TRANSACTION.equals(messageType)) { + messagingProcessor.addTransactionSubscription(context, requestHeader.getProducerGroup(), requestHeader.getTopic()); + } + } + return request(ctx, request, context, Duration.ofSeconds(3).toMillis()); + } + + protected RemotingCommand consumerSendMessage(ChannelHandlerContext ctx, RemotingCommand request, + ProxyContext context) throws Exception { + return request(ctx, request, context, Duration.ofSeconds(3).toMillis()); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/TransactionActivity.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/TransactionActivity.java new file mode 100644 index 00000000000..bc5e0ca35bb --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/activity/TransactionActivity.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.activity; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.EndTransactionRequestHeader; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.proxy.processor.TransactionStatus; +import org.apache.rocketmq.proxy.remoting.pipeline.RequestPipeline; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public class TransactionActivity extends AbstractRemotingActivity { + + public TransactionActivity(RequestPipeline requestPipeline, + MessagingProcessor messagingProcessor) { + super(requestPipeline, messagingProcessor); + } + + @Override + protected RemotingCommand processRequest0(ChannelHandlerContext ctx, RemotingCommand request, + ProxyContext context) throws Exception { + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + + final EndTransactionRequestHeader requestHeader = (EndTransactionRequestHeader) request.decodeCommandCustomHeader(EndTransactionRequestHeader.class); + + TransactionStatus transactionStatus = TransactionStatus.UNKNOWN; + switch (requestHeader.getCommitOrRollback()) { + case MessageSysFlag.TRANSACTION_COMMIT_TYPE: + transactionStatus = TransactionStatus.COMMIT; + break; + case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE: + transactionStatus = TransactionStatus.ROLLBACK; + break; + default: + break; + } + + this.messagingProcessor.endTransaction( + context, + requestHeader.getTransactionId(), + requestHeader.getMsgId(), + requestHeader.getProducerGroup(), + transactionStatus, + requestHeader.getFromTransactionCheck() + ); + return response; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/channel/RemotingChannel.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/channel/RemotingChannel.java new file mode 100644 index 00000000000..d755fdcc493 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/channel/RemotingChannel.java @@ -0,0 +1,260 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.channel; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.TypeReference; +import com.google.common.base.MoreObjects; +import io.netty.channel.Channel; +import io.netty.channel.ChannelConfig; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelMetadata; +import java.time.Duration; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.common.channel.ChannelHelper; +import org.apache.rocketmq.proxy.common.utils.ExceptionUtils; +import org.apache.rocketmq.proxy.common.utils.FutureUtils; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.processor.channel.ChannelExtendAttributeGetter; +import org.apache.rocketmq.proxy.processor.channel.ChannelProtocolType; +import org.apache.rocketmq.proxy.processor.channel.RemoteChannel; +import org.apache.rocketmq.proxy.processor.channel.RemoteChannelConverter; +import org.apache.rocketmq.proxy.remoting.RemotingProxyOutClient; +import org.apache.rocketmq.proxy.remoting.common.RemotingConverter; +import org.apache.rocketmq.proxy.service.relay.ProxyChannel; +import org.apache.rocketmq.proxy.service.relay.ProxyRelayResult; +import org.apache.rocketmq.proxy.service.relay.ProxyRelayService; +import org.apache.rocketmq.proxy.service.transaction.TransactionData; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.header.CheckTransactionStateRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ConsumeMessageDirectlyResultRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerRunningInfoRequestHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; + +public class RemotingChannel extends ProxyChannel implements RemoteChannelConverter, ChannelExtendAttributeGetter { + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + private static final long DEFAULT_MQ_CLIENT_TIMEOUT = Duration.ofSeconds(3).toMillis(); + private final String clientId; + private final String remoteAddress; + private final String localAddress; + private final RemotingProxyOutClient remotingProxyOutClient; + private final Set subscriptionData; + + public RemotingChannel(RemotingProxyOutClient remotingProxyOutClient, ProxyRelayService proxyRelayService, + Channel parent, + String clientId, Set subscriptionData) { + super(proxyRelayService, parent, parent.id(), + NetworkUtil.socketAddress2String(parent.remoteAddress()), + NetworkUtil.socketAddress2String(parent.localAddress())); + this.remotingProxyOutClient = remotingProxyOutClient; + this.clientId = clientId; + this.remoteAddress = NetworkUtil.socketAddress2String(parent.remoteAddress()); + this.localAddress = NetworkUtil.socketAddress2String(parent.localAddress()); + this.subscriptionData = subscriptionData; + } + + @Override + public boolean isOpen() { + return this.parent().isOpen(); + } + + @Override + public boolean isActive() { + return this.parent().isActive(); + } + + @Override + public boolean isWritable() { + return this.parent().isWritable(); + } + + @Override + public ChannelFuture close() { + return this.parent().close(); + } + + @Override + public ChannelConfig config() { + return this.parent().config(); + } + + @Override + public ChannelMetadata metadata() { + return this.parent().metadata(); + } + + @Override + protected CompletableFuture processOtherMessage(Object msg) { + this.parent().writeAndFlush(msg); + return CompletableFuture.completedFuture(null); + } + + @Override + protected CompletableFuture processCheckTransaction(CheckTransactionStateRequestHeader header, + MessageExt messageExt, TransactionData transactionData, + CompletableFuture> responseFuture) { + CompletableFuture writeFuture = new CompletableFuture<>(); + try { + CheckTransactionStateRequestHeader requestHeader = new CheckTransactionStateRequestHeader(); + requestHeader.setCommitLogOffset(transactionData.getCommitLogOffset()); + requestHeader.setTranStateTableOffset(transactionData.getTranStateTableOffset()); + requestHeader.setTransactionId(transactionData.getTransactionId()); + requestHeader.setMsgId(header.getMsgId()); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CHECK_TRANSACTION_STATE, requestHeader); + request.setBody(RemotingConverter.getInstance().convertMsgToBytes(messageExt)); + + this.parent().writeAndFlush(request).addListener((ChannelFutureListener) future -> { + if (future.isSuccess()) { + responseFuture.complete(null); + writeFuture.complete(null); + } else { + Exception e = new RemotingException("write and flush data failed"); + responseFuture.completeExceptionally(e); + writeFuture.completeExceptionally(e); + } + }); + } catch (Throwable t) { + responseFuture.completeExceptionally(t); + writeFuture.completeExceptionally(t); + } + return writeFuture; + } + + @Override + protected CompletableFuture processGetConsumerRunningInfo(RemotingCommand command, + GetConsumerRunningInfoRequestHeader header, + CompletableFuture> responseFuture) { + try { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_CONSUMER_RUNNING_INFO, header); + this.remotingProxyOutClient.invokeToClient(this.parent(), request, DEFAULT_MQ_CLIENT_TIMEOUT) + .thenAccept(response -> { + if (response.getCode() == ResponseCode.SUCCESS) { + ConsumerRunningInfo consumerRunningInfo = ConsumerRunningInfo.decode(response.getBody(), ConsumerRunningInfo.class); + responseFuture.complete(new ProxyRelayResult<>(ResponseCode.SUCCESS, "", consumerRunningInfo)); + } else { + String errMsg = String.format("get consumer running info failed, code:%s remark:%s", response.getCode(), response.getRemark()); + RuntimeException e = new RuntimeException(errMsg); + responseFuture.completeExceptionally(e); + } + }) + .exceptionally(t -> { + responseFuture.completeExceptionally(ExceptionUtils.getRealException(t)); + return null; + }); + return CompletableFuture.completedFuture(null); + } catch (Throwable t) { + responseFuture.completeExceptionally(t); + return FutureUtils.completeExceptionally(t); + } + } + + @Override + protected CompletableFuture processConsumeMessageDirectly(RemotingCommand command, + ConsumeMessageDirectlyResultRequestHeader header, MessageExt messageExt, + CompletableFuture> responseFuture) { + try { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CONSUME_MESSAGE_DIRECTLY, header); + request.setBody(RemotingConverter.getInstance().convertMsgToBytes(messageExt)); + + this.remotingProxyOutClient.invokeToClient(this.parent(), request, DEFAULT_MQ_CLIENT_TIMEOUT) + .thenAccept(response -> { + if (response.getCode() == ResponseCode.SUCCESS) { + ConsumeMessageDirectlyResult result = ConsumeMessageDirectlyResult.decode(response.getBody(), ConsumeMessageDirectlyResult.class); + responseFuture.complete(new ProxyRelayResult<>(ResponseCode.SUCCESS, "", result)); + } else { + String errMsg = String.format("consume message directly failed, code:%s remark:%s", response.getCode(), response.getRemark()); + RuntimeException e = new RuntimeException(errMsg); + responseFuture.completeExceptionally(e); + } + }) + .exceptionally(t -> { + responseFuture.completeExceptionally(ExceptionUtils.getRealException(t)); + return null; + }); + return CompletableFuture.completedFuture(null); + } catch (Throwable t) { + responseFuture.completeExceptionally(t); + return FutureUtils.completeExceptionally(t); + } + } + + public String getClientId() { + return clientId; + } + + @Override + public String getChannelExtendAttribute() { + if (this.subscriptionData == null) { + return null; + } + return JSON.toJSONString(this.subscriptionData); + } + + public static Set parseChannelExtendAttribute(Channel channel) { + if (ChannelHelper.getChannelProtocolType(channel).equals(ChannelProtocolType.REMOTING) && + channel instanceof ChannelExtendAttributeGetter) { + String attr = ((ChannelExtendAttributeGetter) channel).getChannelExtendAttribute(); + if (attr == null) { + return null; + } + + try { + return JSON.parseObject(attr, new TypeReference>() { + }); + } catch (Exception e) { + log.error("convert remoting extend attribute to subscriptionDataSet failed. data:{}", attr, e); + return null; + } + } + return null; + } + + @Override + public RemoteChannel toRemoteChannel() { + return new RemoteChannel( + ConfigurationManager.getProxyConfig().getLocalServeAddr(), + this.getRemoteAddress(), + this.getLocalAddress(), + ChannelProtocolType.REMOTING, + this.getChannelExtendAttribute()); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("parent", parent()) + .add("clientId", clientId) + .add("remoteAddress", remoteAddress) + .add("localAddress", localAddress) + .add("subscriptionData", subscriptionData) + .toString(); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/channel/RemotingChannelManager.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/channel/RemotingChannelManager.java new file mode 100644 index 00000000000..211c3c9275a --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/channel/RemotingChannelManager.java @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.channel; + +import io.netty.channel.Channel; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.common.utils.StartAndShutdown; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.remoting.RemotingProxyOutClient; +import org.apache.rocketmq.proxy.service.relay.ProxyRelayService; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; + +public class RemotingChannelManager implements StartAndShutdown { + protected final static Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + private final ProxyRelayService proxyRelayService; + protected final ConcurrentMap> groupChannelMap = new ConcurrentHashMap<>(); + + private final RemotingProxyOutClient remotingProxyOutClient; + + public RemotingChannelManager(RemotingProxyOutClient remotingProxyOutClient, ProxyRelayService proxyRelayService) { + this.remotingProxyOutClient = remotingProxyOutClient; + this.proxyRelayService = proxyRelayService; + } + + protected String buildProducerKey(String group) { + return buildKey("p", group); + } + + protected String buildConsumerKey(String group) { + return buildKey("c", group); + } + + protected String buildKey(String prefix, String group) { + return prefix + group; + } + + public RemotingChannel createProducerChannel(ProxyContext ctx, Channel channel, String group, String clientId) { + return createChannel(channel, buildProducerKey(group), clientId, Collections.emptySet()); + } + + public RemotingChannel createConsumerChannel(ProxyContext ctx, Channel channel, String group, String clientId, Set subscriptionData) { + return createChannel(channel, buildConsumerKey(group), clientId, subscriptionData); + } + + protected RemotingChannel createChannel(Channel channel, String group, String clientId, Set subscriptionData) { + this.groupChannelMap.compute(group, (groupKey, clientIdMap) -> { + if (clientIdMap == null) { + clientIdMap = new ConcurrentHashMap<>(); + } + clientIdMap.computeIfAbsent(channel, clientIdKey -> new RemotingChannel(remotingProxyOutClient, proxyRelayService, channel, clientId, subscriptionData)); + return clientIdMap; + }); + return getChannel(group, channel); + } + + protected RemotingChannel getChannel(String group, Channel channel) { + Map clientIdChannelMap = this.groupChannelMap.get(group); + if (clientIdChannelMap == null) { + return null; + } + return clientIdChannelMap.get(channel); + } + + public Set removeChannel(Channel channel) { + Set removedChannelSet = new HashSet<>(); + Set groupKeySet = groupChannelMap.keySet(); + for (String group : groupKeySet) { + RemotingChannel remotingChannel = removeChannel(group, channel); + if (remotingChannel != null) { + removedChannelSet.add(remotingChannel); + } + } + return removedChannelSet; + } + + public RemotingChannel removeProducerChannel(ProxyContext ctx, String group, Channel channel) { + return removeChannel(buildProducerKey(group), channel); + } + + public RemotingChannel removeConsumerChannel(ProxyContext ctx, String group, Channel channel) { + return removeChannel(buildConsumerKey(group), channel); + } + + protected RemotingChannel removeChannel(String group, Channel channel) { + AtomicReference channelRef = new AtomicReference<>(); + + this.groupChannelMap.computeIfPresent(group, (groupKey, channelMap) -> { + channelRef.set(channelMap.remove(getOrgRawChannel(channel))); + if (channelMap.isEmpty()) { + return null; + } + return channelMap; + }); + return channelRef.get(); + } + + /** + * to get the org channel pass by nettyRemotingServer + * @param channel + * @return + */ + protected Channel getOrgRawChannel(Channel channel) { + if (channel instanceof RemotingChannel) { + return channel.parent(); + } + return channel; + } + + @Override + public void shutdown() throws Exception { + + } + + @Override + public void start() throws Exception { + + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/common/RemotingConverter.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/common/RemotingConverter.java new file mode 100644 index 00000000000..2bd53d8de1f --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/common/RemotingConverter.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.common; + +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class RemotingConverter { + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + + protected static final Object INSTANCE_CREATE_LOCK = new Object(); + protected static volatile RemotingConverter instance; + + public static RemotingConverter getInstance() { + if (instance == null) { + synchronized (INSTANCE_CREATE_LOCK) { + if (instance == null) { + instance = new RemotingConverter(); + } + } + } + return instance; + } + + public byte[] convertMsgToBytes(final MessageExt msg) throws Exception { + // change to 0 for recalculate storeSize + msg.setStoreSize(0); + if (msg.getTopic().length() > Byte.MAX_VALUE) { + log.warn("Topic length is too long, topic: {}", msg.getTopic()); + } + return MessageDecoder.encode(msg, false); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/pipeline/AuthenticationPipeline.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/pipeline/AuthenticationPipeline.java new file mode 100644 index 00000000000..4bcc1479dcc --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/pipeline/AuthenticationPipeline.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.pipeline; + +import io.netty.channel.ChannelHandlerContext; +import java.util.List; +import org.apache.rocketmq.acl.AccessResource; +import org.apache.rocketmq.acl.AccessValidator; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.ProxyConfig; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public class AuthenticationPipeline implements RequestPipeline { + private final List accessValidatorList; + + public AuthenticationPipeline(List accessValidatorList) { + this.accessValidatorList = accessValidatorList; + } + + @Override + public void execute(ChannelHandlerContext ctx, RemotingCommand request, ProxyContext context) throws Exception { + ProxyConfig config = ConfigurationManager.getProxyConfig(); + if (config.isEnableACL()) { + for (AccessValidator accessValidator : accessValidatorList) { + AccessResource accessResource = accessValidator.parse(request, context.getRemoteAddress()); + accessValidator.validate(accessResource); + } + } + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/pipeline/RequestPipeline.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/pipeline/RequestPipeline.java new file mode 100644 index 00000000000..4c46a6e7d4b --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/pipeline/RequestPipeline.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.pipeline; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public interface RequestPipeline { + + void execute(ChannelHandlerContext ctx, RemotingCommand request, ProxyContext context) throws Exception; + + default RequestPipeline pipe(RequestPipeline source) { + return (ctx, request, context) -> { + source.execute(ctx, request, context); + execute(ctx, request, context); + }; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/protocol/ProtocolHandler.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/protocol/ProtocolHandler.java new file mode 100644 index 00000000000..4b1b03067f2 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/protocol/ProtocolHandler.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.protocol; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; + +public interface ProtocolHandler { + + boolean match(ByteBuf msg); + + void config(final ChannelHandlerContext ctx, final ByteBuf msg); +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/protocol/ProtocolNegotiationHandler.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/protocol/ProtocolNegotiationHandler.java new file mode 100644 index 00000000000..da2dded5f04 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/protocol/ProtocolNegotiationHandler.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.remoting.protocol; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import java.util.ArrayList; +import java.util.List; + +public class ProtocolNegotiationHandler extends ByteToMessageDecoder { + + private final List protocolHandlerList = new ArrayList(); + private final ProtocolHandler fallbackProtocolHandler; + + public ProtocolNegotiationHandler(ProtocolHandler fallbackProtocolHandler) { + this.fallbackProtocolHandler = fallbackProtocolHandler; + } + + public ProtocolNegotiationHandler addProtocolHandler(ProtocolHandler protocolHandler) { + protocolHandlerList.add(protocolHandler); + return this; + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + // use 4 bytes to judge protocol + if (in.readableBytes() < 4) { + return; + } + + ProtocolHandler protocolHandler = null; + for (ProtocolHandler curProtocolHandler : protocolHandlerList) { + if (curProtocolHandler.match(in)) { + protocolHandler = curProtocolHandler; + break; + } + } + + if (protocolHandler == null) { + protocolHandler = fallbackProtocolHandler; + } + + protocolHandler.config(ctx, in); + ctx.pipeline().remove(this); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/protocol/http2proxy/HAProxyMessageForwarder.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/protocol/http2proxy/HAProxyMessageForwarder.java new file mode 100644 index 00000000000..99cb99d5307 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/protocol/http2proxy/HAProxyMessageForwarder.java @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.protocol.http2proxy; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.haproxy.HAProxyCommand; +import io.netty.handler.codec.haproxy.HAProxyMessage; +import io.netty.handler.codec.haproxy.HAProxyProtocolVersion; +import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol; +import io.netty.handler.codec.haproxy.HAProxyTLV; +import io.netty.util.Attribute; +import io.netty.util.DefaultAttributeMap; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.acl.common.AclUtils; +import org.apache.rocketmq.common.constant.HAProxyConstants; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.netty.AttributeKeys; + +import java.lang.reflect.Field; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +public class HAProxyMessageForwarder extends ChannelInboundHandlerAdapter { + + private static final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_REMOTING_NAME); + + private static final Field FIELD_ATTRIBUTE = + FieldUtils.getField(DefaultAttributeMap.class, "attributes", true); + + private final Channel outboundChannel; + + public HAProxyMessageForwarder(final Channel outboundChannel) { + this.outboundChannel = outboundChannel; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + try { + forwardHAProxyMessage(ctx.channel(), outboundChannel); + ctx.fireChannelRead(msg); + } catch (Exception e) { + log.error("Forward HAProxyMessage from Remoting to gRPC server error.", e); + throw e; + } finally { + ctx.pipeline().remove(this); + } + } + + private void forwardHAProxyMessage(Channel inboundChannel, Channel outboundChannel) throws Exception { + if (!inboundChannel.hasAttr(AttributeKeys.PROXY_PROTOCOL_ADDR)) { + return; + } + + if (!(inboundChannel instanceof DefaultAttributeMap)) { + return; + } + + Attribute[] attributes = (Attribute[]) FieldUtils.readField(FIELD_ATTRIBUTE, inboundChannel); + if (ArrayUtils.isEmpty(attributes)) { + return; + } + + String sourceAddress = null, destinationAddress = null; + int sourcePort = 0, destinationPort = 0; + List haProxyTLVs = new ArrayList<>(); + + for (Attribute attribute : attributes) { + String attributeKey = attribute.key().name(); + if (!StringUtils.startsWith(attributeKey, HAProxyConstants.PROXY_PROTOCOL_PREFIX)) { + continue; + } + String attributeValue = (String) attribute.get(); + if (StringUtils.isEmpty(attributeValue)) { + continue; + } + if (attribute.key() == AttributeKeys.PROXY_PROTOCOL_ADDR) { + sourceAddress = attributeValue; + } + if (attribute.key() == AttributeKeys.PROXY_PROTOCOL_PORT) { + sourcePort = Integer.parseInt(attributeValue); + } + if (attribute.key() == AttributeKeys.PROXY_PROTOCOL_SERVER_ADDR) { + destinationAddress = attributeValue; + } + if (attribute.key() == AttributeKeys.PROXY_PROTOCOL_SERVER_PORT) { + destinationPort = Integer.parseInt(attributeValue); + } + if (StringUtils.startsWith(attributeKey, HAProxyConstants.PROXY_PROTOCOL_TLV_PREFIX)) { + HAProxyTLV haProxyTLV = buildHAProxyTLV(attributeKey, attributeValue); + if (haProxyTLV != null) { + haProxyTLVs.add(haProxyTLV); + } + } + } + + HAProxyProxiedProtocol proxiedProtocol = AclUtils.isColon(sourceAddress) ? HAProxyProxiedProtocol.TCP6 : + HAProxyProxiedProtocol.TCP4; + + HAProxyMessage message = new HAProxyMessage(HAProxyProtocolVersion.V2, HAProxyCommand.PROXY, + proxiedProtocol, sourceAddress, destinationAddress, sourcePort, destinationPort, haProxyTLVs); + outboundChannel.writeAndFlush(message).sync(); + } + + protected HAProxyTLV buildHAProxyTLV(String attributeKey, String attributeValue) throws DecoderException { + String typeString = StringUtils.substringAfter(attributeKey, HAProxyConstants.PROXY_PROTOCOL_TLV_PREFIX); + ByteBuf byteBuf = Unpooled.buffer(); + byteBuf.writeBytes(attributeValue.getBytes(Charset.defaultCharset())); + return new HAProxyTLV(Hex.decodeHex(typeString)[0], byteBuf); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/protocol/http2proxy/Http2ProtocolProxyHandler.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/protocol/http2proxy/Http2ProtocolProxyHandler.java new file mode 100644 index 00000000000..7ce563b0305 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/protocol/http2proxy/Http2ProtocolProxyHandler.java @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.protocol.http2proxy; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.handler.codec.haproxy.HAProxyMessageEncoder; +import io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslHandler; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.ProxyConfig; +import org.apache.rocketmq.proxy.remoting.protocol.ProtocolHandler; +import org.apache.rocketmq.remoting.common.TlsMode; +import org.apache.rocketmq.remoting.netty.AttributeKeys; +import org.apache.rocketmq.remoting.netty.TlsSystemConfig; + +import javax.net.ssl.SSLException; + +public class Http2ProtocolProxyHandler implements ProtocolHandler { + private static final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_REMOTING_NAME); + private static final String LOCAL_HOST = "127.0.0.1"; + /** + * The int value of "PRI ". Now use 4 bytes to judge protocol, may be has potential risks if there is a new protocol + * which start with "PRI " in the future + *

    + * The full HTTP/2 connection preface is "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" + *

    + * ref: https://datatracker.ietf.org/doc/html/rfc7540#section-3.5 + */ + private static final int PRI_INT = 0x50524920; + + private final SslContext sslContext; + + public Http2ProtocolProxyHandler() { + try { + TlsMode tlsMode = TlsSystemConfig.tlsMode; + if (TlsMode.DISABLED.equals(tlsMode)) { + sslContext = null; + } else { + sslContext = SslContextBuilder + .forClient() + .sslProvider(SslProvider.OPENSSL) + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .applicationProtocolConfig(new ApplicationProtocolConfig( + ApplicationProtocolConfig.Protocol.ALPN, + ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, + ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2)) + .build(); + } + } catch (SSLException e) { + log.error("Failed to create SSLContext for Http2ProtocolProxyHandler", e); + throw new RuntimeException("Failed to create SSLContext for Http2ProtocolProxyHandler", e); + } + } + + @Override + public boolean match(ByteBuf in) { + if (!ConfigurationManager.getProxyConfig().isEnableRemotingLocalProxyGrpc()) { + return false; + } + + // If starts with 'PRI ' + return in.getInt(in.readerIndex()) == PRI_INT; + } + + @Override + public void config(final ChannelHandlerContext ctx, final ByteBuf msg) { + // proxy channel to http2 server + final Channel inboundChannel = ctx.channel(); + + ProxyConfig config = ConfigurationManager.getProxyConfig(); + // Start the connection attempt. + Bootstrap b = new Bootstrap(); + b.group(inboundChannel.eventLoop()) + .channel(ctx.channel().getClass()) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + ch.pipeline().addLast(null, Http2ProxyBackendHandler.HANDLER_NAME, + new Http2ProxyBackendHandler(inboundChannel)); + } + }) + .option(ChannelOption.AUTO_READ, false) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, config.getLocalProxyConnectTimeoutMs()); + ChannelFuture f; + try { + f = b.connect(LOCAL_HOST, config.getGrpcServerPort()).sync(); + } catch (Exception e) { + log.error("connect http2 server failed. port:{}", config.getGrpcServerPort(), e); + inboundChannel.close(); + return; + } + + final Channel outboundChannel = f.channel(); + configPipeline(inboundChannel, outboundChannel); + + SslHandler sslHandler = null; + if (sslContext != null) { + sslHandler = sslContext.newHandler(outboundChannel.alloc(), LOCAL_HOST, config.getGrpcServerPort()); + } + ctx.pipeline().addLast(new Http2ProxyFrontendHandler(outboundChannel, sslHandler)); + } + + protected void configPipeline(Channel inboundChannel, Channel outboundChannel) { + if (inboundChannel.hasAttr(AttributeKeys.PROXY_PROTOCOL_ADDR)) { + inboundChannel.pipeline().addLast(new HAProxyMessageForwarder(outboundChannel)); + outboundChannel.pipeline().addFirst(HAProxyMessageEncoder.INSTANCE); + } + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/protocol/http2proxy/Http2ProxyBackendHandler.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/protocol/http2proxy/Http2ProxyBackendHandler.java new file mode 100644 index 00000000000..fd5408fae33 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/protocol/http2proxy/Http2ProxyBackendHandler.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.protocol.http2proxy; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class Http2ProxyBackendHandler extends ChannelInboundHandlerAdapter { + private static final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_REMOTING_NAME); + + public static final String HANDLER_NAME = "Http2ProxyBackendHandler"; + + private final Channel inboundChannel; + + public Http2ProxyBackendHandler(Channel inboundChannel) { + this.inboundChannel = inboundChannel; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.read(); + } + + @Override + public void channelRead(final ChannelHandlerContext ctx, Object msg) { + inboundChannel.writeAndFlush(msg).addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + if (future.isSuccess()) { + ctx.channel().read(); + } else { + future.channel().close(); + } + } + }); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + Http2ProxyFrontendHandler.closeOnFlush(inboundChannel); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + log.error("Http2ProxyBackendHandler#exceptionCaught", cause); + Http2ProxyFrontendHandler.closeOnFlush(ctx.channel()); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/protocol/http2proxy/Http2ProxyFrontendHandler.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/protocol/http2proxy/Http2ProxyFrontendHandler.java new file mode 100644 index 00000000000..9b37e85e53c --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/protocol/http2proxy/Http2ProxyFrontendHandler.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.protocol.http2proxy; + +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.ssl.SslHandler; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class Http2ProxyFrontendHandler extends ChannelInboundHandlerAdapter { + private static final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_REMOTING_NAME); + + public static final String HANDLER_NAME = "SslHandler"; + + // As we use inboundChannel.eventLoop() when building the Bootstrap this does not need to be volatile as + // the outboundChannel will use the same EventLoop (and therefore Thread) as the inboundChannel. + private final Channel outboundChannel; + private final SslHandler sslHandler; + + public Http2ProxyFrontendHandler(final Channel outboundChannel, final SslHandler sslHandler) { + this.outboundChannel = outboundChannel; + this.sslHandler = sslHandler; + } + + @Override + public void channelRead(final ChannelHandlerContext ctx, Object msg) { + if (outboundChannel.isActive()) { + if (sslHandler != null && outboundChannel.pipeline().get(HANDLER_NAME) == null) { + outboundChannel.pipeline().addBefore(Http2ProxyBackendHandler.HANDLER_NAME, HANDLER_NAME, sslHandler); + } + + outboundChannel.writeAndFlush(msg).addListener((ChannelFutureListener) future -> { + if (future.isSuccess()) { + // was able to flush out data, start to read the next chunk + ctx.channel().read(); + } else { + future.channel().close(); + } + }); + } + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + if (outboundChannel != null) { + closeOnFlush(outboundChannel); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + log.error("Http2ProxyFrontendHandler#exceptionCaught", cause); + closeOnFlush(ctx.channel()); + } + + /** + * Closes the specified channel after all queued write requests are flushed. + */ + static void closeOnFlush(Channel ch) { + if (ch.isActive()) { + ch.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); + } + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/protocol/remoting/RemotingProtocolHandler.java b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/protocol/remoting/RemotingProtocolHandler.java new file mode 100644 index 00000000000..49fea89cdd3 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/remoting/protocol/remoting/RemotingProtocolHandler.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.protocol.remoting; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import java.util.function.Supplier; +import org.apache.rocketmq.proxy.remoting.protocol.ProtocolHandler; +import org.apache.rocketmq.remoting.netty.NettyDecoder; +import org.apache.rocketmq.remoting.netty.NettyEncoder; +import org.apache.rocketmq.remoting.netty.NettyRemotingServer; +import org.apache.rocketmq.remoting.netty.RemotingCodeDistributionHandler; + +public class RemotingProtocolHandler implements ProtocolHandler { + + private final Supplier encoderSupplier; + private final Supplier remotingCodeDistributionHandlerSupplier; + private final Supplier connectionManageHandlerSupplier; + private final Supplier serverHandlerSupplier; + + public RemotingProtocolHandler(Supplier encoderSupplier, + Supplier remotingCodeDistributionHandlerSupplier, + Supplier connectionManageHandlerSupplier, + Supplier serverHandlerSupplier) { + this.encoderSupplier = encoderSupplier; + this.remotingCodeDistributionHandlerSupplier = remotingCodeDistributionHandlerSupplier; + this.connectionManageHandlerSupplier = connectionManageHandlerSupplier; + this.serverHandlerSupplier = serverHandlerSupplier; + } + + @Override + public boolean match(ByteBuf in) { + return true; + } + + @Override + public void config(ChannelHandlerContext ctx, ByteBuf msg) { + ctx.pipeline().addLast( + this.encoderSupplier.get(), + new NettyDecoder(), + this.remotingCodeDistributionHandlerSupplier.get(), + this.connectionManageHandlerSupplier.get(), + this.serverHandlerSupplier.get() + ); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/ClusterServiceManager.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/ClusterServiceManager.java new file mode 100644 index 00000000000..9786cec5577 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/ClusterServiceManager.java @@ -0,0 +1,199 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.service; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.broker.client.ClientChannelInfo; +import org.apache.rocketmq.broker.client.ConsumerGroupEvent; +import org.apache.rocketmq.broker.client.ConsumerIdsChangeListener; +import org.apache.rocketmq.broker.client.ConsumerManager; +import org.apache.rocketmq.broker.client.ProducerChangeListener; +import org.apache.rocketmq.broker.client.ProducerGroupEvent; +import org.apache.rocketmq.broker.client.ProducerManager; +import org.apache.rocketmq.client.common.NameserverAccessConfig; +import org.apache.rocketmq.client.impl.mqclient.DoNothingClientRemotingProcessor; +import org.apache.rocketmq.client.impl.mqclient.MQClientAPIFactory; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.AbstractStartAndShutdown; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.ProxyConfig; +import org.apache.rocketmq.proxy.service.admin.AdminService; +import org.apache.rocketmq.proxy.service.admin.DefaultAdminService; +import org.apache.rocketmq.proxy.service.client.ClusterConsumerManager; +import org.apache.rocketmq.proxy.service.client.ProxyClientRemotingProcessor; +import org.apache.rocketmq.proxy.service.message.ClusterMessageService; +import org.apache.rocketmq.proxy.service.message.MessageService; +import org.apache.rocketmq.proxy.service.metadata.ClusterMetadataService; +import org.apache.rocketmq.proxy.service.metadata.MetadataService; +import org.apache.rocketmq.proxy.service.relay.ClusterProxyRelayService; +import org.apache.rocketmq.proxy.service.relay.ProxyRelayService; +import org.apache.rocketmq.proxy.service.route.ClusterTopicRouteService; +import org.apache.rocketmq.proxy.service.route.TopicRouteService; +import org.apache.rocketmq.proxy.service.transaction.ClusterTransactionService; +import org.apache.rocketmq.proxy.service.transaction.TransactionService; +import org.apache.rocketmq.remoting.RPCHook; + +public class ClusterServiceManager extends AbstractStartAndShutdown implements ServiceManager { + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + + protected ClusterTransactionService clusterTransactionService; + protected ProducerManager producerManager; + protected ClusterConsumerManager consumerManager; + protected TopicRouteService topicRouteService; + protected MessageService messageService; + protected ProxyRelayService proxyRelayService; + protected ClusterMetadataService metadataService; + protected AdminService adminService; + + protected ScheduledExecutorService scheduledExecutorService; + protected MQClientAPIFactory messagingClientAPIFactory; + protected MQClientAPIFactory operationClientAPIFactory; + protected MQClientAPIFactory transactionClientAPIFactory; + + public ClusterServiceManager(RPCHook rpcHook) { + ProxyConfig proxyConfig = ConfigurationManager.getProxyConfig(); + NameserverAccessConfig nameserverAccessConfig = new NameserverAccessConfig(proxyConfig.getNamesrvAddr(), + proxyConfig.getNamesrvDomain(), proxyConfig.getNamesrvDomainSubgroup()); + this.scheduledExecutorService = ThreadUtils.newScheduledThreadPool(3); + + this.messagingClientAPIFactory = new MQClientAPIFactory( + nameserverAccessConfig, + "ClusterMQClient_", + proxyConfig.getRocketmqMQClientNum(), + new DoNothingClientRemotingProcessor(null), + rpcHook, + scheduledExecutorService); + this.operationClientAPIFactory = new MQClientAPIFactory( + nameserverAccessConfig, + "OperationClient_", + 1, + new DoNothingClientRemotingProcessor(null), + rpcHook, + this.scheduledExecutorService + ); + + this.topicRouteService = new ClusterTopicRouteService(operationClientAPIFactory); + this.messageService = new ClusterMessageService(this.topicRouteService, this.messagingClientAPIFactory); + this.metadataService = new ClusterMetadataService(topicRouteService, operationClientAPIFactory); + this.adminService = new DefaultAdminService(this.operationClientAPIFactory); + + this.producerManager = new ProducerManager(); + this.consumerManager = new ClusterConsumerManager(this.topicRouteService, this.adminService, this.operationClientAPIFactory, new ConsumerIdsChangeListenerImpl(), proxyConfig.getChannelExpiredTimeout(), rpcHook); + + this.transactionClientAPIFactory = new MQClientAPIFactory( + nameserverAccessConfig, + "ClusterTransaction_", + 1, + new ProxyClientRemotingProcessor(producerManager), + rpcHook, + scheduledExecutorService); + this.clusterTransactionService = new ClusterTransactionService(this.topicRouteService, this.producerManager, + this.transactionClientAPIFactory); + this.proxyRelayService = new ClusterProxyRelayService(this.clusterTransactionService); + + this.init(); + } + + protected void init() { + this.producerManager.appendProducerChangeListener(new ProducerChangeListenerImpl()); + + this.scheduledExecutorService.scheduleAtFixedRate(() -> { + try { + producerManager.scanNotActiveChannel(); + consumerManager.scanNotActiveChannel(); + } catch (Throwable e) { + log.error("Error occurred when scan not active client channels.", e); + } + }, 1000 * 10, 1000 * 10, TimeUnit.MILLISECONDS); + + this.appendShutdown(scheduledExecutorService::shutdown); + this.appendStartAndShutdown(this.messagingClientAPIFactory); + this.appendStartAndShutdown(this.operationClientAPIFactory); + this.appendStartAndShutdown(this.transactionClientAPIFactory); + this.appendStartAndShutdown(this.topicRouteService); + this.appendStartAndShutdown(this.clusterTransactionService); + this.appendStartAndShutdown(this.metadataService); + this.appendStartAndShutdown(this.consumerManager); + } + + @Override + public MessageService getMessageService() { + return this.messageService; + } + + @Override + public TopicRouteService getTopicRouteService() { + return topicRouteService; + } + + @Override + public ProducerManager getProducerManager() { + return this.producerManager; + } + + @Override + public ConsumerManager getConsumerManager() { + return this.consumerManager; + } + + @Override + public TransactionService getTransactionService() { + return this.clusterTransactionService; + } + + @Override + public ProxyRelayService getProxyRelayService() { + return this.proxyRelayService; + } + + @Override + public MetadataService getMetadataService() { + return this.metadataService; + } + + @Override + public AdminService getAdminService() { + return this.adminService; + } + + protected static class ConsumerIdsChangeListenerImpl implements ConsumerIdsChangeListener { + + @Override + public void handle(ConsumerGroupEvent event, String group, Object... args) { + + } + + @Override + public void shutdown() { + + } + } + + protected class ProducerChangeListenerImpl implements ProducerChangeListener { + @Override + public void handle(ProducerGroupEvent event, String group, ClientChannelInfo clientChannelInfo) { + if (event == ProducerGroupEvent.GROUP_UNREGISTER) { + getTransactionService().unSubscribeAllTransactionTopic(ProxyContext.createForInner(this.getClass()), group); + } + } + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/LocalServiceManager.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/LocalServiceManager.java new file mode 100644 index 00000000000..59cd92685a3 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/LocalServiceManager.java @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.service; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.client.ConsumerManager; +import org.apache.rocketmq.broker.client.ProducerManager; +import org.apache.rocketmq.client.common.NameserverAccessConfig; +import org.apache.rocketmq.client.impl.mqclient.DoNothingClientRemotingProcessor; +import org.apache.rocketmq.client.impl.mqclient.MQClientAPIFactory; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.utils.AbstractStartAndShutdown; +import org.apache.rocketmq.common.utils.StartAndShutdown; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.ProxyConfig; +import org.apache.rocketmq.proxy.service.admin.AdminService; +import org.apache.rocketmq.proxy.service.admin.DefaultAdminService; +import org.apache.rocketmq.proxy.service.channel.ChannelManager; +import org.apache.rocketmq.proxy.service.message.LocalMessageService; +import org.apache.rocketmq.proxy.service.message.MessageService; +import org.apache.rocketmq.proxy.service.metadata.LocalMetadataService; +import org.apache.rocketmq.proxy.service.metadata.MetadataService; +import org.apache.rocketmq.proxy.service.relay.LocalProxyRelayService; +import org.apache.rocketmq.proxy.service.relay.ProxyRelayService; +import org.apache.rocketmq.proxy.service.route.LocalTopicRouteService; +import org.apache.rocketmq.proxy.service.route.TopicRouteService; +import org.apache.rocketmq.proxy.service.transaction.LocalTransactionService; +import org.apache.rocketmq.proxy.service.transaction.TransactionService; +import org.apache.rocketmq.remoting.RPCHook; + +public class LocalServiceManager extends AbstractStartAndShutdown implements ServiceManager { + + private final BrokerController brokerController; + private final TopicRouteService topicRouteService; + private final MessageService messageService; + private final TransactionService transactionService; + private final ProxyRelayService proxyRelayService; + private final MetadataService metadataService; + private final AdminService adminService; + + private final MQClientAPIFactory mqClientAPIFactory; + private final ChannelManager channelManager; + + private final ScheduledExecutorService scheduledExecutorService = ThreadUtils.newSingleThreadScheduledExecutor( + new ThreadFactoryImpl("LocalServiceManagerScheduledThread")); + + public LocalServiceManager(BrokerController brokerController, RPCHook rpcHook) { + this.brokerController = brokerController; + this.channelManager = new ChannelManager(); + this.messageService = new LocalMessageService(brokerController, channelManager, rpcHook); + ProxyConfig proxyConfig = ConfigurationManager.getProxyConfig(); + NameserverAccessConfig nameserverAccessConfig = new NameserverAccessConfig(proxyConfig.getNamesrvAddr(), + proxyConfig.getNamesrvDomain(), proxyConfig.getNamesrvDomainSubgroup()); + this.mqClientAPIFactory = new MQClientAPIFactory( + nameserverAccessConfig, + "LocalMQClient_", + 1, + new DoNothingClientRemotingProcessor(null), + rpcHook, + scheduledExecutorService + ); + this.topicRouteService = new LocalTopicRouteService(brokerController, mqClientAPIFactory); + this.transactionService = new LocalTransactionService(brokerController.getBrokerConfig()); + this.proxyRelayService = new LocalProxyRelayService(brokerController, this.transactionService); + this.metadataService = new LocalMetadataService(brokerController); + this.adminService = new DefaultAdminService(this.mqClientAPIFactory); + this.init(); + } + + protected void init() { + this.appendStartAndShutdown(this.mqClientAPIFactory); + this.appendStartAndShutdown(this.topicRouteService); + this.appendStartAndShutdown(new LocalServiceManagerStartAndShutdown()); + } + + @Override + public MessageService getMessageService() { + return this.messageService; + } + + @Override + public TopicRouteService getTopicRouteService() { + return this.topicRouteService; + } + + @Override + public ProducerManager getProducerManager() { + return this.brokerController.getProducerManager(); + } + + @Override + public ConsumerManager getConsumerManager() { + return this.brokerController.getConsumerManager(); + } + + @Override + public TransactionService getTransactionService() { + return this.transactionService; + } + + @Override + public ProxyRelayService getProxyRelayService() { + return this.proxyRelayService; + } + + @Override + public MetadataService getMetadataService() { + return this.metadataService; + } + + @Override + public AdminService getAdminService() { + return this.adminService; + } + + private class LocalServiceManagerStartAndShutdown implements StartAndShutdown { + @Override + public void start() throws Exception { + LocalServiceManager.this.scheduledExecutorService.scheduleWithFixedDelay(channelManager::scanAndCleanChannels, 5, 5, TimeUnit.MINUTES); + } + + @Override + public void shutdown() throws Exception { + LocalServiceManager.this.scheduledExecutorService.shutdown(); + } + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/ServiceManager.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/ServiceManager.java new file mode 100644 index 00000000000..c271eca0a11 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/ServiceManager.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.service; + +import org.apache.rocketmq.broker.client.ConsumerManager; +import org.apache.rocketmq.broker.client.ProducerManager; +import org.apache.rocketmq.common.utils.StartAndShutdown; +import org.apache.rocketmq.proxy.service.admin.AdminService; +import org.apache.rocketmq.proxy.service.message.MessageService; +import org.apache.rocketmq.proxy.service.metadata.MetadataService; +import org.apache.rocketmq.proxy.service.relay.ProxyRelayService; +import org.apache.rocketmq.proxy.service.route.TopicRouteService; +import org.apache.rocketmq.proxy.service.transaction.TransactionService; + +public interface ServiceManager extends StartAndShutdown { + MessageService getMessageService(); + + TopicRouteService getTopicRouteService(); + + ProducerManager getProducerManager(); + + ConsumerManager getConsumerManager(); + + TransactionService getTransactionService(); + + ProxyRelayService getProxyRelayService(); + + MetadataService getMetadataService(); + + AdminService getAdminService(); +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/ServiceManagerFactory.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/ServiceManagerFactory.java new file mode 100644 index 00000000000..c186752788d --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/ServiceManagerFactory.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.service; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.remoting.RPCHook; + +public class ServiceManagerFactory { + public static ServiceManager createForLocalMode(BrokerController brokerController) { + return createForLocalMode(brokerController, null); + } + + public static ServiceManager createForLocalMode(BrokerController brokerController, RPCHook rpcHook) { + return new LocalServiceManager(brokerController, rpcHook); + } + + public static ServiceManager createForClusterMode() { + return createForClusterMode(null); + } + + public static ServiceManager createForClusterMode(RPCHook rpcHook) { + return new ClusterServiceManager(rpcHook); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/admin/AdminService.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/admin/AdminService.java new file mode 100644 index 00000000000..a9e6686b438 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/admin/AdminService.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.admin; + +import java.util.List; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; + +public interface AdminService { + + boolean topicExist(String topic); + + boolean createTopicOnTopicBrokerIfNotExist(String createTopic, String sampleTopic, int wQueueNum, + int rQueueNum, boolean examineTopic, int retryCheckCount); + + boolean createTopicOnBroker(String topic, int wQueueNum, int rQueueNum, List curBrokerDataList, + List sampleBrokerDataList, boolean examineTopic, int retryCheckCount) throws Exception; +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/admin/DefaultAdminService.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/admin/DefaultAdminService.java new file mode 100644 index 00000000000..f3c68eab5c4 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/admin/DefaultAdminService.java @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.admin; + +import java.time.Duration; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.constant.PermName; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.client.impl.mqclient.MQClientAPIExt; +import org.apache.rocketmq.client.impl.mqclient.MQClientAPIFactory; +import org.apache.rocketmq.proxy.service.route.TopicRouteHelper; + +public class DefaultAdminService implements AdminService { + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + private final MQClientAPIFactory mqClientAPIFactory; + + public DefaultAdminService(MQClientAPIFactory mqClientAPIFactory) { + this.mqClientAPIFactory = mqClientAPIFactory; + } + + @Override + public boolean topicExist(String topic) { + boolean topicExist; + TopicRouteData topicRouteData; + try { + topicRouteData = this.getTopicRouteDataDirectlyFromNameServer(topic); + topicExist = topicRouteData != null; + } catch (Throwable e) { + topicExist = false; + } + + return topicExist; + } + + @Override + public boolean createTopicOnTopicBrokerIfNotExist(String createTopic, String sampleTopic, int wQueueNum, + int rQueueNum, boolean examineTopic, int retryCheckCount) { + TopicRouteData curTopicRouteData = new TopicRouteData(); + try { + curTopicRouteData = this.getTopicRouteDataDirectlyFromNameServer(createTopic); + } catch (Exception e) { + if (!TopicRouteHelper.isTopicNotExistError(e)) { + log.error("get cur topic route {} failed.", createTopic, e); + return false; + } + } + + TopicRouteData sampleTopicRouteData = null; + try { + sampleTopicRouteData = this.getTopicRouteDataDirectlyFromNameServer(sampleTopic); + } catch (Exception e) { + log.error("create topic {} failed.", createTopic, e); + return false; + } + + if (sampleTopicRouteData == null || sampleTopicRouteData.getBrokerDatas().isEmpty()) { + return false; + } + + try { + return this.createTopicOnBroker(createTopic, wQueueNum, rQueueNum, curTopicRouteData.getBrokerDatas(), + sampleTopicRouteData.getBrokerDatas(), examineTopic, retryCheckCount); + } catch (Exception e) { + log.error("create topic {} failed.", createTopic, e); + } + return false; + } + + @Override + public boolean createTopicOnBroker(String topic, int wQueueNum, int rQueueNum, List curBrokerDataList, + List sampleBrokerDataList, boolean examineTopic, int retryCheckCount) throws Exception { + Set curBrokerAddr = new HashSet<>(); + if (curBrokerDataList != null) { + for (BrokerData brokerData : curBrokerDataList) { + curBrokerAddr.add(brokerData.getBrokerAddrs().get(MixAll.MASTER_ID)); + } + } + + TopicConfig topicConfig = new TopicConfig(); + topicConfig.setTopicName(topic); + topicConfig.setWriteQueueNums(wQueueNum); + topicConfig.setReadQueueNums(rQueueNum); + topicConfig.setPerm(PermName.PERM_READ | PermName.PERM_WRITE); + + for (BrokerData brokerData : sampleBrokerDataList) { + String addr = brokerData.getBrokerAddrs() == null ? null : brokerData.getBrokerAddrs().get(MixAll.MASTER_ID); + if (addr == null) { + continue; + } + if (curBrokerAddr.contains(addr)) { + continue; + } + + try { + this.getClient().createTopic(addr, TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC, topicConfig, Duration.ofSeconds(3).toMillis()); + } catch (Exception e) { + log.error("create topic on broker failed. topic:{}, broker:{}", topicConfig, addr, e); + } + } + + if (examineTopic) { + // examine topic exist. + int count = retryCheckCount; + while (count-- > 0) { + if (this.topicExist(topic)) { + return true; + } + } + } else { + return true; + } + return false; + } + + protected TopicRouteData getTopicRouteDataDirectlyFromNameServer(String topic) throws Exception { + return this.getClient().getTopicRouteInfoFromNameServer(topic, Duration.ofSeconds(3).toMillis()); + } + + protected MQClientAPIExt getClient() { + return this.mqClientAPIFactory.getClient(); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/channel/ChannelManager.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/channel/ChannelManager.java new file mode 100644 index 00000000000..323c8c513b3 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/channel/ChannelManager.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.channel; + +import com.google.common.base.Strings; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.ConcurrentHashMapUtils; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class ChannelManager { + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + private final ConcurrentMap clientIdChannelMap = new ConcurrentHashMap<>(); + + public SimpleChannel createChannel(ProxyContext context) { + final String clientId = anonymousChannelId(context); + if (Strings.isNullOrEmpty(clientId)) { + log.warn("ClientId is unexpected null or empty"); + return createChannelInner(context); + } + SimpleChannel channel = ConcurrentHashMapUtils.computeIfAbsent(this.clientIdChannelMap,clientId, k -> createChannelInner(context)); + channel.updateLastAccessTime(); + return channel; + } + + public SimpleChannel createInvocationChannel(ProxyContext context) { + final String clientId = anonymousChannelId(InvocationChannel.class.getName(), context); + final String clientHost = context.getRemoteAddress(); + final String localAddress = context.getLocalAddress(); + if (Strings.isNullOrEmpty(clientId)) { + log.warn("ClientId is unexpected null or empty"); + return new InvocationChannel(clientHost, localAddress); + } + + SimpleChannel channel = clientIdChannelMap.computeIfAbsent(clientId, k -> new InvocationChannel(clientHost, localAddress)); + channel.updateLastAccessTime(); + return channel; + } + + private String anonymousChannelId(ProxyContext context) { + final String clientHost = context.getRemoteAddress(); + final String localAddress = context.getLocalAddress(); + return clientHost + "@" + localAddress; + } + + private String anonymousChannelId(String key, ProxyContext context) { + final String clientHost = context.getRemoteAddress(); + final String localAddress = context.getLocalAddress(); + return key + "@" + clientHost + "@" + localAddress; + } + + private SimpleChannel createChannelInner(ProxyContext context) { + return new SimpleChannel(context.getRemoteAddress(), context.getLocalAddress()); + } + + public void scanAndCleanChannels() { + try { + Iterator> iterator = clientIdChannelMap.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (!entry.getValue().isActive()) { + iterator.remove(); + } else { + entry.getValue().clearExpireContext(); + } + } + } catch (Throwable e) { + log.error("Unexpected exception", e); + } + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/channel/InvocationChannel.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/channel/InvocationChannel.java new file mode 100644 index 00000000000..00e8cea99c9 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/channel/InvocationChannel.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.channel; + +import io.netty.channel.ChannelFuture; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public class InvocationChannel extends SimpleChannel { + protected final ConcurrentMap inFlightRequestMap; + + public InvocationChannel(String remoteAddress, String localAddress) { + super(remoteAddress, localAddress); + this.inFlightRequestMap = new ConcurrentHashMap<>(); + } + + @Override + public ChannelFuture writeAndFlush(Object msg) { + if (msg instanceof RemotingCommand) { + RemotingCommand responseCommand = (RemotingCommand) msg; + InvocationContextInterface context = inFlightRequestMap.remove(responseCommand.getOpaque()); + if (null != context) { + context.handle(responseCommand); + } + inFlightRequestMap.remove(responseCommand.getOpaque()); + } + return super.writeAndFlush(msg); + } + + @Override + public boolean isWritable() { + return inFlightRequestMap.size() > 0; + } + + @Override + public void registerInvocationContext(int opaque, InvocationContextInterface context) { + inFlightRequestMap.put(opaque, context); + } + + @Override + public void eraseInvocationContext(int opaque) { + inFlightRequestMap.remove(opaque); + } + + @Override + public void clearExpireContext() { + Iterator> iterator = inFlightRequestMap.entrySet().iterator(); + int count = 0; + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (entry.getValue().expired(ConfigurationManager.getProxyConfig().getChannelExpiredInSeconds())) { + iterator.remove(); + count++; + log.debug("An expired request is found, request: {}", entry.getValue()); + } + } + if (count > 0) { + log.warn("[BUG] {} expired in-flight requests is cleaned.", count); + } + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/channel/InvocationContext.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/channel/InvocationContext.java new file mode 100644 index 00000000000..9fb488eb9b1 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/channel/InvocationContext.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.channel; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public class InvocationContext implements InvocationContextInterface { + private final CompletableFuture response; + private final long timestamp = System.currentTimeMillis(); + + public InvocationContext(CompletableFuture resp) { + this.response = resp; + } + + public boolean expired(long expiredTimeSec) { + return System.currentTimeMillis() - timestamp >= Duration.ofSeconds(expiredTimeSec).toMillis(); + } + + public CompletableFuture getResponse() { + return response; + } + + public void handle(RemotingCommand remotingCommand) { + response.complete(remotingCommand); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/channel/InvocationContextInterface.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/channel/InvocationContextInterface.java new file mode 100644 index 00000000000..0db9516486b --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/channel/InvocationContextInterface.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.channel; + +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public interface InvocationContextInterface { + void handle(RemotingCommand remotingCommand); + + boolean expired(long expiredTimeSec); +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/channel/SimpleChannel.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/channel/SimpleChannel.java new file mode 100644 index 00000000000..65c1fd40665 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/channel/SimpleChannel.java @@ -0,0 +1,210 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.channel; + +import com.google.common.base.Strings; +import io.netty.channel.AbstractChannel; +import io.netty.channel.Channel; +import io.netty.channel.ChannelConfig; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelId; +import io.netty.channel.ChannelMetadata; +import io.netty.channel.ChannelOutboundBuffer; +import io.netty.channel.DefaultChannelPromise; +import io.netty.channel.EventLoop; +import io.netty.util.concurrent.GlobalEventExecutor; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +/** + * SimpleChannel is used to handle writeAndFlush situation in processor + * + * @see io.netty.channel.ChannelHandlerContext#writeAndFlush + * @see io.netty.channel.Channel#writeAndFlush + */ +public class SimpleChannel extends AbstractChannel { + protected static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + + protected final String remoteAddress; + protected final String localAddress; + + protected long lastAccessTime; + protected ChannelHandlerContext channelHandlerContext; + + /** + * Creates a new instance. + * + * @param parent the parent of this channel. {@code null} if there's no parent. + * @param remoteAddress Remote address + * @param localAddress Local address + */ + public SimpleChannel(Channel parent, String remoteAddress, String localAddress) { + this(parent, null, remoteAddress, localAddress); + } + + public SimpleChannel(Channel parent, ChannelId id, String remoteAddress, String localAddress) { + super(parent, id); + lastAccessTime = System.currentTimeMillis(); + this.remoteAddress = remoteAddress; + this.localAddress = localAddress; + this.channelHandlerContext = new SimpleChannelHandlerContext(this); + } + + public SimpleChannel(String remoteAddress, String localAddress) { + this(null, remoteAddress, localAddress); + } + + @Override + protected AbstractUnsafe newUnsafe() { + return null; + } + + @Override + protected boolean isCompatible(EventLoop loop) { + return false; + } + + private static SocketAddress parseSocketAddress(String address) { + if (Strings.isNullOrEmpty(address)) { + return null; + } + + String[] segments = address.split(":"); + if (2 == segments.length) { + return new InetSocketAddress(segments[0], Integer.parseInt(segments[1])); + } + + return null; + } + + @Override + protected SocketAddress localAddress0() { + return parseSocketAddress(localAddress); + } + + @Override + public SocketAddress localAddress() { + return localAddress0(); + } + + @Override + public SocketAddress remoteAddress() { + return remoteAddress0(); + } + + @Override + protected SocketAddress remoteAddress0() { + return parseSocketAddress(remoteAddress); + } + + @Override + protected void doBind(SocketAddress localAddress) throws Exception { + + } + + @Override + protected void doDisconnect() throws Exception { + + } + + @Override + public ChannelFuture close() { + DefaultChannelPromise promise = new DefaultChannelPromise(this, GlobalEventExecutor.INSTANCE); + promise.setSuccess(); + return promise; + } + + @Override + protected void doClose() throws Exception { + + } + + @Override + protected void doBeginRead() throws Exception { + + } + + @Override + protected void doWrite(ChannelOutboundBuffer in) throws Exception { + + } + + @Override + public ChannelConfig config() { + return null; + } + + @Override + public boolean isOpen() { + return true; + } + + @Override + public boolean isActive() { + return (System.currentTimeMillis() - lastAccessTime) <= 120L * 1000; + } + + @Override + public ChannelMetadata metadata() { + return null; + } + + @Override + public ChannelFuture writeAndFlush(Object msg) { + DefaultChannelPromise promise = new DefaultChannelPromise(this, GlobalEventExecutor.INSTANCE); + promise.setSuccess(); + return promise; + } + + @Override + public boolean isWritable() { + return true; + } + + public void updateLastAccessTime() { + this.lastAccessTime = System.currentTimeMillis(); + } + + public void registerInvocationContext(int opaque, InvocationContextInterface context) { + + } + + public void eraseInvocationContext(int opaque) { + + } + + public void clearExpireContext() { + + } + + public String getRemoteAddress() { + return remoteAddress; + } + + public String getLocalAddress() { + return localAddress; + } + + public ChannelHandlerContext getChannelHandlerContext() { + return channelHandlerContext; + } +} \ No newline at end of file diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/channel/SimpleChannelHandlerContext.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/channel/SimpleChannelHandlerContext.java new file mode 100644 index 00000000000..801c62ee5fc --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/channel/SimpleChannelHandlerContext.java @@ -0,0 +1,246 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.channel; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelProgressivePromise; +import io.netty.channel.ChannelPromise; +import io.netty.util.Attribute; +import io.netty.util.AttributeKey; +import io.netty.util.concurrent.EventExecutor; +import java.net.SocketAddress; +import org.apache.commons.lang3.NotImplementedException; + +public class SimpleChannelHandlerContext implements ChannelHandlerContext { + + private final Channel channel; + + public SimpleChannelHandlerContext(Channel channel) { + this.channel = channel; + } + + @Override + public Channel channel() { + return channel; + } + + @Override + public EventExecutor executor() { + throw new NotImplementedException("Not implemented"); + } + + @Override + public String name() { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelHandler handler() { + throw new NotImplementedException("Not implemented"); + } + + @Override + public boolean isRemoved() { + return false; + } + + @Override + public ChannelHandlerContext fireChannelRegistered() { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelHandlerContext fireChannelUnregistered() { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelHandlerContext fireChannelActive() { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelHandlerContext fireChannelInactive() { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelHandlerContext fireExceptionCaught(Throwable cause) { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelHandlerContext fireUserEventTriggered(Object evt) { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelHandlerContext fireChannelRead(Object msg) { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelHandlerContext fireChannelReadComplete() { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelHandlerContext fireChannelWritabilityChanged() { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelFuture bind(SocketAddress localAddress) { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelFuture connect(SocketAddress remoteAddress) { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelFuture disconnect() { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelFuture close() { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelFuture deregister() { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelFuture disconnect(ChannelPromise promise) { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelFuture close(ChannelPromise promise) { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelFuture deregister(ChannelPromise promise) { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelHandlerContext read() { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelFuture write(Object msg) { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelFuture write(Object msg, ChannelPromise promise) { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelHandlerContext flush() { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) { + return channel.writeAndFlush(msg, promise); + } + + @Override + public ChannelFuture writeAndFlush(Object msg) { + return channel.writeAndFlush(msg); + } + + @Override + public ChannelPipeline pipeline() { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ByteBufAllocator alloc() { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelPromise newPromise() { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelProgressivePromise newProgressivePromise() { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelFuture newSucceededFuture() { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelFuture newFailedFuture(Throwable cause) { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ChannelPromise voidPromise() { + throw new NotImplementedException("Not implemented"); + } + + @Override + public Attribute attr(AttributeKey key) { + throw new NotImplementedException("Not implemented"); + } + + @Override + public boolean hasAttr(AttributeKey attributeKey) { + return false; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/client/ClusterConsumerManager.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/client/ClusterConsumerManager.java new file mode 100644 index 00000000000..65a4569f830 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/client/ClusterConsumerManager.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.client; + +import java.util.Set; +import org.apache.rocketmq.broker.client.ClientChannelInfo; +import org.apache.rocketmq.broker.client.ConsumerIdsChangeListener; +import org.apache.rocketmq.broker.client.ConsumerManager; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.utils.StartAndShutdown; +import org.apache.rocketmq.proxy.service.admin.AdminService; +import org.apache.rocketmq.client.impl.mqclient.MQClientAPIFactory; +import org.apache.rocketmq.proxy.service.route.TopicRouteService; +import org.apache.rocketmq.proxy.service.sysmessage.HeartbeatSyncer; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; + +public class ClusterConsumerManager extends ConsumerManager implements StartAndShutdown { + + protected HeartbeatSyncer heartbeatSyncer; + + public ClusterConsumerManager(TopicRouteService topicRouteService, AdminService adminService, + MQClientAPIFactory mqClientAPIFactory, ConsumerIdsChangeListener consumerIdsChangeListener, long channelExpiredTimeout, RPCHook rpcHook) { + super(consumerIdsChangeListener, channelExpiredTimeout); + this.heartbeatSyncer = new HeartbeatSyncer(topicRouteService, adminService, this, mqClientAPIFactory, rpcHook); + } + + @Override + public boolean registerConsumer(String group, ClientChannelInfo clientChannelInfo, + ConsumeType consumeType, MessageModel messageModel, ConsumeFromWhere consumeFromWhere, + Set subList, boolean isNotifyConsumerIdsChangedEnable, boolean updateSubscription) { + this.heartbeatSyncer.onConsumerRegister(group, clientChannelInfo, consumeType, messageModel, consumeFromWhere, subList); + return super.registerConsumer(group, clientChannelInfo, consumeType, messageModel, consumeFromWhere, subList, + isNotifyConsumerIdsChangedEnable, updateSubscription); + } + + @Override + public void unregisterConsumer(String group, ClientChannelInfo clientChannelInfo, + boolean isNotifyConsumerIdsChangedEnable) { + this.heartbeatSyncer.onConsumerUnRegister(group, clientChannelInfo); + super.unregisterConsumer(group, clientChannelInfo, isNotifyConsumerIdsChangedEnable); + } + + @Override + public void shutdown() throws Exception { + this.heartbeatSyncer.shutdown(); + } + + @Override + public void start() throws Exception { + this.heartbeatSyncer.start(); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/client/ProxyClientRemotingProcessor.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/client/ProxyClientRemotingProcessor.java new file mode 100644 index 00000000000..655ce7e64dd --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/client/ProxyClientRemotingProcessor.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.service.client; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import java.nio.ByteBuffer; +import org.apache.rocketmq.broker.client.ProducerManager; +import org.apache.rocketmq.client.impl.ClientRemotingProcessor; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.common.utils.ProxyUtils; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.header.CheckTransactionStateRequestHeader; + +public class ProxyClientRemotingProcessor extends ClientRemotingProcessor { + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + private final ProducerManager producerManager; + + public ProxyClientRemotingProcessor(ProducerManager producerManager) { + super(null); + this.producerManager = producerManager; + } + + @Override + public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) + throws RemotingCommandException { + if (request.getCode() == RequestCode.CHECK_TRANSACTION_STATE) { + return this.checkTransactionState(ctx, request); + } + return null; + } + + @Override + public RemotingCommand checkTransactionState(ChannelHandlerContext ctx, + RemotingCommand request) throws RemotingCommandException { + final ByteBuffer byteBuffer = ByteBuffer.wrap(request.getBody()); + final MessageExt messageExt = MessageDecoder.decode(byteBuffer, true, false, false); + if (messageExt != null) { + final String group = messageExt.getProperty(MessageConst.PROPERTY_PRODUCER_GROUP); + if (group != null) { + CheckTransactionStateRequestHeader requestHeader = + (CheckTransactionStateRequestHeader) request.decodeCommandCustomHeader(CheckTransactionStateRequestHeader.class); + request.writeCustomHeader(requestHeader); + request.addExtField(ProxyUtils.BROKER_ADDR, NetworkUtil.socketAddress2String(ctx.channel().remoteAddress())); + Channel channel = this.producerManager.getAvailableChannel(group); + if (channel != null) { + channel.writeAndFlush(request); + } else { + log.warn("check transaction failed, channel is empty. groupId={}, requestHeader:{}", group, requestHeader); + } + } + } + return null; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/message/ClusterMessageService.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/message/ClusterMessageService.java new file mode 100644 index 00000000000..70b72deae18 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/message/ClusterMessageService.java @@ -0,0 +1,261 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.service.message; + +import com.google.common.collect.Lists; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import org.apache.rocketmq.client.consumer.AckResult; +import org.apache.rocketmq.client.consumer.PopResult; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.impl.mqclient.MQClientAPIFactory; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.consumer.ReceiptHandle; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.common.ProxyException; +import org.apache.rocketmq.proxy.common.ProxyExceptionCode; +import org.apache.rocketmq.proxy.common.utils.FutureUtils; +import org.apache.rocketmq.proxy.service.route.AddressableMessageQueue; +import org.apache.rocketmq.proxy.service.route.TopicRouteService; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.body.LockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.body.UnlockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.header.AckMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ChangeInvisibleTimeRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ConsumerSendMsgBackRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.EndTransactionRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMaxOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMinOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PopMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumerOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateConsumerOffsetRequestHeader; + +public class ClusterMessageService implements MessageService { + protected final TopicRouteService topicRouteService; + protected final MQClientAPIFactory mqClientAPIFactory; + + public ClusterMessageService(TopicRouteService topicRouteService, MQClientAPIFactory mqClientAPIFactory) { + this.topicRouteService = topicRouteService; + this.mqClientAPIFactory = mqClientAPIFactory; + } + + @Override + public CompletableFuture> sendMessage(ProxyContext ctx, AddressableMessageQueue messageQueue, + List msgList, SendMessageRequestHeader requestHeader, long timeoutMillis) { + CompletableFuture> future; + if (msgList.size() == 1) { + future = this.mqClientAPIFactory.getClient().sendMessageAsync( + messageQueue.getBrokerAddr(), + messageQueue.getBrokerName(), msgList.get(0), requestHeader, timeoutMillis) + .thenApply(Lists::newArrayList); + } else { + future = this.mqClientAPIFactory.getClient().sendMessageAsync( + messageQueue.getBrokerAddr(), + messageQueue.getBrokerName(), msgList, requestHeader, timeoutMillis) + .thenApply(Lists::newArrayList); + } + return future; + } + + @Override + public CompletableFuture sendMessageBack(ProxyContext ctx, ReceiptHandle handle, String messageId, + ConsumerSendMsgBackRequestHeader requestHeader, long timeoutMillis) { + return this.mqClientAPIFactory.getClient().sendMessageBackAsync( + this.resolveBrokerAddrInReceiptHandle(ctx, handle), + requestHeader, + timeoutMillis + ); + } + + @Override + public CompletableFuture endTransactionOneway(ProxyContext ctx, String brokerName, + EndTransactionRequestHeader requestHeader, + long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + try { + this.mqClientAPIFactory.getClient().endTransactionOneway( + this.resolveBrokerAddr(ctx, brokerName), + requestHeader, + "end transaction from proxy", + timeoutMillis + ); + future.complete(null); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + @Override + public CompletableFuture popMessage(ProxyContext ctx, AddressableMessageQueue messageQueue, + PopMessageRequestHeader requestHeader, long timeoutMillis) { + return this.mqClientAPIFactory.getClient().popMessageAsync( + messageQueue.getBrokerAddr(), + messageQueue.getBrokerName(), + requestHeader, + timeoutMillis + ); + } + + @Override + public CompletableFuture changeInvisibleTime(ProxyContext ctx, ReceiptHandle handle, String messageId, + ChangeInvisibleTimeRequestHeader requestHeader, long timeoutMillis) { + return this.mqClientAPIFactory.getClient().changeInvisibleTimeAsync( + this.resolveBrokerAddrInReceiptHandle(ctx, handle), + handle.getBrokerName(), + requestHeader, + timeoutMillis + ); + } + + @Override + public CompletableFuture ackMessage(ProxyContext ctx, ReceiptHandle handle, String messageId, + AckMessageRequestHeader requestHeader, long timeoutMillis) { + return this.mqClientAPIFactory.getClient().ackMessageAsync( + this.resolveBrokerAddrInReceiptHandle(ctx, handle), + requestHeader, + timeoutMillis + ); + } + + @Override + public CompletableFuture batchAckMessage(ProxyContext ctx, List handleList, String consumerGroup, + String topic, long timeoutMillis) { + List extraInfoList = handleList.stream().map(message -> message.getReceiptHandle().getReceiptHandle()).collect(Collectors.toList()); + return this.mqClientAPIFactory.getClient().batchAckMessageAsync( + this.resolveBrokerAddrInReceiptHandle(ctx, handleList.get(0).getReceiptHandle()), + topic, + consumerGroup, + extraInfoList, + timeoutMillis + ); + } + + @Override + public CompletableFuture pullMessage(ProxyContext ctx, AddressableMessageQueue messageQueue, + PullMessageRequestHeader requestHeader, long timeoutMillis) { + return this.mqClientAPIFactory.getClient().pullMessageAsync( + messageQueue.getBrokerAddr(), + requestHeader, + timeoutMillis + ); + } + + @Override + public CompletableFuture queryConsumerOffset(ProxyContext ctx, AddressableMessageQueue messageQueue, + QueryConsumerOffsetRequestHeader requestHeader, long timeoutMillis) { + return this.mqClientAPIFactory.getClient().queryConsumerOffsetWithFuture( + messageQueue.getBrokerAddr(), + requestHeader, + timeoutMillis + ); + } + + @Override + public CompletableFuture updateConsumerOffset(ProxyContext ctx, AddressableMessageQueue messageQueue, + UpdateConsumerOffsetRequestHeader requestHeader, long timeoutMillis) { + return this.mqClientAPIFactory.getClient().updateConsumerOffsetOneWay( + messageQueue.getBrokerAddr(), + requestHeader, + timeoutMillis + ); + } + + @Override + public CompletableFuture> lockBatchMQ(ProxyContext ctx, AddressableMessageQueue messageQueue, + LockBatchRequestBody requestBody, long timeoutMillis) { + return this.mqClientAPIFactory.getClient().lockBatchMQWithFuture( + messageQueue.getBrokerAddr(), + requestBody, + timeoutMillis + ); + } + + @Override + public CompletableFuture unlockBatchMQ(ProxyContext ctx, AddressableMessageQueue messageQueue, + UnlockBatchRequestBody requestBody, long timeoutMillis) { + return this.mqClientAPIFactory.getClient().unlockBatchMQOneway( + messageQueue.getBrokerAddr(), + requestBody, + timeoutMillis + ); + } + + @Override + public CompletableFuture getMaxOffset(ProxyContext ctx, AddressableMessageQueue messageQueue, + GetMaxOffsetRequestHeader requestHeader, long timeoutMillis) { + return this.mqClientAPIFactory.getClient().getMaxOffset( + messageQueue.getBrokerAddr(), + requestHeader, + timeoutMillis + ); + } + + @Override + public CompletableFuture getMinOffset(ProxyContext ctx, AddressableMessageQueue messageQueue, + GetMinOffsetRequestHeader requestHeader, long timeoutMillis) { + return this.mqClientAPIFactory.getClient().getMinOffset( + messageQueue.getBrokerAddr(), + requestHeader, + timeoutMillis + ); + } + + @Override + public CompletableFuture request(ProxyContext ctx, String brokerName, RemotingCommand request, + long timeoutMillis) { + try { + String brokerAddress = topicRouteService.getBrokerAddr(ctx, brokerName); + return mqClientAPIFactory.getClient().invoke(brokerAddress, request, timeoutMillis); + } catch (Throwable t) { + return FutureUtils.completeExceptionally(t); + } + } + + @Override + public CompletableFuture requestOneway(ProxyContext ctx, String brokerName, RemotingCommand request, + long timeoutMillis) { + try { + String brokerAddress = topicRouteService.getBrokerAddr(ctx, brokerName); + return mqClientAPIFactory.getClient().invokeOneway(brokerAddress, request, timeoutMillis); + } catch (Throwable t) { + return FutureUtils.completeExceptionally(t); + } + } + + protected String resolveBrokerAddrInReceiptHandle(ProxyContext ctx, ReceiptHandle handle) { + try { + return this.topicRouteService.getBrokerAddr(ctx, handle.getBrokerName()); + } catch (Throwable t) { + throw new ProxyException(ProxyExceptionCode.INVALID_RECEIPT_HANDLE, "cannot find broker " + handle.getBrokerName(), t); + } + } + + protected String resolveBrokerAddr(ProxyContext ctx, String brokerName) { + try { + return this.topicRouteService.getBrokerAddr(ctx, brokerName); + } catch (Throwable t) { + throw new ProxyException(ProxyExceptionCode.INVALID_BROKER_NAME, "cannot find broker " + brokerName, t); + } + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/message/LocalMessageService.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/message/LocalMessageService.java new file mode 100644 index 00000000000..9181f966f4c --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/message/LocalMessageService.java @@ -0,0 +1,477 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.service.message; + +import io.netty.channel.ChannelHandlerContext; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import org.apache.commons.lang3.NotImplementedException; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.client.consumer.AckResult; +import org.apache.rocketmq.client.consumer.AckStatus; +import org.apache.rocketmq.client.consumer.PopResult; +import org.apache.rocketmq.client.consumer.PopStatus; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.consumer.ReceiptHandle; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageBatch; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.common.ProxyException; +import org.apache.rocketmq.proxy.common.ProxyExceptionCode; +import org.apache.rocketmq.proxy.service.channel.ChannelManager; +import org.apache.rocketmq.proxy.service.channel.InvocationContext; +import org.apache.rocketmq.proxy.service.channel.SimpleChannel; +import org.apache.rocketmq.proxy.service.route.AddressableMessageQueue; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.BatchAck; +import org.apache.rocketmq.remoting.protocol.body.BatchAckMessageRequestBody; +import org.apache.rocketmq.remoting.protocol.body.LockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.body.UnlockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.header.AckMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ChangeInvisibleTimeRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ChangeInvisibleTimeResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.ConsumerSendMsgBackRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.EndTransactionRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ExtraInfoUtil; +import org.apache.rocketmq.remoting.protocol.header.GetMaxOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMinOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PopMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PopMessageResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumerOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateConsumerOffsetRequestHeader; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class LocalMessageService implements MessageService { + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + private final BrokerController brokerController; + private final ChannelManager channelManager; + + public LocalMessageService(BrokerController brokerController, ChannelManager channelManager, RPCHook rpcHook) { + this.brokerController = brokerController; + this.channelManager = channelManager; + } + + @Override + public CompletableFuture> sendMessage(ProxyContext ctx, AddressableMessageQueue messageQueue, + List msgList, SendMessageRequestHeader requestHeader, long timeoutMillis) { + byte[] body; + String messageId; + if (msgList.size() > 1) { + requestHeader.setBatch(true); + MessageBatch msgBatch = MessageBatch.generateFromList(msgList); + MessageClientIDSetter.setUniqID(msgBatch); + body = msgBatch.encode(); + msgBatch.setBody(body); + messageId = MessageClientIDSetter.getUniqID(msgBatch); + } else { + Message message = msgList.get(0); + body = message.getBody(); + messageId = MessageClientIDSetter.getUniqID(message); + } + RemotingCommand request = LocalRemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, requestHeader, ctx.getLanguage()); + request.setBody(body); + CompletableFuture future = new CompletableFuture<>(); + SimpleChannel channel = channelManager.createInvocationChannel(ctx); + InvocationContext invocationContext = new InvocationContext(future); + channel.registerInvocationContext(request.getOpaque(), invocationContext); + ChannelHandlerContext simpleChannelHandlerContext = channel.getChannelHandlerContext(); + try { + RemotingCommand response = brokerController.getSendMessageProcessor().processRequest(simpleChannelHandlerContext, request); + if (response != null) { + invocationContext.handle(response); + channel.eraseInvocationContext(request.getOpaque()); + } + } catch (Exception e) { + future.completeExceptionally(e); + channel.eraseInvocationContext(request.getOpaque()); + log.error("Failed to process sendMessage command", e); + } + return future.thenApply(r -> { + SendResult sendResult = new SendResult(); + SendMessageResponseHeader responseHeader = (SendMessageResponseHeader) r.readCustomHeader(); + SendStatus sendStatus; + switch (r.getCode()) { + case ResponseCode.FLUSH_DISK_TIMEOUT: { + sendStatus = SendStatus.FLUSH_DISK_TIMEOUT; + break; + } + case ResponseCode.FLUSH_SLAVE_TIMEOUT: { + sendStatus = SendStatus.FLUSH_SLAVE_TIMEOUT; + break; + } + case ResponseCode.SLAVE_NOT_AVAILABLE: { + sendStatus = SendStatus.SLAVE_NOT_AVAILABLE; + break; + } + case ResponseCode.SUCCESS: { + sendStatus = SendStatus.SEND_OK; + break; + } + default: { + throw new ProxyException(ProxyExceptionCode.INTERNAL_SERVER_ERROR, r.getRemark()); + } + } + sendResult.setSendStatus(sendStatus); + sendResult.setMsgId(messageId); + sendResult.setMessageQueue(new MessageQueue(requestHeader.getTopic(), brokerController.getBrokerConfig().getBrokerName(), requestHeader.getQueueId())); + sendResult.setQueueOffset(responseHeader.getQueueOffset()); + sendResult.setTransactionId(responseHeader.getTransactionId()); + sendResult.setOffsetMsgId(responseHeader.getMsgId()); + return Collections.singletonList(sendResult); + }); + } + + @Override + public CompletableFuture sendMessageBack(ProxyContext ctx, ReceiptHandle handle, String messageId, + ConsumerSendMsgBackRequestHeader requestHeader, long timeoutMillis) { + SimpleChannel channel = channelManager.createChannel(ctx); + ChannelHandlerContext channelHandlerContext = channel.getChannelHandlerContext(); + RemotingCommand command = LocalRemotingCommand.createRequestCommand(RequestCode.CONSUMER_SEND_MSG_BACK, requestHeader, ctx.getLanguage()); + CompletableFuture future = new CompletableFuture<>(); + try { + RemotingCommand response = brokerController.getSendMessageProcessor() + .processRequest(channelHandlerContext, command); + future.complete(response); + } catch (Exception e) { + log.error("Fail to process sendMessageBack command", e); + future.completeExceptionally(e); + } + return future; + } + + @Override + public CompletableFuture endTransactionOneway(ProxyContext ctx, String brokerName, EndTransactionRequestHeader requestHeader, + long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + SimpleChannel channel = channelManager.createChannel(ctx); + ChannelHandlerContext channelHandlerContext = channel.getChannelHandlerContext(); + RemotingCommand command = LocalRemotingCommand.createRequestCommand(RequestCode.END_TRANSACTION, requestHeader, ctx.getLanguage()); + try { + brokerController.getEndTransactionProcessor() + .processRequest(channelHandlerContext, command); + future.complete(null); + } catch (Exception e) { + future.completeExceptionally(e); + } + return future; + } + + @Override + public CompletableFuture popMessage(ProxyContext ctx, AddressableMessageQueue messageQueue, + PopMessageRequestHeader requestHeader, long timeoutMillis) { + RemotingCommand request = LocalRemotingCommand.createRequestCommand(RequestCode.POP_MESSAGE, requestHeader, ctx.getLanguage()); + CompletableFuture future = new CompletableFuture<>(); + SimpleChannel channel = channelManager.createInvocationChannel(ctx); + InvocationContext invocationContext = new InvocationContext(future); + channel.registerInvocationContext(request.getOpaque(), invocationContext); + ChannelHandlerContext simpleChannelHandlerContext = channel.getChannelHandlerContext(); + try { + RemotingCommand response = brokerController.getPopMessageProcessor().processRequest(simpleChannelHandlerContext, request); + if (response != null) { + invocationContext.handle(response); + channel.eraseInvocationContext(request.getOpaque()); + } + } catch (Exception e) { + future.completeExceptionally(e); + channel.eraseInvocationContext(request.getOpaque()); + log.error("Failed to process popMessage command", e); + } + return future.thenApply(r -> { + PopStatus popStatus; + List messageExtList = new ArrayList<>(); + switch (r.getCode()) { + case ResponseCode.SUCCESS: + popStatus = PopStatus.FOUND; + ByteBuffer byteBuffer = ByteBuffer.wrap(r.getBody()); + messageExtList = MessageDecoder.decodesBatch( + byteBuffer, + true, + false, + true + ); + break; + case ResponseCode.POLLING_FULL: + popStatus = PopStatus.POLLING_FULL; + break; + case ResponseCode.POLLING_TIMEOUT: + case ResponseCode.PULL_NOT_FOUND: + popStatus = PopStatus.POLLING_NOT_FOUND; + break; + default: + throw new ProxyException(ProxyExceptionCode.INTERNAL_SERVER_ERROR, r.getRemark()); + } + PopResult popResult = new PopResult(popStatus, messageExtList); + PopMessageResponseHeader responseHeader = (PopMessageResponseHeader) r.readCustomHeader(); + + if (popStatus == PopStatus.FOUND) { + Map startOffsetInfo; + Map> msgOffsetInfo; + Map orderCountInfo; + popResult.setInvisibleTime(responseHeader.getInvisibleTime()); + popResult.setPopTime(responseHeader.getPopTime()); + startOffsetInfo = ExtraInfoUtil.parseStartOffsetInfo(responseHeader.getStartOffsetInfo()); + msgOffsetInfo = ExtraInfoUtil.parseMsgOffsetInfo(responseHeader.getMsgOffsetInfo()); + orderCountInfo = ExtraInfoUtil.parseOrderCountInfo(responseHeader.getOrderCountInfo()); + // + Map> sortMap = new HashMap<>(16); + for (MessageExt messageExt : messageExtList) { + // Value of POP_CK is used to determine whether it is a pop retry, + // cause topic could be rewritten by broker. + String key = ExtraInfoUtil.getStartOffsetInfoMapKey(messageExt.getTopic(), + messageExt.getProperty(MessageConst.PROPERTY_POP_CK), messageExt.getQueueId()); + if (!sortMap.containsKey(key)) { + sortMap.put(key, new ArrayList<>(4)); + } + sortMap.get(key).add(messageExt.getQueueOffset()); + } + Map map = new HashMap<>(5); + for (MessageExt messageExt : messageExtList) { + if (startOffsetInfo == null) { + // we should set the check point info to extraInfo field , if the command is popMsg + // find pop ck offset + String key = messageExt.getTopic() + messageExt.getQueueId(); + if (!map.containsKey(messageExt.getTopic() + messageExt.getQueueId())) { + map.put(key, ExtraInfoUtil.buildExtraInfo(messageExt.getQueueOffset(), responseHeader.getPopTime(), responseHeader.getInvisibleTime(), responseHeader.getReviveQid(), + messageExt.getTopic(), messageQueue.getBrokerName(), messageExt.getQueueId())); + } + messageExt.getProperties().put(MessageConst.PROPERTY_POP_CK, map.get(key) + MessageConst.KEY_SEPARATOR + messageExt.getQueueOffset()); + } else { + if (messageExt.getProperty(MessageConst.PROPERTY_POP_CK) == null) { + String key = ExtraInfoUtil.getStartOffsetInfoMapKey(messageExt.getTopic(), messageExt.getQueueId()); + int index = sortMap.get(key).indexOf(messageExt.getQueueOffset()); + Long msgQueueOffset = msgOffsetInfo.get(key).get(index); + if (msgQueueOffset != messageExt.getQueueOffset()) { + log.warn("Queue offset [{}] of msg is strange, not equal to the stored in msg, {}", msgQueueOffset, messageExt); + } + + messageExt.getProperties().put(MessageConst.PROPERTY_POP_CK, + ExtraInfoUtil.buildExtraInfo(startOffsetInfo.get(key), responseHeader.getPopTime(), responseHeader.getInvisibleTime(), + responseHeader.getReviveQid(), messageExt.getTopic(), messageQueue.getBrokerName(), messageExt.getQueueId(), msgQueueOffset) + ); + if (requestHeader.isOrder() && orderCountInfo != null) { + Integer count = orderCountInfo.get(key); + if (count != null && count > 0) { + messageExt.setReconsumeTimes(count); + } + } + } + } + messageExt.getProperties().computeIfAbsent(MessageConst.PROPERTY_FIRST_POP_TIME, k -> String.valueOf(responseHeader.getPopTime())); + messageExt.setBrokerName(messageExt.getBrokerName()); + messageExt.setTopic(messageQueue.getTopic()); + } + } + return popResult; + }); + } + + @Override + public CompletableFuture changeInvisibleTime(ProxyContext ctx, ReceiptHandle handle, String messageId, + ChangeInvisibleTimeRequestHeader requestHeader, long timeoutMillis) { + SimpleChannel channel = channelManager.createChannel(ctx); + ChannelHandlerContext channelHandlerContext = channel.getChannelHandlerContext(); + RemotingCommand command = LocalRemotingCommand.createRequestCommand(RequestCode.CHANGE_MESSAGE_INVISIBLETIME, requestHeader, ctx.getLanguage()); + CompletableFuture future = new CompletableFuture<>(); + try { + RemotingCommand response = brokerController.getChangeInvisibleTimeProcessor() + .processRequest(channelHandlerContext, command); + future.complete(response); + } catch (Exception e) { + log.error("Fail to process changeInvisibleTime command", e); + future.completeExceptionally(e); + } + return future.thenApply(r -> { + ChangeInvisibleTimeResponseHeader responseHeader = (ChangeInvisibleTimeResponseHeader) r.readCustomHeader(); + AckResult ackResult = new AckResult(); + if (ResponseCode.SUCCESS == r.getCode()) { + ackResult.setStatus(AckStatus.OK); + } else { + ackResult.setStatus(AckStatus.NO_EXIST); + } + ackResult.setPopTime(responseHeader.getPopTime()); + ackResult.setExtraInfo(ReceiptHandle.builder() + .startOffset(handle.getStartOffset()) + .retrieveTime(responseHeader.getPopTime()) + .invisibleTime(responseHeader.getInvisibleTime()) + .reviveQueueId(responseHeader.getReviveQid()) + .topicType(handle.getTopicType()) + .brokerName(handle.getBrokerName()) + .queueId(handle.getQueueId()) + .offset(handle.getOffset()) + .build() + .encode()); + return ackResult; + }); + } + + @Override + public CompletableFuture ackMessage(ProxyContext ctx, ReceiptHandle handle, String messageId, + AckMessageRequestHeader requestHeader, long timeoutMillis) { + SimpleChannel channel = channelManager.createChannel(ctx); + ChannelHandlerContext channelHandlerContext = channel.getChannelHandlerContext(); + RemotingCommand command = LocalRemotingCommand.createRequestCommand(RequestCode.ACK_MESSAGE, requestHeader, ctx.getLanguage()); + CompletableFuture future = new CompletableFuture<>(); + try { + RemotingCommand response = brokerController.getAckMessageProcessor() + .processRequest(channelHandlerContext, command); + future.complete(response); + } catch (Exception e) { + log.error("Fail to process ackMessage command", e); + future.completeExceptionally(e); + } + return future.thenApply(r -> { + AckResult ackResult = new AckResult(); + if (ResponseCode.SUCCESS == r.getCode()) { + ackResult.setStatus(AckStatus.OK); + } else { + ackResult.setStatus(AckStatus.NO_EXIST); + } + return ackResult; + }); + } + + @Override + public CompletableFuture batchAckMessage(ProxyContext ctx, List handleList, + String consumerGroup, String topic, long timeoutMillis) { + SimpleChannel channel = channelManager.createChannel(ctx); + ChannelHandlerContext channelHandlerContext = channel.getChannelHandlerContext(); + RemotingCommand command = LocalRemotingCommand.createRequestCommand(RequestCode.BATCH_ACK_MESSAGE, null); + + Map batchAckMap = new HashMap<>(); + for (ReceiptHandleMessage receiptHandleMessage : handleList) { + String extraInfo = receiptHandleMessage.getReceiptHandle().getReceiptHandle(); + String[] extraInfoData = ExtraInfoUtil.split(extraInfo); + String mergeKey = ExtraInfoUtil.getRetry(extraInfoData) + "@" + + ExtraInfoUtil.getQueueId(extraInfoData) + "@" + + ExtraInfoUtil.getCkQueueOffset(extraInfoData) + "@" + + ExtraInfoUtil.getPopTime(extraInfoData); + BatchAck bAck = batchAckMap.computeIfAbsent(mergeKey, k -> { + BatchAck newBatchAck = new BatchAck(); + newBatchAck.setConsumerGroup(consumerGroup); + newBatchAck.setTopic(topic); + newBatchAck.setRetry(ExtraInfoUtil.getRetry(extraInfoData)); + newBatchAck.setStartOffset(ExtraInfoUtil.getCkQueueOffset(extraInfoData)); + newBatchAck.setQueueId(ExtraInfoUtil.getQueueId(extraInfoData)); + newBatchAck.setReviveQueueId(ExtraInfoUtil.getReviveQid(extraInfoData)); + newBatchAck.setPopTime(ExtraInfoUtil.getPopTime(extraInfoData)); + newBatchAck.setInvisibleTime(ExtraInfoUtil.getInvisibleTime(extraInfoData)); + newBatchAck.setBitSet(new BitSet()); + return newBatchAck; + }); + bAck.getBitSet().set((int) (ExtraInfoUtil.getQueueOffset(extraInfoData) - ExtraInfoUtil.getCkQueueOffset(extraInfoData))); + } + BatchAckMessageRequestBody requestBody = new BatchAckMessageRequestBody(); + requestBody.setBrokerName(brokerController.getBrokerConfig().getBrokerName()); + requestBody.setAcks(new ArrayList<>(batchAckMap.values())); + + command.setBody(requestBody.encode()); + CompletableFuture future = new CompletableFuture<>(); + try { + RemotingCommand response = brokerController.getAckMessageProcessor() + .processRequest(channelHandlerContext, command); + future.complete(response); + } catch (Exception e) { + log.error("Fail to process batchAckMessage command", e); + future.completeExceptionally(e); + } + return future.thenApply(r -> { + AckResult ackResult = new AckResult(); + if (ResponseCode.SUCCESS == r.getCode()) { + ackResult.setStatus(AckStatus.OK); + } else { + ackResult.setStatus(AckStatus.NO_EXIST); + } + return ackResult; + }); + } + + @Override + public CompletableFuture pullMessage(ProxyContext ctx, AddressableMessageQueue messageQueue, + PullMessageRequestHeader requestHeader, long timeoutMillis) { + throw new NotImplementedException("pullMessage is not implemented in LocalMessageService"); + } + + @Override + public CompletableFuture queryConsumerOffset(ProxyContext ctx, AddressableMessageQueue messageQueue, + QueryConsumerOffsetRequestHeader requestHeader, long timeoutMillis) { + throw new NotImplementedException("queryConsumerOffset is not implemented in LocalMessageService"); + } + + @Override + public CompletableFuture updateConsumerOffset(ProxyContext ctx, AddressableMessageQueue messageQueue, + UpdateConsumerOffsetRequestHeader requestHeader, long timeoutMillis) { + throw new NotImplementedException("updateConsumerOffset is not implemented in LocalMessageService"); + } + + @Override + public CompletableFuture> lockBatchMQ(ProxyContext ctx, AddressableMessageQueue messageQueue, + LockBatchRequestBody requestBody, long timeoutMillis) { + throw new NotImplementedException("lockBatchMQ is not implemented in LocalMessageService"); + } + + @Override + public CompletableFuture unlockBatchMQ(ProxyContext ctx, AddressableMessageQueue messageQueue, + UnlockBatchRequestBody requestBody, long timeoutMillis) { + throw new NotImplementedException("unlockBatchMQ is not implemented in LocalMessageService"); + } + + @Override + public CompletableFuture getMaxOffset(ProxyContext ctx, AddressableMessageQueue messageQueue, + GetMaxOffsetRequestHeader requestHeader, long timeoutMillis) { + throw new NotImplementedException("getMaxOffset is not implemented in LocalMessageService"); + } + + @Override + public CompletableFuture getMinOffset(ProxyContext ctx, AddressableMessageQueue messageQueue, + GetMinOffsetRequestHeader requestHeader, long timeoutMillis) { + throw new NotImplementedException("getMinOffset is not implemented in LocalMessageService"); + } + + @Override + public CompletableFuture request(ProxyContext ctx, String brokerName, RemotingCommand request, + long timeoutMillis) { + throw new NotImplementedException("request is not implemented in LocalMessageService"); + } + + @Override + public CompletableFuture requestOneway(ProxyContext ctx, String brokerName, RemotingCommand request, + long timeoutMillis) { + throw new NotImplementedException("requestOneway is not implemented in LocalMessageService"); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/message/LocalRemotingCommand.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/message/LocalRemotingCommand.java new file mode 100644 index 00000000000..7bf4a169820 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/message/LocalRemotingCommand.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.service.message; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.LanguageCode; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +import java.util.HashMap; + +public class LocalRemotingCommand extends RemotingCommand { + + public static LocalRemotingCommand createRequestCommand(int code, CommandCustomHeader customHeader, String language) { + LocalRemotingCommand cmd = new LocalRemotingCommand(); + cmd.setCode(code); + cmd.setLanguage(LanguageCode.getCode(language)); + cmd.writeCustomHeader(customHeader); + cmd.setExtFields(new HashMap<>()); + setCmdVersion(cmd); + cmd.makeCustomHeaderToNet(); + return cmd; + } + + @Override + public CommandCustomHeader decodeCommandCustomHeader( + Class classHeader) throws RemotingCommandException { + return classHeader.cast(readCustomHeader()); + } + +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/message/MessageService.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/message/MessageService.java new file mode 100644 index 00000000000..58a835adb46 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/message/MessageService.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.service.message; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.client.consumer.AckResult; +import org.apache.rocketmq.client.consumer.PopResult; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.consumer.ReceiptHandle; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.service.route.AddressableMessageQueue; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.body.LockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.body.UnlockBatchRequestBody; +import org.apache.rocketmq.remoting.protocol.header.AckMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ChangeInvisibleTimeRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ConsumerSendMsgBackRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.EndTransactionRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMaxOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMinOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PopMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumerOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateConsumerOffsetRequestHeader; + +public interface MessageService { + + CompletableFuture> sendMessage( + ProxyContext ctx, + AddressableMessageQueue messageQueue, + List msgList, + SendMessageRequestHeader requestHeader, + long timeoutMillis + ); + + CompletableFuture sendMessageBack( + ProxyContext ctx, + ReceiptHandle handle, + String messageId, + ConsumerSendMsgBackRequestHeader requestHeader, + long timeoutMillis + ); + + CompletableFuture endTransactionOneway( + ProxyContext ctx, + String brokerName, + EndTransactionRequestHeader requestHeader, + long timeoutMillis + ); + + CompletableFuture popMessage( + ProxyContext ctx, + AddressableMessageQueue messageQueue, + PopMessageRequestHeader requestHeader, + long timeoutMillis + ); + + CompletableFuture changeInvisibleTime( + ProxyContext ctx, + ReceiptHandle handle, + String messageId, + ChangeInvisibleTimeRequestHeader requestHeader, + long timeoutMillis + ); + + CompletableFuture ackMessage( + ProxyContext ctx, + ReceiptHandle handle, + String messageId, + AckMessageRequestHeader requestHeader, + long timeoutMillis + ); + + CompletableFuture batchAckMessage( + ProxyContext ctx, + List handleList, + String consumerGroup, + String topic, + long timeoutMillis + ); + + CompletableFuture pullMessage( + ProxyContext ctx, + AddressableMessageQueue messageQueue, + PullMessageRequestHeader requestHeader, + long timeoutMillis + ); + + CompletableFuture queryConsumerOffset( + ProxyContext ctx, + AddressableMessageQueue messageQueue, + QueryConsumerOffsetRequestHeader requestHeader, + long timeoutMillis + ); + + CompletableFuture updateConsumerOffset( + ProxyContext ctx, + AddressableMessageQueue messageQueue, + UpdateConsumerOffsetRequestHeader requestHeader, + long timeoutMillis + ); + + CompletableFuture> lockBatchMQ( + ProxyContext ctx, + AddressableMessageQueue messageQueue, + LockBatchRequestBody requestBody, + long timeoutMillis + ); + + CompletableFuture unlockBatchMQ( + ProxyContext ctx, + AddressableMessageQueue messageQueue, + UnlockBatchRequestBody requestBody, + long timeoutMillis + ); + + CompletableFuture getMaxOffset( + ProxyContext ctx, + AddressableMessageQueue messageQueue, + GetMaxOffsetRequestHeader requestHeader, + long timeoutMillis + ); + + CompletableFuture getMinOffset( + ProxyContext ctx, + AddressableMessageQueue messageQueue, + GetMinOffsetRequestHeader requestHeader, + long timeoutMillis + ); + + CompletableFuture request(ProxyContext ctx, String brokerName, RemotingCommand request, + long timeoutMillis); + + CompletableFuture requestOneway(ProxyContext ctx, String brokerName, RemotingCommand request, + long timeoutMillis); +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/message/ReceiptHandleMessage.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/message/ReceiptHandleMessage.java new file mode 100644 index 00000000000..ae63fed491e --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/message/ReceiptHandleMessage.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.message; + +import org.apache.rocketmq.common.consumer.ReceiptHandle; + +public class ReceiptHandleMessage { + + private final ReceiptHandle receiptHandle; + private final String messageId; + + public ReceiptHandleMessage(ReceiptHandle receiptHandle, String messageId) { + this.receiptHandle = receiptHandle; + this.messageId = messageId; + } + + public ReceiptHandle getReceiptHandle() { + return receiptHandle; + } + + public String getMessageId() { + return messageId; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/metadata/ClusterMetadataService.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/metadata/ClusterMetadataService.java new file mode 100644 index 00000000000..d34a0efd9e1 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/metadata/ClusterMetadataService.java @@ -0,0 +1,172 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.metadata; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.LoadingCache; +import java.util.Optional; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.common.attribute.TopicMessageType; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.thread.ThreadPoolMonitor; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.common.AbstractCacheLoader; +import org.apache.rocketmq.common.utils.AbstractStartAndShutdown; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.ProxyConfig; +import org.apache.rocketmq.client.impl.mqclient.MQClientAPIFactory; +import org.apache.rocketmq.proxy.service.route.TopicRouteHelper; +import org.apache.rocketmq.proxy.service.route.TopicRouteService; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicConfigAndQueueMapping; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; + +public class ClusterMetadataService extends AbstractStartAndShutdown implements MetadataService { + protected static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + private static final long DEFAULT_TIMEOUT = 3000; + + private final TopicRouteService topicRouteService; + private final MQClientAPIFactory mqClientAPIFactory; + + protected final ThreadPoolExecutor cacheRefreshExecutor; + + protected final LoadingCache topicConfigCache; + protected final static TopicConfigAndQueueMapping EMPTY_TOPIC_CONFIG = new TopicConfigAndQueueMapping(); + + protected final LoadingCache subscriptionGroupConfigCache; + protected final static SubscriptionGroupConfig EMPTY_SUBSCRIPTION_GROUP_CONFIG = new SubscriptionGroupConfig(); + + public ClusterMetadataService(TopicRouteService topicRouteService, MQClientAPIFactory mqClientAPIFactory) { + this.topicRouteService = topicRouteService; + this.mqClientAPIFactory = mqClientAPIFactory; + + ProxyConfig config = ConfigurationManager.getProxyConfig(); + this.cacheRefreshExecutor = ThreadPoolMonitor.createAndMonitor( + config.getMetadataThreadPoolNums(), + config.getMetadataThreadPoolNums(), + 1000 * 60, + TimeUnit.MILLISECONDS, + "MetadataCacheRefresh", + config.getMetadataThreadPoolQueueCapacity() + ); + this.topicConfigCache = CacheBuilder.newBuilder() + .maximumSize(config.getTopicConfigCacheMaxNum()) + .expireAfterAccess(config.getTopicConfigCacheExpiredSeconds(), TimeUnit.SECONDS) + .refreshAfterWrite(config.getTopicConfigCacheRefreshSeconds(), TimeUnit.SECONDS) + .build(new ClusterTopicConfigCacheLoader()); + this.subscriptionGroupConfigCache = CacheBuilder.newBuilder() + .maximumSize(config.getSubscriptionGroupConfigCacheMaxNum()) + .expireAfterAccess(config.getSubscriptionGroupConfigCacheExpiredSeconds(), TimeUnit.SECONDS) + .refreshAfterWrite(config.getSubscriptionGroupConfigCacheRefreshSeconds(), TimeUnit.SECONDS) + .build(new ClusterSubscriptionGroupConfigCacheLoader()); + + this.init(); + } + + protected void init() { + this.appendShutdown(this.cacheRefreshExecutor::shutdown); + } + + @Override + public TopicMessageType getTopicMessageType(ProxyContext ctx, String topic) { + TopicConfigAndQueueMapping topicConfigAndQueueMapping; + try { + topicConfigAndQueueMapping = topicConfigCache.get(topic); + } catch (Exception e) { + return TopicMessageType.UNSPECIFIED; + } + if (topicConfigAndQueueMapping.equals(EMPTY_TOPIC_CONFIG)) { + return TopicMessageType.UNSPECIFIED; + } + return topicConfigAndQueueMapping.getTopicMessageType(); + } + + @Override + public SubscriptionGroupConfig getSubscriptionGroupConfig(ProxyContext ctx, String group) { + SubscriptionGroupConfig config; + try { + config = this.subscriptionGroupConfigCache.get(group); + } catch (Exception e) { + return null; + } + if (config == EMPTY_SUBSCRIPTION_GROUP_CONFIG) { + return null; + } + return config; + } + + protected class ClusterSubscriptionGroupConfigCacheLoader extends AbstractCacheLoader { + + public ClusterSubscriptionGroupConfigCacheLoader() { + super(cacheRefreshExecutor); + } + + @Override + protected SubscriptionGroupConfig getDirectly(String consumerGroup) throws Exception { + ProxyConfig config = ConfigurationManager.getProxyConfig(); + String clusterName = config.getRocketMQClusterName(); + Optional brokerDataOptional = findOneBroker(clusterName); + if (brokerDataOptional.isPresent()) { + String brokerAddress = brokerDataOptional.get().selectBrokerAddr(); + return mqClientAPIFactory.getClient().getSubscriptionGroupConfig(brokerAddress, consumerGroup, DEFAULT_TIMEOUT); + } + return EMPTY_SUBSCRIPTION_GROUP_CONFIG; + } + + @Override + protected void onErr(String consumerGroup, Exception e) { + log.error("load subscription config failed. consumerGroup:{}", consumerGroup, e); + } + } + + protected class ClusterTopicConfigCacheLoader extends AbstractCacheLoader { + + public ClusterTopicConfigCacheLoader() { + super(cacheRefreshExecutor); + } + + @Override + protected TopicConfigAndQueueMapping getDirectly(String topic) throws Exception { + Optional brokerDataOptional = findOneBroker(topic); + if (brokerDataOptional.isPresent()) { + String brokerAddress = brokerDataOptional.get().selectBrokerAddr(); + return mqClientAPIFactory.getClient().getTopicConfig(brokerAddress, topic, DEFAULT_TIMEOUT); + } + return EMPTY_TOPIC_CONFIG; + } + + @Override + protected void onErr(String key, Exception e) { + log.error("load topic config failed. topic:{}", key, e); + } + } + + protected Optional findOneBroker(String topic) throws Exception { + try { + return topicRouteService.getAllMessageQueueView(ProxyContext.createForInner(this.getClass()), topic).getTopicRouteData().getBrokerDatas().stream().findAny(); + } catch (Exception e) { + if (TopicRouteHelper.isTopicNotExistError(e)) { + return Optional.empty(); + } + throw e; + } + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/metadata/LocalMetadataService.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/metadata/LocalMetadataService.java new file mode 100644 index 00000000000..7f3c041f259 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/metadata/LocalMetadataService.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.metadata; + +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.attribute.TopicMessageType; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; + +public class LocalMetadataService implements MetadataService { + private final BrokerController brokerController; + + public LocalMetadataService(BrokerController brokerController) { + this.brokerController = brokerController; + } + + @Override + public TopicMessageType getTopicMessageType(ProxyContext ctx, String topic) { + TopicConfig topicConfig = brokerController.getTopicConfigManager().selectTopicConfig(topic); + if (topicConfig == null) { + return TopicMessageType.UNSPECIFIED; + } + return topicConfig.getTopicMessageType(); + } + + @Override + public SubscriptionGroupConfig getSubscriptionGroupConfig(ProxyContext ctx, String group) { + return this.brokerController.getSubscriptionGroupManager().getSubscriptionGroupTable().get(group); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/metadata/MetadataService.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/metadata/MetadataService.java new file mode 100644 index 00000000000..3ee0f3eacd3 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/metadata/MetadataService.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.metadata; + +import org.apache.rocketmq.common.attribute.TopicMessageType; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; + +public interface MetadataService { + + TopicMessageType getTopicMessageType(ProxyContext ctx, String topic); + + SubscriptionGroupConfig getSubscriptionGroupConfig(ProxyContext ctx, String group); +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManager.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManager.java new file mode 100644 index 00000000000..207603fe811 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManager.java @@ -0,0 +1,282 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.receipt; + +import com.google.common.base.Stopwatch; +import io.netty.channel.Channel; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.broker.client.ClientChannelInfo; +import org.apache.rocketmq.broker.client.ConsumerGroupEvent; +import org.apache.rocketmq.broker.client.ConsumerIdsChangeListener; +import org.apache.rocketmq.broker.client.ConsumerManager; +import org.apache.rocketmq.client.consumer.AckResult; +import org.apache.rocketmq.client.consumer.AckStatus; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.consumer.ReceiptHandle; +import org.apache.rocketmq.common.state.StateEventListener; +import org.apache.rocketmq.common.thread.ThreadPoolMonitor; +import org.apache.rocketmq.common.utils.AbstractStartAndShutdown; +import org.apache.rocketmq.common.utils.ConcurrentHashMapUtils; +import org.apache.rocketmq.common.utils.StartAndShutdown; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.common.MessageReceiptHandle; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.common.ProxyException; +import org.apache.rocketmq.proxy.common.ProxyExceptionCode; +import org.apache.rocketmq.proxy.common.ReceiptHandleGroup; +import org.apache.rocketmq.proxy.common.ReceiptHandleGroupKey; +import org.apache.rocketmq.proxy.common.RenewEvent; +import org.apache.rocketmq.proxy.common.RenewStrategyPolicy; +import org.apache.rocketmq.proxy.common.channel.ChannelHelper; +import org.apache.rocketmq.proxy.common.utils.ExceptionUtils; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.ProxyConfig; +import org.apache.rocketmq.proxy.service.metadata.MetadataService; +import org.apache.rocketmq.remoting.protocol.subscription.RetryPolicy; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; + +public class DefaultReceiptHandleManager extends AbstractStartAndShutdown implements ReceiptHandleManager { + protected final static Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + protected final MetadataService metadataService; + protected final ConsumerManager consumerManager; + protected final ConcurrentMap receiptHandleGroupMap; + protected final StateEventListener eventListener; + protected final static RetryPolicy RENEW_POLICY = new RenewStrategyPolicy(); + protected final ScheduledExecutorService scheduledExecutorService = + ThreadUtils.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("RenewalScheduledThread_")); + protected final ThreadPoolExecutor renewalWorkerService; + + public DefaultReceiptHandleManager(MetadataService metadataService, ConsumerManager consumerManager, StateEventListener eventListener) { + this.metadataService = metadataService; + this.consumerManager = consumerManager; + this.eventListener = eventListener; + ProxyConfig proxyConfig = ConfigurationManager.getProxyConfig(); + this.renewalWorkerService = ThreadPoolMonitor.createAndMonitor( + proxyConfig.getRenewThreadPoolNums(), + proxyConfig.getRenewMaxThreadPoolNums(), + 1, TimeUnit.MINUTES, + "RenewalWorkerThread", + proxyConfig.getRenewThreadPoolQueueCapacity() + ); + consumerManager.appendConsumerIdsChangeListener(new ConsumerIdsChangeListener() { + @Override + public void handle(ConsumerGroupEvent event, String group, Object... args) { + if (ConsumerGroupEvent.CLIENT_UNREGISTER.equals(event)) { + if (args == null || args.length < 1) { + return; + } + if (args[0] instanceof ClientChannelInfo) { + ClientChannelInfo clientChannelInfo = (ClientChannelInfo) args[0]; + if (ChannelHelper.isRemote(clientChannelInfo.getChannel())) { + // if the channel sync from other proxy is expired, not to clear data of connect to current proxy + return; + } + clearGroup(new ReceiptHandleGroupKey(clientChannelInfo.getChannel(), group)); + log.info("clear handle of this client when client unregister. group:{}, clientChannelInfo:{}", group, clientChannelInfo); + } + } + } + + @Override + public void shutdown() { + + } + }); + this.receiptHandleGroupMap = new ConcurrentHashMap<>(); + this.renewalWorkerService.setRejectedExecutionHandler((r, executor) -> log.warn("add renew task failed. queueSize:{}", executor.getQueue().size())); + this.appendStartAndShutdown(new StartAndShutdown() { + @Override + public void start() throws Exception { + scheduledExecutorService.scheduleWithFixedDelay(() -> scheduleRenewTask(), 0, + ConfigurationManager.getProxyConfig().getRenewSchedulePeriodMillis(), TimeUnit.MILLISECONDS); + } + + @Override + public void shutdown() throws Exception { + scheduledExecutorService.shutdown(); + clearAllHandle(); + } + }); + } + + public void addReceiptHandle(ProxyContext context, Channel channel, String group, String msgID, MessageReceiptHandle messageReceiptHandle) { + ConcurrentHashMapUtils.computeIfAbsent(this.receiptHandleGroupMap, new ReceiptHandleGroupKey(channel, group), + k -> new ReceiptHandleGroup()).put(msgID, messageReceiptHandle); + } + + public MessageReceiptHandle removeReceiptHandle(ProxyContext context, Channel channel, String group, String msgID, String receiptHandle) { + ReceiptHandleGroup handleGroup = receiptHandleGroupMap.get(new ReceiptHandleGroupKey(channel, group)); + if (handleGroup == null) { + return null; + } + return handleGroup.remove(msgID, receiptHandle); + } + + protected boolean clientIsOffline(ReceiptHandleGroupKey groupKey) { + return this.consumerManager.findChannel(groupKey.getGroup(), groupKey.getChannel()) == null; + } + + protected void scheduleRenewTask() { + Stopwatch stopwatch = Stopwatch.createStarted(); + try { + ProxyConfig proxyConfig = ConfigurationManager.getProxyConfig(); + for (Map.Entry entry : receiptHandleGroupMap.entrySet()) { + ReceiptHandleGroupKey key = entry.getKey(); + if (clientIsOffline(key)) { + clearGroup(key); + continue; + } + + ReceiptHandleGroup group = entry.getValue(); + group.scan((msgID, handleStr, v) -> { + long current = System.currentTimeMillis(); + ReceiptHandle handle = ReceiptHandle.decode(v.getReceiptHandleStr()); + if (handle.getNextVisibleTime() - current > proxyConfig.getRenewAheadTimeMillis()) { + return; + } + renewalWorkerService.submit(() -> renewMessage(key, group, msgID, handleStr)); + }); + } + } catch (Exception e) { + log.error("unexpect error when schedule renew task", e); + } + + log.debug("scan for renewal done. cost:{}ms", stopwatch.elapsed().toMillis()); + } + + protected void renewMessage(ReceiptHandleGroupKey key, ReceiptHandleGroup group, String msgID, String handleStr) { + try { + group.computeIfPresent(msgID, handleStr, messageReceiptHandle -> startRenewMessage(key, messageReceiptHandle)); + } catch (Exception e) { + log.error("error when renew message. msgID:{}, handleStr:{}", msgID, handleStr, e); + } + } + + protected CompletableFuture startRenewMessage(ReceiptHandleGroupKey key, MessageReceiptHandle messageReceiptHandle) { + CompletableFuture resFuture = new CompletableFuture<>(); + ProxyConfig proxyConfig = ConfigurationManager.getProxyConfig(); + long current = System.currentTimeMillis(); + try { + if (messageReceiptHandle.getRenewRetryTimes() >= proxyConfig.getMaxRenewRetryTimes()) { + log.warn("handle has exceed max renewRetryTimes. handle:{}", messageReceiptHandle); + return CompletableFuture.completedFuture(null); + } + if (current - messageReceiptHandle.getConsumeTimestamp() < proxyConfig.getRenewMaxTimeMillis()) { + CompletableFuture future = new CompletableFuture<>(); + eventListener.fireEvent(new RenewEvent(key, messageReceiptHandle, RENEW_POLICY.nextDelayDuration(messageReceiptHandle.getRenewTimes()), RenewEvent.EventType.RENEW, future)); + future.whenComplete((ackResult, throwable) -> { + if (throwable != null) { + log.error("error when renew. handle:{}", messageReceiptHandle, throwable); + if (renewExceptionNeedRetry(throwable)) { + messageReceiptHandle.incrementAndGetRenewRetryTimes(); + resFuture.complete(messageReceiptHandle); + } else { + resFuture.complete(null); + } + } else if (AckStatus.OK.equals(ackResult.getStatus())) { + messageReceiptHandle.updateReceiptHandle(ackResult.getExtraInfo()); + messageReceiptHandle.resetRenewRetryTimes(); + messageReceiptHandle.incrementRenewTimes(); + resFuture.complete(messageReceiptHandle); + } else { + log.error("renew response is not ok. result:{}, handle:{}", ackResult, messageReceiptHandle); + resFuture.complete(null); + } + }); + } else { + ProxyContext context = createContext("RenewMessage"); + SubscriptionGroupConfig subscriptionGroupConfig = + metadataService.getSubscriptionGroupConfig(context, messageReceiptHandle.getGroup()); + if (subscriptionGroupConfig == null) { + log.error("group's subscriptionGroupConfig is null when renew. handle: {}", messageReceiptHandle); + return CompletableFuture.completedFuture(null); + } + RetryPolicy retryPolicy = subscriptionGroupConfig.getGroupRetryPolicy().getRetryPolicy(); + CompletableFuture future = new CompletableFuture<>(); + eventListener.fireEvent(new RenewEvent(key, messageReceiptHandle, retryPolicy.nextDelayDuration(messageReceiptHandle.getReconsumeTimes()), RenewEvent.EventType.STOP_RENEW, future)); + future.whenComplete((ackResult, throwable) -> { + if (throwable != null) { + log.error("error when nack in renew. handle:{}", messageReceiptHandle, throwable); + } + resFuture.complete(null); + }); + } + } catch (Throwable t) { + log.error("unexpect error when renew message, stop to renew it. handle:{}", messageReceiptHandle, t); + resFuture.complete(null); + } + return resFuture; + } + + protected void clearGroup(ReceiptHandleGroupKey key) { + if (key == null) { + return; + } + ProxyConfig proxyConfig = ConfigurationManager.getProxyConfig(); + ReceiptHandleGroup handleGroup = receiptHandleGroupMap.remove(key); + if (handleGroup == null) { + return; + } + handleGroup.scan((msgID, handle, v) -> { + try { + handleGroup.computeIfPresent(msgID, handle, messageReceiptHandle -> { + CompletableFuture future = new CompletableFuture<>(); + eventListener.fireEvent(new RenewEvent(key, messageReceiptHandle, proxyConfig.getInvisibleTimeMillisWhenClear(), RenewEvent.EventType.CLEAR_GROUP, future)); + return CompletableFuture.completedFuture(null); + }); + } catch (Exception e) { + log.error("error when clear handle for group. key:{}", key, e); + } + }); + } + + protected void clearAllHandle() { + log.info("start clear all handle in receiptHandleProcessor"); + Set keySet = receiptHandleGroupMap.keySet(); + for (ReceiptHandleGroupKey key : keySet) { + clearGroup(key); + } + log.info("clear all handle in receiptHandleProcessor done"); + } + + protected boolean renewExceptionNeedRetry(Throwable t) { + t = ExceptionUtils.getRealException(t); + if (t instanceof ProxyException) { + ProxyException proxyException = (ProxyException) t; + if (ProxyExceptionCode.INVALID_BROKER_NAME.equals(proxyException.getCode()) || + ProxyExceptionCode.INVALID_RECEIPT_HANDLE.equals(proxyException.getCode())) { + return false; + } + } + return true; + } + + protected ProxyContext createContext(String actionName) { + return ProxyContext.createForInner(this.getClass().getSimpleName() + actionName); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/ReceiptHandleManager.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/ReceiptHandleManager.java new file mode 100644 index 00000000000..6a8888e97ef --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/receipt/ReceiptHandleManager.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.receipt; + +import io.netty.channel.Channel; +import org.apache.rocketmq.proxy.common.MessageReceiptHandle; +import org.apache.rocketmq.proxy.common.ProxyContext; + +public interface ReceiptHandleManager { + void addReceiptHandle(ProxyContext context, Channel channel, String group, String msgID, MessageReceiptHandle messageReceiptHandle); + + MessageReceiptHandle removeReceiptHandle(ProxyContext context, Channel channel, String group, String msgID, String receiptHandle); +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/relay/AbstractProxyRelayService.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/relay/AbstractProxyRelayService.java new file mode 100644 index 00000000000..08f00bd83c0 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/relay/AbstractProxyRelayService.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.relay; + +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.common.ProxyException; +import org.apache.rocketmq.proxy.common.ProxyExceptionCode; +import org.apache.rocketmq.proxy.common.utils.ProxyUtils; +import org.apache.rocketmq.proxy.service.transaction.TransactionData; +import org.apache.rocketmq.proxy.service.transaction.TransactionService; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.header.CheckTransactionStateRequestHeader; + +public abstract class AbstractProxyRelayService implements ProxyRelayService { + + protected final TransactionService transactionService; + + public AbstractProxyRelayService(TransactionService transactionService) { + this.transactionService = transactionService; + } + + @Override + public RelayData processCheckTransactionState(ProxyContext context, + RemotingCommand command, CheckTransactionStateRequestHeader header, MessageExt messageExt) { + CompletableFuture> future = new CompletableFuture<>(); + String group = messageExt.getProperty(MessageConst.PROPERTY_PRODUCER_GROUP); + TransactionData transactionData = transactionService.addTransactionDataByBrokerAddr( + context, + command.getExtFields().get(ProxyUtils.BROKER_ADDR), + group, + header.getTranStateTableOffset(), + header.getCommitLogOffset(), + header.getTransactionId(), + messageExt); + if (transactionData == null) { + throw new ProxyException(ProxyExceptionCode.INTERNAL_SERVER_ERROR, + String.format("add transaction data failed. request:%s, message:%s", command, messageExt)); + } + future.exceptionally(throwable -> { + this.transactionService.onSendCheckTransactionStateFailed(context, group, transactionData); + return null; + }); + return new RelayData<>(transactionData, future); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/relay/ClusterProxyRelayService.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/relay/ClusterProxyRelayService.java new file mode 100644 index 00000000000..71ce222a8c0 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/relay/ClusterProxyRelayService.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.service.relay; + +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.service.transaction.TransactionService; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.header.ConsumeMessageDirectlyResultRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerRunningInfoRequestHeader; + +/** + * not implement yet + */ +public class ClusterProxyRelayService extends AbstractProxyRelayService { + + public ClusterProxyRelayService(TransactionService transactionService) { + super(transactionService); + } + + @Override + public CompletableFuture> processGetConsumerRunningInfo( + ProxyContext context, RemotingCommand command, + GetConsumerRunningInfoRequestHeader header) { + return null; + } + + @Override + public CompletableFuture> processConsumeMessageDirectly( + ProxyContext context, RemotingCommand command, + ConsumeMessageDirectlyResultRequestHeader header) { + return null; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/relay/LocalProxyRelayService.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/relay/LocalProxyRelayService.java new file mode 100644 index 00000000000..9fcc27fc53d --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/relay/LocalProxyRelayService.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.service.relay; + +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.service.channel.SimpleChannel; +import org.apache.rocketmq.proxy.service.transaction.TransactionService; +import org.apache.rocketmq.remoting.RemotingServer; +import org.apache.rocketmq.remoting.netty.NettyRemotingAbstract; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.header.ConsumeMessageDirectlyResultRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerRunningInfoRequestHeader; + +public class LocalProxyRelayService extends AbstractProxyRelayService { + + private final BrokerController brokerController; + + public LocalProxyRelayService(BrokerController brokerController, TransactionService transactionService) { + super(transactionService); + this.brokerController = brokerController; + } + + @Override + public CompletableFuture> processGetConsumerRunningInfo( + ProxyContext context, RemotingCommand command, GetConsumerRunningInfoRequestHeader header) { + CompletableFuture> future = new CompletableFuture<>(); + future.thenAccept(proxyOutResult -> { + RemotingServer remotingServer = this.brokerController.getRemotingServer(); + if (remotingServer instanceof NettyRemotingAbstract) { + NettyRemotingAbstract nettyRemotingAbstract = (NettyRemotingAbstract) remotingServer; + RemotingCommand remotingCommand = RemotingCommand.createResponseCommand(null); + remotingCommand.setOpaque(command.getOpaque()); + remotingCommand.setCode(proxyOutResult.getCode()); + remotingCommand.setRemark(proxyOutResult.getRemark()); + if (proxyOutResult.getCode() == ResponseCode.SUCCESS && proxyOutResult.getResult() != null) { + ConsumerRunningInfo consumerRunningInfo = proxyOutResult.getResult(); + remotingCommand.setBody(consumerRunningInfo.encode()); + } + SimpleChannel simpleChannel = new SimpleChannel(context.getRemoteAddress(), context.getLocalAddress()); + nettyRemotingAbstract.processResponseCommand(simpleChannel.getChannelHandlerContext(), remotingCommand); + } + }); + return future; + } + + @Override + public CompletableFuture> processConsumeMessageDirectly( + ProxyContext context, RemotingCommand command, + ConsumeMessageDirectlyResultRequestHeader header) { + CompletableFuture> future = new CompletableFuture<>(); + future.thenAccept(proxyOutResult -> { + RemotingServer remotingServer = this.brokerController.getRemotingServer(); + if (remotingServer instanceof NettyRemotingAbstract) { + NettyRemotingAbstract nettyRemotingAbstract = (NettyRemotingAbstract) remotingServer; + RemotingCommand remotingCommand = RemotingCommand.createResponseCommand(null); + remotingCommand.setOpaque(command.getOpaque()); + remotingCommand.setCode(proxyOutResult.getCode()); + remotingCommand.setRemark(proxyOutResult.getRemark()); + if (proxyOutResult.getCode() == ResponseCode.SUCCESS && proxyOutResult.getResult() != null) { + ConsumeMessageDirectlyResult consumeMessageDirectlyResult = proxyOutResult.getResult(); + remotingCommand.setBody(consumeMessageDirectlyResult.encode()); + } + SimpleChannel simpleChannel = new SimpleChannel(context.getRemoteAddress(), context.getLocalAddress()); + nettyRemotingAbstract.processResponseCommand(simpleChannel.getChannelHandlerContext(), remotingCommand); + } + }); + return future; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/relay/ProxyChannel.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/relay/ProxyChannel.java new file mode 100644 index 00000000000..5a1185a81e8 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/relay/ProxyChannel.java @@ -0,0 +1,200 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.relay; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelConfig; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelId; +import io.netty.channel.ChannelMetadata; +import io.netty.channel.ChannelOutboundBuffer; +import io.netty.channel.DefaultChannelPromise; +import io.netty.channel.EventLoop; +import io.netty.util.concurrent.GlobalEventExecutor; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.service.channel.SimpleChannel; +import org.apache.rocketmq.proxy.service.transaction.TransactionData; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.header.CheckTransactionStateRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ConsumeMessageDirectlyResultRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerRunningInfoRequestHeader; + +public abstract class ProxyChannel extends SimpleChannel { + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + protected final SocketAddress remoteSocketAddress; + protected final SocketAddress localSocketAddress; + + protected final ProxyRelayService proxyRelayService; + + protected ProxyChannel(ProxyRelayService proxyRelayService, Channel parent, String remoteAddress, + String localAddress) { + super(parent, remoteAddress, localAddress); + this.proxyRelayService = proxyRelayService; + this.remoteSocketAddress = NetworkUtil.string2SocketAddress(remoteAddress); + this.localSocketAddress = NetworkUtil.string2SocketAddress(localAddress); + } + + protected ProxyChannel(ProxyRelayService proxyRelayService, Channel parent, ChannelId id, String remoteAddress, + String localAddress) { + super(parent, id, remoteAddress, localAddress); + this.proxyRelayService = proxyRelayService; + this.remoteSocketAddress = NetworkUtil.string2SocketAddress(remoteAddress); + this.localSocketAddress = NetworkUtil.string2SocketAddress(localAddress); + } + + @Override + public ChannelFuture writeAndFlush(Object msg) { + CompletableFuture processFuture = new CompletableFuture<>(); + + try { + if (msg instanceof RemotingCommand) { + ProxyContext context = ProxyContext.createForInner(this.getClass()) + .setRemoteAddress(remoteAddress) + .setLocalAddress(localAddress); + RemotingCommand command = (RemotingCommand) msg; + if (command.getExtFields() == null) { + command.setExtFields(new HashMap<>()); + } + switch (command.getCode()) { + case RequestCode.CHECK_TRANSACTION_STATE: { + CheckTransactionStateRequestHeader header = (CheckTransactionStateRequestHeader) command.readCustomHeader(); + MessageExt messageExt = MessageDecoder.decode(ByteBuffer.wrap(command.getBody()), true, false, false); + RelayData relayData = this.proxyRelayService.processCheckTransactionState(context, command, header, messageExt); + processFuture = this.processCheckTransaction(header, messageExt, relayData.getProcessResult(), relayData.getRelayFuture()); + break; + } + case RequestCode.GET_CONSUMER_RUNNING_INFO: { + GetConsumerRunningInfoRequestHeader header = (GetConsumerRunningInfoRequestHeader) command.readCustomHeader(); + CompletableFuture> relayFuture = this.proxyRelayService.processGetConsumerRunningInfo(context, command, header); + processFuture = this.processGetConsumerRunningInfo(command, header, relayFuture); + break; + } + case RequestCode.CONSUME_MESSAGE_DIRECTLY: { + ConsumeMessageDirectlyResultRequestHeader header = (ConsumeMessageDirectlyResultRequestHeader) command.readCustomHeader(); + MessageExt messageExt = MessageDecoder.decode(ByteBuffer.wrap(command.getBody()), true, false, false); + processFuture = this.processConsumeMessageDirectly(command, header, messageExt, + this.proxyRelayService.processConsumeMessageDirectly(context, command, header)); + break; + } + default: + break; + } + } else { + processFuture = processOtherMessage(msg); + } + } catch (Throwable t) { + log.error("process failed. msg:{}", msg, t); + processFuture.completeExceptionally(t); + } + + DefaultChannelPromise promise = new DefaultChannelPromise(this, GlobalEventExecutor.INSTANCE); + processFuture.thenAccept(ignore -> promise.setSuccess()) + .exceptionally(t -> { + promise.setFailure(t); + return null; + }); + return promise; + } + + protected abstract CompletableFuture processOtherMessage(Object msg); + + protected abstract CompletableFuture processCheckTransaction( + CheckTransactionStateRequestHeader header, + MessageExt messageExt, + TransactionData transactionData, + CompletableFuture> responseFuture); + + protected abstract CompletableFuture processGetConsumerRunningInfo( + RemotingCommand command, + GetConsumerRunningInfoRequestHeader header, + CompletableFuture> responseFuture); + + protected abstract CompletableFuture processConsumeMessageDirectly( + RemotingCommand command, + ConsumeMessageDirectlyResultRequestHeader header, + MessageExt messageExt, + CompletableFuture> responseFuture); + + @Override + public ChannelConfig config() { + return null; + } + + @Override + public ChannelMetadata metadata() { + return null; + } + + @Override + protected AbstractUnsafe newUnsafe() { + return null; + } + + @Override + protected boolean isCompatible(EventLoop loop) { + return false; + } + + @Override + protected void doBind(SocketAddress localAddress) throws Exception { + + } + + @Override + protected void doDisconnect() throws Exception { + + } + + @Override + protected void doClose() throws Exception { + + } + + @Override + protected void doBeginRead() throws Exception { + + } + + @Override + protected void doWrite(ChannelOutboundBuffer in) throws Exception { + + } + + @Override + protected SocketAddress localAddress0() { + return this.localSocketAddress; + } + + @Override + protected SocketAddress remoteAddress0() { + return this.remoteSocketAddress; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/relay/ProxyRelayResult.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/relay/ProxyRelayResult.java new file mode 100644 index 00000000000..95b98d4d6b1 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/relay/ProxyRelayResult.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.relay; + +public class ProxyRelayResult { + private int code; + private String remark; + private T result; + + public ProxyRelayResult(int code, String remark, T result) { + this.code = code; + this.remark = remark; + this.result = result; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public T getResult() { + return result; + } + + public void setResult(T result) { + this.result = result; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/relay/ProxyRelayService.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/relay/ProxyRelayService.java new file mode 100644 index 00000000000..96d3b699578 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/relay/ProxyRelayService.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.service.relay; + +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.service.transaction.TransactionData; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.header.CheckTransactionStateRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ConsumeMessageDirectlyResultRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerRunningInfoRequestHeader; + +public interface ProxyRelayService { + + CompletableFuture> processGetConsumerRunningInfo( + ProxyContext context, + RemotingCommand command, + GetConsumerRunningInfoRequestHeader header + ); + + CompletableFuture> processConsumeMessageDirectly( + ProxyContext context, + RemotingCommand command, + ConsumeMessageDirectlyResultRequestHeader header + ); + + RelayData processCheckTransactionState( + ProxyContext context, + RemotingCommand command, + CheckTransactionStateRequestHeader header, + MessageExt messageExt + ); +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/relay/RelayData.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/relay/RelayData.java new file mode 100644 index 00000000000..20ee0f5fdfa --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/relay/RelayData.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.relay; + +import java.util.concurrent.CompletableFuture; + +public class RelayData { + private T processResult; + private CompletableFuture> relayFuture; + + public RelayData(T processResult, CompletableFuture> relayFuture) { + this.processResult = processResult; + this.relayFuture = relayFuture; + } + + public CompletableFuture> getRelayFuture() { + return relayFuture; + } + + public void setRelayFuture( + CompletableFuture> relayFuture) { + this.relayFuture = relayFuture; + } + + public T getProcessResult() { + return processResult; + } + + public void setProcessResult(T processResult) { + this.processResult = processResult; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/AddressableMessageQueue.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/AddressableMessageQueue.java new file mode 100644 index 00000000000..ca877f3278f --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/AddressableMessageQueue.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.service.route; + +import com.google.common.base.MoreObjects; +import java.util.Objects; +import org.apache.rocketmq.common.message.MessageQueue; + +public class AddressableMessageQueue implements Comparable { + + private final MessageQueue messageQueue; + private final String brokerAddr; + + public AddressableMessageQueue(MessageQueue messageQueue, String brokerAddr) { + this.messageQueue = messageQueue; + this.brokerAddr = brokerAddr; + } + + @Override + public int compareTo(AddressableMessageQueue o) { + return messageQueue.compareTo(o.messageQueue); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof AddressableMessageQueue)) { + return false; + } + AddressableMessageQueue queue = (AddressableMessageQueue) o; + return Objects.equals(messageQueue, queue.messageQueue); + } + + @Override + public int hashCode() { + return messageQueue == null ? 1 : messageQueue.hashCode(); + } + + public int getQueueId() { + return this.messageQueue.getQueueId(); + } + + public String getBrokerName() { + return this.messageQueue.getBrokerName(); + } + + public String getTopic() { + return messageQueue.getTopic(); + } + + public MessageQueue getMessageQueue() { + return messageQueue; + } + + public String getBrokerAddr() { + return brokerAddr; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("messageQueue", messageQueue) + .add("brokerAddr", brokerAddr) + .toString(); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/ClusterTopicRouteService.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/ClusterTopicRouteService.java new file mode 100644 index 00000000000..84252f8b8e7 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/ClusterTopicRouteService.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.service.route; + +import java.util.List; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.proxy.common.Address; +import org.apache.rocketmq.client.impl.mqclient.MQClientAPIFactory; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; + +public class ClusterTopicRouteService extends TopicRouteService { + + public ClusterTopicRouteService(MQClientAPIFactory mqClientAPIFactory) { + super(mqClientAPIFactory); + } + + @Override + public MessageQueueView getCurrentMessageQueueView(ProxyContext ctx, String topicName) throws Exception { + return getAllMessageQueueView(ctx, topicName); + } + + @Override + public ProxyTopicRouteData getTopicRouteForProxy(ProxyContext ctx, List

    requestHostAndPortList, + String topicName) throws Exception { + TopicRouteData topicRouteData = getAllMessageQueueView(ctx, topicName).getTopicRouteData(); + + ProxyTopicRouteData proxyTopicRouteData = new ProxyTopicRouteData(); + proxyTopicRouteData.setQueueDatas(topicRouteData.getQueueDatas()); + + for (BrokerData brokerData : topicRouteData.getBrokerDatas()) { + ProxyTopicRouteData.ProxyBrokerData proxyBrokerData = new ProxyTopicRouteData.ProxyBrokerData(); + proxyBrokerData.setCluster(brokerData.getCluster()); + proxyBrokerData.setBrokerName(brokerData.getBrokerName()); + for (Long brokerId : brokerData.getBrokerAddrs().keySet()) { + proxyBrokerData.getBrokerAddrs().put(brokerId, requestHostAndPortList); + } + proxyTopicRouteData.getBrokerDatas().add(proxyBrokerData); + } + + return proxyTopicRouteData; + } + + @Override + public String getBrokerAddr(ProxyContext ctx, String brokerName) throws Exception { + TopicRouteWrapper topicRouteWrapper = getAllMessageQueueView(ctx, brokerName).getTopicRouteWrapper(); + return topicRouteWrapper.getMasterAddr(brokerName); + } + + @Override + public AddressableMessageQueue buildAddressableMessageQueue(ProxyContext ctx, MessageQueue messageQueue) throws Exception { + String brokerAddress = getBrokerAddr(ctx, messageQueue.getBrokerName()); + return new AddressableMessageQueue(messageQueue, brokerAddress); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/LocalTopicRouteService.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/LocalTopicRouteService.java new file mode 100644 index 00000000000..aced15cee51 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/LocalTopicRouteService.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.service.route; + +import com.google.common.collect.Lists; +import com.google.common.net.HostAndPort; +import java.util.HashMap; +import java.util.List; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.proxy.common.Address; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.client.impl.mqclient.MQClientAPIFactory; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; + +public class LocalTopicRouteService extends TopicRouteService { + + private final BrokerController brokerController; + private final List brokerDataList; + private final int grpcPort; + + public LocalTopicRouteService(BrokerController brokerController, MQClientAPIFactory mqClientAPIFactory) { + super(mqClientAPIFactory); + this.brokerController = brokerController; + BrokerConfig brokerConfig = this.brokerController.getBrokerConfig(); + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(MixAll.MASTER_ID, this.brokerController.getBrokerAddr()); + this.brokerDataList = Lists.newArrayList( + new BrokerData(brokerConfig.getBrokerClusterName(), brokerConfig.getBrokerName(), brokerAddrs) + ); + this.grpcPort = ConfigurationManager.getProxyConfig().getGrpcServerPort(); + } + + @Override + public MessageQueueView getCurrentMessageQueueView(ProxyContext ctx, String topic) throws Exception { + TopicConfig topicConfig = this.brokerController.getTopicConfigManager().getTopicConfigTable().get(topic); + return new MessageQueueView(topic, toTopicRouteData(topicConfig), null); + } + + @Override + public ProxyTopicRouteData getTopicRouteForProxy(ProxyContext ctx, List
    requestHostAndPortList, + String topicName) throws Exception { + MessageQueueView messageQueueView = getAllMessageQueueView(ctx, topicName); + TopicRouteData topicRouteData = messageQueueView.getTopicRouteData(); + + ProxyTopicRouteData proxyTopicRouteData = new ProxyTopicRouteData(); + proxyTopicRouteData.setQueueDatas(topicRouteData.getQueueDatas()); + + for (BrokerData brokerData : topicRouteData.getBrokerDatas()) { + ProxyTopicRouteData.ProxyBrokerData proxyBrokerData = new ProxyTopicRouteData.ProxyBrokerData(); + proxyBrokerData.setCluster(brokerData.getCluster()); + proxyBrokerData.setBrokerName(brokerData.getBrokerName()); + for (Long brokerId : brokerData.getBrokerAddrs().keySet()) { + String brokerAddr = brokerData.getBrokerAddrs().get(brokerId); + HostAndPort brokerHostAndPort = HostAndPort.fromString(brokerAddr); + HostAndPort grpcHostAndPort = HostAndPort.fromParts(brokerHostAndPort.getHost(), grpcPort); + + proxyBrokerData.getBrokerAddrs().put(brokerId, Lists.newArrayList(new Address(Address.AddressScheme.IPv4, grpcHostAndPort))); + } + proxyTopicRouteData.getBrokerDatas().add(proxyBrokerData); + } + + return proxyTopicRouteData; + } + + @Override + public String getBrokerAddr(ProxyContext ctx, String brokerName) throws Exception { + return this.brokerController.getBrokerAddr(); + } + + @Override + public AddressableMessageQueue buildAddressableMessageQueue(ProxyContext ctx, MessageQueue messageQueue) throws Exception { + String brokerAddress = getBrokerAddr(ctx, messageQueue.getBrokerName()); + return new AddressableMessageQueue(messageQueue, brokerAddress); + } + + protected TopicRouteData toTopicRouteData(TopicConfig topicConfig) { + TopicRouteData topicRouteData = new TopicRouteData(); + topicRouteData.setBrokerDatas(brokerDataList); + + QueueData queueData = new QueueData(); + queueData.setPerm(topicConfig.getPerm()); + queueData.setReadQueueNums(topicConfig.getReadQueueNums()); + queueData.setWriteQueueNums(topicConfig.getWriteQueueNums()); + queueData.setTopicSysFlag(topicConfig.getTopicSysFlag()); + queueData.setBrokerName(this.brokerController.getBrokerConfig().getBrokerName()); + topicRouteData.setQueueDatas(Lists.newArrayList(queueData)); + + return topicRouteData; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/MessageQueueSelector.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/MessageQueueSelector.java new file mode 100644 index 00000000000..f25fb907ef2 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/MessageQueueSelector.java @@ -0,0 +1,314 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.service.route; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; +import com.google.common.math.IntMath; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.impl.producer.TopicPublishInfo; +import org.apache.rocketmq.client.latency.MQFaultStrategy; +import org.apache.rocketmq.common.constant.PermName; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.route.QueueData; + +public class MessageQueueSelector { + private static final int BROKER_ACTING_QUEUE_ID = -1; + + // multiple queues for brokers with queueId : normal + private final List queues = new ArrayList<>(); + // one queue for brokers with queueId : -1 + private final List brokerActingQueues = new ArrayList<>(); + private final Map brokerNameQueueMap = new ConcurrentHashMap<>(); + private final AtomicInteger queueIndex; + private final AtomicInteger brokerIndex; + private MQFaultStrategy mqFaultStrategy; + + public MessageQueueSelector(TopicRouteWrapper topicRouteWrapper, MQFaultStrategy mqFaultStrategy, boolean read) { + if (read) { + this.queues.addAll(buildRead(topicRouteWrapper)); + } else { + this.queues.addAll(buildWrite(topicRouteWrapper)); + } + buildBrokerActingQueues(topicRouteWrapper.getTopicName(), this.queues); + Random random = new Random(); + this.queueIndex = new AtomicInteger(random.nextInt()); + this.brokerIndex = new AtomicInteger(random.nextInt()); + this.mqFaultStrategy = mqFaultStrategy; + } + + private static List buildRead(TopicRouteWrapper topicRoute) { + Set queueSet = new HashSet<>(); + List qds = topicRoute.getQueueDatas(); + if (qds == null) { + return new ArrayList<>(); + } + + for (QueueData qd : qds) { + if (PermName.isReadable(qd.getPerm())) { + String brokerAddr = topicRoute.getMasterAddrPrefer(qd.getBrokerName()); + if (brokerAddr == null) { + continue; + } + + for (int i = 0; i < qd.getReadQueueNums(); i++) { + AddressableMessageQueue mq = new AddressableMessageQueue( + new MessageQueue(topicRoute.getTopicName(), qd.getBrokerName(), i), + brokerAddr); + queueSet.add(mq); + } + } + } + + return queueSet.stream().sorted().collect(Collectors.toList()); + } + + private static List buildWrite(TopicRouteWrapper topicRoute) { + Set queueSet = new HashSet<>(); + // order topic route. + if (StringUtils.isNotBlank(topicRoute.getOrderTopicConf())) { + String[] brokers = topicRoute.getOrderTopicConf().split(";"); + for (String broker : brokers) { + String[] item = broker.split(":"); + String brokerName = item[0]; + String brokerAddr = topicRoute.getMasterAddr(brokerName); + if (brokerAddr == null) { + continue; + } + + int nums = Integer.parseInt(item[1]); + for (int i = 0; i < nums; i++) { + AddressableMessageQueue mq = new AddressableMessageQueue( + new MessageQueue(topicRoute.getTopicName(), brokerName, i), + brokerAddr); + queueSet.add(mq); + } + } + } else { + List qds = topicRoute.getQueueDatas(); + if (qds == null) { + return new ArrayList<>(); + } + + for (QueueData qd : qds) { + if (PermName.isWriteable(qd.getPerm())) { + String brokerAddr = topicRoute.getMasterAddr(qd.getBrokerName()); + if (brokerAddr == null) { + continue; + } + + for (int i = 0; i < qd.getWriteQueueNums(); i++) { + AddressableMessageQueue mq = new AddressableMessageQueue( + new MessageQueue(topicRoute.getTopicName(), qd.getBrokerName(), i), + brokerAddr); + queueSet.add(mq); + } + } + } + } + + return queueSet.stream().sorted().collect(Collectors.toList()); + } + + private void buildBrokerActingQueues(String topic, List normalQueues) { + for (AddressableMessageQueue mq : normalQueues) { + AddressableMessageQueue brokerActingQueue = new AddressableMessageQueue( + new MessageQueue(topic, mq.getMessageQueue().getBrokerName(), BROKER_ACTING_QUEUE_ID), + mq.getBrokerAddr()); + + if (!brokerActingQueues.contains(brokerActingQueue)) { + brokerActingQueues.add(brokerActingQueue); + brokerNameQueueMap.put(brokerActingQueue.getBrokerName(), brokerActingQueue); + } + } + + Collections.sort(brokerActingQueues); + } + + public AddressableMessageQueue getQueueByBrokerName(String brokerName) { + return this.brokerNameQueueMap.get(brokerName); + } + + public AddressableMessageQueue selectOne(boolean onlyBroker) { + int nextIndex = onlyBroker ? brokerIndex.getAndIncrement() : queueIndex.getAndIncrement(); + return selectOneByIndex(nextIndex, onlyBroker); + } + + public AddressableMessageQueue selectOneByPipeline(boolean onlyBroker) { + if (mqFaultStrategy != null && mqFaultStrategy.isSendLatencyFaultEnable()) { + List messageQueueList = null; + MessageQueue messageQueue = null; + if (onlyBroker) { + messageQueueList = transferAddressableQueues(brokerActingQueues); + } else { + messageQueueList = transferAddressableQueues(queues); + } + AddressableMessageQueue addressableMessageQueue = null; + + // use both available filter. + messageQueue = selectOneMessageQueue(messageQueueList, onlyBroker ? brokerIndex : queueIndex, + mqFaultStrategy.getAvailableFilter(), mqFaultStrategy.getReachableFilter()); + addressableMessageQueue = transferQueue2Addressable(messageQueue); + if (addressableMessageQueue != null) { + return addressableMessageQueue; + } + + // use available filter. + messageQueue = selectOneMessageQueue(messageQueueList, onlyBroker ? brokerIndex : queueIndex, + mqFaultStrategy.getAvailableFilter()); + addressableMessageQueue = transferQueue2Addressable(messageQueue); + if (addressableMessageQueue != null) { + return addressableMessageQueue; + } + + // no available filter, then use reachable filter. + messageQueue = selectOneMessageQueue(messageQueueList, onlyBroker ? brokerIndex : queueIndex, + mqFaultStrategy.getReachableFilter()); + addressableMessageQueue = transferQueue2Addressable(messageQueue); + if (addressableMessageQueue != null) { + return addressableMessageQueue; + } + } + + // SendLatency is not enabled, or no queue is selected, then select by index. + return selectOne(onlyBroker); + } + + private MessageQueue selectOneMessageQueue(List messageQueueList, AtomicInteger sendQueue, TopicPublishInfo.QueueFilter...filter) { + if (messageQueueList == null || messageQueueList.isEmpty()) { + return null; + } + if (filter != null && filter.length != 0) { + for (int i = 0; i < messageQueueList.size(); i++) { + int index = Math.abs(sendQueue.incrementAndGet() % messageQueueList.size()); + MessageQueue mq = messageQueueList.get(index); + boolean filterResult = true; + for (TopicPublishInfo.QueueFilter f: filter) { + Preconditions.checkNotNull(f); + filterResult &= f.filter(mq); + } + if (filterResult) { + return mq; + } + } + } + return null; + } + + public List transferAddressableQueues(List addressableMessageQueueList) { + if (addressableMessageQueueList == null) { + return null; + } + + return addressableMessageQueueList.stream() + .map(AddressableMessageQueue::getMessageQueue) + .collect(Collectors.toList()); + } + + private AddressableMessageQueue transferQueue2Addressable(MessageQueue messageQueue) { + for (AddressableMessageQueue amq: queues) { + if (amq.getMessageQueue().equals(messageQueue)) { + return amq; + } + } + return null; + } + + public AddressableMessageQueue selectNextOne(AddressableMessageQueue last) { + boolean onlyBroker = last.getQueueId() < 0; + AddressableMessageQueue newOne = last; + int count = onlyBroker ? brokerActingQueues.size() : queues.size(); + + for (int i = 0; i < count; i++) { + newOne = selectOne(onlyBroker); + if (!newOne.getBrokerName().equals(last.getBrokerName()) || newOne.getQueueId() != last.getQueueId()) { + break; + } + } + return newOne; + } + + public AddressableMessageQueue selectOneByIndex(int index, boolean onlyBroker) { + if (onlyBroker) { + if (brokerActingQueues.isEmpty()) { + return null; + } + return brokerActingQueues.get(IntMath.mod(index, brokerActingQueues.size())); + } + + if (queues.isEmpty()) { + return null; + } + return queues.get(IntMath.mod(index, queues.size())); + } + + public List getQueues() { + return queues; + } + + public List getBrokerActingQueues() { + return brokerActingQueues; + } + + public MQFaultStrategy getMQFaultStrategy() { + return mqFaultStrategy; + } + + public void setMQFaultStrategy(MQFaultStrategy mqFaultStrategy) { + this.mqFaultStrategy = mqFaultStrategy; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MessageQueueSelector)) { + return false; + } + MessageQueueSelector queue = (MessageQueueSelector) o; + return Objects.equals(queues, queue.queues) && + Objects.equals(brokerActingQueues, queue.brokerActingQueues); + } + + @Override + public int hashCode() { + return Objects.hash(queues, brokerActingQueues); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("queues", queues) + .add("brokerActingQueues", brokerActingQueues) + .add("brokerNameQueueMap", brokerNameQueueMap) + .add("queueIndex", queueIndex) + .add("brokerIndex", brokerIndex) + .toString(); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/MessageQueueView.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/MessageQueueView.java new file mode 100644 index 00000000000..898e529f8cb --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/MessageQueueView.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.service.route; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.client.latency.MQFaultStrategy; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; + +public class MessageQueueView { + public static final MessageQueueView WRAPPED_EMPTY_QUEUE = new MessageQueueView("", new TopicRouteData(), null); + + private final MessageQueueSelector readSelector; + private final MessageQueueSelector writeSelector; + private final TopicRouteWrapper topicRouteWrapper; + + public MessageQueueView(String topic, TopicRouteData topicRouteData, MQFaultStrategy mqFaultStrategy) { + this.topicRouteWrapper = new TopicRouteWrapper(topicRouteData, topic); + + this.readSelector = new MessageQueueSelector(topicRouteWrapper, mqFaultStrategy, true); + this.writeSelector = new MessageQueueSelector(topicRouteWrapper, mqFaultStrategy, false); + } + + public TopicRouteData getTopicRouteData() { + return topicRouteWrapper.getTopicRouteData(); + } + + public TopicRouteWrapper getTopicRouteWrapper() { + return topicRouteWrapper; + } + + public String getTopicName() { + return topicRouteWrapper.getTopicName(); + } + + public boolean isEmptyCachedQueue() { + return this == WRAPPED_EMPTY_QUEUE; + } + + public MessageQueueSelector getReadSelector() { + return readSelector; + } + + public MessageQueueSelector getWriteSelector() { + return writeSelector; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("readSelector", readSelector) + .add("writeSelector", writeSelector) + .add("topicRouteWrapper", topicRouteWrapper) + .toString(); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/ProxyTopicRouteData.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/ProxyTopicRouteData.java new file mode 100644 index 00000000000..da8b3f61127 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/ProxyTopicRouteData.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.service.route; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.apache.rocketmq.proxy.common.Address; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; + +public class ProxyTopicRouteData { + + public static class ProxyBrokerData { + private String cluster; + private String brokerName; + private Map/* broker address */> brokerAddrs = new HashMap<>(); + + public String getCluster() { + return cluster; + } + + public void setCluster(String cluster) { + this.cluster = cluster; + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + public Map> getBrokerAddrs() { + return brokerAddrs; + } + + public void setBrokerAddrs(Map> brokerAddrs) { + this.brokerAddrs = brokerAddrs; + } + + public BrokerData buildBrokerData() { + BrokerData brokerData = new BrokerData(); + brokerData.setCluster(cluster); + brokerData.setBrokerName(brokerName); + HashMap buildBrokerAddress = new HashMap<>(); + brokerAddrs.forEach((k, v) -> { + if (!v.isEmpty()) { + buildBrokerAddress.put(k, v.get(0).getHostAndPort().toString()); + } + }); + brokerData.setBrokerAddrs(buildBrokerAddress); + return brokerData; + } + } + + private List queueDatas = new ArrayList<>(); + private List brokerDatas = new ArrayList<>(); + + public List getQueueDatas() { + return queueDatas; + } + + public void setQueueDatas(List queueDatas) { + this.queueDatas = queueDatas; + } + + public List getBrokerDatas() { + return brokerDatas; + } + + public void setBrokerDatas(List brokerDatas) { + this.brokerDatas = brokerDatas; + } + + public TopicRouteData buildTopicRouteData() { + TopicRouteData topicRouteData = new TopicRouteData(); + topicRouteData.setQueueDatas(queueDatas); + topicRouteData.setBrokerDatas(brokerDatas.stream() + .map(ProxyBrokerData::buildBrokerData) + .collect(Collectors.toList())); + return topicRouteData; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/TopicRouteHelper.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/TopicRouteHelper.java new file mode 100644 index 00000000000..651010ce178 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/TopicRouteHelper.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.service.route; + +import org.apache.rocketmq.client.common.ClientErrorCode; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.remoting.protocol.ResponseCode; + +public class TopicRouteHelper { + + public static boolean isTopicNotExistError(Throwable e) { + if (e instanceof MQBrokerException) { + if (((MQBrokerException) e).getResponseCode() == ResponseCode.TOPIC_NOT_EXIST) { + return true; + } + } + + if (e instanceof MQClientException) { + int code = ((MQClientException) e).getResponseCode(); + if (code == ResponseCode.TOPIC_NOT_EXIST || code == ClientErrorCode.NOT_FOUND_TOPIC_EXCEPTION) { + return true; + } + + Throwable cause = e.getCause(); + if (cause instanceof MQClientException) { + int causeCode = ((MQClientException) cause).getResponseCode(); + return causeCode == ResponseCode.TOPIC_NOT_EXIST || causeCode == ClientErrorCode.NOT_FOUND_TOPIC_EXCEPTION; + } + } + + return false; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/TopicRouteService.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/TopicRouteService.java new file mode 100644 index 00000000000..ccf094c03aa --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/TopicRouteService.java @@ -0,0 +1,228 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.service.route; + +import com.github.benmanes.caffeine.cache.CacheLoader; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import com.google.common.base.Optional; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.impl.mqclient.MQClientAPIFactory; +import org.apache.rocketmq.client.latency.MQFaultStrategy; +import org.apache.rocketmq.client.latency.Resolver; +import org.apache.rocketmq.client.latency.ServiceDetector; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.common.thread.ThreadPoolMonitor; +import org.apache.rocketmq.common.utils.AbstractStartAndShutdown; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.common.Address; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.ProxyConfig; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.GetMaxOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +public abstract class TopicRouteService extends AbstractStartAndShutdown { + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + + private final MQClientAPIFactory mqClientAPIFactory; + private MQFaultStrategy mqFaultStrategy; + + protected final LoadingCache topicCache; + protected final ScheduledExecutorService scheduledExecutorService; + protected final ThreadPoolExecutor cacheRefreshExecutor; + + public TopicRouteService(MQClientAPIFactory mqClientAPIFactory) { + ProxyConfig config = ConfigurationManager.getProxyConfig(); + + this.scheduledExecutorService = ThreadUtils.newSingleThreadScheduledExecutor( + new ThreadFactoryImpl("TopicRouteService_") + ); + this.cacheRefreshExecutor = ThreadPoolMonitor.createAndMonitor( + config.getTopicRouteServiceThreadPoolNums(), + config.getTopicRouteServiceThreadPoolNums(), + 1000 * 60, + TimeUnit.MILLISECONDS, + "TopicRouteCacheRefresh", + config.getTopicRouteServiceThreadPoolQueueCapacity() + ); + this.mqClientAPIFactory = mqClientAPIFactory; + + this.topicCache = Caffeine.newBuilder().maximumSize(config.getTopicRouteServiceCacheMaxNum()) + .expireAfterAccess(config.getTopicRouteServiceCacheExpiredSeconds(), TimeUnit.SECONDS) + .refreshAfterWrite(config.getTopicRouteServiceCacheRefreshSeconds(), TimeUnit.SECONDS) + .executor(cacheRefreshExecutor) + .build(new CacheLoader() { + @Override + public @Nullable MessageQueueView load(String topic) throws Exception { + try { + TopicRouteData topicRouteData = mqClientAPIFactory.getClient().getTopicRouteInfoFromNameServer(topic, Duration.ofSeconds(3).toMillis()); + return buildMessageQueueView(topic, topicRouteData); + } catch (Exception e) { + if (TopicRouteHelper.isTopicNotExistError(e)) { + return MessageQueueView.WRAPPED_EMPTY_QUEUE; + } + throw e; + } + } + + @Override + public @Nullable MessageQueueView reload(@NonNull String key, + @NonNull MessageQueueView oldValue) throws Exception { + try { + return load(key); + } catch (Exception e) { + log.warn(String.format("reload topic route from namesrv. topic: %s", key), e); + return oldValue; + } + } + }); + ServiceDetector serviceDetector = new ServiceDetector() { + @Override + public boolean detect(String endpoint, long timeoutMillis) { + Optional candidateTopic = pickTopic(); + if (!candidateTopic.isPresent()) { + return false; + } + try { + GetMaxOffsetRequestHeader requestHeader = new GetMaxOffsetRequestHeader(); + requestHeader.setTopic(candidateTopic.get()); + requestHeader.setQueueId(0); + Long maxOffset = mqClientAPIFactory.getClient().getMaxOffset(endpoint, requestHeader, timeoutMillis).get(); + return true; + } catch (Exception e) { + return false; + } + } + }; + mqFaultStrategy = new MQFaultStrategy(extractClientConfigFromProxyConfig(config), new Resolver() { + @Override + public String resolve(String name) { + try { + String brokerAddr = getBrokerAddr(ProxyContext.createForInner("MQFaultStrategy"), name); + return brokerAddr; + } catch (Exception e) { + return null; + } + } + }, serviceDetector); + this.init(); + } + + // pickup one topic in the topic cache + private Optional pickTopic() { + if (topicCache.asMap().isEmpty()) { + return Optional.absent(); + } + return Optional.of(topicCache.asMap().keySet().iterator().next()); + } + + protected void init() { + this.appendShutdown(this.scheduledExecutorService::shutdown); + this.appendStartAndShutdown(this.mqClientAPIFactory); + } + + @Override + public void shutdown() throws Exception { + if (this.mqFaultStrategy.isStartDetectorEnable()) { + mqFaultStrategy.shutdown(); + } + } + + @Override + public void start() throws Exception { + if (this.mqFaultStrategy.isStartDetectorEnable()) { + this.mqFaultStrategy.startDetector(); + } + } + + public ClientConfig extractClientConfigFromProxyConfig(ProxyConfig proxyConfig) { + ClientConfig tempClientConfig = new ClientConfig(); + tempClientConfig.setSendLatencyEnable(proxyConfig.getSendLatencyEnable()); + tempClientConfig.setStartDetectorEnable(proxyConfig.getStartDetectorEnable()); + tempClientConfig.setDetectTimeout(proxyConfig.getDetectTimeout()); + tempClientConfig.setDetectInterval(proxyConfig.getDetectInterval()); + return tempClientConfig; + } + + public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation, + boolean reachable) { + checkSendFaultToleranceEnable(); + this.mqFaultStrategy.updateFaultItem(brokerName, currentLatency, isolation, reachable); + } + + public void checkSendFaultToleranceEnable() { + boolean hotLatencySwitch = ConfigurationManager.getProxyConfig().isSendLatencyEnable(); + boolean hotDetectorSwitch = ConfigurationManager.getProxyConfig().isStartDetectorEnable(); + this.mqFaultStrategy.setSendLatencyFaultEnable(hotLatencySwitch); + this.mqFaultStrategy.setStartDetectorEnable(hotDetectorSwitch); + } + + public MQFaultStrategy getMqFaultStrategy() { + return this.mqFaultStrategy; + } + + public MessageQueueView getAllMessageQueueView(ProxyContext ctx, String topicName) throws Exception { + return getCacheMessageQueueWrapper(this.topicCache, topicName); + } + + public abstract MessageQueueView getCurrentMessageQueueView(ProxyContext ctx, String topicName) throws Exception; + + public abstract ProxyTopicRouteData getTopicRouteForProxy(ProxyContext ctx, List
    requestHostAndPortList, + String topicName) throws Exception; + + public abstract String getBrokerAddr(ProxyContext ctx, String brokerName) throws Exception; + + public abstract AddressableMessageQueue buildAddressableMessageQueue(ProxyContext ctx, MessageQueue messageQueue) throws Exception; + + protected static MessageQueueView getCacheMessageQueueWrapper(LoadingCache topicCache, + String key) throws Exception { + MessageQueueView res = topicCache.get(key); + if (res != null && res.isEmptyCachedQueue()) { + throw new MQClientException(ResponseCode.TOPIC_NOT_EXIST, + "No topic route info in name server for the topic: " + key); + } + return res; + } + + protected static boolean isTopicRouteValid(TopicRouteData routeData) { + return routeData != null && routeData.getQueueDatas() != null && !routeData.getQueueDatas().isEmpty() + && routeData.getBrokerDatas() != null && !routeData.getBrokerDatas().isEmpty(); + } + + protected MessageQueueView buildMessageQueueView(String topic, TopicRouteData topicRouteData) { + if (isTopicRouteValid(topicRouteData)) { + MessageQueueView tmp = new MessageQueueView(topic, topicRouteData, TopicRouteService.this.getMqFaultStrategy()); + log.debug("load topic route from namesrv. topic: {}, queue: {}", topic, tmp); + return tmp; + } + return MessageQueueView.WRAPPED_EMPTY_QUEUE; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/TopicRouteWrapper.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/TopicRouteWrapper.java new file mode 100644 index 00000000000..7956c6284ea --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/route/TopicRouteWrapper.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.service.route; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; + +public class TopicRouteWrapper { + + private final TopicRouteData topicRouteData; + private final String topicName; + private final Map brokerNameRouteData = new HashMap<>(); + + public TopicRouteWrapper(TopicRouteData topicRouteData, String topicName) { + this.topicRouteData = topicRouteData; + this.topicName = topicName; + + if (this.topicRouteData.getBrokerDatas() != null) { + for (BrokerData brokerData : this.topicRouteData.getBrokerDatas()) { + this.brokerNameRouteData.put(brokerData.getBrokerName(), brokerData); + } + } + } + + public String getMasterAddr(String brokerName) { + return this.brokerNameRouteData.get(brokerName).getBrokerAddrs().get(MixAll.MASTER_ID); + } + + public String getMasterAddrPrefer(String brokerName) { + HashMap brokerAddr = brokerNameRouteData.get(brokerName).getBrokerAddrs(); + String addr = brokerAddr.get(MixAll.MASTER_ID); + if (addr == null) { + Optional optional = brokerAddr.keySet().stream().findFirst(); + return optional.map(brokerAddr::get).orElse(null); + } + return addr; + } + + public String getTopicName() { + return topicName; + } + + public TopicRouteData getTopicRouteData() { + return topicRouteData; + } + + public List getQueueDatas() { + return this.topicRouteData.getQueueDatas(); + } + + public String getOrderTopicConf() { + return this.topicRouteData.getOrderTopicConf(); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/sysmessage/AbstractSystemMessageSyncer.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/sysmessage/AbstractSystemMessageSyncer.java new file mode 100644 index 00000000000..734fbeba1b8 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/sysmessage/AbstractSystemMessageSyncer.java @@ -0,0 +1,185 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.sysmessage; + +import com.alibaba.fastjson.JSON; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.common.ProxyException; +import org.apache.rocketmq.proxy.common.ProxyExceptionCode; +import org.apache.rocketmq.common.utils.StartAndShutdown; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.ProxyConfig; +import org.apache.rocketmq.proxy.service.admin.AdminService; +import org.apache.rocketmq.client.impl.mqclient.MQClientAPIFactory; +import org.apache.rocketmq.proxy.service.route.AddressableMessageQueue; +import org.apache.rocketmq.proxy.service.route.TopicRouteService; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; + +public abstract class AbstractSystemMessageSyncer implements StartAndShutdown, MessageListenerConcurrently { + protected static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + protected final TopicRouteService topicRouteService; + protected final AdminService adminService; + protected final MQClientAPIFactory mqClientAPIFactory; + protected final RPCHook rpcHook; + protected DefaultMQPushConsumer defaultMQPushConsumer; + + public AbstractSystemMessageSyncer(TopicRouteService topicRouteService, AdminService adminService, MQClientAPIFactory mqClientAPIFactory, RPCHook rpcHook) { + this.topicRouteService = topicRouteService; + this.adminService = adminService; + this.mqClientAPIFactory = mqClientAPIFactory; + this.rpcHook = rpcHook; + } + + protected String getSystemMessageProducerId() { + return "PID_" + getBroadcastTopicName(); + } + + protected String getSystemMessageConsumerId() { + return "CID_" + getBroadcastTopicName(); + } + + protected String getBroadcastTopicName() { + return ConfigurationManager.getProxyConfig().getHeartbeatSyncerTopicName(); + } + + protected String getSubTag() { + return "*"; + } + + protected String getBroadcastTopicClusterName() { + ProxyConfig proxyConfig = ConfigurationManager.getProxyConfig(); + return proxyConfig.getHeartbeatSyncerTopicClusterName(); + } + + protected int getBroadcastTopicQueueNum() { + return 1; + } + + public RPCHook getRpcHook() { + return rpcHook; + } + + protected void sendSystemMessage(Object data) { + String targetTopic = this.getBroadcastTopicName(); + try { + Message message = new Message( + targetTopic, + JSON.toJSONString(data).getBytes(StandardCharsets.UTF_8) + ); + + AddressableMessageQueue messageQueue = this.topicRouteService.getAllMessageQueueView(ProxyContext.createForInner(this.getClass()), targetTopic) + .getWriteSelector().selectOne(true); + this.mqClientAPIFactory.getClient().sendMessageAsync( + messageQueue.getBrokerAddr(), + messageQueue.getBrokerName(), + message, + buildSendMessageRequestHeader(message, this.getSystemMessageProducerId(), messageQueue.getQueueId()), + Duration.ofSeconds(3).toMillis() + ).whenCompleteAsync((result, throwable) -> { + if (throwable != null) { + log.error("send system message failed. data: {}, topic: {}", data, getBroadcastTopicName(), throwable); + return; + } + if (SendStatus.SEND_OK != result.getSendStatus()) { + log.error("send system message failed. data: {}, topic: {}, sendResult:{}", data, getBroadcastTopicName(), result); + } + }); + } catch (Throwable t) { + log.error("send system message failed. data: {}, topic: {}", data, targetTopic, t); + } + } + + protected SendMessageRequestHeader buildSendMessageRequestHeader(Message message, + String producerGroup, int queueId) { + SendMessageRequestHeader requestHeader = new SendMessageRequestHeader(); + + requestHeader.setProducerGroup(producerGroup); + requestHeader.setTopic(message.getTopic()); + requestHeader.setDefaultTopic(TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC); + requestHeader.setDefaultTopicQueueNums(0); + requestHeader.setQueueId(queueId); + requestHeader.setSysFlag(0); + requestHeader.setBornTimestamp(System.currentTimeMillis()); + requestHeader.setFlag(message.getFlag()); + requestHeader.setProperties(MessageDecoder.messageProperties2String(message.getProperties())); + requestHeader.setReconsumeTimes(0); + requestHeader.setBatch(false); + return requestHeader; + } + + @Override + public void start() throws Exception { + this.createSysTopic(); + RPCHook rpcHook = this.getRpcHook(); + this.defaultMQPushConsumer = new DefaultMQPushConsumer(this.getSystemMessageConsumerId(), rpcHook); + + this.defaultMQPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); + this.defaultMQPushConsumer.setMessageModel(MessageModel.BROADCASTING); + try { + this.defaultMQPushConsumer.subscribe(this.getBroadcastTopicName(), this.getSubTag()); + } catch (MQClientException e) { + throw new ProxyException(ProxyExceptionCode.INTERNAL_SERVER_ERROR, "subscribe to broadcast topic " + this.getBroadcastTopicName() + " failed. " + e.getMessage()); + } + this.defaultMQPushConsumer.registerMessageListener(this); + this.defaultMQPushConsumer.start(); + } + + protected void createSysTopic() { + if (this.adminService.topicExist(this.getBroadcastTopicName())) { + return; + } + + String clusterName = this.getBroadcastTopicClusterName(); + if (StringUtils.isEmpty(clusterName)) { + throw new ProxyException(ProxyExceptionCode.INTERNAL_SERVER_ERROR, "system topic cluster cannot be empty"); + } + + boolean createSuccess = this.adminService.createTopicOnTopicBrokerIfNotExist( + this.getBroadcastTopicName(), + clusterName, + this.getBroadcastTopicQueueNum(), + this.getBroadcastTopicQueueNum(), + true, + 3 + ); + if (!createSuccess) { + throw new ProxyException(ProxyExceptionCode.INTERNAL_SERVER_ERROR, "create system broadcast topic " + this.getBroadcastTopicName() + " failed on cluster " + clusterName); + } + } + + @Override + public void shutdown() throws Exception { + this.defaultMQPushConsumer.shutdown(); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/sysmessage/HeartbeatSyncer.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/sysmessage/HeartbeatSyncer.java new file mode 100644 index 00000000000..fee3ea87d27 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/sysmessage/HeartbeatSyncer.java @@ -0,0 +1,240 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.sysmessage; + +import com.alibaba.fastjson.JSON; +import io.netty.channel.Channel; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.broker.client.ClientChannelInfo; +import org.apache.rocketmq.broker.client.ConsumerGroupEvent; +import org.apache.rocketmq.broker.client.ConsumerIdsChangeListener; +import org.apache.rocketmq.broker.client.ConsumerManager; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.thread.ThreadPoolMonitor; +import org.apache.rocketmq.proxy.common.channel.ChannelHelper; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.ProxyConfig; +import org.apache.rocketmq.proxy.processor.channel.RemoteChannel; +import org.apache.rocketmq.proxy.service.admin.AdminService; +import org.apache.rocketmq.client.impl.mqclient.MQClientAPIFactory; +import org.apache.rocketmq.proxy.service.route.TopicRouteService; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; + +public class HeartbeatSyncer extends AbstractSystemMessageSyncer { + + protected ThreadPoolExecutor threadPoolExecutor; + protected ConsumerManager consumerManager; + protected final Map remoteChannelMap = new ConcurrentHashMap<>(); + protected String localProxyId; + + public HeartbeatSyncer(TopicRouteService topicRouteService, AdminService adminService, + ConsumerManager consumerManager, MQClientAPIFactory mqClientAPIFactory, RPCHook rpcHook) { + super(topicRouteService, adminService, mqClientAPIFactory, rpcHook); + this.consumerManager = consumerManager; + this.localProxyId = buildLocalProxyId(); + this.init(); + } + + protected void init() { + ProxyConfig proxyConfig = ConfigurationManager.getProxyConfig(); + this.threadPoolExecutor = ThreadPoolMonitor.createAndMonitor( + proxyConfig.getHeartbeatSyncerThreadPoolNums(), + proxyConfig.getHeartbeatSyncerThreadPoolNums(), + 1, + TimeUnit.MINUTES, + "HeartbeatSyncer", + proxyConfig.getHeartbeatSyncerThreadPoolQueueCapacity() + ); + this.consumerManager.appendConsumerIdsChangeListener(new ConsumerIdsChangeListener() { + @Override + public void handle(ConsumerGroupEvent event, String group, Object... args) { + processConsumerGroupEvent(event, group, args); + } + + @Override + public void shutdown() { + + } + }); + } + + @Override + public void shutdown() throws Exception { + this.threadPoolExecutor.shutdown(); + super.shutdown(); + } + + protected void processConsumerGroupEvent(ConsumerGroupEvent event, String group, Object... args) { + if (event == ConsumerGroupEvent.CLIENT_UNREGISTER) { + if (args == null || args.length < 1) { + return; + } + if (args[0] instanceof ClientChannelInfo) { + ClientChannelInfo clientChannelInfo = (ClientChannelInfo) args[0]; + remoteChannelMap.remove(buildKey(group, clientChannelInfo.getChannel())); + } + } + } + + public void onConsumerRegister(String consumerGroup, ClientChannelInfo clientChannelInfo, + ConsumeType consumeType, MessageModel messageModel, ConsumeFromWhere consumeFromWhere, + Set subList) { + if (clientChannelInfo == null || ChannelHelper.isRemote(clientChannelInfo.getChannel())) { + return; + } + try { + this.threadPoolExecutor.submit(() -> { + try { + RemoteChannel remoteChannel = RemoteChannel.create(clientChannelInfo.getChannel()); + if (remoteChannel == null) { + return; + } + HeartbeatSyncerData data = new HeartbeatSyncerData( + HeartbeatType.REGISTER, + clientChannelInfo.getClientId(), + clientChannelInfo.getLanguage(), + clientChannelInfo.getVersion(), + consumerGroup, + consumeType, + messageModel, + consumeFromWhere, + localProxyId, + remoteChannel.encode() + ); + data.setSubscriptionDataSet(subList); + + log.debug("sync register heart beat. topic:{}, data:{}", this.getBroadcastTopicName(), data); + this.sendSystemMessage(data); + } catch (Throwable t) { + log.error("heartbeat register broadcast failed. group:{}, clientChannelInfo:{}, consumeType:{}, messageModel:{}, consumeFromWhere:{}, subList:{}", + consumerGroup, clientChannelInfo, consumeType, messageModel, consumeFromWhere, subList, t); + } + }); + } catch (Throwable t) { + log.error("heartbeat submit register broadcast failed. group:{}, clientChannelInfo:{}, consumeType:{}, messageModel:{}, consumeFromWhere:{}, subList:{}", + consumerGroup, clientChannelInfo, consumeType, messageModel, consumeFromWhere, subList, t); + } + } + + public void onConsumerUnRegister(String consumerGroup, ClientChannelInfo clientChannelInfo) { + if (clientChannelInfo == null || ChannelHelper.isRemote(clientChannelInfo.getChannel())) { + return; + } + try { + this.threadPoolExecutor.submit(() -> { + try { + RemoteChannel remoteChannel = RemoteChannel.create(clientChannelInfo.getChannel()); + if (remoteChannel == null) { + return; + } + HeartbeatSyncerData data = new HeartbeatSyncerData( + HeartbeatType.UNREGISTER, + clientChannelInfo.getClientId(), + clientChannelInfo.getLanguage(), + clientChannelInfo.getVersion(), + consumerGroup, + null, + null, + null, + localProxyId, + remoteChannel.encode() + ); + + log.debug("sync unregister heart beat. topic:{}, data:{}", this.getBroadcastTopicName(), data); + this.sendSystemMessage(data); + } catch (Throwable t) { + log.error("heartbeat unregister broadcast failed. group:{}, clientChannelInfo:{}, consumeType:{}", + consumerGroup, clientChannelInfo, t); + } + }); + } catch (Throwable t) { + log.error("heartbeat submit unregister broadcast failed. group:{}, clientChannelInfo:{}, consumeType:{}", + consumerGroup, clientChannelInfo, t); + } + } + + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + if (msgs == null || msgs.isEmpty()) { + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + + for (MessageExt msg : msgs) { + try { + HeartbeatSyncerData data = JSON.parseObject(new String(msg.getBody(), StandardCharsets.UTF_8), HeartbeatSyncerData.class); + if (data.getLocalProxyId().equals(localProxyId)) { + continue; + } + + RemoteChannel decodedChannel = RemoteChannel.decode(data.getChannelData()); + RemoteChannel channel = remoteChannelMap.computeIfAbsent(buildKey(data.getGroup(), decodedChannel), key -> decodedChannel); + channel.setExtendAttribute(decodedChannel.getChannelExtendAttribute()); + ClientChannelInfo clientChannelInfo = new ClientChannelInfo( + channel, + data.getClientId(), + data.getLanguage(), + data.getVersion() + ); + log.debug("start process remote channel. data:{}, clientChannelInfo:{}", data, clientChannelInfo); + if (data.getHeartbeatType().equals(HeartbeatType.REGISTER)) { + this.consumerManager.registerConsumer( + data.getGroup(), + clientChannelInfo, + data.getConsumeType(), + data.getMessageModel(), + data.getConsumeFromWhere(), + data.getSubscriptionDataSet(), + false + ); + } else { + this.consumerManager.unregisterConsumer( + data.getGroup(), + clientChannelInfo, + false + ); + } + } catch (Throwable t) { + log.error("heartbeat consume message failed. msg:{}, data:{}", msg, new String(msg.getBody(), StandardCharsets.UTF_8), t); + } + } + + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + + private String buildLocalProxyId() { + ProxyConfig proxyConfig = ConfigurationManager.getProxyConfig(); + // use local address, remoting port and grpc port to build unique local proxy Id + return proxyConfig.getLocalServeAddr() + "%" + proxyConfig.getRemotingListenPort() + "%" + proxyConfig.getGrpcServerPort(); + } + + private static String buildKey(String group, Channel channel) { + return group + "@" + channel.id().asLongText(); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/sysmessage/HeartbeatSyncerData.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/sysmessage/HeartbeatSyncerData.java new file mode 100644 index 00000000000..97760506f14 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/sysmessage/HeartbeatSyncerData.java @@ -0,0 +1,176 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.sysmessage; + +import com.google.common.base.MoreObjects; +import java.util.Set; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.remoting.protocol.LanguageCode; + +public class HeartbeatSyncerData { + private HeartbeatType heartbeatType; + private String clientId; + private LanguageCode language; + private int version; + private long lastUpdateTimestamp = System.currentTimeMillis(); + private Set subscriptionDataSet; + private String group; + private ConsumeType consumeType; + private MessageModel messageModel; + private ConsumeFromWhere consumeFromWhere; + private String localProxyId; + private String channelData; + + public HeartbeatSyncerData() { + } + + public HeartbeatSyncerData(HeartbeatType heartbeatType, String clientId, + LanguageCode language, int version, String group, + ConsumeType consumeType, MessageModel messageModel, + ConsumeFromWhere consumeFromWhere, String localProxyId, + String channelData) { + this.heartbeatType = heartbeatType; + this.clientId = clientId; + this.language = language; + this.version = version; + this.group = group; + this.consumeType = consumeType; + this.messageModel = messageModel; + this.consumeFromWhere = consumeFromWhere; + this.localProxyId = localProxyId; + this.channelData = channelData; + } + + public HeartbeatType getHeartbeatType() { + return heartbeatType; + } + + public void setHeartbeatType(HeartbeatType heartbeatType) { + this.heartbeatType = heartbeatType; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public LanguageCode getLanguage() { + return language; + } + + public void setLanguage(LanguageCode language) { + this.language = language; + } + + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } + + public long getLastUpdateTimestamp() { + return lastUpdateTimestamp; + } + + public void setLastUpdateTimestamp(long lastUpdateTimestamp) { + this.lastUpdateTimestamp = lastUpdateTimestamp; + } + + public Set getSubscriptionDataSet() { + return subscriptionDataSet; + } + + public void setSubscriptionDataSet( + Set subscriptionDataSet) { + this.subscriptionDataSet = subscriptionDataSet; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public ConsumeType getConsumeType() { + return consumeType; + } + + public void setConsumeType(ConsumeType consumeType) { + this.consumeType = consumeType; + } + + public MessageModel getMessageModel() { + return messageModel; + } + + public void setMessageModel(MessageModel messageModel) { + this.messageModel = messageModel; + } + + public ConsumeFromWhere getConsumeFromWhere() { + return consumeFromWhere; + } + + public void setConsumeFromWhere(ConsumeFromWhere consumeFromWhere) { + this.consumeFromWhere = consumeFromWhere; + } + + public String getLocalProxyId() { + return localProxyId; + } + + public void setLocalProxyId(String localProxyId) { + this.localProxyId = localProxyId; + } + + public String getChannelData() { + return channelData; + } + + public void setChannelData(String channelData) { + this.channelData = channelData; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("heartbeatType", heartbeatType) + .add("clientId", clientId) + .add("language", language) + .add("version", version) + .add("lastUpdateTimestamp", lastUpdateTimestamp) + .add("subscriptionDataSet", subscriptionDataSet) + .add("group", group) + .add("consumeType", consumeType) + .add("messageModel", messageModel) + .add("consumeFromWhere", consumeFromWhere) + .add("connectProxyIp", localProxyId) + .add("channelData", channelData) + .toString(); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/sysmessage/HeartbeatType.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/sysmessage/HeartbeatType.java new file mode 100644 index 00000000000..8f0801f54d6 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/sysmessage/HeartbeatType.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.sysmessage; + +public enum HeartbeatType { + REGISTER, + UNREGISTER; +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/transaction/AbstractTransactionService.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/transaction/AbstractTransactionService.java new file mode 100644 index 00000000000..f0e083adead --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/transaction/AbstractTransactionService.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.transaction; + +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.common.utils.StartAndShutdown; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.remoting.protocol.header.EndTransactionRequestHeader; + +public abstract class AbstractTransactionService implements TransactionService, StartAndShutdown { + + protected TransactionDataManager transactionDataManager = new TransactionDataManager(); + + @Override + public TransactionData addTransactionDataByBrokerAddr(ProxyContext ctx, String brokerAddr, String producerGroup, long tranStateTableOffset, long commitLogOffset, String transactionId, + Message message) { + return this.addTransactionDataByBrokerName(ctx, this.getBrokerNameByAddr(brokerAddr), producerGroup, tranStateTableOffset, commitLogOffset, transactionId, message); + } + + @Override + public TransactionData addTransactionDataByBrokerName(ProxyContext ctx, String brokerName, String producerGroup, long tranStateTableOffset, long commitLogOffset, String transactionId, + Message message) { + if (StringUtils.isBlank(brokerName)) { + return null; + } + TransactionData transactionData = new TransactionData( + brokerName, + tranStateTableOffset, commitLogOffset, transactionId, + System.currentTimeMillis(), + ConfigurationManager.getProxyConfig().getTransactionDataExpireMillis()); + + this.transactionDataManager.addTransactionData( + producerGroup, + transactionId, + transactionData + ); + return transactionData; + } + + @Override + public EndTransactionRequestData genEndTransactionRequestHeader(ProxyContext ctx, String producerGroup, Integer commitOrRollback, + boolean fromTransactionCheck, String msgId, String transactionId) { + TransactionData transactionData = this.transactionDataManager.pollNoExpireTransactionData(producerGroup, transactionId); + if (transactionData == null) { + return null; + } + EndTransactionRequestHeader header = new EndTransactionRequestHeader(); + header.setProducerGroup(producerGroup); + header.setCommitOrRollback(commitOrRollback); + header.setFromTransactionCheck(fromTransactionCheck); + header.setMsgId(msgId); + header.setTransactionId(transactionId); + header.setTranStateTableOffset(transactionData.getTranStateTableOffset()); + header.setCommitLogOffset(transactionData.getCommitLogOffset()); + return new EndTransactionRequestData(transactionData.getBrokerName(), header); + } + + @Override + public void onSendCheckTransactionStateFailed(ProxyContext context, String producerGroup, TransactionData transactionData) { + this.transactionDataManager.removeTransactionData(producerGroup, transactionData.getTransactionId(), transactionData); + } + + protected abstract String getBrokerNameByAddr(String brokerAddr); + + @Override + public void shutdown() throws Exception { + this.transactionDataManager.shutdown(); + } + + @Override + public void start() throws Exception { + this.transactionDataManager.start(); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/transaction/ClusterTransactionService.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/transaction/ClusterTransactionService.java new file mode 100644 index 00000000000..1ec42864636 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/transaction/ClusterTransactionService.java @@ -0,0 +1,301 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.service.transaction; + +import com.google.common.collect.Sets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.broker.client.ProducerManager; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.thread.ThreadPoolMonitor; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.ProxyConfig; +import org.apache.rocketmq.client.impl.mqclient.MQClientAPIFactory; +import org.apache.rocketmq.proxy.service.route.MessageQueueView; +import org.apache.rocketmq.proxy.service.route.TopicRouteService; +import org.apache.rocketmq.remoting.protocol.heartbeat.HeartbeatData; +import org.apache.rocketmq.remoting.protocol.heartbeat.ProducerData; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; + +public class ClusterTransactionService extends AbstractTransactionService { + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + + private static final String TRANS_HEARTBEAT_CLIENT_ID = "rmq-proxy-producer-client"; + + private final MQClientAPIFactory mqClientAPIFactory; + private final TopicRouteService topicRouteService; + private final ProducerManager producerManager; + + private ThreadPoolExecutor heartbeatExecutors; + private final Map/* cluster list */> groupClusterData = new ConcurrentHashMap<>(); + private final AtomicReference> brokerAddrNameMapRef = new AtomicReference<>(); + private TxHeartbeatServiceThread txHeartbeatServiceThread; + + public ClusterTransactionService(TopicRouteService topicRouteService, ProducerManager producerManager, + MQClientAPIFactory mqClientAPIFactory) { + this.topicRouteService = topicRouteService; + this.producerManager = producerManager; + this.mqClientAPIFactory = mqClientAPIFactory; + } + + @Override + public void addTransactionSubscription(ProxyContext ctx, String group, List topicList) { + for (String topic : topicList) { + addTransactionSubscription(ctx, group, topic); + } + } + + @Override + public void addTransactionSubscription(ProxyContext ctx, String group, String topic) { + try { + groupClusterData.compute(group, (groupName, clusterDataSet) -> { + if (clusterDataSet == null) { + clusterDataSet = Sets.newHashSet(); + } + clusterDataSet.addAll(getClusterDataFromTopic(ctx, topic)); + return clusterDataSet; + }); + } catch (Exception e) { + log.error("add producer group err in txHeartBeat. groupId: {}, err: {}", group, e); + } + } + + @Override + public void replaceTransactionSubscription(ProxyContext ctx, String group, List topicList) { + Set clusterDataSet = new HashSet<>(); + for (String topic : topicList) { + clusterDataSet.addAll(getClusterDataFromTopic(ctx, topic)); + } + groupClusterData.put(group, clusterDataSet); + } + + private Set getClusterDataFromTopic(ProxyContext ctx, String topic) { + try { + MessageQueueView messageQueue = this.topicRouteService.getAllMessageQueueView(ctx, topic); + List brokerDataList = messageQueue.getTopicRouteData().getBrokerDatas(); + + if (brokerDataList == null) { + return Collections.emptySet(); + } + Set res = Sets.newHashSet(); + for (BrokerData brokerData : brokerDataList) { + res.add(new ClusterData(brokerData.getCluster())); + } + return res; + } catch (Throwable t) { + log.error("get cluster data failed in txHeartBeat. topic: {}, err: {}", topic, t); + } + return Collections.emptySet(); + } + + @Override + public void unSubscribeAllTransactionTopic(ProxyContext ctx, String group) { + groupClusterData.remove(group); + } + + public void scanProducerHeartBeat() { + Set groupSet = groupClusterData.keySet(); + + Map> clusterHeartbeatData = new HashMap<>(); + for (String group : groupSet) { + groupClusterData.computeIfPresent(group, (groupName, clusterDataSet) -> { + if (clusterDataSet.isEmpty()) { + return null; + } + if (!this.producerManager.groupOnline(groupName)) { + return null; + } + + ProducerData producerData = new ProducerData(); + producerData.setGroupName(groupName); + + for (ClusterData clusterData : clusterDataSet) { + List heartbeatDataList = clusterHeartbeatData.get(clusterData.cluster); + if (heartbeatDataList == null) { + heartbeatDataList = new ArrayList<>(); + } + + HeartbeatData heartbeatData; + if (heartbeatDataList.isEmpty()) { + heartbeatData = new HeartbeatData(); + heartbeatData.setClientID(TRANS_HEARTBEAT_CLIENT_ID); + heartbeatDataList.add(heartbeatData); + } else { + heartbeatData = heartbeatDataList.get(heartbeatDataList.size() - 1); + if (heartbeatData.getProducerDataSet().size() >= ConfigurationManager.getProxyConfig().getTransactionHeartbeatBatchNum()) { + heartbeatData = new HeartbeatData(); + heartbeatData.setClientID(TRANS_HEARTBEAT_CLIENT_ID); + heartbeatDataList.add(heartbeatData); + } + } + + heartbeatData.getProducerDataSet().add(producerData); + clusterHeartbeatData.put(clusterData.cluster, heartbeatDataList); + } + + if (clusterDataSet.isEmpty()) { + return null; + } + return clusterDataSet; + }); + } + + if (clusterHeartbeatData.isEmpty()) { + return; + } + Map brokerAddrNameMap = new ConcurrentHashMap<>(); + Set>> clusterEntry = clusterHeartbeatData.entrySet(); + for (Map.Entry> entry : clusterEntry) { + sendHeartBeatToCluster(entry.getKey(), entry.getValue(), brokerAddrNameMap); + } + this.brokerAddrNameMapRef.set(brokerAddrNameMap); + } + + public Map> getGroupClusterData() { + return groupClusterData; + } + + protected void sendHeartBeatToCluster(String clusterName, List heartbeatDataList, Map brokerAddrNameMap) { + if (heartbeatDataList == null) { + return; + } + for (HeartbeatData heartbeatData : heartbeatDataList) { + sendHeartBeatToCluster(clusterName, heartbeatData, brokerAddrNameMap); + } + this.brokerAddrNameMapRef.set(brokerAddrNameMap); + } + + protected void sendHeartBeatToCluster(String clusterName, HeartbeatData heartbeatData, Map brokerAddrNameMap) { + try { + MessageQueueView messageQueue = this.topicRouteService.getAllMessageQueueView(ProxyContext.createForInner(this.getClass()), clusterName); + List brokerDataList = messageQueue.getTopicRouteData().getBrokerDatas(); + if (brokerDataList == null) { + return; + } + for (BrokerData brokerData : brokerDataList) { + brokerAddrNameMap.put(brokerData.selectBrokerAddr(), brokerData.getBrokerName()); + heartbeatExecutors.submit(() -> { + String brokerAddr = brokerData.selectBrokerAddr(); + this.mqClientAPIFactory.getClient() + .sendHeartbeatOneway(brokerAddr, heartbeatData, Duration.ofSeconds(3).toMillis()) + .exceptionally(t -> { + log.error("Send transactionHeartbeat to broker err. brokerAddr: {}", brokerAddr, t); + return null; + }); + }); + } + } catch (Exception e) { + log.error("get broker add in cluster failed in tx. clusterName: {}", clusterName, e); + } + } + + @Override + protected String getBrokerNameByAddr(String brokerAddr) { + if (StringUtils.isBlank(brokerAddr)) { + return null; + } + return brokerAddrNameMapRef.get().get(brokerAddr); + } + + static class ClusterData { + private final String cluster; + + public ClusterData(String cluster) { + this.cluster = cluster; + } + + public String getCluster() { + return cluster; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof ClusterData)) { + return super.equals(obj); + } + + ClusterData other = (ClusterData) obj; + return cluster.equals(other.cluster); + } + + @Override + public int hashCode() { + return cluster.hashCode(); + } + } + + class TxHeartbeatServiceThread extends ServiceThread { + + @Override + public String getServiceName() { + return TxHeartbeatServiceThread.class.getName(); + } + + @Override + public void run() { + while (!this.isStopped()) { + this.waitForRunning(TimeUnit.SECONDS.toMillis(ConfigurationManager.getProxyConfig().getTransactionHeartbeatPeriodSecond())); + } + } + + @Override + protected void onWaitEnd() { + scanProducerHeartBeat(); + } + } + + @Override + public void start() throws Exception { + ProxyConfig proxyConfig = ConfigurationManager.getProxyConfig(); + txHeartbeatServiceThread = new TxHeartbeatServiceThread(); + + super.start(); + txHeartbeatServiceThread.start(); + heartbeatExecutors = ThreadPoolMonitor.createAndMonitor( + proxyConfig.getTransactionHeartbeatThreadPoolNums(), + proxyConfig.getTransactionHeartbeatThreadPoolNums(), + 0L, TimeUnit.MILLISECONDS, + "TransactionHeartbeatRegisterThread", + proxyConfig.getTransactionHeartbeatThreadPoolQueueCapacity() + ); + } + + @Override + public void shutdown() throws Exception { + txHeartbeatServiceThread.shutdown(); + heartbeatExecutors.shutdown(); + super.shutdown(); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/transaction/EndTransactionRequestData.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/transaction/EndTransactionRequestData.java new file mode 100644 index 00000000000..dbf247640ef --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/transaction/EndTransactionRequestData.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.transaction; + +import org.apache.rocketmq.remoting.protocol.header.EndTransactionRequestHeader; + +public class EndTransactionRequestData { + private String brokerName; + private EndTransactionRequestHeader requestHeader; + + public EndTransactionRequestData(String brokerName, EndTransactionRequestHeader requestHeader) { + this.brokerName = brokerName; + this.requestHeader = requestHeader; + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + public EndTransactionRequestHeader getRequestHeader() { + return requestHeader; + } + + public void setRequestHeader(EndTransactionRequestHeader requestHeader) { + this.requestHeader = requestHeader; + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/transaction/LocalTransactionService.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/transaction/LocalTransactionService.java new file mode 100644 index 00000000000..4a27e4ff24a --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/transaction/LocalTransactionService.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.service.transaction; + +import java.util.List; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.proxy.common.ProxyContext; + +/** + * no need to implements, because the channel of producer will put into the broker's producerManager + */ +public class LocalTransactionService extends AbstractTransactionService { + + protected final BrokerConfig brokerConfig; + + public LocalTransactionService(BrokerConfig brokerConfig) { + this.brokerConfig = brokerConfig; + } + + @Override + public void addTransactionSubscription(ProxyContext ctx, String group, List topicList) { + + } + + @Override + public void addTransactionSubscription(ProxyContext ctx, String group, String topic) { + + } + + @Override + public void replaceTransactionSubscription(ProxyContext ctx, String group, List topicList) { + + } + + @Override + public void unSubscribeAllTransactionTopic(ProxyContext ctx, String group) { + + } + + @Override + protected String getBrokerNameByAddr(String brokerAddr) { + return this.brokerConfig.getBrokerName(); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/transaction/TransactionData.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/transaction/TransactionData.java new file mode 100644 index 00000000000..88fbf44396e --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/transaction/TransactionData.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.transaction; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.collect.ComparisonChain; + +public class TransactionData implements Comparable { + private final String brokerName; + private final long tranStateTableOffset; + private final long commitLogOffset; + private final String transactionId; + private final long checkTimestamp; + private final long expireMs; + + public TransactionData(String brokerName, long tranStateTableOffset, long commitLogOffset, String transactionId, + long checkTimestamp, long expireMs) { + this.brokerName = brokerName; + this.tranStateTableOffset = tranStateTableOffset; + this.commitLogOffset = commitLogOffset; + this.transactionId = transactionId; + this.checkTimestamp = checkTimestamp; + this.expireMs = expireMs; + } + + public String getBrokerName() { + return brokerName; + } + + public long getTranStateTableOffset() { + return tranStateTableOffset; + } + + public long getCommitLogOffset() { + return commitLogOffset; + } + + public String getTransactionId() { + return transactionId; + } + + public long getCheckTimestamp() { + return checkTimestamp; + } + + public long getExpireMs() { + return expireMs; + } + + public long getExpireTime() { + return checkTimestamp + expireMs; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TransactionData data = (TransactionData) o; + return tranStateTableOffset == data.tranStateTableOffset && commitLogOffset == data.commitLogOffset && + getExpireTime() == data.getExpireTime() && Objects.equal(brokerName, data.brokerName) && + Objects.equal(transactionId, data.transactionId); + } + + @Override + public int hashCode() { + return Objects.hashCode(brokerName, transactionId, tranStateTableOffset, commitLogOffset, getExpireTime()); + } + + @Override + public int compareTo(TransactionData o) { + return ComparisonChain.start() + .compare(getExpireTime(), o.getExpireTime()) + .compare(brokerName, o.brokerName) + .compare(commitLogOffset, o.commitLogOffset) + .compare(tranStateTableOffset, o.tranStateTableOffset) + .compare(transactionId, o.transactionId) + .result(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("brokerName", brokerName) + .add("tranStateTableOffset", tranStateTableOffset) + .add("commitLogOffset", commitLogOffset) + .add("transactionId", transactionId) + .add("checkTimestamp", checkTimestamp) + .add("expireMs", expireMs) + .toString(); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/transaction/TransactionDataManager.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/transaction/TransactionDataManager.java new file mode 100644 index 00000000000..81cd26ee9d3 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/transaction/TransactionDataManager.java @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.transaction; + +import java.util.Iterator; +import java.util.Map; +import java.util.NavigableSet; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.common.utils.StartAndShutdown; +import org.apache.rocketmq.proxy.config.ConfigurationManager; + +public class TransactionDataManager implements StartAndShutdown { + private static final Logger log = LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME); + + protected final AtomicLong maxTransactionDataExpireTime = new AtomicLong(System.currentTimeMillis()); + protected final Map> transactionIdDataMap = new ConcurrentHashMap<>(); + protected final TransactionDataCleaner transactionDataCleaner = new TransactionDataCleaner(); + + protected String buildKey(String producerGroup, String transactionId) { + return producerGroup + "@" + transactionId; + } + + public void addTransactionData(String producerGroup, String transactionId, TransactionData transactionData) { + this.transactionIdDataMap.compute(buildKey(producerGroup, transactionId), (key, dataSet) -> { + if (dataSet == null) { + dataSet = new ConcurrentSkipListSet<>(); + } + dataSet.add(transactionData); + if (dataSet.size() > ConfigurationManager.getProxyConfig().getTransactionDataMaxNum()) { + dataSet.pollFirst(); + } + return dataSet; + }); + } + + public TransactionData pollNoExpireTransactionData(String producerGroup, String transactionId) { + AtomicReference res = new AtomicReference<>(); + long currTimestamp = System.currentTimeMillis(); + this.transactionIdDataMap.computeIfPresent(buildKey(producerGroup, transactionId), (key, dataSet) -> { + TransactionData data = dataSet.pollLast(); + while (data != null && data.getExpireTime() < currTimestamp) { + data = dataSet.pollLast(); + } + if (data != null) { + res.set(data); + } + if (dataSet.isEmpty()) { + return null; + } + return dataSet; + }); + return res.get(); + } + + public void removeTransactionData(String producerGroup, String transactionId, TransactionData transactionData) { + this.transactionIdDataMap.computeIfPresent(buildKey(producerGroup, transactionId), (key, dataSet) -> { + dataSet.remove(transactionData); + if (dataSet.isEmpty()) { + return null; + } + return dataSet; + }); + } + + protected void cleanExpireTransactionData() { + long currTimestamp = System.currentTimeMillis(); + Set transactionIdSet = this.transactionIdDataMap.keySet(); + for (String transactionId : transactionIdSet) { + this.transactionIdDataMap.computeIfPresent(transactionId, (transactionIdKey, dataSet) -> { + Iterator iterator = dataSet.iterator(); + while (iterator.hasNext()) { + try { + TransactionData data = iterator.next(); + if (data.getExpireTime() < currTimestamp) { + iterator.remove(); + } else { + break; + } + } catch (NoSuchElementException ignore) { + break; + } + } + if (dataSet.isEmpty()) { + return null; + } + try { + TransactionData maxData = dataSet.last(); + maxTransactionDataExpireTime.set(Math.max(maxTransactionDataExpireTime.get(), maxData.getExpireTime())); + } catch (NoSuchElementException ignore) { + } + return dataSet; + }); + } + } + + protected class TransactionDataCleaner extends ServiceThread { + + @Override + public String getServiceName() { + return "TransactionDataCleaner"; + } + + @Override + public void run() { + log.info(this.getServiceName() + " service started"); + while (!this.isStopped()) { + this.waitForRunning(ConfigurationManager.getProxyConfig().getTransactionDataExpireScanPeriodMillis()); + } + log.info(this.getServiceName() + " service stopped"); + } + + @Override + protected void onWaitEnd() { + cleanExpireTransactionData(); + } + } + + protected void waitTransactionDataClear() throws InterruptedException { + this.cleanExpireTransactionData(); + long waitMs = Math.max(this.maxTransactionDataExpireTime.get() - System.currentTimeMillis(), 0); + waitMs = Math.min(waitMs, ConfigurationManager.getProxyConfig().getTransactionDataMaxWaitClearMillis()); + + if (waitMs > 0) { + TimeUnit.MILLISECONDS.sleep(waitMs); + } + } + + @Override + public void shutdown() throws Exception { + this.transactionDataCleaner.shutdown(); + this.waitTransactionDataClear(); + } + + @Override + public void start() throws Exception { + this.transactionDataCleaner.start(); + } +} diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/service/transaction/TransactionService.java b/proxy/src/main/java/org/apache/rocketmq/proxy/service/transaction/TransactionService.java new file mode 100644 index 00000000000..a7ab3532424 --- /dev/null +++ b/proxy/src/main/java/org/apache/rocketmq/proxy/service/transaction/TransactionService.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.service.transaction; + +import java.util.List; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.proxy.common.ProxyContext; + +public interface TransactionService { + + void addTransactionSubscription(ProxyContext ctx, String group, List topicList); + + void addTransactionSubscription(ProxyContext ctx, String group, String topic); + + void replaceTransactionSubscription(ProxyContext ctx, String group, List topicList); + + void unSubscribeAllTransactionTopic(ProxyContext ctx, String group); + + TransactionData addTransactionDataByBrokerAddr(ProxyContext ctx, String brokerAddr, String producerGroup, long tranStateTableOffset, long commitLogOffset, String transactionId, + Message message); + + TransactionData addTransactionDataByBrokerName(ProxyContext ctx, String brokerName, String producerGroup, long tranStateTableOffset, long commitLogOffset, String transactionId, + Message message); + + EndTransactionRequestData genEndTransactionRequestHeader(ProxyContext ctx, String producerGroup, Integer commitOrRollback, + boolean fromTransactionCheck, String msgId, String transactionId); + + void onSendCheckTransactionStateFailed(ProxyContext context, String producerGroup, TransactionData transactionData); +} diff --git a/proxy/src/main/resources/rmq.proxy.logback.xml b/proxy/src/main/resources/rmq.proxy.logback.xml new file mode 100644 index 00000000000..f968a45e631 --- /dev/null +++ b/proxy/src/main/resources/rmq.proxy.logback.xml @@ -0,0 +1,433 @@ + + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}proxy.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}proxy.%i.log.gz + 1 + 10 + + + 128MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}proxy_watermark.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}proxy_watermark.%i.log.gz + 1 + 10 + + + 128MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8}%m%n + UTF-8 + + + + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}broker_default.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}broker_default.%i.log.gz + 1 + 10 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}broker.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}broker.%i.log.gz + 1 + 20 + + + 128MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}protection.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}protection.%i.log.gz + 1 + 10 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} - %m%n + UTF-8 + + + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}watermark.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}watermark.%i.log.gz + 1 + 10 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} - %m%n + UTF-8 + + + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}store.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}store.%i.log.gz + 1 + 10 + + + 128MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}remoting.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}remoting.%i.log.gz + 1 + 10 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}storeerror.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}storeerror.%i.log.gz + 1 + 10 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}transaction.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}transaction.%i.log.gz + 1 + 10 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}lock.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}lock.%i.log.gz + 1 + 5 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}filter.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}filter.%i.log.gz + 1 + 10 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}stats.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}stats.%i.log.gz + 1 + 5 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p - %m%n + UTF-8 + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}commercial.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}commercial.%i.log.gz + 1 + 10 + + + 500MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p - %m%n + UTF-8 + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}pop.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}pop.%i.log + + 1 + 20 + + + 128MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}proxy_metric.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}proxy_metric.%i.log.gz + 1 + 10 + + + 500MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p - %m%n + UTF-8 + + + + + + + + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/ProxyStartupTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/ProxyStartupTest.java new file mode 100644 index 00000000000..58213df4adf --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/ProxyStartupTest.java @@ -0,0 +1,295 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy; + +import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Iterator; +import java.util.UUID; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.BrokerStartup; +import org.apache.rocketmq.broker.metrics.BrokerMetricsManager; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.proxy.config.Configuration; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.ProxyConfig; +import org.apache.rocketmq.proxy.processor.DefaultMessagingProcessor; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; + +import static org.apache.rocketmq.proxy.config.ConfigurationManager.RMQ_PROXY_HOME; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; + +public class ProxyStartupTest { + + private File proxyHome; + + @Before + public void before() throws Throwable { + proxyHome = new File(System.getProperty("java.io.tmpdir"), UUID.randomUUID().toString().replace('-', '_')); + if (!proxyHome.exists()) { + proxyHome.mkdirs(); + } + String folder = "rmq-proxy-home"; + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(getClass().getClassLoader()); + Resource[] resources = resolver.getResources(String.format("classpath:%s/**/*", folder)); + for (Resource resource : resources) { + if (!resource.isReadable()) { + continue; + } + String description = resource.getDescription(); + int start = description.indexOf('['); + int end = description.lastIndexOf(']'); + String path = description.substring(start + 1, end); + try (InputStream inputStream = resource.getInputStream()) { + copyTo(path, inputStream, proxyHome, folder); + } + } + System.setProperty(RMQ_PROXY_HOME, proxyHome.getAbsolutePath()); + } + + private void copyTo(String path, InputStream src, File dstDir, String flag) throws IOException { + Preconditions.checkNotNull(flag); + Iterator iterator = Splitter.on(File.separatorChar).split(path).iterator(); + boolean found = false; + File dir = dstDir; + while (iterator.hasNext()) { + String current = iterator.next(); + if (!found && flag.equals(current)) { + found = true; + continue; + } + if (found) { + if (!iterator.hasNext()) { + dir = new File(dir, current); + } else { + dir = new File(dir, current); + if (!dir.exists()) { + Assert.assertTrue(dir.mkdir()); + } + } + } + } + + Assert.assertTrue(dir.createNewFile()); + byte[] buffer = new byte[4096]; + BufferedInputStream bis = new BufferedInputStream(src); + int len = 0; + try (BufferedOutputStream bos = new BufferedOutputStream(Files.newOutputStream(dir.toPath()))) { + while ((len = bis.read(buffer)) > 0) { + bos.write(buffer, 0, len); + } + } + } + + private void recursiveDelete(File file) { + if (file.isFile()) { + file.delete(); + return; + } + + File[] files = file.listFiles(); + for (File f : files) { + recursiveDelete(f); + } + file.delete(); + } + + @After + public void after() { + System.clearProperty(RMQ_PROXY_HOME); + System.clearProperty(MixAll.NAMESRV_ADDR_PROPERTY); + System.clearProperty(Configuration.CONFIG_PATH_PROPERTY); + recursiveDelete(proxyHome); + } + + @Test + public void testParseAndInitCommandLineArgument() throws Exception { + Path configFilePath = Files.createTempFile("testParseAndInitCommandLineArgument", ".json"); + String configData = "{}"; + Files.write(configFilePath, configData.getBytes(StandardCharsets.UTF_8)); + + String brokerConfigPath = "brokerConfigPath"; + String proxyConfigPath = configFilePath.toAbsolutePath().toString(); + String proxyMode = "LOCAL"; + String namesrvAddr = "namesrvAddr"; + CommandLineArgument commandLineArgument = ProxyStartup.parseCommandLineArgument(new String[] { + "-bc", brokerConfigPath, + "-pc", proxyConfigPath, + "-pm", proxyMode, + "-n", namesrvAddr + }); + + assertEquals(brokerConfigPath, commandLineArgument.getBrokerConfigPath()); + assertEquals(proxyConfigPath, commandLineArgument.getProxyConfigPath()); + assertEquals(proxyMode, commandLineArgument.getProxyMode()); + assertEquals(namesrvAddr, commandLineArgument.getNamesrvAddr()); + + ProxyStartup.initConfiguration(commandLineArgument); + + ProxyConfig config = ConfigurationManager.getProxyConfig(); + assertEquals(brokerConfigPath, config.getBrokerConfigPath()); + assertEquals(proxyMode, config.getProxyMode()); + assertEquals(namesrvAddr, config.getNamesrvAddr()); + } + + @Test + public void testLocalModeWithNameSrvAddrByProperty() throws Exception { + String namesrvAddr = "namesrvAddr"; + System.setProperty(MixAll.NAMESRV_ADDR_PROPERTY, namesrvAddr); + CommandLineArgument commandLineArgument = ProxyStartup.parseCommandLineArgument(new String[] { + "-pm", "local" + }); + ProxyStartup.initConfiguration(commandLineArgument); + + ProxyConfig config = ConfigurationManager.getProxyConfig(); + assertEquals(namesrvAddr, config.getNamesrvAddr()); + + validateBrokerCreateArgsWithNamsrvAddr(config, namesrvAddr); + } + + private void validateBrokerCreateArgsWithNamsrvAddr(ProxyConfig config, String namesrvAddr) { + try (MockedStatic brokerStartupMocked = mockStatic(BrokerStartup.class); + MockedStatic messagingProcessorMocked = mockStatic(DefaultMessagingProcessor.class)) { + ArgumentCaptor args = ArgumentCaptor.forClass(Object.class); + BrokerController brokerControllerMocked = mock(BrokerController.class); + BrokerMetricsManager brokerMetricsManagerMocked = mock(BrokerMetricsManager.class); + Mockito.when(brokerMetricsManagerMocked.getBrokerMeter()).thenReturn(OpenTelemetrySdk.builder().build().getMeter("test")); + Mockito.when(brokerControllerMocked.getBrokerMetricsManager()).thenReturn(brokerMetricsManagerMocked); + brokerStartupMocked.when(() -> BrokerStartup.createBrokerController((String[]) args.capture())) + .thenReturn(brokerControllerMocked); + messagingProcessorMocked.when(() -> DefaultMessagingProcessor.createForLocalMode(any(), any())) + .thenReturn(mock(DefaultMessagingProcessor.class)); + + ProxyStartup.createMessagingProcessor(); + String[] passArgs = (String[]) args.getValue(); + assertEquals("-c", passArgs[0]); + assertEquals(config.getBrokerConfigPath(), passArgs[1]); + assertEquals("-n", passArgs[2]); + assertEquals(namesrvAddr, passArgs[3]); + assertEquals(4, passArgs.length); + } + } + + @Test + public void testLocalModeWithNameSrvAddrByConfigFile() throws Exception { + String namesrvAddr = "namesrvAddr"; + System.setProperty(MixAll.NAMESRV_ADDR_PROPERTY, "foo"); + Path configFilePath = Files.createTempFile("testLocalModeWithNameSrvAddrByConfigFile", ".json"); + String configData = "{\n" + + " \"namesrvAddr\": \"namesrvAddr\"\n" + + "}"; + Files.write(configFilePath, configData.getBytes(StandardCharsets.UTF_8)); + + CommandLineArgument commandLineArgument = ProxyStartup.parseCommandLineArgument(new String[] { + "-pm", "local", + "-pc", configFilePath.toAbsolutePath().toString() + }); + ProxyStartup.initConfiguration(commandLineArgument); + + ProxyConfig config = ConfigurationManager.getProxyConfig(); + assertEquals(namesrvAddr, config.getNamesrvAddr()); + + validateBrokerCreateArgsWithNamsrvAddr(config, namesrvAddr); + } + + @Test + public void testLocalModeWithNameSrvAddrByCommandLine() throws Exception { + String namesrvAddr = "namesrvAddr"; + System.setProperty(MixAll.NAMESRV_ADDR_PROPERTY, "foo"); + Path configFilePath = Files.createTempFile("testLocalModeWithNameSrvAddrByCommandLine", ".json"); + String configData = "{\n" + + " \"namesrvAddr\": \"foo\"\n" + + "}"; + Files.write(configFilePath, configData.getBytes(StandardCharsets.UTF_8)); + + CommandLineArgument commandLineArgument = ProxyStartup.parseCommandLineArgument(new String[] { + "-pm", "local", + "-pc", configFilePath.toAbsolutePath().toString(), + "-n", namesrvAddr + }); + ProxyStartup.initConfiguration(commandLineArgument); + + ProxyConfig config = ConfigurationManager.getProxyConfig(); + assertEquals(namesrvAddr, config.getNamesrvAddr()); + + validateBrokerCreateArgsWithNamsrvAddr(config, namesrvAddr); + } + + @Test + public void testLocalModeWithAllArgs() throws Exception { + String namesrvAddr = "namesrvAddr"; + System.setProperty(MixAll.NAMESRV_ADDR_PROPERTY, "foo"); + Path configFilePath = Files.createTempFile("testLocalMode", ".json"); + String configData = "{\n" + + " \"namesrvAddr\": \"foo\"\n" + + "}"; + Files.write(configFilePath, configData.getBytes(StandardCharsets.UTF_8)); + Path brokerConfigFilePath = Files.createTempFile("testLocalModeBrokerConfig", ".json"); + + CommandLineArgument commandLineArgument = ProxyStartup.parseCommandLineArgument(new String[] { + "-pm", "local", + "-pc", configFilePath.toAbsolutePath().toString(), + "-n", namesrvAddr, + "-bc", brokerConfigFilePath.toAbsolutePath().toString() + }); + ProxyStartup.initConfiguration(commandLineArgument); + + ProxyConfig config = ConfigurationManager.getProxyConfig(); + assertEquals(namesrvAddr, config.getNamesrvAddr()); + assertEquals(brokerConfigFilePath.toAbsolutePath().toString(), config.getBrokerConfigPath()); + + validateBrokerCreateArgsWithNamsrvAddr(config, namesrvAddr); + } + + @Test + public void testClusterMode() throws Exception { + CommandLineArgument commandLineArgument = ProxyStartup.parseCommandLineArgument(new String[] { + "-pm", "cluster" + }); + ProxyStartup.initConfiguration(commandLineArgument); + + try (MockedStatic messagingProcessorMocked = mockStatic(DefaultMessagingProcessor.class)) { + DefaultMessagingProcessor processor = mock(DefaultMessagingProcessor.class); + messagingProcessorMocked.when(DefaultMessagingProcessor::createForClusterMode) + .thenReturn(processor); + + assertSame(processor, ProxyStartup.createMessagingProcessor()); + } + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/common/ReceiptHandleGroupTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/common/ReceiptHandleGroupTest.java new file mode 100644 index 00000000000..0a7e2f757d4 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/common/ReceiptHandleGroupTest.java @@ -0,0 +1,315 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.common; + +import java.time.Duration; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.rocketmq.common.consumer.ReceiptHandle; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.proxy.common.utils.FutureUtils; +import org.apache.rocketmq.proxy.config.InitConfigTest; +import org.junit.Before; +import org.junit.Test; + +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class ReceiptHandleGroupTest extends InitConfigTest { + + private static final String TOPIC = "topic"; + private static final String GROUP = "group"; + private ReceiptHandleGroup receiptHandleGroup; + private String msgID; + private final Random random = new Random(); + + @Before + public void before() throws Throwable { + super.before(); + receiptHandleGroup = new ReceiptHandleGroup(); + msgID = MessageClientIDSetter.createUniqID(); + } + + protected String createHandle() { + return ReceiptHandle.builder() + .startOffset(0L) + .retrieveTime(System.currentTimeMillis()) + .invisibleTime(3000) + .reviveQueueId(1) + .topicType(ReceiptHandle.NORMAL_TOPIC) + .brokerName("brokerName") + .queueId(random.nextInt(10)) + .offset(random.nextInt(10)) + .commitLogOffset(0L) + .build().encode(); + } + + @Test + public void testAddDuplicationHandle() { + String handle1 = ReceiptHandle.builder() + .startOffset(0L) + .retrieveTime(System.currentTimeMillis()) + .invisibleTime(3000) + .reviveQueueId(1) + .topicType(ReceiptHandle.NORMAL_TOPIC) + .brokerName("brokerName") + .queueId(1) + .offset(123) + .commitLogOffset(0L) + .build().encode(); + String handle2 = ReceiptHandle.builder() + .startOffset(0L) + .retrieveTime(System.currentTimeMillis() + 1000) + .invisibleTime(3000) + .reviveQueueId(1) + .topicType(ReceiptHandle.NORMAL_TOPIC) + .brokerName("brokerName") + .queueId(1) + .offset(123) + .commitLogOffset(0L) + .build().encode(); + + receiptHandleGroup.put(msgID, createMessageReceiptHandle(handle1, msgID)); + receiptHandleGroup.put(msgID, createMessageReceiptHandle(handle2, msgID)); + + assertEquals(1, receiptHandleGroup.receiptHandleMap.get(msgID).size()); + } + + @Test + public void testGetWhenComputeIfPresent() { + String handle1 = createHandle(); + String handle2 = createHandle(); + AtomicReference getHandleRef = new AtomicReference<>(); + + receiptHandleGroup.put(msgID, createMessageReceiptHandle(handle1, msgID)); + CountDownLatch latch = new CountDownLatch(2); + Thread getThread = new Thread(() -> { + try { + latch.countDown(); + latch.await(); + getHandleRef.set(receiptHandleGroup.get(msgID, handle1)); + } catch (Exception ignored) { + } + }, "getThread"); + Thread computeThread = new Thread(() -> { + try { + receiptHandleGroup.computeIfPresent(msgID, handle1, messageReceiptHandle -> { + try { + latch.countDown(); + latch.await(); + } catch (Exception ignored) { + } + messageReceiptHandle.updateReceiptHandle(handle2); + return FutureUtils.addExecutor(CompletableFuture.completedFuture(messageReceiptHandle), Executors.newCachedThreadPool()); + }); + } catch (Exception ignored) { + } + }, "computeThread"); + getThread.start(); + computeThread.start(); + + await().atMost(Duration.ofSeconds(1)).until(() -> getHandleRef.get() != null); + assertEquals(handle2, getHandleRef.get().getReceiptHandleStr()); + assertFalse(receiptHandleGroup.isEmpty()); + } + + @Test + public void testGetWhenComputeIfPresentReturnNull() { + String handle1 = createHandle(); + AtomicBoolean getCalled = new AtomicBoolean(false); + AtomicReference getHandleRef = new AtomicReference<>(); + + receiptHandleGroup.put(msgID, createMessageReceiptHandle(handle1, msgID)); + CountDownLatch latch = new CountDownLatch(2); + Thread getThread = new Thread(() -> { + try { + latch.countDown(); + latch.await(); + getHandleRef.set(receiptHandleGroup.get(msgID, handle1)); + getCalled.set(true); + } catch (Exception ignored) { + } + }, "getThread"); + Thread computeThread = new Thread(() -> { + try { + receiptHandleGroup.computeIfPresent(msgID, handle1, messageReceiptHandle -> { + try { + latch.countDown(); + latch.await(); + } catch (Exception ignored) { + } + return FutureUtils.addExecutor(CompletableFuture.completedFuture(null), Executors.newCachedThreadPool()); + }); + } catch (Exception ignored) { + } + }, "computeThread"); + getThread.start(); + computeThread.start(); + + await().atMost(Duration.ofSeconds(1)).until(getCalled::get); + assertNull(getHandleRef.get()); + assertTrue(receiptHandleGroup.isEmpty()); + } + + @Test + public void testRemoveWhenComputeIfPresent() { + String handle1 = createHandle(); + String handle2 = createHandle(); + AtomicReference removeHandleRef = new AtomicReference<>(); + + receiptHandleGroup.put(msgID, createMessageReceiptHandle(handle1, msgID)); + CountDownLatch latch = new CountDownLatch(2); + Thread removeThread = new Thread(() -> { + try { + latch.countDown(); + latch.await(); + removeHandleRef.set(receiptHandleGroup.remove(msgID, handle1)); + } catch (Exception ignored) { + } + }, "removeThread"); + Thread computeThread = new Thread(() -> { + try { + receiptHandleGroup.computeIfPresent(msgID, handle1, messageReceiptHandle -> { + try { + latch.countDown(); + latch.await(); + } catch (Exception ignored) { + } + messageReceiptHandle.updateReceiptHandle(handle2); + return FutureUtils.addExecutor(CompletableFuture.completedFuture(messageReceiptHandle), Executors.newCachedThreadPool()); + }); + } catch (Exception ignored) { + } + }, "computeThread"); + removeThread.start(); + computeThread.start(); + + await().atMost(Duration.ofSeconds(1)).until(() -> removeHandleRef.get() != null); + assertEquals(handle2, removeHandleRef.get().getReceiptHandleStr()); + assertTrue(receiptHandleGroup.isEmpty()); + } + + @Test + public void testRemoveWhenComputeIfPresentReturnNull() { + String handle1 = createHandle(); + AtomicBoolean removeCalled = new AtomicBoolean(false); + AtomicReference removeHandleRef = new AtomicReference<>(); + + receiptHandleGroup.put(msgID, createMessageReceiptHandle(handle1, msgID)); + CountDownLatch latch = new CountDownLatch(2); + Thread removeThread = new Thread(() -> { + try { + latch.countDown(); + latch.await(); + removeHandleRef.set(receiptHandleGroup.remove(msgID, handle1)); + removeCalled.set(true); + } catch (Exception ignored) { + } + }, "removeThread"); + Thread computeThread = new Thread(() -> { + try { + receiptHandleGroup.computeIfPresent(msgID, handle1, messageReceiptHandle -> { + try { + latch.countDown(); + latch.await(); + } catch (Exception ignored) { + } + return FutureUtils.addExecutor(CompletableFuture.completedFuture(null), Executors.newCachedThreadPool()); + }); + } catch (Exception ignored) { + } + }, "computeThread"); + removeThread.start(); + computeThread.start(); + + await().atMost(Duration.ofSeconds(1)).until(removeCalled::get); + assertNull(removeHandleRef.get()); + assertTrue(receiptHandleGroup.isEmpty()); + } + + @Test + public void testRemoveMultiThread() { + String handle1 = createHandle(); + AtomicReference removeHandleRef = new AtomicReference<>(); + AtomicInteger count = new AtomicInteger(); + + receiptHandleGroup.put(msgID, createMessageReceiptHandle(handle1, msgID)); + int threadNum = Math.max(Runtime.getRuntime().availableProcessors(), 3); + CountDownLatch latch = new CountDownLatch(threadNum); + for (int i = 0; i < threadNum; i++) { + Thread thread = new Thread(() -> { + try { + latch.countDown(); + latch.await(); + MessageReceiptHandle handle = receiptHandleGroup.remove(msgID, handle1); + if (handle != null) { + removeHandleRef.set(handle); + count.incrementAndGet(); + } + } catch (Exception ignored) { + } + }); + thread.start(); + } + + await().atMost(Duration.ofSeconds(1)).untilAsserted(() -> assertEquals(1, count.get())); + assertEquals(handle1, removeHandleRef.get().getReceiptHandleStr()); + assertTrue(receiptHandleGroup.isEmpty()); + } + + @Test + public void testRemoveOne() { + String handle1 = createHandle(); + AtomicReference removeHandleRef = new AtomicReference<>(); + AtomicInteger count = new AtomicInteger(); + + receiptHandleGroup.put(msgID, createMessageReceiptHandle(handle1, msgID)); + int threadNum = Math.max(Runtime.getRuntime().availableProcessors(), 3); + CountDownLatch latch = new CountDownLatch(threadNum); + for (int i = 0; i < threadNum; i++) { + Thread thread = new Thread(() -> { + try { + latch.countDown(); + latch.await(); + MessageReceiptHandle handle = receiptHandleGroup.removeOne(msgID); + if (handle != null) { + removeHandleRef.set(handle); + count.incrementAndGet(); + } + } catch (Exception ignored) { + } + }); + thread.start(); + } + + await().atMost(Duration.ofSeconds(1)).untilAsserted(() -> assertEquals(1, count.get())); + assertEquals(handle1, removeHandleRef.get().getReceiptHandleStr()); + assertTrue(receiptHandleGroup.isEmpty()); + } + + private MessageReceiptHandle createMessageReceiptHandle(String handle, String msgID) { + return new MessageReceiptHandle(GROUP, TOPIC, 0, handle, msgID, 0, 0); + } +} \ No newline at end of file diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/common/RenewStrategyPolicyTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/common/RenewStrategyPolicyTest.java new file mode 100644 index 00000000000..54e6272749a --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/common/RenewStrategyPolicyTest.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.common; + +import org.apache.rocketmq.remoting.protocol.subscription.RetryPolicy; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; + +public class RenewStrategyPolicyTest { + + private RetryPolicy retryPolicy; + private final AtomicInteger times = new AtomicInteger(0); + + @Before + public void before() throws Throwable { + this.retryPolicy = new RenewStrategyPolicy(); + } + + @Test + public void testNextDelayDuration() { + long value = this.retryPolicy.nextDelayDuration(times.getAndIncrement()); + assertEquals(value, TimeUnit.MINUTES.toMillis(1)); + + value = this.retryPolicy.nextDelayDuration(times.getAndIncrement()); + assertEquals(value, TimeUnit.MINUTES.toMillis(3)); + + value = this.retryPolicy.nextDelayDuration(times.getAndIncrement()); + assertEquals(value, TimeUnit.MINUTES.toMillis(5)); + + value = this.retryPolicy.nextDelayDuration(times.getAndIncrement()); + assertEquals(value, TimeUnit.MINUTES.toMillis(10)); + + value = this.retryPolicy.nextDelayDuration(times.getAndIncrement()); + assertEquals(value, TimeUnit.MINUTES.toMillis(30)); + + value = this.retryPolicy.nextDelayDuration(times.getAndIncrement()); + assertEquals(value, TimeUnit.HOURS.toMillis(1)); + } + + + @After + public void after() { + } + +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/common/utils/FilterUtilTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/common/utils/FilterUtilTest.java new file mode 100644 index 00000000000..7c9d84015a7 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/common/utils/FilterUtilTest.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.common.utils; + +import org.apache.rocketmq.remoting.protocol.filter.FilterAPI; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class FilterUtilTest { + @Test + public void testTagMatched() throws Exception { + SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData("", "tagA"); + assertThat(FilterUtils.isTagMatched(subscriptionData.getTagsSet(), "tagA")).isTrue(); + } + + @Test + public void testTagNotMatched() throws Exception { + SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData("", "tagA"); + assertThat(FilterUtils.isTagMatched(subscriptionData.getTagsSet(), "tagB")).isFalse(); + } + + @Test + public void testTagMatchedStar() throws Exception { + SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData("", "*"); + assertThat(FilterUtils.isTagMatched(subscriptionData.getTagsSet(), "tagA")).isTrue(); + } + + @Test + public void testTagNotMatchedNull() throws Exception { + SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData("", "tagA"); + assertThat(FilterUtils.isTagMatched(subscriptionData.getTagsSet(), null)).isFalse(); + } + + @Test + public void testBuildSubscriptionData() throws Exception { + // Test case 1: expressionType is null, will use TAG as default. + String topic = "topic"; + String subString = "substring"; + String expressionType = null; + SubscriptionData result = FilterAPI.buildSubscriptionData(topic, subString, expressionType); + assertThat(result).isNotNull(); + assertThat(topic).isEqualTo(result.getTopic()); + assertThat(subString).isEqualTo(result.getSubString()); + assertThat(result.getExpressionType()).isEqualTo("TAG"); + assertThat(result.getCodeSet().size()).isEqualTo(1); + + // Test case 2: expressionType is not null + topic = "topic"; + subString = "substring1||substring2"; + expressionType = "SQL92"; + result = FilterAPI.buildSubscriptionData(topic, subString, expressionType); + assertThat(result).isNotNull(); + assertThat(topic).isEqualTo(result.getTopic()); + assertThat(subString).isEqualTo(result.getSubString()); + assertThat(result.getExpressionType()).isEqualTo(expressionType); + assertThat(result.getCodeSet().size()).isEqualTo(2); + } + +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/config/ConfigurationManagerTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/config/ConfigurationManagerTest.java new file mode 100644 index 00000000000..74803609ba5 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/config/ConfigurationManagerTest.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.config; + +import org.apache.rocketmq.proxy.ProxyMode; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ConfigurationManagerTest extends InitConfigTest { + + @Test + public void testIntConfig() { + assertThat(ConfigurationManager.getProxyConfig()).isNotNull(); + assertThat(ConfigurationManager.getProxyConfig().getProxyMode()).isEqualToIgnoringCase(ProxyMode.CLUSTER.toString()); + + String brokerConfig = ConfigurationManager.getProxyConfig().getBrokerConfigPath(); + assertThat(brokerConfig).isEqualTo(ConfigurationManager.getProxyHome() + "/conf/broker.conf"); + } + + @Test + public void testGetProxyHome() { + // test configured proxy home + assertThat(ConfigurationManager.getProxyHome()).isIn(mockProxyHome, "./"); + } + + @Test + public void testGetProxyConfig() { + assertThat(ConfigurationManager.getProxyConfig()).isNotNull(); + } + +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/config/InitConfigTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/config/InitConfigTest.java new file mode 100644 index 00000000000..d71d163ac69 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/config/InitConfigTest.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.config; + +import java.net.URL; +import org.assertj.core.util.Strings; + +import org.junit.After; +import org.junit.Before; + +import static org.apache.rocketmq.proxy.config.ConfigurationManager.RMQ_PROXY_HOME; + +public class InitConfigTest { + public static String mockProxyHome = "/mock/rmq/proxy/home"; + + @Before + public void before() throws Throwable { + URL mockProxyHomeURL = getClass().getClassLoader().getResource("rmq-proxy-home"); + if (mockProxyHomeURL != null) { + mockProxyHome = mockProxyHomeURL.toURI().getPath(); + } + + if (!Strings.isNullOrEmpty(mockProxyHome)) { + System.setProperty(RMQ_PROXY_HOME, mockProxyHome); + } + + ConfigurationManager.initEnv(); + ConfigurationManager.intConfig(); + } + + @After + public void after() { + System.clearProperty(RMQ_PROXY_HOME); + } +} \ No newline at end of file diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/config/MetricCollectorModeTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/config/MetricCollectorModeTest.java new file mode 100644 index 00000000000..60c754e607c --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/config/MetricCollectorModeTest.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.config; + +import org.junit.Assert; +import org.junit.Test; + +public class MetricCollectorModeTest { + + @Test + public void testGetEnumByOrdinal() { + Assert.assertEquals(MetricCollectorMode.OFF, MetricCollectorMode.getEnumByString("off")); + Assert.assertEquals(MetricCollectorMode.ON, MetricCollectorMode.getEnumByString("on")); + Assert.assertEquals(MetricCollectorMode.PROXY, MetricCollectorMode.getEnumByString("proxy")); + + Assert.assertEquals(MetricCollectorMode.OFF, MetricCollectorMode.getEnumByString("OFF")); + Assert.assertEquals(MetricCollectorMode.ON, MetricCollectorMode.getEnumByString("ON")); + Assert.assertEquals(MetricCollectorMode.PROXY, MetricCollectorMode.getEnumByString("PROXY")); + } + +} \ No newline at end of file diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/ProxyAndTlsProtocolNegotiatorTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/ProxyAndTlsProtocolNegotiatorTest.java new file mode 100644 index 00000000000..699491f03d1 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/ProxyAndTlsProtocolNegotiatorTest.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.grpc; + +import io.grpc.Attributes; +import io.grpc.netty.shaded.io.netty.buffer.ByteBuf; +import io.grpc.netty.shaded.io.netty.buffer.Unpooled; +import io.grpc.netty.shaded.io.netty.handler.codec.haproxy.HAProxyTLV; +import java.nio.charset.StandardCharsets; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class ProxyAndTlsProtocolNegotiatorTest { + + private ProxyAndTlsProtocolNegotiator negotiator; + + @Before + public void setUp() throws Exception { + ConfigurationManager.intConfig(); + ConfigurationManager.getProxyConfig().setTlsTestModeEnable(true); + negotiator = new ProxyAndTlsProtocolNegotiator(); + } + + @Test + public void handleHAProxyTLV() { + ByteBuf content = Unpooled.buffer(); + content.writeBytes("xxxx".getBytes(StandardCharsets.UTF_8)); + HAProxyTLV haProxyTLV = new HAProxyTLV((byte) 0xE1, content); + negotiator.handleHAProxyTLV(haProxyTLV, Attributes.newBuilder()); + } +} \ No newline at end of file diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/AbstractMessingActivityTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/AbstractMessingActivityTest.java new file mode 100644 index 00000000000..3c2967357f0 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/AbstractMessingActivityTest.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.v2; + +import apache.rocketmq.v2.Resource; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.proxy.config.InitConfigTest; +import org.apache.rocketmq.proxy.grpc.v2.channel.GrpcChannelManager; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcClientSettingsManager; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcProxyException; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertThrows; + +public class AbstractMessingActivityTest extends InitConfigTest { + + public static class MockMessingActivity extends AbstractMessingActivity { + + public MockMessingActivity(MessagingProcessor messagingProcessor, + GrpcClientSettingsManager grpcClientSettingsManager, + GrpcChannelManager grpcChannelManager) { + super(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); + } + } + + private AbstractMessingActivity messingActivity; + + @Before + public void before() throws Throwable { + super.before(); + this.messingActivity = new MockMessingActivity(null, null, null); + } + + @Test + public void testValidateTopic() { + assertThrows(GrpcProxyException.class, () -> messingActivity.validateTopic(Resource.newBuilder().build())); + assertThrows(GrpcProxyException.class, () -> messingActivity.validateTopic(Resource.newBuilder().setName(TopicValidator.RMQ_SYS_TRACE_TOPIC).build())); + assertThrows(GrpcProxyException.class, () -> messingActivity.validateTopic(Resource.newBuilder().setName("@").build())); + assertThrows(GrpcProxyException.class, () -> messingActivity.validateTopic(Resource.newBuilder().setName(createString(128)).build())); + messingActivity.validateTopic(Resource.newBuilder().setName(createString(127)).build()); + } + + @Test + public void testValidateConsumer() { + assertThrows(GrpcProxyException.class, () -> messingActivity.validateConsumerGroup(Resource.newBuilder().build())); + assertThrows(GrpcProxyException.class, () -> messingActivity.validateConsumerGroup(Resource.newBuilder().setName(MixAll.CID_SYS_RMQ_TRANS).build())); + assertThrows(GrpcProxyException.class, () -> messingActivity.validateConsumerGroup(Resource.newBuilder().setName("@").build())); + assertThrows(GrpcProxyException.class, () -> messingActivity.validateConsumerGroup(Resource.newBuilder().setName(createString(256)).build())); + messingActivity.validateConsumerGroup(Resource.newBuilder().setName(createString(255)).build()); + } + + private static String createString(int len) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < len; i++) { + sb.append('a'); + } + return sb.toString(); + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/BaseActivityTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/BaseActivityTest.java new file mode 100644 index 00000000000..524945bd6fe --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/BaseActivityTest.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.v2; + +import io.grpc.Metadata; +import java.time.Duration; +import java.util.Random; +import java.util.UUID; +import org.apache.rocketmq.proxy.common.ContextVariable; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.InitConfigTest; +import org.apache.rocketmq.proxy.grpc.interceptor.InterceptorConstants; +import org.apache.rocketmq.proxy.grpc.v2.channel.GrpcChannelManager; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcClientSettingsManager; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.proxy.processor.ReceiptHandleProcessor; +import org.apache.rocketmq.proxy.service.metadata.MetadataService; +import org.apache.rocketmq.proxy.service.relay.ProxyRelayService; +import org.apache.rocketmq.remoting.protocol.header.ExtraInfoUtil; +import org.junit.Ignore; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@Ignore +@RunWith(MockitoJUnitRunner.Silent.class) +public class BaseActivityTest extends InitConfigTest { + protected static final Random RANDOM = new Random(); + protected MessagingProcessor messagingProcessor; + protected GrpcClientSettingsManager grpcClientSettingsManager; + protected GrpcChannelManager grpcChannelManager; + protected ProxyRelayService proxyRelayService; + protected ReceiptHandleProcessor receiptHandleProcessor; + protected MetadataService metadataService; + + protected static final String REMOTE_ADDR = "192.168.0.1:8080"; + protected static final String LOCAL_ADDR = "127.0.0.1:8080"; + protected Metadata metadata = new Metadata(); + + protected static final String CLIENT_ID = "client-id" + UUID.randomUUID(); + protected static final String JAVA = "JAVA"; + + public void before() throws Throwable { + super.before(); + messagingProcessor = mock(MessagingProcessor.class); + grpcClientSettingsManager = mock(GrpcClientSettingsManager.class); + proxyRelayService = mock(ProxyRelayService.class); + receiptHandleProcessor = mock(ReceiptHandleProcessor.class); + metadataService = mock(MetadataService.class); + + metadata.put(InterceptorConstants.CLIENT_ID, CLIENT_ID); + metadata.put(InterceptorConstants.LANGUAGE, JAVA); + metadata.put(InterceptorConstants.REMOTE_ADDRESS, REMOTE_ADDR); + metadata.put(InterceptorConstants.LOCAL_ADDRESS, LOCAL_ADDR); + when(messagingProcessor.getProxyRelayService()).thenReturn(proxyRelayService); + when(messagingProcessor.getMetadataService()).thenReturn(metadataService); + grpcChannelManager = new GrpcChannelManager(messagingProcessor.getProxyRelayService(), grpcClientSettingsManager); + } + + protected ProxyContext createContext() { + return ProxyContext.create() + .withVal(ContextVariable.CLIENT_ID, CLIENT_ID) + .withVal(ContextVariable.LANGUAGE, JAVA) + .withVal(ContextVariable.REMOTE_ADDRESS, REMOTE_ADDR) + .withVal(ContextVariable.LOCAL_ADDRESS, LOCAL_ADDR) + .withVal(ContextVariable.REMAINING_MS, Duration.ofSeconds(10).toMillis()); + } + + protected static String buildReceiptHandle(String topic, long popTime, long invisibleTime) { + return ExtraInfoUtil.buildExtraInfo( + RANDOM.nextInt(Integer.MAX_VALUE), + popTime, + invisibleTime, + 0, + topic, + "brokerName", + RANDOM.nextInt(8), + RANDOM.nextInt(Integer.MAX_VALUE) + ); + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/GrpcMessagingApplicationTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/GrpcMessagingApplicationTest.java new file mode 100644 index 00000000000..7ba03fdbf1f --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/GrpcMessagingApplicationTest.java @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.v2; + +import apache.rocketmq.v2.Address; +import apache.rocketmq.v2.AddressScheme; +import apache.rocketmq.v2.Code; +import apache.rocketmq.v2.Endpoints; +import apache.rocketmq.v2.MessageQueue; +import apache.rocketmq.v2.QueryRouteRequest; +import apache.rocketmq.v2.QueryRouteResponse; +import apache.rocketmq.v2.Resource; +import io.grpc.Context; +import io.grpc.Metadata; +import io.grpc.stub.StreamObserver; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.InitConfigTest; +import org.apache.rocketmq.proxy.grpc.interceptor.InterceptorConstants; +import org.apache.rocketmq.proxy.grpc.v2.common.ResponseBuilder; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; + +@RunWith(MockitoJUnitRunner.class) +public class GrpcMessagingApplicationTest extends InitConfigTest { + protected static final String REMOTE_ADDR = "192.168.0.1:8080"; + protected static final String LOCAL_ADDR = "127.0.0.1:8080"; + protected static final String CLIENT_ID = "client-id" + UUID.randomUUID(); + protected static final String JAVA = "JAVA"; + @Mock + StreamObserver queryRouteResponseStreamObserver; + @Mock + GrpcMessingActivity grpcMessingActivity; + GrpcMessagingApplication grpcMessagingApplication; + + private static final String TOPIC = "topic"; + private static Endpoints grpcEndpoints = Endpoints.newBuilder() + .setScheme(AddressScheme.IPv4) + .addAddresses(Address.newBuilder().setHost("127.0.0.1").setPort(8080).build()) + .addAddresses(Address.newBuilder().setHost("127.0.0.2").setPort(8080).build()) + .build(); + + @Before + public void setUp() throws Throwable { + super.before(); + grpcMessagingApplication = new GrpcMessagingApplication(grpcMessingActivity); + } + + @Test + public void testQueryRoute() { + Metadata metadata = new Metadata(); + metadata.put(InterceptorConstants.CLIENT_ID, CLIENT_ID); + metadata.put(InterceptorConstants.LANGUAGE, JAVA); + metadata.put(InterceptorConstants.REMOTE_ADDRESS, REMOTE_ADDR); + metadata.put(InterceptorConstants.LOCAL_ADDRESS, LOCAL_ADDR); + + Assert.assertNotNull(Context.current() + .withValue(InterceptorConstants.METADATA, metadata) + .attach()); + + CompletableFuture future = new CompletableFuture<>(); + QueryRouteRequest request = QueryRouteRequest.newBuilder() + .setEndpoints(grpcEndpoints) + .setTopic(Resource.newBuilder().setName(TOPIC).build()) + .build(); + Mockito.when(grpcMessingActivity.queryRoute(Mockito.any(ProxyContext.class), Mockito.eq(request))) + .thenReturn(future); + QueryRouteResponse response = QueryRouteResponse.newBuilder() + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.OK, Code.OK.name())) + .addMessageQueues(MessageQueue.getDefaultInstance()) + .build(); + grpcMessagingApplication.queryRoute(request, queryRouteResponseStreamObserver); + future.complete(response); + await().untilAsserted(() -> { + Mockito.verify(queryRouteResponseStreamObserver, Mockito.times(1)).onNext(Mockito.same(response)); + }); + } + + @Test + public void testQueryRouteWithBadClientID() { + Metadata metadata = new Metadata(); + metadata.put(InterceptorConstants.LANGUAGE, JAVA); + metadata.put(InterceptorConstants.REMOTE_ADDRESS, REMOTE_ADDR); + metadata.put(InterceptorConstants.LOCAL_ADDRESS, LOCAL_ADDR); + + Assert.assertNotNull(Context.current() + .withValue(InterceptorConstants.METADATA, metadata) + .attach()); + + QueryRouteRequest request = QueryRouteRequest.newBuilder() + .setEndpoints(grpcEndpoints) + .setTopic(Resource.newBuilder().setName(TOPIC).build()) + .build(); + grpcMessagingApplication.queryRoute(request, queryRouteResponseStreamObserver); + + ArgumentCaptor responseArgumentCaptor = ArgumentCaptor.forClass(QueryRouteResponse.class); + await().untilAsserted(() -> { + Mockito.verify(queryRouteResponseStreamObserver, Mockito.times(1)).onNext(responseArgumentCaptor.capture()); + }); + + assertEquals(Code.CLIENT_ID_REQUIRED, responseArgumentCaptor.getValue().getStatus().getCode()); + } +} \ No newline at end of file diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/channel/GrpcClientChannelTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/channel/GrpcClientChannelTest.java new file mode 100644 index 00000000000..1bdbdd9befe --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/channel/GrpcClientChannelTest.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.v2.channel; + +import apache.rocketmq.v2.Publishing; +import apache.rocketmq.v2.Resource; +import apache.rocketmq.v2.Settings; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.InitConfigTest; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcClientSettingsManager; +import org.apache.rocketmq.proxy.processor.channel.ChannelProtocolType; +import org.apache.rocketmq.proxy.processor.channel.RemoteChannel; +import org.apache.rocketmq.proxy.remoting.channel.RemotingChannel; +import org.apache.rocketmq.proxy.service.relay.ProxyRelayService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class GrpcClientChannelTest extends InitConfigTest { + + @Mock + private ProxyRelayService proxyRelayService; + @Mock + private GrpcClientSettingsManager grpcClientSettingsManager; + @Mock + private GrpcChannelManager grpcChannelManager; + + private String clientId; + private GrpcClientChannel grpcClientChannel; + + @Before + public void before() throws Throwable { + super.before(); + this.clientId = RandomStringUtils.randomAlphabetic(10); + this.grpcClientChannel = new GrpcClientChannel(proxyRelayService, grpcClientSettingsManager, grpcChannelManager, + ProxyContext.create().setRemoteAddress("10.152.39.53:9768").setLocalAddress("11.193.0.1:1210"), + this.clientId); + } + + @Test + public void testChannelExtendAttributeParse() { + Settings clientSettings = Settings.newBuilder() + .setPublishing(Publishing.newBuilder() + .addTopics(Resource.newBuilder() + .setName("topic") + .build()) + .build()) + .build(); + when(grpcClientSettingsManager.getRawClientSettings(eq(clientId))).thenReturn(clientSettings); + + RemoteChannel remoteChannel = this.grpcClientChannel.toRemoteChannel(); + assertEquals(ChannelProtocolType.GRPC_V2, remoteChannel.getType()); + assertEquals(clientSettings, GrpcClientChannel.parseChannelExtendAttribute(remoteChannel)); + assertEquals(clientSettings, GrpcClientChannel.parseChannelExtendAttribute(this.grpcClientChannel)); + assertNull(GrpcClientChannel.parseChannelExtendAttribute(mock(RemotingChannel.class))); + } +} \ No newline at end of file diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/client/ClientActivityTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/client/ClientActivityTest.java new file mode 100644 index 00000000000..0c1ebcdfae7 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/client/ClientActivityTest.java @@ -0,0 +1,428 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.v2.client; + +import apache.rocketmq.v2.ClientType; +import apache.rocketmq.v2.Code; +import apache.rocketmq.v2.FilterExpression; +import apache.rocketmq.v2.FilterType; +import apache.rocketmq.v2.HeartbeatRequest; +import apache.rocketmq.v2.HeartbeatResponse; +import apache.rocketmq.v2.NotifyClientTerminationRequest; +import apache.rocketmq.v2.NotifyClientTerminationResponse; +import apache.rocketmq.v2.Publishing; +import apache.rocketmq.v2.Resource; +import apache.rocketmq.v2.Settings; +import apache.rocketmq.v2.Subscription; +import apache.rocketmq.v2.SubscriptionEntry; +import apache.rocketmq.v2.TelemetryCommand; +import apache.rocketmq.v2.ThreadStackTrace; +import apache.rocketmq.v2.VerifyMessageResult; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.stub.StreamObserver; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import org.apache.rocketmq.broker.client.ClientChannelInfo; +import org.apache.rocketmq.common.attribute.TopicMessageType; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.grpc.v2.BaseActivityTest; +import org.apache.rocketmq.proxy.grpc.v2.ContextStreamObserver; +import org.apache.rocketmq.proxy.grpc.v2.channel.GrpcChannelManager; +import org.apache.rocketmq.proxy.grpc.v2.channel.GrpcClientChannel; +import org.apache.rocketmq.proxy.grpc.v2.common.ResponseBuilder; +import org.apache.rocketmq.proxy.service.relay.ProxyRelayResult; +import org.apache.rocketmq.remoting.protocol.LanguageCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.CMResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.assertj.core.util.Lists; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ClientActivityTest extends BaseActivityTest { + + private static final String TOPIC = "topic"; + private static final String CONSUMER_GROUP = "consumerGroup"; + + private ClientActivity clientActivity; + @Mock + private GrpcChannelManager grpcChannelManagerMock; + @Mock + private CompletableFuture> runningInfoFutureMock; + @Captor + ArgumentCaptor> runningInfoArgumentCaptor; + @Mock + private CompletableFuture> resultFutureMock; + @Captor + ArgumentCaptor> resultArgumentCaptor; + + @Before + public void before() throws Throwable { + super.before(); + this.clientActivity = new ClientActivity(this.messagingProcessor, this.grpcClientSettingsManager, grpcChannelManager); + } + + protected TelemetryCommand sendProducerTelemetry(ProxyContext context) throws Throwable { + return this.sendClientTelemetry( + context, + Settings.newBuilder() + .setClientType(ClientType.PRODUCER) + .setPublishing(Publishing.newBuilder() + .addTopics(Resource.newBuilder().setName(TOPIC).build()) + .build()) + .build()).get(); + } + + protected HeartbeatResponse sendProducerHeartbeat(ProxyContext context) throws Throwable { + return this.clientActivity.heartbeat(context, HeartbeatRequest.newBuilder() + .setClientType(ClientType.PRODUCER) + .build()).get(); + } + + @Test + public void testProducerHeartbeat() throws Throwable { + ProxyContext context = createContext(); + + this.sendProducerTelemetry(context); + + ArgumentCaptor registerProducerGroupArgumentCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor channelInfoArgumentCaptor = ArgumentCaptor.forClass(ClientChannelInfo.class); + doNothing().when(this.messagingProcessor).registerProducer(any(), + registerProducerGroupArgumentCaptor.capture(), + channelInfoArgumentCaptor.capture()); + + ArgumentCaptor txProducerGroupArgumentCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor txProducerTopicArgumentCaptor = ArgumentCaptor.forClass(String.class); + doNothing().when(this.messagingProcessor).addTransactionSubscription(any(), + txProducerGroupArgumentCaptor.capture(), + txProducerTopicArgumentCaptor.capture() + ); + + when(this.metadataService.getTopicMessageType(any(), anyString())).thenReturn(TopicMessageType.TRANSACTION); + + HeartbeatResponse response = this.sendProducerHeartbeat(context); + + assertEquals(Code.OK, response.getStatus().getCode()); + + assertEquals(Lists.newArrayList(TOPIC), registerProducerGroupArgumentCaptor.getAllValues()); + ClientChannelInfo clientChannelInfo = channelInfoArgumentCaptor.getValue(); + assertClientChannelInfo(clientChannelInfo, TOPIC); + + assertEquals(Lists.newArrayList(TOPIC), txProducerGroupArgumentCaptor.getAllValues()); + assertEquals(Lists.newArrayList(TOPIC), txProducerTopicArgumentCaptor.getAllValues()); + } + + protected TelemetryCommand sendConsumerTelemetry(ProxyContext context) throws Throwable { + return this.sendClientTelemetry( + context, + Settings.newBuilder() + .setClientType(ClientType.PUSH_CONSUMER) + .setSubscription(Subscription.newBuilder() + .setGroup(Resource.newBuilder().setName("Group").build()) + .addSubscriptions(SubscriptionEntry.newBuilder() + .setExpression(FilterExpression.newBuilder() + .setExpression("tag") + .setType(FilterType.TAG) + .build()) + .setTopic(Resource.newBuilder().setName(TOPIC).build()) + .build()) + .build()) + .build()).get(); + } + + protected HeartbeatResponse sendConsumerHeartbeat(ProxyContext context) throws Throwable { + return this.clientActivity.heartbeat(context, HeartbeatRequest.newBuilder() + .setClientType(ClientType.PUSH_CONSUMER) + .setGroup(Resource.newBuilder().setName(CONSUMER_GROUP).build()) + .build()).get(); + } + + @Test + public void testConsumerHeartbeat() throws Throwable { + ProxyContext context = createContext(); + this.sendConsumerTelemetry(context); + + ArgumentCaptor> subscriptionDatasArgumentCaptor = ArgumentCaptor.forClass(Set.class); + ArgumentCaptor channelInfoArgumentCaptor = ArgumentCaptor.forClass(ClientChannelInfo.class); + doNothing().when(this.messagingProcessor).registerConsumer(any(), + anyString(), + channelInfoArgumentCaptor.capture(), + any(), + any(), + any(), + subscriptionDatasArgumentCaptor.capture(), + anyBoolean() + ); + + HeartbeatResponse response = this.sendConsumerHeartbeat(context); + assertEquals(Code.OK, response.getStatus().getCode()); + + ClientChannelInfo clientChannelInfo = channelInfoArgumentCaptor.getValue(); + assertClientChannelInfo(clientChannelInfo, CONSUMER_GROUP); + + SubscriptionData data = subscriptionDatasArgumentCaptor.getValue().stream().findAny().get(); + assertEquals("TAG", data.getExpressionType()); + assertEquals("tag", data.getSubString()); + } + + protected void assertClientChannelInfo(ClientChannelInfo clientChannelInfo, String group) { + assertEquals(LanguageCode.JAVA, clientChannelInfo.getLanguage()); + assertEquals(CLIENT_ID, clientChannelInfo.getClientId()); + assertTrue(clientChannelInfo.getChannel() instanceof GrpcClientChannel); + GrpcClientChannel channel = (GrpcClientChannel) clientChannelInfo.getChannel(); + assertEquals(REMOTE_ADDR, channel.getRemoteAddress()); + assertEquals(LOCAL_ADDR, channel.getLocalAddress()); + } + + @Test + public void testProducerNotifyClientTermination() throws Throwable { + ProxyContext context = createContext(); + + when(this.grpcClientSettingsManager.removeAndGetClientSettings(any())).thenReturn(Settings.newBuilder() + .setClientType(ClientType.PRODUCER) + .setPublishing(Publishing.newBuilder() + .addTopics(Resource.newBuilder().setName(TOPIC).build()) + .build()) + .build()); + ArgumentCaptor channelInfoArgumentCaptor = ArgumentCaptor.forClass(ClientChannelInfo.class); + doNothing().when(this.messagingProcessor).unRegisterProducer(any(), anyString(), channelInfoArgumentCaptor.capture()); + when(this.metadataService.getTopicMessageType(any(), anyString())).thenReturn(TopicMessageType.NORMAL); + + this.sendProducerTelemetry(context); + this.sendProducerHeartbeat(context); + + NotifyClientTerminationResponse response = this.clientActivity.notifyClientTermination( + context, + NotifyClientTerminationRequest.newBuilder() + .build() + ).get(); + + assertEquals(Code.OK, response.getStatus().getCode()); + ClientChannelInfo clientChannelInfo = channelInfoArgumentCaptor.getValue(); + assertClientChannelInfo(clientChannelInfo, TOPIC); + } + + @Test + public void testConsumerNotifyClientTermination() throws Throwable { + ProxyContext context = createContext(); + + when(this.grpcClientSettingsManager.removeAndGetClientSettings(any())).thenReturn(Settings.newBuilder() + .setClientType(ClientType.PUSH_CONSUMER) + .build()); + ArgumentCaptor channelInfoArgumentCaptor = ArgumentCaptor.forClass(ClientChannelInfo.class); + doNothing().when(this.messagingProcessor).unRegisterConsumer(any(), anyString(), channelInfoArgumentCaptor.capture()); + + this.sendConsumerTelemetry(context); + this.sendConsumerHeartbeat(context); + + NotifyClientTerminationResponse response = this.clientActivity.notifyClientTermination( + context, + NotifyClientTerminationRequest.newBuilder() + .setGroup(Resource.newBuilder().setName(CONSUMER_GROUP).build()) + .build() + ).get(); + + assertEquals(Code.OK, response.getStatus().getCode()); + ClientChannelInfo clientChannelInfo = channelInfoArgumentCaptor.getValue(); + assertClientChannelInfo(clientChannelInfo, CONSUMER_GROUP); + } + + @Test + public void testErrorConsumerGroupName() throws Throwable { + ProxyContext context = createContext(); + try { + this.sendClientTelemetry( + context, + Settings.newBuilder() + .setClientType(ClientType.PUSH_CONSUMER) + .setSubscription(Subscription.newBuilder() + .addSubscriptions(SubscriptionEntry.newBuilder() + .setExpression(FilterExpression.newBuilder() + .setExpression("tag") + .setType(FilterType.TAG) + .build()) + .setTopic(Resource.newBuilder().setName(TOPIC).build()) + .build()) + .build()) + .build()).get(); + fail(); + } catch (ExecutionException e) { + StatusRuntimeException exception = (StatusRuntimeException) e.getCause(); + assertEquals(Status.Code.INVALID_ARGUMENT, exception.getStatus().getCode()); + } + } + + @Test + public void testErrorProducerConfig() throws Throwable { + ProxyContext context = createContext(); + try { + this.sendClientTelemetry( + context, + Settings.newBuilder() + .setClientType(ClientType.PRODUCER) + .setPublishing(Publishing.newBuilder() + .addTopics(Resource.newBuilder().setName("()").build()) + .build()) + .build()).get(); + fail(); + } catch (ExecutionException e) { + StatusRuntimeException exception = (StatusRuntimeException) e.getCause(); + assertEquals(Status.Code.INVALID_ARGUMENT, exception.getStatus().getCode()); + } + } + + @Test + public void testEmptySettings() throws Throwable { + ProxyContext context = createContext(); + try { + this.sendClientTelemetry( + context, + Settings.getDefaultInstance()).get(); + fail(); + } catch (ExecutionException e) { + StatusRuntimeException exception = (StatusRuntimeException) e.getCause(); + assertEquals(Status.Code.INVALID_ARGUMENT, exception.getStatus().getCode()); + } + } + + @Test + public void testEmptyProducerSettings() throws Throwable { + ProxyContext context = createContext(); + TelemetryCommand command = this.sendClientTelemetry( + context, + Settings.newBuilder() + .setClientType(ClientType.PRODUCER) + .setPublishing(Publishing.getDefaultInstance()) + .build()).get(); + assertTrue(command.hasSettings()); + assertTrue(command.getSettings().hasPublishing()); + } + + @Test + public void testReportThreadStackTrace() { + this.clientActivity = new ClientActivity(this.messagingProcessor, this.grpcClientSettingsManager, grpcChannelManagerMock); + String jstack = "jstack"; + String nonce = "123"; + when(grpcChannelManagerMock.getAndRemoveResponseFuture(anyString())).thenReturn((CompletableFuture) runningInfoFutureMock); + ProxyContext context = createContext(); + ContextStreamObserver streamObserver = clientActivity.telemetry(new StreamObserver() { + @Override + public void onNext(TelemetryCommand value) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onCompleted() { + } + }); + streamObserver.onNext(context, TelemetryCommand.newBuilder() + .setThreadStackTrace(ThreadStackTrace.newBuilder() + .setThreadStackTrace(jstack) + .setNonce(nonce) + .build()) + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.OK, Code.OK.name())) + .build()); + verify(runningInfoFutureMock, times(1)).complete(runningInfoArgumentCaptor.capture()); + ProxyRelayResult result = runningInfoArgumentCaptor.getValue(); + assertThat(result.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(result.getResult().getJstack()).isEqualTo(jstack); + } + + @Test + public void testReportVerifyMessageResult() { + this.clientActivity = new ClientActivity(this.messagingProcessor, this.grpcClientSettingsManager, grpcChannelManagerMock); + String nonce = "123"; + when(grpcChannelManagerMock.getAndRemoveResponseFuture(anyString())).thenReturn((CompletableFuture) resultFutureMock); + ProxyContext context = createContext(); + ContextStreamObserver streamObserver = clientActivity.telemetry(new StreamObserver() { + @Override + public void onNext(TelemetryCommand value) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onCompleted() { + } + }); + streamObserver.onNext(context, TelemetryCommand.newBuilder() + .setVerifyMessageResult(VerifyMessageResult.newBuilder() + .setNonce(nonce) + .build()) + .setStatus(ResponseBuilder.getInstance().buildStatus(Code.OK, Code.OK.name())) + .build()); + verify(resultFutureMock, times(1)).complete(resultArgumentCaptor.capture()); + ProxyRelayResult result = resultArgumentCaptor.getValue(); + assertThat(result.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(result.getResult().getConsumeResult()).isEqualTo(CMResult.CR_SUCCESS); + } + + protected CompletableFuture sendClientTelemetry(ProxyContext ctx, Settings settings) { + when(grpcClientSettingsManager.getClientSettings(any())).thenReturn(settings); + + CompletableFuture future = new CompletableFuture<>(); + StreamObserver responseObserver = new StreamObserver() { + @Override + public void onNext(TelemetryCommand value) { + future.complete(value); + } + + @Override + public void onError(Throwable t) { + future.completeExceptionally(t); + } + + @Override + public void onCompleted() { + + } + }; + ContextStreamObserver requestObserver = this.clientActivity.telemetry(responseObserver); + requestObserver.onNext(ctx, TelemetryCommand.newBuilder() + .setSettings(settings) + .build()); + return future; + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/common/GrpcClientSettingsManagerTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/common/GrpcClientSettingsManagerTest.java new file mode 100644 index 00000000000..6742f094c82 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/common/GrpcClientSettingsManagerTest.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.v2.common; + +import apache.rocketmq.v2.CustomizedBackoff; +import apache.rocketmq.v2.ExponentialBackoff; +import apache.rocketmq.v2.Publishing; +import apache.rocketmq.v2.Resource; +import apache.rocketmq.v2.RetryPolicy; +import apache.rocketmq.v2.Settings; +import apache.rocketmq.v2.Subscription; +import com.google.protobuf.util.Durations; +import org.apache.rocketmq.proxy.common.ContextVariable; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.grpc.v2.BaseActivityTest; +import org.apache.rocketmq.remoting.protocol.subscription.CustomizedRetryPolicy; +import org.apache.rocketmq.remoting.protocol.subscription.ExponentialRetryPolicy; +import org.apache.rocketmq.remoting.protocol.subscription.GroupRetryPolicyType; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +public class GrpcClientSettingsManagerTest extends BaseActivityTest { + private GrpcClientSettingsManager grpcClientSettingsManager; + + @Before + public void before() throws Throwable { + super.before(); + this.grpcClientSettingsManager = new GrpcClientSettingsManager(this.messagingProcessor); + } + + @Test + public void testGetProducerData() { + ProxyContext context = ProxyContext.create().withVal(ContextVariable.CLIENT_ID, CLIENT_ID); + + this.grpcClientSettingsManager.updateClientSettings(context, CLIENT_ID, Settings.newBuilder() + .setBackoffPolicy(RetryPolicy.getDefaultInstance()) + .setPublishing(Publishing.getDefaultInstance()) + .build()); + Settings settings = this.grpcClientSettingsManager.getClientSettings(context); + assertNotEquals(settings.getBackoffPolicy(), settings.getBackoffPolicy().getDefaultInstanceForType()); + assertNotEquals(settings.getPublishing(), settings.getPublishing().getDefaultInstanceForType()); + } + + @Test + public void testGetSubscriptionData() { + ProxyContext context = ProxyContext.create().withVal(ContextVariable.CLIENT_ID, CLIENT_ID); + + SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); + when(this.messagingProcessor.getSubscriptionGroupConfig(any(), any())) + .thenReturn(subscriptionGroupConfig); + + this.grpcClientSettingsManager.updateClientSettings(context, CLIENT_ID, Settings.newBuilder() + .setSubscription(Subscription.newBuilder() + .setGroup(Resource.newBuilder().setName("group").build()) + .build()) + .build()); + + Settings settings = this.grpcClientSettingsManager.getClientSettings(context); + assertEquals(settings.getBackoffPolicy(), this.grpcClientSettingsManager.createDefaultConsumerSettingsBuilder().build().getBackoffPolicy()); + + subscriptionGroupConfig.setRetryMaxTimes(3); + subscriptionGroupConfig.getGroupRetryPolicy().setType(GroupRetryPolicyType.CUSTOMIZED); + subscriptionGroupConfig.getGroupRetryPolicy().setCustomizedRetryPolicy(new CustomizedRetryPolicy(new long[] {1000})); + settings = this.grpcClientSettingsManager.getClientSettings(context); + assertEquals(RetryPolicy.newBuilder() + .setMaxAttempts(4) + .setCustomizedBackoff(CustomizedBackoff.newBuilder() + .addNext(Durations.fromSeconds(1)) + .build()) + .build(), settings.getBackoffPolicy()); + + subscriptionGroupConfig.setRetryMaxTimes(10); + subscriptionGroupConfig.getGroupRetryPolicy().setType(GroupRetryPolicyType.EXPONENTIAL); + subscriptionGroupConfig.getGroupRetryPolicy().setExponentialRetryPolicy(new ExponentialRetryPolicy(1000, 2000, 3)); + settings = this.grpcClientSettingsManager.getClientSettings(context); + assertEquals(RetryPolicy.newBuilder() + .setMaxAttempts(11) + .setExponentialBackoff(ExponentialBackoff.newBuilder() + .setMax(Durations.fromSeconds(2)) + .setInitial(Durations.fromSeconds(1)) + .setMultiplier(3) + .build()) + .build(), settings.getBackoffPolicy()); + + Settings settings1 = this.grpcClientSettingsManager.removeAndGetClientSettings(context); + assertEquals(settings, settings1); + + assertNull(this.grpcClientSettingsManager.getClientSettings(context)); + assertNull(this.grpcClientSettingsManager.removeAndGetClientSettings(context)); + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/common/GrpcConverterTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/common/GrpcConverterTest.java new file mode 100644 index 00000000000..bc9b8a60b40 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/common/GrpcConverterTest.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.v2.common; + +import apache.rocketmq.v2.MessageQueue; +import org.apache.rocketmq.common.message.MessageExt; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GrpcConverterTest { + @Test + public void testBuildMessageQueue() { + String topic = "topic"; + String brokerName = "brokerName"; + int queueId = 1; + MessageExt messageExt = new MessageExt(); + messageExt.setQueueId(queueId); + messageExt.setTopic(topic); + + MessageQueue messageQueue = GrpcConverter.getInstance().buildMessageQueue(messageExt, brokerName); + assertThat(messageQueue.getTopic().getName()).isEqualTo(topic); + assertThat(messageQueue.getBroker().getName()).isEqualTo(brokerName); + assertThat(messageQueue.getId()).isEqualTo(queueId); + } +} \ No newline at end of file diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/common/GrpcValidatorTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/common/GrpcValidatorTest.java new file mode 100644 index 00000000000..df42844e95e --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/common/GrpcValidatorTest.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.v2.common; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertThrows; + +public class GrpcValidatorTest { + + private GrpcValidator grpcValidator; + + @Before + public void before() { + this.grpcValidator = GrpcValidator.getInstance(); + } + + @Test + public void testValidateTopic() { + assertThrows(GrpcProxyException.class, () -> grpcValidator.validateTopic("")); + assertThrows(GrpcProxyException.class, () -> grpcValidator.validateTopic("rmq_sys_xxxx")); + grpcValidator.validateTopic("topicName"); + } + + @Test + public void testValidateConsumerGroup() { + assertThrows(GrpcProxyException.class, () -> grpcValidator.validateConsumerGroup("")); + assertThrows(GrpcProxyException.class, () -> grpcValidator.validateConsumerGroup("CID_RMQ_SYS_xxxx")); + grpcValidator.validateConsumerGroup("consumerGroupName"); + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/consumer/AckMessageActivityTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/consumer/AckMessageActivityTest.java new file mode 100644 index 00000000000..3c474610518 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/consumer/AckMessageActivityTest.java @@ -0,0 +1,259 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.v2.consumer; + +import apache.rocketmq.v2.AckMessageEntry; +import apache.rocketmq.v2.AckMessageRequest; +import apache.rocketmq.v2.AckMessageResponse; +import apache.rocketmq.v2.AckMessageResultEntry; +import apache.rocketmq.v2.Code; +import apache.rocketmq.v2.Resource; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.client.consumer.AckResult; +import org.apache.rocketmq.client.consumer.AckStatus; +import org.apache.rocketmq.proxy.common.ProxyException; +import org.apache.rocketmq.proxy.common.ProxyExceptionCode; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.grpc.v2.BaseActivityTest; +import org.apache.rocketmq.proxy.processor.BatchAckResult; +import org.apache.rocketmq.proxy.service.message.ReceiptHandleMessage; +import org.junit.Before; +import org.junit.Test; +import org.mockito.stubbing.Answer; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.when; + +public class AckMessageActivityTest extends BaseActivityTest { + + private AckMessageActivity ackMessageActivity; + + private static final String TOPIC = "topic"; + private static final String GROUP = "group"; + + @Before + public void before() throws Throwable { + super.before(); + this.ackMessageActivity = new AckMessageActivity(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); + } + + @Test + public void testAckMessage() throws Throwable { + ConfigurationManager.getProxyConfig().setEnableBatchAck(false); + + String msg1 = "msg1"; + String msg2 = "msg2"; + String msg3 = "msg3"; + + when(this.messagingProcessor.ackMessage(any(), any(), eq(msg1), anyString(), anyString())) + .thenThrow(new ProxyException(ProxyExceptionCode.INVALID_RECEIPT_HANDLE, "receipt handle is expired")); + + AckResult msg2AckResult = new AckResult(); + msg2AckResult.setStatus(AckStatus.OK); + when(this.messagingProcessor.ackMessage(any(), any(), eq(msg2), anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(msg2AckResult)); + + AckResult msg3AckResult = new AckResult(); + msg3AckResult.setStatus(AckStatus.NO_EXIST); + when(this.messagingProcessor.ackMessage(any(), any(), eq(msg3), anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(msg3AckResult)); + + { + AckMessageResponse response = this.ackMessageActivity.ackMessage( + createContext(), + AckMessageRequest.newBuilder() + .setTopic(Resource.newBuilder().setName(TOPIC).build()) + .setGroup(Resource.newBuilder().setName(GROUP).build()) + .addEntries(AckMessageEntry.newBuilder() + .setMessageId(msg1) + .setReceiptHandle(buildReceiptHandle(TOPIC, System.currentTimeMillis() - 10000, 1000)) + .build()) + .build() + ).get(); + assertEquals(Code.INVALID_RECEIPT_HANDLE, response.getStatus().getCode()); + } + { + AckMessageResponse response = this.ackMessageActivity.ackMessage( + createContext(), + AckMessageRequest.newBuilder() + .setTopic(Resource.newBuilder().setName(TOPIC).build()) + .setGroup(Resource.newBuilder().setName(GROUP).build()) + .addEntries(AckMessageEntry.newBuilder() + .setMessageId(msg2) + .setReceiptHandle(buildReceiptHandle(TOPIC, System.currentTimeMillis() - 10000, 1000)) + .build()) + .build() + ).get(); + assertEquals(Code.OK, response.getStatus().getCode()); + } + { + AckMessageResponse response = this.ackMessageActivity.ackMessage( + createContext(), + AckMessageRequest.newBuilder() + .setTopic(Resource.newBuilder().setName(TOPIC).build()) + .setGroup(Resource.newBuilder().setName(GROUP).build()) + .addEntries(AckMessageEntry.newBuilder() + .setMessageId(msg3) + .setReceiptHandle(buildReceiptHandle(TOPIC, System.currentTimeMillis() - 10000, 1000)) + .build()) + .build() + ).get(); + assertEquals(Code.INTERNAL_SERVER_ERROR, response.getStatus().getCode()); + } + { + AckMessageResponse response = this.ackMessageActivity.ackMessage( + createContext(), + AckMessageRequest.newBuilder() + .setTopic(Resource.newBuilder().setName(TOPIC).build()) + .setGroup(Resource.newBuilder().setName(GROUP).build()) + .addEntries(AckMessageEntry.newBuilder() + .setMessageId(msg1) + .setReceiptHandle(buildReceiptHandle(TOPIC, System.currentTimeMillis() - 10000, 1000)) + .build()) + .addEntries(AckMessageEntry.newBuilder() + .setMessageId(msg2) + .setReceiptHandle(buildReceiptHandle(TOPIC, System.currentTimeMillis(), 3000)) + .build()) + .addEntries(AckMessageEntry.newBuilder() + .setMessageId(msg3) + .setReceiptHandle(buildReceiptHandle(TOPIC, System.currentTimeMillis(), 3000)) + .build()) + .build() + ).get(); + + assertEquals(Code.MULTIPLE_RESULTS, response.getStatus().getCode()); + assertEquals(3, response.getEntriesCount()); + assertEquals(Code.INVALID_RECEIPT_HANDLE, response.getEntries(0).getStatus().getCode()); + assertEquals(Code.OK, response.getEntries(1).getStatus().getCode()); + assertEquals(Code.INTERNAL_SERVER_ERROR, response.getEntries(2).getStatus().getCode()); + } + } + + @Test + public void testAckMessageInBatch() throws Throwable { + ConfigurationManager.getProxyConfig().setEnableBatchAck(true); + + String successMessageId = "msg1"; + String notOkMessageId = "msg2"; + String exceptionMessageId = "msg3"; + + doAnswer((Answer>>) invocation -> { + List receiptHandleMessageList = invocation.getArgument(1, List.class); + List batchAckResultList = new ArrayList<>(); + for (ReceiptHandleMessage receiptHandleMessage : receiptHandleMessageList) { + BatchAckResult batchAckResult; + if (receiptHandleMessage.getMessageId().equals(successMessageId)) { + AckResult ackResult = new AckResult(); + ackResult.setStatus(AckStatus.OK); + batchAckResult = new BatchAckResult(receiptHandleMessage, ackResult); + } else if (receiptHandleMessage.getMessageId().equals(notOkMessageId)) { + AckResult ackResult = new AckResult(); + ackResult.setStatus(AckStatus.NO_EXIST); + batchAckResult = new BatchAckResult(receiptHandleMessage, ackResult); + } else { + batchAckResult = new BatchAckResult(receiptHandleMessage, new ProxyException(ProxyExceptionCode.INVALID_RECEIPT_HANDLE, "")); + } + batchAckResultList.add(batchAckResult); + } + return CompletableFuture.completedFuture(batchAckResultList); + }).when(this.messagingProcessor).batchAckMessage(any(), anyList(), anyString(), anyString()); + + { + AckMessageResponse response = this.ackMessageActivity.ackMessage( + createContext(), + AckMessageRequest.newBuilder() + .setTopic(Resource.newBuilder().setName(TOPIC).build()) + .setGroup(Resource.newBuilder().setName(GROUP).build()) + .addEntries(AckMessageEntry.newBuilder() + .setMessageId(successMessageId) + .setReceiptHandle(buildReceiptHandle(TOPIC, System.currentTimeMillis(), 3000)) + .build()) + .build() + ).get(); + assertEquals(Code.OK, response.getStatus().getCode()); + } + { + AckMessageResponse response = this.ackMessageActivity.ackMessage( + createContext(), + AckMessageRequest.newBuilder() + .setTopic(Resource.newBuilder().setName(TOPIC).build()) + .setGroup(Resource.newBuilder().setName(GROUP).build()) + .addEntries(AckMessageEntry.newBuilder() + .setMessageId(notOkMessageId) + .setReceiptHandle(buildReceiptHandle(TOPIC, System.currentTimeMillis(), 3000)) + .build()) + .build() + ).get(); + assertEquals(Code.INTERNAL_SERVER_ERROR, response.getStatus().getCode()); + } + { + AckMessageResponse response = this.ackMessageActivity.ackMessage( + createContext(), + AckMessageRequest.newBuilder() + .setTopic(Resource.newBuilder().setName(TOPIC).build()) + .setGroup(Resource.newBuilder().setName(GROUP).build()) + .addEntries(AckMessageEntry.newBuilder() + .setMessageId(exceptionMessageId) + .setReceiptHandle(buildReceiptHandle(TOPIC, System.currentTimeMillis(), 3000)) + .build()) + .build() + ).get(); + assertEquals(Code.INVALID_RECEIPT_HANDLE, response.getStatus().getCode()); + } + { + AckMessageResponse response = this.ackMessageActivity.ackMessage( + createContext(), + AckMessageRequest.newBuilder() + .setTopic(Resource.newBuilder().setName(TOPIC).build()) + .setGroup(Resource.newBuilder().setName(GROUP).build()) + .addEntries(AckMessageEntry.newBuilder() + .setMessageId(successMessageId) + .setReceiptHandle(buildReceiptHandle(TOPIC, System.currentTimeMillis(), 3000)) + .build()) + .addEntries(AckMessageEntry.newBuilder() + .setMessageId(notOkMessageId) + .setReceiptHandle(buildReceiptHandle(TOPIC, System.currentTimeMillis(), 3000)) + .build()) + .addEntries(AckMessageEntry.newBuilder() + .setMessageId(exceptionMessageId) + .setReceiptHandle(buildReceiptHandle(TOPIC, System.currentTimeMillis(), 3000)) + .build()) + .build() + ).get(); + + assertEquals(Code.MULTIPLE_RESULTS, response.getStatus().getCode()); + assertEquals(3, response.getEntriesCount()); + Map msgCode = new HashMap<>(); + for (AckMessageResultEntry entry : response.getEntriesList()) { + msgCode.put(entry.getMessageId(), entry.getStatus().getCode()); + } + assertEquals(Code.OK, msgCode.get(successMessageId)); + assertEquals(Code.INTERNAL_SERVER_ERROR, msgCode.get(notOkMessageId)); + assertEquals(Code.INVALID_RECEIPT_HANDLE, msgCode.get(exceptionMessageId)); + } + } +} \ No newline at end of file diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ChangeInvisibleDurationActivityTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ChangeInvisibleDurationActivityTest.java new file mode 100644 index 00000000000..2de9a066be5 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ChangeInvisibleDurationActivityTest.java @@ -0,0 +1,177 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.v2.consumer; + +import apache.rocketmq.v2.ChangeInvisibleDurationRequest; +import apache.rocketmq.v2.ChangeInvisibleDurationResponse; +import apache.rocketmq.v2.Code; +import apache.rocketmq.v2.Resource; +import com.google.protobuf.util.Durations; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.client.consumer.AckResult; +import org.apache.rocketmq.client.consumer.AckStatus; +import org.apache.rocketmq.common.consumer.ReceiptHandle; +import org.apache.rocketmq.proxy.common.MessageReceiptHandle; +import org.apache.rocketmq.proxy.grpc.v2.BaseActivityTest; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcProxyException; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +public class ChangeInvisibleDurationActivityTest extends BaseActivityTest { + + private static final String TOPIC = "topic"; + private static final String CONSUMER_GROUP = "consumerGroup"; + private ChangeInvisibleDurationActivity changeInvisibleDurationActivity; + + @Before + public void before() throws Throwable { + super.before(); + this.changeInvisibleDurationActivity = new ChangeInvisibleDurationActivity(messagingProcessor, + grpcClientSettingsManager, grpcChannelManager); + } + + @Test + public void testChangeInvisibleDurationActivity() throws Throwable { + String newHandle = "newHandle"; + ArgumentCaptor invisibleTimeArgumentCaptor = ArgumentCaptor.forClass(Long.class); + AckResult ackResult = new AckResult(); + ackResult.setExtraInfo(newHandle); + ackResult.setStatus(AckStatus.OK); + when(this.messagingProcessor.changeInvisibleTime( + any(), any(), anyString(), anyString(), anyString(), invisibleTimeArgumentCaptor.capture() + )).thenReturn(CompletableFuture.completedFuture(ackResult)); + + ChangeInvisibleDurationResponse response = this.changeInvisibleDurationActivity.changeInvisibleDuration( + createContext(), + ChangeInvisibleDurationRequest.newBuilder() + .setInvisibleDuration(Durations.fromSeconds(3)) + .setTopic(Resource.newBuilder().setName(TOPIC).build()) + .setGroup(Resource.newBuilder().setName(CONSUMER_GROUP).build()) + .setMessageId("msgId") + .setReceiptHandle(buildReceiptHandle(TOPIC, System.currentTimeMillis(), 3000)) + .build() + ).get(); + + assertEquals(Code.OK, response.getStatus().getCode()); + assertEquals(TimeUnit.SECONDS.toMillis(3), invisibleTimeArgumentCaptor.getValue().longValue()); + assertEquals(newHandle, response.getReceiptHandle()); + } + + @Test + public void testChangeInvisibleDurationActivityWhenHasMappingHandle() throws Throwable { + String newHandle = "newHandle"; + ArgumentCaptor invisibleTimeArgumentCaptor = ArgumentCaptor.forClass(Long.class); + AckResult ackResult = new AckResult(); + ackResult.setExtraInfo(newHandle); + ackResult.setStatus(AckStatus.OK); + String savedHandleStr = buildReceiptHandle("topic", System.currentTimeMillis(),3000); + ArgumentCaptor receiptHandleCaptor = ArgumentCaptor.forClass(ReceiptHandle.class); + when(this.messagingProcessor.changeInvisibleTime( + any(), receiptHandleCaptor.capture(), anyString(), anyString(), anyString(), invisibleTimeArgumentCaptor.capture() + )).thenReturn(CompletableFuture.completedFuture(ackResult)); + when(messagingProcessor.removeReceiptHandle(any(), any(), anyString(), anyString(), anyString())) + .thenReturn(new MessageReceiptHandle("group", "topic", 0, savedHandleStr, "msgId", 0, 0)); + + ChangeInvisibleDurationResponse response = this.changeInvisibleDurationActivity.changeInvisibleDuration( + createContext(), + ChangeInvisibleDurationRequest.newBuilder() + .setInvisibleDuration(Durations.fromSeconds(3)) + .setTopic(Resource.newBuilder().setName(TOPIC).build()) + .setGroup(Resource.newBuilder().setName(CONSUMER_GROUP).build()) + .setMessageId("msgId") + .setReceiptHandle(buildReceiptHandle(TOPIC, System.currentTimeMillis(), 3000)) + .build() + ).get(); + + assertEquals(Code.OK, response.getStatus().getCode()); + assertEquals(TimeUnit.SECONDS.toMillis(3), invisibleTimeArgumentCaptor.getValue().longValue()); + assertEquals(savedHandleStr, receiptHandleCaptor.getValue().getReceiptHandle()); + assertEquals(newHandle, response.getReceiptHandle()); + } + + + @Test + public void testChangeInvisibleDurationActivityFailed() throws Throwable { + ArgumentCaptor invisibleTimeArgumentCaptor = ArgumentCaptor.forClass(Long.class); + AckResult ackResult = new AckResult(); + ackResult.setStatus(AckStatus.NO_EXIST); + when(this.messagingProcessor.changeInvisibleTime( + any(), any(), anyString(), anyString(), anyString(), invisibleTimeArgumentCaptor.capture() + )).thenReturn(CompletableFuture.completedFuture(ackResult)); + + ChangeInvisibleDurationResponse response = this.changeInvisibleDurationActivity.changeInvisibleDuration( + createContext(), + ChangeInvisibleDurationRequest.newBuilder() + .setInvisibleDuration(Durations.fromSeconds(3)) + .setTopic(Resource.newBuilder().setName(TOPIC).build()) + .setGroup(Resource.newBuilder().setName(CONSUMER_GROUP).build()) + .setMessageId("msgId") + .setReceiptHandle(buildReceiptHandle(TOPIC, System.currentTimeMillis(), 3000)) + .build() + ).get(); + + assertEquals(Code.INTERNAL_SERVER_ERROR, response.getStatus().getCode()); + assertEquals(TimeUnit.SECONDS.toMillis(3), invisibleTimeArgumentCaptor.getValue().longValue()); + } + + @Test + public void testChangeInvisibleDurationInvisibleTimeTooSmall() throws Throwable { + try { + this.changeInvisibleDurationActivity.changeInvisibleDuration( + createContext(), + ChangeInvisibleDurationRequest.newBuilder() + .setInvisibleDuration(Durations.fromSeconds(-1)) + .setTopic(Resource.newBuilder().setName(TOPIC).build()) + .setGroup(Resource.newBuilder().setName(CONSUMER_GROUP).build()) + .setMessageId("msgId") + .setReceiptHandle(buildReceiptHandle(TOPIC, System.currentTimeMillis(), 3000)) + .build() + ).get(); + } catch (ExecutionException executionException) { + GrpcProxyException exception = (GrpcProxyException) executionException.getCause(); + assertEquals(Code.ILLEGAL_INVISIBLE_TIME, exception.getCode()); + } + } + + @Test + public void testChangeInvisibleDurationInvisibleTimeTooLarge() throws Throwable { + try { + this.changeInvisibleDurationActivity.changeInvisibleDuration( + createContext(), + ChangeInvisibleDurationRequest.newBuilder() + .setInvisibleDuration(Durations.fromDays(7)) + .setTopic(Resource.newBuilder().setName(TOPIC).build()) + .setGroup(Resource.newBuilder().setName(CONSUMER_GROUP).build()) + .setMessageId("msgId") + .setReceiptHandle(buildReceiptHandle(TOPIC, System.currentTimeMillis(), 3000)) + .build() + ).get(); + } catch (ExecutionException executionException) { + GrpcProxyException exception = (GrpcProxyException) executionException.getCause(); + assertEquals(Code.ILLEGAL_INVISIBLE_TIME, exception.getCode()); + } + } +} \ No newline at end of file diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivityTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivityTest.java new file mode 100644 index 00000000000..77ae5e4d111 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageActivityTest.java @@ -0,0 +1,316 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.v2.consumer; + +import apache.rocketmq.v2.Code; +import apache.rocketmq.v2.FilterExpression; +import apache.rocketmq.v2.FilterType; +import apache.rocketmq.v2.MessageQueue; +import apache.rocketmq.v2.ReceiveMessageRequest; +import apache.rocketmq.v2.ReceiveMessageResponse; +import apache.rocketmq.v2.Resource; +import apache.rocketmq.v2.Settings; +import com.google.protobuf.Duration; +import com.google.protobuf.util.Durations; +import io.grpc.stub.ServerCallStreamObserver; +import io.grpc.stub.StreamObserver; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.client.consumer.PopResult; +import org.apache.rocketmq.client.consumer.PopStatus; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.constant.PermName; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.grpc.v2.BaseActivityTest; +import org.apache.rocketmq.proxy.service.route.AddressableMessageQueue; +import org.apache.rocketmq.proxy.service.route.MessageQueueView; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ReceiveMessageActivityTest extends BaseActivityTest { + + protected static final String BROKER_NAME = "broker"; + protected static final String CLUSTER_NAME = "cluster"; + protected static final String BROKER_ADDR = "127.0.0.1:10911"; + private static final String TOPIC = "topic"; + private static final String CONSUMER_GROUP = "consumerGroup"; + private ReceiveMessageActivity receiveMessageActivity; + + @Before + public void before() throws Throwable { + super.before(); + ConfigurationManager.getProxyConfig().setGrpcClientConsumerMinLongPollingTimeoutMillis(0); + this.receiveMessageActivity = new ReceiveMessageActivity(messagingProcessor, + grpcClientSettingsManager, grpcChannelManager); + } + + @Test + public void testReceiveMessagePollingTime() { + StreamObserver receiveStreamObserver = mock(ServerCallStreamObserver.class); + ArgumentCaptor responseArgumentCaptor = ArgumentCaptor.forClass(ReceiveMessageResponse.class); + doNothing().when(receiveStreamObserver).onNext(responseArgumentCaptor.capture()); + + ArgumentCaptor pollTimeCaptor = ArgumentCaptor.forClass(Long.class); + when(this.grpcClientSettingsManager.getClientSettings(any())).thenReturn(Settings.newBuilder() + .setRequestTimeout(Durations.fromSeconds(3)) + .build()); + when(this.messagingProcessor.popMessage(any(), any(), anyString(), anyString(), anyInt(), anyLong(), + pollTimeCaptor.capture(), anyInt(), any(), anyBoolean(), any(), isNull(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(new PopResult(PopStatus.NO_NEW_MSG, Collections.emptyList()))); + + ProxyContext context = createContext(); + context.setRemainingMs(1L); + this.receiveMessageActivity.receiveMessage( + context, + ReceiveMessageRequest.newBuilder() + .setGroup(Resource.newBuilder().setName(CONSUMER_GROUP).build()) + .setMessageQueue(MessageQueue.newBuilder().setTopic(Resource.newBuilder().setName(TOPIC).build()).build()) + .setAutoRenew(true) + .setFilterExpression(FilterExpression.newBuilder() + .setType(FilterType.TAG) + .setExpression("*") + .build()) + .build(), + receiveStreamObserver + ); + + assertEquals(Code.MESSAGE_NOT_FOUND, getResponseCodeFromReceiveMessageResponseList(responseArgumentCaptor.getAllValues())); + assertEquals(0L, pollTimeCaptor.getValue().longValue()); + } + + @Test + public void testReceiveMessageWithIllegalPollingTime() { + StreamObserver receiveStreamObserver = mock(ServerCallStreamObserver.class); + ArgumentCaptor responseArgumentCaptor0 = ArgumentCaptor.forClass(ReceiveMessageResponse.class); + doNothing().when(receiveStreamObserver).onNext(responseArgumentCaptor0.capture()); + + when(this.grpcClientSettingsManager.getClientSettings(any())).thenReturn(Settings.newBuilder().getDefaultInstanceForType()); + + final ProxyContext context = createContext(); + context.setClientVersion("5.0.2"); + context.setRemainingMs(-1L); + final ReceiveMessageRequest request = ReceiveMessageRequest.newBuilder() + .setGroup(Resource.newBuilder().setName(CONSUMER_GROUP).build()) + .setMessageQueue(MessageQueue.newBuilder().setTopic(Resource.newBuilder().setName(TOPIC).build()).build()) + .setAutoRenew(false) + .setLongPollingTimeout(Duration.newBuilder().setSeconds(20).build()) + .setFilterExpression(FilterExpression.newBuilder() + .setType(FilterType.TAG) + .setExpression("*") + .build()) + .build(); + this.receiveMessageActivity.receiveMessage( + context, + request, + receiveStreamObserver + ); + assertEquals(Code.BAD_REQUEST, getResponseCodeFromReceiveMessageResponseList(responseArgumentCaptor0.getAllValues())); + + ArgumentCaptor responseArgumentCaptor1 = + ArgumentCaptor.forClass(ReceiveMessageResponse.class); + doNothing().when(receiveStreamObserver).onNext(responseArgumentCaptor1.capture()); + context.setClientVersion("5.0.3"); + this.receiveMessageActivity.receiveMessage( + context, + request, + receiveStreamObserver + ); + assertEquals(Code.ILLEGAL_POLLING_TIME, + getResponseCodeFromReceiveMessageResponseList(responseArgumentCaptor1.getAllValues())); + } + + @Test + public void testReceiveMessageIllegalFilter() { + StreamObserver receiveStreamObserver = mock(ServerCallStreamObserver.class); + ArgumentCaptor responseArgumentCaptor = ArgumentCaptor.forClass(ReceiveMessageResponse.class); + doNothing().when(receiveStreamObserver).onNext(responseArgumentCaptor.capture()); + + when(this.grpcClientSettingsManager.getClientSettings(any())).thenReturn(Settings.newBuilder().getDefaultInstanceForType()); + + this.receiveMessageActivity.receiveMessage( + createContext(), + ReceiveMessageRequest.newBuilder() + .setGroup(Resource.newBuilder().setName(CONSUMER_GROUP).build()) + .setMessageQueue(MessageQueue.newBuilder().setTopic(Resource.newBuilder().setName(TOPIC).build()).build()) + .setAutoRenew(true) + .setFilterExpression(FilterExpression.newBuilder() + .setType(FilterType.SQL) + .setExpression("") + .build()) + .build(), + receiveStreamObserver + ); + + assertEquals(Code.ILLEGAL_FILTER_EXPRESSION, getResponseCodeFromReceiveMessageResponseList(responseArgumentCaptor.getAllValues())); + } + + @Test + public void testReceiveMessageIllegalInvisibleTimeTooSmall() { + StreamObserver receiveStreamObserver = mock(ServerCallStreamObserver.class); + ArgumentCaptor responseArgumentCaptor = ArgumentCaptor.forClass(ReceiveMessageResponse.class); + doNothing().when(receiveStreamObserver).onNext(responseArgumentCaptor.capture()); + + when(this.grpcClientSettingsManager.getClientSettings(any())).thenReturn(Settings.newBuilder().getDefaultInstanceForType()); + + this.receiveMessageActivity.receiveMessage( + createContext(), + ReceiveMessageRequest.newBuilder() + .setGroup(Resource.newBuilder().setName(CONSUMER_GROUP).build()) + .setMessageQueue(MessageQueue.newBuilder().setTopic(Resource.newBuilder().setName(TOPIC).build()).build()) + .setAutoRenew(false) + .setInvisibleDuration(Durations.fromSeconds(0)) + .build(), + receiveStreamObserver + ); + + assertEquals(Code.ILLEGAL_INVISIBLE_TIME, getResponseCodeFromReceiveMessageResponseList(responseArgumentCaptor.getAllValues())); + } + + @Test + public void testReceiveMessageIllegalInvisibleTimeTooLarge() { + StreamObserver receiveStreamObserver = mock(ServerCallStreamObserver.class); + ArgumentCaptor responseArgumentCaptor = ArgumentCaptor.forClass(ReceiveMessageResponse.class); + doNothing().when(receiveStreamObserver).onNext(responseArgumentCaptor.capture()); + + when(this.grpcClientSettingsManager.getClientSettings(any())).thenReturn(Settings.newBuilder().getDefaultInstanceForType()); + + this.receiveMessageActivity.receiveMessage( + createContext(), + ReceiveMessageRequest.newBuilder() + .setGroup(Resource.newBuilder().setName(CONSUMER_GROUP).build()) + .setMessageQueue(MessageQueue.newBuilder().setTopic(Resource.newBuilder().setName(TOPIC).build()).build()) + .setAutoRenew(false) + .setInvisibleDuration(Durations.fromDays(7)) + .build(), + receiveStreamObserver + ); + + assertEquals(Code.ILLEGAL_INVISIBLE_TIME, getResponseCodeFromReceiveMessageResponseList(responseArgumentCaptor.getAllValues())); + } + + @Test + public void testReceiveMessage() { + StreamObserver receiveStreamObserver = mock(ServerCallStreamObserver.class); + ArgumentCaptor responseArgumentCaptor = ArgumentCaptor.forClass(ReceiveMessageResponse.class); + doNothing().when(receiveStreamObserver).onNext(responseArgumentCaptor.capture()); + + when(this.grpcClientSettingsManager.getClientSettings(any())).thenReturn(Settings.newBuilder().getDefaultInstanceForType()); + + PopResult popResult = new PopResult(PopStatus.NO_NEW_MSG, new ArrayList<>()); + when(this.messagingProcessor.popMessage( + any(), + any(), + anyString(), + anyString(), + anyInt(), + anyLong(), + anyLong(), + anyInt(), + any(), + anyBoolean(), + any(), + isNull(), + anyLong())).thenReturn(CompletableFuture.completedFuture(popResult)); + + this.receiveMessageActivity.receiveMessage( + createContext(), + ReceiveMessageRequest.newBuilder() + .setGroup(Resource.newBuilder().setName(CONSUMER_GROUP).build()) + .setMessageQueue(MessageQueue.newBuilder().setTopic(Resource.newBuilder().setName(TOPIC).build()).build()) + .setAutoRenew(true) + .setFilterExpression(FilterExpression.newBuilder() + .setType(FilterType.TAG) + .setExpression("*") + .build()) + .build(), + receiveStreamObserver + ); + assertEquals(Code.MESSAGE_NOT_FOUND, getResponseCodeFromReceiveMessageResponseList(responseArgumentCaptor.getAllValues())); + } + + private Code getResponseCodeFromReceiveMessageResponseList(List responseList) { + for (ReceiveMessageResponse response : responseList) { + if (response.hasStatus()) { + return response.getStatus().getCode(); + } + } + return null; + } + + @Test + public void testReceiveMessageQueueSelector() throws Exception { + TopicRouteData topicRouteData = new TopicRouteData(); + List queueDatas = new ArrayList<>(); + for (int i = 0; i < 2; i++) { + QueueData queueData = new QueueData(); + queueData.setBrokerName(BROKER_NAME + i); + queueData.setReadQueueNums(1); + queueData.setPerm(PermName.PERM_READ); + queueDatas.add(queueData); + } + topicRouteData.setQueueDatas(queueDatas); + + List brokerDatas = new ArrayList<>(); + for (int i = 0; i < 2; i++) { + BrokerData brokerData = new BrokerData(); + brokerData.setCluster(CLUSTER_NAME); + brokerData.setBrokerName(BROKER_NAME + i); + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(MixAll.MASTER_ID, BROKER_ADDR); + brokerData.setBrokerAddrs(brokerAddrs); + brokerDatas.add(brokerData); + } + topicRouteData.setBrokerDatas(brokerDatas); + + MessageQueueView messageQueueView = new MessageQueueView(TOPIC, topicRouteData, null); + ReceiveMessageActivity.ReceiveMessageQueueSelector selector = new ReceiveMessageActivity.ReceiveMessageQueueSelector(""); + + AddressableMessageQueue firstSelect = selector.select(ProxyContext.create(), messageQueueView); + AddressableMessageQueue secondSelect = selector.select(ProxyContext.create(), messageQueueView); + AddressableMessageQueue thirdSelect = selector.select(ProxyContext.create(), messageQueueView); + + assertEquals(firstSelect, thirdSelect); + assertNotEquals(firstSelect, secondSelect); + + for (int i = 0; i < 2; i++) { + ReceiveMessageActivity.ReceiveMessageQueueSelector selectorBrokerName = + new ReceiveMessageActivity.ReceiveMessageQueueSelector(BROKER_NAME + i); + assertEquals(BROKER_NAME + i, selectorBrokerName.select(ProxyContext.create(), messageQueueView).getBrokerName()); + } + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageResponseStreamWriterTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageResponseStreamWriterTest.java new file mode 100644 index 00000000000..fb449a89989 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/consumer/ReceiveMessageResponseStreamWriterTest.java @@ -0,0 +1,158 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.v2.consumer; + +import apache.rocketmq.v2.Code; +import apache.rocketmq.v2.FilterExpression; +import apache.rocketmq.v2.FilterType; +import apache.rocketmq.v2.MessageQueue; +import apache.rocketmq.v2.ReceiveMessageRequest; +import apache.rocketmq.v2.ReceiveMessageResponse; +import apache.rocketmq.v2.Resource; +import io.grpc.stub.StreamObserver; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.rocketmq.client.consumer.AckResult; +import org.apache.rocketmq.client.consumer.PopResult; +import org.apache.rocketmq.client.consumer.PopStatus; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.grpc.v2.BaseActivityTest; +import org.apache.rocketmq.remoting.protocol.header.ExtraInfoUtil; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class ReceiveMessageResponseStreamWriterTest extends BaseActivityTest { + + private static final String TOPIC = "topic"; + private static final String CONSUMER_GROUP = "consumerGroup"; + private ReceiveMessageResponseStreamWriter writer; + private StreamObserver streamObserver; + + @Before + public void before() throws Throwable { + super.before(); + this.streamObserver = mock(StreamObserver.class); + this.writer = new ReceiveMessageResponseStreamWriter(this.messagingProcessor, this.streamObserver); + } + + @Test + public void testWriteMessage() { + ArgumentCaptor changeInvisibleTimeMsgIdCaptor = ArgumentCaptor.forClass(String.class); + doReturn(CompletableFuture.completedFuture(mock(AckResult.class))).when(this.messagingProcessor) + .changeInvisibleTime(any(), any(), changeInvisibleTimeMsgIdCaptor.capture(), anyString(), anyString(), anyLong()); + + ArgumentCaptor responseArgumentCaptor = ArgumentCaptor.forClass(ReceiveMessageResponse.class); + AtomicInteger onNextCallNum = new AtomicInteger(0); + doAnswer(mock -> { + if (onNextCallNum.incrementAndGet() > 2) { + throw new RuntimeException(); + } + return null; + }).when(streamObserver).onNext(responseArgumentCaptor.capture()); + + List messageExtList = new ArrayList<>(); + messageExtList.add(createMessageExt(TOPIC, "tag")); + messageExtList.add(createMessageExt(TOPIC, "tag")); + PopResult popResult = new PopResult(PopStatus.FOUND, messageExtList); + writer.writeAndComplete( + ProxyContext.create(), + ReceiveMessageRequest.newBuilder() + .setGroup(Resource.newBuilder().setName(CONSUMER_GROUP).build()) + .setMessageQueue(MessageQueue.newBuilder().setTopic(Resource.newBuilder().setName(TOPIC).build()).build()) + .setFilterExpression(FilterExpression.newBuilder() + .setType(FilterType.TAG) + .setExpression("*") + .build()) + .build(), + popResult + ); + + verify(streamObserver, times(1)).onCompleted(); + verify(streamObserver, times(4)).onNext(any()); + verify(this.messagingProcessor, times(1)) + .changeInvisibleTime(any(), any(), anyString(), anyString(), anyString(), anyLong()); + + assertTrue(responseArgumentCaptor.getAllValues().get(0).hasStatus()); + assertEquals(Code.OK, responseArgumentCaptor.getAllValues().get(0).getStatus().getCode()); + assertTrue(responseArgumentCaptor.getAllValues().get(1).hasMessage()); + assertEquals(messageExtList.get(0).getMsgId(), responseArgumentCaptor.getAllValues().get(1).getMessage().getSystemProperties().getMessageId()); + + assertEquals(messageExtList.get(1).getMsgId(), changeInvisibleTimeMsgIdCaptor.getValue()); + } + + @Test + public void testPollingFull() { + ArgumentCaptor responseArgumentCaptor = ArgumentCaptor.forClass(ReceiveMessageResponse.class); + doNothing().when(streamObserver).onNext(responseArgumentCaptor.capture()); + + PopResult popResult = new PopResult(PopStatus.POLLING_FULL, new ArrayList<>()); + writer.writeAndComplete( + ProxyContext.create(), + ReceiveMessageRequest.newBuilder() + .setGroup(Resource.newBuilder().setName(CONSUMER_GROUP).build()) + .setMessageQueue(MessageQueue.newBuilder().setTopic(Resource.newBuilder().setName(TOPIC).build()).build()) + .setFilterExpression(FilterExpression.newBuilder() + .setType(FilterType.TAG) + .setExpression("*") + .build()) + .build(), + popResult + ); + + ReceiveMessageResponse response = responseArgumentCaptor.getAllValues().stream().filter(ReceiveMessageResponse::hasStatus) + .findFirst().get(); + assertEquals(Code.TOO_MANY_REQUESTS, response.getStatus().getCode()); + } + + private static MessageExt createMessageExt(String topic, String tags) { + String msgId = MessageClientIDSetter.createUniqID(); + + MessageExt messageExt = new MessageExt(); + messageExt.setTopic(topic); + messageExt.setTags(tags); + messageExt.setBody(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8)); + messageExt.setMsgId(msgId); + MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX, msgId); + messageExt.setCommitLogOffset(RANDOM.nextInt(Integer.MAX_VALUE)); + MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_POP_CK, + ExtraInfoUtil.buildExtraInfo(RANDOM.nextInt(Integer.MAX_VALUE), System.currentTimeMillis(), 3000, + RANDOM.nextInt(Integer.MAX_VALUE), topic, "mockBroker", RANDOM.nextInt(Integer.MAX_VALUE), RANDOM.nextInt(Integer.MAX_VALUE))); + return messageExt; + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/producer/ForwardMessageToDLQActivityTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/producer/ForwardMessageToDLQActivityTest.java new file mode 100644 index 00000000000..87824e5b4bc --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/producer/ForwardMessageToDLQActivityTest.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.v2.producer; + +import apache.rocketmq.v2.Code; +import apache.rocketmq.v2.ForwardMessageToDeadLetterQueueRequest; +import apache.rocketmq.v2.ForwardMessageToDeadLetterQueueResponse; +import apache.rocketmq.v2.Resource; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.common.consumer.ReceiptHandle; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.proxy.common.MessageReceiptHandle; +import org.apache.rocketmq.proxy.grpc.v2.BaseActivityTest; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +public class ForwardMessageToDLQActivityTest extends BaseActivityTest { + + private ForwardMessageToDLQActivity forwardMessageToDLQActivity; + + @Before + public void before() throws Throwable { + super.before(); + this.forwardMessageToDLQActivity = new ForwardMessageToDLQActivity(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); + } + + @Test + public void testForwardMessageToDeadLetterQueue() throws Throwable { + ArgumentCaptor receiptHandleCaptor = ArgumentCaptor.forClass(ReceiptHandle.class); + when(this.messagingProcessor.forwardMessageToDeadLetterQueue(any(), receiptHandleCaptor.capture(), anyString(), anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(RemotingCommand.createResponseCommand(ResponseCode.SUCCESS, ""))); + + String handleStr = buildReceiptHandle("topic", System.currentTimeMillis(), 3000); + ForwardMessageToDeadLetterQueueResponse response = this.forwardMessageToDLQActivity.forwardMessageToDeadLetterQueue( + createContext(), + ForwardMessageToDeadLetterQueueRequest.newBuilder() + .setTopic(Resource.newBuilder().setName("topic").build()) + .setGroup(Resource.newBuilder().setName("group").build()) + .setMessageId(MessageClientIDSetter.createUniqID()) + .setReceiptHandle(handleStr) + .build() + ).get(); + + assertEquals(Code.OK, response.getStatus().getCode()); + assertEquals(handleStr, receiptHandleCaptor.getValue().getReceiptHandle()); + } + + @Test + public void testForwardMessageToDeadLetterQueueWhenHasMappingHandle() throws Throwable { + ArgumentCaptor receiptHandleCaptor = ArgumentCaptor.forClass(ReceiptHandle.class); + when(this.messagingProcessor.forwardMessageToDeadLetterQueue(any(), receiptHandleCaptor.capture(), anyString(), anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(RemotingCommand.createResponseCommand(ResponseCode.SUCCESS, ""))); + + String savedHandleStr = buildReceiptHandle("topic", System.currentTimeMillis(),3000); + when(messagingProcessor.removeReceiptHandle(any(), any(), anyString(), anyString(), anyString())) + .thenReturn(new MessageReceiptHandle("group", "topic", 0, savedHandleStr, "msgId", 0, 0)); + + ForwardMessageToDeadLetterQueueResponse response = this.forwardMessageToDLQActivity.forwardMessageToDeadLetterQueue( + createContext(), + ForwardMessageToDeadLetterQueueRequest.newBuilder() + .setTopic(Resource.newBuilder().setName("topic").build()) + .setGroup(Resource.newBuilder().setName("group").build()) + .setMessageId(MessageClientIDSetter.createUniqID()) + .setReceiptHandle(buildReceiptHandle("topic", System.currentTimeMillis(), 3000)) + .build() + ).get(); + + assertEquals(Code.OK, response.getStatus().getCode()); + assertEquals(savedHandleStr, receiptHandleCaptor.getValue().getReceiptHandle()); + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/producer/SendMessageActivityTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/producer/SendMessageActivityTest.java new file mode 100644 index 00000000000..4882a5ed8b7 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/producer/SendMessageActivityTest.java @@ -0,0 +1,929 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.v2.producer; + +import apache.rocketmq.v2.Code; +import apache.rocketmq.v2.Encoding; +import apache.rocketmq.v2.Message; +import apache.rocketmq.v2.MessageType; +import apache.rocketmq.v2.Resource; +import apache.rocketmq.v2.SendMessageRequest; +import apache.rocketmq.v2.SendMessageResponse; +import apache.rocketmq.v2.SystemProperties; +import com.google.protobuf.ByteString; +import com.google.protobuf.util.Durations; +import com.google.protobuf.util.Timestamps; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.client.latency.MQFaultStrategy; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.constant.PermName; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.grpc.v2.BaseActivityTest; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcProxyException; +import org.apache.rocketmq.proxy.service.route.AddressableMessageQueue; +import org.apache.rocketmq.proxy.service.route.MessageQueueView; +import org.apache.rocketmq.proxy.service.route.TopicRouteService; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.assertj.core.util.Lists; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class SendMessageActivityTest extends BaseActivityTest { + + protected static final String BROKER_NAME = "broker"; + protected static final String BROKER_NAME2 = "broker2"; + protected static final String CLUSTER_NAME = "cluster"; + protected static final String BROKER_ADDR = "127.0.0.1:10911"; + protected static final String BROKER_ADDR2 = "127.0.0.1:10912"; + private static final String TOPIC = "topic"; + private static final String CONSUMER_GROUP = "consumerGroup"; + MQFaultStrategy mqFaultStrategy; + + private SendMessageActivity sendMessageActivity; + + @Before + public void before() throws Throwable { + super.before(); + this.sendMessageActivity = new SendMessageActivity(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); + } + + @Test + public void sendMessage() throws Exception { + String msgId = MessageClientIDSetter.createUniqID(); + + SendResult sendResult = new SendResult(); + sendResult.setSendStatus(SendStatus.SEND_OK); + sendResult.setMsgId(msgId); + when(this.messagingProcessor.sendMessage(any(), any(), anyString(), anyInt(), any())) + .thenReturn(CompletableFuture.completedFuture(Lists.newArrayList(sendResult))); + + SendMessageResponse response = this.sendMessageActivity.sendMessage( + createContext(), + SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(TOPIC) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId(msgId) + .setQueueId(0) + .setMessageType(MessageType.NORMAL) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .build()) + .setBody(ByteString.copyFromUtf8("123")) + .build()) + .build() + ).get(); + + assertEquals(Code.OK, response.getStatus().getCode()); + assertEquals(msgId, response.getEntries(0).getMessageId()); + } + + @Test + public void testConvertToSendMessageResponse() { + { + SendMessageResponse response = this.sendMessageActivity.convertToSendMessageResponse( + ProxyContext.create(), + SendMessageRequest.newBuilder().build(), + Lists.newArrayList(new SendResult(SendStatus.FLUSH_DISK_TIMEOUT, null, null, null, 0)) + ); + assertEquals(Code.MASTER_PERSISTENCE_TIMEOUT, response.getStatus().getCode()); + assertEquals(Code.MASTER_PERSISTENCE_TIMEOUT, response.getEntries(0).getStatus().getCode()); + } + + { + SendMessageResponse response = this.sendMessageActivity.convertToSendMessageResponse( + ProxyContext.create(), + SendMessageRequest.newBuilder().build(), + Lists.newArrayList(new SendResult(SendStatus.FLUSH_SLAVE_TIMEOUT, null, null, null, 0)) + ); + assertEquals(Code.SLAVE_PERSISTENCE_TIMEOUT, response.getStatus().getCode()); + assertEquals(Code.SLAVE_PERSISTENCE_TIMEOUT, response.getEntries(0).getStatus().getCode()); + } + + { + SendMessageResponse response = this.sendMessageActivity.convertToSendMessageResponse( + ProxyContext.create(), + SendMessageRequest.newBuilder().build(), + Lists.newArrayList(new SendResult(SendStatus.SLAVE_NOT_AVAILABLE, null, null, null, 0)) + ); + assertEquals(Code.HA_NOT_AVAILABLE, response.getStatus().getCode()); + assertEquals(Code.HA_NOT_AVAILABLE, response.getEntries(0).getStatus().getCode()); + } + + { + SendMessageResponse response = this.sendMessageActivity.convertToSendMessageResponse( + ProxyContext.create(), + SendMessageRequest.newBuilder().build(), + Lists.newArrayList(new SendResult(SendStatus.SEND_OK, null, null, null, 0)) + ); + assertEquals(Code.OK, response.getStatus().getCode()); + assertEquals(Code.OK, response.getEntries(0).getStatus().getCode()); + } + + { + SendMessageResponse response = this.sendMessageActivity.convertToSendMessageResponse( + ProxyContext.create(), + SendMessageRequest.newBuilder().build(), + Lists.newArrayList( + new SendResult(SendStatus.SEND_OK, null, null, null, 0), + new SendResult(SendStatus.SLAVE_NOT_AVAILABLE, null, null, null, 0) + ) + ); + assertEquals(Code.MULTIPLE_RESULTS, response.getStatus().getCode()); + } + } + + @Test(expected = GrpcProxyException.class) + public void testBuildErrorMessage() { + this.sendMessageActivity.buildMessage(null, + Lists.newArrayList( + Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(TOPIC) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId(MessageClientIDSetter.createUniqID()) + .setQueueId(0) + .setMessageType(MessageType.NORMAL) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .build()) + .setBody(ByteString.copyFromUtf8("123")) + .build(), + Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(TOPIC + 2) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId(MessageClientIDSetter.createUniqID()) + .setQueueId(0) + .setMessageType(MessageType.NORMAL) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .build()) + .setBody(ByteString.copyFromUtf8("123")) + .build() + ), + Resource.newBuilder().setName(TOPIC).build()); + } + + @Test + public void testBuildMessage() { + long deliveryTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(5); + ConfigurationManager.getProxyConfig().setMessageDelayLevel("1s 5s"); + ConfigurationManager.getProxyConfig().initData(); + String msgId = MessageClientIDSetter.createUniqID(); + + org.apache.rocketmq.common.message.Message messageExt = this.sendMessageActivity.buildMessage(null, + Lists.newArrayList( + Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(TOPIC) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId(msgId) + .setQueueId(0) + .setMessageType(MessageType.DELAY) + .setDeliveryTimestamp(Timestamps.fromMillis(deliveryTime)) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .build()) + .setBody(ByteString.copyFromUtf8("123")) + .build() + ), + Resource.newBuilder().setName(TOPIC).build()).get(0); + + assertEquals(MessageClientIDSetter.getUniqID(messageExt), msgId); + assertEquals(deliveryTime, Long.parseLong(messageExt.getProperty(MessageConst.PROPERTY_TIMER_DELIVER_MS))); + } + + @Test + public void testTxMessage() { + String msgId = MessageClientIDSetter.createUniqID(); + + Message message = Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(TOPIC) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId(msgId) + .setQueueId(0) + .setMessageType(MessageType.TRANSACTION) + .setOrphanedTransactionRecoveryDuration(Durations.fromSeconds(30)) + .setBodyEncoding(Encoding.GZIP) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .build()) + .setBody(ByteString.copyFromUtf8("123")) + .build(); + org.apache.rocketmq.common.message.Message messageExt = this.sendMessageActivity.buildMessage(null, + Lists.newArrayList( + message + ), + Resource.newBuilder().setName(TOPIC).build()).get(0); + + assertEquals(MessageClientIDSetter.getUniqID(messageExt), msgId); + assertEquals(MessageSysFlag.TRANSACTION_PREPARED_TYPE | MessageSysFlag.COMPRESSED_FLAG, sendMessageActivity.buildSysFlag(message)); + } + + @Test + public void testSendOrderMessageQueueSelector() throws Exception { + TopicRouteData topicRouteData = new TopicRouteData(); + QueueData queueData = new QueueData(); + BrokerData brokerData = new BrokerData(); + queueData.setBrokerName(BROKER_NAME); + queueData.setWriteQueueNums(8); + queueData.setPerm(PermName.PERM_WRITE); + topicRouteData.setQueueDatas(Lists.newArrayList(queueData)); + brokerData.setCluster(CLUSTER_NAME); + brokerData.setBrokerName(BROKER_NAME); + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(MixAll.MASTER_ID, BROKER_ADDR); + brokerData.setBrokerAddrs(brokerAddrs); + topicRouteData.setBrokerDatas(Lists.newArrayList(brokerData)); + + MessageQueueView messageQueueView = new MessageQueueView(TOPIC, topicRouteData, null); + SendMessageActivity.SendMessageQueueSelector selector1 = new SendMessageActivity.SendMessageQueueSelector( + SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setSystemProperties(SystemProperties.newBuilder() + .setMessageGroup(String.valueOf(1)) + .build()) + .build()) + .build() + ); + + TopicRouteService topicRouteService = mock(TopicRouteService.class); + MQFaultStrategy mqFaultStrategy = mock(MQFaultStrategy.class); + when(topicRouteService.getAllMessageQueueView(any(), any())).thenReturn(messageQueueView); + when(topicRouteService.getMqFaultStrategy()).thenReturn(mqFaultStrategy); + when(mqFaultStrategy.isSendLatencyFaultEnable()).thenReturn(false); + + SendMessageActivity.SendMessageQueueSelector selector2 = new SendMessageActivity.SendMessageQueueSelector( + SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setSystemProperties(SystemProperties.newBuilder() + .setMessageGroup(String.valueOf(1)) + .build()) + .build()) + .build() + ); + + SendMessageActivity.SendMessageQueueSelector selector3 = new SendMessageActivity.SendMessageQueueSelector( + SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setSystemProperties(SystemProperties.newBuilder() + .setMessageGroup(String.valueOf(2)) + .build()) + .build()) + .build() + ); + + assertEquals(selector1.select(ProxyContext.create(), messageQueueView), selector2.select(ProxyContext.create(), messageQueueView)); + assertNotEquals(selector1.select(ProxyContext.create(), messageQueueView), selector3.select(ProxyContext.create(), messageQueueView)); + } + + @Test + public void testSendNormalMessageQueueSelector() { + TopicRouteData topicRouteData = new TopicRouteData(); + QueueData queueData = new QueueData(); + BrokerData brokerData = new BrokerData(); + queueData.setBrokerName(BROKER_NAME); + queueData.setWriteQueueNums(2); + queueData.setPerm(PermName.PERM_WRITE); + topicRouteData.setQueueDatas(Lists.newArrayList(queueData)); + brokerData.setCluster(CLUSTER_NAME); + brokerData.setBrokerName(BROKER_NAME); + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(MixAll.MASTER_ID, BROKER_ADDR); + brokerData.setBrokerAddrs(brokerAddrs); + topicRouteData.setBrokerDatas(Lists.newArrayList(brokerData)); + + + SendMessageActivity.SendMessageQueueSelector selector = new SendMessageActivity.SendMessageQueueSelector( + SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder().build()) + .build() + ); + TopicRouteService topicRouteService = mock(TopicRouteService.class); + MQFaultStrategy mqFaultStrategy = mock(MQFaultStrategy.class); + when(topicRouteService.getMqFaultStrategy()).thenReturn(mqFaultStrategy); + when(mqFaultStrategy.isSendLatencyFaultEnable()).thenReturn(false); + MessageQueueView messageQueueView = new MessageQueueView(TOPIC, topicRouteData, topicRouteService.getMqFaultStrategy()); + + AddressableMessageQueue firstSelect = selector.select(ProxyContext.create(), messageQueueView); + AddressableMessageQueue secondSelect = selector.select(ProxyContext.create(), messageQueueView); + AddressableMessageQueue thirdSelect = selector.select(ProxyContext.create(), messageQueueView); + + assertEquals(firstSelect, thirdSelect); + assertNotEquals(firstSelect, secondSelect); + } + + @Test + public void testSendNormalMessageQueueSelectorPipeLine() throws Exception { + TopicRouteData topicRouteData = new TopicRouteData(); + int queueNums = 2; + + QueueData queueData = createQueueData(BROKER_NAME, queueNums); + QueueData queueData2 = createQueueData(BROKER_NAME2, queueNums); + topicRouteData.setQueueDatas(Lists.newArrayList(queueData,queueData2)); + + + BrokerData brokerData = createBrokerData(CLUSTER_NAME, BROKER_NAME, BROKER_ADDR); + BrokerData brokerData2 = createBrokerData(CLUSTER_NAME, BROKER_NAME2, BROKER_ADDR2); + topicRouteData.setBrokerDatas(Lists.newArrayList(brokerData, brokerData2)); + + SendMessageActivity.SendMessageQueueSelector selector = new SendMessageActivity.SendMessageQueueSelector( + SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder().build()) + .build() + ); + + ClientConfig cc = new ClientConfig(); + this.mqFaultStrategy = new MQFaultStrategy(cc, null, null); + mqFaultStrategy.setSendLatencyFaultEnable(true); + mqFaultStrategy.updateFaultItem(BROKER_NAME2, 1000, true, true); + mqFaultStrategy.updateFaultItem(BROKER_NAME, 1000, true, false); + + TopicRouteService topicRouteService = mock(TopicRouteService.class); + when(topicRouteService.getMqFaultStrategy()).thenReturn(mqFaultStrategy); + MessageQueueView messageQueueView = new MessageQueueView(TOPIC, topicRouteData, topicRouteService.getMqFaultStrategy()); + + + AddressableMessageQueue firstSelect = selector.select(ProxyContext.create(), messageQueueView); + assertEquals(firstSelect.getBrokerName(), BROKER_NAME2); + + mqFaultStrategy.updateFaultItem(BROKER_NAME2, 1000, true, false); + mqFaultStrategy.updateFaultItem(BROKER_NAME, 1000, true, true); + AddressableMessageQueue secondSelect = selector.select(ProxyContext.create(), messageQueueView); + assertEquals(secondSelect.getBrokerName(), BROKER_NAME); + } + @Test + public void testParameterValidate() { + // too large message body + assertThrows(GrpcProxyException.class, () -> { + try { + this.sendMessageActivity.sendMessage( + createContext(), + SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(TOPIC) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId("msgId") + .setQueueId(0) + .setMessageType(MessageType.NORMAL) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .build()) + .setBody(ByteString.copyFrom(new byte[4 * 1024 * 1024 + 1])) + .build()) + .build() + ).get(); + } catch (ExecutionException t) { + GrpcProxyException e = (GrpcProxyException) t.getCause(); + assertEquals(Code.MESSAGE_BODY_TOO_LARGE, e.getCode()); + throw e; + } + }); + + // black tag + assertThrows(GrpcProxyException.class, () -> { + try { + this.sendMessageActivity.sendMessage( + createContext(), + SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(TOPIC) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId("msgId") + .setQueueId(0) + .setTag(" ") + .setMessageType(MessageType.NORMAL) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .build()) + .setBody(ByteString.copyFrom(new byte[3])) + .build()) + .build() + ).get(); + } catch (ExecutionException t) { + GrpcProxyException e = (GrpcProxyException) t.getCause(); + assertEquals(Code.ILLEGAL_MESSAGE_TAG, e.getCode()); + throw e; + } + }); + + // tag with '|' + assertThrows(GrpcProxyException.class, () -> { + try { + this.sendMessageActivity.sendMessage( + createContext(), + SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(TOPIC) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId("msgId") + .setQueueId(0) + .setTag("|") + .setMessageType(MessageType.NORMAL) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .build()) + .setBody(ByteString.copyFrom(new byte[3])) + .build()) + .build() + ).get(); + } catch (ExecutionException t) { + GrpcProxyException e = (GrpcProxyException) t.getCause(); + assertEquals(Code.ILLEGAL_MESSAGE_TAG, e.getCode()); + throw e; + } + }); + + // tag with \t + assertThrows(GrpcProxyException.class, () -> { + try { + this.sendMessageActivity.sendMessage( + createContext(), + SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(TOPIC) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId("msgId") + .setQueueId(0) + .setTag("\t") + .setMessageType(MessageType.NORMAL) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .build()) + .setBody(ByteString.copyFrom(new byte[3])) + .build()) + .build() + ).get(); + } catch (ExecutionException t) { + GrpcProxyException e = (GrpcProxyException) t.getCause(); + assertEquals(Code.ILLEGAL_MESSAGE_TAG, e.getCode()); + throw e; + } + }); + + // blank message key + assertThrows(GrpcProxyException.class, () -> { + try { + this.sendMessageActivity.sendMessage( + createContext(), + SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(TOPIC) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId("msgId") + .setQueueId(0) + .addKeys(" ") + .setMessageType(MessageType.NORMAL) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .build()) + .setBody(ByteString.copyFrom(new byte[3])) + .build()) + .build() + ).get(); + } catch (ExecutionException t) { + GrpcProxyException e = (GrpcProxyException) t.getCause(); + assertEquals(Code.ILLEGAL_MESSAGE_KEY, e.getCode()); + throw e; + } + }); + + // blank message with \t + assertThrows(GrpcProxyException.class, () -> { + try { + this.sendMessageActivity.sendMessage( + createContext(), + SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(TOPIC) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId("msgId") + .setQueueId(0) + .addKeys("\t") + .setMessageType(MessageType.NORMAL) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .build()) + .setBody(ByteString.copyFrom(new byte[3])) + .build()) + .build() + ).get(); + } catch (ExecutionException t) { + GrpcProxyException e = (GrpcProxyException) t.getCause(); + assertEquals(Code.ILLEGAL_MESSAGE_KEY, e.getCode()); + throw e; + } + }); + + // blank message group + assertThrows(GrpcProxyException.class, () -> { + try { + this.sendMessageActivity.sendMessage( + createContext(), + SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(TOPIC) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId("msgId") + .setQueueId(0) + .setMessageGroup(" ") + .setMessageType(MessageType.NORMAL) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .build()) + .setBody(ByteString.copyFrom(new byte[3])) + .build()) + .build() + ).get(); + } catch (ExecutionException t) { + GrpcProxyException e = (GrpcProxyException) t.getCause(); + assertEquals(Code.ILLEGAL_MESSAGE_GROUP, e.getCode()); + throw e; + } + }); + + // long message group + assertThrows(GrpcProxyException.class, () -> { + try { + this.sendMessageActivity.sendMessage( + createContext(), + SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(TOPIC) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId("msgId") + .setQueueId(0) + .setMessageGroup(createStr(ConfigurationManager.getProxyConfig().getMaxMessageGroupSize() + 1)) + .setMessageType(MessageType.NORMAL) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .build()) + .setBody(ByteString.copyFrom(new byte[3])) + .build()) + .build() + ).get(); + } catch (ExecutionException t) { + GrpcProxyException e = (GrpcProxyException) t.getCause(); + assertEquals(Code.ILLEGAL_MESSAGE_GROUP, e.getCode()); + throw e; + } + }); + + // message group with \t + assertThrows(GrpcProxyException.class, () -> { + try { + this.sendMessageActivity.sendMessage( + createContext(), + SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(TOPIC) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId("msgId") + .setQueueId(0) + .setMessageGroup("\t") + .setMessageType(MessageType.NORMAL) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .build()) + .setBody(ByteString.copyFrom(new byte[3])) + .build()) + .build() + ).get(); + } catch (ExecutionException t) { + GrpcProxyException e = (GrpcProxyException) t.getCause(); + assertEquals(Code.ILLEGAL_MESSAGE_GROUP, e.getCode()); + throw e; + } + }); + + // too large message property + assertThrows(GrpcProxyException.class, () -> { + try { + this.sendMessageActivity.sendMessage( + createContext(), + SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(TOPIC) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId("msgId") + .setQueueId(0) + .setMessageType(MessageType.NORMAL) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .build()) + .putUserProperties("key", createStr(16 * 1024 + 1)) + .setBody(ByteString.copyFrom(new byte[3])) + .build()) + .build() + ).get(); + } catch (ExecutionException t) { + GrpcProxyException e = (GrpcProxyException) t.getCause(); + assertEquals(Code.MESSAGE_PROPERTIES_TOO_LARGE, e.getCode()); + throw e; + } + }); + + // too large message property + assertThrows(GrpcProxyException.class, () -> { + Map p = new HashMap<>(); + for (int i = 0; i <= ConfigurationManager.getProxyConfig().getUserPropertyMaxNum(); i++) { + p.put(String.valueOf(i), String.valueOf(i)); + } + try { + this.sendMessageActivity.sendMessage( + createContext(), + SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(TOPIC) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId("msgId") + .setQueueId(0) + .setMessageType(MessageType.NORMAL) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .build()) + .putAllUserProperties(p) + .setBody(ByteString.copyFrom(new byte[3])) + .build()) + .build() + ).get(); + } catch (ExecutionException t) { + GrpcProxyException e = (GrpcProxyException) t.getCause(); + assertEquals(Code.MESSAGE_PROPERTIES_TOO_LARGE, e.getCode()); + throw e; + } + }); + + // set system properties + assertThrows(GrpcProxyException.class, () -> { + try { + this.sendMessageActivity.sendMessage( + createContext(), + SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(TOPIC) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId("msgId") + .setQueueId(0) + .setMessageType(MessageType.NORMAL) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .build()) + .putUserProperties(MessageConst.PROPERTY_TRACE_SWITCH, "false") + .setBody(ByteString.copyFrom(new byte[3])) + .build()) + .build() + ).get(); + } catch (ExecutionException t) { + GrpcProxyException e = (GrpcProxyException) t.getCause(); + assertEquals(Code.ILLEGAL_MESSAGE_PROPERTY_KEY, e.getCode()); + throw e; + } + }); + + // set the key of user property with control character + assertThrows(GrpcProxyException.class, () -> { + try { + this.sendMessageActivity.sendMessage( + createContext(), + SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(TOPIC) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId("msgId") + .setQueueId(0) + .setMessageType(MessageType.NORMAL) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .build()) + .putUserProperties("\u0000", "hello") + .setBody(ByteString.copyFrom(new byte[3])) + .build()) + .build() + ).get(); + } catch (ExecutionException t) { + GrpcProxyException e = (GrpcProxyException) t.getCause(); + assertEquals(Code.ILLEGAL_MESSAGE_PROPERTY_KEY, e.getCode()); + throw e; + } + }); + + // set the value of user property with control character + assertThrows(GrpcProxyException.class, () -> { + try { + this.sendMessageActivity.sendMessage( + createContext(), + SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(TOPIC) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId("msgId") + .setQueueId(0) + .setMessageType(MessageType.NORMAL) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .build()) + .putUserProperties("p", "\u0000") + .setBody(ByteString.copyFrom(new byte[3])) + .build()) + .build() + ).get(); + } catch (ExecutionException t) { + GrpcProxyException e = (GrpcProxyException) t.getCause(); + assertEquals(Code.ILLEGAL_MESSAGE_PROPERTY_KEY, e.getCode()); + throw e; + } + }); + + // empty message id + assertThrows(GrpcProxyException.class, () -> { + try { + this.sendMessageActivity.sendMessage( + createContext(), + SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(TOPIC) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId(" ") + .setQueueId(0) + .setMessageType(MessageType.NORMAL) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .build()) + .setBody(ByteString.copyFrom(new byte[3])) + .build()) + .build() + ).get(); + } catch (ExecutionException t) { + GrpcProxyException e = (GrpcProxyException) t.getCause(); + assertEquals(Code.ILLEGAL_MESSAGE_ID, e.getCode()); + throw e; + } + }); + + // delay time + assertThrows(GrpcProxyException.class, () -> { + try { + this.sendMessageActivity.sendMessage( + createContext(), + SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(TOPIC) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId("id") + .setDeliveryTimestamp( + Timestamps.fromMillis(System.currentTimeMillis() + Duration.ofDays(1).toMillis() + Duration.ofSeconds(10).toMillis())) + .setQueueId(0) + .setMessageType(MessageType.NORMAL) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .build()) + .setBody(ByteString.copyFrom(new byte[3])) + .build()) + .build() + ).get(); + } catch (ExecutionException t) { + GrpcProxyException e = (GrpcProxyException) t.getCause(); + assertEquals(Code.ILLEGAL_DELIVERY_TIME, e.getCode()); + throw e; + } + }); + + // transactionRecoverySecond + assertThrows(GrpcProxyException.class, () -> { + try { + this.sendMessageActivity.sendMessage( + createContext(), + SendMessageRequest.newBuilder() + .addMessages(Message.newBuilder() + .setTopic(Resource.newBuilder() + .setName(TOPIC) + .build()) + .setSystemProperties(SystemProperties.newBuilder() + .setMessageId("id") + .setQueueId(0) + .setMessageType(MessageType.NORMAL) + .setBornTimestamp(Timestamps.fromMillis(System.currentTimeMillis())) + .setBornHost(StringUtils.defaultString(NetworkUtil.getLocalAddress(), "127.0.0.1:1234")) + .setOrphanedTransactionRecoveryDuration(Durations.fromHours(2)) + .setMessageType(MessageType.TRANSACTION) + .build()) + .setBody(ByteString.copyFrom(new byte[3])) + .build()) + .build() + ).get(); + } catch (ExecutionException t) { + GrpcProxyException e = (GrpcProxyException) t.getCause(); + assertEquals(Code.BAD_REQUEST, e.getCode()); + throw e; + } + }); + } + + private static String createStr(int len) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < len; i++) { + sb.append("a"); + } + return sb.toString(); + } + + private static QueueData createQueueData(String brokerName, int writeQueueNums) { + QueueData queueData = new QueueData(); + queueData.setBrokerName(brokerName); + queueData.setWriteQueueNums(writeQueueNums); + queueData.setPerm(PermName.PERM_WRITE); + return queueData; + } + + private static BrokerData createBrokerData(String clusterName, String brokerName, String brokerAddrs) { + BrokerData brokerData = new BrokerData(); + brokerData.setCluster(clusterName); + brokerData.setBrokerName(brokerName); + HashMap brokerAddrsMap = new HashMap<>(); + brokerAddrsMap.put(MixAll.MASTER_ID, brokerAddrs); + brokerData.setBrokerAddrs(brokerAddrsMap); + + return brokerData; + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/route/RouteActivityTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/route/RouteActivityTest.java new file mode 100644 index 00000000000..a7ba69098bc --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/route/RouteActivityTest.java @@ -0,0 +1,285 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.v2.route; + +import apache.rocketmq.v2.Address; +import apache.rocketmq.v2.AddressScheme; +import apache.rocketmq.v2.Broker; +import apache.rocketmq.v2.Code; +import apache.rocketmq.v2.Endpoints; +import apache.rocketmq.v2.MessageQueue; +import apache.rocketmq.v2.MessageType; +import apache.rocketmq.v2.Permission; +import apache.rocketmq.v2.QueryAssignmentRequest; +import apache.rocketmq.v2.QueryAssignmentResponse; +import apache.rocketmq.v2.QueryRouteRequest; +import apache.rocketmq.v2.QueryRouteResponse; +import apache.rocketmq.v2.Resource; +import com.google.common.net.HostAndPort; +import java.util.ArrayList; +import java.util.List; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.common.attribute.TopicMessageType; +import org.apache.rocketmq.common.constant.PermName; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.grpc.v2.BaseActivityTest; +import org.apache.rocketmq.proxy.grpc.v2.common.ResponseBuilder; +import org.apache.rocketmq.proxy.service.metadata.LocalMetadataService; +import org.apache.rocketmq.proxy.service.metadata.MetadataService; +import org.apache.rocketmq.proxy.service.route.ProxyTopicRouteData; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +public class RouteActivityTest extends BaseActivityTest { + + private RouteActivity routeActivity; + + private static final String CLUSTER = "cluster"; + private static final String TOPIC = "topic"; + private static final String GROUP = "group"; + private static final String BROKER_NAME = "brokerName"; + private static final Broker GRPC_BROKER = Broker.newBuilder().setName(BROKER_NAME).build(); + private static final Resource GRPC_TOPIC = Resource.newBuilder() + .setName(TOPIC) + .build(); + private static final Resource GRPC_GROUP = Resource.newBuilder() + .setName(GROUP) + .build(); + private static Endpoints grpcEndpoints = Endpoints.newBuilder() + .setScheme(AddressScheme.IPv4) + .addAddresses(Address.newBuilder().setHost("127.0.0.1").setPort(8080).build()) + .addAddresses(Address.newBuilder().setHost("127.0.0.2").setPort(8080).build()) + .build(); + private static List addressArrayList = new ArrayList<>(); + + static { + addressArrayList.add(new org.apache.rocketmq.proxy.common.Address( + org.apache.rocketmq.proxy.common.Address.AddressScheme.IPv4, + HostAndPort.fromParts("127.0.0.1", 8080))); + addressArrayList.add(new org.apache.rocketmq.proxy.common.Address( + org.apache.rocketmq.proxy.common.Address.AddressScheme.IPv4, + HostAndPort.fromParts("127.0.0.2", 8080))); + } + + @Before + public void before() throws Throwable { + super.before(); + this.routeActivity = new RouteActivity(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); + } + + @Test + public void testQueryRoute() throws Throwable { + ConfigurationManager.getProxyConfig().setGrpcServerPort(8080); + ArgumentCaptor> addressListCaptor = ArgumentCaptor.forClass(List.class); + when(this.messagingProcessor.getTopicRouteDataForProxy(any(), addressListCaptor.capture(), anyString())) + .thenReturn(createProxyTopicRouteData(2, 2, 6)); + MetadataService metadataService = Mockito.mock(LocalMetadataService.class); + when(this.messagingProcessor.getMetadataService()).thenReturn(metadataService); + when(metadataService.getTopicMessageType(any(), anyString())).thenReturn(TopicMessageType.NORMAL); + + QueryRouteResponse response = this.routeActivity.queryRoute( + createContext(), + QueryRouteRequest.newBuilder() + .setEndpoints(grpcEndpoints) + .setTopic(Resource.newBuilder().setName(TOPIC).build()) + .build() + ).get(); + + assertEquals(Code.OK, response.getStatus().getCode()); + assertEquals(4, response.getMessageQueuesCount()); + for (MessageQueue messageQueue : response.getMessageQueuesList()) { + assertEquals(grpcEndpoints, messageQueue.getBroker().getEndpoints()); + assertEquals(Permission.READ_WRITE, messageQueue.getPermission()); + } + } + + @Test + public void testQueryRouteTopicExist() throws Throwable { + when(this.messagingProcessor.getTopicRouteDataForProxy(any(), any(), anyString())) + .thenThrow(new MQBrokerException(ResponseCode.TOPIC_NOT_EXIST, "")); + + try { + this.routeActivity.queryRoute( + createContext(), + QueryRouteRequest.newBuilder() + .setEndpoints(grpcEndpoints) + .setTopic(GRPC_TOPIC) + .build() + ).get(); + } catch (Throwable t) { + assertEquals(Code.TOPIC_NOT_FOUND, ResponseBuilder.getInstance().buildStatus(t).getCode()); + return; + } + fail(); + } + + @Test + public void testQueryAssignmentWithNoReadPerm() throws Throwable { + when(this.messagingProcessor.getTopicRouteDataForProxy(any(), any(), anyString())) + .thenReturn(createProxyTopicRouteData(2, 2, PermName.PERM_WRITE)); + + QueryAssignmentResponse response = this.routeActivity.queryAssignment( + createContext(), + QueryAssignmentRequest.newBuilder() + .setEndpoints(grpcEndpoints) + .setTopic(GRPC_TOPIC) + .setGroup(GRPC_GROUP) + .build() + ).get(); + + assertEquals(Code.FORBIDDEN, response.getStatus().getCode()); + } + + @Test + public void testQueryAssignmentWithNoReadQueue() throws Throwable { + when(this.messagingProcessor.getTopicRouteDataForProxy(any(), any(), anyString())) + .thenReturn(createProxyTopicRouteData(0, 2, 6)); + + QueryAssignmentResponse response = this.routeActivity.queryAssignment( + createContext(), + QueryAssignmentRequest.newBuilder() + .setEndpoints(grpcEndpoints) + .setTopic(GRPC_TOPIC) + .setGroup(GRPC_GROUP) + .build() + ).get(); + + assertEquals(Code.FORBIDDEN, response.getStatus().getCode()); + } + + @Test + public void testQueryAssignment() throws Throwable { + when(this.messagingProcessor.getTopicRouteDataForProxy(any(), any(), anyString())) + .thenReturn(createProxyTopicRouteData(2, 2, 6)); + + QueryAssignmentResponse response = this.routeActivity.queryAssignment( + createContext(), + QueryAssignmentRequest.newBuilder() + .setEndpoints(grpcEndpoints) + .setTopic(GRPC_TOPIC) + .setGroup(GRPC_GROUP) + .build() + ).get(); + + assertEquals(Code.OK, response.getStatus().getCode()); + assertEquals(1, response.getAssignmentsCount()); + assertEquals(grpcEndpoints, response.getAssignments(0).getMessageQueue().getBroker().getEndpoints()); + } + + @Test + public void testQueryFifoAssignment() throws Throwable { + when(this.messagingProcessor.getTopicRouteDataForProxy(any(), any(), anyString())) + .thenReturn(createProxyTopicRouteData(2, 2, 6)); + SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); + subscriptionGroupConfig.setConsumeMessageOrderly(true); + when(this.messagingProcessor.getSubscriptionGroupConfig(any(), anyString())).thenReturn(subscriptionGroupConfig); + + QueryAssignmentResponse response = this.routeActivity.queryAssignment( + createContext(), + QueryAssignmentRequest.newBuilder() + .setEndpoints(grpcEndpoints) + .setTopic(GRPC_TOPIC) + .setGroup(GRPC_GROUP) + .build() + ).get(); + + assertEquals(Code.OK, response.getStatus().getCode()); + assertEquals(2, response.getAssignmentsCount()); + assertEquals(grpcEndpoints, response.getAssignments(0).getMessageQueue().getBroker().getEndpoints()); + } + + private static ProxyTopicRouteData createProxyTopicRouteData(int r, int w, int p) { + ProxyTopicRouteData proxyTopicRouteData = new ProxyTopicRouteData(); + proxyTopicRouteData.getQueueDatas().add(createQueueData(r, w, p)); + ProxyTopicRouteData.ProxyBrokerData proxyBrokerData = new ProxyTopicRouteData.ProxyBrokerData(); + proxyBrokerData.setCluster(CLUSTER); + proxyBrokerData.setBrokerName(BROKER_NAME); + proxyBrokerData.getBrokerAddrs().put(0L, addressArrayList); + proxyBrokerData.getBrokerAddrs().put(1L, addressArrayList); + proxyTopicRouteData.getBrokerDatas().add(proxyBrokerData); + return proxyTopicRouteData; + } + + @Test + public void testGenPartitionFromQueueData() throws Exception { + // test queueData with 8 read queues, 8 write queues, and rw permission, expect 8 rw queues. + QueueData queueDataWith8R8WPermRW = createQueueData(8, 8, PermName.PERM_READ | PermName.PERM_WRITE); + List partitionWith8R8WPermRW = this.routeActivity.genMessageQueueFromQueueData(queueDataWith8R8WPermRW, GRPC_TOPIC, TopicMessageType.NORMAL, GRPC_BROKER); + assertEquals(8, partitionWith8R8WPermRW.size()); + assertEquals(8, partitionWith8R8WPermRW.stream().filter(a -> a.getAcceptMessageTypesValue(0) == MessageType.NORMAL.getNumber()).count()); + assertEquals(8, partitionWith8R8WPermRW.stream().filter(a -> a.getPermission() == Permission.READ_WRITE).count()); + assertEquals(0, partitionWith8R8WPermRW.stream().filter(a -> a.getPermission() == Permission.READ).count()); + assertEquals(0, partitionWith8R8WPermRW.stream().filter(a -> a.getPermission() == Permission.WRITE).count()); + + // test queueData with 8 read queues, 8 write queues, and read only permission, expect 8 read only queues. + QueueData queueDataWith8R8WPermR = createQueueData(8, 8, PermName.PERM_READ); + List partitionWith8R8WPermR = this.routeActivity.genMessageQueueFromQueueData(queueDataWith8R8WPermR, GRPC_TOPIC, TopicMessageType.FIFO, GRPC_BROKER); + assertEquals(8, partitionWith8R8WPermR.size()); + assertEquals(8, partitionWith8R8WPermR.stream().filter(a -> a.getAcceptMessageTypesValue(0) == MessageType.FIFO.getNumber()).count()); + assertEquals(8, partitionWith8R8WPermR.stream().filter(a -> a.getPermission() == Permission.READ).count()); + assertEquals(0, partitionWith8R8WPermR.stream().filter(a -> a.getPermission() == Permission.READ_WRITE).count()); + assertEquals(0, partitionWith8R8WPermR.stream().filter(a -> a.getPermission() == Permission.WRITE).count()); + + // test queueData with 8 read queues, 8 write queues, and write only permission, expect 8 write only queues. + QueueData queueDataWith8R8WPermW = createQueueData(8, 8, PermName.PERM_WRITE); + List partitionWith8R8WPermW = this.routeActivity.genMessageQueueFromQueueData(queueDataWith8R8WPermW, GRPC_TOPIC, TopicMessageType.TRANSACTION, GRPC_BROKER); + assertEquals(8, partitionWith8R8WPermW.size()); + assertEquals(8, partitionWith8R8WPermW.stream().filter(a -> a.getAcceptMessageTypesValue(0) == MessageType.TRANSACTION.getNumber()).count()); + assertEquals(8, partitionWith8R8WPermW.stream().filter(a -> a.getPermission() == Permission.WRITE).count()); + assertEquals(0, partitionWith8R8WPermW.stream().filter(a -> a.getPermission() == Permission.READ_WRITE).count()); + assertEquals(0, partitionWith8R8WPermW.stream().filter(a -> a.getPermission() == Permission.READ).count()); + + // test queueData with 8 read queues, 0 write queues, and rw permission, expect 8 read only queues. + QueueData queueDataWith8R0WPermRW = createQueueData(8, 0, PermName.PERM_READ | PermName.PERM_WRITE); + List partitionWith8R0WPermRW = this.routeActivity.genMessageQueueFromQueueData(queueDataWith8R0WPermRW, GRPC_TOPIC, TopicMessageType.DELAY, GRPC_BROKER); + assertEquals(8, partitionWith8R0WPermRW.size()); + assertEquals(8, partitionWith8R0WPermRW.stream().filter(a -> a.getAcceptMessageTypesValue(0) == MessageType.DELAY.getNumber()).count()); + assertEquals(8, partitionWith8R0WPermRW.stream().filter(a -> a.getPermission() == Permission.READ).count()); + assertEquals(0, partitionWith8R0WPermRW.stream().filter(a -> a.getPermission() == Permission.READ_WRITE).count()); + assertEquals(0, partitionWith8R0WPermRW.stream().filter(a -> a.getPermission() == Permission.WRITE).count()); + + // test queueData with 4 read queues, 8 write queues, and rw permission, expect 4 rw queues and 4 write only queues. + QueueData queueDataWith4R8WPermRW = createQueueData(4, 8, PermName.PERM_READ | PermName.PERM_WRITE); + List partitionWith4R8WPermRW = this.routeActivity.genMessageQueueFromQueueData(queueDataWith4R8WPermRW, GRPC_TOPIC, TopicMessageType.UNSPECIFIED, GRPC_BROKER); + assertEquals(8, partitionWith4R8WPermRW.size()); + assertEquals(8, partitionWith4R8WPermRW.stream().filter(a -> a.getAcceptMessageTypesValue(0) == MessageType.MESSAGE_TYPE_UNSPECIFIED.getNumber()).count()); + assertEquals(4, partitionWith4R8WPermRW.stream().filter(a -> a.getPermission() == Permission.WRITE).count()); + assertEquals(4, partitionWith4R8WPermRW.stream().filter(a -> a.getPermission() == Permission.READ_WRITE).count()); + assertEquals(0, partitionWith4R8WPermRW.stream().filter(a -> a.getPermission() == Permission.READ).count()); + } + + private static QueueData createQueueData(int r, int w, int perm) { + QueueData queueData = new QueueData(); + queueData.setBrokerName(BROKER_NAME); + queueData.setReadQueueNums(r); + queueData.setWriteQueueNums(w); + queueData.setPerm(perm); + return queueData; + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/transaction/EndTransactionActivityTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/transaction/EndTransactionActivityTest.java new file mode 100644 index 00000000000..07a3abb9937 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/v2/transaction/EndTransactionActivityTest.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.grpc.v2.transaction; + +import apache.rocketmq.v2.Code; +import apache.rocketmq.v2.EndTransactionRequest; +import apache.rocketmq.v2.EndTransactionResponse; +import apache.rocketmq.v2.Resource; +import apache.rocketmq.v2.TransactionResolution; +import apache.rocketmq.v2.TransactionSource; +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.proxy.grpc.v2.BaseActivityTest; +import org.apache.rocketmq.proxy.processor.TransactionStatus; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.mockito.ArgumentCaptor; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +@RunWith(Parameterized.class) +public class EndTransactionActivityTest extends BaseActivityTest { + + private EndTransactionActivity endTransactionActivity; + private TransactionResolution resolution; + private TransactionSource source; + private TransactionStatus transactionStatus; + private Boolean fromTransactionCheck; + + public EndTransactionActivityTest(TransactionResolution resolution, TransactionSource source, + TransactionStatus transactionStatus, Boolean fromTransactionCheck) { + this.resolution = resolution; + this.source = source; + this.transactionStatus = transactionStatus; + this.fromTransactionCheck = fromTransactionCheck; + } + + @Before + public void before() throws Throwable { + super.before(); + this.endTransactionActivity = new EndTransactionActivity(messagingProcessor, grpcClientSettingsManager, grpcChannelManager); + } + + @Test + public void testEndTransaction() throws Throwable { + ArgumentCaptor transactionStatusCaptor = ArgumentCaptor.forClass(TransactionStatus.class); + ArgumentCaptor fromTransactionCheckCaptor = ArgumentCaptor.forClass(Boolean.class); + when(this.messagingProcessor.endTransaction(any(), any(), anyString(), anyString(), + transactionStatusCaptor.capture(), + fromTransactionCheckCaptor.capture())).thenReturn(CompletableFuture.completedFuture(null)); + + EndTransactionResponse response = this.endTransactionActivity.endTransaction( + createContext(), + EndTransactionRequest.newBuilder() + .setResolution(resolution) + .setTopic(Resource.newBuilder().setName("topic").build()) + .setMessageId(MessageClientIDSetter.createUniqID()) + .setTransactionId(MessageClientIDSetter.createUniqID()) + .setSource(source) + .build() + ).get(); + + assertEquals(Code.OK, response.getStatus().getCode()); + assertEquals(transactionStatus, transactionStatusCaptor.getValue()); + assertEquals(fromTransactionCheck, fromTransactionCheckCaptor.getValue()); + } + + @Parameterized.Parameters + public static Collection parameters() { + Object[][] p = new Object[][] { + {TransactionResolution.COMMIT, TransactionSource.SOURCE_CLIENT, TransactionStatus.COMMIT, false}, + {TransactionResolution.ROLLBACK, TransactionSource.SOURCE_SERVER_CHECK, TransactionStatus.ROLLBACK, true}, + {TransactionResolution.TRANSACTION_RESOLUTION_UNSPECIFIED, TransactionSource.SOURCE_SERVER_CHECK, TransactionStatus.UNKNOWN, true}, + }; + return Arrays.asList(p); + } +} \ No newline at end of file diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/processor/BaseProcessorTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/processor/BaseProcessorTest.java new file mode 100644 index 00000000000..072630e3947 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/processor/BaseProcessorTest.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.processor; + +import java.nio.charset.StandardCharsets; +import java.util.Random; +import java.util.UUID; +import org.apache.rocketmq.broker.client.ConsumerManager; +import org.apache.rocketmq.broker.client.ProducerManager; +import org.apache.rocketmq.common.consumer.ReceiptHandle; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.InitConfigTest; +import org.apache.rocketmq.proxy.service.ServiceManager; +import org.apache.rocketmq.proxy.service.message.MessageService; +import org.apache.rocketmq.proxy.service.metadata.MetadataService; +import org.apache.rocketmq.proxy.service.relay.ProxyRelayService; +import org.apache.rocketmq.proxy.service.route.TopicRouteService; +import org.apache.rocketmq.proxy.service.transaction.TransactionService; +import org.apache.rocketmq.remoting.protocol.header.ExtraInfoUtil; +import org.junit.Ignore; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.mockito.Mockito.when; + +@Ignore +@RunWith(MockitoJUnitRunner.Silent.class) +public class BaseProcessorTest extends InitConfigTest { + protected static final Random RANDOM = new Random(); + + @Mock + protected MessagingProcessor messagingProcessor; + @Mock + protected ServiceManager serviceManager; + @Mock + protected MessageService messageService; + @Mock + protected TopicRouteService topicRouteService; + @Mock + protected ProducerManager producerManager; + @Mock + protected ConsumerManager consumerManager; + @Mock + protected TransactionService transactionService; + @Mock + protected ProxyRelayService proxyRelayService; + @Mock + protected MetadataService metadataService; + + public void before() throws Throwable { + super.before(); + when(serviceManager.getMessageService()).thenReturn(messageService); + when(serviceManager.getTopicRouteService()).thenReturn(topicRouteService); + when(serviceManager.getProducerManager()).thenReturn(producerManager); + when(serviceManager.getConsumerManager()).thenReturn(consumerManager); + when(serviceManager.getTransactionService()).thenReturn(transactionService); + when(serviceManager.getProxyRelayService()).thenReturn(proxyRelayService); + when(serviceManager.getMetadataService()).thenReturn(metadataService); + when(messagingProcessor.getMetadataService()).thenReturn(metadataService); + } + + protected static ProxyContext createContext() { + return ProxyContext.create(); + } + + protected static MessageExt createMessageExt(String topic, String tags, int reconsumeTimes, long invisibleTime) { + return createMessageExt(topic, tags, reconsumeTimes, invisibleTime, System.currentTimeMillis(), + RANDOM.nextInt(Integer.MAX_VALUE), RANDOM.nextInt(Integer.MAX_VALUE), RANDOM.nextInt(Integer.MAX_VALUE), + RANDOM.nextInt(Integer.MAX_VALUE), "mockBroker"); + } + + protected static MessageExt createMessageExt(String topic, String tags, int reconsumeTimes, long invisibleTime, long popTime, + long startOffset, int reviveQid, int queueId, long queueOffset, String brokerName) { + MessageExt messageExt = new MessageExt(); + messageExt.setTopic(topic); + messageExt.setTags(tags); + messageExt.setReconsumeTimes(reconsumeTimes); + messageExt.setBody(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8)); + messageExt.setMsgId(MessageClientIDSetter.createUniqID()); + messageExt.setCommitLogOffset(RANDOM.nextInt(Integer.MAX_VALUE)); + MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_POP_CK, + ExtraInfoUtil.buildExtraInfo(startOffset, popTime, invisibleTime, reviveQid, topic, brokerName, queueId, queueOffset)); + return messageExt; + } + + protected static ReceiptHandle create(MessageExt messageExt) { + String ckInfo = messageExt.getProperty(MessageConst.PROPERTY_POP_CK); + if (ckInfo == null) { + return null; + } + return ReceiptHandle.decode(ckInfo + MessageConst.KEY_SEPARATOR + messageExt.getCommitLogOffset()); + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/processor/ConsumerProcessorTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/processor/ConsumerProcessorTest.java new file mode 100644 index 00000000000..f154033e45f --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/processor/ConsumerProcessorTest.java @@ -0,0 +1,360 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.processor; + +import com.google.common.collect.Sets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import org.apache.rocketmq.client.consumer.AckResult; +import org.apache.rocketmq.client.consumer.AckStatus; +import org.apache.rocketmq.client.consumer.PopResult; +import org.apache.rocketmq.client.consumer.PopStatus; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.KeyBuilder; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.constant.ConsumeInitMode; +import org.apache.rocketmq.common.consumer.ReceiptHandle; +import org.apache.rocketmq.common.filter.ExpressionType; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.common.ProxyExceptionCode; +import org.apache.rocketmq.proxy.common.utils.FutureUtils; +import org.apache.rocketmq.proxy.common.utils.ProxyUtils; +import org.apache.rocketmq.proxy.service.message.ReceiptHandleMessage; +import org.apache.rocketmq.proxy.service.route.AddressableMessageQueue; +import org.apache.rocketmq.proxy.service.route.MessageQueueView; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.filter.FilterAPI; +import org.apache.rocketmq.remoting.protocol.header.AckMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ChangeInvisibleTimeRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PopMessageRequestHeader; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.stubbing.Answer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class ConsumerProcessorTest extends BaseProcessorTest { + + private static final String CONSUMER_GROUP = "consumerGroup"; + private static final String TOPIC = "topic"; + private static final String CLIENT_ID = "clientId"; + + private ConsumerProcessor consumerProcessor; + + @Before + public void before() throws Throwable { + super.before(); + this.consumerProcessor = new ConsumerProcessor(messagingProcessor, serviceManager, Executors.newCachedThreadPool()); + } + + @Test + public void testPopMessage() throws Throwable { + final String tag = "tag"; + final long invisibleTime = Duration.ofSeconds(15).toMillis(); + ArgumentCaptor messageQueueArgumentCaptor = ArgumentCaptor.forClass(AddressableMessageQueue.class); + ArgumentCaptor requestHeaderArgumentCaptor = ArgumentCaptor.forClass(PopMessageRequestHeader.class); + + List messageExtList = new ArrayList<>(); + messageExtList.add(createMessageExt(TOPIC, "noMatch", 0, invisibleTime)); + messageExtList.add(createMessageExt(TOPIC, tag, 0, invisibleTime)); + messageExtList.add(createMessageExt(TOPIC, tag, 1, invisibleTime)); + PopResult innerPopResult = new PopResult(PopStatus.FOUND, messageExtList); + when(this.messageService.popMessage(any(), messageQueueArgumentCaptor.capture(), requestHeaderArgumentCaptor.capture(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(innerPopResult)); + + when(this.topicRouteService.getCurrentMessageQueueView(any(), anyString())) + .thenReturn(mock(MessageQueueView.class)); + + ArgumentCaptor ackMessageIdArgumentCaptor = ArgumentCaptor.forClass(String.class); + when(this.messagingProcessor.ackMessage(any(), any(), ackMessageIdArgumentCaptor.capture(), anyString(), anyString(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(mock(AckResult.class))); + + ArgumentCaptor toDLQMessageIdArgumentCaptor = ArgumentCaptor.forClass(String.class); + when(this.messagingProcessor.forwardMessageToDeadLetterQueue(any(), any(), toDLQMessageIdArgumentCaptor.capture(), anyString(), anyString(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(mock(RemotingCommand.class))); + + AddressableMessageQueue messageQueue = mock(AddressableMessageQueue.class); + PopResult popResult = this.consumerProcessor.popMessage( + createContext(), + (ctx, messageQueueView) -> messageQueue, + CONSUMER_GROUP, + TOPIC, + 60, + invisibleTime, + Duration.ofSeconds(3).toMillis(), + ConsumeInitMode.MAX, + FilterAPI.build(TOPIC, tag, ExpressionType.TAG), + false, + (ctx, consumerGroup, subscriptionData, messageExt) -> { + if (!messageExt.getTags().equals(tag)) { + return PopMessageResultFilter.FilterResult.NO_MATCH; + } + if (messageExt.getReconsumeTimes() > 0) { + return PopMessageResultFilter.FilterResult.TO_DLQ; + } + return PopMessageResultFilter.FilterResult.MATCH; + }, + null, + Duration.ofSeconds(3).toMillis() + ).get(); + + assertSame(messageQueue, messageQueueArgumentCaptor.getValue()); + assertEquals(CONSUMER_GROUP, requestHeaderArgumentCaptor.getValue().getConsumerGroup()); + assertEquals(TOPIC, requestHeaderArgumentCaptor.getValue().getTopic()); + assertEquals(ProxyUtils.MAX_MSG_NUMS_FOR_POP_REQUEST, requestHeaderArgumentCaptor.getValue().getMaxMsgNums()); + assertEquals(tag, requestHeaderArgumentCaptor.getValue().getExp()); + assertEquals(ExpressionType.TAG, requestHeaderArgumentCaptor.getValue().getExpType()); + + assertEquals(PopStatus.FOUND, popResult.getPopStatus()); + assertEquals(1, popResult.getMsgFoundList().size()); + assertEquals(messageExtList.get(1), popResult.getMsgFoundList().get(0)); + + assertEquals(messageExtList.get(0).getMsgId(), ackMessageIdArgumentCaptor.getValue()); + assertEquals(messageExtList.get(2).getMsgId(), toDLQMessageIdArgumentCaptor.getValue()); + } + + @Test + public void testAckMessage() throws Throwable { + ReceiptHandle handle = create(createMessageExt(MixAll.RETRY_GROUP_TOPIC_PREFIX + TOPIC, "", 0, 3000)); + assertNotNull(handle); + + ArgumentCaptor requestHeaderArgumentCaptor = ArgumentCaptor.forClass(AckMessageRequestHeader.class); + AckResult innerAckResult = new AckResult(); + innerAckResult.setStatus(AckStatus.OK); + when(this.messageService.ackMessage(any(), any(), anyString(), requestHeaderArgumentCaptor.capture(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(innerAckResult)); + + AckResult ackResult = this.consumerProcessor.ackMessage(createContext(), handle, MessageClientIDSetter.createUniqID(), + CONSUMER_GROUP, TOPIC, 3000).get(); + + assertEquals(AckStatus.OK, ackResult.getStatus()); + assertEquals(KeyBuilder.buildPopRetryTopic(TOPIC, CONSUMER_GROUP, new BrokerConfig().isEnableRetryTopicV2()), requestHeaderArgumentCaptor.getValue().getTopic()); + assertEquals(CONSUMER_GROUP, requestHeaderArgumentCaptor.getValue().getConsumerGroup()); + assertEquals(handle.getReceiptHandle(), requestHeaderArgumentCaptor.getValue().getExtraInfo()); + } + + @Test + public void testBatchAckExpireMessage() throws Throwable { + String brokerName1 = "brokerName1"; + + List receiptHandleMessageList = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + MessageExt expireMessage = createMessageExt(TOPIC, "", 0, 3000, System.currentTimeMillis() - 10000, + 0, 0, 0, i, brokerName1); + ReceiptHandle expireHandle = create(expireMessage); + receiptHandleMessageList.add(new ReceiptHandleMessage(expireHandle, expireMessage.getMsgId())); + } + + List batchAckResultList = this.consumerProcessor.batchAckMessage(createContext(), receiptHandleMessageList, CONSUMER_GROUP, TOPIC, 3000).get(); + + verify(this.messageService, never()).batchAckMessage(any(), anyList(), anyString(), anyString(), anyLong()); + assertEquals(receiptHandleMessageList.size(), batchAckResultList.size()); + for (BatchAckResult batchAckResult : batchAckResultList) { + assertNull(batchAckResult.getAckResult()); + assertNotNull(batchAckResult.getProxyException()); + assertNotNull(batchAckResult.getReceiptHandleMessage()); + } + + } + + @Test + public void testBatchAckMessage() throws Throwable { + String brokerName1 = "brokerName1"; + String brokerName2 = "brokerName2"; + String errThrowBrokerName = "errThrowBrokerName"; + MessageExt expireMessage = createMessageExt(TOPIC, "", 0, 3000, System.currentTimeMillis() - 10000, + 0, 0, 0, 0, brokerName1); + ReceiptHandle expireHandle = create(expireMessage); + + List receiptHandleMessageList = new ArrayList<>(); + receiptHandleMessageList.add(new ReceiptHandleMessage(expireHandle, expireMessage.getMsgId())); + List broker1Msg = new ArrayList<>(); + List broker2Msg = new ArrayList<>(); + + long now = System.currentTimeMillis(); + int msgNum = 3; + for (int i = 0; i < msgNum; i++) { + MessageExt brokerMessage = createMessageExt(TOPIC, "", 0, 3000, now, + 0, 0, 0, i + 1, brokerName1); + ReceiptHandle brokerHandle = create(brokerMessage); + receiptHandleMessageList.add(new ReceiptHandleMessage(brokerHandle, brokerMessage.getMsgId())); + broker1Msg.add(brokerMessage.getMsgId()); + } + for (int i = 0; i < msgNum; i++) { + MessageExt brokerMessage = createMessageExt(TOPIC, "", 0, 3000, now, + 0, 0, 0, i + 1, brokerName2); + ReceiptHandle brokerHandle = create(brokerMessage); + receiptHandleMessageList.add(new ReceiptHandleMessage(brokerHandle, brokerMessage.getMsgId())); + broker2Msg.add(brokerMessage.getMsgId()); + } + + // for this message, will throw exception in batchAckMessage + MessageExt errThrowMessage = createMessageExt(TOPIC, "", 0, 3000, now, + 0, 0, 0, 0, errThrowBrokerName); + ReceiptHandle errThrowHandle = create(errThrowMessage); + receiptHandleMessageList.add(new ReceiptHandleMessage(errThrowHandle, errThrowMessage.getMsgId())); + + Collections.shuffle(receiptHandleMessageList); + + doAnswer((Answer>) invocation -> { + List handleMessageList = invocation.getArgument(1, List.class); + AckResult ackResult = new AckResult(); + String brokerName = handleMessageList.get(0).getReceiptHandle().getBrokerName(); + if (brokerName.equals(brokerName1)) { + ackResult.setStatus(AckStatus.OK); + } else if (brokerName.equals(brokerName2)) { + ackResult.setStatus(AckStatus.NO_EXIST); + } else { + return FutureUtils.completeExceptionally(new RuntimeException()); + } + + return CompletableFuture.completedFuture(ackResult); + }).when(this.messageService).batchAckMessage(any(), anyList(), anyString(), anyString(), anyLong()); + + List batchAckResultList = this.consumerProcessor.batchAckMessage(createContext(), receiptHandleMessageList, CONSUMER_GROUP, TOPIC, 3000).get(); + assertEquals(receiptHandleMessageList.size(), batchAckResultList.size()); + + // check ackResult for each msg + Map msgBatchAckResult = new HashMap<>(); + for (BatchAckResult batchAckResult : batchAckResultList) { + msgBatchAckResult.put(batchAckResult.getReceiptHandleMessage().getMessageId(), batchAckResult); + } + for (String msgId : broker1Msg) { + assertEquals(AckStatus.OK, msgBatchAckResult.get(msgId).getAckResult().getStatus()); + assertNull(msgBatchAckResult.get(msgId).getProxyException()); + } + for (String msgId : broker2Msg) { + assertEquals(AckStatus.NO_EXIST, msgBatchAckResult.get(msgId).getAckResult().getStatus()); + assertNull(msgBatchAckResult.get(msgId).getProxyException()); + } + assertNotNull(msgBatchAckResult.get(expireMessage.getMsgId()).getProxyException()); + assertEquals(ProxyExceptionCode.INVALID_RECEIPT_HANDLE, msgBatchAckResult.get(expireMessage.getMsgId()).getProxyException().getCode()); + assertNull(msgBatchAckResult.get(expireMessage.getMsgId()).getAckResult()); + + assertNotNull(msgBatchAckResult.get(errThrowMessage.getMsgId()).getProxyException()); + assertEquals(ProxyExceptionCode.INTERNAL_SERVER_ERROR, msgBatchAckResult.get(errThrowMessage.getMsgId()).getProxyException().getCode()); + assertNull(msgBatchAckResult.get(errThrowMessage.getMsgId()).getAckResult()); + } + + @Test + public void testChangeInvisibleTime() throws Throwable { + ReceiptHandle handle = create(createMessageExt(MixAll.RETRY_GROUP_TOPIC_PREFIX + TOPIC, "", 0, 3000)); + assertNotNull(handle); + + ArgumentCaptor requestHeaderArgumentCaptor = ArgumentCaptor.forClass(ChangeInvisibleTimeRequestHeader.class); + AckResult innerAckResult = new AckResult(); + innerAckResult.setStatus(AckStatus.OK); + when(this.messageService.changeInvisibleTime(any(), any(), anyString(), requestHeaderArgumentCaptor.capture(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(innerAckResult)); + + AckResult ackResult = this.consumerProcessor.changeInvisibleTime(createContext(), handle, MessageClientIDSetter.createUniqID(), + CONSUMER_GROUP, TOPIC, 1000, 3000).get(); + + assertEquals(AckStatus.OK, ackResult.getStatus()); + assertEquals(KeyBuilder.buildPopRetryTopic(TOPIC, CONSUMER_GROUP, new BrokerConfig().isEnableRetryTopicV2()), requestHeaderArgumentCaptor.getValue().getTopic()); + assertEquals(CONSUMER_GROUP, requestHeaderArgumentCaptor.getValue().getConsumerGroup()); + assertEquals(1000, requestHeaderArgumentCaptor.getValue().getInvisibleTime().longValue()); + assertEquals(handle.getReceiptHandle(), requestHeaderArgumentCaptor.getValue().getExtraInfo()); + } + + @Test + public void testLockBatch() throws Throwable { + Set mqSet = new HashSet<>(); + MessageQueue mq1 = new MessageQueue(TOPIC, "broker1", 0); + AddressableMessageQueue addressableMessageQueue1 = new AddressableMessageQueue(mq1, "127.0.0.1"); + MessageQueue mq2 = new MessageQueue(TOPIC, "broker2", 0); + AddressableMessageQueue addressableMessageQueue2 = new AddressableMessageQueue(mq2, "127.0.0.1"); + mqSet.add(mq1); + mqSet.add(mq2); + when(this.topicRouteService.buildAddressableMessageQueue(any(), any())).thenAnswer(i -> new AddressableMessageQueue((MessageQueue) i.getArguments()[1], "127.0.0.1")); + when(this.messageService.lockBatchMQ(any(), eq(addressableMessageQueue1), any(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(Sets.newHashSet(mq1))); + when(this.messageService.lockBatchMQ(any(), eq(addressableMessageQueue2), any(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(Sets.newHashSet(mq2))); + Set result = this.consumerProcessor.lockBatchMQ(ProxyContext.create(), mqSet, CONSUMER_GROUP, CLIENT_ID, 1000) + .get(); + assertThat(result).isEqualTo(mqSet); + } + + @Test + public void testLockBatchPartialSuccess() throws Throwable { + Set mqSet = new HashSet<>(); + MessageQueue mq1 = new MessageQueue(TOPIC, "broker1", 0); + AddressableMessageQueue addressableMessageQueue1 = new AddressableMessageQueue(mq1, "127.0.0.1"); + MessageQueue mq2 = new MessageQueue(TOPIC, "broker2", 0); + AddressableMessageQueue addressableMessageQueue2 = new AddressableMessageQueue(mq2, "127.0.0.1"); + mqSet.add(mq1); + mqSet.add(mq2); + when(this.topicRouteService.buildAddressableMessageQueue(any(), any())).thenAnswer(i -> new AddressableMessageQueue((MessageQueue) i.getArguments()[1], "127.0.0.1")); + when(this.messageService.lockBatchMQ(any(), eq(addressableMessageQueue1), any(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(Sets.newHashSet(mq1))); + when(this.messageService.lockBatchMQ(any(), eq(addressableMessageQueue2), any(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(Sets.newHashSet())); + Set result = this.consumerProcessor.lockBatchMQ(ProxyContext.create(), mqSet, CONSUMER_GROUP, CLIENT_ID, 1000) + .get(); + assertThat(result).isEqualTo(Sets.newHashSet(mq1)); + } + + @Test + public void testLockBatchPartialSuccessWithException() throws Throwable { + Set mqSet = new HashSet<>(); + MessageQueue mq1 = new MessageQueue(TOPIC, "broker1", 0); + AddressableMessageQueue addressableMessageQueue1 = new AddressableMessageQueue(mq1, "127.0.0.1"); + MessageQueue mq2 = new MessageQueue(TOPIC, "broker2", 0); + AddressableMessageQueue addressableMessageQueue2 = new AddressableMessageQueue(mq2, "127.0.0.1"); + mqSet.add(mq1); + mqSet.add(mq2); + when(this.topicRouteService.buildAddressableMessageQueue(any(), any())).thenAnswer(i -> new AddressableMessageQueue((MessageQueue) i.getArguments()[1], "127.0.0.1")); + when(this.messageService.lockBatchMQ(any(), eq(addressableMessageQueue1), any(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(Sets.newHashSet(mq1))); + CompletableFuture> future = new CompletableFuture<>(); + future.completeExceptionally(new MQBrokerException(1, "err")); + when(this.messageService.lockBatchMQ(any(), eq(addressableMessageQueue2), any(), anyLong())) + .thenReturn(future); + Set result = this.consumerProcessor.lockBatchMQ(ProxyContext.create(), mqSet, CONSUMER_GROUP, CLIENT_ID, 1000) + .get(); + assertThat(result).isEqualTo(Sets.newHashSet(mq1)); + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/processor/ProducerProcessorTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/processor/ProducerProcessorTest.java new file mode 100644 index 00000000000..af61c803153 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/processor/ProducerProcessorTest.java @@ -0,0 +1,213 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.processor; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.KeyBuilder; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.attribute.TopicMessageType; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.proxy.service.route.AddressableMessageQueue; +import org.apache.rocketmq.proxy.service.transaction.TransactionData; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.header.ConsumerSendMsgBackRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.assertj.core.util.Lists; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ProducerProcessorTest extends BaseProcessorTest { + + private static final String PRODUCER_GROUP = "producerGroup"; + private static final String CONSUMER_GROUP = "consumerGroup"; + private static final String TOPIC = "topic"; + + private ProducerProcessor producerProcessor; + + @Before + public void before() throws Throwable { + super.before(); + this.producerProcessor = new ProducerProcessor(this.messagingProcessor, this.serviceManager, Executors.newCachedThreadPool()); + } + + @Test + public void testSendMessage() throws Throwable { + when(metadataService.getTopicMessageType(any(), eq(TOPIC))).thenReturn(TopicMessageType.NORMAL); + String txId = MessageClientIDSetter.createUniqID(); + String msgId = MessageClientIDSetter.createUniqID(); + long commitLogOffset = 1000L; + long queueOffset = 100L; + + SendResult sendResult = new SendResult(); + sendResult.setSendStatus(SendStatus.SEND_OK); + sendResult.setTransactionId(txId); + sendResult.setMsgId(msgId); + sendResult.setOffsetMsgId(createOffsetMsgId(commitLogOffset)); + sendResult.setQueueOffset(queueOffset); + ArgumentCaptor requestHeaderArgumentCaptor = ArgumentCaptor.forClass(SendMessageRequestHeader.class); + when(this.messageService.sendMessage(any(), any(), any(), requestHeaderArgumentCaptor.capture(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(Lists.newArrayList(sendResult))); + + List messageList = new ArrayList<>(); + Message messageExt = createMessageExt(TOPIC, "tag", 0, 0); + messageList.add(messageExt); + AddressableMessageQueue messageQueue = mock(AddressableMessageQueue.class); + when(messageQueue.getBrokerName()).thenReturn("mockBroker"); + + ArgumentCaptor brokerNameCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor tranStateTableOffsetCaptor = ArgumentCaptor.forClass(Long.class); + ArgumentCaptor commitLogOffsetCaptor = ArgumentCaptor.forClass(Long.class); + when(transactionService.addTransactionDataByBrokerName( + any(), + brokerNameCaptor.capture(), + anyString(), + tranStateTableOffsetCaptor.capture(), + commitLogOffsetCaptor.capture(), + anyString(), any())).thenReturn(mock(TransactionData.class)); + + List sendResultList = this.producerProcessor.sendMessage( + createContext(), + (ctx, messageQueueView) -> messageQueue, + PRODUCER_GROUP, + MessageSysFlag.TRANSACTION_PREPARED_TYPE, + messageList, + 3000 + ).get(); + + assertNotNull(sendResultList); + assertEquals("mockBroker", brokerNameCaptor.getValue()); + assertEquals(queueOffset, tranStateTableOffsetCaptor.getValue().longValue()); + assertEquals(commitLogOffset, commitLogOffsetCaptor.getValue().longValue()); + + SendMessageRequestHeader requestHeader = requestHeaderArgumentCaptor.getValue(); + assertEquals(PRODUCER_GROUP, requestHeader.getProducerGroup()); + assertEquals(TOPIC, requestHeader.getTopic()); + } + + @Test + public void testSendRetryMessage() throws Throwable { + String txId = MessageClientIDSetter.createUniqID(); + String msgId = MessageClientIDSetter.createUniqID(); + long commitLogOffset = 1000L; + long queueOffset = 100L; + + SendResult sendResult = new SendResult(); + sendResult.setSendStatus(SendStatus.SEND_OK); + sendResult.setTransactionId(txId); + sendResult.setMsgId(msgId); + sendResult.setOffsetMsgId(createOffsetMsgId(commitLogOffset)); + sendResult.setQueueOffset(queueOffset); + ArgumentCaptor requestHeaderArgumentCaptor = ArgumentCaptor.forClass(SendMessageRequestHeader.class); + when(this.messageService.sendMessage(any(), any(), any(), requestHeaderArgumentCaptor.capture(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(Lists.newArrayList(sendResult))); + + List messageExtList = new ArrayList<>(); + Message messageExt = createMessageExt(MixAll.getRetryTopic(CONSUMER_GROUP), "tag", 0, 0); + MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_RECONSUME_TIME, "1"); + MessageAccessor.putProperty(messageExt, MessageConst.PROPERTY_MAX_RECONSUME_TIMES, "16"); + messageExtList.add(messageExt); + AddressableMessageQueue messageQueue = mock(AddressableMessageQueue.class); + when(messageQueue.getBrokerName()).thenReturn("mockBroker"); + + ArgumentCaptor brokerNameCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor tranStateTableOffsetCaptor = ArgumentCaptor.forClass(Long.class); + ArgumentCaptor commitLogOffsetCaptor = ArgumentCaptor.forClass(Long.class); + when(transactionService.addTransactionDataByBrokerName( + any(), + brokerNameCaptor.capture(), + anyString(), + tranStateTableOffsetCaptor.capture(), + commitLogOffsetCaptor.capture(), + anyString(), any())).thenReturn(mock(TransactionData.class)); + + List sendResultList = this.producerProcessor.sendMessage( + createContext(), + (ctx, messageQueueView) -> messageQueue, + PRODUCER_GROUP, + MessageSysFlag.TRANSACTION_PREPARED_TYPE, + messageExtList, + 3000 + ).get(); + + assertNotNull(sendResultList); + assertEquals("mockBroker", brokerNameCaptor.getValue()); + assertEquals(queueOffset, tranStateTableOffsetCaptor.getValue().longValue()); + assertEquals(commitLogOffset, commitLogOffsetCaptor.getValue().longValue()); + + SendMessageRequestHeader requestHeader = requestHeaderArgumentCaptor.getValue(); + assertEquals(PRODUCER_GROUP, requestHeader.getProducerGroup()); + assertEquals(MixAll.getRetryTopic(CONSUMER_GROUP), requestHeader.getTopic()); + assertEquals(1, requestHeader.getReconsumeTimes().intValue()); + assertEquals(16, requestHeader.getMaxReconsumeTimes().intValue()); + } + + @Test + public void testForwardMessageToDeadLetterQueue() throws Throwable { + ArgumentCaptor requestHeaderArgumentCaptor = ArgumentCaptor.forClass(ConsumerSendMsgBackRequestHeader.class); + when(this.messageService.sendMessageBack(any(), any(), anyString(), requestHeaderArgumentCaptor.capture(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(mock(RemotingCommand.class))); + + MessageExt messageExt = createMessageExt(KeyBuilder.buildPopRetryTopic(TOPIC, CONSUMER_GROUP, new BrokerConfig().isEnableRetryTopicV2()), "", 16, 3000); + RemotingCommand remotingCommand = this.producerProcessor.forwardMessageToDeadLetterQueue( + createContext(), + create(messageExt), + messageExt.getMsgId(), + CONSUMER_GROUP, + TOPIC, + 3000 + ).get(); + + assertNotNull(remotingCommand); + ConsumerSendMsgBackRequestHeader requestHeader = requestHeaderArgumentCaptor.getValue(); + assertEquals(messageExt.getTopic(), requestHeader.getOriginTopic()); + assertEquals(messageExt.getMsgId(), requestHeader.getOriginMsgId()); + assertEquals(CONSUMER_GROUP, requestHeader.getGroup()); + } + + private static String createOffsetMsgId(long commitLogOffset) { + int msgIDLength = 4 + 4 + 8; + ByteBuffer byteBufferMsgId = ByteBuffer.allocate(msgIDLength); + return MessageDecoder.createMessageId(byteBufferMsgId, + MessageExt.socketAddress2ByteBuffer(NetworkUtil.string2SocketAddress("127.0.0.1:10911")), + commitLogOffset); + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/processor/TransactionProcessorTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/processor/TransactionProcessorTest.java new file mode 100644 index 00000000000..6bffb15bd13 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/processor/TransactionProcessorTest.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.processor; + +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.proxy.service.transaction.EndTransactionRequestData; +import org.apache.rocketmq.remoting.protocol.header.EndTransactionRequestHeader; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; + +public class TransactionProcessorTest extends BaseProcessorTest { + + private static final String PRODUCER_GROUP = "producerGroup"; + private TransactionProcessor transactionProcessor; + + @Before + public void before() throws Throwable { + super.before(); + this.transactionProcessor = new TransactionProcessor(this.messagingProcessor, this.serviceManager); + } + + @Test + public void testEndTransaction() throws Throwable { + testEndTransaction(MessageSysFlag.TRANSACTION_COMMIT_TYPE, TransactionStatus.COMMIT); + testEndTransaction(MessageSysFlag.TRANSACTION_NOT_TYPE, TransactionStatus.UNKNOWN); + testEndTransaction(MessageSysFlag.TRANSACTION_ROLLBACK_TYPE, TransactionStatus.ROLLBACK); + } + + protected void testEndTransaction(int sysFlag, TransactionStatus transactionStatus) throws Throwable { + when(this.messageService.endTransactionOneway(any(), any(), any(), anyLong())).thenReturn(CompletableFuture.completedFuture(null)); + ArgumentCaptor commitOrRollbackCaptor = ArgumentCaptor.forClass(Integer.class); + when(transactionService.genEndTransactionRequestHeader(any(), anyString(), commitOrRollbackCaptor.capture(), anyBoolean(), anyString(), anyString())) + .thenReturn(new EndTransactionRequestData("brokerName", new EndTransactionRequestHeader())); + + this.transactionProcessor.endTransaction( + createContext(), + "transactionId", + "msgId", + PRODUCER_GROUP, + transactionStatus, + true, + 3000 + ); + + assertEquals(sysFlag, commitOrRollbackCaptor.getValue().intValue()); + + reset(this.messageService); + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/processor/channel/RemoteChannelTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/processor/channel/RemoteChannelTest.java new file mode 100644 index 00000000000..d504fdc5f99 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/processor/channel/RemoteChannelTest.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.processor.channel; + +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +public class RemoteChannelTest { + + @Test + public void testEncodeAndDecode() { + String remoteProxyIp = "11.193.0.1"; + String remoteAddress = "10.152.39.53:9768"; + String localAddress = "11.193.0.1:1210"; + ChannelProtocolType type = ChannelProtocolType.REMOTING; + String extendAttribute = RandomStringUtils.randomAlphabetic(10); + RemoteChannel remoteChannel = new RemoteChannel(remoteProxyIp, remoteAddress, localAddress, type, extendAttribute); + + String encodedData = remoteChannel.encode(); + assertNotNull(encodedData); + + RemoteChannel decodedChannel = RemoteChannel.decode(encodedData); + assertEquals(remoteProxyIp, decodedChannel.remoteProxyIp); + assertEquals(remoteAddress, decodedChannel.getRemoteAddress()); + assertEquals(localAddress, decodedChannel.getLocalAddress()); + assertEquals(type, decodedChannel.type); + assertEquals(extendAttribute, decodedChannel.extendAttribute); + + assertNull(RemoteChannel.decode("")); + } +} \ No newline at end of file diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/activity/AbstractRemotingActivityTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/activity/AbstractRemotingActivityTest.java new file mode 100644 index 00000000000..b2bd3a35f16 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/activity/AbstractRemotingActivityTest.java @@ -0,0 +1,218 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.activity; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import org.apache.rocketmq.acl.common.AclException; +import org.apache.rocketmq.client.exception.MQBrokerException; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.common.ProxyException; +import org.apache.rocketmq.proxy.common.ProxyExceptionCode; +import org.apache.rocketmq.proxy.config.InitConfigTest; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.proxy.processor.channel.ChannelProtocolType; +import org.apache.rocketmq.proxy.service.channel.SimpleChannel; +import org.apache.rocketmq.proxy.service.channel.SimpleChannelHandlerContext; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.netty.AttributeKeys; +import org.apache.rocketmq.remoting.protocol.LanguageCode; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.concurrent.CompletableFuture; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class AbstractRemotingActivityTest extends InitConfigTest { + + private static final String CLIENT_ID = "test@clientId"; + AbstractRemotingActivity remotingActivity; + @Mock + MessagingProcessor messagingProcessorMock; + @Spy + ChannelHandlerContext ctx = new SimpleChannelHandlerContext(new SimpleChannel(null, "0.0.0.0:0", "1.1.1.1:1")) { + @Override + public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) { + return null; + } + }; + + @Before + public void setup() { + remotingActivity = new AbstractRemotingActivity(null, messagingProcessorMock) { + @Override + protected RemotingCommand processRequest0(ChannelHandlerContext ctx, RemotingCommand request, + ProxyContext context) throws Exception { + return null; + } + }; + Channel channel = ctx.channel(); + RemotingHelper.setPropertyToAttr(channel, AttributeKeys.CLIENT_ID_KEY, CLIENT_ID); + RemotingHelper.setPropertyToAttr(channel, AttributeKeys.LANGUAGE_CODE_KEY, LanguageCode.JAVA); + RemotingHelper.setPropertyToAttr(channel, AttributeKeys.VERSION_KEY, MQVersion.CURRENT_VERSION); + } + + @Test + public void testCreateContext() { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, null); + ProxyContext context = remotingActivity.createContext(ctx, request); + + Assert.assertEquals(context.getAction(), RemotingHelper.getRequestCodeDesc(RequestCode.PULL_MESSAGE)); + Assert.assertEquals(context.getProtocolType(), ChannelProtocolType.REMOTING.getName()); + Assert.assertEquals(context.getLanguage(), LanguageCode.JAVA.name()); + Assert.assertEquals(context.getClientID(), CLIENT_ID); + Assert.assertEquals(context.getClientVersion(), MQVersion.getVersionDesc(MQVersion.CURRENT_VERSION)); + + } + + @Test + public void testRequest() throws Exception { + String brokerName = "broker"; + RemotingCommand response = RemotingCommand.createResponseCommand(ResponseCode.SUCCESS, "remark"); + when(messagingProcessorMock.request(any(), eq(brokerName), any(), anyLong())).thenReturn(CompletableFuture.completedFuture( + response + )); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, null); + request.addExtField(AbstractRemotingActivity.BROKER_NAME_FIELD, brokerName); + RemotingCommand remotingCommand = remotingActivity.request(ctx, request, null, 10000); + assertThat(remotingCommand).isNull(); + verify(ctx, times(1)).writeAndFlush(response); + } + + @Test + public void testRequestOneway() throws Exception { + String brokerName = "broker"; + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, null); + request.markOnewayRPC(); + request.addExtField(AbstractRemotingActivity.BROKER_NAME_FIELD, brokerName); + RemotingCommand remotingCommand = remotingActivity.request(ctx, request, null, 10000); + assertThat(remotingCommand).isNull(); + verify(messagingProcessorMock, times(1)).requestOneway(any(), eq(brokerName), any(), anyLong()); + } + + @Test + public void testRequestInvalid() throws Exception { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, null); + request.addExtField("test", "test"); + RemotingCommand remotingCommand = remotingActivity.request(ctx, request, null, 10000); + assertThat(remotingCommand.getCode()).isEqualTo(ResponseCode.VERSION_NOT_SUPPORTED); + verify(ctx, never()).writeAndFlush(any()); + } + + @Test + public void testRequestProxyException() throws Exception { + ArgumentCaptor captor = ArgumentCaptor.forClass(RemotingCommand.class); + String brokerName = "broker"; + String remark = "exception"; + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new ProxyException(ProxyExceptionCode.FORBIDDEN, remark)); + when(messagingProcessorMock.request(any(), eq(brokerName), any(), anyLong())).thenReturn(future); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, null); + request.addExtField(AbstractRemotingActivity.BROKER_NAME_FIELD, brokerName); + RemotingCommand remotingCommand = remotingActivity.request(ctx, request, null, 10000); + assertThat(remotingCommand).isNull(); + verify(ctx, times(1)).writeAndFlush(captor.capture()); + assertThat(captor.getValue().getCode()).isEqualTo(ResponseCode.NO_PERMISSION); + } + + @Test + public void testRequestClientException() throws Exception { + ArgumentCaptor captor = ArgumentCaptor.forClass(RemotingCommand.class); + String brokerName = "broker"; + String remark = "exception"; + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new MQClientException(remark, null)); + when(messagingProcessorMock.request(any(), eq(brokerName), any(), anyLong())).thenReturn(future); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, null); + request.addExtField(AbstractRemotingActivity.BROKER_NAME_FIELD, brokerName); + RemotingCommand remotingCommand = remotingActivity.request(ctx, request, null, 10000); + assertThat(remotingCommand).isNull(); + verify(ctx, times(1)).writeAndFlush(captor.capture()); + assertThat(captor.getValue().getCode()).isEqualTo(-1); + } + + @Test + public void testRequestBrokerException() throws Exception { + ArgumentCaptor captor = ArgumentCaptor.forClass(RemotingCommand.class); + String brokerName = "broker"; + String remark = "exception"; + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new MQBrokerException(ResponseCode.FLUSH_DISK_TIMEOUT, remark)); + when(messagingProcessorMock.request(any(), eq(brokerName), any(), anyLong())).thenReturn(future); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, null); + request.addExtField(AbstractRemotingActivity.BROKER_NAME_FIELD, brokerName); + RemotingCommand remotingCommand = remotingActivity.request(ctx, request, null, 10000); + assertThat(remotingCommand).isNull(); + verify(ctx, times(1)).writeAndFlush(captor.capture()); + assertThat(captor.getValue().getCode()).isEqualTo(ResponseCode.FLUSH_DISK_TIMEOUT); + } + + @Test + public void testRequestAclException() throws Exception { + ArgumentCaptor captor = ArgumentCaptor.forClass(RemotingCommand.class); + String brokerName = "broker"; + String remark = "exception"; + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new AclException(remark, ResponseCode.MESSAGE_ILLEGAL)); + when(messagingProcessorMock.request(any(), eq(brokerName), any(), anyLong())).thenReturn(future); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, null); + request.addExtField(AbstractRemotingActivity.BROKER_NAME_FIELD, brokerName); + RemotingCommand remotingCommand = remotingActivity.request(ctx, request, null, 10000); + assertThat(remotingCommand).isNull(); + verify(ctx, times(1)).writeAndFlush(captor.capture()); + assertThat(captor.getValue().getCode()).isEqualTo(ResponseCode.NO_PERMISSION); + } + + @Test + public void testRequestDefaultException() throws Exception { + ArgumentCaptor captor = ArgumentCaptor.forClass(RemotingCommand.class); + String brokerName = "broker"; + String remark = "exception"; + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new Exception(remark)); + when(messagingProcessorMock.request(any(), eq(brokerName), any(), anyLong())).thenReturn(future); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, null); + request.addExtField(AbstractRemotingActivity.BROKER_NAME_FIELD, brokerName); + RemotingCommand remotingCommand = remotingActivity.request(ctx, request, null, 10000); + assertThat(remotingCommand).isNull(); + verify(ctx, times(1)).writeAndFlush(captor.capture()); + assertThat(captor.getValue().getCode()).isEqualTo(ResponseCode.SYSTEM_ERROR); + } +} \ No newline at end of file diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/activity/PullMessageActivityTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/activity/PullMessageActivityTest.java new file mode 100644 index 00000000000..1dfc7ce3431 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/activity/PullMessageActivityTest.java @@ -0,0 +1,165 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.activity; + +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.broker.client.ConsumerGroupInfo; +import org.apache.rocketmq.common.sysflag.PullSysFlag; +import org.apache.rocketmq.proxy.config.InitConfigTest; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.proxy.service.channel.SimpleChannel; +import org.apache.rocketmq.proxy.service.channel.SimpleChannelHandlerContext; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class PullMessageActivityTest extends InitConfigTest { + PullMessageActivity pullMessageActivity; + + @Mock + MessagingProcessor messagingProcessorMock; + @Mock + ConsumerGroupInfo consumerGroupInfoMock; + + String topic = "topic"; + String group = "group"; + String brokerName = "brokerName"; + String subString = "sub"; + String type = "type"; + @Spy + ChannelHandlerContext ctx = new SimpleChannelHandlerContext(new SimpleChannel(null, "1", "2")) { + @Override + public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) { + return null; + } + }; + + @Before + public void setup() throws Exception { + pullMessageActivity = new PullMessageActivity(null, messagingProcessorMock); + } + + @Test + public void testPullMessageWithoutSub() throws Exception { + when(messagingProcessorMock.getConsumerGroupInfo(any(), eq(group))) + .thenReturn(consumerGroupInfoMock); + SubscriptionData subscriptionData = new SubscriptionData(); + subscriptionData.setSubString(subString); + subscriptionData.setExpressionType(type); + when(consumerGroupInfoMock.findSubscriptionData(eq(topic))) + .thenReturn(subscriptionData); + + PullMessageRequestHeader header = new PullMessageRequestHeader(); + header.setTopic(topic); + header.setConsumerGroup(group); + header.setQueueId(0); + header.setQueueOffset(0L); + header.setMaxMsgNums(16); + header.setSysFlag(PullSysFlag.buildSysFlag(true, false, false, false)); + header.setCommitOffset(0L); + header.setSuspendTimeoutMillis(1000L); + header.setSubVersion(0L); + header.setBrokerName(brokerName); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, header); + request.makeCustomHeaderToNet(); + RemotingCommand expectResponse = RemotingCommand.createResponseCommand(ResponseCode.NO_MESSAGE, "success"); + PullMessageRequestHeader newHeader = new PullMessageRequestHeader(); + newHeader.setTopic(topic); + newHeader.setConsumerGroup(group); + newHeader.setQueueId(0); + newHeader.setQueueOffset(0L); + newHeader.setMaxMsgNums(16); + newHeader.setSysFlag(PullSysFlag.buildSysFlag(true, false, true, false)); + newHeader.setCommitOffset(0L); + newHeader.setSuspendTimeoutMillis(1000L); + newHeader.setSubVersion(0L); + newHeader.setBrokerName(brokerName); + newHeader.setSubscription(subString); + newHeader.setExpressionType(type); + RemotingCommand matchRequest = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, newHeader); + matchRequest.setOpaque(request.getOpaque()); + matchRequest.makeCustomHeaderToNet(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(RemotingCommand.class); + when(messagingProcessorMock.request(any(), eq(brokerName), captor.capture(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(expectResponse)); + RemotingCommand response = pullMessageActivity.processRequest0(ctx, request, null); + assertThat(captor.getValue().getExtFields()).isEqualTo(matchRequest.getExtFields()); + assertThat(response).isNull(); + verify(ctx, times(1)).writeAndFlush(eq(expectResponse)); + } + + @Test + public void testPullMessageWithSub() throws Exception { + when(messagingProcessorMock.getConsumerGroupInfo(any(), eq(group))) + .thenReturn(consumerGroupInfoMock); + SubscriptionData subscriptionData = new SubscriptionData(); + subscriptionData.setSubString(subString); + subscriptionData.setExpressionType(type); + when(consumerGroupInfoMock.findSubscriptionData(eq(topic))) + .thenReturn(subscriptionData); + + PullMessageRequestHeader header = new PullMessageRequestHeader(); + header.setTopic(topic); + header.setConsumerGroup(group); + header.setQueueId(0); + header.setQueueOffset(0L); + header.setMaxMsgNums(16); + header.setSysFlag(PullSysFlag.buildSysFlag(true, true, false, false)); + header.setCommitOffset(0L); + header.setSuspendTimeoutMillis(1000L); + header.setSubVersion(0L); + header.setBrokerName(brokerName); + header.setSubscription(subString); + header.setExpressionType(type); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, header); + request.makeCustomHeaderToNet(); + RemotingCommand expectResponse = RemotingCommand.createResponseCommand(ResponseCode.NO_MESSAGE, "success"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(RemotingCommand.class); + when(messagingProcessorMock.request(any(), eq(brokerName), captor.capture(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(expectResponse)); + RemotingCommand response = pullMessageActivity.processRequest0(ctx, request, null); + assertThat(captor.getValue().getExtFields()).isEqualTo(request.getExtFields()); + assertThat(response).isNull(); + verify(ctx, times(1)).writeAndFlush(eq(expectResponse)); + } +} \ No newline at end of file diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/activity/SendMessageActivityTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/activity/SendMessageActivityTest.java new file mode 100644 index 00000000000..4b7589c3410 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/activity/SendMessageActivityTest.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.activity; + +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.common.attribute.TopicMessageType; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.proxy.config.InitConfigTest; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.proxy.service.channel.SimpleChannel; +import org.apache.rocketmq.proxy.service.channel.SimpleChannelHandlerContext; +import org.apache.rocketmq.proxy.service.metadata.MetadataService; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class SendMessageActivityTest extends InitConfigTest { + SendMessageActivity sendMessageActivity; + + @Mock + MessagingProcessor messagingProcessorMock; + @Mock + MetadataService metadataServiceMock; + + String topic = "topic"; + String producerGroup = "group"; + String brokerName = "brokerName"; + @Spy + ChannelHandlerContext ctx = new SimpleChannelHandlerContext(new SimpleChannel(null, "1", "2")) { + @Override + public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) { + return null; + } + }; + + @Before + public void setup() { + sendMessageActivity = new SendMessageActivity(null, messagingProcessorMock); + when(messagingProcessorMock.getMetadataService()).thenReturn(metadataServiceMock); + } + + @Test + public void testSendMessage() throws Exception { + when(metadataServiceMock.getTopicMessageType(any(), eq(topic))).thenReturn(TopicMessageType.NORMAL); + Message message = new Message(topic, "123".getBytes()); + message.putUserProperty("a", "b"); + SendMessageRequestHeader sendMessageRequestHeader = new SendMessageRequestHeader(); + sendMessageRequestHeader.setTopic(topic); + sendMessageRequestHeader.setProducerGroup(producerGroup); + sendMessageRequestHeader.setDefaultTopic(""); + sendMessageRequestHeader.setDefaultTopicQueueNums(0); + sendMessageRequestHeader.setQueueId(0); + sendMessageRequestHeader.setSysFlag(0); + sendMessageRequestHeader.setBrokerName(brokerName); + sendMessageRequestHeader.setProperties(MessageDecoder.messageProperties2String(message.getProperties())); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, sendMessageRequestHeader); + remotingCommand.setBody(message.getBody()); + remotingCommand.makeCustomHeaderToNet(); + + RemotingCommand expectResponse = RemotingCommand.createResponseCommand(ResponseCode.SUCCESS, "success"); + when(messagingProcessorMock.request(any(), eq(brokerName), eq(remotingCommand), anyLong())) + .thenReturn(CompletableFuture.completedFuture(expectResponse)); + RemotingCommand response = sendMessageActivity.processRequest0(ctx, remotingCommand, null); + assertThat(response).isNull(); + verify(ctx, times(1)).writeAndFlush(eq(expectResponse)); + } +} \ No newline at end of file diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/channel/RemotingChannelManagerTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/channel/RemotingChannelManagerTest.java new file mode 100644 index 00000000000..11224059375 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/channel/RemotingChannelManagerTest.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.channel; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelId; +import java.util.HashSet; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.remoting.RemotingProxyOutClient; +import org.apache.rocketmq.proxy.service.channel.SimpleChannel; +import org.apache.rocketmq.proxy.service.relay.ProxyRelayService; +import org.jetbrains.annotations.NotNull; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +@RunWith(MockitoJUnitRunner.class) +public class RemotingChannelManagerTest { + @Mock + private RemotingProxyOutClient remotingProxyOutClient; + @Mock + private ProxyRelayService proxyRelayService; + + private final String remoteAddress = "10.152.39.53:9768"; + private final String localAddress = "11.193.0.1:1210"; + private RemotingChannelManager remotingChannelManager; + private final ProxyContext ctx = ProxyContext.createForInner(this.getClass()); + + @Before + public void before() { + this.remotingChannelManager = new RemotingChannelManager(this.remotingProxyOutClient, this.proxyRelayService); + } + + @Test + public void testCreateChannel() { + String group = "group"; + String clientId = RandomStringUtils.randomAlphabetic(10); + + Channel producerChannel = createMockChannel(); + RemotingChannel producerRemotingChannel = this.remotingChannelManager.createProducerChannel(ctx, producerChannel, group, clientId); + assertNotNull(producerRemotingChannel); + assertSame(producerRemotingChannel, this.remotingChannelManager.createProducerChannel(ctx, producerChannel, group, clientId)); + + Channel consumerChannel = createMockChannel(); + RemotingChannel consumerRemotingChannel = this.remotingChannelManager.createConsumerChannel(ctx, consumerChannel, group, clientId, new HashSet<>()); + assertSame(consumerRemotingChannel, this.remotingChannelManager.createConsumerChannel(ctx, consumerChannel, group, clientId, new HashSet<>())); + assertNotNull(consumerRemotingChannel); + + assertNotSame(producerRemotingChannel, consumerRemotingChannel); + } + + @Test + public void testRemoveProducerChannel() { + String group = "group"; + String clientId = RandomStringUtils.randomAlphabetic(10); + + { + Channel producerChannel = createMockChannel(); + RemotingChannel producerRemotingChannel = this.remotingChannelManager.createProducerChannel(ctx, producerChannel, group, clientId); + assertSame(producerRemotingChannel, this.remotingChannelManager.removeProducerChannel(ctx, group, producerRemotingChannel)); + assertTrue(this.remotingChannelManager.groupChannelMap.isEmpty()); + } + { + Channel producerChannel = createMockChannel(); + RemotingChannel producerRemotingChannel = this.remotingChannelManager.createProducerChannel(ctx, producerChannel, group, clientId); + assertSame(producerRemotingChannel, this.remotingChannelManager.removeProducerChannel(ctx, group, producerChannel)); + assertTrue(this.remotingChannelManager.groupChannelMap.isEmpty()); + } + } + + @Test + public void testRemoveConsumerChannel() { + String group = "group"; + String clientId = RandomStringUtils.randomAlphabetic(10); + + { + Channel consumerChannel = createMockChannel(); + RemotingChannel consumerRemotingChannel = this.remotingChannelManager.createConsumerChannel(ctx, consumerChannel, group, clientId, new HashSet<>()); + assertSame(consumerRemotingChannel, this.remotingChannelManager.removeConsumerChannel(ctx, group, consumerRemotingChannel)); + assertTrue(this.remotingChannelManager.groupChannelMap.isEmpty()); + } + { + Channel consumerChannel = createMockChannel(); + RemotingChannel consumerRemotingChannel = this.remotingChannelManager.createConsumerChannel(ctx, consumerChannel, group, clientId, new HashSet<>()); + assertSame(consumerRemotingChannel, this.remotingChannelManager.removeConsumerChannel(ctx, group, consumerChannel)); + assertTrue(this.remotingChannelManager.groupChannelMap.isEmpty()); + } + } + + @Test + public void testRemoveChannel() { + String consumerGroup = "consumerGroup"; + String producerGroup = "producerGroup"; + String clientId = RandomStringUtils.randomAlphabetic(10); + + Channel consumerChannel = createMockChannel(); + RemotingChannel consumerRemotingChannel = this.remotingChannelManager.createConsumerChannel(ctx, consumerChannel, consumerGroup, clientId, new HashSet<>()); + Channel producerChannel = createMockChannel(); + RemotingChannel producerRemotingChannel = this.remotingChannelManager.createProducerChannel(ctx, producerChannel, producerGroup, clientId); + + assertSame(consumerRemotingChannel, this.remotingChannelManager.removeChannel(consumerChannel).stream().findFirst().get()); + assertSame(producerRemotingChannel, this.remotingChannelManager.removeChannel(producerChannel).stream().findFirst().get()); + + assertTrue(this.remotingChannelManager.groupChannelMap.isEmpty()); + } + + private Channel createMockChannel() { + return new MockChannel(RandomStringUtils.randomAlphabetic(10)); + } + + private class MockChannel extends SimpleChannel { + + public MockChannel(String channelId) { + super(null, new MockChannelId(channelId), RemotingChannelManagerTest.this.remoteAddress, RemotingChannelManagerTest.this.localAddress); + } + } + + private static class MockChannelId implements ChannelId { + + private final String channelId; + + public MockChannelId(String channelId) { + this.channelId = channelId; + } + + @Override + public String asShortText() { + return channelId; + } + + @Override + public String asLongText() { + return channelId; + } + + @Override + public int compareTo(@NotNull ChannelId o) { + return this.channelId.compareTo(o.asLongText()); + } + } +} \ No newline at end of file diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/channel/RemotingChannelTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/channel/RemotingChannelTest.java new file mode 100644 index 00000000000..d947fa5d533 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/channel/RemotingChannelTest.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.channel; + +import io.netty.channel.Channel; +import java.util.HashSet; +import java.util.Set; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.proxy.config.InitConfigTest; +import org.apache.rocketmq.proxy.grpc.v2.channel.GrpcClientChannel; +import org.apache.rocketmq.proxy.processor.channel.ChannelProtocolType; +import org.apache.rocketmq.proxy.processor.channel.RemoteChannel; +import org.apache.rocketmq.proxy.remoting.RemotingProxyOutClient; +import org.apache.rocketmq.proxy.service.relay.ProxyRelayService; +import org.apache.rocketmq.remoting.protocol.filter.FilterAPI; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class RemotingChannelTest extends InitConfigTest { + @Mock + private RemotingProxyOutClient remotingProxyOutClient; + @Mock + private ProxyRelayService proxyRelayService; + @Mock + private Channel parent; + + private String clientId; + private Set subscriptionData; + private RemotingChannel remotingChannel; + + private final String remoteAddress = "10.152.39.53:9768"; + private final String localAddress = "11.193.0.1:1210"; + + @Before + public void before() throws Throwable { + super.before(); + this.clientId = RandomStringUtils.randomAlphabetic(10); + when(parent.remoteAddress()).thenReturn(NetworkUtil.string2SocketAddress(remoteAddress)); + when(parent.localAddress()).thenReturn(NetworkUtil.string2SocketAddress(localAddress)); + this.subscriptionData = new HashSet<>(); + this.subscriptionData.add(FilterAPI.buildSubscriptionData("topic", "subTag")); + this.remotingChannel = new RemotingChannel(remotingProxyOutClient, proxyRelayService, + parent, clientId, subscriptionData); + } + + @Test + public void testChannelExtendAttributeParse() { + RemoteChannel remoteChannel = this.remotingChannel.toRemoteChannel(); + assertEquals(ChannelProtocolType.REMOTING, remoteChannel.getType()); + assertEquals(subscriptionData, RemotingChannel.parseChannelExtendAttribute(remoteChannel)); + assertEquals(subscriptionData, RemotingChannel.parseChannelExtendAttribute(this.remotingChannel)); + assertNull(RemotingChannel.parseChannelExtendAttribute(mock(GrpcClientChannel.class))); + } +} \ No newline at end of file diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/protocol/http2proxy/HAProxyMessageForwarderTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/protocol/http2proxy/HAProxyMessageForwarderTest.java new file mode 100644 index 00000000000..f57116f0da6 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/protocol/http2proxy/HAProxyMessageForwarderTest.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.remoting.protocol.http2proxy; + +import io.netty.channel.Channel; +import io.netty.handler.codec.haproxy.HAProxyTLV; +import org.apache.commons.codec.DecoderException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class HAProxyMessageForwarderTest { + + private HAProxyMessageForwarder haProxyMessageForwarder; + + @Mock + private Channel outboundChannel; + + @Before + public void setUp() throws Exception { + haProxyMessageForwarder = new HAProxyMessageForwarder(outboundChannel); + } + + @Test + public void buildHAProxyTLV() throws DecoderException { + HAProxyTLV haProxyTLV = haProxyMessageForwarder.buildHAProxyTLV("proxy_protocol_tlv_0xe1", "xxxx"); + assert haProxyTLV != null; + assert haProxyTLV.typeByteValue() == (byte) 0xe1; + } +} \ No newline at end of file diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/protocol/http2proxy/Http2ProtocolProxyHandlerTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/protocol/http2proxy/Http2ProtocolProxyHandlerTest.java new file mode 100644 index 00000000000..bf03786d348 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/remoting/protocol/http2proxy/Http2ProtocolProxyHandlerTest.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.remoting.protocol.http2proxy; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.haproxy.HAProxyMessageEncoder; +import org.apache.rocketmq.remoting.netty.AttributeKeys; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class Http2ProtocolProxyHandlerTest { + + private Http2ProtocolProxyHandler http2ProtocolProxyHandler; + @Mock + private Channel inboundChannel; + @Mock + private ChannelPipeline inboundPipeline; + @Mock + private Channel outboundChannel; + @Mock + private ChannelPipeline outboundPipeline; + + @Before + public void setUp() throws Exception { + http2ProtocolProxyHandler = new Http2ProtocolProxyHandler(); + } + + @Test + public void configPipeline() { + when(inboundChannel.hasAttr(eq(AttributeKeys.PROXY_PROTOCOL_ADDR))).thenReturn(true); + when(inboundChannel.pipeline()).thenReturn(inboundPipeline); + when(inboundPipeline.addLast(any(HAProxyMessageForwarder.class))).thenReturn(inboundPipeline); + when(outboundChannel.pipeline()).thenReturn(outboundPipeline); + when(outboundPipeline.addFirst(any(HAProxyMessageEncoder.class))).thenReturn(outboundPipeline); + http2ProtocolProxyHandler.configPipeline(inboundChannel, outboundChannel); + } +} \ No newline at end of file diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/service/BaseServiceTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/service/BaseServiceTest.java new file mode 100644 index 00000000000..ca6fe909e28 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/service/BaseServiceTest.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service; + +import java.util.HashMap; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.proxy.config.InitConfigTest; +import org.apache.rocketmq.client.impl.mqclient.MQClientAPIExt; +import org.apache.rocketmq.client.impl.mqclient.MQClientAPIFactory; +import org.apache.rocketmq.proxy.service.route.MessageQueueView; +import org.apache.rocketmq.proxy.service.route.TopicRouteService; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.assertj.core.util.Lists; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@Ignore +@RunWith(MockitoJUnitRunner.Silent.class) +public class BaseServiceTest extends InitConfigTest { + + protected TopicRouteService topicRouteService; + protected MQClientAPIFactory mqClientAPIFactory; + protected MQClientAPIExt mqClientAPIExt; + + protected static final String ERR_TOPIC = "errTopic"; + protected static final String TOPIC = "topic"; + protected static final String GROUP = "group"; + protected static final String BROKER_NAME = "broker"; + protected static final String CLUSTER_NAME = "cluster"; + protected static final String BROKER_ADDR = "127.0.0.1:10911"; + + protected final TopicRouteData topicRouteData = new TopicRouteData(); + protected final QueueData queueData = new QueueData(); + protected final BrokerData brokerData = new BrokerData(); + + @Before + public void before() throws Throwable { + super.before(); + + topicRouteService = mock(TopicRouteService.class); + mqClientAPIFactory = mock(MQClientAPIFactory.class); + mqClientAPIExt = mock(MQClientAPIExt.class); + when(mqClientAPIFactory.getClient()).thenReturn(mqClientAPIExt); + + queueData.setBrokerName(BROKER_NAME); + topicRouteData.setQueueDatas(Lists.newArrayList(queueData)); + brokerData.setCluster(CLUSTER_NAME); + brokerData.setBrokerName(BROKER_NAME); + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(MixAll.MASTER_ID, BROKER_ADDR); + brokerData.setBrokerAddrs(brokerAddrs); + topicRouteData.setBrokerDatas(Lists.newArrayList(brokerData)); + + when(this.topicRouteService.getAllMessageQueueView(any(), eq(ERR_TOPIC))).thenThrow(new MQClientException(ResponseCode.TOPIC_NOT_EXIST, "")); + when(this.topicRouteService.getAllMessageQueueView(any(), eq(TOPIC))).thenReturn(new MessageQueueView(TOPIC, topicRouteData, null)); + when(this.topicRouteService.getAllMessageQueueView(any(), eq(CLUSTER_NAME))).thenReturn(new MessageQueueView(CLUSTER_NAME, topicRouteData, null)); + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/service/admin/DefaultAdminServiceTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/service/admin/DefaultAdminServiceTest.java new file mode 100644 index 00000000000..cdfc7f7fc23 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/service/admin/DefaultAdminServiceTest.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.admin; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.apache.rocketmq.client.impl.mqclient.MQClientAPIExt; +import org.apache.rocketmq.client.impl.mqclient.MQClientAPIFactory; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class DefaultAdminServiceTest { + @Mock + private MQClientAPIFactory mqClientAPIFactory; + @Mock + private MQClientAPIExt mqClientAPIExt; + + private DefaultAdminService defaultAdminService; + + @Before + public void before() { + when(mqClientAPIFactory.getClient()).thenReturn(mqClientAPIExt); + defaultAdminService = new DefaultAdminService(mqClientAPIFactory); + } + + @Test + public void testCreateTopic() throws Exception { + when(mqClientAPIExt.getTopicRouteInfoFromNameServer(eq("createTopic"), anyLong())) + .thenThrow(new MQClientException(ResponseCode.TOPIC_NOT_EXIST, "")) + .thenReturn(createTopicRouteData(1)); + when(mqClientAPIExt.getTopicRouteInfoFromNameServer(eq("sampleTopic"), anyLong())) + .thenReturn(createTopicRouteData(2)); + + ArgumentCaptor addrArgumentCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor topicConfigArgumentCaptor = ArgumentCaptor.forClass(TopicConfig.class); + doNothing().when(mqClientAPIExt).createTopic(addrArgumentCaptor.capture(), anyString(), topicConfigArgumentCaptor.capture(), anyLong()); + + assertTrue(defaultAdminService.createTopicOnTopicBrokerIfNotExist( + "createTopic", + "sampleTopic", + 7, + 8, + true, + 1 + )); + + assertEquals(2, addrArgumentCaptor.getAllValues().size()); + Set createAddr = new HashSet<>(addrArgumentCaptor.getAllValues()); + assertTrue(createAddr.contains("127.0.0.1:10911")); + assertTrue(createAddr.contains("127.0.0.2:10911")); + assertEquals("createTopic", topicConfigArgumentCaptor.getValue().getTopicName()); + assertEquals(7, topicConfigArgumentCaptor.getValue().getWriteQueueNums()); + assertEquals(8, topicConfigArgumentCaptor.getValue().getReadQueueNums()); + } + + private TopicRouteData createTopicRouteData(int brokerNum) { + TopicRouteData topicRouteData = new TopicRouteData(); + for (int i = 0; i < brokerNum; i++) { + BrokerData brokerData = new BrokerData(); + HashMap addrMap = new HashMap<>(); + addrMap.put(0L, "127.0.0." + (i + 1) + ":10911"); + brokerData.setBrokerAddrs(addrMap); + brokerData.setBrokerName("broker-" + i); + brokerData.setCluster("cluster"); + topicRouteData.getBrokerDatas().add(brokerData); + } + return topicRouteData; + } +} \ No newline at end of file diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/service/message/ClusterMessageServiceTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/service/message/ClusterMessageServiceTest.java new file mode 100644 index 00000000000..7e4d25f0c09 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/service/message/ClusterMessageServiceTest.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.proxy.service.message; + +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.consumer.ReceiptHandle; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.common.ProxyException; +import org.apache.rocketmq.proxy.common.ProxyExceptionCode; +import org.apache.rocketmq.client.impl.mqclient.MQClientAPIFactory; +import org.apache.rocketmq.proxy.service.route.TopicRouteService; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.AckMessageRequestHeader; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ClusterMessageServiceTest { + + private TopicRouteService topicRouteService; + private ClusterMessageService clusterMessageService; + + @Before + public void before() { + this.topicRouteService = mock(TopicRouteService.class); + MQClientAPIFactory mqClientAPIFactory = mock(MQClientAPIFactory.class); + this.clusterMessageService = new ClusterMessageService(this.topicRouteService, mqClientAPIFactory); + } + + @Test + public void testAckMessageByInvalidBrokerNameHandle() throws Exception { + when(topicRouteService.getBrokerAddr(any(), anyString())).thenThrow(new MQClientException(ResponseCode.TOPIC_NOT_EXIST, "")); + try { + this.clusterMessageService.ackMessage( + ProxyContext.create(), + ReceiptHandle.builder() + .startOffset(0L) + .retrieveTime(System.currentTimeMillis()) + .invisibleTime(3000) + .reviveQueueId(1) + .topicType(ReceiptHandle.NORMAL_TOPIC) + .brokerName("notExistBroker") + .queueId(0) + .offset(123) + .commitLogOffset(0L) + .build(), + MessageClientIDSetter.createUniqID(), + new AckMessageRequestHeader(), + 3000); + fail(); + } catch (Exception e) { + assertTrue(e instanceof ProxyException); + ProxyException proxyException = (ProxyException) e; + assertEquals(ProxyExceptionCode.INVALID_RECEIPT_HANDLE, proxyException.getCode()); + } + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/service/message/LocalMessageServiceTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/service/message/LocalMessageServiceTest.java new file mode 100644 index 00000000000..3e3d37086b5 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/service/message/LocalMessageServiceTest.java @@ -0,0 +1,451 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.message; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.processor.AckMessageProcessor; +import org.apache.rocketmq.broker.processor.ChangeInvisibleTimeProcessor; +import org.apache.rocketmq.broker.processor.EndTransactionProcessor; +import org.apache.rocketmq.broker.processor.PopMessageProcessor; +import org.apache.rocketmq.broker.processor.SendMessageProcessor; +import org.apache.rocketmq.client.consumer.AckResult; +import org.apache.rocketmq.client.consumer.AckStatus; +import org.apache.rocketmq.client.consumer.PopResult; +import org.apache.rocketmq.client.consumer.PopStatus; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.consumer.ReceiptHandle; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageBatch; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.proxy.common.ContextVariable; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.common.ProxyException; +import org.apache.rocketmq.proxy.common.ProxyExceptionCode; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.InitConfigTest; +import org.apache.rocketmq.proxy.service.channel.ChannelManager; +import org.apache.rocketmq.proxy.service.channel.SimpleChannelHandlerContext; +import org.apache.rocketmq.proxy.service.route.AddressableMessageQueue; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.AckMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ChangeInvisibleTimeRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ChangeInvisibleTimeResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.ConsumerSendMsgBackRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.EndTransactionRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ExtraInfoUtil; +import org.apache.rocketmq.remoting.protocol.header.PopMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PopMessageResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageResponseHeader; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowableOfType; + +@RunWith(MockitoJUnitRunner.class) +public class LocalMessageServiceTest extends InitConfigTest { + private LocalMessageService localMessageService; + @Mock + private SendMessageProcessor sendMessageProcessorMock; + @Mock + private EndTransactionProcessor endTransactionProcessorMock; + @Mock + private PopMessageProcessor popMessageProcessorMock; + @Mock + private ChangeInvisibleTimeProcessor changeInvisibleTimeProcessorMock; + @Mock + private AckMessageProcessor ackMessageProcessorMock; + @Mock + private BrokerController brokerControllerMock; + + private ProxyContext proxyContext; + + private ChannelManager channelManager; + + private String topic = "topic"; + + private String brokerName = "brokerName"; + + private int queueId = 0; + + private long queueOffset = 0L; + + private String transactionId = "transactionId"; + + private String offsetMessageId = "offsetMessageId"; + + @Before + public void setUp() throws Throwable { + super.before(); + ConfigurationManager.getProxyConfig().setNamesrvAddr("1.1.1.1"); + channelManager = new ChannelManager(); + Mockito.when(brokerControllerMock.getSendMessageProcessor()).thenReturn(sendMessageProcessorMock); + Mockito.when(brokerControllerMock.getPopMessageProcessor()).thenReturn(popMessageProcessorMock); + Mockito.when(brokerControllerMock.getChangeInvisibleTimeProcessor()).thenReturn(changeInvisibleTimeProcessorMock); + Mockito.when(brokerControllerMock.getAckMessageProcessor()).thenReturn(ackMessageProcessorMock); + Mockito.when(brokerControllerMock.getEndTransactionProcessor()).thenReturn(endTransactionProcessorMock); + Mockito.when(brokerControllerMock.getBrokerConfig()).thenReturn(new BrokerConfig()); + localMessageService = new LocalMessageService(brokerControllerMock, channelManager, null); + proxyContext = ProxyContext.create().withVal(ContextVariable.REMOTE_ADDRESS, "0.0.0.1") + .withVal(ContextVariable.LOCAL_ADDRESS, "0.0.0.2"); + } + + @Test + public void testSendMessageWriteAndFlush() throws Exception { + Message message = new Message(topic, "body".getBytes(StandardCharsets.UTF_8)); + MessageClientIDSetter.setUniqID(message); + List messagesList = Collections.singletonList(message); + SendMessageRequestHeader requestHeader = new SendMessageRequestHeader(); + requestHeader.setTopic(topic); + requestHeader.setQueueId(queueId); + Mockito.when(sendMessageProcessorMock.processRequest(Mockito.any(SimpleChannelHandlerContext.class), Mockito.argThat(argument -> { + boolean first = argument.getCode() == RequestCode.SEND_MESSAGE; + boolean second = Arrays.equals(argument.getBody(), message.getBody()); + return first & second; + }))).thenAnswer(invocation -> { + SimpleChannelHandlerContext simpleChannelHandlerContext = invocation.getArgument(0); + RemotingCommand request = invocation.getArgument(1); + RemotingCommand response = RemotingCommand.createResponseCommand(SendMessageResponseHeader.class); + response.setOpaque(request.getOpaque()); + response.setCode(ResponseCode.SUCCESS); + response.setBody(message.getBody()); + SendMessageResponseHeader sendMessageResponseHeader = (SendMessageResponseHeader) response.readCustomHeader(); + sendMessageResponseHeader.setQueueId(queueId); + sendMessageResponseHeader.setQueueOffset(queueOffset); + sendMessageResponseHeader.setMsgId(offsetMessageId); + sendMessageResponseHeader.setTransactionId(transactionId); + simpleChannelHandlerContext.writeAndFlush(response); + return null; + }); + + CompletableFuture> future = localMessageService.sendMessage(proxyContext, null, messagesList, requestHeader, 1000L); + SendResult sendResult = future.get().get(0); + assertThat(sendResult.getSendStatus()).isEqualTo(SendStatus.SEND_OK); + assertThat(sendResult.getMsgId()).isEqualTo(MessageClientIDSetter.getUniqID(message)); + assertThat(sendResult.getMessageQueue()) + .isEqualTo(new MessageQueue(topic, brokerControllerMock.getBrokerConfig().getBrokerName(), queueId)); + assertThat(sendResult.getQueueOffset()).isEqualTo(queueOffset); + assertThat(sendResult.getTransactionId()).isEqualTo(transactionId); + assertThat(sendResult.getOffsetMsgId()).isEqualTo(offsetMessageId); + } + + @Test + public void testSendBatchMessageWriteAndFlush() throws Exception { + Message message1 = new Message(topic, "body1".getBytes(StandardCharsets.UTF_8)); + Message message2 = new Message(topic, "body2".getBytes(StandardCharsets.UTF_8)); + MessageClientIDSetter.setUniqID(message1); + MessageClientIDSetter.setUniqID(message2); + List messagesList = Arrays.asList(message1, message2); + MessageBatch msgBatch = MessageBatch.generateFromList(messagesList); + MessageClientIDSetter.setUniqID(msgBatch); + byte[] body = msgBatch.encode(); + msgBatch.setBody(body); + SendMessageRequestHeader requestHeader = new SendMessageRequestHeader(); + requestHeader.setTopic(topic); + requestHeader.setQueueId(queueId); + Mockito.when(sendMessageProcessorMock.processRequest(Mockito.any(SimpleChannelHandlerContext.class), Mockito.argThat(argument -> { + boolean first = argument.getCode() == RequestCode.SEND_MESSAGE; + boolean second = Arrays.equals(argument.getBody(), body); + return first & second; + }))).thenAnswer(invocation -> { + SimpleChannelHandlerContext simpleChannelHandlerContext = invocation.getArgument(0); + RemotingCommand request = invocation.getArgument(1); + RemotingCommand response = RemotingCommand.createResponseCommand(SendMessageResponseHeader.class); + response.setOpaque(request.getOpaque()); + response.setCode(ResponseCode.SUCCESS); + response.setBody(body); + SendMessageResponseHeader sendMessageResponseHeader = (SendMessageResponseHeader) response.readCustomHeader(); + sendMessageResponseHeader.setQueueId(queueId); + sendMessageResponseHeader.setQueueOffset(queueOffset); + sendMessageResponseHeader.setMsgId(offsetMessageId); + sendMessageResponseHeader.setTransactionId(transactionId); + simpleChannelHandlerContext.writeAndFlush(response); + return null; + }); + + CompletableFuture> future = localMessageService.sendMessage(proxyContext, null, messagesList, requestHeader, 1000L); + SendResult sendResult = future.get().get(0); + assertThat(sendResult.getSendStatus()).isEqualTo(SendStatus.SEND_OK); + assertThat(sendResult.getMessageQueue()) + .isEqualTo(new MessageQueue(topic, brokerControllerMock.getBrokerConfig().getBrokerName(), queueId)); + assertThat(sendResult.getQueueOffset()).isEqualTo(queueOffset); + assertThat(sendResult.getTransactionId()).isEqualTo(transactionId); + assertThat(sendResult.getOffsetMsgId()).isEqualTo(offsetMessageId); + } + + @Test + public void testSendMessageError() throws Exception { + RemotingCommand response = RemotingCommand.createResponseCommand(SendMessageResponseHeader.class); + response.setCode(ResponseCode.SYSTEM_ERROR); + Message message = new Message("topic", "body".getBytes(StandardCharsets.UTF_8)); + MessageClientIDSetter.setUniqID(message); + List messagesList = Collections.singletonList(message); + SendMessageRequestHeader sendMessageRequestHeader = new SendMessageRequestHeader(); + sendMessageRequestHeader.setTopic(topic); + sendMessageRequestHeader.setQueueId(queueId); + + Mockito.when(sendMessageProcessorMock.processRequest(Mockito.any(SimpleChannelHandlerContext.class), Mockito.any(RemotingCommand.class))) + .thenReturn(response); + + CompletableFuture> future = localMessageService.sendMessage(proxyContext, null, messagesList, sendMessageRequestHeader, 1000L); + ExecutionException exception = catchThrowableOfType(future::get, ExecutionException.class); + assertThat(exception.getCause()).isInstanceOf(ProxyException.class); + assertThat(((ProxyException) exception.getCause()).getCode()).isEqualTo(ProxyExceptionCode.INTERNAL_SERVER_ERROR); + } + + @Test + public void testSendMessageWithException() throws Exception { + Mockito.when(sendMessageProcessorMock.processRequest(Mockito.any(SimpleChannelHandlerContext.class), Mockito.any(RemotingCommand.class))) + .thenThrow(new RemotingCommandException("test")); + Message message = new Message("topic", "body".getBytes(StandardCharsets.UTF_8)); + MessageClientIDSetter.setUniqID(message); + List messagesList = Collections.singletonList(message); + SendMessageRequestHeader sendMessageRequestHeader = new SendMessageRequestHeader(); + CompletableFuture> future = localMessageService.sendMessage(proxyContext, null, messagesList, sendMessageRequestHeader, 1000L); + ExecutionException exception = catchThrowableOfType(future::get, ExecutionException.class); + assertThat(exception.getCause()).isInstanceOf(RemotingCommandException.class); + } + + @Test + public void testSendMessageBack() throws Exception { + RemotingCommand remotingCommand = RemotingCommand.createResponseCommand(ResponseCode.SUCCESS, ""); + Mockito.when(sendMessageProcessorMock.processRequest(Mockito.any(SimpleChannelHandlerContext.class), Mockito.argThat(argument -> { + boolean first = argument.getCode() == RequestCode.CONSUMER_SEND_MSG_BACK; + boolean second = argument.readCustomHeader() instanceof ConsumerSendMsgBackRequestHeader; + return first && second; + }))).thenReturn(remotingCommand); + ConsumerSendMsgBackRequestHeader requestHeader = new ConsumerSendMsgBackRequestHeader(); + CompletableFuture future = localMessageService.sendMessageBack(proxyContext, null, null, requestHeader, 1000L); + RemotingCommand response = future.get(); + assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS); + } + + @Test + public void testEndTransaction() throws Exception { + EndTransactionRequestHeader requestHeader = new EndTransactionRequestHeader(); + localMessageService.endTransactionOneway(proxyContext, null, requestHeader, 1000L); + Mockito.verify(endTransactionProcessorMock, Mockito.times(1)).processRequest(Mockito.any(SimpleChannelHandlerContext.class), Mockito.argThat(argument -> { + boolean first = argument.getCode() == RequestCode.END_TRANSACTION; + boolean second = argument.readCustomHeader() instanceof EndTransactionRequestHeader; + return first && second; + })); + } + + @Test + public void testPopMessageWriteAndFlush() throws Exception { + int reviveQueueId = 1; + long popTime = System.currentTimeMillis(); + long invisibleTime = 3000L; + long startOffset = 100L; + long restNum = 0L; + StringBuilder startOffsetStringBuilder = new StringBuilder(); + StringBuilder messageOffsetStringBuilder = new StringBuilder(); + List messageExtList = new ArrayList<>(); + List messageOffsetList = new ArrayList<>(); + MessageExt message1 = buildMessageExt(topic, 0, startOffset); + messageExtList.add(message1); + messageOffsetList.add(startOffset); + byte[] body1 = MessageDecoder.encode(message1, false); + MessageExt message2 = buildMessageExt(topic, 0, startOffset + 1); + messageExtList.add(message2); + messageOffsetList.add(startOffset + 1); + ExtraInfoUtil.buildStartOffsetInfo(startOffsetStringBuilder, topic, queueId, startOffset); + ExtraInfoUtil.buildMsgOffsetInfo(messageOffsetStringBuilder, topic, queueId, messageOffsetList); + byte[] body2 = MessageDecoder.encode(message2, false); + ByteBuffer byteBuffer1 = ByteBuffer.wrap(body1); + ByteBuffer byteBuffer2 = ByteBuffer.wrap(body2); + ByteBuffer b3 = ByteBuffer.allocate(byteBuffer1.limit() + byteBuffer2.limit()); + b3.put(byteBuffer1); + b3.put(byteBuffer2); + PopMessageRequestHeader requestHeader = new PopMessageRequestHeader(); + requestHeader.setInvisibleTime(invisibleTime); + Mockito.when(popMessageProcessorMock.processRequest(Mockito.any(SimpleChannelHandlerContext.class), Mockito.argThat(argument -> { + boolean first = argument.getCode() == RequestCode.POP_MESSAGE; + boolean second = argument.readCustomHeader() instanceof PopMessageRequestHeader; + return first && second; + }))).thenAnswer(invocation -> { + SimpleChannelHandlerContext simpleChannelHandlerContext = invocation.getArgument(0); + RemotingCommand request = invocation.getArgument(1); + RemotingCommand response = RemotingCommand.createResponseCommand(PopMessageResponseHeader.class); + response.setOpaque(request.getOpaque()); + response.setCode(ResponseCode.SUCCESS); + response.setBody(b3.array()); + PopMessageResponseHeader responseHeader = (PopMessageResponseHeader) response.readCustomHeader(); + responseHeader.setStartOffsetInfo(startOffsetStringBuilder.toString()); + responseHeader.setMsgOffsetInfo(messageOffsetStringBuilder.toString()); + responseHeader.setInvisibleTime(requestHeader.getInvisibleTime()); + responseHeader.setPopTime(popTime); + responseHeader.setRestNum(restNum); + responseHeader.setReviveQid(reviveQueueId); + simpleChannelHandlerContext.writeAndFlush(response); + return null; + }); + MessageQueue messageQueue = new MessageQueue(topic, brokerName, queueId); + CompletableFuture future = localMessageService.popMessage(proxyContext, new AddressableMessageQueue(messageQueue, ""), requestHeader, 1000L); + PopResult popResult = future.get(); + assertThat(popResult.getPopTime()).isEqualTo(popTime); + assertThat(popResult.getInvisibleTime()).isEqualTo(invisibleTime); + assertThat(popResult.getPopStatus()).isEqualTo(PopStatus.FOUND); + assertThat(popResult.getRestNum()).isEqualTo(restNum); + assertThat(popResult.getMsgFoundList().size()).isEqualTo(messageExtList.size()); + for (int i = 0; i < popResult.getMsgFoundList().size(); i++) { + assertMessageExt(popResult.getMsgFoundList().get(i), messageExtList.get(i)); + } + } + + @Test + public void testPopMessagePollingTimeout() throws Exception { + RemotingCommand remotingCommand = RemotingCommand.createResponseCommand(ResponseCode.POLLING_TIMEOUT, ""); + Mockito.when(popMessageProcessorMock.processRequest(Mockito.any(SimpleChannelHandlerContext.class), Mockito.argThat(argument -> { + boolean first = argument.getCode() == RequestCode.POP_MESSAGE; + boolean second = argument.readCustomHeader() instanceof PopMessageRequestHeader; + return first && second; + }))).thenReturn(remotingCommand); + PopMessageRequestHeader requestHeader = new PopMessageRequestHeader(); + CompletableFuture future = localMessageService.popMessage(proxyContext, null, requestHeader, 1000L); + PopResult popResult = future.get(); + assertThat(popResult.getPopStatus()).isEqualTo(PopStatus.POLLING_NOT_FOUND); + } + + @Test + public void testChangeInvisibleTime() throws Exception { + String messageId = "messageId"; + long popTime = System.currentTimeMillis(); + long invisibleTime = 3000L; + int reviveQueueId = 1; + ReceiptHandle handle = ReceiptHandle.builder() + .startOffset(0L) + .retrieveTime(popTime) + .invisibleTime(invisibleTime) + .reviveQueueId(reviveQueueId) + .topicType(ReceiptHandle.NORMAL_TOPIC) + .brokerName(brokerName) + .queueId(queueId) + .offset(queueOffset) + .build(); + RemotingCommand remotingCommand = RemotingCommand.createResponseCommand(ChangeInvisibleTimeResponseHeader.class); + remotingCommand.setCode(ResponseCode.SUCCESS); + remotingCommand.setRemark(""); + long newPopTime = System.currentTimeMillis(); + long newInvisibleTime = 5000L; + int newReviveQueueId = 2; + ChangeInvisibleTimeResponseHeader responseHeader = (ChangeInvisibleTimeResponseHeader) remotingCommand.readCustomHeader(); + responseHeader.setReviveQid(newReviveQueueId); + responseHeader.setInvisibleTime(newInvisibleTime); + responseHeader.setPopTime(newPopTime); + Mockito.when(changeInvisibleTimeProcessorMock.processRequest(Mockito.any(SimpleChannelHandlerContext.class), Mockito.argThat(argument -> { + boolean first = argument.getCode() == RequestCode.CHANGE_MESSAGE_INVISIBLETIME; + boolean second = argument.readCustomHeader() instanceof ChangeInvisibleTimeRequestHeader; + return first && second; + }))).thenReturn(remotingCommand); + ChangeInvisibleTimeRequestHeader requestHeader = new ChangeInvisibleTimeRequestHeader(); + CompletableFuture future = localMessageService.changeInvisibleTime(proxyContext, handle, messageId, + requestHeader, 1000L); + AckResult ackResult = future.get(); + assertThat(ackResult.getStatus()).isEqualTo(AckStatus.OK); + assertThat(ackResult.getPopTime()).isEqualTo(newPopTime); + assertThat(ackResult.getExtraInfo()).isEqualTo(ReceiptHandle.builder() + .startOffset(0L) + .retrieveTime(newPopTime) + .invisibleTime(newInvisibleTime) + .reviveQueueId(newReviveQueueId) + .topicType(ReceiptHandle.NORMAL_TOPIC) + .brokerName(brokerName) + .queueId(queueId) + .offset(queueOffset) + .build() + .encode()); + } + + @Test + public void testAckMessage() throws Exception { + String messageId = "messageId"; + long popTime = System.currentTimeMillis(); + long invisibleTime = 3000L; + int reviveQueueId = 1; + ReceiptHandle handle = ReceiptHandle.builder() + .startOffset(0L) + .retrieveTime(popTime) + .invisibleTime(invisibleTime) + .reviveQueueId(reviveQueueId) + .topicType(ReceiptHandle.NORMAL_TOPIC) + .brokerName(brokerName) + .queueId(queueId) + .offset(queueOffset) + .build(); + RemotingCommand remotingCommand = RemotingCommand.createResponseCommand(ResponseCode.SUCCESS, null); + Mockito.when(ackMessageProcessorMock.processRequest(Mockito.any(SimpleChannelHandlerContext.class), Mockito.argThat(argument -> { + boolean first = argument.getCode() == RequestCode.ACK_MESSAGE; + boolean second = argument.readCustomHeader() instanceof AckMessageRequestHeader; + return first && second; + }))).thenReturn(remotingCommand); + AckMessageRequestHeader requestHeader = new AckMessageRequestHeader(); + CompletableFuture future = localMessageService.ackMessage(proxyContext, handle, messageId, + requestHeader, 1000L); + AckResult ackResult = future.get(); + assertThat(ackResult.getStatus()).isEqualTo(AckStatus.OK); + } + + private MessageExt buildMessageExt(String topic, int queueId, long queueOffset) { + MessageExt message1 = new MessageExt(); + message1.setTopic(topic); + message1.setBody("body".getBytes(StandardCharsets.UTF_8)); + message1.setFlag(0); + message1.setQueueId(queueId); + message1.setQueueOffset(queueOffset); + message1.setCommitLogOffset(1000L); + message1.setSysFlag(0); + message1.setBornTimestamp(0L); + InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 80); + message1.setBornHost(inetSocketAddress); + message1.setStoreHost(inetSocketAddress); + message1.setReconsumeTimes(0); + message1.setPreparedTransactionOffset(0L); + message1.putUserProperty("K", "V"); + return message1; + } + + private void assertMessageExt(MessageExt messageExt1, MessageExt messageExt2) { + assertThat(messageExt1.getBody()).isEqualTo(messageExt2.getBody()); + assertThat(messageExt1.getTopic()).isEqualTo(messageExt2.getTopic()); + assertThat(messageExt1.getQueueId()).isEqualTo(messageExt2.getQueueId()); + assertThat(messageExt1.getQueueOffset()).isEqualTo(messageExt2.getQueueOffset()); + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/service/metadata/ClusterMetadataServiceTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/service/metadata/ClusterMetadataServiceTest.java new file mode 100644 index 00000000000..98bf1104f8b --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/service/metadata/ClusterMetadataServiceTest.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.metadata; + +import java.util.HashMap; +import org.apache.rocketmq.common.attribute.TopicMessageType; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.service.BaseServiceTest; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicConfigAndQueueMapping; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +public class ClusterMetadataServiceTest extends BaseServiceTest { + + private ClusterMetadataService clusterMetadataService; + + @Before + public void before() throws Throwable { + super.before(); + ConfigurationManager.getProxyConfig().setRocketMQClusterName(CLUSTER_NAME); + + TopicConfigAndQueueMapping topicConfigAndQueueMapping = new TopicConfigAndQueueMapping(); + topicConfigAndQueueMapping.setAttributes(new HashMap<>()); + topicConfigAndQueueMapping.setTopicMessageType(TopicMessageType.NORMAL); + when(this.mqClientAPIExt.getTopicConfig(anyString(), eq(TOPIC), anyLong())).thenReturn(topicConfigAndQueueMapping); + + when(this.mqClientAPIExt.getSubscriptionGroupConfig(anyString(), eq(GROUP), anyLong())).thenReturn(new SubscriptionGroupConfig()); + + this.clusterMetadataService = new ClusterMetadataService(this.topicRouteService, this.mqClientAPIFactory); + } + + @Test + public void testGetTopicMessageType() { + ProxyContext ctx = ProxyContext.create(); + assertEquals(TopicMessageType.UNSPECIFIED, this.clusterMetadataService.getTopicMessageType(ctx, ERR_TOPIC)); + assertEquals(1, this.clusterMetadataService.topicConfigCache.asMap().size()); + assertEquals(TopicMessageType.UNSPECIFIED, this.clusterMetadataService.getTopicMessageType(ctx, ERR_TOPIC)); + + assertEquals(TopicMessageType.NORMAL, this.clusterMetadataService.getTopicMessageType(ctx, TOPIC)); + assertEquals(2, this.clusterMetadataService.topicConfigCache.asMap().size()); + } + + @Test + public void testGetSubscriptionGroupConfig() { + ProxyContext ctx = ProxyContext.create(); + assertNotNull(this.clusterMetadataService.getSubscriptionGroupConfig(ctx, GROUP)); + assertEquals(1, this.clusterMetadataService.subscriptionGroupConfigCache.asMap().size()); + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/service/mqclient/MQClientAPIExtTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/service/mqclient/MQClientAPIExtTest.java new file mode 100644 index 00000000000..e2d05b0f5a8 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/service/mqclient/MQClientAPIExtTest.java @@ -0,0 +1,350 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.mqclient; + +import java.lang.reflect.Field; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import org.apache.rocketmq.client.ClientConfig; +import org.apache.rocketmq.client.consumer.AckCallback; +import org.apache.rocketmq.client.consumer.AckResult; +import org.apache.rocketmq.client.consumer.PopCallback; +import org.apache.rocketmq.client.consumer.PopResult; +import org.apache.rocketmq.client.consumer.PopStatus; +import org.apache.rocketmq.client.consumer.PullCallback; +import org.apache.rocketmq.client.consumer.PullResult; +import org.apache.rocketmq.client.consumer.PullStatus; +import org.apache.rocketmq.client.impl.CommunicationMode; +import org.apache.rocketmq.client.impl.MQClientAPIImpl; +import org.apache.rocketmq.client.impl.consumer.PullResultExt; +import org.apache.rocketmq.client.impl.mqclient.DoNothingClientRemotingProcessor; +import org.apache.rocketmq.client.impl.mqclient.MQClientAPIExt; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.remoting.InvokeCallback; +import org.apache.rocketmq.remoting.RemotingClient; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.ResponseFuture; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.AckMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ChangeInvisibleTimeRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ConsumerSendMsgBackRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerListByGroupRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerListByGroupResponseBody; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerListByGroupResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMaxOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMaxOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.PopMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SearchOffsetRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SearchOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.SendMessageResponseHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.HeartbeatData; +import org.assertj.core.util.Lists; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; + +@RunWith(MockitoJUnitRunner.class) +public class MQClientAPIExtTest { + + private static final String BROKER_ADDR = "127.0.0.1:10911"; + private static final String BROKER_NAME = "brokerName"; + private static final long TIMEOUT = 3000; + private static final String CONSUMER_GROUP = "group"; + private static final String TOPIC = "topic"; + + @Spy + private final MQClientAPIExt mqClientAPI = new MQClientAPIExt(new ClientConfig(), new NettyClientConfig(), new DoNothingClientRemotingProcessor(null), null); + @Mock + private RemotingClient remotingClient; + + @Before + public void init() throws Exception { + Field field = MQClientAPIImpl.class.getDeclaredField("remotingClient"); + field.setAccessible(true); + field.set(mqClientAPI, remotingClient); + } + + @Test + public void testSendHeartbeatAsync() throws Exception { + CompletableFuture future = new CompletableFuture<>(); + future.complete(RemotingCommand.createResponseCommand(ResponseCode.SUCCESS, "")); + doReturn(future).when(remotingClient).invoke(anyString(), any(RemotingCommand.class), anyLong()); + + assertNotNull(mqClientAPI.sendHeartbeatAsync(BROKER_ADDR, new HeartbeatData(), TIMEOUT).get()); + } + + @Test + public void testSendMessageAsync() throws Exception { + AtomicReference msgIdRef = new AtomicReference<>(); + CompletableFuture future = new CompletableFuture<>(); + RemotingCommand response = RemotingCommand.createResponseCommand(SendMessageResponseHeader.class); + SendMessageResponseHeader sendMessageResponseHeader = (SendMessageResponseHeader) response.readCustomHeader(); + sendMessageResponseHeader.setMsgId(msgIdRef.get()); + sendMessageResponseHeader.setQueueId(0); + sendMessageResponseHeader.setQueueOffset(1L); + response.setCode(ResponseCode.SUCCESS); + response.makeCustomHeaderToNet(); + future.complete(response); + doReturn(future).when(remotingClient).invoke(anyString(), any(RemotingCommand.class), anyLong()); + + MessageExt messageExt = createMessage(); + msgIdRef.set(MessageClientIDSetter.getUniqID(messageExt)); + + SendResult sendResult = mqClientAPI.sendMessageAsync(BROKER_ADDR, BROKER_NAME, messageExt, new SendMessageRequestHeader(), TIMEOUT) + .get(); + assertNotNull(sendResult); + assertEquals(msgIdRef.get(), sendResult.getMsgId()); + assertEquals(SendStatus.SEND_OK, sendResult.getSendStatus()); + } + + @Test + public void testSendMessageListAsync() throws Exception { + CompletableFuture future = new CompletableFuture<>(); + RemotingCommand response = RemotingCommand.createResponseCommand(SendMessageResponseHeader.class); + SendMessageResponseHeader sendMessageResponseHeader = (SendMessageResponseHeader) response.readCustomHeader(); + sendMessageResponseHeader.setMsgId(""); + sendMessageResponseHeader.setQueueId(0); + sendMessageResponseHeader.setQueueOffset(1L); + response.setCode(ResponseCode.SUCCESS); + response.makeCustomHeaderToNet(); + future.complete(response); + doReturn(future).when(remotingClient).invoke(anyString(), any(RemotingCommand.class), anyLong()); + + List messageExtList = new ArrayList<>(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 3; i++) { + MessageExt messageExt = createMessage(); + sb.append(sb.length() == 0 ? "" : ",").append(MessageClientIDSetter.getUniqID(messageExt)); + messageExtList.add(messageExt); + } + + SendResult sendResult = mqClientAPI.sendMessageAsync(BROKER_ADDR, BROKER_NAME, messageExtList, new SendMessageRequestHeader(), TIMEOUT) + .get(); + assertNotNull(sendResult); + assertEquals(sb.toString(), sendResult.getMsgId()); + assertEquals(SendStatus.SEND_OK, sendResult.getSendStatus()); + } + + @Test + public void testSendMessageBackAsync() throws Exception { + CompletableFuture future = new CompletableFuture<>(); + future.complete(RemotingCommand.createResponseCommand(ResponseCode.SUCCESS, "")); + doReturn(future).when(remotingClient).invoke(anyString(), any(RemotingCommand.class), anyLong()); + + RemotingCommand remotingCommand = mqClientAPI.sendMessageBackAsync(BROKER_ADDR, new ConsumerSendMsgBackRequestHeader(), TIMEOUT) + .get(); + assertNotNull(remotingCommand); + assertEquals(ResponseCode.SUCCESS, remotingCommand.getCode()); + } + + @Test + public void testPopMessageAsync() throws Exception { + PopResult popResult = new PopResult(PopStatus.POLLING_NOT_FOUND, null); + doAnswer((Answer) mock -> { + PopCallback popCallback = mock.getArgument(4); + popCallback.onSuccess(popResult); + return null; + }).when(mqClientAPI).popMessageAsync(anyString(), anyString(), any(), anyLong(), any()); + + assertSame(popResult, mqClientAPI.popMessageAsync(BROKER_ADDR, BROKER_NAME, new PopMessageRequestHeader(), TIMEOUT).get()); + } + + @Test + public void testAckMessageAsync() throws Exception { + AckResult ackResult = new AckResult(); + doAnswer((Answer) mock -> { + AckCallback ackCallback = mock.getArgument(2); + ackCallback.onSuccess(ackResult); + return null; + }).when(mqClientAPI).ackMessageAsync(anyString(), anyLong(), any(AckCallback.class), any()); + + assertSame(ackResult, mqClientAPI.ackMessageAsync(BROKER_ADDR, new AckMessageRequestHeader(), TIMEOUT).get()); + } + + @Test + public void testBatchAckMessageAsync() throws Exception { + AckResult ackResult = new AckResult(); + doAnswer((Answer) mock -> { + AckCallback ackCallback = mock.getArgument(2); + ackCallback.onSuccess(ackResult); + return null; + }).when(mqClientAPI).batchAckMessageAsync(anyString(), anyLong(), any(AckCallback.class), any()); + + assertSame(ackResult, mqClientAPI.batchAckMessageAsync(BROKER_ADDR, TOPIC, CONSUMER_GROUP, new ArrayList<>(), TIMEOUT).get()); + } + + @Test + public void testChangeInvisibleTimeAsync() throws Exception { + AckResult ackResult = new AckResult(); + doAnswer((Answer) mock -> { + AckCallback ackCallback = mock.getArgument(4); + ackCallback.onSuccess(ackResult); + return null; + }).when(mqClientAPI).changeInvisibleTimeAsync(anyString(), anyString(), any(), anyLong(), any(AckCallback.class)); + + assertSame(ackResult, mqClientAPI.changeInvisibleTimeAsync(BROKER_ADDR, BROKER_NAME, new ChangeInvisibleTimeRequestHeader(), TIMEOUT).get()); + } + + @Test + public void testPullMessageAsync() throws Exception { + MessageExt msg1 = createMessage(); + byte[] msg1Byte = MessageDecoder.encode(msg1, false); + MessageExt msg2 = createMessage(); + byte[] msg2Byte = MessageDecoder.encode(msg2, false); + + ByteBuffer byteBuffer = ByteBuffer.allocate(msg1Byte.length + msg2Byte.length); + byteBuffer.put(msg1Byte); + byteBuffer.put(msg2Byte); + + PullResultExt pullResultExt = new PullResultExt(PullStatus.FOUND, 0, 0, 1, null, 0, + byteBuffer.array()); + doAnswer((Answer) mock -> { + PullCallback pullCallback = mock.getArgument(4); + pullCallback.onSuccess(pullResultExt); + return null; + }).when(mqClientAPI).pullMessage(anyString(), any(), anyLong(), any(CommunicationMode.class), any(PullCallback.class)); + + PullResult pullResult = mqClientAPI.pullMessageAsync(BROKER_ADDR, new PullMessageRequestHeader(), TIMEOUT).get(); + assertNotNull(pullResult); + assertEquals(2, pullResult.getMsgFoundList().size()); + + Set msgIdSet = pullResult.getMsgFoundList().stream().map(MessageClientIDSetter::getUniqID).collect(Collectors.toSet()); + assertTrue(msgIdSet.contains(MessageClientIDSetter.getUniqID(msg1))); + assertTrue(msgIdSet.contains(MessageClientIDSetter.getUniqID(msg2))); + } + + @Test + public void testGetConsumerListByGroupAsync() throws Exception { + List clientIds = Lists.newArrayList("clientIds"); + doAnswer((Answer) mock -> { + InvokeCallback invokeCallback = mock.getArgument(3); + ResponseFuture responseFuture = new ResponseFuture(null, 0, 3000, invokeCallback, null); + RemotingCommand response = RemotingCommand.createResponseCommand(GetConsumerListByGroupResponseHeader.class); + response.setCode(ResponseCode.SUCCESS); + response.makeCustomHeaderToNet(); + GetConsumerListByGroupResponseBody body = new GetConsumerListByGroupResponseBody(); + body.setConsumerIdList(clientIds); + response.setBody(body.encode()); + responseFuture.putResponse(response); + invokeCallback.operationSucceed(responseFuture.getResponseCommand()); + return null; + }).when(remotingClient).invokeAsync(anyString(), any(RemotingCommand.class), anyLong(), any()); + + List res = mqClientAPI.getConsumerListByGroupAsync(BROKER_ADDR, new GetConsumerListByGroupRequestHeader(), TIMEOUT).get(); + assertEquals(clientIds, res); + } + + @Test + public void testGetEmptyConsumerListByGroupAsync() throws Exception { + doAnswer((Answer) mock -> { + InvokeCallback invokeCallback = mock.getArgument(3); + ResponseFuture responseFuture = new ResponseFuture(null, 0, 3000, invokeCallback, null); + RemotingCommand response = RemotingCommand.createResponseCommand(GetConsumerListByGroupRequestHeader.class); + response.setCode(ResponseCode.SYSTEM_ERROR); + response.makeCustomHeaderToNet(); + responseFuture.putResponse(response); + invokeCallback.operationSucceed(responseFuture.getResponseCommand()); + return null; + }).when(remotingClient).invokeAsync(anyString(), any(RemotingCommand.class), anyLong(), any()); + + List res = mqClientAPI.getConsumerListByGroupAsync(BROKER_ADDR, new GetConsumerListByGroupRequestHeader(), TIMEOUT).get(); + assertTrue(res.isEmpty()); + } + + @Test + public void testGetMaxOffsetAsync() throws Exception { + long offset = ThreadLocalRandom.current().nextLong(); + doAnswer((Answer) mock -> { + InvokeCallback invokeCallback = mock.getArgument(3); + ResponseFuture responseFuture = new ResponseFuture(null, 0, 3000, invokeCallback, null); + RemotingCommand response = RemotingCommand.createResponseCommand(GetMaxOffsetResponseHeader.class); + GetMaxOffsetResponseHeader responseHeader = (GetMaxOffsetResponseHeader) response.readCustomHeader(); + responseHeader.setOffset(offset); + response.setCode(ResponseCode.SUCCESS); + response.makeCustomHeaderToNet(); + responseFuture.putResponse(response); + invokeCallback.operationSucceed(responseFuture.getResponseCommand()); + return null; + }).when(remotingClient).invokeAsync(anyString(), any(RemotingCommand.class), anyLong(), any()); + + GetMaxOffsetRequestHeader requestHeader = new GetMaxOffsetRequestHeader(); + requestHeader.setTopic(TOPIC); + requestHeader.setQueueId(0); + assertEquals(offset, mqClientAPI.getMaxOffset(BROKER_ADDR, requestHeader, TIMEOUT).get().longValue()); + } + + @Test + public void testSearchOffsetAsync() throws Exception { + long offset = ThreadLocalRandom.current().nextLong(); + CompletableFuture future = new CompletableFuture<>(); + RemotingCommand response = RemotingCommand.createResponseCommand(SearchOffsetResponseHeader.class); + SearchOffsetResponseHeader responseHeader = (SearchOffsetResponseHeader) response.readCustomHeader(); + responseHeader.setOffset(offset); + response.setCode(ResponseCode.SUCCESS); + response.makeCustomHeaderToNet(); + future.complete(response); + + doReturn(future).when(remotingClient).invoke(anyString(), any(RemotingCommand.class), anyLong()); + + SearchOffsetRequestHeader requestHeader = new SearchOffsetRequestHeader(); + requestHeader.setTopic(TOPIC); + requestHeader.setQueueId(0); + requestHeader.setTimestamp(System.currentTimeMillis()); + assertEquals(offset, mqClientAPI.searchOffset(BROKER_ADDR, requestHeader, TIMEOUT).get().longValue()); + } + + protected MessageExt createMessage() { + MessageExt messageExt = new MessageExt(); + messageExt.setTopic("topic"); + messageExt.setBornHost(NetworkUtil.string2SocketAddress("127.0.0.2:8888")); + messageExt.setStoreHost(NetworkUtil.string2SocketAddress("127.0.0.1:10911")); + messageExt.setBody(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8)); + MessageClientIDSetter.setUniqID(messageExt); + return messageExt; + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/service/mqclient/ProxyClientRemotingProcessorTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/service/mqclient/ProxyClientRemotingProcessorTest.java new file mode 100644 index 00000000000..a6d807937e2 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/service/mqclient/ProxyClientRemotingProcessorTest.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.mqclient; + +import apache.rocketmq.v2.TelemetryCommand; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.stub.ServerCallStreamObserver; +import io.netty.channel.Channel; +import java.net.InetSocketAddress; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.rocketmq.broker.client.ProducerManager; +import org.apache.rocketmq.proxy.service.client.ProxyClientRemotingProcessor; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.grpc.v2.channel.GrpcClientChannel; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcClientSettingsManager; +import org.apache.rocketmq.proxy.service.channel.SimpleChannelHandlerContext; +import org.apache.rocketmq.proxy.service.relay.ProxyRelayResult; +import org.apache.rocketmq.proxy.service.relay.ProxyRelayService; +import org.apache.rocketmq.proxy.service.relay.RelayData; +import org.apache.rocketmq.proxy.service.transaction.TransactionData; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.header.CheckTransactionStateRequestHeader; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ProxyClientRemotingProcessorTest { + @Mock + private ProducerManager producerManager; + @Mock + private GrpcClientSettingsManager grpcClientSettingsManager; + @Mock + private ProxyRelayService proxyRelayService; + + @Test + public void testTransactionCheck() throws Exception { + CompletableFuture> proxyRelayResultFuture = new CompletableFuture<>(); + when(proxyRelayService.processCheckTransactionState(any(), any(), any(), any())) + .thenReturn(new RelayData<>( + new TransactionData("brokerName", 0, 0, "id", System.currentTimeMillis(), 3000), + proxyRelayResultFuture)); + + GrpcClientChannel grpcClientChannel = new GrpcClientChannel(proxyRelayService, grpcClientSettingsManager, null, + ProxyContext.create().setRemoteAddress("127.0.0.1:8888").setLocalAddress("127.0.0.1:10911"), "clientId"); + when(producerManager.getAvailableChannel(anyString())) + .thenReturn(grpcClientChannel); + + ProxyClientRemotingProcessor processor = new ProxyClientRemotingProcessor(producerManager); + CheckTransactionStateRequestHeader requestHeader = new CheckTransactionStateRequestHeader(); + RemotingCommand command = RemotingCommand.createRequestCommand(RequestCode.CHECK_TRANSACTION_STATE, requestHeader); + MessageExt message = new MessageExt(); + message.setQueueId(0); + message.setFlag(12); + message.setQueueOffset(0L); + message.setCommitLogOffset(100L); + message.setSysFlag(0); + message.setBornTimestamp(System.currentTimeMillis()); + message.setBornHost(new InetSocketAddress("127.0.0.1", 10)); + message.setStoreTimestamp(System.currentTimeMillis()); + message.setStoreHost(new InetSocketAddress("127.0.0.1", 11)); + message.setBody("body".getBytes()); + message.setTopic("topic"); + MessageAccessor.putProperty(message, MessageConst.PROPERTY_PRODUCER_GROUP, "group"); + command.setBody(MessageDecoder.encode(message, false)); + + processor.processRequest(new MockChannelHandlerContext(null), command); + + ServerCallStreamObserver observer = mock(ServerCallStreamObserver.class); + grpcClientChannel.setClientObserver(observer); + + processor.processRequest(new MockChannelHandlerContext(null), command); + verify(observer, times(1)).onNext(any()); + + // throw exception to test clear observer + doThrow(new StatusRuntimeException(Status.CANCELLED)).when(observer).onNext(any()); + + ExecutorService executorService = Executors.newCachedThreadPool(); + AtomicInteger count = new AtomicInteger(); + for (int i = 0; i < 100; i++) { + executorService.submit(() -> { + try { + processor.processRequest(new MockChannelHandlerContext(null), command); + count.incrementAndGet(); + } catch (RemotingCommandException ignored) { + } + }); + } + await().atMost(Duration.ofSeconds(1)).until(() -> count.get() == 100); + verify(observer, times(2)).onNext(any()); + } + + protected static class MockChannelHandlerContext extends SimpleChannelHandlerContext { + + public MockChannelHandlerContext(Channel channel) { + super(channel); + } + + @Override + public Channel channel() { + Channel channel = mock(Channel.class); + when(channel.remoteAddress()).thenReturn(NetworkUtil.string2SocketAddress("127.0.0.1:10911")); + return channel; + } + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManagerTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManagerTest.java new file mode 100644 index 00000000000..25ae1509a95 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/service/receipt/DefaultReceiptHandleManagerTest.java @@ -0,0 +1,466 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.receipt; + +import io.netty.channel.Channel; +import io.netty.channel.local.LocalChannel; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.rocketmq.broker.client.ClientChannelInfo; +import org.apache.rocketmq.broker.client.ConsumerGroupEvent; +import org.apache.rocketmq.broker.client.ConsumerIdsChangeListener; +import org.apache.rocketmq.broker.client.ConsumerManager; +import org.apache.rocketmq.client.consumer.AckResult; +import org.apache.rocketmq.client.consumer.AckStatus; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.consumer.ReceiptHandle; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.common.state.StateEventListener; +import org.apache.rocketmq.proxy.common.RenewEvent; +import org.apache.rocketmq.proxy.common.ContextVariable; +import org.apache.rocketmq.proxy.common.MessageReceiptHandle; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.common.ProxyException; +import org.apache.rocketmq.proxy.common.ProxyExceptionCode; +import org.apache.rocketmq.proxy.common.ReceiptHandleGroup; +import org.apache.rocketmq.proxy.common.RenewStrategyPolicy; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.ProxyConfig; +import org.apache.rocketmq.proxy.processor.MessagingProcessor; +import org.apache.rocketmq.proxy.common.ReceiptHandleGroupKey; +import org.apache.rocketmq.proxy.service.BaseServiceTest; +import org.apache.rocketmq.proxy.service.metadata.MetadataService; +import org.apache.rocketmq.remoting.protocol.LanguageCode; +import org.apache.rocketmq.remoting.protocol.subscription.RetryPolicy; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; + +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class DefaultReceiptHandleManagerTest extends BaseServiceTest { + private DefaultReceiptHandleManager receiptHandleManager; + @Mock + protected MessagingProcessor messagingProcessor; + @Mock + protected MetadataService metadataService; + @Mock + protected ConsumerManager consumerManager; + + private static final ProxyContext PROXY_CONTEXT = ProxyContext.create(); + private static final String GROUP = "group"; + private static final String TOPIC = "topic"; + private static final String BROKER_NAME = "broker"; + private static final int QUEUE_ID = 1; + private static final String MESSAGE_ID = "messageId"; + private static final long OFFSET = 123L; + private static final long INVISIBLE_TIME = 60000L; + private static final int RECONSUME_TIMES = 1; + private static final String MSG_ID = MessageClientIDSetter.createUniqID(); + private MessageReceiptHandle messageReceiptHandle; + + private String receiptHandle; + + @Before + public void setup() { + receiptHandleManager = new DefaultReceiptHandleManager(metadataService, consumerManager, new StateEventListener() { + @Override + public void fireEvent(RenewEvent event) { + MessageReceiptHandle messageReceiptHandle = event.getMessageReceiptHandle(); + ReceiptHandle handle = ReceiptHandle.decode(messageReceiptHandle.getReceiptHandleStr()); + messagingProcessor.changeInvisibleTime(PROXY_CONTEXT, handle, messageReceiptHandle.getMessageId(), + messageReceiptHandle.getGroup(), messageReceiptHandle.getTopic(), event.getRenewTime()) + .whenComplete((v, t) -> { + if (t != null) { + event.getFuture().completeExceptionally(t); + return; + } + event.getFuture().complete(v); + }); + } + }); + ProxyConfig config = ConfigurationManager.getProxyConfig(); + receiptHandle = ReceiptHandle.builder() + .startOffset(0L) + .retrieveTime(System.currentTimeMillis() - INVISIBLE_TIME + config.getRenewAheadTimeMillis() - 5) + .invisibleTime(INVISIBLE_TIME) + .reviveQueueId(1) + .topicType(ReceiptHandle.NORMAL_TOPIC) + .brokerName(BROKER_NAME) + .queueId(QUEUE_ID) + .offset(OFFSET) + .commitLogOffset(0L) + .build().encode(); + PROXY_CONTEXT.withVal(ContextVariable.CLIENT_ID, "channel-id"); + PROXY_CONTEXT.withVal(ContextVariable.CHANNEL, new LocalChannel()); + Mockito.doNothing().when(consumerManager).appendConsumerIdsChangeListener(Mockito.any(ConsumerIdsChangeListener.class)); + messageReceiptHandle = new MessageReceiptHandle(GROUP, TOPIC, QUEUE_ID, receiptHandle, MESSAGE_ID, OFFSET, + RECONSUME_TIMES); + } + + @Test + public void testAddReceiptHandle() { + Channel channel = new LocalChannel(); + receiptHandleManager.addReceiptHandle(PROXY_CONTEXT, channel, GROUP, MSG_ID, messageReceiptHandle); + Mockito.when(metadataService.getSubscriptionGroupConfig(Mockito.any(), Mockito.eq(GROUP))).thenReturn(new SubscriptionGroupConfig()); + Mockito.when(consumerManager.findChannel(Mockito.eq(GROUP), Mockito.eq(channel))).thenReturn(Mockito.mock(ClientChannelInfo.class)); + receiptHandleManager.scheduleRenewTask(); + Mockito.verify(messagingProcessor, Mockito.timeout(1000).times(1)) + .changeInvisibleTime(Mockito.any(ProxyContext.class), Mockito.any(ReceiptHandle.class), Mockito.eq(MESSAGE_ID), + Mockito.eq(GROUP), Mockito.eq(TOPIC), Mockito.eq(ConfigurationManager.getProxyConfig().getDefaultInvisibleTimeMills())); + } + + @Test + public void testAddDuplicationMessage() { + ProxyConfig config = ConfigurationManager.getProxyConfig(); + Channel channel = PROXY_CONTEXT.getVal(ContextVariable.CHANNEL); + { + String receiptHandle = ReceiptHandle.builder() + .startOffset(0L) + .retrieveTime(System.currentTimeMillis() - INVISIBLE_TIME + config.getRenewAheadTimeMillis() - 1000) + .invisibleTime(INVISIBLE_TIME) + .reviveQueueId(1) + .topicType(ReceiptHandle.NORMAL_TOPIC) + .brokerName(BROKER_NAME) + .queueId(QUEUE_ID) + .offset(OFFSET) + .commitLogOffset(0L) + .build().encode(); + MessageReceiptHandle messageReceiptHandle = new MessageReceiptHandle(GROUP, TOPIC, QUEUE_ID, receiptHandle, MESSAGE_ID, OFFSET, + RECONSUME_TIMES); + receiptHandleManager.addReceiptHandle(PROXY_CONTEXT, channel, GROUP, MSG_ID, messageReceiptHandle); + } + receiptHandleManager.addReceiptHandle(PROXY_CONTEXT, channel, GROUP, MSG_ID, messageReceiptHandle); + Mockito.when(metadataService.getSubscriptionGroupConfig(Mockito.any(), Mockito.eq(GROUP))).thenReturn(new SubscriptionGroupConfig()); + Mockito.when(consumerManager.findChannel(Mockito.eq(GROUP), Mockito.eq(channel))).thenReturn(Mockito.mock(ClientChannelInfo.class)); + receiptHandleManager.scheduleRenewTask(); + ArgumentCaptor handleArgumentCaptor = ArgumentCaptor.forClass(ReceiptHandle.class); + Mockito.verify(messagingProcessor, Mockito.timeout(1000).times(1)) + .changeInvisibleTime(Mockito.any(ProxyContext.class), handleArgumentCaptor.capture(), Mockito.eq(MESSAGE_ID), + Mockito.eq(GROUP), Mockito.eq(TOPIC), Mockito.eq(ConfigurationManager.getProxyConfig().getDefaultInvisibleTimeMills())); + + assertEquals(receiptHandle, handleArgumentCaptor.getValue().encode()); + } + + @Test + public void testRenewReceiptHandle() { + ProxyConfig config = ConfigurationManager.getProxyConfig(); + Channel channel = PROXY_CONTEXT.getVal(ContextVariable.CHANNEL); + receiptHandleManager.addReceiptHandle(PROXY_CONTEXT, channel, GROUP, MSG_ID, messageReceiptHandle); + SubscriptionGroupConfig groupConfig = new SubscriptionGroupConfig(); + Mockito.when(metadataService.getSubscriptionGroupConfig(Mockito.any(), Mockito.eq(GROUP))).thenReturn(groupConfig); + Mockito.when(consumerManager.findChannel(Mockito.eq(GROUP), Mockito.eq(channel))).thenReturn(Mockito.mock(ClientChannelInfo.class)); + long newInvisibleTime = 18000L; + + ReceiptHandle newReceiptHandleClass = ReceiptHandle.builder() + .startOffset(0L) + .retrieveTime(System.currentTimeMillis() - newInvisibleTime + config.getRenewAheadTimeMillis() - 5) + .invisibleTime(newInvisibleTime) + .reviveQueueId(1) + .topicType(ReceiptHandle.NORMAL_TOPIC) + .brokerName(BROKER_NAME) + .queueId(QUEUE_ID) + .offset(OFFSET) + .commitLogOffset(0L) + .build(); + String newReceiptHandle = newReceiptHandleClass.encode(); + + RetryPolicy retryPolicy = new RenewStrategyPolicy(); + AtomicInteger times = new AtomicInteger(0); + + AckResult ackResult = new AckResult(); + ackResult.setStatus(AckStatus.OK); + ackResult.setExtraInfo(newReceiptHandle); + + Mockito.when(messagingProcessor.changeInvisibleTime(Mockito.any(ProxyContext.class), Mockito.any(ReceiptHandle.class), Mockito.eq(MESSAGE_ID), + Mockito.eq(GROUP), Mockito.eq(TOPIC), Mockito.eq(retryPolicy.nextDelayDuration(times.get())))) + .thenReturn(CompletableFuture.completedFuture(ackResult)); + receiptHandleManager.scheduleRenewTask(); + + Mockito.verify(messagingProcessor, Mockito.timeout(1000).times(1)) + .changeInvisibleTime(Mockito.any(ProxyContext.class), Mockito.argThat(r -> r.getInvisibleTime() == INVISIBLE_TIME), Mockito.eq(MESSAGE_ID), + Mockito.eq(GROUP), Mockito.eq(TOPIC), Mockito.eq(retryPolicy.nextDelayDuration(times.get()))); + receiptHandleManager.scheduleRenewTask(); + + Mockito.verify(messagingProcessor, Mockito.timeout(1000).times(1)) + .changeInvisibleTime(Mockito.any(ProxyContext.class), Mockito.argThat(r -> r.getInvisibleTime() == newInvisibleTime), Mockito.eq(MESSAGE_ID), + Mockito.eq(GROUP), Mockito.eq(TOPIC), Mockito.eq(retryPolicy.nextDelayDuration(times.incrementAndGet()))); + receiptHandleManager.scheduleRenewTask(); + } + + @Test + public void testRenewExceedMaxRenewTimes() { + Channel channel = PROXY_CONTEXT.getVal(ContextVariable.CHANNEL); + Mockito.when(consumerManager.findChannel(Mockito.eq(GROUP), Mockito.eq(channel))).thenReturn(Mockito.mock(ClientChannelInfo.class)); + receiptHandleManager.addReceiptHandle(PROXY_CONTEXT, channel, GROUP, MSG_ID, messageReceiptHandle); + + CompletableFuture ackResultFuture = new CompletableFuture<>(); + ackResultFuture.completeExceptionally(new MQClientException(0, "error")); + + RetryPolicy retryPolicy = new RenewStrategyPolicy(); + + Mockito.when(messagingProcessor.changeInvisibleTime(Mockito.any(ProxyContext.class), Mockito.any(ReceiptHandle.class), Mockito.eq(MESSAGE_ID), + Mockito.eq(GROUP), Mockito.eq(TOPIC), Mockito.eq(retryPolicy.nextDelayDuration(messageReceiptHandle.getRenewTimes())))) + .thenReturn(ackResultFuture); + + await().atMost(Duration.ofSeconds(1)).until(() -> { + receiptHandleManager.scheduleRenewTask(); + try { + ReceiptHandleGroup receiptHandleGroup = receiptHandleManager.receiptHandleGroupMap.values().stream().findFirst().get(); + return receiptHandleGroup.isEmpty(); + } catch (Exception e) { + return false; + } + }); + + Mockito.verify(messagingProcessor, Mockito.times(3)) + .changeInvisibleTime(Mockito.any(ProxyContext.class), Mockito.any(ReceiptHandle.class), Mockito.eq(MESSAGE_ID), + Mockito.eq(GROUP), Mockito.eq(TOPIC), Mockito.eq(retryPolicy.nextDelayDuration(messageReceiptHandle.getRenewTimes()))); + } + + @Test + public void testRenewWithInvalidHandle() { + Channel channel = PROXY_CONTEXT.getVal(ContextVariable.CHANNEL); + Mockito.when(consumerManager.findChannel(Mockito.eq(GROUP), Mockito.eq(channel))).thenReturn(Mockito.mock(ClientChannelInfo.class)); + receiptHandleManager.addReceiptHandle(PROXY_CONTEXT, channel, GROUP, MSG_ID, messageReceiptHandle); + + CompletableFuture ackResultFuture = new CompletableFuture<>(); + ackResultFuture.completeExceptionally(new ProxyException(ProxyExceptionCode.INVALID_RECEIPT_HANDLE, "error")); + Mockito.when(messagingProcessor.changeInvisibleTime(Mockito.any(ProxyContext.class), Mockito.any(ReceiptHandle.class), Mockito.eq(MESSAGE_ID), + Mockito.eq(GROUP), Mockito.eq(TOPIC), Mockito.eq(ConfigurationManager.getProxyConfig().getDefaultInvisibleTimeMills()))) + .thenReturn(ackResultFuture); + + await().atMost(Duration.ofSeconds(1)).until(() -> { + receiptHandleManager.scheduleRenewTask(); + try { + ReceiptHandleGroup receiptHandleGroup = receiptHandleManager.receiptHandleGroupMap.values().stream().findFirst().get(); + return receiptHandleGroup.isEmpty(); + } catch (Exception e) { + return false; + } + }); + } + + @Test + public void testRenewWithErrorThenOK() { + ProxyConfig config = ConfigurationManager.getProxyConfig(); + Channel channel = PROXY_CONTEXT.getVal(ContextVariable.CHANNEL); + Mockito.when(consumerManager.findChannel(Mockito.eq(GROUP), Mockito.eq(channel))).thenReturn(Mockito.mock(ClientChannelInfo.class)); + receiptHandleManager.addReceiptHandle(PROXY_CONTEXT, channel, GROUP, MSG_ID, messageReceiptHandle); + + AtomicInteger count = new AtomicInteger(0); + List> futureList = new ArrayList<>(); + { + CompletableFuture ackResultFuture = new CompletableFuture<>(); + ackResultFuture.completeExceptionally(new MQClientException(0, "error")); + futureList.add(ackResultFuture); + futureList.add(ackResultFuture); + } + { + long newInvisibleTime = 2000L; + ReceiptHandle newReceiptHandleClass = ReceiptHandle.builder() + .startOffset(0L) + .retrieveTime(System.currentTimeMillis() - newInvisibleTime + config.getRenewAheadTimeMillis() - 5) + .invisibleTime(newInvisibleTime) + .reviveQueueId(1) + .topicType(ReceiptHandle.NORMAL_TOPIC) + .brokerName(BROKER_NAME) + .queueId(QUEUE_ID) + .offset(OFFSET) + .commitLogOffset(0L) + .build(); + String newReceiptHandle = newReceiptHandleClass.encode(); + AckResult ackResult = new AckResult(); + ackResult.setStatus(AckStatus.OK); + ackResult.setExtraInfo(newReceiptHandle); + futureList.add(CompletableFuture.completedFuture(ackResult)); + } + { + CompletableFuture ackResultFuture = new CompletableFuture<>(); + ackResultFuture.completeExceptionally(new MQClientException(0, "error")); + futureList.add(ackResultFuture); + futureList.add(ackResultFuture); + futureList.add(ackResultFuture); + futureList.add(ackResultFuture); + } + + RetryPolicy retryPolicy = new RenewStrategyPolicy(); + AtomicInteger times = new AtomicInteger(0); + for (int i = 0; i < 6; i++) { + Mockito.doAnswer((Answer>) mock -> { + return futureList.get(count.getAndIncrement()); + }).when(messagingProcessor).changeInvisibleTime(Mockito.any(ProxyContext.class), Mockito.any(ReceiptHandle.class), Mockito.eq(MESSAGE_ID), + Mockito.eq(GROUP), Mockito.eq(TOPIC), Mockito.eq(retryPolicy.nextDelayDuration(times.getAndIncrement()))); + } + + await().pollDelay(Duration.ZERO).pollInterval(Duration.ofMillis(10)).atMost(Duration.ofSeconds(10)).until(() -> { + receiptHandleManager.scheduleRenewTask(); + try { + ReceiptHandleGroup receiptHandleGroup = receiptHandleManager.receiptHandleGroupMap.values().stream().findFirst().get(); + return receiptHandleGroup.isEmpty(); + } catch (Exception e) { + return false; + } + }); + + assertEquals(6, count.get()); + } + + @Test + public void testRenewReceiptHandleWhenTimeout() { + long newInvisibleTime = 200L; + long maxRenewMs = ConfigurationManager.getProxyConfig().getRenewMaxTimeMillis(); + String newReceiptHandle = ReceiptHandle.builder() + .startOffset(0L) + .retrieveTime(System.currentTimeMillis() - maxRenewMs) + .invisibleTime(newInvisibleTime) + .reviveQueueId(1) + .topicType(ReceiptHandle.NORMAL_TOPIC) + .brokerName(BROKER_NAME) + .queueId(QUEUE_ID) + .offset(OFFSET) + .commitLogOffset(0L) + .build().encode(); + messageReceiptHandle = new MessageReceiptHandle(GROUP, TOPIC, QUEUE_ID, newReceiptHandle, MESSAGE_ID, OFFSET, + RECONSUME_TIMES); + Channel channel = PROXY_CONTEXT.getVal(ContextVariable.CHANNEL); + receiptHandleManager.addReceiptHandle(PROXY_CONTEXT, channel, GROUP, MSG_ID, messageReceiptHandle); + Mockito.when(consumerManager.findChannel(Mockito.eq(GROUP), Mockito.eq(channel))).thenReturn(Mockito.mock(ClientChannelInfo.class)); + SubscriptionGroupConfig groupConfig = new SubscriptionGroupConfig(); + Mockito.when(metadataService.getSubscriptionGroupConfig(Mockito.any(), Mockito.eq(GROUP))).thenReturn(groupConfig); + Mockito.when(messagingProcessor.changeInvisibleTime(Mockito.any(), Mockito.any(), Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyLong())) + .thenReturn(CompletableFuture.completedFuture(new AckResult())); + receiptHandleManager.scheduleRenewTask(); + Mockito.verify(messagingProcessor, Mockito.timeout(1000).times(1)) + .changeInvisibleTime(Mockito.any(ProxyContext.class), Mockito.any(ReceiptHandle.class), Mockito.eq(MESSAGE_ID), + Mockito.eq(GROUP), Mockito.eq(TOPIC), Mockito.eq(groupConfig.getGroupRetryPolicy().getRetryPolicy().nextDelayDuration(RECONSUME_TIMES))); + + await().atMost(Duration.ofSeconds(1)).untilAsserted(() -> { + ReceiptHandleGroup receiptHandleGroup = receiptHandleManager.receiptHandleGroupMap.values().stream().findFirst().get(); + assertTrue(receiptHandleGroup.isEmpty()); + }); + } + + @Test + public void testRenewReceiptHandleWhenTimeoutWithNoSubscription() { + long newInvisibleTime = 0L; + String newReceiptHandle = ReceiptHandle.builder() + .startOffset(0L) + .retrieveTime(0) + .invisibleTime(newInvisibleTime) + .reviveQueueId(1) + .topicType(ReceiptHandle.NORMAL_TOPIC) + .brokerName(BROKER_NAME) + .queueId(QUEUE_ID) + .offset(OFFSET) + .commitLogOffset(0L) + .build().encode(); + messageReceiptHandle = new MessageReceiptHandle(GROUP, TOPIC, QUEUE_ID, newReceiptHandle, MESSAGE_ID, OFFSET, + RECONSUME_TIMES); + Channel channel = PROXY_CONTEXT.getVal(ContextVariable.CHANNEL); + receiptHandleManager.addReceiptHandle(PROXY_CONTEXT, channel, GROUP, MSG_ID, messageReceiptHandle); + Mockito.when(consumerManager.findChannel(Mockito.eq(GROUP), Mockito.eq(channel))).thenReturn(Mockito.mock(ClientChannelInfo.class)); + Mockito.when(metadataService.getSubscriptionGroupConfig(Mockito.any(), Mockito.eq(GROUP))).thenReturn(null); + Mockito.when(messagingProcessor.changeInvisibleTime(Mockito.any(), Mockito.any(), Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyLong())) + .thenReturn(CompletableFuture.completedFuture(new AckResult())); + receiptHandleManager.scheduleRenewTask(); + await().atMost(Duration.ofSeconds(1)).until(() -> { + try { + ReceiptHandleGroup receiptHandleGroup = receiptHandleManager.receiptHandleGroupMap.values().stream().findFirst().get(); + return receiptHandleGroup.isEmpty(); + } catch (Exception e) { + return false; + } + }); + + Mockito.verify(messagingProcessor, Mockito.timeout(1000).times(0)) + .changeInvisibleTime(Mockito.any(ProxyContext.class), Mockito.any(ReceiptHandle.class), Mockito.anyString(), + Mockito.anyString(), Mockito.anyString(), Mockito.anyLong()); + } + + @Test + public void testRenewReceiptHandleWhenNotArrivingTime() { + String newReceiptHandle = ReceiptHandle.builder() + .startOffset(0L) + .retrieveTime(System.currentTimeMillis()) + .invisibleTime(INVISIBLE_TIME) + .reviveQueueId(1) + .topicType(ReceiptHandle.NORMAL_TOPIC) + .brokerName(BROKER_NAME) + .queueId(QUEUE_ID) + .offset(OFFSET) + .commitLogOffset(0L) + .build().encode(); + messageReceiptHandle = new MessageReceiptHandle(GROUP, TOPIC, QUEUE_ID, newReceiptHandle, MESSAGE_ID, OFFSET, + RECONSUME_TIMES); + Channel channel = PROXY_CONTEXT.getVal(ContextVariable.CHANNEL); + receiptHandleManager.addReceiptHandle(PROXY_CONTEXT, channel, GROUP, MSG_ID, messageReceiptHandle); + SubscriptionGroupConfig groupConfig = new SubscriptionGroupConfig(); + Mockito.when(metadataService.getSubscriptionGroupConfig(Mockito.any(), Mockito.eq(GROUP))).thenReturn(groupConfig); + Mockito.when(consumerManager.findChannel(Mockito.eq(GROUP), Mockito.eq(channel))).thenReturn(Mockito.mock(ClientChannelInfo.class)); + receiptHandleManager.scheduleRenewTask(); + Mockito.verify(messagingProcessor, Mockito.timeout(1000).times(0)) + .changeInvisibleTime(Mockito.any(ProxyContext.class), Mockito.any(ReceiptHandle.class), Mockito.anyString(), + Mockito.anyString(), Mockito.anyString(), Mockito.anyLong()); + } + + @Test + public void testRemoveReceiptHandle() { + Channel channel = PROXY_CONTEXT.getVal(ContextVariable.CHANNEL); + receiptHandleManager.addReceiptHandle(PROXY_CONTEXT, channel, GROUP, MSG_ID, messageReceiptHandle); + receiptHandleManager.removeReceiptHandle(PROXY_CONTEXT, channel, GROUP, MSG_ID, receiptHandle); + SubscriptionGroupConfig groupConfig = new SubscriptionGroupConfig(); + Mockito.when(metadataService.getSubscriptionGroupConfig(Mockito.any(), Mockito.eq(GROUP))).thenReturn(groupConfig); + receiptHandleManager.scheduleRenewTask(); + Mockito.verify(messagingProcessor, Mockito.timeout(1000).times(0)) + .changeInvisibleTime(Mockito.any(ProxyContext.class), Mockito.any(ReceiptHandle.class), Mockito.anyString(), + Mockito.anyString(), Mockito.anyString(), Mockito.anyLong()); + } + + @Test + public void testClearGroup() { + Channel channel = PROXY_CONTEXT.getVal(ContextVariable.CHANNEL); + receiptHandleManager.addReceiptHandle(PROXY_CONTEXT, channel, GROUP, MSG_ID, messageReceiptHandle); + receiptHandleManager.clearGroup(new ReceiptHandleGroupKey(channel, GROUP)); + SubscriptionGroupConfig groupConfig = new SubscriptionGroupConfig(); + Mockito.when(metadataService.getSubscriptionGroupConfig(Mockito.any(), Mockito.eq(GROUP))).thenReturn(groupConfig); + receiptHandleManager.scheduleRenewTask(); + Mockito.verify(messagingProcessor, Mockito.timeout(1000).times(1)) + .changeInvisibleTime(Mockito.any(ProxyContext.class), Mockito.any(ReceiptHandle.class), Mockito.eq(MESSAGE_ID), + Mockito.eq(GROUP), Mockito.eq(TOPIC), Mockito.eq(ConfigurationManager.getProxyConfig().getInvisibleTimeMillisWhenClear())); + } + + @Test + public void testClientOffline() { + ArgumentCaptor listenerArgumentCaptor = ArgumentCaptor.forClass(ConsumerIdsChangeListener.class); + Mockito.verify(consumerManager, Mockito.times(1)).appendConsumerIdsChangeListener(listenerArgumentCaptor.capture()); + Channel channel = PROXY_CONTEXT.getVal(ContextVariable.CHANNEL); + receiptHandleManager.addReceiptHandle(PROXY_CONTEXT, channel, GROUP, MSG_ID, messageReceiptHandle); + listenerArgumentCaptor.getValue().handle(ConsumerGroupEvent.CLIENT_UNREGISTER, GROUP, new ClientChannelInfo(channel, "", LanguageCode.JAVA, 0)); + assertTrue(receiptHandleManager.receiptHandleGroupMap.isEmpty()); + } +} \ No newline at end of file diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/service/relay/LocalProxyRelayServiceTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/service/relay/LocalProxyRelayServiceTest.java new file mode 100644 index 00000000000..4ec797d1aaf --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/service/relay/LocalProxyRelayServiceTest.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.relay; + +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.service.channel.SimpleChannelHandlerContext; +import org.apache.rocketmq.proxy.service.transaction.TransactionService; +import org.apache.rocketmq.remoting.netty.NettyRemotingServer; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.body.CMResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.header.ConsumeMessageDirectlyResultRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerRunningInfoRequestHeader; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(MockitoJUnitRunner.class) +public class LocalProxyRelayServiceTest { + private LocalProxyRelayService localProxyRelayService; + @Mock + private BrokerController brokerControllerMock; + @Mock + private TransactionService transactionService; + @Mock + private NettyRemotingServer nettyRemotingServerMock; + + @Before + public void setUp() { + localProxyRelayService = new LocalProxyRelayService(brokerControllerMock, transactionService); + Mockito.when(brokerControllerMock.getRemotingServer()).thenReturn(nettyRemotingServerMock); + } + + @Test + public void testProcessGetConsumerRunningInfo() { + ConsumerRunningInfo runningInfo = new ConsumerRunningInfo(); + runningInfo.setJstack("jstack"); + String remark = "ok"; + int opaque = 123; + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.GET_CONSUMER_RUNNING_INFO, null); + remotingCommand.setOpaque(opaque); + GetConsumerRunningInfoRequestHeader requestHeader = new GetConsumerRunningInfoRequestHeader(); + requestHeader.setJstackEnable(true); + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(RemotingCommand.class); + CompletableFuture> future = + localProxyRelayService.processGetConsumerRunningInfo(ProxyContext.create(), remotingCommand, requestHeader); + future.complete(new ProxyRelayResult<>(ResponseCode.SUCCESS, remark, runningInfo)); + Mockito.verify(nettyRemotingServerMock, Mockito.times(1)) + .processResponseCommand(Mockito.any(SimpleChannelHandlerContext.class), argumentCaptor.capture()); + RemotingCommand remotingCommand1 = argumentCaptor.getValue(); + assertThat(remotingCommand1.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(remotingCommand1.getRemark()).isEqualTo(remark); + assertThat(remotingCommand1.getBody()).isEqualTo(runningInfo.encode()); + } + + @Test + public void testProcessConsumeMessageDirectly() { + ConsumeMessageDirectlyResultRequestHeader requestHeader = new ConsumeMessageDirectlyResultRequestHeader(); + String remark = "ok"; + int opaque = 123; + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.CONSUME_MESSAGE_DIRECTLY, null); + remotingCommand.setOpaque(opaque); + ConsumeMessageDirectlyResult result = new ConsumeMessageDirectlyResult(); + result.setConsumeResult(CMResult.CR_SUCCESS); + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(RemotingCommand.class); + CompletableFuture> future = + localProxyRelayService.processConsumeMessageDirectly(ProxyContext.create(), remotingCommand, requestHeader); + future.complete(new ProxyRelayResult<>(ResponseCode.SUCCESS, remark, result)); + Mockito.verify(nettyRemotingServerMock, Mockito.times(1)) + .processResponseCommand(Mockito.any(SimpleChannelHandlerContext.class), argumentCaptor.capture()); + RemotingCommand remotingCommand1 = argumentCaptor.getValue(); + assertThat(remotingCommand1.getCode()).isEqualTo(ResponseCode.SUCCESS); + assertThat(remotingCommand1.getRemark()).isEqualTo(remark); + assertThat(remotingCommand1.getBody()).isEqualTo(result.encode()); + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/service/relay/ProxyChannelTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/service/relay/ProxyChannelTest.java new file mode 100644 index 00000000000..947ae2c24f5 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/service/relay/ProxyChannelTest.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.relay; + +import io.netty.channel.Channel; +import java.nio.charset.StandardCharsets; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.proxy.service.transaction.TransactionData; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult; +import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo; +import org.apache.rocketmq.remoting.protocol.header.CheckTransactionStateRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.ConsumeMessageDirectlyResultRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.GetConsumerRunningInfoRequestHeader; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ProxyChannelTest { + + @Mock + private ProxyRelayService proxyRelayService; + + protected abstract static class MockProxyChannel extends ProxyChannel { + + protected MockProxyChannel(ProxyRelayService proxyRelayService, Channel parent, + String remoteAddress, String localAddress) { + super(proxyRelayService, parent, remoteAddress, localAddress); + } + + @Override + public boolean isOpen() { + return false; + } + + @Override + public boolean isActive() { + return false; + } + } + + @Test + public void testWriteAndFlush() throws Exception { + when(this.proxyRelayService.processCheckTransactionState(any(), any(), any(), any())) + .thenReturn(new RelayData<>(mock(TransactionData.class), new CompletableFuture<>())); + + ArgumentCaptor consumeMessageDirectlyArgumentCaptor = + ArgumentCaptor.forClass(ConsumeMessageDirectlyResultRequestHeader.class); + when(this.proxyRelayService.processConsumeMessageDirectly(any(), any(), consumeMessageDirectlyArgumentCaptor.capture())) + .thenReturn(new CompletableFuture<>()); + + ArgumentCaptor getConsumerRunningInfoArgumentCaptor = + ArgumentCaptor.forClass(GetConsumerRunningInfoRequestHeader.class); + when(this.proxyRelayService.processGetConsumerRunningInfo(any(), any(), getConsumerRunningInfoArgumentCaptor.capture())) + .thenReturn(new CompletableFuture<>()); + + CheckTransactionStateRequestHeader checkTransactionStateRequestHeader = new CheckTransactionStateRequestHeader(); + checkTransactionStateRequestHeader.setTransactionId(MessageClientIDSetter.createUniqID()); + RemotingCommand checkTransactionRequest = RemotingCommand.createRequestCommand(RequestCode.CHECK_TRANSACTION_STATE, checkTransactionStateRequestHeader); + MessageExt transactionMessageExt = new MessageExt(); + transactionMessageExt.setTopic("topic"); + transactionMessageExt.setTags("tags"); + transactionMessageExt.setBornHost(NetworkUtil.string2SocketAddress("127.0.0.2:8888")); + transactionMessageExt.setStoreHost(NetworkUtil.string2SocketAddress("127.0.0.1:10911")); + transactionMessageExt.setBody(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8)); + transactionMessageExt.setMsgId(MessageClientIDSetter.createUniqID()); + checkTransactionRequest.setBody(MessageDecoder.encode(transactionMessageExt, false)); + + GetConsumerRunningInfoRequestHeader consumerRunningInfoRequestHeader = new GetConsumerRunningInfoRequestHeader(); + consumerRunningInfoRequestHeader.setConsumerGroup("group"); + consumerRunningInfoRequestHeader.setClientId("clientId"); + RemotingCommand consumerRunningInfoRequest = RemotingCommand.createRequestCommand(RequestCode.GET_CONSUMER_RUNNING_INFO, consumerRunningInfoRequestHeader); + + ConsumeMessageDirectlyResultRequestHeader consumeMessageDirectlyResultRequestHeader = new ConsumeMessageDirectlyResultRequestHeader(); + consumeMessageDirectlyResultRequestHeader.setConsumerGroup("group"); + consumeMessageDirectlyResultRequestHeader.setClientId("clientId"); + MessageExt consumeMessageDirectlyMessageExt = new MessageExt(); + consumeMessageDirectlyMessageExt.setTopic("topic"); + consumeMessageDirectlyMessageExt.setTags("tags"); + consumeMessageDirectlyMessageExt.setBornHost(NetworkUtil.string2SocketAddress("127.0.0.2:8888")); + consumeMessageDirectlyMessageExt.setStoreHost(NetworkUtil.string2SocketAddress("127.0.0.1:10911")); + consumeMessageDirectlyMessageExt.setBody(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8)); + consumeMessageDirectlyMessageExt.setMsgId(MessageClientIDSetter.createUniqID()); + RemotingCommand consumeMessageDirectlyResult = RemotingCommand.createRequestCommand(RequestCode.CONSUME_MESSAGE_DIRECTLY, consumeMessageDirectlyResultRequestHeader); + consumeMessageDirectlyResult.setBody(MessageDecoder.encode(consumeMessageDirectlyMessageExt, false)); + + MockProxyChannel channel = new MockProxyChannel(this.proxyRelayService, null, "127.0.0.2:8888", "127.0.0.1:10911") { + @Override + protected CompletableFuture processOtherMessage(Object msg) { + return CompletableFuture.completedFuture(null); + } + + @Override + protected CompletableFuture processCheckTransaction(CheckTransactionStateRequestHeader header, + MessageExt messageExt, TransactionData transactionData, CompletableFuture> responseFuture) { + assertEquals(checkTransactionStateRequestHeader, header); + assertArrayEquals(transactionMessageExt.getBody(), messageExt.getBody()); + return CompletableFuture.completedFuture(null); + } + + @Override + protected CompletableFuture processGetConsumerRunningInfo(RemotingCommand command, + GetConsumerRunningInfoRequestHeader header, + CompletableFuture> responseFuture) { + assertEquals(consumerRunningInfoRequestHeader, getConsumerRunningInfoArgumentCaptor.getValue()); + assertEquals(consumerRunningInfoRequestHeader, header); + return CompletableFuture.completedFuture(null); + } + + @Override + protected CompletableFuture processConsumeMessageDirectly(RemotingCommand command, + ConsumeMessageDirectlyResultRequestHeader header, MessageExt messageExt, + CompletableFuture> responseFuture) { + assertEquals(consumeMessageDirectlyResultRequestHeader, consumeMessageDirectlyArgumentCaptor.getValue()); + assertEquals(consumeMessageDirectlyResultRequestHeader, header); + assertArrayEquals(consumeMessageDirectlyMessageExt.getBody(), messageExt.getBody()); + return CompletableFuture.completedFuture(null); + } + }; + + assertTrue(channel.writeAndFlush(checkTransactionRequest).isSuccess()); + assertTrue(channel.writeAndFlush(consumerRunningInfoRequest).isSuccess()); + assertTrue(channel.writeAndFlush(consumeMessageDirectlyResult).isSuccess()); + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/service/route/ClusterTopicRouteServiceTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/service/route/ClusterTopicRouteServiceTest.java new file mode 100644 index 00000000000..15d83483b9d --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/service/route/ClusterTopicRouteServiceTest.java @@ -0,0 +1,167 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.route; + +import com.github.benmanes.caffeine.cache.CacheLoader; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import com.google.common.net.HostAndPort; + +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.thread.ThreadPoolMonitor; +import org.apache.rocketmq.proxy.common.Address; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.service.BaseServiceTest; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.assertj.core.util.Lists; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowableOfType; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +public class ClusterTopicRouteServiceTest extends BaseServiceTest { + + private ClusterTopicRouteService topicRouteService; + + protected static final String BROKER2_NAME = "broker2"; + protected static final String BROKER2_ADDR = "127.0.0.2:10911"; + + @Before + public void before() throws Throwable { + super.before(); + this.topicRouteService = new ClusterTopicRouteService(this.mqClientAPIFactory); + + when(this.mqClientAPIExt.getTopicRouteInfoFromNameServer(eq(TOPIC), anyLong())).thenReturn(topicRouteData); + when(this.mqClientAPIExt.getTopicRouteInfoFromNameServer(eq(ERR_TOPIC), anyLong())).thenThrow(new MQClientException(ResponseCode.TOPIC_NOT_EXIST, "")); + + // build broker + BrokerData brokerData = new BrokerData(); + brokerData.setCluster(CLUSTER_NAME); + brokerData.setBrokerName(BROKER_NAME); + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(MixAll.MASTER_ID, BROKER_ADDR); + brokerData.setBrokerAddrs(brokerAddrs); + + // build broker2 + BrokerData broke2Data = new BrokerData(); + broke2Data.setCluster(CLUSTER_NAME); + broke2Data.setBrokerName(BROKER2_NAME); + HashMap broker2Addrs = new HashMap<>(); + broker2Addrs.put(MixAll.MASTER_ID, BROKER2_ADDR); + broke2Data.setBrokerAddrs(broker2Addrs); + + // add brokers + TopicRouteData brokerTopicRouteData = new TopicRouteData(); + brokerTopicRouteData.setBrokerDatas(Lists.newArrayList(brokerData, broke2Data)); + + // add queue data + QueueData queueData = new QueueData(); + queueData.setBrokerName(BROKER_NAME); + + QueueData queue2Data = new QueueData(); + queue2Data.setBrokerName(BROKER2_NAME); + brokerTopicRouteData.setQueueDatas(Lists.newArrayList(queueData, queue2Data)); + when(this.mqClientAPIExt.getTopicRouteInfoFromNameServer(eq(BROKER_NAME), anyLong())).thenReturn(brokerTopicRouteData); + when(this.mqClientAPIExt.getTopicRouteInfoFromNameServer(eq(BROKER2_NAME), anyLong())).thenReturn(brokerTopicRouteData); + } + + @Test + public void testGetCurrentMessageQueueView() throws Throwable { + ProxyContext ctx = ProxyContext.create(); + MQClientException exception = catchThrowableOfType(() -> this.topicRouteService.getCurrentMessageQueueView(ctx, ERR_TOPIC), MQClientException.class); + assertTrue(TopicRouteHelper.isTopicNotExistError(exception)); + assertEquals(1, this.topicRouteService.topicCache.asMap().size()); + + assertNotNull(this.topicRouteService.getCurrentMessageQueueView(ctx, TOPIC)); + assertEquals(2, this.topicRouteService.topicCache.asMap().size()); + } + + @Test + public void testGetBrokerAddr() throws Throwable { + ProxyContext ctx = ProxyContext.create(); + assertEquals(BROKER_ADDR, topicRouteService.getBrokerAddr(ctx, BROKER_NAME)); + assertEquals(BROKER2_ADDR, topicRouteService.getBrokerAddr(ctx, BROKER2_NAME)); + } + + @Test + public void testGetTopicRouteForProxy() throws Throwable { + ProxyContext ctx = ProxyContext.create(); + List
    addressList = Lists.newArrayList(new Address(Address.AddressScheme.IPv4, HostAndPort.fromParts("127.0.0.1", 8888))); + ProxyTopicRouteData proxyTopicRouteData = this.topicRouteService.getTopicRouteForProxy(ctx, addressList, TOPIC); + + assertEquals(1, proxyTopicRouteData.getBrokerDatas().size()); + assertEquals(addressList, proxyTopicRouteData.getBrokerDatas().get(0).getBrokerAddrs().get(MixAll.MASTER_ID)); + } + + @Test + public void testTopicRouteCaffeineCache() throws InterruptedException { + String key = "abc"; + String value = key; + final AtomicBoolean throwException = new AtomicBoolean(); + ThreadPoolExecutor cacheRefreshExecutor = ThreadPoolMonitor.createAndMonitor( + 10, 10, 30L, TimeUnit.SECONDS, "test", 10); + LoadingCache topicCache = Caffeine.newBuilder().maximumSize(30). + refreshAfterWrite(2, TimeUnit.SECONDS).executor(cacheRefreshExecutor).build(new CacheLoader() { + @Override public @Nullable String load(@NonNull String key) throws Exception { + try { + if (throwException.get()) { + throw new RuntimeException(); + } else { + throwException.set(true); + return value; + } + } catch (Exception e) { + if (TopicRouteHelper.isTopicNotExistError(e)) { + return ""; + } + throw e; + } + } + + @Override + public @Nullable String reload(@NonNull String key, @NonNull String oldValue) throws Exception { + try { + return load(key); + } catch (Exception e) { + return oldValue; + } + } + }); + assertThat(value).isEqualTo(topicCache.get(key)); + TimeUnit.SECONDS.sleep(5); + assertThat(value).isEqualTo(topicCache.get(key)); + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/service/route/LocalTopicRouteServiceTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/service/route/LocalTopicRouteServiceTest.java new file mode 100644 index 00000000000..1ad39a1db64 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/service/route/LocalTopicRouteServiceTest.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.route; + +import com.google.common.net.HostAndPort; +import java.util.ArrayList; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.apache.rocketmq.broker.BrokerController; +import org.apache.rocketmq.broker.topic.TopicConfigManager; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.constant.PermName; +import org.apache.rocketmq.proxy.common.Address; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.service.BaseServiceTest; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.assertj.core.util.Lists; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +public class LocalTopicRouteServiceTest extends BaseServiceTest { + + private static final String LOCAL_BROKER_NAME = "localBroker"; + private static final String LOCAL_CLUSTER_NAME = "localCluster"; + private static final String LOCAL_HOST = "127.0.0.2"; + private static final int LOCAL_PORT = 10911; + private static final String LOCAL_ADDR = LOCAL_HOST + ":" + LOCAL_PORT; + @Mock + private BrokerController brokerController; + @Mock + private TopicConfigManager topicConfigManager; + private ConcurrentMap topicConfigTable = new ConcurrentHashMap<>(); + private BrokerConfig brokerConfig = new BrokerConfig(); + private LocalTopicRouteService topicRouteService; + + @Before + public void before() throws Throwable { + super.before(); + this.brokerConfig.setBrokerName(LOCAL_BROKER_NAME); + this.brokerConfig.setBrokerClusterName(LOCAL_CLUSTER_NAME); + + when(this.brokerController.getBrokerAddr()).thenReturn(LOCAL_ADDR); + when(this.brokerController.getBrokerConfig()).thenReturn(this.brokerConfig); + when(this.brokerController.getTopicConfigManager()).thenReturn(this.topicConfigManager); + when(this.topicConfigManager.getTopicConfigTable()).thenReturn(this.topicConfigTable); + + this.topicRouteService = new LocalTopicRouteService(this.brokerController, this.mqClientAPIFactory); + + when(this.mqClientAPIExt.getTopicRouteInfoFromNameServer(eq(TOPIC), anyLong())).thenReturn(topicRouteData); + when(this.mqClientAPIExt.getTopicRouteInfoFromNameServer(eq(ERR_TOPIC), anyLong())).thenThrow(new MQClientException(ResponseCode.TOPIC_NOT_EXIST, "")); + } + + @Test + public void testGetCurrentMessageQueueView() throws Throwable { + ProxyContext ctx = ProxyContext.create(); + this.topicConfigTable.put(TOPIC, new TopicConfig(TOPIC, 3, 2, PermName.PERM_WRITE | PermName.PERM_READ)); + MessageQueueView messageQueueView = this.topicRouteService.getCurrentMessageQueueView(ctx, TOPIC); + assertEquals(3, messageQueueView.getReadSelector().getQueues().size()); + assertEquals(2, messageQueueView.getWriteSelector().getQueues().size()); + assertEquals(1, messageQueueView.getReadSelector().getBrokerActingQueues().size()); + assertEquals(1, messageQueueView.getWriteSelector().getBrokerActingQueues().size()); + + assertEquals(LOCAL_ADDR, messageQueueView.getReadSelector().selectOne(true).getBrokerAddr()); + assertEquals(LOCAL_BROKER_NAME, messageQueueView.getReadSelector().selectOne(true).getBrokerName()); + assertEquals(messageQueueView.getReadSelector().selectOne(true), messageQueueView.getWriteSelector().selectOne(true)); + } + + @Test + public void testGetTopicRouteForProxy() throws Throwable { + ProxyContext ctx = ProxyContext.create(); + ProxyTopicRouteData proxyTopicRouteData = this.topicRouteService.getTopicRouteForProxy(ctx, new ArrayList<>(), TOPIC); + + assertEquals(1, proxyTopicRouteData.getBrokerDatas().size()); + assertEquals( + Lists.newArrayList(new Address(Address.AddressScheme.IPv4, HostAndPort.fromParts( + HostAndPort.fromString(BROKER_ADDR).getHost(), + ConfigurationManager.getProxyConfig().getGrpcServerPort()))), + proxyTopicRouteData.getBrokerDatas().get(0).getBrokerAddrs().get(MixAll.MASTER_ID)); + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/service/route/MessageQueueSelectorTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/service/route/MessageQueueSelectorTest.java new file mode 100644 index 00000000000..d150f87c409 --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/service/route/MessageQueueSelectorTest.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.route; + +import org.apache.rocketmq.common.constant.PermName; +import org.apache.rocketmq.proxy.service.BaseServiceTest; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class MessageQueueSelectorTest extends BaseServiceTest { + + @Test + public void testReadMessageQueue() { + queueData.setPerm(PermName.PERM_READ); + queueData.setReadQueueNums(0); + MessageQueueSelector messageQueueSelector = new MessageQueueSelector(new TopicRouteWrapper(topicRouteData, TOPIC), null, true); + assertTrue(messageQueueSelector.getQueues().isEmpty()); + + queueData.setPerm(PermName.PERM_READ); + queueData.setReadQueueNums(3); + messageQueueSelector = new MessageQueueSelector(new TopicRouteWrapper(topicRouteData, TOPIC), null, true); + assertEquals(3, messageQueueSelector.getQueues().size()); + assertEquals(1, messageQueueSelector.getBrokerActingQueues().size()); + for (int i = 0; i < messageQueueSelector.getQueues().size(); i++) { + AddressableMessageQueue messageQueue = messageQueueSelector.getQueues().get(i); + assertEquals(i, messageQueue.getQueueId()); + } + + AddressableMessageQueue brokerQueue = messageQueueSelector.getQueueByBrokerName(BROKER_NAME); + assertEquals(brokerQueue, messageQueueSelector.getBrokerActingQueues().get(0)); + assertEquals(brokerQueue, messageQueueSelector.selectOne(true)); + assertEquals(brokerQueue, messageQueueSelector.selectOneByIndex(3, true)); + + AddressableMessageQueue queue = messageQueueSelector.selectOne(false); + messageQueueSelector.selectOne(false); + messageQueueSelector.selectOne(false); + assertEquals(queue, messageQueueSelector.selectOne(false)); + } + + @Test + public void testWriteMessageQueue() { + queueData.setPerm(PermName.PERM_WRITE); + queueData.setReadQueueNums(0); + MessageQueueSelector messageQueueSelector = new MessageQueueSelector(new TopicRouteWrapper(topicRouteData, TOPIC), null, false); + assertTrue(messageQueueSelector.getQueues().isEmpty()); + + queueData.setPerm(PermName.PERM_WRITE); + queueData.setWriteQueueNums(3); + messageQueueSelector = new MessageQueueSelector(new TopicRouteWrapper(topicRouteData, TOPIC), null, false); + assertEquals(3, messageQueueSelector.getQueues().size()); + assertEquals(1, messageQueueSelector.getBrokerActingQueues().size()); + for (int i = 0; i < messageQueueSelector.getQueues().size(); i++) { + AddressableMessageQueue messageQueue = messageQueueSelector.getQueues().get(i); + assertEquals(i, messageQueue.getQueueId()); + } + + AddressableMessageQueue brokerQueue = messageQueueSelector.getQueueByBrokerName(BROKER_NAME); + assertEquals(brokerQueue, messageQueueSelector.getBrokerActingQueues().get(0)); + assertEquals(brokerQueue, messageQueueSelector.selectOne(true)); + assertEquals(brokerQueue, messageQueueSelector.selectOneByIndex(3, true)); + + AddressableMessageQueue queue = messageQueueSelector.selectOne(false); + messageQueueSelector.selectOne(false); + messageQueueSelector.selectOne(false); + assertEquals(queue, messageQueueSelector.selectOne(false)); + } +} \ No newline at end of file diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/service/sysmessage/HeartbeatSyncerTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/service/sysmessage/HeartbeatSyncerTest.java new file mode 100644 index 00000000000..9a2c5e3437d --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/service/sysmessage/HeartbeatSyncerTest.java @@ -0,0 +1,436 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.sysmessage; + +import apache.rocketmq.v2.FilterExpression; +import apache.rocketmq.v2.FilterType; +import apache.rocketmq.v2.Resource; +import apache.rocketmq.v2.Settings; +import apache.rocketmq.v2.Subscription; +import apache.rocketmq.v2.SubscriptionEntry; +import com.google.common.collect.Sets; +import io.netty.channel.Channel; +import io.netty.channel.ChannelId; +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.rocketmq.broker.client.ClientChannelInfo; +import org.apache.rocketmq.broker.client.ConsumerGroupEvent; +import org.apache.rocketmq.broker.client.ConsumerManager; +import org.apache.rocketmq.client.impl.mqclient.MQClientAPIExt; +import org.apache.rocketmq.client.impl.mqclient.MQClientAPIFactory; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.config.InitConfigTest; +import org.apache.rocketmq.proxy.grpc.v2.channel.GrpcChannelManager; +import org.apache.rocketmq.proxy.grpc.v2.channel.GrpcClientChannel; +import org.apache.rocketmq.proxy.grpc.v2.common.GrpcClientSettingsManager; +import org.apache.rocketmq.proxy.remoting.RemotingProxyOutClient; +import org.apache.rocketmq.proxy.remoting.channel.RemotingChannel; +import org.apache.rocketmq.proxy.service.admin.AdminService; +import org.apache.rocketmq.proxy.service.channel.SimpleChannel; +import org.apache.rocketmq.proxy.service.relay.ProxyRelayService; +import org.apache.rocketmq.proxy.service.route.MessageQueueView; +import org.apache.rocketmq.proxy.service.route.TopicRouteService; +import org.apache.rocketmq.remoting.protocol.LanguageCode; +import org.apache.rocketmq.remoting.protocol.filter.FilterAPI; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.assertj.core.util.Lists; +import org.jetbrains.annotations.NotNull; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class HeartbeatSyncerTest extends InitConfigTest { + @Mock + private TopicRouteService topicRouteService; + @Mock + private AdminService adminService; + @Mock + private ConsumerManager consumerManager; + @Mock + private MQClientAPIFactory mqClientAPIFactory; + @Mock + private MQClientAPIExt mqClientAPIExt; + @Mock + private ProxyRelayService proxyRelayService; + + private String clientId; + private final String remoteAddress = "10.152.39.53:9768"; + private final String localAddress = "11.193.0.1:1210"; + private final String clusterName = "cluster"; + private final String brokerName = "broker-01"; + + @Before + public void before() throws Throwable { + super.before(); + this.clientId = RandomStringUtils.randomAlphabetic(10); + when(mqClientAPIFactory.getClient()).thenReturn(mqClientAPIExt); + + { + TopicRouteData topicRouteData = new TopicRouteData(); + QueueData queueData = new QueueData(); + queueData.setReadQueueNums(8); + queueData.setWriteQueueNums(8); + queueData.setPerm(6); + queueData.setBrokerName(brokerName); + topicRouteData.getQueueDatas().add(queueData); + BrokerData brokerData = new BrokerData(); + brokerData.setCluster(clusterName); + brokerData.setBrokerName(brokerName); + HashMap brokerAddr = new HashMap<>(); + brokerAddr.put(0L, "127.0.0.1:10911"); + brokerData.setBrokerAddrs(brokerAddr); + topicRouteData.getBrokerDatas().add(brokerData); + MessageQueueView messageQueueView = new MessageQueueView("foo", topicRouteData, null); + when(this.topicRouteService.getAllMessageQueueView(any(), anyString())).thenReturn(messageQueueView); + } + } + + @Test + public void testSyncGrpcV2Channel() throws Exception { + String consumerGroup = "consumerGroup"; + GrpcClientSettingsManager grpcClientSettingsManager = mock(GrpcClientSettingsManager.class); + GrpcChannelManager grpcChannelManager = mock(GrpcChannelManager.class); + GrpcClientChannel grpcClientChannel = new GrpcClientChannel( + proxyRelayService, grpcClientSettingsManager, grpcChannelManager, + ProxyContext.create().setRemoteAddress(remoteAddress).setLocalAddress(localAddress), + clientId); + ClientChannelInfo clientChannelInfo = new ClientChannelInfo( + grpcClientChannel, + clientId, + LanguageCode.JAVA, + 5 + ); + + ArgumentCaptor messageArgumentCaptor = ArgumentCaptor.forClass(Message.class); + SendResult sendResult = new SendResult(); + sendResult.setSendStatus(SendStatus.SEND_OK); + doReturn(CompletableFuture.completedFuture(sendResult)).when(this.mqClientAPIExt) + .sendMessageAsync(anyString(), anyString(), messageArgumentCaptor.capture(), any(), anyLong()); + + Settings settings = Settings.newBuilder() + .setSubscription(Subscription.newBuilder() + .addSubscriptions(SubscriptionEntry.newBuilder() + .setTopic(Resource.newBuilder().setName("topic").build()) + .setExpression(FilterExpression.newBuilder() + .setType(FilterType.TAG) + .setExpression("tag") + .build()) + .build()) + .build()) + .build(); + when(grpcClientSettingsManager.getRawClientSettings(eq(clientId))).thenReturn(settings); + + HeartbeatSyncer heartbeatSyncer = new HeartbeatSyncer(topicRouteService, adminService, consumerManager, mqClientAPIFactory, null); + heartbeatSyncer.onConsumerRegister( + consumerGroup, + clientChannelInfo, + ConsumeType.CONSUME_PASSIVELY, + MessageModel.CLUSTERING, + ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET, + Sets.newHashSet(FilterAPI.buildSubscriptionData("topic", "tag")) + ); + + await().atMost(Duration.ofSeconds(3)).until(() -> !messageArgumentCaptor.getAllValues().isEmpty()); + heartbeatSyncer.consumeMessage(Lists.newArrayList(convertFromMessage(messageArgumentCaptor.getValue())), null); + verify(consumerManager, never()).registerConsumer(anyString(), any(), any(), any(), any(), any(), anyBoolean()); + + String localServeAddr = ConfigurationManager.getProxyConfig().getLocalServeAddr(); + // change local serve addr, to simulate other proxy receive messages + heartbeatSyncer.localProxyId = RandomStringUtils.randomAlphabetic(10); + ArgumentCaptor syncChannelInfoArgumentCaptor = ArgumentCaptor.forClass(ClientChannelInfo.class); + doReturn(true).when(consumerManager).registerConsumer(anyString(), syncChannelInfoArgumentCaptor.capture(), any(), any(), any(), any(), anyBoolean()); + + heartbeatSyncer.consumeMessage(Lists.newArrayList(convertFromMessage(messageArgumentCaptor.getValue())), null); + heartbeatSyncer.consumeMessage(Lists.newArrayList(convertFromMessage(messageArgumentCaptor.getValue())), null); + assertEquals(2, syncChannelInfoArgumentCaptor.getAllValues().size()); + List channelInfoList = syncChannelInfoArgumentCaptor.getAllValues(); + assertSame(channelInfoList.get(0).getChannel(), channelInfoList.get(1).getChannel()); + assertEquals(settings, GrpcClientChannel.parseChannelExtendAttribute(channelInfoList.get(0).getChannel())); + assertEquals(settings, GrpcClientChannel.parseChannelExtendAttribute(channelInfoList.get(1).getChannel())); + + // start test sync client unregister + // reset localServeAddr + ConfigurationManager.getProxyConfig().setLocalServeAddr(localServeAddr); + heartbeatSyncer.onConsumerUnRegister(consumerGroup, clientChannelInfo); + await().atMost(Duration.ofSeconds(3)).until(() -> messageArgumentCaptor.getAllValues().size() == 2); + + ArgumentCaptor syncUnRegisterChannelInfoArgumentCaptor = ArgumentCaptor.forClass(ClientChannelInfo.class); + doNothing().when(consumerManager).unregisterConsumer(anyString(), syncUnRegisterChannelInfoArgumentCaptor.capture(), anyBoolean()); + + // change local serve addr, to simulate other proxy receive messages + heartbeatSyncer.localProxyId = RandomStringUtils.randomAlphabetic(10); + heartbeatSyncer.consumeMessage(Lists.newArrayList(convertFromMessage(messageArgumentCaptor.getAllValues().get(1))), null); + assertSame(channelInfoList.get(0).getChannel(), syncUnRegisterChannelInfoArgumentCaptor.getValue().getChannel()); + } + + @Test + public void testSyncRemotingChannel() throws Exception { + String consumerGroup = "consumerGroup"; + String consumerGroup2 = "consumerGroup2"; + Channel channel = createMockChannel(); + Set subscriptionDataSet = new HashSet<>(); + subscriptionDataSet.add(FilterAPI.buildSubscriptionData("topic", "tagSub")); + Set subscriptionDataSet2 = new HashSet<>(); + subscriptionDataSet2.add(FilterAPI.buildSubscriptionData("topic2", "tagSub2")); + RemotingProxyOutClient remotingProxyOutClient = mock(RemotingProxyOutClient.class); + RemotingChannel remotingChannel = new RemotingChannel(remotingProxyOutClient, proxyRelayService, channel, clientId, subscriptionDataSet); + ClientChannelInfo clientChannelInfo = new ClientChannelInfo( + remotingChannel, + clientId, + LanguageCode.JAVA, + 4 + ); + RemotingChannel remotingChannel2 = new RemotingChannel(remotingProxyOutClient, proxyRelayService, channel, clientId, subscriptionDataSet2); + ClientChannelInfo clientChannelInfo2 = new ClientChannelInfo( + remotingChannel2, + clientId, + LanguageCode.JAVA, + 4 + ); + + HeartbeatSyncer heartbeatSyncer = new HeartbeatSyncer(topicRouteService, adminService, consumerManager, mqClientAPIFactory, null); + SendResult okSendResult = new SendResult(); + okSendResult.setSendStatus(SendStatus.SEND_OK); + { + ArgumentCaptor messageArgumentCaptor = ArgumentCaptor.forClass(Message.class); + doReturn(CompletableFuture.completedFuture(okSendResult)).when(this.mqClientAPIExt) + .sendMessageAsync(anyString(), anyString(), messageArgumentCaptor.capture(), any(), anyLong()); + + heartbeatSyncer.onConsumerRegister( + consumerGroup, + clientChannelInfo, + ConsumeType.CONSUME_PASSIVELY, + MessageModel.CLUSTERING, + ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET, + subscriptionDataSet + ); + heartbeatSyncer.onConsumerRegister( + consumerGroup2, + clientChannelInfo2, + ConsumeType.CONSUME_PASSIVELY, + MessageModel.CLUSTERING, + ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET, + subscriptionDataSet2 + ); + + await().atMost(Duration.ofSeconds(3)).until(() -> messageArgumentCaptor.getAllValues().size() == 2); + heartbeatSyncer.consumeMessage(convertFromMessage(messageArgumentCaptor.getAllValues()), null); + verify(consumerManager, never()).registerConsumer(anyString(), any(), any(), any(), any(), any(), anyBoolean()); + + // change local serve addr, to simulate other proxy receive messages + heartbeatSyncer.localProxyId = RandomStringUtils.randomAlphabetic(10); + ArgumentCaptor syncChannelInfoArgumentCaptor = ArgumentCaptor.forClass(ClientChannelInfo.class); + doReturn(true).when(consumerManager).registerConsumer(anyString(), syncChannelInfoArgumentCaptor.capture(), any(), any(), any(), any(), anyBoolean()); + + heartbeatSyncer.consumeMessage(convertFromMessage(messageArgumentCaptor.getAllValues()), null); + heartbeatSyncer.consumeMessage(convertFromMessage(messageArgumentCaptor.getAllValues()), null); + /* + data in syncChannelInfoArgumentCaptor will be like: + 1st, data of group1 + 2nd, data of group2 + 3rd, data of group1 + 4th, data of group2 + */ + assertEquals(4, syncChannelInfoArgumentCaptor.getAllValues().size()); + List channelInfoList = syncChannelInfoArgumentCaptor.getAllValues(); + assertSame(channelInfoList.get(0).getChannel(), channelInfoList.get(2).getChannel()); + assertNotSame(channelInfoList.get(0).getChannel(), channelInfoList.get(1).getChannel()); + Set> checkSubscriptionDatas = new HashSet<>(); + checkSubscriptionDatas.add(RemotingChannel.parseChannelExtendAttribute(channelInfoList.get(0).getChannel())); + checkSubscriptionDatas.add(RemotingChannel.parseChannelExtendAttribute(channelInfoList.get(1).getChannel())); + assertTrue(checkSubscriptionDatas.contains(subscriptionDataSet)); + assertTrue(checkSubscriptionDatas.contains(subscriptionDataSet2)); + } + + { + // start test sync client unregister + // reset localServeAddr + ArgumentCaptor messageArgumentCaptor = ArgumentCaptor.forClass(Message.class); + doReturn(CompletableFuture.completedFuture(okSendResult)).when(this.mqClientAPIExt) + .sendMessageAsync(anyString(), anyString(), messageArgumentCaptor.capture(), any(), anyLong()); + heartbeatSyncer.onConsumerUnRegister(consumerGroup, clientChannelInfo); + heartbeatSyncer.onConsumerUnRegister(consumerGroup2, clientChannelInfo2); + await().atMost(Duration.ofSeconds(3)).until(() -> messageArgumentCaptor.getAllValues().size() == 2); + + ArgumentCaptor syncUnRegisterChannelInfoArgumentCaptor = ArgumentCaptor.forClass(ClientChannelInfo.class); + doNothing().when(consumerManager).unregisterConsumer(anyString(), syncUnRegisterChannelInfoArgumentCaptor.capture(), anyBoolean()); + + // change local serve addr, to simulate other proxy receive messages + heartbeatSyncer.localProxyId = RandomStringUtils.randomAlphabetic(10); + heartbeatSyncer.consumeMessage(convertFromMessage(messageArgumentCaptor.getAllValues()), null); + List channelInfoList = syncUnRegisterChannelInfoArgumentCaptor.getAllValues(); + assertNotSame(channelInfoList.get(0).getChannel(), channelInfoList.get(1).getChannel()); + Set> checkSubscriptionDatas = new HashSet<>(); + checkSubscriptionDatas.add(RemotingChannel.parseChannelExtendAttribute(channelInfoList.get(0).getChannel())); + checkSubscriptionDatas.add(RemotingChannel.parseChannelExtendAttribute(channelInfoList.get(1).getChannel())); + assertTrue(checkSubscriptionDatas.contains(subscriptionDataSet)); + assertTrue(checkSubscriptionDatas.contains(subscriptionDataSet2)); + } + } + + @Test + public void testProcessConsumerGroupEventForRemoting() { + String consumerGroup = "consumerGroup"; + Channel channel = createMockChannel(); + RemotingProxyOutClient remotingProxyOutClient = mock(RemotingProxyOutClient.class); + RemotingChannel remotingChannel = new RemotingChannel(remotingProxyOutClient, proxyRelayService, channel, clientId, Collections.emptySet()); + ClientChannelInfo clientChannelInfo = new ClientChannelInfo( + remotingChannel, + clientId, + LanguageCode.JAVA, + 4 + ); + + testProcessConsumerGroupEvent(consumerGroup, clientChannelInfo); + } + + @Test + public void testProcessConsumerGroupEventForGrpcV2() { + String consumerGroup = "consumerGroup"; + GrpcClientSettingsManager grpcClientSettingsManager = mock(GrpcClientSettingsManager.class); + GrpcChannelManager grpcChannelManager = mock(GrpcChannelManager.class); + GrpcClientChannel grpcClientChannel = new GrpcClientChannel( + proxyRelayService, grpcClientSettingsManager, grpcChannelManager, + ProxyContext.create().setRemoteAddress(remoteAddress).setLocalAddress(localAddress), + clientId); + ClientChannelInfo clientChannelInfo = new ClientChannelInfo( + grpcClientChannel, + clientId, + LanguageCode.JAVA, + 5 + ); + + testProcessConsumerGroupEvent(consumerGroup, clientChannelInfo); + } + + private void testProcessConsumerGroupEvent(String consumerGroup, ClientChannelInfo clientChannelInfo) { + HeartbeatSyncer heartbeatSyncer = new HeartbeatSyncer(topicRouteService, adminService, consumerManager, mqClientAPIFactory, null); + SendResult okSendResult = new SendResult(); + okSendResult.setSendStatus(SendStatus.SEND_OK); + + ArgumentCaptor messageArgumentCaptor = ArgumentCaptor.forClass(Message.class); + doReturn(CompletableFuture.completedFuture(okSendResult)).when(this.mqClientAPIExt) + .sendMessageAsync(anyString(), anyString(), messageArgumentCaptor.capture(), any(), anyLong()); + + heartbeatSyncer.onConsumerRegister( + consumerGroup, + clientChannelInfo, + ConsumeType.CONSUME_PASSIVELY, + MessageModel.CLUSTERING, + ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET, + Collections.emptySet() + ); + await().atMost(Duration.ofSeconds(3)).until(() -> messageArgumentCaptor.getAllValues().size() == 1); + + // change local serve addr, to simulate other proxy receive messages + heartbeatSyncer.localProxyId = RandomStringUtils.randomAlphabetic(10); + ArgumentCaptor channelInfoArgumentCaptor = ArgumentCaptor.forClass(ClientChannelInfo.class); + doReturn(true).when(consumerManager).registerConsumer(anyString(), channelInfoArgumentCaptor.capture(), any(), any(), any(), any(), anyBoolean()); + + heartbeatSyncer.consumeMessage(convertFromMessage(messageArgumentCaptor.getAllValues()), null); + assertEquals(1, heartbeatSyncer.remoteChannelMap.size()); + + heartbeatSyncer.processConsumerGroupEvent(ConsumerGroupEvent.CLIENT_UNREGISTER, consumerGroup, channelInfoArgumentCaptor.getValue()); + assertTrue(heartbeatSyncer.remoteChannelMap.isEmpty()); + } + + private MessageExt convertFromMessage(Message message) { + MessageExt messageExt = new MessageExt(); + messageExt.setTopic(message.getTopic()); + messageExt.setBody(message.getBody()); + return messageExt; + } + + private List convertFromMessage(List message) { + return message.stream().map(this::convertFromMessage).collect(Collectors.toList()); + } + + private Channel createMockChannel() { + return new MockChannel(RandomStringUtils.randomAlphabetic(10)); + } + + private class MockChannel extends SimpleChannel { + + public MockChannel(String channelId) { + super(null, new MockChannelId(channelId), HeartbeatSyncerTest.this.remoteAddress, HeartbeatSyncerTest.this.localAddress); + } + } + + private static class MockChannelId implements ChannelId { + + private final String channelId; + + public MockChannelId(String channelId) { + this.channelId = channelId; + } + + @Override + public String asShortText() { + return channelId; + } + + @Override + public String asLongText() { + return channelId; + } + + @Override + public int compareTo(@NotNull ChannelId o) { + return this.channelId.compareTo(o.asLongText()); + } + } +} \ No newline at end of file diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/service/transaction/AbstractTransactionServiceTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/service/transaction/AbstractTransactionServiceTest.java new file mode 100644 index 00000000000..81de5ec843a --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/service/transaction/AbstractTransactionServiceTest.java @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.transaction; + +import java.util.List; +import java.util.Random; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.InitConfigTest; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +public class AbstractTransactionServiceTest extends InitConfigTest { + + private static final String BROKER_NAME = "mockBroker"; + private static final String PRODUCER_GROUP = "producerGroup"; + private static final Random RANDOM = new Random(); + private final ProxyContext ctx = ProxyContext.createForInner(this.getClass()); + + public static class MockAbstractTransactionServiceTest extends AbstractTransactionService { + + @Override + protected String getBrokerNameByAddr(String brokerAddr) { + return BROKER_NAME; + } + + @Override + public void addTransactionSubscription(ProxyContext ctx, String group, List topicList) { + + } + + @Override + public void addTransactionSubscription(ProxyContext ctx, String group, String topic) { + + } + + @Override + public void replaceTransactionSubscription(ProxyContext ctx, String group, List topicList) { + + } + + @Override + public void unSubscribeAllTransactionTopic(ProxyContext ctx, String group) { + + } + } + + private TransactionService transactionService; + + @Before + public void before() throws Throwable { + super.before(); + this.transactionService = new MockAbstractTransactionServiceTest(); + } + + @Test + public void testAddAndGenEndHeader() { + Message message = new Message(); + message.putUserProperty(MessageConst.PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS, "30"); + String txId = MessageClientIDSetter.createUniqID(); + + TransactionData transactionData = transactionService.addTransactionDataByBrokerName( + ctx, + BROKER_NAME, + PRODUCER_GROUP, + RANDOM.nextLong(), + RANDOM.nextLong(), + txId, + message + ); + assertNotNull(transactionData); + + EndTransactionRequestData requestData = transactionService.genEndTransactionRequestHeader( + ctx, + PRODUCER_GROUP, + MessageSysFlag.TRANSACTION_COMMIT_TYPE, + true, + txId, + txId + ); + + assertEquals(BROKER_NAME, requestData.getBrokerName()); + assertEquals(BROKER_NAME, transactionData.getBrokerName()); + assertEquals(transactionData.getCommitLogOffset(), requestData.getRequestHeader().getCommitLogOffset().longValue()); + assertEquals(transactionData.getTranStateTableOffset(), requestData.getRequestHeader().getTranStateTableOffset().longValue()); + + assertNull(transactionService.genEndTransactionRequestHeader( + ctx, + "group", + MessageSysFlag.TRANSACTION_COMMIT_TYPE, + true, + txId, + txId + )); + } + + @Test + public void testOnSendCheckTransactionStateFailedFailed() { + Message message = new Message(); + message.putUserProperty(MessageConst.PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS, "30"); + String txId = MessageClientIDSetter.createUniqID(); + + TransactionData transactionData = transactionService.addTransactionDataByBrokerName( + ctx, + BROKER_NAME, + PRODUCER_GROUP, + RANDOM.nextLong(), + RANDOM.nextLong(), + txId, + message + ); + transactionService.onSendCheckTransactionStateFailed(ProxyContext.createForInner(this.getClass()), PRODUCER_GROUP, transactionData); + assertNull(transactionService.genEndTransactionRequestHeader( + ctx, + PRODUCER_GROUP, + MessageSysFlag.TRANSACTION_COMMIT_TYPE, + true, + txId, + txId + )); + } +} \ No newline at end of file diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/service/transaction/ClusterTransactionServiceTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/service/transaction/ClusterTransactionServiceTest.java new file mode 100644 index 00000000000..91af74cbefc --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/service/transaction/ClusterTransactionServiceTest.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.transaction; + +import java.time.Duration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import org.apache.rocketmq.broker.client.ProducerManager; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.proxy.common.ProxyContext; +import org.apache.rocketmq.proxy.config.ConfigurationManager; +import org.apache.rocketmq.proxy.service.BaseServiceTest; +import org.apache.rocketmq.proxy.service.route.MessageQueueView; +import org.apache.rocketmq.remoting.protocol.heartbeat.HeartbeatData; +import org.apache.rocketmq.remoting.protocol.heartbeat.ProducerData; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.QueueData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.assertj.core.util.Lists; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; + +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +public class ClusterTransactionServiceTest extends BaseServiceTest { + + @Mock + private ProducerManager producerManager; + private ProxyContext ctx = ProxyContext.create(); + private ClusterTransactionService clusterTransactionService; + + @Before + public void before() throws Throwable { + super.before(); + this.clusterTransactionService = new ClusterTransactionService(this.topicRouteService, this.producerManager, + this.mqClientAPIFactory); + + MessageQueueView messageQueueView = new MessageQueueView(TOPIC, topicRouteData, null); + when(this.topicRouteService.getAllMessageQueueView(any(), anyString())) + .thenReturn(messageQueueView); + + when(mqClientAPIFactory.getClient()).thenReturn(mqClientAPIExt); + } + + @Test + public void testAddTransactionSubscription() { + this.clusterTransactionService.addTransactionSubscription(ctx, GROUP, TOPIC); + + assertEquals(1, this.clusterTransactionService.getGroupClusterData().size()); + assertEquals(CLUSTER_NAME, this.clusterTransactionService.getGroupClusterData().get(GROUP).stream().findAny().get().getCluster()); + } + + @Test + public void testAddTransactionSubscriptionTopicList() { + this.clusterTransactionService.addTransactionSubscription(ctx, GROUP, Lists.newArrayList(TOPIC + 1, TOPIC + 2)); + + assertEquals(1, this.clusterTransactionService.getGroupClusterData().size()); + assertEquals(CLUSTER_NAME, this.clusterTransactionService.getGroupClusterData().get(GROUP).stream().findAny().get().getCluster()); + } + + @Test + public void testReplaceTransactionSubscription() { + this.clusterTransactionService.addTransactionSubscription(ctx, GROUP, TOPIC); + + assertEquals(1, this.clusterTransactionService.getGroupClusterData().size()); + assertEquals(CLUSTER_NAME, this.clusterTransactionService.getGroupClusterData().get(GROUP).stream().findAny().get().getCluster()); + + this.brokerData.setCluster(CLUSTER_NAME + 1); + this.clusterTransactionService.replaceTransactionSubscription(ctx, GROUP, Lists.newArrayList(TOPIC + 1)); + assertEquals(1, this.clusterTransactionService.getGroupClusterData().size()); + assertEquals(CLUSTER_NAME + 1, this.clusterTransactionService.getGroupClusterData().get(GROUP).stream().findAny().get().getCluster()); + } + + @Test + public void testUnSubscribeAllTransactionTopic() { + this.clusterTransactionService.addTransactionSubscription(ctx, GROUP, TOPIC); + this.clusterTransactionService.unSubscribeAllTransactionTopic(ctx, GROUP); + + assertEquals(0, this.clusterTransactionService.getGroupClusterData().size()); + } + + @Test + public void testScanProducerHeartBeat() throws Exception { + when(this.producerManager.groupOnline(anyString())).thenReturn(true); + + Mockito.reset(this.topicRouteService); + String brokerName2 = "broker-2-01"; + String clusterName2 = "broker-2"; + String brokerAddr2 = "127.0.0.2:10911"; + + BrokerData brokerData = new BrokerData(); + QueueData queueData = new QueueData(); + queueData.setBrokerName(brokerName2); + brokerData.setCluster(clusterName2); + brokerData.setBrokerName(brokerName2); + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(MixAll.MASTER_ID, brokerName2); + brokerData.setBrokerAddrs(brokerAddrs); + topicRouteData.getQueueDatas().add(queueData); + topicRouteData.getBrokerDatas().add(brokerData); + when(this.topicRouteService.getAllMessageQueueView(any(), eq(TOPIC))).thenReturn(new MessageQueueView(TOPIC, topicRouteData, null)); + + TopicRouteData clusterTopicRouteData = new TopicRouteData(); + QueueData clusterQueueData = new QueueData(); + BrokerData clusterBrokerData = new BrokerData(); + + clusterQueueData.setBrokerName(BROKER_NAME); + clusterTopicRouteData.setQueueDatas(Lists.newArrayList(clusterQueueData)); + clusterBrokerData.setCluster(CLUSTER_NAME); + clusterBrokerData.setBrokerName(BROKER_NAME); + brokerAddrs = new HashMap<>(); + brokerAddrs.put(MixAll.MASTER_ID, BROKER_ADDR); + clusterBrokerData.setBrokerAddrs(brokerAddrs); + clusterTopicRouteData.setBrokerDatas(Lists.newArrayList(clusterBrokerData)); + when(this.topicRouteService.getAllMessageQueueView(any(), eq(CLUSTER_NAME))).thenReturn(new MessageQueueView(CLUSTER_NAME, clusterTopicRouteData, null)); + + TopicRouteData clusterTopicRouteData2 = new TopicRouteData(); + QueueData clusterQueueData2 = new QueueData(); + BrokerData clusterBrokerData2 = new BrokerData(); + + clusterQueueData2.setBrokerName(brokerName2); + clusterTopicRouteData2.setQueueDatas(Lists.newArrayList(clusterQueueData2)); + clusterBrokerData2.setCluster(clusterName2); + clusterBrokerData2.setBrokerName(brokerName2); + brokerAddrs = new HashMap<>(); + brokerAddrs.put(MixAll.MASTER_ID, brokerAddr2); + clusterBrokerData2.setBrokerAddrs(brokerAddrs); + clusterTopicRouteData2.setBrokerDatas(Lists.newArrayList(clusterBrokerData2)); + when(this.topicRouteService.getAllMessageQueueView(any(), eq(clusterName2))).thenReturn(new MessageQueueView(clusterName2, clusterTopicRouteData2, null)); + + ConfigurationManager.getProxyConfig().setTransactionHeartbeatBatchNum(2); + this.clusterTransactionService.start(); + Set groupSet = new HashSet<>(); + + for (int i = 0; i < 3; i++) { + groupSet.add(GROUP + i); + this.clusterTransactionService.addTransactionSubscription(ctx, GROUP + i, TOPIC); + } + + ArgumentCaptor brokerAddrArgumentCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor heartbeatDataArgumentCaptor = ArgumentCaptor.forClass(HeartbeatData.class); + when(mqClientAPIExt.sendHeartbeatOneway( + brokerAddrArgumentCaptor.capture(), + heartbeatDataArgumentCaptor.capture(), + anyLong() + )).thenReturn(CompletableFuture.completedFuture(null)); + + this.clusterTransactionService.scanProducerHeartBeat(); + + await().atMost(Duration.ofSeconds(1)).until(() -> brokerAddrArgumentCaptor.getAllValues().size() == 4); + + assertEquals(Lists.newArrayList(BROKER_ADDR, BROKER_ADDR, brokerAddr2, brokerAddr2), + brokerAddrArgumentCaptor.getAllValues().stream().sorted().collect(Collectors.toList())); + + List heartbeatDataList = heartbeatDataArgumentCaptor.getAllValues(); + + for (final HeartbeatData heartbeatData : heartbeatDataList) { + for (ProducerData producerData : heartbeatData.getProducerDataSet()) { + groupSet.remove(producerData.getGroupName()); + } + } + + assertTrue(groupSet.isEmpty()); + assertEquals(brokerName2, this.clusterTransactionService.getBrokerNameByAddr(brokerAddr2)); + assertEquals(BROKER_NAME, this.clusterTransactionService.getBrokerNameByAddr(BROKER_ADDR)); + } +} diff --git a/proxy/src/test/java/org/apache/rocketmq/proxy/service/transaction/TransactionDataManagerTest.java b/proxy/src/test/java/org/apache/rocketmq/proxy/service/transaction/TransactionDataManagerTest.java new file mode 100644 index 00000000000..f92a7846c5d --- /dev/null +++ b/proxy/src/test/java/org/apache/rocketmq/proxy/service/transaction/TransactionDataManagerTest.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.proxy.service.transaction; + +import java.time.Duration; +import java.util.Random; +import org.apache.commons.lang3.time.StopWatch; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.proxy.config.InitConfigTest; +import org.junit.After; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; + +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class TransactionDataManagerTest extends InitConfigTest { + private static final String PRODUCER_GROUP = "producerGroup"; + private static final Random RANDOM = new Random(); + private TransactionDataManager transactionDataManager; + + @Before + public void before() throws Throwable { + super.before(); + this.transactionDataManager = new TransactionDataManager(); + } + + @After + public void after() { + super.after(); + } + + @Test + public void testAddAndRemove() { + TransactionData transactionData1 = createTransactionData(); + TransactionData transactionData2 = createTransactionData(transactionData1.getTransactionId()); + this.transactionDataManager.addTransactionData(PRODUCER_GROUP, transactionData1.getTransactionId(), transactionData1); + this.transactionDataManager.addTransactionData(PRODUCER_GROUP, transactionData1.getTransactionId(), transactionData2); + + assertEquals(1, this.transactionDataManager.transactionIdDataMap.size()); + assertEquals(2, this.transactionDataManager.transactionIdDataMap.get( + transactionDataManager.buildKey(PRODUCER_GROUP, transactionData1.getTransactionId())).size()); + + this.transactionDataManager.removeTransactionData(PRODUCER_GROUP, transactionData1.getTransactionId(), transactionData1); + assertEquals(1, this.transactionDataManager.transactionIdDataMap.size()); + this.transactionDataManager.removeTransactionData(PRODUCER_GROUP, transactionData1.getTransactionId(), transactionData2); + assertEquals(0, this.transactionDataManager.transactionIdDataMap.size()); + } + + @Test + public void testPoll() { + String txId = MessageClientIDSetter.createUniqID(); + TransactionData transactionData1 = createTransactionData(txId, System.currentTimeMillis() - Duration.ofMinutes(2).toMillis()); + TransactionData transactionData2 = createTransactionData(txId); + + this.transactionDataManager.addTransactionData(PRODUCER_GROUP, txId, transactionData1); + this.transactionDataManager.addTransactionData(PRODUCER_GROUP, txId, transactionData2); + + TransactionData resTransactionData = this.transactionDataManager.pollNoExpireTransactionData(PRODUCER_GROUP, txId); + assertSame(transactionData2, resTransactionData); + assertNull(this.transactionDataManager.pollNoExpireTransactionData(PRODUCER_GROUP, txId)); + } + + @Test + public void testCleanExpire() { + String txId = MessageClientIDSetter.createUniqID(); + this.transactionDataManager.addTransactionData(PRODUCER_GROUP, txId, + createTransactionData(txId, System.currentTimeMillis(), Duration.ofMillis(100).toMillis())); + this.transactionDataManager.addTransactionData(PRODUCER_GROUP, txId, + createTransactionData(txId, System.currentTimeMillis(), Duration.ofMillis(500).toMillis())); + + this.transactionDataManager.addTransactionData(PRODUCER_GROUP, MessageClientIDSetter.createUniqID(), + createTransactionData(txId, System.currentTimeMillis(), Duration.ofMillis(1000).toMillis())); + + await().atMost(Duration.ofSeconds(2)).until(() -> { + this.transactionDataManager.cleanExpireTransactionData(); + return this.transactionDataManager.transactionIdDataMap.isEmpty(); + }); + } + + @Test + public void testWaitTransactionDataClear() throws InterruptedException { + // Skip this test case on Mac as it's not stable enough. + Assume.assumeFalse(MixAll.isMac()); + String txId = MessageClientIDSetter.createUniqID(); + this.transactionDataManager.addTransactionData(PRODUCER_GROUP, txId, + createTransactionData(txId, System.currentTimeMillis(), Duration.ofMillis(100).toMillis())); + this.transactionDataManager.addTransactionData(PRODUCER_GROUP, txId, + createTransactionData(txId, System.currentTimeMillis(), Duration.ofMillis(500).toMillis())); + + this.transactionDataManager.addTransactionData(PRODUCER_GROUP, MessageClientIDSetter.createUniqID(), + createTransactionData(txId, System.currentTimeMillis(), Duration.ofMillis(1000).toMillis())); + + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + this.transactionDataManager.waitTransactionDataClear(); + stopWatch.stop(); + assertTrue(Math.abs(stopWatch.getTime() - 1000) <= 50); + } + + private static TransactionData createTransactionData() { + return createTransactionData(MessageClientIDSetter.createUniqID()); + } + + private static TransactionData createTransactionData(String txId) { + return createTransactionData(txId, System.currentTimeMillis()); + } + + private static TransactionData createTransactionData(String txId, long checkTimestamp) { + return createTransactionData(txId, checkTimestamp, Duration.ofMinutes(1).toMillis()); + } + + private static TransactionData createTransactionData(String txId, long checkTimestamp, long checkImmunityTime) { + return new TransactionData( + "brokerName", + RANDOM.nextLong(), + RANDOM.nextLong(), + txId, + checkTimestamp, + checkImmunityTime + ); + } +} \ No newline at end of file diff --git a/proxy/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/proxy/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 00000000000..ca6ee9cea8e --- /dev/null +++ b/proxy/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/proxy/src/test/resources/rmq-proxy-home/conf/broker.conf b/proxy/src/test/resources/rmq-proxy-home/conf/broker.conf new file mode 100644 index 00000000000..0c0b28b7b8e --- /dev/null +++ b/proxy/src/test/resources/rmq-proxy-home/conf/broker.conf @@ -0,0 +1,22 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +brokerClusterName = DefaultCluster +brokerName = broker-a +brokerId = 0 +deleteWhen = 04 +fileReservedTime = 48 +brokerRole = ASYNC_MASTER +flushDiskType = ASYNC_FLUSH diff --git a/proxy/src/test/resources/rmq-proxy-home/conf/logback_proxy.xml b/proxy/src/test/resources/rmq-proxy-home/conf/logback_proxy.xml new file mode 100644 index 00000000000..091a51fcabf --- /dev/null +++ b/proxy/src/test/resources/rmq-proxy-home/conf/logback_proxy.xml @@ -0,0 +1,420 @@ + + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}proxy.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}proxy.%i.log.gz + 1 + 10 + + + 128MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}proxy_watermark.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}proxy_watermark.%i.log.gz + 1 + 10 + + + 128MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8}%m%n + UTF-8 + + + + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}broker_default.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}broker_default.%i.log.gz + 1 + 10 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}broker.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}broker.%i.log.gz + 1 + 20 + + + 128MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}protection.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}protection.%i.log.gz + 1 + 10 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} - %m%n + UTF-8 + + + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}watermark.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}watermark.%i.log.gz + 1 + 10 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} - %m%n + UTF-8 + + + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}store.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}store.%i.log.gz + 1 + 10 + + + 128MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}remoting.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}remoting.%i.log.gz + 1 + 10 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}storeerror.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}storeerror.%i.log.gz + 1 + 10 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}transaction.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}transaction.%i.log.gz + 1 + 10 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}lock.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}lock.%i.log.gz + 1 + 5 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}filter.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}filter.%i.log.gz + 1 + 10 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}stats.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}stats.%i.log.gz + 1 + 5 + + + 100MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p - %m%n + UTF-8 + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}${brokerLogDir:-${file.separator}}${file.separator}commercial.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}${brokerLogDir:-${file.separator}}${file.separator}commercial.%i.log.gz + 1 + 10 + + + 500MB + + + + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}pop.log + true + + ${user.home}${file.separator}logs${file.separator}rocketmqlogs${file.separator}otherdays${file.separator}pop.%i.log + + 1 + 20 + + + 128MB + + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + + true + + %d{yyy-MM-dd HH:mm:ss,GMT+8} %p %t - %m%n + UTF-8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/proxy/src/test/resources/rmq-proxy-home/conf/rmq-proxy.json b/proxy/src/test/resources/rmq-proxy-home/conf/rmq-proxy.json new file mode 100644 index 00000000000..f0873e2a35d --- /dev/null +++ b/proxy/src/test/resources/rmq-proxy-home/conf/rmq-proxy.json @@ -0,0 +1,3 @@ +{ + "proxyMode": "cluster" +} \ No newline at end of file diff --git a/proxy/src/test/resources/rmq.logback-test.xml b/proxy/src/test/resources/rmq.logback-test.xml new file mode 100644 index 00000000000..8695d52d57c --- /dev/null +++ b/proxy/src/test/resources/rmq.logback-test.xml @@ -0,0 +1,36 @@ + + + + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + \ No newline at end of file diff --git a/remoting/BUILD.bazel b/remoting/BUILD.bazel new file mode 100644 index 00000000000..072148bc08c --- /dev/null +++ b/remoting/BUILD.bazel @@ -0,0 +1,79 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +load("//bazel:GenTestRules.bzl", "GenTestRules") + +java_library( + name = "remoting", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//common", + "@maven//:com_alibaba_fastjson", + "@maven//:com_google_guava_guava", + "@maven//:com_google_code_findbugs_jsr305", + "@maven//:com_squareup_okio_okio_jvm", + "@maven//:io_netty_netty_all", + "@maven//:io_opentelemetry_opentelemetry_api", + "@maven//:io_opentelemetry_opentelemetry_context", + "@maven//:io_opentelemetry_opentelemetry_exporter_otlp", + "@maven//:io_opentelemetry_opentelemetry_exporter_prometheus", + "@maven//:io_opentelemetry_opentelemetry_sdk", + "@maven//:io_opentelemetry_opentelemetry_sdk_common", + "@maven//:io_opentelemetry_opentelemetry_sdk_metrics", + "@maven//:org_apache_tomcat_annotations_api", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:io_github_aliyunmq_rocketmq_slf4j_api", + "@maven//:io_github_aliyunmq_rocketmq_logback_classic", + "@maven//:commons_collections_commons_collections", + ], +) + +java_library( + name = "tests", + srcs = glob(["src/test/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + ":remoting", + "//common", + "//:test_deps", + "@maven//:com_alibaba_fastjson", + "@maven//:com_google_code_gson_gson", + "@maven//:com_google_guava_guava", + "@maven//:com_google_code_findbugs_jsr305", + "@maven//:com_squareup_okio_okio_jvm", + "@maven//:io_netty_netty_all", + "@maven//:io_opentelemetry_opentelemetry_api", + "@maven//:io_opentelemetry_opentelemetry_context", + "@maven//:io_opentelemetry_opentelemetry_exporter_otlp", + "@maven//:io_opentelemetry_opentelemetry_exporter_prometheus", + "@maven//:io_opentelemetry_opentelemetry_sdk", + "@maven//:io_opentelemetry_opentelemetry_sdk_common", + "@maven//:io_opentelemetry_opentelemetry_sdk_metrics", + "@maven//:org_apache_tomcat_annotations_api", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:org_jetbrains_annotations", + ], + resources = glob(["src/test/resources/certs/*.pem"]) + glob(["src/test/resources/certs/*.key"]) +) + +GenTestRules( + name = "GeneratedTestRules", + test_files = glob(["src/test/java/**/*Test.java"]), + deps = [ + ":tests", + ], +) diff --git a/remoting/pom.xml b/remoting/pom.xml index 4dc0e7390ab..6f2f1cc75b6 100644 --- a/remoting/pom.xml +++ b/remoting/pom.xml @@ -19,7 +19,7 @@ org.apache.rocketmq rocketmq-all - 4.2.0 + 5.2.0 4.0.0 @@ -28,39 +28,23 @@ rocketmq-remoting ${project.version} - 1.6 - 1.6 + ${basedir}/.. - com.alibaba - fastjson + ${project.groupId} + rocketmq-common - io.netty - netty-all + org.apache.commons + commons-lang3 - org.slf4j - slf4j-api - - - - io.netty - netty-tcnative - 1.1.33.Fork22 - ${os.detected.classifier} + com.google.code.gson + gson + 2.9.0 + test - - - - - kr.motd.maven - os-maven-plugin - 1.4.0.Final - - - diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/ChannelEventListener.java b/remoting/src/main/java/org/apache/rocketmq/remoting/ChannelEventListener.java index c99133b3a2d..6802e69b90d 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/ChannelEventListener.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/ChannelEventListener.java @@ -26,4 +26,6 @@ public interface ChannelEventListener { void onChannelException(final String remoteAddr, final Channel channel); void onChannelIdle(final String remoteAddr, final Channel channel); + + void onChannelActive(final String remoteAddr, final Channel channel); } diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/Configuration.java b/remoting/src/main/java/org/apache/rocketmq/remoting/Configuration.java new file mode 100644 index 00000000000..5b3e5ca3797 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/Configuration.java @@ -0,0 +1,345 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.remoting.protocol.DataVersion; + +public class Configuration { + + private final Logger log; + + private List configObjectList = new ArrayList<>(4); + private String storePath; + private boolean storePathFromConfig = false; + private Object storePathObject; + private Field storePathField; + private DataVersion dataVersion = new DataVersion(); + private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + /** + * All properties include configs in object and extend properties. + */ + private Properties allConfigs = new Properties(); + + public Configuration(Logger log) { + this.log = log; + } + + public Configuration(Logger log, Object... configObjects) { + this.log = log; + if (configObjects == null || configObjects.length == 0) { + return; + } + for (Object configObject : configObjects) { + if (configObject == null) { + continue; + } + registerConfig(configObject); + } + } + + public Configuration(Logger log, String storePath, Object... configObjects) { + this(log, configObjects); + this.storePath = storePath; + } + + /** + * register config object + * + * @return the current Configuration object + */ + public Configuration registerConfig(Object configObject) { + try { + readWriteLock.writeLock().lockInterruptibly(); + + try { + + Properties registerProps = MixAll.object2Properties(configObject); + + merge(registerProps, this.allConfigs); + + configObjectList.add(configObject); + } finally { + readWriteLock.writeLock().unlock(); + } + } catch (InterruptedException e) { + log.error("registerConfig lock error"); + } + return this; + } + + /** + * register config properties + * + * @return the current Configuration object + */ + public Configuration registerConfig(Properties extProperties) { + if (extProperties == null) { + return this; + } + + try { + readWriteLock.writeLock().lockInterruptibly(); + + try { + merge(extProperties, this.allConfigs); + } finally { + readWriteLock.writeLock().unlock(); + } + } catch (InterruptedException e) { + log.error("register lock error. {}" + extProperties); + } + + return this; + } + + /** + * The store path will be gotten from the field of object. + * + * @throws java.lang.RuntimeException if the field of object is not exist. + */ + public void setStorePathFromConfig(Object object, String fieldName) { + assert object != null; + + try { + readWriteLock.writeLock().lockInterruptibly(); + + try { + this.storePathFromConfig = true; + this.storePathObject = object; + // check + this.storePathField = object.getClass().getDeclaredField(fieldName); + assert this.storePathField != null + && !Modifier.isStatic(this.storePathField.getModifiers()); + this.storePathField.setAccessible(true); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } finally { + readWriteLock.writeLock().unlock(); + } + } catch (InterruptedException e) { + log.error("setStorePathFromConfig lock error"); + } + } + + private String getStorePath() { + String realStorePath = null; + try { + readWriteLock.readLock().lockInterruptibly(); + + try { + realStorePath = this.storePath; + + if (this.storePathFromConfig) { + try { + realStorePath = (String) storePathField.get(this.storePathObject); + } catch (IllegalAccessException e) { + log.error("getStorePath error, ", e); + } + } + } finally { + readWriteLock.readLock().unlock(); + } + } catch (InterruptedException e) { + log.error("getStorePath lock error"); + } + + return realStorePath; + } + + public void setStorePath(final String storePath) { + this.storePath = storePath; + } + + public void update(Properties properties) { + try { + readWriteLock.writeLock().lockInterruptibly(); + + try { + // the property must be exist when update + mergeIfExist(properties, this.allConfigs); + + for (Object configObject : configObjectList) { + // not allConfigs to update... + MixAll.properties2Object(properties, configObject); + } + + this.dataVersion.nextVersion(); + + } finally { + readWriteLock.writeLock().unlock(); + } + } catch (InterruptedException e) { + log.error("update lock error, {}", properties); + return; + } + + persist(); + } + + public void persist() { + try { + readWriteLock.readLock().lockInterruptibly(); + + try { + String allConfigs = getAllConfigsInternal(); + + MixAll.string2File(allConfigs, getStorePath()); + } catch (IOException e) { + log.error("persist string2File error, ", e); + } finally { + readWriteLock.readLock().unlock(); + } + } catch (InterruptedException e) { + log.error("persist lock error"); + } + } + + public String getAllConfigsFormatString() { + try { + readWriteLock.readLock().lockInterruptibly(); + + try { + + return getAllConfigsInternal(); + + } finally { + readWriteLock.readLock().unlock(); + } + } catch (InterruptedException e) { + log.error("getAllConfigsFormatString lock error"); + } + + return null; + } + + public String getClientConfigsFormatString(List clientKeys) { + try { + readWriteLock.readLock().lockInterruptibly(); + + try { + + return getClientConfigsInternal(clientKeys); + + } finally { + readWriteLock.readLock().unlock(); + } + } catch (InterruptedException e) { + log.error("getAllConfigsFormatString lock error"); + } + + return null; + } + + public String getDataVersionJson() { + return this.dataVersion.toJson(); + } + + public Properties getAllConfigs() { + try { + readWriteLock.readLock().lockInterruptibly(); + + try { + + return this.allConfigs; + + } finally { + readWriteLock.readLock().unlock(); + } + } catch (InterruptedException e) { + log.error("getAllConfigs lock error"); + } + + return null; + } + + private String getAllConfigsInternal() { + StringBuilder stringBuilder = new StringBuilder(); + + // reload from config object ? + for (Object configObject : this.configObjectList) { + Properties properties = MixAll.object2Properties(configObject); + if (properties != null) { + merge(properties, this.allConfigs); + } else { + log.warn("getAllConfigsInternal object2Properties is null, {}", configObject.getClass()); + } + } + + { + stringBuilder.append(MixAll.properties2String(this.allConfigs, true)); + } + + return stringBuilder.toString(); + } + + private String getClientConfigsInternal(List clientConigKeys) { + StringBuilder stringBuilder = new StringBuilder(); + Properties clientProperties = new Properties(); + + // reload from config object ? + for (Object configObject : this.configObjectList) { + Properties properties = MixAll.object2Properties(configObject); + + for (String nameNow : clientConigKeys) { + if (properties.containsKey(nameNow)) { + clientProperties.put(nameNow, properties.get(nameNow)); + } + } + + } + stringBuilder.append(MixAll.properties2String(clientProperties)); + + return stringBuilder.toString(); + } + + private void merge(Properties from, Properties to) { + for (Entry next : from.entrySet()) { + Object fromObj = next.getValue(), toObj = to.get(next.getKey()); + if (toObj != null && !toObj.equals(fromObj)) { + log.info("Replace, key: {}, value: {} -> {}", next.getKey(), toObj, fromObj); + } + to.put(next.getKey(), fromObj); + } + } + + private void mergeIfExist(Properties from, Properties to) { + for (Entry next : from.entrySet()) { + if (!to.containsKey(next.getKey())) { + continue; + } + + Object fromObj = next.getValue(), toObj = to.get(next.getKey()); + if (toObj != null && !toObj.equals(fromObj)) { + log.info("Replace, key: {}, value: {} -> {}", next.getKey(), toObj, fromObj); + } + to.put(next.getKey(), fromObj); + } + } + +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/InvokeCallback.java b/remoting/src/main/java/org/apache/rocketmq/remoting/InvokeCallback.java index ce78fa923ff..6be49174571 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/InvokeCallback.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/InvokeCallback.java @@ -17,7 +17,22 @@ package org.apache.rocketmq.remoting; import org.apache.rocketmq.remoting.netty.ResponseFuture; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; public interface InvokeCallback { + /** + * This method is expected to be invoked after {@link #operationSucceed(RemotingCommand)} + * or {@link #operationFail(Throwable)} + * + * @param responseFuture the returned object contains response or exception + */ void operationComplete(final ResponseFuture responseFuture); + + default void operationSucceed(final RemotingCommand response) { + + } + + default void operationFail(final Throwable throwable) { + + } } diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/RPCHook.java b/remoting/src/main/java/org/apache/rocketmq/remoting/RPCHook.java index 6292fc09189..ebaeea40af2 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/RPCHook.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/RPCHook.java @@ -23,5 +23,5 @@ public interface RPCHook { void doBeforeRequest(final String remoteAddr, final RemotingCommand request); void doAfterResponse(final String remoteAddr, final RemotingCommand request, - final RemotingCommand response); + final RemotingCommand response); } diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/RemotingClient.java b/remoting/src/main/java/org/apache/rocketmq/remoting/RemotingClient.java index 2aea14cb9d6..c8389eedb18 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/RemotingClient.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/RemotingClient.java @@ -17,12 +17,14 @@ package org.apache.rocketmq.remoting; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import org.apache.rocketmq.remoting.exception.RemotingConnectException; import org.apache.rocketmq.remoting.exception.RemotingSendRequestException; import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; import org.apache.rocketmq.remoting.exception.RemotingTooMuchRequestException; import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; +import org.apache.rocketmq.remoting.netty.ResponseFuture; import org.apache.rocketmq.remoting.protocol.RemotingCommand; public interface RemotingClient extends RemotingService { @@ -31,6 +33,8 @@ public interface RemotingClient extends RemotingService { List getNameServerAddressList(); + List getAvailableNameSrvList(); + RemotingCommand invokeSync(final String addr, final RemotingCommand request, final long timeoutMillis) throws InterruptedException, RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException; @@ -43,10 +47,41 @@ void invokeOneway(final String addr, final RemotingCommand request, final long t throws InterruptedException, RemotingConnectException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException; + default CompletableFuture invoke(final String addr, final RemotingCommand request, + final long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + try { + invokeAsync(addr, request, timeoutMillis, new InvokeCallback() { + + @Override + public void operationComplete(ResponseFuture responseFuture) { + + } + + @Override + public void operationSucceed(RemotingCommand response) { + future.complete(response); + } + + @Override + public void operationFail(Throwable throwable) { + future.completeExceptionally(throwable); + } + }); + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + void registerProcessor(final int requestCode, final NettyRequestProcessor processor, final ExecutorService executor); void setCallbackExecutor(final ExecutorService callbackExecutor); boolean isChannelWritable(final String addr); + + boolean isAddressReachable(final String addr); + + void closeChannels(final List addrList); } diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/RemotingServer.java b/remoting/src/main/java/org/apache/rocketmq/remoting/RemotingServer.java index a12c089c74c..8cfa1e1a083 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/RemotingServer.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/RemotingServer.java @@ -18,7 +18,7 @@ import io.netty.channel.Channel; import java.util.concurrent.ExecutorService; -import org.apache.rocketmq.remoting.common.Pair; +import org.apache.rocketmq.common.Pair; import org.apache.rocketmq.remoting.exception.RemotingSendRequestException; import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; import org.apache.rocketmq.remoting.exception.RemotingTooMuchRequestException; @@ -36,6 +36,12 @@ void registerProcessor(final int requestCode, final NettyRequestProcessor proces Pair getProcessorPair(final int requestCode); + Pair getDefaultProcessorPair(); + + RemotingServer newRemotingServer(int port); + + void removeRemotingServer(int port); + RemotingCommand invokeSync(final Channel channel, final RemotingCommand request, final long timeoutMillis) throws InterruptedException, RemotingSendRequestException, RemotingTimeoutException; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/RemotingService.java b/remoting/src/main/java/org/apache/rocketmq/remoting/RemotingService.java index 2f887972172..c718f2e65c0 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/RemotingService.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/RemotingService.java @@ -23,4 +23,9 @@ public interface RemotingService { void shutdown(); void registerRPCHook(RPCHook rpcHook); + + /** + * Remove all rpc hooks. + */ + void clearRPCHook(); } diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/common/HeartbeatV2Result.java b/remoting/src/main/java/org/apache/rocketmq/remoting/common/HeartbeatV2Result.java new file mode 100644 index 00000000000..2401233c48e --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/common/HeartbeatV2Result.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.common; + +public class HeartbeatV2Result { + private int version = 0; + private boolean isSubChange = false; + private boolean isSupportV2 = false; + + public HeartbeatV2Result(int version, boolean isSubChange, boolean isSupportV2) { + this.version = version; + this.isSubChange = isSubChange; + this.isSupportV2 = isSupportV2; + } + + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } + + public boolean isSubChange() { + return isSubChange; + } + + public void setSubChange(boolean subChange) { + isSubChange = subChange; + } + + public boolean isSupportV2() { + return isSupportV2; + } + + public void setSupportV2(boolean supportV2) { + isSupportV2 = supportV2; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/common/Pair.java b/remoting/src/main/java/org/apache/rocketmq/remoting/common/Pair.java deleted file mode 100644 index 01e761fcfae..00000000000 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/common/Pair.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.remoting.common; - -public class Pair { - private T1 object1; - private T2 object2; - - public Pair(T1 object1, T2 object2) { - this.object1 = object1; - this.object2 = object2; - } - - public T1 getObject1() { - return object1; - } - - public void setObject1(T1 object1) { - this.object1 = object1; - } - - public T2 getObject2() { - return object2; - } - - public void setObject2(T2 object2) { - this.object2 = object2; - } -} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/common/RemotingHelper.java b/remoting/src/main/java/org/apache/rocketmq/remoting/common/RemotingHelper.java index 79957305c0f..363b22eac71 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/common/RemotingHelper.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/common/RemotingHelper.java @@ -17,53 +17,97 @@ package org.apache.rocketmq.remoting.common; import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.util.Attribute; +import io.netty.util.AttributeKey; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; import org.apache.rocketmq.remoting.exception.RemotingConnectException; import org.apache.rocketmq.remoting.exception.RemotingSendRequestException; import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; +import org.apache.rocketmq.remoting.netty.AttributeKeys; +import org.apache.rocketmq.remoting.netty.NettySystemConfig; import org.apache.rocketmq.remoting.protocol.RemotingCommand; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; import java.io.IOException; +import java.lang.reflect.Field; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; +import java.util.HashMap; +import java.util.Map; public class RemotingHelper { - public static final String ROCKETMQ_REMOTING = "RocketmqRemoting"; public static final String DEFAULT_CHARSET = "UTF-8"; + public static final String DEFAULT_CIDR_ALL = "0.0.0.0/0"; - private static final Logger log = LoggerFactory.getLogger(ROCKETMQ_REMOTING); + private static final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_REMOTING_NAME); - public static String exceptionSimpleDesc(final Throwable e) { - StringBuffer sb = new StringBuffer(); - if (e != null) { - sb.append(e.toString()); + public static final Map REQUEST_CODE_MAP = new HashMap() { + { + try { + Field[] f = RequestCode.class.getFields(); + for (Field field : f) { + if (field.getType() == int.class) { + put((int) field.get(null), field.getName().toLowerCase()); + } + } + } catch (IllegalAccessException ignore) { + } + } + }; - StackTraceElement[] stackTrace = e.getStackTrace(); - if (stackTrace != null && stackTrace.length > 0) { - StackTraceElement elment = stackTrace[0]; - sb.append(", "); - sb.append(elment.toString()); + public static final Map RESPONSE_CODE_MAP = new HashMap() { + { + try { + Field[] f = ResponseCode.class.getFields(); + for (Field field : f) { + if (field.getType() == int.class) { + put((int) field.get(null), field.getName().toLowerCase()); + } + } + } catch (IllegalAccessException ignore) { } } + }; - return sb.toString(); + public static T getAttributeValue(AttributeKey key, final Channel channel) { + if (channel.hasAttr(key)) { + Attribute attribute = channel.attr(key); + return attribute.get(); + } + return null; + } + + public static void setPropertyToAttr(final Channel channel, AttributeKey attributeKey, T value) { + if (channel == null) { + return; + } + channel.attr(attributeKey).set(value); } public static SocketAddress string2SocketAddress(final String addr) { - String[] s = addr.split(":"); - InetSocketAddress isa = new InetSocketAddress(s[0], Integer.parseInt(s[1])); + int split = addr.lastIndexOf(":"); + String host = addr.substring(0, split); + String port = addr.substring(split + 1); + InetSocketAddress isa = new InetSocketAddress(host, Integer.parseInt(port)); return isa; } public static RemotingCommand invokeSync(final String addr, final RemotingCommand request, final long timeoutMillis) throws InterruptedException, RemotingConnectException, - RemotingSendRequestException, RemotingTimeoutException { + RemotingSendRequestException, RemotingTimeoutException, RemotingCommandException { long beginTime = System.currentTimeMillis(); - SocketAddress socketAddress = RemotingUtil.string2SocketAddress(addr); - SocketChannel socketChannel = RemotingUtil.connect(socketAddress); + SocketAddress socketAddress = NetworkUtil.string2SocketAddress(addr); + SocketChannel socketChannel = connect(socketAddress); if (socketChannel != null) { boolean sendRequestOK = false; @@ -154,6 +198,36 @@ public static String parseChannelRemoteAddr(final Channel channel) { if (null == channel) { return ""; } + String addr = getProxyProtocolAddress(channel); + if (StringUtils.isNotBlank(addr)) { + return addr; + } + Attribute att = channel.attr(AttributeKeys.REMOTE_ADDR_KEY); + if (att == null) { + // mocked in unit test + return parseChannelRemoteAddr0(channel); + } + addr = att.get(); + if (addr == null) { + addr = parseChannelRemoteAddr0(channel); + att.set(addr); + } + return addr; + } + + private static String getProxyProtocolAddress(Channel channel) { + if (!channel.hasAttr(AttributeKeys.PROXY_PROTOCOL_ADDR)) { + return null; + } + String proxyProtocolAddr = getAttributeValue(AttributeKeys.PROXY_PROTOCOL_ADDR, channel); + String proxyProtocolPort = getAttributeValue(AttributeKeys.PROXY_PROTOCOL_PORT, channel); + if (StringUtils.isBlank(proxyProtocolAddr) || proxyProtocolPort == null) { + return null; + } + return proxyProtocolAddr + ":" + proxyProtocolPort; + } + + private static String parseChannelRemoteAddr0(final Channel channel) { SocketAddress remote = channel.remoteAddress(); final String addr = remote != null ? remote.toString() : ""; @@ -169,15 +243,107 @@ public static String parseChannelRemoteAddr(final Channel channel) { return ""; } + public static String parseHostFromAddress(String address) { + if (address == null) { + return ""; + } + + String[] addressSplits = address.split(":"); + if (addressSplits.length < 1) { + return ""; + } + + return addressSplits[0]; + } + public static String parseSocketAddressAddr(SocketAddress socketAddress) { if (socketAddress != null) { + // Default toString of InetSocketAddress is "hostName/IP:port" final String addr = socketAddress.toString(); + int index = addr.lastIndexOf("/"); + return (index != -1) ? addr.substring(index + 1) : addr; + } + return ""; + } + + public static Integer parseSocketAddressPort(SocketAddress socketAddress) { + if (socketAddress instanceof InetSocketAddress) { + return ((InetSocketAddress) socketAddress).getPort(); + } + return -1; + } + + public static int ipToInt(String ip) { + String[] ips = ip.split("\\."); + return (Integer.parseInt(ips[0]) << 24) + | (Integer.parseInt(ips[1]) << 16) + | (Integer.parseInt(ips[2]) << 8) + | Integer.parseInt(ips[3]); + } + + public static boolean ipInCIDR(String ip, String cidr) { + int ipAddr = ipToInt(ip); + String[] cidrArr = cidr.split("/"); + int netId = Integer.parseInt(cidrArr[1]); + int mask = 0xFFFFFFFF << (32 - netId); + int cidrIpAddr = ipToInt(cidrArr[0]); + + return (ipAddr & mask) == (cidrIpAddr & mask); + } + + public static SocketChannel connect(SocketAddress remote) { + return connect(remote, 1000 * 5); + } - if (addr.length() > 0) { - return addr.substring(1); + public static SocketChannel connect(SocketAddress remote, final int timeoutMillis) { + SocketChannel sc = null; + try { + sc = SocketChannel.open(); + sc.configureBlocking(true); + sc.socket().setSoLinger(false, -1); + sc.socket().setTcpNoDelay(true); + if (NettySystemConfig.socketSndbufSize > 0) { + sc.socket().setReceiveBufferSize(NettySystemConfig.socketSndbufSize); + } + if (NettySystemConfig.socketRcvbufSize > 0) { + sc.socket().setSendBufferSize(NettySystemConfig.socketRcvbufSize); + } + sc.socket().connect(remote, timeoutMillis); + sc.configureBlocking(false); + return sc; + } catch (Exception e) { + if (sc != null) { + try { + sc.close(); + } catch (IOException e1) { + e1.printStackTrace(); + } } } - return ""; + + return null; + } + + public static void closeChannel(Channel channel) { + final String addrRemote = RemotingHelper.parseChannelRemoteAddr(channel); + if ("".equals(addrRemote)) { + channel.close(); + } else { + channel.close().addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + log.info("closeChannel: close the connection to remote address[{}] result: {}", addrRemote, + future.isSuccess()); + } + }); + } } + public static String getRequestCodeDesc(int code) { + return REQUEST_CODE_MAP.getOrDefault(code, String.valueOf(code)); + } + + public static String getResponseCodeDesc(int code) { + return RESPONSE_CODE_MAP.getOrDefault(code, String.valueOf(code)); + } } diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/common/RemotingUtil.java b/remoting/src/main/java/org/apache/rocketmq/remoting/common/RemotingUtil.java deleted file mode 100644 index 8d24e76b4f8..00000000000 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/common/RemotingUtil.java +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.remoting.common; - -import io.netty.channel.Channel; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelFutureListener; -import java.io.IOException; -import java.lang.reflect.Method; -import java.net.Inet6Address; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.NetworkInterface; -import java.net.SocketAddress; -import java.nio.channels.Selector; -import java.nio.channels.SocketChannel; -import java.nio.channels.spi.SelectorProvider; -import java.util.ArrayList; -import java.util.Enumeration; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class RemotingUtil { - public static final String OS_NAME = System.getProperty("os.name"); - - private static final Logger log = LoggerFactory.getLogger(RemotingHelper.ROCKETMQ_REMOTING); - private static boolean isLinuxPlatform = false; - private static boolean isWindowsPlatform = false; - - static { - if (OS_NAME != null && OS_NAME.toLowerCase().contains("linux")) { - isLinuxPlatform = true; - } - - if (OS_NAME != null && OS_NAME.toLowerCase().contains("windows")) { - isWindowsPlatform = true; - } - } - - public static boolean isWindowsPlatform() { - return isWindowsPlatform; - } - - public static Selector openSelector() throws IOException { - Selector result = null; - - if (isLinuxPlatform()) { - try { - final Class providerClazz = Class.forName("sun.nio.ch.EPollSelectorProvider"); - if (providerClazz != null) { - try { - final Method method = providerClazz.getMethod("provider"); - if (method != null) { - final SelectorProvider selectorProvider = (SelectorProvider) method.invoke(null); - if (selectorProvider != null) { - result = selectorProvider.openSelector(); - } - } - } catch (final Exception e) { - log.warn("Open ePoll Selector for linux platform exception", e); - } - } - } catch (final Exception e) { - // ignore - } - } - - if (result == null) { - result = Selector.open(); - } - - return result; - } - - public static boolean isLinuxPlatform() { - return isLinuxPlatform; - } - - public static String getLocalAddress() { - try { - // Traversal Network interface to get the first non-loopback and non-private address - Enumeration enumeration = NetworkInterface.getNetworkInterfaces(); - ArrayList ipv4Result = new ArrayList(); - ArrayList ipv6Result = new ArrayList(); - while (enumeration.hasMoreElements()) { - final NetworkInterface networkInterface = enumeration.nextElement(); - final Enumeration en = networkInterface.getInetAddresses(); - while (en.hasMoreElements()) { - final InetAddress address = en.nextElement(); - if (!address.isLoopbackAddress()) { - if (address instanceof Inet6Address) { - ipv6Result.add(normalizeHostAddress(address)); - } else { - ipv4Result.add(normalizeHostAddress(address)); - } - } - } - } - - // prefer ipv4 - if (!ipv4Result.isEmpty()) { - for (String ip : ipv4Result) { - if (ip.startsWith("127.0") || ip.startsWith("192.168")) { - continue; - } - - return ip; - } - - return ipv4Result.get(ipv4Result.size() - 1); - } else if (!ipv6Result.isEmpty()) { - return ipv6Result.get(0); - } - //If failed to find,fall back to localhost - final InetAddress localHost = InetAddress.getLocalHost(); - return normalizeHostAddress(localHost); - } catch (Exception e) { - log.error("Failed to obtain local address", e); - } - - return null; - } - - public static String normalizeHostAddress(final InetAddress localHost) { - if (localHost instanceof Inet6Address) { - return "[" + localHost.getHostAddress() + "]"; - } else { - return localHost.getHostAddress(); - } - } - - public static SocketAddress string2SocketAddress(final String addr) { - String[] s = addr.split(":"); - InetSocketAddress isa = new InetSocketAddress(s[0], Integer.parseInt(s[1])); - return isa; - } - - public static String socketAddress2String(final SocketAddress addr) { - StringBuilder sb = new StringBuilder(); - InetSocketAddress inetSocketAddress = (InetSocketAddress) addr; - sb.append(inetSocketAddress.getAddress().getHostAddress()); - sb.append(":"); - sb.append(inetSocketAddress.getPort()); - return sb.toString(); - } - - public static SocketChannel connect(SocketAddress remote) { - return connect(remote, 1000 * 5); - } - - public static SocketChannel connect(SocketAddress remote, final int timeoutMillis) { - SocketChannel sc = null; - try { - sc = SocketChannel.open(); - sc.configureBlocking(true); - sc.socket().setSoLinger(false, -1); - sc.socket().setTcpNoDelay(true); - sc.socket().setReceiveBufferSize(1024 * 64); - sc.socket().setSendBufferSize(1024 * 64); - sc.socket().connect(remote, timeoutMillis); - sc.configureBlocking(false); - return sc; - } catch (Exception e) { - if (sc != null) { - try { - sc.close(); - } catch (IOException e1) { - e1.printStackTrace(); - } - } - } - - return null; - } - - public static void closeChannel(Channel channel) { - final String addrRemote = RemotingHelper.parseChannelRemoteAddr(channel); - channel.close().addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) throws Exception { - log.info("closeChannel: close the connection to remote address[{}] result: {}", addrRemote, - future.isSuccess()); - } - }); - } - -} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/common/ServiceThread.java b/remoting/src/main/java/org/apache/rocketmq/remoting/common/ServiceThread.java index 346e72c5a2a..a4ee814175e 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/common/ServiceThread.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/common/ServiceThread.java @@ -16,14 +16,16 @@ */ package org.apache.rocketmq.remoting.common; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; + +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; /** * Base class for background thread */ public abstract class ServiceThread implements Runnable { - private static final Logger log = LoggerFactory.getLogger(RemotingHelper.ROCKETMQ_REMOTING); + private static final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_REMOTING_NAME); private static final long JOIN_TIME = 90 * 1000; protected final Thread thread; @@ -61,8 +63,8 @@ public void shutdown(final boolean interrupt) { long beginTime = System.currentTimeMillis(); this.thread.join(this.getJointime()); - long eclipseTime = System.currentTimeMillis() - beginTime; - log.info("join thread " + this.getServiceName() + " eclipse time(ms) " + eclipseTime + " " + long elapsedTime = System.currentTimeMillis() - beginTime; + log.info("join thread " + this.getServiceName() + " elapsed time(ms) " + elapsedTime + " " + this.getJointime()); } catch (InterruptedException e) { log.error("Interrupted", e); diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/exception/RemotingConnectException.java b/remoting/src/main/java/org/apache/rocketmq/remoting/exception/RemotingConnectException.java index 8286177c2ef..40e3b5aeefb 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/exception/RemotingConnectException.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/exception/RemotingConnectException.java @@ -24,6 +24,6 @@ public RemotingConnectException(String addr) { } public RemotingConnectException(String addr, Throwable cause) { - super("connect to <" + addr + "> failed", cause); + super("connect to " + addr + " failed", cause); } } diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/metrics/RemotingMetricsConstant.java b/remoting/src/main/java/org/apache/rocketmq/remoting/metrics/RemotingMetricsConstant.java new file mode 100644 index 00000000000..f9b3e4c6fa4 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/metrics/RemotingMetricsConstant.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.metrics; + +public class RemotingMetricsConstant { + public static final String HISTOGRAM_RPC_LATENCY = "rocketmq_rpc_latency"; + public static final String LABEL_PROTOCOL_TYPE = "protocol_type"; + public static final String LABEL_REQUEST_CODE = "request_code"; + public static final String LABEL_RESPONSE_CODE = "response_code"; + public static final String LABEL_IS_LONG_POLLING = "is_long_polling"; + public static final String LABEL_RESULT = "result"; + + public static final String PROTOCOL_TYPE_REMOTING = "remoting"; + + public static final String RESULT_ONEWAY = "oneway"; + public static final String RESULT_SUCCESS = "success"; + public static final String RESULT_CANCELED = "cancelled"; + public static final String RESULT_PROCESS_REQUEST_FAILED = "process_request_failed"; + public static final String RESULT_WRITE_CHANNEL_FAILED = "write_channel_failed"; + +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/metrics/RemotingMetricsManager.java b/remoting/src/main/java/org/apache/rocketmq/remoting/metrics/RemotingMetricsManager.java new file mode 100644 index 00000000000..2e0d70856cf --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/metrics/RemotingMetricsManager.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.metrics; + +import com.google.common.collect.Lists; +import io.netty.util.concurrent.Future; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.sdk.metrics.Aggregation; +import io.opentelemetry.sdk.metrics.InstrumentSelector; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.View; +import io.opentelemetry.sdk.metrics.ViewBuilder; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.function.Supplier; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.metrics.NopLongHistogram; + +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.HISTOGRAM_RPC_LATENCY; +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.LABEL_PROTOCOL_TYPE; +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.PROTOCOL_TYPE_REMOTING; +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.RESULT_CANCELED; +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.RESULT_SUCCESS; +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.RESULT_WRITE_CHANNEL_FAILED; + +public class RemotingMetricsManager { + public static LongHistogram rpcLatency = new NopLongHistogram(); + public static Supplier attributesBuilderSupplier; + + public static AttributesBuilder newAttributesBuilder() { + if (attributesBuilderSupplier == null) { + return Attributes.builder(); + } + return attributesBuilderSupplier.get() + .put(LABEL_PROTOCOL_TYPE, PROTOCOL_TYPE_REMOTING); + } + + public static void initMetrics(Meter meter, Supplier attributesBuilderSupplier) { + RemotingMetricsManager.attributesBuilderSupplier = attributesBuilderSupplier; + rpcLatency = meter.histogramBuilder(HISTOGRAM_RPC_LATENCY) + .setDescription("Rpc latency") + .setUnit("milliseconds") + .ofLongs() + .build(); + } + + public static List> getMetricsView() { + List rpcCostTimeBuckets = Arrays.asList( + (double) Duration.ofMillis(1).toMillis(), + (double) Duration.ofMillis(3).toMillis(), + (double) Duration.ofMillis(5).toMillis(), + (double) Duration.ofMillis(7).toMillis(), + (double) Duration.ofMillis(10).toMillis(), + (double) Duration.ofMillis(100).toMillis(), + (double) Duration.ofSeconds(1).toMillis(), + (double) Duration.ofSeconds(2).toMillis(), + (double) Duration.ofSeconds(3).toMillis() + ); + InstrumentSelector selector = InstrumentSelector.builder() + .setType(InstrumentType.HISTOGRAM) + .setName(HISTOGRAM_RPC_LATENCY) + .build(); + ViewBuilder viewBuilder = View.builder() + .setAggregation(Aggregation.explicitBucketHistogram(rpcCostTimeBuckets)); + return Lists.newArrayList(new Pair<>(selector, viewBuilder)); + } + + public static String getWriteAndFlushResult(Future future) { + String result = RESULT_SUCCESS; + if (future.isCancelled()) { + result = RESULT_CANCELED; + } else if (!future.isSuccess()) { + result = RESULT_WRITE_CHANNEL_FAILED; + } + return result; + } + +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/AttributeKeys.java b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/AttributeKeys.java new file mode 100644 index 00000000000..ebdde31f418 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/AttributeKeys.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.netty; + + +import io.netty.util.AttributeKey; +import org.apache.rocketmq.common.constant.HAProxyConstants; +import org.apache.rocketmq.remoting.protocol.LanguageCode; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class AttributeKeys { + + public static final AttributeKey REMOTE_ADDR_KEY = AttributeKey.valueOf("RemoteAddr"); + + public static final AttributeKey CLIENT_ID_KEY = AttributeKey.valueOf("ClientId"); + + public static final AttributeKey VERSION_KEY = AttributeKey.valueOf("Version"); + + public static final AttributeKey LANGUAGE_CODE_KEY = AttributeKey.valueOf("LanguageCode"); + + public static final AttributeKey PROXY_PROTOCOL_ADDR = + AttributeKey.valueOf(HAProxyConstants.PROXY_PROTOCOL_ADDR); + + public static final AttributeKey PROXY_PROTOCOL_PORT = + AttributeKey.valueOf(HAProxyConstants.PROXY_PROTOCOL_PORT); + + public static final AttributeKey PROXY_PROTOCOL_SERVER_ADDR = + AttributeKey.valueOf(HAProxyConstants.PROXY_PROTOCOL_SERVER_ADDR); + + public static final AttributeKey PROXY_PROTOCOL_SERVER_PORT = + AttributeKey.valueOf(HAProxyConstants.PROXY_PROTOCOL_SERVER_PORT); + + private static final Map> ATTRIBUTE_KEY_MAP = new ConcurrentHashMap<>(); + + public static AttributeKey valueOf(String name) { + return ATTRIBUTE_KEY_MAP.computeIfAbsent(name, AttributeKey::valueOf); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/FileRegionEncoder.java b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/FileRegionEncoder.java index 2bd15aea3c2..3522c7965c1 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/FileRegionEncoder.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/FileRegionEncoder.java @@ -18,6 +18,9 @@ package org.apache.rocketmq.remoting.netty; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.FileRegion; import io.netty.handler.codec.MessageToByteEncoder; @@ -50,9 +53,13 @@ public class FileRegionEncoder extends MessageToByteEncoder { protected void encode(ChannelHandlerContext ctx, FileRegion msg, final ByteBuf out) throws Exception { WritableByteChannel writableByteChannel = new WritableByteChannel() { @Override - public int write(ByteBuffer src) throws IOException { - out.writeBytes(src); - return out.capacity(); + public int write(ByteBuffer src) { + // To prevent mem_copy. + CompositeByteBuf b = (CompositeByteBuf) out; + // Have to increase writerIndex manually. + ByteBuf unpooled = Unpooled.wrappedBuffer(src); + b.addComponent(true, unpooled); + return unpooled.readableBytes(); } @Override @@ -68,11 +75,17 @@ public void close() throws IOException { long toTransfer = msg.count(); while (true) { - long transferred = msg.transfered(); + long transferred = msg.transferred(); if (toTransfer - transferred <= 0) { break; } msg.transferTo(writableByteChannel, transferred); } } + + @Override + protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, FileRegion msg, boolean preferDirect) throws Exception { + ByteBufAllocator allocator = ctx.alloc(); + return preferDirect ? allocator.compositeDirectBuffer() : allocator.compositeHeapBuffer(); + } } \ No newline at end of file diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyClientConfig.java b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyClientConfig.java index fbc071b28f7..c28288786a3 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyClientConfig.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyClientConfig.java @@ -16,29 +16,48 @@ */ package org.apache.rocketmq.remoting.netty; +import org.apache.rocketmq.remoting.common.TlsMode; + +import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_ENABLE; + public class NettyClientConfig { /** * Worker thread number */ - private int clientWorkerThreads = 4; + private int clientWorkerThreads = NettySystemConfig.clientWorkerSize; private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); private int clientOnewaySemaphoreValue = NettySystemConfig.CLIENT_ONEWAY_SEMAPHORE_VALUE; private int clientAsyncSemaphoreValue = NettySystemConfig.CLIENT_ASYNC_SEMAPHORE_VALUE; - private int connectTimeoutMillis = 3000; + private int connectTimeoutMillis = NettySystemConfig.connectTimeoutMillis; private long channelNotActiveInterval = 1000 * 60; /** * IdleStateEvent will be triggered when neither read nor write was performed for * the specified period of this time. Specify {@code 0} to disable */ - private int clientChannelMaxIdleTimeSeconds = 120; + private int clientChannelMaxIdleTimeSeconds = NettySystemConfig.clientChannelMaxIdleTimeSeconds; private int clientSocketSndBufSize = NettySystemConfig.socketSndbufSize; private int clientSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; private boolean clientPooledByteBufAllocatorEnable = false; - private boolean clientCloseSocketIfTimeout = false; + private boolean clientCloseSocketIfTimeout = NettySystemConfig.clientCloseSocketIfTimeout; + + private boolean useTLS = Boolean.parseBoolean(System.getProperty(TLS_ENABLE, + String.valueOf(TlsSystemConfig.tlsMode == TlsMode.ENFORCING))); + + private String socksProxyConfig = "{}"; + + private int writeBufferHighWaterMark = NettySystemConfig.writeBufferHighWaterMark; + private int writeBufferLowWaterMark = NettySystemConfig.writeBufferLowWaterMark; - private boolean useTLS; + private boolean disableCallbackExecutor = false; + private boolean disableNettyWorkerGroup = false; + + private long maxReconnectIntervalTimeSeconds = 60; + + private boolean enableReconnectForGoAway = true; + + private boolean enableTransparentRetry = true; public boolean isClientCloseSocketIfTimeout() { return clientCloseSocketIfTimeout; @@ -135,4 +154,68 @@ public boolean isUseTLS() { public void setUseTLS(boolean useTLS) { this.useTLS = useTLS; } + + public int getWriteBufferLowWaterMark() { + return writeBufferLowWaterMark; + } + + public void setWriteBufferLowWaterMark(int writeBufferLowWaterMark) { + this.writeBufferLowWaterMark = writeBufferLowWaterMark; + } + + public int getWriteBufferHighWaterMark() { + return writeBufferHighWaterMark; + } + + public void setWriteBufferHighWaterMark(int writeBufferHighWaterMark) { + this.writeBufferHighWaterMark = writeBufferHighWaterMark; + } + + public boolean isDisableCallbackExecutor() { + return disableCallbackExecutor; + } + + public void setDisableCallbackExecutor(boolean disableCallbackExecutor) { + this.disableCallbackExecutor = disableCallbackExecutor; + } + + public boolean isDisableNettyWorkerGroup() { + return disableNettyWorkerGroup; + } + + public void setDisableNettyWorkerGroup(boolean disableNettyWorkerGroup) { + this.disableNettyWorkerGroup = disableNettyWorkerGroup; + } + + public long getMaxReconnectIntervalTimeSeconds() { + return maxReconnectIntervalTimeSeconds; + } + + public void setMaxReconnectIntervalTimeSeconds(long maxReconnectIntervalTimeSeconds) { + this.maxReconnectIntervalTimeSeconds = maxReconnectIntervalTimeSeconds; + } + + public boolean isEnableReconnectForGoAway() { + return enableReconnectForGoAway; + } + + public void setEnableReconnectForGoAway(boolean enableReconnectForGoAway) { + this.enableReconnectForGoAway = enableReconnectForGoAway; + } + + public boolean isEnableTransparentRetry() { + return enableTransparentRetry; + } + + public void setEnableTransparentRetry(boolean enableTransparentRetry) { + this.enableTransparentRetry = enableTransparentRetry; + } + + public String getSocksProxyConfig() { + return socksProxyConfig; + } + + public void setSocksProxyConfig(String socksProxyConfig) { + this.socksProxyConfig = socksProxyConfig; + } } diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyDecoder.java b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyDecoder.java index 6e99b32f4cb..19624d74028 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyDecoder.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyDecoder.java @@ -16,18 +16,18 @@ */ package org.apache.rocketmq.remoting.netty; +import com.google.common.base.Stopwatch; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.LengthFieldBasedFrameDecoder; -import java.nio.ByteBuffer; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.remoting.common.RemotingHelper; -import org.apache.rocketmq.remoting.common.RemotingUtil; import org.apache.rocketmq.remoting.protocol.RemotingCommand; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class NettyDecoder extends LengthFieldBasedFrameDecoder { - private static final Logger log = LoggerFactory.getLogger(RemotingHelper.ROCKETMQ_REMOTING); + private static final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_REMOTING_NAME); private static final int FRAME_MAX_LENGTH = Integer.parseInt(System.getProperty("com.rocketmq.remoting.frameMaxLength", "16777216")); @@ -39,18 +39,18 @@ public NettyDecoder() { @Override public Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception { ByteBuf frame = null; + Stopwatch timer = Stopwatch.createStarted(); try { frame = (ByteBuf) super.decode(ctx, in); if (null == frame) { return null; } - - ByteBuffer byteBuffer = frame.nioBuffer(); - - return RemotingCommand.decode(byteBuffer); + RemotingCommand cmd = RemotingCommand.decode(frame); + cmd.setProcessTimer(timer); + return cmd; } catch (Exception e) { log.error("decode exception, " + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), e); - RemotingUtil.closeChannel(ctx.channel()); + RemotingHelper.closeChannel(ctx.channel()); } finally { if (null != frame) { frame.release(); diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyEncoder.java b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyEncoder.java index e46730411a7..2af0af6b725 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyEncoder.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyEncoder.java @@ -17,24 +17,24 @@ package org.apache.rocketmq.remoting.netty; import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToByteEncoder; -import java.nio.ByteBuffer; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.remoting.common.RemotingHelper; -import org.apache.rocketmq.remoting.common.RemotingUtil; import org.apache.rocketmq.remoting.protocol.RemotingCommand; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +@ChannelHandler.Sharable public class NettyEncoder extends MessageToByteEncoder { - private static final Logger log = LoggerFactory.getLogger(RemotingHelper.ROCKETMQ_REMOTING); + private static final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_REMOTING_NAME); @Override public void encode(ChannelHandlerContext ctx, RemotingCommand remotingCommand, ByteBuf out) throws Exception { try { - ByteBuffer header = remotingCommand.encodeHeader(); - out.writeBytes(header); + remotingCommand.fastEncodeHeader(out); byte[] body = remotingCommand.getBody(); if (body != null) { out.writeBytes(body); @@ -44,7 +44,7 @@ public void encode(ChannelHandlerContext ctx, RemotingCommand remotingCommand, B if (remotingCommand != null) { log.error(remotingCommand.toString()); } - RemotingUtil.closeChannel(ctx.channel()); + RemotingHelper.closeChannel(ctx.channel()); } } } diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyEventType.java b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyEventType.java index 9ac944aad30..4bc9d57dda0 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyEventType.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyEventType.java @@ -20,5 +20,6 @@ public enum NettyEventType { CONNECT, CLOSE, IDLE, - EXCEPTION + EXCEPTION, + ACTIVE } diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyLogger.java b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyLogger.java new file mode 100644 index 00000000000..955ffc1bc4f --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyLogger.java @@ -0,0 +1,361 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.netty; + + +import io.netty.util.internal.logging.InternalLogLevel; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class NettyLogger { + + private static AtomicBoolean nettyLoggerSeted = new AtomicBoolean(false); + + private static InternalLogLevel nettyLogLevel = InternalLogLevel.ERROR; + + public static void initNettyLogger() { + if (!nettyLoggerSeted.get()) { + try { + io.netty.util.internal.logging.InternalLoggerFactory.setDefaultFactory(new NettyBridgeLoggerFactory()); + } catch (Throwable e) { + //ignore + } + nettyLoggerSeted.set(true); + } + } + + private static class NettyBridgeLoggerFactory extends io.netty.util.internal.logging.InternalLoggerFactory { + @Override + protected io.netty.util.internal.logging.InternalLogger newInstance(String s) { + return new NettyBridgeLogger(s); + } + } + + private static class NettyBridgeLogger implements io.netty.util.internal.logging.InternalLogger { + + private Logger logger = null; + + private static final String EXCEPTION_MESSAGE = "Unexpected exception:"; + + public NettyBridgeLogger(String name) { + logger = LoggerFactory.getLogger(name); + } + + @Override + public String name() { + return logger.getName(); + } + + @Override + public boolean isEnabled(InternalLogLevel internalLogLevel) { + return nettyLogLevel.ordinal() <= internalLogLevel.ordinal(); + } + + @Override + public void log(InternalLogLevel internalLogLevel, String s) { + if (internalLogLevel.equals(InternalLogLevel.DEBUG)) { + logger.debug(s); + } + if (internalLogLevel.equals(InternalLogLevel.TRACE)) { + logger.info(s); + } + if (internalLogLevel.equals(InternalLogLevel.INFO)) { + logger.info(s); + } + if (internalLogLevel.equals(InternalLogLevel.WARN)) { + logger.warn(s); + } + if (internalLogLevel.equals(InternalLogLevel.ERROR)) { + logger.error(s); + } + } + + @Override + public void log(InternalLogLevel internalLogLevel, String s, Object o) { + if (internalLogLevel.equals(InternalLogLevel.DEBUG)) { + logger.debug(s, o); + } + if (internalLogLevel.equals(InternalLogLevel.TRACE)) { + logger.info(s, o); + } + if (internalLogLevel.equals(InternalLogLevel.INFO)) { + logger.info(s, o); + } + if (internalLogLevel.equals(InternalLogLevel.WARN)) { + logger.warn(s, o); + } + if (internalLogLevel.equals(InternalLogLevel.ERROR)) { + logger.error(s, o); + } + } + + @Override + public void log(InternalLogLevel internalLogLevel, String s, Object o, Object o1) { + if (internalLogLevel.equals(InternalLogLevel.DEBUG)) { + logger.debug(s, o, o1); + } + if (internalLogLevel.equals(InternalLogLevel.TRACE)) { + logger.info(s, o, o1); + } + if (internalLogLevel.equals(InternalLogLevel.INFO)) { + logger.info(s, o, o1); + } + if (internalLogLevel.equals(InternalLogLevel.WARN)) { + logger.warn(s, o, o1); + } + if (internalLogLevel.equals(InternalLogLevel.ERROR)) { + logger.error(s, o, o1); + } + } + + @Override + public void log(InternalLogLevel internalLogLevel, String s, Object... objects) { + if (internalLogLevel.equals(InternalLogLevel.DEBUG)) { + logger.debug(s, objects); + } + if (internalLogLevel.equals(InternalLogLevel.TRACE)) { + logger.info(s, objects); + } + if (internalLogLevel.equals(InternalLogLevel.INFO)) { + logger.info(s, objects); + } + if (internalLogLevel.equals(InternalLogLevel.WARN)) { + logger.warn(s, objects); + } + if (internalLogLevel.equals(InternalLogLevel.ERROR)) { + logger.error(s, objects); + } + } + + @Override + public void log(InternalLogLevel internalLogLevel, String s, Throwable throwable) { + if (internalLogLevel.equals(InternalLogLevel.DEBUG)) { + logger.debug(s, throwable); + } + if (internalLogLevel.equals(InternalLogLevel.TRACE)) { + logger.info(s, throwable); + } + if (internalLogLevel.equals(InternalLogLevel.INFO)) { + logger.info(s, throwable); + } + if (internalLogLevel.equals(InternalLogLevel.WARN)) { + logger.warn(s, throwable); + } + if (internalLogLevel.equals(InternalLogLevel.ERROR)) { + logger.error(s, throwable); + } + } + + @Override + public void log(InternalLogLevel internalLogLevel, Throwable throwable) { + if (internalLogLevel.equals(InternalLogLevel.DEBUG)) { + logger.debug(EXCEPTION_MESSAGE, throwable); + } + if (internalLogLevel.equals(InternalLogLevel.TRACE)) { + logger.info(EXCEPTION_MESSAGE, throwable); + } + if (internalLogLevel.equals(InternalLogLevel.INFO)) { + logger.info(EXCEPTION_MESSAGE, throwable); + } + if (internalLogLevel.equals(InternalLogLevel.WARN)) { + logger.warn(EXCEPTION_MESSAGE, throwable); + } + if (internalLogLevel.equals(InternalLogLevel.ERROR)) { + logger.error(EXCEPTION_MESSAGE, throwable); + } + } + + @Override + public boolean isTraceEnabled() { + return isEnabled(InternalLogLevel.TRACE); + } + + @Override + public void trace(String var1) { + logger.info(var1); + } + + @Override + public void trace(String var1, Object var2) { + logger.info(var1, var2); + } + + @Override + public void trace(String var1, Object var2, Object var3) { + logger.info(var1, var2, var3); + } + + @Override + public void trace(String var1, Object... var2) { + logger.info(var1, var2); + } + + @Override + public void trace(String var1, Throwable var2) { + logger.info(var1, var2); + } + + @Override + public void trace(Throwable var1) { + logger.info(EXCEPTION_MESSAGE, var1); + } + + @Override + public boolean isDebugEnabled() { + return isEnabled(InternalLogLevel.DEBUG); + } + + @Override + public void debug(String var1) { + logger.debug(var1); + } + + @Override + public void debug(String var1, Object var2) { + logger.debug(var1, var2); + } + + @Override + public void debug(String var1, Object var2, Object var3) { + logger.debug(var1, var2, var3); + } + + @Override + public void debug(String var1, Object... var2) { + logger.debug(var1, var2); + } + + @Override + public void debug(String var1, Throwable var2) { + logger.debug(var1, var2); + } + + @Override + public void debug(Throwable var1) { + logger.debug(EXCEPTION_MESSAGE, var1); + } + + @Override + public boolean isInfoEnabled() { + return isEnabled(InternalLogLevel.INFO); + } + + @Override + public void info(String var1) { + logger.info(var1); + } + + @Override + public void info(String var1, Object var2) { + logger.info(var1, var2); + } + + @Override + public void info(String var1, Object var2, Object var3) { + logger.info(var1, var2, var3); + } + + @Override + public void info(String var1, Object... var2) { + logger.info(var1, var2); + } + + @Override + public void info(String var1, Throwable var2) { + logger.info(var1, var2); + } + + @Override + public void info(Throwable var1) { + logger.info(EXCEPTION_MESSAGE, var1); + } + + @Override + public boolean isWarnEnabled() { + return isEnabled(InternalLogLevel.WARN); + } + + @Override + public void warn(String var1) { + logger.warn(var1); + } + + @Override + public void warn(String var1, Object var2) { + logger.warn(var1, var2); + } + + @Override + public void warn(String var1, Object... var2) { + logger.warn(var1, var2); + } + + @Override + public void warn(String var1, Object var2, Object var3) { + logger.warn(var1, var2, var3); + } + + @Override + public void warn(String var1, Throwable var2) { + logger.warn(var1, var2); + } + + @Override + public void warn(Throwable var1) { + logger.warn(EXCEPTION_MESSAGE, var1); + } + + @Override + public boolean isErrorEnabled() { + return isEnabled(InternalLogLevel.ERROR); + } + + @Override + public void error(String var1) { + logger.error(var1); + } + + @Override + public void error(String var1, Object var2) { + logger.error(var1, var2); + } + + @Override + public void error(String var1, Object var2, Object var3) { + logger.error(var1, var2, var3); + } + + @Override + public void error(String var1, Object... var2) { + logger.error(var1, var2); + } + + @Override + public void error(String var1, Throwable var2) { + logger.error(var1, var2); + } + + @Override + public void error(Throwable var1) { + logger.error(EXCEPTION_MESSAGE, var1); + } + } + +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRemotingAbstract.java b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRemotingAbstract.java index 76752529af2..62a8a72901c 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRemotingAbstract.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRemotingAbstract.java @@ -17,45 +17,67 @@ package org.apache.rocketmq.remoting.netty; import io.netty.channel.Channel; -import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslHandler; -import java.net.SocketAddress; +import io.netty.util.concurrent.Future; +import io.opentelemetry.api.common.AttributesBuilder; +import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import javax.annotation.Nullable; +import org.apache.rocketmq.common.AbortProcessException; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.remoting.ChannelEventListener; import org.apache.rocketmq.remoting.InvokeCallback; import org.apache.rocketmq.remoting.RPCHook; -import org.apache.rocketmq.remoting.common.Pair; import org.apache.rocketmq.remoting.common.RemotingHelper; import org.apache.rocketmq.remoting.common.SemaphoreReleaseOnlyOnce; -import org.apache.rocketmq.remoting.common.ServiceThread; import org.apache.rocketmq.remoting.exception.RemotingSendRequestException; import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; import org.apache.rocketmq.remoting.exception.RemotingTooMuchRequestException; +import org.apache.rocketmq.remoting.metrics.RemotingMetricsManager; import org.apache.rocketmq.remoting.protocol.RemotingCommand; import org.apache.rocketmq.remoting.protocol.RemotingSysResponseCode; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.ResponseCode; + +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.LABEL_IS_LONG_POLLING; +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.LABEL_REQUEST_CODE; +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.LABEL_RESPONSE_CODE; +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.LABEL_RESULT; +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.RESULT_ONEWAY; +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.RESULT_PROCESS_REQUEST_FAILED; +import static org.apache.rocketmq.remoting.metrics.RemotingMetricsConstant.RESULT_WRITE_CHANNEL_FAILED; public abstract class NettyRemotingAbstract { /** * Remoting logger instance. */ - private static final Logger log = LoggerFactory.getLogger(RemotingHelper.ROCKETMQ_REMOTING); + private static final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_REMOTING_NAME); /** * Semaphore to limit maximum number of on-going one-way requests, which protects system memory footprint. @@ -71,14 +93,14 @@ public abstract class NettyRemotingAbstract { * This map caches all on-going requests. */ protected final ConcurrentMap responseTable = - new ConcurrentHashMap(256); + new ConcurrentHashMap<>(256); /** * This container holds all processors per request code, aka, for each incoming request, we may look up the * responding processor in this map to handle the request. */ protected final HashMap> processorTable = - new HashMap>(64); + new HashMap<>(64); /** * Executor to feed netty events to user defined {@link ChannelEventListener}. @@ -86,20 +108,32 @@ public abstract class NettyRemotingAbstract { protected final NettyEventExecutor nettyEventExecutor = new NettyEventExecutor(); /** - * The default request processor to use in case there is no exact match in {@link #processorTable} per request code. + * The default request processor to use in case there is no exact match in {@link #processorTable} per request + * code. */ - protected Pair defaultRequestProcessor; + protected Pair defaultRequestProcessorPair; /** * SSL context via which to create {@link SslHandler}. */ - protected SslContext sslContext; + protected volatile SslContext sslContext; + + /** + * custom rpc hooks + */ + protected List rpcHooks = new ArrayList<>(); + + protected AtomicBoolean isShuttingDown = new AtomicBoolean(false); + + static { + NettyLogger.initNettyLogger(); + } /** * Constructor, specifying capacity of one-way and asynchronous semaphores. * * @param permitsOneway Number of permits for one-way requests. - * @param permitsAsync Number of permits for asynchronous requests. + * @param permitsAsync Number of permits for asynchronous requests. */ public NettyRemotingAbstract(final int permitsOneway, final int permitsAsync) { this.semaphoreOneway = new Semaphore(permitsOneway, true); @@ -136,17 +170,15 @@ public void putNettyEvent(final NettyEvent event) { * * @param ctx Channel handler context. * @param msg incoming remoting command. - * @throws Exception if there were any error while processing the incoming command. */ - public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception { - final RemotingCommand cmd = msg; - if (cmd != null) { - switch (cmd.getType()) { + public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand msg) { + if (msg != null) { + switch (msg.getType()) { case REQUEST_COMMAND: - processRequestCommand(ctx, cmd); + processRequestCommand(ctx, msg); break; case RESPONSE_COMMAND: - processResponseCommand(ctx, cmd); + processResponseCommand(ctx, msg); break; default: break; @@ -154,6 +186,66 @@ public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand ms } } + protected void doBeforeRpcHooks(String addr, RemotingCommand request) { + if (rpcHooks.size() > 0) { + for (RPCHook rpcHook : rpcHooks) { + rpcHook.doBeforeRequest(addr, request); + } + } + } + + public void doAfterRpcHooks(String addr, RemotingCommand request, RemotingCommand response) { + if (rpcHooks.size() > 0) { + for (RPCHook rpcHook : rpcHooks) { + rpcHook.doAfterResponse(addr, request, response); + } + } + } + + public static void writeResponse(Channel channel, RemotingCommand request, @Nullable RemotingCommand response) { + writeResponse(channel, request, response, null); + } + + public static void writeResponse(Channel channel, RemotingCommand request, @Nullable RemotingCommand response, + Consumer> callback) { + if (response == null) { + return; + } + AttributesBuilder attributesBuilder = RemotingMetricsManager.newAttributesBuilder() + .put(LABEL_IS_LONG_POLLING, request.isSuspended()) + .put(LABEL_REQUEST_CODE, RemotingHelper.getRequestCodeDesc(request.getCode())) + .put(LABEL_RESPONSE_CODE, RemotingHelper.getResponseCodeDesc(response.getCode())); + if (request.isOnewayRPC()) { + attributesBuilder.put(LABEL_RESULT, RESULT_ONEWAY); + RemotingMetricsManager.rpcLatency.record(request.getProcessTimer().elapsed(TimeUnit.MILLISECONDS), attributesBuilder.build()); + return; + } + response.setOpaque(request.getOpaque()); + response.markResponseType(); + try { + channel.writeAndFlush(response).addListener((ChannelFutureListener) future -> { + if (future.isSuccess()) { + log.debug("Response[request code: {}, response code: {}, opaque: {}] is written to channel{}", + request.getCode(), response.getCode(), response.getOpaque(), channel); + } else { + log.error("Failed to write response[request code: {}, response code: {}, opaque: {}] to channel{}", + request.getCode(), response.getCode(), response.getOpaque(), channel, future.cause()); + } + attributesBuilder.put(LABEL_RESULT, RemotingMetricsManager.getWriteAndFlushResult(future)); + RemotingMetricsManager.rpcLatency.record(request.getProcessTimer().elapsed(TimeUnit.MILLISECONDS), attributesBuilder.build()); + if (callback != null) { + callback.accept(future); + } + }); + } catch (Throwable e) { + log.error("process request over, but response failed", e); + log.error(request.toString()); + log.error(response.toString()); + attributesBuilder.put(LABEL_RESULT, RESULT_WRITE_CHANNEL_FAILED); + RemotingMetricsManager.rpcLatency.record(request.getProcessTimer().elapsed(TimeUnit.MILLISECONDS), attributesBuilder.build()); + } + } + /** * Process incoming request command issued by remote peer. * @@ -162,87 +254,114 @@ public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand ms */ public void processRequestCommand(final ChannelHandlerContext ctx, final RemotingCommand cmd) { final Pair matched = this.processorTable.get(cmd.getCode()); - final Pair pair = null == matched ? this.defaultRequestProcessor : matched; + final Pair pair = null == matched ? this.defaultRequestProcessorPair : matched; final int opaque = cmd.getOpaque(); - if (pair != null) { - Runnable run = new Runnable() { - @Override - public void run() { - try { - RPCHook rpcHook = NettyRemotingAbstract.this.getRPCHook(); - if (rpcHook != null) { - rpcHook.doBeforeRequest(RemotingHelper.parseChannelRemoteAddr(ctx.channel()), cmd); - } - - final RemotingCommand response = pair.getObject1().processRequest(ctx, cmd); - if (rpcHook != null) { - rpcHook.doAfterResponse(RemotingHelper.parseChannelRemoteAddr(ctx.channel()), cmd, response); - } + if (pair == null) { + String error = " request type " + cmd.getCode() + " not supported"; + final RemotingCommand response = + RemotingCommand.createResponseCommand(RemotingSysResponseCode.REQUEST_CODE_NOT_SUPPORTED, error); + response.setOpaque(opaque); + writeResponse(ctx.channel(), cmd, response); + log.error(RemotingHelper.parseChannelRemoteAddr(ctx.channel()) + error); + return; + } - if (!cmd.isOnewayRPC()) { - if (response != null) { - response.setOpaque(opaque); - response.markResponseType(); - try { - ctx.writeAndFlush(response); - } catch (Throwable e) { - log.error("process request over, but response failed", e); - log.error(cmd.toString()); - log.error(response.toString()); - } - } else { - - } - } - } catch (Throwable e) { - log.error("process request exception", e); - log.error(cmd.toString()); - - if (!cmd.isOnewayRPC()) { - final RemotingCommand response = RemotingCommand.createResponseCommand(RemotingSysResponseCode.SYSTEM_ERROR, - RemotingHelper.exceptionSimpleDesc(e)); - response.setOpaque(opaque); - ctx.writeAndFlush(response); - } - } - } - }; + Runnable run = buildProcessRequestHandler(ctx, cmd, pair, opaque); - if (pair.getObject1().rejectRequest()) { - final RemotingCommand response = RemotingCommand.createResponseCommand(RemotingSysResponseCode.SYSTEM_BUSY, - "[REJECTREQUEST]system busy, start flow control for a while"); + if (isShuttingDown.get()) { + if (cmd.getVersion() > MQVersion.Version.V5_1_4.ordinal()) { + final RemotingCommand response = RemotingCommand.createResponseCommand(ResponseCode.GO_AWAY, + "please go away"); response.setOpaque(opaque); - ctx.writeAndFlush(response); + writeResponse(ctx.channel(), cmd, response); return; } + } + + if (pair.getObject1().rejectRequest()) { + final RemotingCommand response = RemotingCommand.createResponseCommand(RemotingSysResponseCode.SYSTEM_BUSY, + "[REJECTREQUEST]system busy, start flow control for a while"); + response.setOpaque(opaque); + writeResponse(ctx.channel(), cmd, response); + return; + } + + try { + final RequestTask requestTask = new RequestTask(run, ctx.channel(), cmd); + //async execute task, current thread return directly + pair.getObject2().submit(requestTask); + } catch (RejectedExecutionException e) { + if ((System.currentTimeMillis() % 10000) == 0) { + log.warn(RemotingHelper.parseChannelRemoteAddr(ctx.channel()) + + ", too many requests and system thread pool busy, RejectedExecutionException " + + pair.getObject2().toString() + + " request code: " + cmd.getCode()); + } + + final RemotingCommand response = RemotingCommand.createResponseCommand(RemotingSysResponseCode.SYSTEM_BUSY, + "[OVERLOAD]system busy, start flow control for a while"); + response.setOpaque(opaque); + writeResponse(ctx.channel(), cmd, response); + } catch (Throwable e) { + AttributesBuilder attributesBuilder = RemotingMetricsManager.newAttributesBuilder() + .put(LABEL_REQUEST_CODE, RemotingHelper.getRequestCodeDesc(cmd.getCode())) + .put(LABEL_RESULT, RESULT_PROCESS_REQUEST_FAILED); + RemotingMetricsManager.rpcLatency.record(cmd.getProcessTimer().elapsed(TimeUnit.MILLISECONDS), attributesBuilder.build()); + } + } + + private Runnable buildProcessRequestHandler(ChannelHandlerContext ctx, RemotingCommand cmd, + Pair pair, int opaque) { + return () -> { + Exception exception = null; + RemotingCommand response; try { - final RequestTask requestTask = new RequestTask(run, ctx.channel(), cmd); - pair.getObject2().submit(requestTask); - } catch (RejectedExecutionException e) { - if ((System.currentTimeMillis() % 10000) == 0) { - log.warn(RemotingHelper.parseChannelRemoteAddr(ctx.channel()) - + ", too many requests and system thread pool busy, RejectedExecutionException " - + pair.getObject2().toString() - + " request code: " + cmd.getCode()); + String remoteAddr = RemotingHelper.parseChannelRemoteAddr(ctx.channel()); + try { + doBeforeRpcHooks(remoteAddr, cmd); + } catch (AbortProcessException e) { + throw e; + } catch (Exception e) { + exception = e; + } + + if (exception == null) { + response = pair.getObject1().processRequest(ctx, cmd); + } else { + response = RemotingCommand.createResponseCommand(RemotingSysResponseCode.SYSTEM_ERROR, null); + } + + try { + doAfterRpcHooks(remoteAddr, cmd, response); + } catch (AbortProcessException e) { + throw e; + } catch (Exception e) { + exception = e; + } + + if (exception != null) { + throw exception; } + writeResponse(ctx.channel(), cmd, response); + } catch (AbortProcessException e) { + response = RemotingCommand.createResponseCommand(e.getResponseCode(), e.getErrorMessage()); + response.setOpaque(opaque); + writeResponse(ctx.channel(), cmd, response); + } catch (Throwable e) { + log.error("process request exception", e); + log.error(cmd.toString()); + if (!cmd.isOnewayRPC()) { - final RemotingCommand response = RemotingCommand.createResponseCommand(RemotingSysResponseCode.SYSTEM_BUSY, - "[OVERLOAD]system busy, start flow control for a while"); + response = RemotingCommand.createResponseCommand(RemotingSysResponseCode.SYSTEM_ERROR, + UtilAll.exceptionSimpleDesc(e)); response.setOpaque(opaque); - ctx.writeAndFlush(response); + writeResponse(ctx.channel(), cmd, response); } } - } else { - String error = " request type " + cmd.getCode() + " not supported"; - final RemotingCommand response = - RemotingCommand.createResponseCommand(RemotingSysResponseCode.REQUEST_CODE_NOT_SUPPORTED, error); - response.setOpaque(opaque); - ctx.writeAndFlush(response); - log.error(RemotingHelper.parseChannelRemoteAddr(ctx.channel()) + error); - } + }; } /** @@ -266,8 +385,7 @@ public void processResponseCommand(ChannelHandlerContext ctx, RemotingCommand cm responseFuture.release(); } } else { - log.warn("receive response, but not matched any request, " + RemotingHelper.parseChannelRemoteAddr(ctx.channel())); - log.warn(cmd.toString()); + log.warn("receive response, cmd={}, but not matched any request, address={}", cmd, RemotingHelper.parseChannelRemoteAddr(ctx.channel())); } } @@ -277,18 +395,15 @@ public void processResponseCommand(ChannelHandlerContext ctx, RemotingCommand cm private void executeInvokeCallback(final ResponseFuture responseFuture) { boolean runInThisThread = false; ExecutorService executor = this.getCallbackExecutor(); - if (executor != null) { + if (executor != null && !executor.isShutdown()) { try { - executor.submit(new Runnable() { - @Override - public void run() { - try { - responseFuture.executeInvokeCallback(); - } catch (Throwable e) { - log.warn("execute callback in executor exception, and callback throw", e); - } finally { - responseFuture.release(); - } + executor.submit(() -> { + try { + responseFuture.executeInvokeCallback(); + } catch (Throwable e) { + log.warn("execute callback in executor exception, and callback throw", e); + } finally { + responseFuture.release(); } }); } catch (Exception e) { @@ -311,11 +426,23 @@ public void run() { } /** - * Custom RPC hook. + * Custom RPC hooks. * - * @return RPC hook if specified; null otherwise. + * @return RPC hooks if specified; null otherwise. */ - public abstract RPCHook getRPCHook(); + public List getRPCHook() { + return rpcHooks; + } + + public void registerRPCHook(RPCHook rpcHook) { + if (rpcHook != null && !rpcHooks.contains(rpcHook)) { + rpcHooks.add(rpcHook); + } + } + + public void clearRPCHook() { + rpcHooks.clear(); + } /** * This method specifies thread pool to use while invoking callback methods. @@ -331,7 +458,7 @@ public void run() { *

    */ public void scanResponseTable() { - final List rfList = new LinkedList(); + final List rfList = new LinkedList<>(); Iterator> it = this.responseTable.entrySet().iterator(); while (it.hasNext()) { Entry next = it.next(); @@ -357,87 +484,89 @@ public void scanResponseTable() { public RemotingCommand invokeSyncImpl(final Channel channel, final RemotingCommand request, final long timeoutMillis) throws InterruptedException, RemotingSendRequestException, RemotingTimeoutException { - final int opaque = request.getOpaque(); - try { - final ResponseFuture responseFuture = new ResponseFuture(opaque, timeoutMillis, null, null); - this.responseTable.put(opaque, responseFuture); - final SocketAddress addr = channel.remoteAddress(); - channel.writeAndFlush(request).addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture f) throws Exception { - if (f.isSuccess()) { - responseFuture.setSendRequestOK(true); - return; - } else { - responseFuture.setSendRequestOK(false); - } - - responseTable.remove(opaque); - responseFuture.setCause(f.cause()); - responseFuture.putResponse(null); - log.warn("send a request command to channel <" + addr + "> failed."); - } - }); + return invokeImpl(channel, request, timeoutMillis).thenApply(ResponseFuture::getResponseCommand) + .get(timeoutMillis, TimeUnit.MILLISECONDS); + } catch (ExecutionException e) { + throw new RemotingSendRequestException(channel.remoteAddress().toString(), e.getCause()); + } catch (TimeoutException e) { + throw new RemotingTimeoutException(channel.remoteAddress().toString(), timeoutMillis, e.getCause()); + } + } - RemotingCommand responseCommand = responseFuture.waitResponse(timeoutMillis); - if (null == responseCommand) { - if (responseFuture.isSendRequestOK()) { - throw new RemotingTimeoutException(RemotingHelper.parseSocketAddressAddr(addr), timeoutMillis, - responseFuture.getCause()); - } else { - throw new RemotingSendRequestException(RemotingHelper.parseSocketAddressAddr(addr), responseFuture.getCause()); - } + public CompletableFuture invokeImpl(final Channel channel, final RemotingCommand request, + final long timeoutMillis) { + String channelRemoteAddr = RemotingHelper.parseChannelRemoteAddr(channel); + doBeforeRpcHooks(channelRemoteAddr, request); + return invoke0(channel, request, timeoutMillis).whenComplete((v, t) -> { + if (t == null) { + doAfterRpcHooks(channelRemoteAddr, request, v.getResponseCommand()); } - - return responseCommand; - } finally { - this.responseTable.remove(opaque); - } + }); } - public void invokeAsyncImpl(final Channel channel, final RemotingCommand request, final long timeoutMillis, - final InvokeCallback invokeCallback) - throws InterruptedException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException { + protected CompletableFuture invoke0(final Channel channel, final RemotingCommand request, + final long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + long beginStartTime = System.currentTimeMillis(); final int opaque = request.getOpaque(); - boolean acquired = this.semaphoreAsync.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS); + + boolean acquired; + try { + acquired = this.semaphoreAsync.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS); + } catch (Throwable t) { + future.completeExceptionally(t); + return future; + } if (acquired) { final SemaphoreReleaseOnlyOnce once = new SemaphoreReleaseOnlyOnce(this.semaphoreAsync); + long costTime = System.currentTimeMillis() - beginStartTime; + if (timeoutMillis < costTime) { + once.release(); + future.completeExceptionally(new RemotingTimeoutException("invokeAsyncImpl call timeout")); + return future; + } - final ResponseFuture responseFuture = new ResponseFuture(opaque, timeoutMillis, invokeCallback, once); - this.responseTable.put(opaque, responseFuture); - try { - channel.writeAndFlush(request).addListener(new ChannelFutureListener() { + AtomicReference responseFutureReference = new AtomicReference<>(); + final ResponseFuture responseFuture = new ResponseFuture(channel, opaque, request, timeoutMillis - costTime, + new InvokeCallback() { @Override - public void operationComplete(ChannelFuture f) throws Exception { - if (f.isSuccess()) { - responseFuture.setSendRequestOK(true); - return; - } else { - responseFuture.setSendRequestOK(false); - } + public void operationComplete(ResponseFuture responseFuture) { - responseFuture.putResponse(null); - responseTable.remove(opaque); - try { - executeInvokeCallback(responseFuture); - } catch (Throwable e) { - log.warn("excute callback in writeAndFlush addListener, and callback throw", e); - } finally { - responseFuture.release(); - } + } + + @Override + public void operationSucceed(RemotingCommand response) { + future.complete(responseFutureReference.get()); + } - log.warn("send a request command to channel <{}> failed.", RemotingHelper.parseChannelRemoteAddr(channel)); + @Override + public void operationFail(Throwable throwable) { + future.completeExceptionally(throwable); + } + }, once); + responseFutureReference.set(responseFuture); + this.responseTable.put(opaque, responseFuture); + try { + channel.writeAndFlush(request).addListener((ChannelFutureListener) f -> { + if (f.isSuccess()) { + responseFuture.setSendRequestOK(true); + return; } + requestFail(opaque); + log.warn("send a request command to channel <{}> failed.", RemotingHelper.parseChannelRemoteAddr(channel)); }); + return future; } catch (Exception e) { + responseTable.remove(opaque); responseFuture.release(); log.warn("send a request command to channel <" + RemotingHelper.parseChannelRemoteAddr(channel) + "> Exception", e); - throw new RemotingSendRequestException(RemotingHelper.parseChannelRemoteAddr(channel), e); + future.completeExceptionally(new RemotingSendRequestException(RemotingHelper.parseChannelRemoteAddr(channel), e)); + return future; } } else { if (timeoutMillis <= 0) { - throw new RemotingTooMuchRequestException("invokeAsyncImpl invoke too fast"); + future.completeExceptionally(new RemotingTooMuchRequestException("invokeAsyncImpl invoke too fast")); } else { String info = String.format("invokeAsyncImpl tryAcquire semaphore timeout, %dms, waiting thread nums: %d semaphoreAsyncValue: %d", @@ -446,7 +575,58 @@ public void operationComplete(ChannelFuture f) throws Exception { this.semaphoreAsync.availablePermits() ); log.warn(info); - throw new RemotingTimeoutException(info); + future.completeExceptionally(new RemotingTimeoutException(info)); + } + return future; + } + } + + public void invokeAsyncImpl(final Channel channel, final RemotingCommand request, final long timeoutMillis, + final InvokeCallback invokeCallback) { + invokeImpl(channel, request, timeoutMillis) + .whenComplete((v, t) -> { + if (t == null) { + invokeCallback.operationComplete(v); + } else { + ResponseFuture responseFuture = new ResponseFuture(channel, request.getOpaque(), request, timeoutMillis, null, null); + responseFuture.setCause(t); + invokeCallback.operationComplete(responseFuture); + } + }) + .thenAccept(responseFuture -> invokeCallback.operationSucceed(responseFuture.getResponseCommand())) + .exceptionally(t -> { + invokeCallback.operationFail(t); + return null; + }); + } + + private void requestFail(final int opaque) { + ResponseFuture responseFuture = responseTable.remove(opaque); + if (responseFuture != null) { + responseFuture.setSendRequestOK(false); + responseFuture.putResponse(null); + try { + executeInvokeCallback(responseFuture); + } catch (Throwable e) { + log.warn("execute callback in requestFail, and callback throw", e); + } finally { + responseFuture.release(); + } + } + } + + /** + * mark the request of the specified channel as fail and to invoke fail callback immediately + * + * @param channel the channel which is close already + */ + protected void failFast(final Channel channel) { + for (Entry entry : responseTable.entrySet()) { + if (entry.getValue().getChannel() == channel) { + Integer opaque = entry.getKey(); + if (opaque != null) { + requestFail(opaque); + } } } } @@ -458,13 +638,10 @@ public void invokeOnewayImpl(final Channel channel, final RemotingCommand reques if (acquired) { final SemaphoreReleaseOnlyOnce once = new SemaphoreReleaseOnlyOnce(this.semaphoreOneway); try { - channel.writeAndFlush(request).addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture f) throws Exception { - once.release(); - if (!f.isSuccess()) { - log.warn("send a request command to channel <" + channel.remoteAddress() + "> failed."); - } + channel.writeAndFlush(request).addListener((ChannelFutureListener) f -> { + once.release(); + if (!f.isSuccess()) { + log.warn("send a request command to channel <" + channel.remoteAddress() + "> failed."); } }); } catch (Exception e) { @@ -477,7 +654,7 @@ public void operationComplete(ChannelFuture f) throws Exception { throw new RemotingTooMuchRequestException("invokeOnewayImpl invoke too fast"); } else { String info = String.format( - "invokeOnewayImpl tryAcquire semaphore timeout, %dms, waiting thread nums: %d semaphoreAsyncValue: %d", + "invokeOnewayImpl tryAcquire semaphore timeout, %dms, waiting thread nums: %d semaphoreOnewayValue: %d", timeoutMillis, this.semaphoreOneway.getQueueLength(), this.semaphoreOneway.availablePermits() @@ -489,14 +666,15 @@ public void operationComplete(ChannelFuture f) throws Exception { } class NettyEventExecutor extends ServiceThread { - private final LinkedBlockingQueue eventQueue = new LinkedBlockingQueue(); - private final int maxSize = 10000; + private final LinkedBlockingQueue eventQueue = new LinkedBlockingQueue<>(); public void putNettyEvent(final NettyEvent event) { - if (this.eventQueue.size() <= maxSize) { + int currentSize = this.eventQueue.size(); + int maxSize = 10000; + if (currentSize <= maxSize) { this.eventQueue.add(event); } else { - log.warn("event queue size[{}] enough, so drop this event {}", this.eventQueue.size(), event.toString()); + log.warn("event queue size [{}] over the limit [{}], so drop this event {}", currentSize, maxSize, event.toString()); } } @@ -523,6 +701,9 @@ public void run() { case EXCEPTION: listener.onChannelException(event.getRemoteAddr(), event.getChannel()); break; + case ACTIVE: + listener.onChannelActive(event.getRemoteAddr(), event.getChannel()); + break; default: break; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRemotingClient.java b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRemotingClient.java index dcc80cba050..f5157d03049 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRemotingClient.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRemotingClient.java @@ -16,7 +16,11 @@ */ package org.apache.rocketmq.remoting.netty; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.TypeReference; +import com.google.common.base.Stopwatch; import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.PooledByteBufAllocator; import io.netty.channel.Channel; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelFuture; @@ -27,74 +31,96 @@ import io.netty.channel.ChannelPromise; import io.netty.channel.EventLoopGroup; import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.WriteBufferWaterMark; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.proxy.Socks5ProxyHandler; import io.netty.handler.timeout.IdleState; import io.netty.handler.timeout.IdleStateEvent; import io.netty.handler.timeout.IdleStateHandler; +import io.netty.resolver.NoopAddressResolverGroup; +import io.netty.util.HashedWheelTimer; +import io.netty.util.Timeout; +import io.netty.util.TimerTask; import io.netty.util.concurrent.DefaultEventExecutorGroup; +import io.netty.util.concurrent.EventExecutorGroup; import java.io.IOException; +import java.net.InetSocketAddress; import java.net.SocketAddress; import java.security.cert.CertificateException; +import java.time.Duration; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Random; -import java.util.Timer; -import java.util.TimerTask; +import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.remoting.ChannelEventListener; import org.apache.rocketmq.remoting.InvokeCallback; -import org.apache.rocketmq.remoting.RPCHook; import org.apache.rocketmq.remoting.RemotingClient; -import org.apache.rocketmq.remoting.common.Pair; import org.apache.rocketmq.remoting.common.RemotingHelper; -import org.apache.rocketmq.remoting.common.RemotingUtil; import org.apache.rocketmq.remoting.exception.RemotingConnectException; import org.apache.rocketmq.remoting.exception.RemotingSendRequestException; import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; import org.apache.rocketmq.remoting.exception.RemotingTooMuchRequestException; import org.apache.rocketmq.remoting.protocol.RemotingCommand; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.proxy.SocksProxyConfig; public class NettyRemotingClient extends NettyRemotingAbstract implements RemotingClient { - private static final Logger log = LoggerFactory.getLogger(RemotingHelper.ROCKETMQ_REMOTING); + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.ROCKETMQ_REMOTING_NAME); private static final long LOCK_TIMEOUT_MILLIS = 3000; + private static final long MIN_CLOSE_TIMEOUT_MILLIS = 100; private final NettyClientConfig nettyClientConfig; private final Bootstrap bootstrap = new Bootstrap(); private final EventLoopGroup eventLoopGroupWorker; private final Lock lockChannelTables = new ReentrantLock(); - private final ConcurrentMap channelTables = new ConcurrentHashMap(); + private final Map proxyMap = new HashMap<>(); + private final ConcurrentHashMap bootstrapMap = new ConcurrentHashMap<>(); + private final ConcurrentMap channelTables = new ConcurrentHashMap<>(); + private final ConcurrentMap channelWrapperTables = new ConcurrentHashMap<>(); - private final Timer timer = new Timer("ClientHouseKeepingService", true); + private final HashedWheelTimer timer = new HashedWheelTimer(r -> new Thread(r, "ClientHouseKeepingService")); - private final AtomicReference> namesrvAddrList = new AtomicReference>(); - private final AtomicReference namesrvAddrChoosed = new AtomicReference(); + private final AtomicReference> namesrvAddrList = new AtomicReference<>(); + private final ConcurrentMap availableNamesrvAddrMap = new ConcurrentHashMap<>(); + private final AtomicReference namesrvAddrChoosed = new AtomicReference<>(); private final AtomicInteger namesrvIndex = new AtomicInteger(initValueIndex()); - private final Lock lockNamesrvChannel = new ReentrantLock(); + private final Lock namesrvChannelLock = new ReentrantLock(); private final ExecutorService publicExecutor; + private final ExecutorService scanExecutor; /** * Invoke the callback methods in this executor when process response. */ private ExecutorService callbackExecutor; private final ChannelEventListener channelEventListener; - private DefaultEventExecutorGroup defaultEventExecutorGroup; - private RPCHook rpcHook; + private EventExecutorGroup defaultEventExecutorGroup; public NettyRemotingClient(final NettyClientConfig nettyClientConfig) { this(nettyClientConfig, null); @@ -102,41 +128,44 @@ public NettyRemotingClient(final NettyClientConfig nettyClientConfig) { public NettyRemotingClient(final NettyClientConfig nettyClientConfig, final ChannelEventListener channelEventListener) { + this(nettyClientConfig, channelEventListener, null, null); + } + + public NettyRemotingClient(final NettyClientConfig nettyClientConfig, + final ChannelEventListener channelEventListener, + final EventLoopGroup eventLoopGroup, + final EventExecutorGroup eventExecutorGroup) { super(nettyClientConfig.getClientOnewaySemaphoreValue(), nettyClientConfig.getClientAsyncSemaphoreValue()); this.nettyClientConfig = nettyClientConfig; this.channelEventListener = channelEventListener; + this.loadSocksProxyJson(); + int publicThreadNums = nettyClientConfig.getClientCallbackExecutorThreads(); if (publicThreadNums <= 0) { publicThreadNums = 4; } - this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums, new ThreadFactory() { - private AtomicInteger threadIndex = new AtomicInteger(0); - - @Override - public Thread newThread(Runnable r) { - return new Thread(r, "NettyClientPublicExecutor_" + this.threadIndex.incrementAndGet()); - } - }); + this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums, new ThreadFactoryImpl("NettyClientPublicExecutor_")); - this.eventLoopGroupWorker = new NioEventLoopGroup(1, new ThreadFactory() { - private AtomicInteger threadIndex = new AtomicInteger(0); + this.scanExecutor = ThreadUtils.newThreadPoolExecutor(4, 10, 60, TimeUnit.SECONDS, + new ArrayBlockingQueue<>(32), new ThreadFactoryImpl("NettyClientScan_thread_")); - @Override - public Thread newThread(Runnable r) { - return new Thread(r, String.format("NettyClientSelector_%d", this.threadIndex.incrementAndGet())); - } - }); + if (eventLoopGroup != null) { + this.eventLoopGroupWorker = eventLoopGroup; + } else { + this.eventLoopGroupWorker = new NioEventLoopGroup(1, new ThreadFactoryImpl("NettyClientSelector_")); + } + this.defaultEventExecutorGroup = eventExecutorGroup; if (nettyClientConfig.isUseTLS()) { try { sslContext = TlsHelper.buildSslContext(true); - log.info("SSL enabled for client"); + LOGGER.info("SSL enabled for client"); } catch (IOException e) { - log.error("Failed to create SSLContext", e); + LOGGER.error("Failed to create SSLContext", e); } catch (CertificateException e) { - log.error("Failed to create SSLContext", e); + LOGGER.error("Failed to create SSLContext", e); throw new RuntimeException("Failed to create SSLContext", e); } } @@ -144,30 +173,29 @@ public Thread newThread(Runnable r) { private static int initValueIndex() { Random r = new Random(); + return r.nextInt(999); + } - return Math.abs(r.nextInt() % 999) % 999; + private void loadSocksProxyJson() { + Map sockProxyMap = JSON.parseObject( + nettyClientConfig.getSocksProxyConfig(), new TypeReference>() { + }); + if (sockProxyMap != null) { + proxyMap.putAll(sockProxyMap); + } } @Override public void start() { - this.defaultEventExecutorGroup = new DefaultEventExecutorGroup( - nettyClientConfig.getClientWorkerThreads(), - new ThreadFactory() { - - private AtomicInteger threadIndex = new AtomicInteger(0); - - @Override - public Thread newThread(Runnable r) { - return new Thread(r, "NettyClientWorkerThread_" + this.threadIndex.incrementAndGet()); - } - }); - + if (this.defaultEventExecutorGroup == null) { + this.defaultEventExecutorGroup = new DefaultEventExecutorGroup( + nettyClientConfig.getClientWorkerThreads(), + new ThreadFactoryImpl("NettyClientWorkerThread_")); + } Bootstrap handler = this.bootstrap.group(this.eventLoopGroupWorker).channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .option(ChannelOption.SO_KEEPALIVE, false) .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, nettyClientConfig.getConnectTimeoutMillis()) - .option(ChannelOption.SO_SNDBUF, nettyClientConfig.getClientSocketSndBufSize()) - .option(ChannelOption.SO_RCVBUF, nettyClientConfig.getClientSocketRcvBufSize()) .handler(new ChannelInitializer() { @Override public void initChannel(SocketChannel ch) throws Exception { @@ -175,13 +203,13 @@ public void initChannel(SocketChannel ch) throws Exception { if (nettyClientConfig.isUseTLS()) { if (null != sslContext) { pipeline.addFirst(defaultEventExecutorGroup, "sslHandler", sslContext.newHandler(ch.alloc())); - log.info("Prepend SSL handler"); + LOGGER.info("Prepend SSL handler"); } else { - log.warn("Connections are insecure as SSLContext is null!"); + LOGGER.warn("Connections are insecure as SSLContext is null!"); } } - pipeline.addLast( - defaultEventExecutorGroup, + ch.pipeline().addLast( + nettyClientConfig.isDisableNettyWorkerGroup() ? null : defaultEventExecutorGroup, new NettyEncoder(), new NettyDecoder(), new IdleStateHandler(0, 0, nettyClientConfig.getClientChannelMaxIdleTimeSeconds()), @@ -189,32 +217,156 @@ public void initChannel(SocketChannel ch) throws Exception { new NettyClientHandler()); } }); + if (nettyClientConfig.getClientSocketSndBufSize() > 0) { + LOGGER.info("client set SO_SNDBUF to {}", nettyClientConfig.getClientSocketSndBufSize()); + handler.option(ChannelOption.SO_SNDBUF, nettyClientConfig.getClientSocketSndBufSize()); + } + if (nettyClientConfig.getClientSocketRcvBufSize() > 0) { + LOGGER.info("client set SO_RCVBUF to {}", nettyClientConfig.getClientSocketRcvBufSize()); + handler.option(ChannelOption.SO_RCVBUF, nettyClientConfig.getClientSocketRcvBufSize()); + } + if (nettyClientConfig.getWriteBufferLowWaterMark() > 0 && nettyClientConfig.getWriteBufferHighWaterMark() > 0) { + LOGGER.info("client set netty WRITE_BUFFER_WATER_MARK to {},{}", + nettyClientConfig.getWriteBufferLowWaterMark(), nettyClientConfig.getWriteBufferHighWaterMark()); + handler.option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark( + nettyClientConfig.getWriteBufferLowWaterMark(), nettyClientConfig.getWriteBufferHighWaterMark())); + } + if (nettyClientConfig.isClientPooledByteBufAllocatorEnable()) { + handler.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT); + } - this.timer.scheduleAtFixedRate(new TimerTask() { + nettyEventExecutor.start(); + + TimerTask timerTaskScanResponseTable = new TimerTask() { @Override - public void run() { + public void run(Timeout timeout) { try { NettyRemotingClient.this.scanResponseTable(); } catch (Throwable e) { - log.error("scanResponseTable exception", e); + LOGGER.error("scanResponseTable exception", e); + } finally { + timer.newTimeout(this, 1000, TimeUnit.MILLISECONDS); } } - }, 1000 * 3, 1000); + }; + this.timer.newTimeout(timerTaskScanResponseTable, 1000 * 3, TimeUnit.MILLISECONDS); + + int connectTimeoutMillis = this.nettyClientConfig.getConnectTimeoutMillis(); + TimerTask timerTaskScanAvailableNameSrv = new TimerTask() { + @Override + public void run(Timeout timeout) { + try { + NettyRemotingClient.this.scanAvailableNameSrv(); + } catch (Exception e) { + LOGGER.error("scanAvailableNameSrv exception", e); + } finally { + timer.newTimeout(this, connectTimeoutMillis, TimeUnit.MILLISECONDS); + } + } + }; + this.timer.newTimeout(timerTaskScanAvailableNameSrv, 0, TimeUnit.MILLISECONDS); + } + + private Map.Entry getProxy(String addr) { + if (StringUtils.isBlank(addr) || !addr.contains(":")) { + return null; + } + String[] hostAndPort = this.getHostAndPort(addr); + for (Map.Entry entry : proxyMap.entrySet()) { + String cidr = entry.getKey(); + if (RemotingHelper.DEFAULT_CIDR_ALL.equals(cidr) || RemotingHelper.ipInCIDR(hostAndPort[0], cidr)) { + return entry; + } + } + return null; + } + + private Bootstrap fetchBootstrap(String addr) { + Map.Entry proxyEntry = getProxy(addr); + if (proxyEntry == null) { + return bootstrap; + } + + String cidr = proxyEntry.getKey(); + SocksProxyConfig socksProxyConfig = proxyEntry.getValue(); + + LOGGER.info("Netty fetch bootstrap, addr: {}, cidr: {}, proxy: {}", + addr, cidr, socksProxyConfig != null ? socksProxyConfig.getAddr() : ""); + + Bootstrap bootstrapWithProxy = bootstrapMap.get(cidr); + if (bootstrapWithProxy == null) { + bootstrapWithProxy = createBootstrap(socksProxyConfig); + Bootstrap old = bootstrapMap.putIfAbsent(cidr, bootstrapWithProxy); + if (old != null) { + bootstrapWithProxy = old; + } + } + return bootstrapWithProxy; + } + + private Bootstrap createBootstrap(final SocksProxyConfig proxy) { + Bootstrap bootstrap = new Bootstrap(); + bootstrap.group(this.eventLoopGroupWorker).channel(NioSocketChannel.class) + .option(ChannelOption.TCP_NODELAY, true) + .option(ChannelOption.SO_KEEPALIVE, false) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, nettyClientConfig.getConnectTimeoutMillis()) + .option(ChannelOption.SO_SNDBUF, nettyClientConfig.getClientSocketSndBufSize()) + .option(ChannelOption.SO_RCVBUF, nettyClientConfig.getClientSocketRcvBufSize()) + .handler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) { + ChannelPipeline pipeline = ch.pipeline(); + if (nettyClientConfig.isUseTLS()) { + if (null != sslContext) { + pipeline.addFirst(defaultEventExecutorGroup, + "sslHandler", sslContext.newHandler(ch.alloc())); + LOGGER.info("Prepend SSL handler"); + } else { + LOGGER.warn("Connections are insecure as SSLContext is null!"); + } + } + + // Netty Socks5 Proxy + if (proxy != null) { + String[] hostAndPort = getHostAndPort(proxy.getAddr()); + pipeline.addFirst(new Socks5ProxyHandler( + new InetSocketAddress(hostAndPort[0], Integer.parseInt(hostAndPort[1])), + proxy.getUsername(), proxy.getPassword())); + } + + pipeline.addLast( + nettyClientConfig.isDisableNettyWorkerGroup() ? null : defaultEventExecutorGroup, + new NettyEncoder(), + new NettyDecoder(), + new IdleStateHandler(0, 0, nettyClientConfig.getClientChannelMaxIdleTimeSeconds()), + new NettyConnectManageHandler(), + new NettyClientHandler()); + } + }); - if (this.channelEventListener != null) { - this.nettyEventExecutor.start(); + // Support Netty Socks5 Proxy + if (proxy != null) { + bootstrap.resolver(NoopAddressResolverGroup.INSTANCE); } + return bootstrap; + } + + // Do not use RemotingHelper.string2SocketAddress(), it will directly resolve the domain + private String[] getHostAndPort(String address) { + int split = address.lastIndexOf(":"); + return split < 0 ? new String[]{address} : new String[]{address.substring(0, split), address.substring(split + 1)}; } @Override public void shutdown() { try { - this.timer.cancel(); + this.timer.stop(); - for (ChannelWrapper cw : this.channelTables.values()) { - this.closeChannel(null, cw.getChannel()); + for (String addr : this.channelTables.keySet()) { + this.channelTables.get(addr).close(); } + this.channelWrapperTables.clear(); this.channelTables.clear(); this.eventLoopGroupWorker.shutdownGracefully(); @@ -227,21 +379,30 @@ public void shutdown() { this.defaultEventExecutorGroup.shutdownGracefully(); } } catch (Exception e) { - log.error("NettyRemotingClient shutdown exception, ", e); + LOGGER.error("NettyRemotingClient shutdown exception, ", e); } if (this.publicExecutor != null) { try { this.publicExecutor.shutdown(); } catch (Exception e) { - log.error("NettyRemotingServer shutdown exception, ", e); + LOGGER.error("NettyRemotingServer shutdown exception, ", e); + } + } + + if (this.scanExecutor != null) { + try { + this.scanExecutor.shutdown(); + } catch (Exception e) { + LOGGER.error("NettyRemotingServer shutdown exception, ", e); } } } public void closeChannel(final String addr, final Channel channel) { - if (null == channel) + if (null == channel) { return; + } final String addrRemote = null == addr ? RemotingHelper.parseChannelRemoteAddr(channel) : addr; @@ -251,44 +412,43 @@ public void closeChannel(final String addr, final Channel channel) { boolean removeItemFromTable = true; final ChannelWrapper prevCW = this.channelTables.get(addrRemote); - log.info("closeChannel: begin close the channel[{}] Found: {}", addrRemote, prevCW != null); + LOGGER.info("closeChannel: begin close the channel[{}] Found: {}", addrRemote, prevCW != null); if (null == prevCW) { - log.info("closeChannel: the channel[{}] has been removed from the channel table before", addrRemote); + LOGGER.info("closeChannel: the channel[{}] has been removed from the channel table before", addrRemote); removeItemFromTable = false; } else if (prevCW.getChannel() != channel) { - log.info("closeChannel: the channel[{}] has been closed before, and has been created again, nothing to do.", + LOGGER.info("closeChannel: the channel[{}] has been closed before, and has been created again, nothing to do.", addrRemote); removeItemFromTable = false; } if (removeItemFromTable) { - this.channelTables.remove(addrRemote); - log.info("closeChannel: the channel[{}] was removed from channel table", addrRemote); + ChannelWrapper channelWrapper = this.channelWrapperTables.remove(channel); + if (channelWrapper != null && channelWrapper.tryClose(channel)) { + this.channelTables.remove(addrRemote); + } + LOGGER.info("closeChannel: the channel[{}] was removed from channel table", addrRemote); } - RemotingUtil.closeChannel(channel); + RemotingHelper.closeChannel(channel); } catch (Exception e) { - log.error("closeChannel: close the channel exception", e); + LOGGER.error("closeChannel: close the channel exception", e); } finally { this.lockChannelTables.unlock(); } } else { - log.warn("closeChannel: try to lock channel table, but timeout, {}ms", LOCK_TIMEOUT_MILLIS); + LOGGER.warn("closeChannel: try to lock channel table, but timeout, {}ms", LOCK_TIMEOUT_MILLIS); } } catch (InterruptedException e) { - log.error("closeChannel exception", e); + LOGGER.error("closeChannel exception", e); } } - @Override - public void registerRPCHook(RPCHook rpcHook) { - this.rpcHook = rpcHook; - } - public void closeChannel(final Channel channel) { - if (null == channel) + if (null == channel) { return; + } try { if (this.lockChannelTables.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { @@ -309,25 +469,28 @@ public void closeChannel(final Channel channel) { } if (null == prevCW) { - log.info("eventCloseChannel: the channel[{}] has been removed from the channel table before", addrRemote); + LOGGER.info("eventCloseChannel: the channel[{}] has been removed from the channel table before", addrRemote); removeItemFromTable = false; } if (removeItemFromTable) { - this.channelTables.remove(addrRemote); - log.info("closeChannel: the channel[{}] was removed from channel table", addrRemote); - RemotingUtil.closeChannel(channel); + ChannelWrapper channelWrapper = this.channelWrapperTables.remove(channel); + if (channelWrapper != null && channelWrapper.tryClose(channel)) { + this.channelTables.remove(addrRemote); + } + LOGGER.info("closeChannel: the channel[{}] was removed from channel table", addrRemote); + RemotingHelper.closeChannel(channel); } } catch (Exception e) { - log.error("closeChannel: close the channel exception", e); + LOGGER.error("closeChannel: close the channel exception", e); } finally { this.lockChannelTables.unlock(); } } else { - log.warn("closeChannel: try to lock channel table, but timeout, {}ms", LOCK_TIMEOUT_MILLIS); + LOGGER.warn("closeChannel: try to lock channel table, but timeout, {}ms", LOCK_TIMEOUT_MILLIS); } } catch (InterruptedException e) { - log.error("closeChannel exception", e); + LOGGER.error("closeChannel exception", e); } } @@ -342,17 +505,31 @@ public void updateNameServerAddressList(List addrs) { } else if (addrs.size() != old.size()) { update = true; } else { - for (int i = 0; i < addrs.size() && !update; i++) { - if (!old.contains(addrs.get(i))) { + for (String addr : addrs) { + if (!old.contains(addr)) { update = true; + break; } } } if (update) { Collections.shuffle(addrs); - log.info("name server address updated. NEW : {} , OLD: {}", addrs, old); + LOGGER.info("name server address updated. NEW : {} , OLD: {}", addrs, old); this.namesrvAddrList.set(addrs); + + // should close the channel if choosed addr is not exist. + if (this.namesrvAddrChoosed.get() != null && !addrs.contains(this.namesrvAddrChoosed.get())) { + String namesrvAddr = this.namesrvAddrChoosed.get(); + for (String addr : this.channelTables.keySet()) { + if (addr.contains(namesrvAddr)) { + ChannelWrapper channelWrapper = this.channelTables.get(addr); + if (channelWrapper != null) { + channelWrapper.close(); + } + } + } + } } } } @@ -360,27 +537,32 @@ public void updateNameServerAddressList(List addrs) { @Override public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis) throws InterruptedException, RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException { + long beginStartTime = System.currentTimeMillis(); final Channel channel = this.getAndCreateChannel(addr); + String channelRemoteAddr = RemotingHelper.parseChannelRemoteAddr(channel); if (channel != null && channel.isActive()) { + long left = timeoutMillis; try { - if (this.rpcHook != null) { - this.rpcHook.doBeforeRequest(addr, request); - } - RemotingCommand response = this.invokeSyncImpl(channel, request, timeoutMillis); - if (this.rpcHook != null) { - this.rpcHook.doAfterResponse(RemotingHelper.parseChannelRemoteAddr(channel), request, response); + long costTime = System.currentTimeMillis() - beginStartTime; + left -= costTime; + if (left <= 0) { + throw new RemotingTimeoutException("invokeSync call the addr[" + channelRemoteAddr + "] timeout"); } + RemotingCommand response = this.invokeSyncImpl(channel, request, left); + updateChannelLastResponseTime(addr); return response; } catch (RemotingSendRequestException e) { - log.warn("invokeSync: send request exception, so close the channel[{}]", addr); + LOGGER.warn("invokeSync: send request exception, so close the channel[{}]", channelRemoteAddr); this.closeChannel(addr, channel); throw e; } catch (RemotingTimeoutException e) { - if (nettyClientConfig.isClientCloseSocketIfTimeout()) { + // avoid close the success channel if left timeout is small, since it may cost too much time in get the success channel, the left timeout for read is small + boolean shouldClose = left > MIN_CLOSE_TIMEOUT_MILLIS || left > timeoutMillis / 4; + if (nettyClientConfig.isClientCloseSocketIfTimeout() && shouldClose) { this.closeChannel(addr, channel); - log.warn("invokeSync: close socket because of timeout, {}ms, {}", timeoutMillis, addr); + LOGGER.warn("invokeSync: close socket because of timeout, {}ms, {}", timeoutMillis, channelRemoteAddr); } - log.warn("invokeSync: wait response timeout exception, the channel[{}]", addr); + LOGGER.warn("invokeSync: wait response timeout exception, the channel[{}]", channelRemoteAddr); throw e; } } else { @@ -389,9 +571,52 @@ public RemotingCommand invokeSync(String addr, final RemotingCommand request, lo } } + @Override + public void closeChannels(List addrList) { + for (String addr : addrList) { + ChannelWrapper cw = this.channelTables.get(addr); + if (cw == null) { + continue; + } + this.closeChannel(addr, cw.getChannel()); + } + interruptPullRequests(new HashSet<>(addrList)); + } + + private void interruptPullRequests(Set brokerAddrSet) { + for (ResponseFuture responseFuture : responseTable.values()) { + RemotingCommand cmd = responseFuture.getRequestCommand(); + if (cmd == null) { + continue; + } + String remoteAddr = RemotingHelper.parseChannelRemoteAddr(responseFuture.getChannel()); + // interrupt only pull message request + if (brokerAddrSet.contains(remoteAddr) && (cmd.getCode() == 11 || cmd.getCode() == 361)) { + LOGGER.info("interrupt {}", cmd); + responseFuture.interrupt(); + } + } + } + + private void updateChannelLastResponseTime(final String addr) { + String address = addr; + if (address == null) { + address = this.namesrvAddrChoosed.get(); + } + if (address == null) { + LOGGER.warn("[updateChannelLastResponseTime] could not find address!!"); + return; + } + ChannelWrapper channelWrapper = this.channelTables.get(address); + if (channelWrapper != null && channelWrapper.isOK()) { + channelWrapper.updateLastResponseTime(); + } + } + private Channel getAndCreateChannel(final String addr) throws InterruptedException { - if (null == addr) + if (null == addr) { return getAndCreateNameserverChannel(); + } ChannelWrapper cw = this.channelTables.get(addr); if (cw != null && cw.isOK()) { @@ -411,7 +636,7 @@ private Channel getAndCreateNameserverChannel() throws InterruptedException { } final List addrList = this.namesrvAddrList.get(); - if (this.lockNamesrvChannel.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { + if (this.namesrvChannelLock.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { try { addr = this.namesrvAddrChoosed.get(); if (addr != null) { @@ -429,19 +654,21 @@ private Channel getAndCreateNameserverChannel() throws InterruptedException { String newAddr = addrList.get(index); this.namesrvAddrChoosed.set(newAddr); - log.info("new name server is chosen. OLD: {} , NEW: {}. namesrvIndex = {}", addr, newAddr, namesrvIndex); + LOGGER.info("new name server is chosen. OLD: {} , NEW: {}. namesrvIndex = {}", addr, newAddr, namesrvIndex); Channel channelNew = this.createChannel(newAddr); - if (channelNew != null) + if (channelNew != null) { return channelNew; + } } + throw new RemotingConnectException(addrList.toString()); } } catch (Exception e) { - log.error("getAndCreateNameserverChannel: create name server channel exception", e); + LOGGER.error("getAndCreateNameserverChannel: create name server channel exception", e); } finally { - this.lockNamesrvChannel.unlock(); + this.namesrvChannelLock.unlock(); } } else { - log.warn("getAndCreateNameserverChannel: try to lock name server, but timeout, {}ms", LOCK_TIMEOUT_MILLIS); + LOGGER.warn("getAndCreateNameserverChannel: try to lock name server, but timeout, {}ms", LOCK_TIMEOUT_MILLIS); } return null; @@ -450,8 +677,7 @@ private Channel getAndCreateNameserverChannel() throws InterruptedException { private Channel createChannel(final String addr) throws InterruptedException { ChannelWrapper cw = this.channelTables.get(addr); if (cw != null && cw.isOK()) { - cw.getChannel().close(); - channelTables.remove(addr); + return cw.getChannel(); } if (this.lockChannelTables.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { @@ -461,9 +687,7 @@ private Channel createChannel(final String addr) throws InterruptedException { if (cw != null) { if (cw.isOK()) { - cw.getChannel().close(); - this.channelTables.remove(addr); - createNewConnection = true; + return cw.getChannel(); } else if (!cw.getChannelFuture().isDone()) { createNewConnection = false; } else { @@ -475,35 +699,43 @@ private Channel createChannel(final String addr) throws InterruptedException { } if (createNewConnection) { - ChannelFuture channelFuture = this.bootstrap.connect(RemotingHelper.string2SocketAddress(addr)); - log.info("createChannel: begin to connect remote host[{}] asynchronously", addr); - cw = new ChannelWrapper(channelFuture); + String[] hostAndPort = getHostAndPort(addr); + ChannelFuture channelFuture = fetchBootstrap(addr) + .connect(hostAndPort[0], Integer.parseInt(hostAndPort[1])); + LOGGER.info("createChannel: begin to connect remote host[{}] asynchronously", addr); + cw = new ChannelWrapper(addr, channelFuture); this.channelTables.put(addr, cw); + this.channelWrapperTables.put(channelFuture.channel(), cw); } } catch (Exception e) { - log.error("createChannel: create channel exception", e); + LOGGER.error("createChannel: create channel exception", e); } finally { this.lockChannelTables.unlock(); } } else { - log.warn("createChannel: try to lock channel table, but timeout, {}ms", LOCK_TIMEOUT_MILLIS); + LOGGER.warn("createChannel: try to lock channel table, but timeout, {}ms", LOCK_TIMEOUT_MILLIS); } if (cw != null) { - ChannelFuture channelFuture = cw.getChannelFuture(); - if (channelFuture.awaitUninterruptibly(this.nettyClientConfig.getConnectTimeoutMillis())) { - if (cw.isOK()) { - log.info("createChannel: connect remote host[{}] success, {}", addr, channelFuture.toString()); - return cw.getChannel(); - } else { - log.warn("createChannel: connect remote host[" + addr + "] failed, " + channelFuture.toString(), channelFuture.cause()); - } + return waitChannelFuture(addr, cw); + } + + return null; + } + + private Channel waitChannelFuture(String addr, ChannelWrapper cw) { + ChannelFuture channelFuture = cw.getChannelFuture(); + if (channelFuture.awaitUninterruptibly(this.nettyClientConfig.getConnectTimeoutMillis())) { + if (cw.isOK()) { + LOGGER.info("createChannel: connect remote host[{}] success, {}", addr, channelFuture.toString()); + return cw.getChannel(); } else { - log.warn("createChannel: connect remote host[{}] timeout {}ms, {}", addr, this.nettyClientConfig.getConnectTimeoutMillis(), - channelFuture.toString()); + LOGGER.warn("createChannel: connect remote host[{}] failed, {}", addr, channelFuture.toString()); } + } else { + LOGGER.warn("createChannel: connect remote host[{}] timeout {}ms, {}", addr, this.nettyClientConfig.getConnectTimeoutMillis(), + channelFuture.toString()); } - return null; } @@ -511,18 +743,15 @@ private Channel createChannel(final String addr) throws InterruptedException { public void invokeAsync(String addr, RemotingCommand request, long timeoutMillis, InvokeCallback invokeCallback) throws InterruptedException, RemotingConnectException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException { + long beginStartTime = System.currentTimeMillis(); final Channel channel = this.getAndCreateChannel(addr); + String channelRemoteAddr = RemotingHelper.parseChannelRemoteAddr(channel); if (channel != null && channel.isActive()) { - try { - if (this.rpcHook != null) { - this.rpcHook.doBeforeRequest(addr, request); - } - this.invokeAsyncImpl(channel, request, timeoutMillis, invokeCallback); - } catch (RemotingSendRequestException e) { - log.warn("invokeAsync: send request exception, so close the channel[{}]", addr); - this.closeChannel(addr, channel); - throw e; + long costTime = System.currentTimeMillis() - beginStartTime; + if (timeoutMillis < costTime) { + throw new RemotingTooMuchRequestException("invokeAsync call the addr[" + channelRemoteAddr + "] timeout"); } + this.invokeAsyncImpl(channel, request, timeoutMillis - costTime, new InvokeCallbackWrapper(invokeCallback, addr)); } else { this.closeChannel(addr, channel); throw new RemotingConnectException(addr); @@ -533,14 +762,13 @@ public void invokeAsync(String addr, RemotingCommand request, long timeoutMillis public void invokeOneway(String addr, RemotingCommand request, long timeoutMillis) throws InterruptedException, RemotingConnectException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException { final Channel channel = this.getAndCreateChannel(addr); + String channelRemoteAddr = RemotingHelper.parseChannelRemoteAddr(channel); if (channel != null && channel.isActive()) { try { - if (this.rpcHook != null) { - this.rpcHook.doBeforeRequest(addr, request); - } + doBeforeRpcHooks(channelRemoteAddr, request); this.invokeOnewayImpl(channel, request, timeoutMillis); } catch (RemotingSendRequestException e) { - log.warn("invokeOneway: send request exception, so close the channel[{}]", addr); + LOGGER.warn("invokeOneway: send request exception, so close the channel[{}]", channelRemoteAddr); this.closeChannel(addr, channel); throw e; } @@ -550,6 +778,70 @@ public void invokeOneway(String addr, RemotingCommand request, long timeoutMilli } } + @Override + public CompletableFuture invoke(String addr, RemotingCommand request, + long timeoutMillis) { + CompletableFuture future = new CompletableFuture<>(); + try { + final Channel channel = this.getAndCreateChannel(addr); + if (channel != null && channel.isActive()) { + return invokeImpl(channel, request, timeoutMillis).whenComplete((v, t) -> { + if (t == null) { + updateChannelLastResponseTime(addr); + } + }).thenApply(ResponseFuture::getResponseCommand); + } else { + this.closeChannel(addr, channel); + future.completeExceptionally(new RemotingConnectException(addr)); + } + } catch (Throwable t) { + future.completeExceptionally(t); + } + return future; + } + + @Override + public CompletableFuture invokeImpl(final Channel channel, final RemotingCommand request, + final long timeoutMillis) { + Stopwatch stopwatch = Stopwatch.createStarted(); + return super.invokeImpl(channel, request, timeoutMillis).thenCompose(responseFuture -> { + RemotingCommand response = responseFuture.getResponseCommand(); + if (response.getCode() == ResponseCode.GO_AWAY) { + if (nettyClientConfig.isEnableReconnectForGoAway()) { + ChannelWrapper channelWrapper = channelWrapperTables.computeIfPresent(channel, (channel0, channelWrapper0) -> { + try { + if (channelWrapper0.reconnect()) { + LOGGER.info("Receive go away from channel {}, recreate the channel", channel0); + channelWrapperTables.put(channelWrapper0.getChannel(), channelWrapper0); + } + } catch (Throwable t) { + LOGGER.error("Channel {} reconnect error", channelWrapper0, t); + } + return channelWrapper0; + }); + if (channelWrapper != null) { + if (nettyClientConfig.isEnableTransparentRetry()) { + long duration = stopwatch.elapsed(TimeUnit.MILLISECONDS); + stopwatch.stop(); + RemotingCommand retryRequest = RemotingCommand.createRequestCommand(request.getCode(), request.readCustomHeader()); + retryRequest.setBody(request.getBody()); + Channel retryChannel; + if (channelWrapper.isOK()) { + retryChannel = channelWrapper.getChannel(); + } else { + retryChannel = waitChannelFuture(channelWrapper.getChannelAddress(), channelWrapper); + } + if (retryChannel != null && channel != retryChannel) { + return super.invokeImpl(retryChannel, retryRequest, timeoutMillis - duration); + } + } + } + } + } + return CompletableFuture.completedFuture(responseFuture); + }); + } + @Override public void registerProcessor(int requestCode, NettyRequestProcessor processor, ExecutorService executor) { ExecutorService executorThis = executor; @@ -557,7 +849,7 @@ public void registerProcessor(int requestCode, NettyRequestProcessor processor, executorThis = this.publicExecutor; } - Pair pair = new Pair(processor, executorThis); + Pair pair = new Pair<>(processor, executorThis); this.processorTable.put(requestCode, pair); } @@ -570,23 +862,40 @@ public boolean isChannelWritable(String addr) { return true; } + @Override + public boolean isAddressReachable(String addr) { + if (addr == null || addr.isEmpty()) { + return false; + } + try { + Channel channel = getAndCreateChannel(addr); + return channel != null && channel.isActive(); + } catch (Exception e) { + LOGGER.warn("Get and create channel of {} failed", addr, e); + return false; + } + } + @Override public List getNameServerAddressList() { return this.namesrvAddrList.get(); } @Override - public ChannelEventListener getChannelEventListener() { - return channelEventListener; + public List getAvailableNameSrvList() { + return new ArrayList<>(this.availableNamesrvAddrMap.keySet()); } @Override - public RPCHook getRPCHook() { - return this.rpcHook; + public ChannelEventListener getChannelEventListener() { + return channelEventListener; } @Override public ExecutorService getCallbackExecutor() { + if (nettyClientConfig.isDisableCallbackExecutor()) { + return null; + } return callbackExecutor != null ? callbackExecutor : publicExecutor; } @@ -595,27 +904,183 @@ public void setCallbackExecutor(final ExecutorService callbackExecutor) { this.callbackExecutor = callbackExecutor; } - static class ChannelWrapper { - private final ChannelFuture channelFuture; + protected void scanChannelTablesOfNameServer() { + List nameServerList = this.namesrvAddrList.get(); + if (nameServerList == null) { + LOGGER.warn("[SCAN] Addresses of name server is empty!"); + return; + } + + for (String addr : this.channelTables.keySet()) { + ChannelWrapper channelWrapper = this.channelTables.get(addr); + if (channelWrapper == null) { + continue; + } + + if ((System.currentTimeMillis() - channelWrapper.getLastResponseTime()) > this.nettyClientConfig.getChannelNotActiveInterval()) { + LOGGER.warn("[SCAN] No response after {} from name server {}, so close it!", channelWrapper.getLastResponseTime(), + addr); + closeChannel(addr, channelWrapper.getChannel()); + } + } + } + + private void scanAvailableNameSrv() { + List nameServerList = this.namesrvAddrList.get(); + if (nameServerList == null) { + LOGGER.debug("scanAvailableNameSrv addresses of name server is null!"); + return; + } + + for (String address : NettyRemotingClient.this.availableNamesrvAddrMap.keySet()) { + if (!nameServerList.contains(address)) { + LOGGER.warn("scanAvailableNameSrv remove invalid address {}", address); + NettyRemotingClient.this.availableNamesrvAddrMap.remove(address); + } + } + + for (final String namesrvAddr : nameServerList) { + scanExecutor.execute(new Runnable() { + @Override + public void run() { + try { + Channel channel = NettyRemotingClient.this.getAndCreateChannel(namesrvAddr); + if (channel != null) { + NettyRemotingClient.this.availableNamesrvAddrMap.putIfAbsent(namesrvAddr, true); + } else { + Boolean value = NettyRemotingClient.this.availableNamesrvAddrMap.remove(namesrvAddr); + if (value != null) { + LOGGER.warn("scanAvailableNameSrv remove unconnected address {}", namesrvAddr); + } + } + } catch (Exception e) { + LOGGER.error("scanAvailableNameSrv get channel of {} failed, ", namesrvAddr, e); + } + } + }); + } + } - public ChannelWrapper(ChannelFuture channelFuture) { + class ChannelWrapper { + private final ReentrantReadWriteLock lock; + private ChannelFuture channelFuture; + // only affected by sync or async request, oneway is not included. + private ChannelFuture channelToClose; + private long lastResponseTime; + private volatile long lastReconnectTimestamp = 0L; + private final String channelAddress; + + public ChannelWrapper(String address, ChannelFuture channelFuture) { + this.lock = new ReentrantReadWriteLock(); this.channelFuture = channelFuture; + this.lastResponseTime = System.currentTimeMillis(); + this.channelAddress = address; } public boolean isOK() { - return this.channelFuture.channel() != null && this.channelFuture.channel().isActive(); + return getChannel() != null && getChannel().isActive(); } public boolean isWritable() { - return this.channelFuture.channel().isWritable(); + return getChannel().isWritable(); } private Channel getChannel() { - return this.channelFuture.channel(); + return getChannelFuture().channel(); } public ChannelFuture getChannelFuture() { - return channelFuture; + lock.readLock().lock(); + try { + return this.channelFuture; + } finally { + lock.readLock().unlock(); + } + } + + public long getLastResponseTime() { + return this.lastResponseTime; + } + + public void updateLastResponseTime() { + this.lastResponseTime = System.currentTimeMillis(); + } + + public String getChannelAddress() { + return channelAddress; + } + + public boolean reconnect() { + if (lock.writeLock().tryLock()) { + try { + if (lastReconnectTimestamp == 0L || System.currentTimeMillis() - lastReconnectTimestamp > Duration.ofSeconds(nettyClientConfig.getMaxReconnectIntervalTimeSeconds()).toMillis()) { + channelToClose = channelFuture; + String[] hostAndPort = getHostAndPort(channelAddress); + channelFuture = fetchBootstrap(channelAddress) + .connect(hostAndPort[0], Integer.parseInt(hostAndPort[1])); + lastReconnectTimestamp = System.currentTimeMillis(); + return true; + } + } finally { + lock.writeLock().unlock(); + } + } + return false; + } + + public boolean tryClose(Channel channel) { + try { + lock.readLock().lock(); + if (channelFuture != null) { + if (channelFuture.channel().equals(channel)) { + return true; + } + } + } finally { + lock.readLock().unlock(); + } + return false; + } + + public void close() { + try { + lock.writeLock().lock(); + if (channelFuture != null) { + closeChannel(channelFuture.channel()); + } + if (channelToClose != null) { + closeChannel(channelToClose.channel()); + } + } finally { + lock.writeLock().unlock(); + } + } + } + + class InvokeCallbackWrapper implements InvokeCallback { + + private final InvokeCallback invokeCallback; + private final String addr; + + public InvokeCallbackWrapper(InvokeCallback invokeCallback, String addr) { + this.invokeCallback = invokeCallback; + this.addr = addr; + } + + @Override + public void operationComplete(ResponseFuture responseFuture) { + this.invokeCallback.operationComplete(responseFuture); + } + + @Override + public void operationSucceed(RemotingCommand response) { + updateChannelLastResponseTime(addr); + this.invokeCallback.operationSucceed(response); + } + + @Override + public void operationFail(final Throwable throwable) { + this.invokeCallback.operationFail(throwable); } } @@ -633,7 +1098,7 @@ public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, Sock ChannelPromise promise) throws Exception { final String local = localAddress == null ? "UNKNOWN" : RemotingHelper.parseSocketAddressAddr(localAddress); final String remote = remoteAddress == null ? "UNKNOWN" : RemotingHelper.parseSocketAddressAddr(remoteAddress); - log.info("NETTY CLIENT PIPELINE: CONNECT {} => {}", local, remote); + LOGGER.info("NETTY CLIENT PIPELINE: CONNECT {} => {}", local, remote); super.connect(ctx, remoteAddress, localAddress, promise); @@ -642,10 +1107,21 @@ public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, Sock } } + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + final String remoteAddress = RemotingHelper.parseChannelRemoteAddr(ctx.channel()); + LOGGER.info("NETTY CLIENT PIPELINE: ACTIVE, {}", remoteAddress); + super.channelActive(ctx); + + if (NettyRemotingClient.this.channelEventListener != null) { + NettyRemotingClient.this.putNettyEvent(new NettyEvent(NettyEventType.ACTIVE, remoteAddress, ctx.channel())); + } + } + @Override public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { final String remoteAddress = RemotingHelper.parseChannelRemoteAddr(ctx.channel()); - log.info("NETTY CLIENT PIPELINE: DISCONNECT {}", remoteAddress); + LOGGER.info("NETTY CLIENT PIPELINE: DISCONNECT {}", remoteAddress); closeChannel(ctx.channel()); super.disconnect(ctx, promise); @@ -657,22 +1133,30 @@ public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws @Override public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { final String remoteAddress = RemotingHelper.parseChannelRemoteAddr(ctx.channel()); - log.info("NETTY CLIENT PIPELINE: CLOSE {}", remoteAddress); + LOGGER.info("NETTY CLIENT PIPELINE: CLOSE {}", remoteAddress); closeChannel(ctx.channel()); super.close(ctx, promise); - + NettyRemotingClient.this.failFast(ctx.channel()); if (NettyRemotingClient.this.channelEventListener != null) { NettyRemotingClient.this.putNettyEvent(new NettyEvent(NettyEventType.CLOSE, remoteAddress, ctx.channel())); } } + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + final String remoteAddress = RemotingHelper.parseChannelRemoteAddr(ctx.channel()); + LOGGER.info("NETTY CLIENT PIPELINE: channelInactive, the channel[{}]", remoteAddress); + closeChannel(ctx.channel()); + super.channelInactive(ctx); + } + @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof IdleStateEvent) { IdleStateEvent event = (IdleStateEvent) evt; if (event.state().equals(IdleState.ALL_IDLE)) { final String remoteAddress = RemotingHelper.parseChannelRemoteAddr(ctx.channel()); - log.warn("NETTY CLIENT PIPELINE: IDLE exception [{}]", remoteAddress); + LOGGER.warn("NETTY CLIENT PIPELINE: IDLE exception [{}]", remoteAddress); closeChannel(ctx.channel()); if (NettyRemotingClient.this.channelEventListener != null) { NettyRemotingClient.this @@ -687,8 +1171,8 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { final String remoteAddress = RemotingHelper.parseChannelRemoteAddr(ctx.channel()); - log.warn("NETTY CLIENT PIPELINE: exceptionCaught {}", remoteAddress); - log.warn("NETTY CLIENT PIPELINE: exceptionCaught exception.", cause); + LOGGER.warn("NETTY CLIENT PIPELINE: exceptionCaught {}", remoteAddress); + LOGGER.warn("NETTY CLIENT PIPELINE: exceptionCaught exception.", cause); closeChannel(ctx.channel()); if (NettyRemotingClient.this.channelEventListener != null) { NettyRemotingClient.this.putNettyEvent(new NettyEvent(NettyEventType.EXCEPTION, remoteAddress, ctx.channel())); diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRemotingServer.java b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRemotingServer.java index cd6ed4704f4..51f8b85009e 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRemotingServer.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyRemotingServer.java @@ -18,70 +18,113 @@ import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; import io.netty.buffer.PooledByteBufAllocator; import io.netty.channel.Channel; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.WriteBufferWaterMark; import io.netty.channel.epoll.Epoll; import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.epoll.EpollServerSocketChannel; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.ProtocolDetectionResult; +import io.netty.handler.codec.ProtocolDetectionState; +import io.netty.handler.codec.haproxy.HAProxyMessage; +import io.netty.handler.codec.haproxy.HAProxyMessageDecoder; +import io.netty.handler.codec.haproxy.HAProxyProtocolVersion; +import io.netty.handler.codec.haproxy.HAProxyTLV; import io.netty.handler.timeout.IdleState; import io.netty.handler.timeout.IdleStateEvent; import io.netty.handler.timeout.IdleStateHandler; +import io.netty.util.AttributeKey; +import io.netty.util.CharsetUtil; +import io.netty.util.HashedWheelTimer; +import io.netty.util.Timeout; +import io.netty.util.TimerTask; import io.netty.util.concurrent.DefaultEventExecutorGroup; import java.io.IOException; import java.net.InetSocketAddress; import java.security.cert.CertificateException; +import java.time.Duration; +import java.util.List; import java.util.NoSuchElementException; -import java.util.Timer; -import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.constant.HAProxyConstants; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.BinaryUtil; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.remoting.ChannelEventListener; import org.apache.rocketmq.remoting.InvokeCallback; -import org.apache.rocketmq.remoting.RPCHook; import org.apache.rocketmq.remoting.RemotingServer; -import org.apache.rocketmq.remoting.common.Pair; import org.apache.rocketmq.remoting.common.RemotingHelper; -import org.apache.rocketmq.remoting.common.RemotingUtil; import org.apache.rocketmq.remoting.common.TlsMode; import org.apache.rocketmq.remoting.exception.RemotingSendRequestException; import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; import org.apache.rocketmq.remoting.exception.RemotingTooMuchRequestException; import org.apache.rocketmq.remoting.protocol.RemotingCommand; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +@SuppressWarnings("NullableProblems") public class NettyRemotingServer extends NettyRemotingAbstract implements RemotingServer { - private static final Logger log = LoggerFactory.getLogger(RemotingHelper.ROCKETMQ_REMOTING); + private static final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_REMOTING_NAME); + private static final Logger TRAFFIC_LOGGER = LoggerFactory.getLogger(LoggerName.ROCKETMQ_TRAFFIC_NAME); + private final ServerBootstrap serverBootstrap; private final EventLoopGroup eventLoopGroupSelector; private final EventLoopGroup eventLoopGroupBoss; private final NettyServerConfig nettyServerConfig; private final ExecutorService publicExecutor; + private final ScheduledExecutorService scheduledExecutorService; private final ChannelEventListener channelEventListener; - private final Timer timer = new Timer("ServerHouseKeepingService", true); - private DefaultEventExecutorGroup defaultEventExecutorGroup; - - private RPCHook rpcHook; + private final HashedWheelTimer timer = new HashedWheelTimer(r -> new Thread(r, "ServerHouseKeepingService")); - private int port = 0; + private DefaultEventExecutorGroup defaultEventExecutorGroup; - private static final String HANDSHAKE_HANDLER_NAME = "handshakeHandler"; - private static final String TLS_HANDLER_NAME = "sslHandler"; - private static final String FILE_REGION_ENCODER_NAME = "fileRegionEncoder"; + /** + * NettyRemotingServer may hold multiple SubRemotingServer, each server will be stored in this container with a + * ListenPort key. + */ + private final ConcurrentMap remotingServerTable = new ConcurrentHashMap<>(); + + public static final String HANDSHAKE_HANDLER_NAME = "handshakeHandler"; + public static final String HA_PROXY_DECODER = "HAProxyDecoder"; + public static final String HA_PROXY_HANDLER = "HAProxyHandler"; + public static final String TLS_MODE_HANDLER = "TlsModeHandler"; + public static final String TLS_HANDLER_NAME = "sslHandler"; + public static final String FILE_REGION_ENCODER_NAME = "fileRegionEncoder"; + + // sharable handlers + private TlsModeHandler tlsModeHandler; + private NettyEncoder encoder; + private NettyConnectManageHandler connectionManageHandler; + private NettyServerHandler serverHandler; + private RemotingCodeDistributionHandler distributionHandler; public NettyRemotingServer(final NettyServerConfig nettyServerConfig) { this(nettyServerConfig, null); @@ -94,51 +137,47 @@ public NettyRemotingServer(final NettyServerConfig nettyServerConfig, this.nettyServerConfig = nettyServerConfig; this.channelEventListener = channelEventListener; - int publicThreadNums = nettyServerConfig.getServerCallbackExecutorThreads(); - if (publicThreadNums <= 0) { - publicThreadNums = 4; - } - - this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums, new ThreadFactory() { - private AtomicInteger threadIndex = new AtomicInteger(0); - - @Override - public Thread newThread(Runnable r) { - return new Thread(r, "NettyServerPublicExecutor_" + this.threadIndex.incrementAndGet()); - } - }); + this.publicExecutor = buildPublicExecutor(nettyServerConfig); + this.scheduledExecutorService = buildScheduleExecutor(); - this.eventLoopGroupBoss = new NioEventLoopGroup(1, new ThreadFactory() { - private AtomicInteger threadIndex = new AtomicInteger(0); + this.eventLoopGroupBoss = buildBossEventLoopGroup(); + this.eventLoopGroupSelector = buildEventLoopGroupSelector(); - @Override - public Thread newThread(Runnable r) { - return new Thread(r, String.format("NettyBoss_%d", this.threadIndex.incrementAndGet())); - } - }); + loadSslContext(); + } + private EventLoopGroup buildEventLoopGroupSelector() { if (useEpoll()) { - this.eventLoopGroupSelector = new EpollEventLoopGroup(nettyServerConfig.getServerSelectorThreads(), new ThreadFactory() { - private AtomicInteger threadIndex = new AtomicInteger(0); - private int threadTotal = nettyServerConfig.getServerSelectorThreads(); + return new EpollEventLoopGroup(nettyServerConfig.getServerSelectorThreads(), new ThreadFactoryImpl("NettyServerEPOLLSelector_")); + } else { + return new NioEventLoopGroup(nettyServerConfig.getServerSelectorThreads(), new ThreadFactoryImpl("NettyServerNIOSelector_")); + } + } - @Override - public Thread newThread(Runnable r) { - return new Thread(r, String.format("NettyServerEPOLLSelector_%d_%d", threadTotal, this.threadIndex.incrementAndGet())); - } - }); + private EventLoopGroup buildBossEventLoopGroup() { + if (useEpoll()) { + return new EpollEventLoopGroup(1, new ThreadFactoryImpl("NettyEPOLLBoss_")); } else { - this.eventLoopGroupSelector = new NioEventLoopGroup(nettyServerConfig.getServerSelectorThreads(), new ThreadFactory() { - private AtomicInteger threadIndex = new AtomicInteger(0); - private int threadTotal = nettyServerConfig.getServerSelectorThreads(); + return new NioEventLoopGroup(1, new ThreadFactoryImpl("NettyNIOBoss_")); + } + } - @Override - public Thread newThread(Runnable r) { - return new Thread(r, String.format("NettyServerNIOSelector_%d_%d", threadTotal, this.threadIndex.incrementAndGet())); - } - }); + private ExecutorService buildPublicExecutor(NettyServerConfig nettyServerConfig) { + int publicThreadNums = nettyServerConfig.getServerCallbackExecutorThreads(); + if (publicThreadNums <= 0) { + publicThreadNums = 4; } + return Executors.newFixedThreadPool(publicThreadNums, new ThreadFactoryImpl("NettyServerPublicExecutor_")); + } + + private ScheduledExecutorService buildScheduleExecutor() { + return ThreadUtils.newScheduledThreadPool(1, + new ThreadFactoryImpl("NettyServerScheduler_", true), + new ThreadPoolExecutor.DiscardOldestPolicy()); + } + + public void loadSslContext() { TlsMode tlsMode = TlsSystemConfig.tlsMode; log.info("Server is running in TLS {} mode", tlsMode.getName()); @@ -146,103 +185,138 @@ public Thread newThread(Runnable r) { try { sslContext = TlsHelper.buildSslContext(false); log.info("SSLContext created for server"); - } catch (CertificateException e) { - log.error("Failed to create SSLContext for server", e); - } catch (IOException e) { + } catch (CertificateException | IOException e) { log.error("Failed to create SSLContext for server", e); } } } private boolean useEpoll() { - return RemotingUtil.isLinuxPlatform() + return NetworkUtil.isLinuxPlatform() && nettyServerConfig.isUseEpollNativeSelector() && Epoll.isAvailable(); } @Override public void start() { - this.defaultEventExecutorGroup = new DefaultEventExecutorGroup( - nettyServerConfig.getServerWorkerThreads(), - new ThreadFactory() { - - private AtomicInteger threadIndex = new AtomicInteger(0); - + this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(nettyServerConfig.getServerWorkerThreads(), + new ThreadFactoryImpl("NettyServerCodecThread_")); + + prepareSharableHandlers(); + + serverBootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupSelector) + .channel(useEpoll() ? EpollServerSocketChannel.class : NioServerSocketChannel.class) + .option(ChannelOption.SO_BACKLOG, 1024) + .option(ChannelOption.SO_REUSEADDR, true) + .childOption(ChannelOption.SO_KEEPALIVE, false) + .childOption(ChannelOption.TCP_NODELAY, true) + .localAddress(new InetSocketAddress(this.nettyServerConfig.getBindAddress(), + this.nettyServerConfig.getListenPort())) + .childHandler(new ChannelInitializer() { @Override - public Thread newThread(Runnable r) { - return new Thread(r, "NettyServerCodecThread_" + this.threadIndex.incrementAndGet()); + public void initChannel(SocketChannel ch) { + configChannel(ch); } }); - ServerBootstrap childHandler = - this.serverBootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupSelector) - .channel(useEpoll() ? EpollServerSocketChannel.class : NioServerSocketChannel.class) - .option(ChannelOption.SO_BACKLOG, 1024) - .option(ChannelOption.SO_REUSEADDR, true) - .option(ChannelOption.SO_KEEPALIVE, false) - .childOption(ChannelOption.TCP_NODELAY, true) - .childOption(ChannelOption.SO_SNDBUF, nettyServerConfig.getServerSocketSndBufSize()) - .childOption(ChannelOption.SO_RCVBUF, nettyServerConfig.getServerSocketRcvBufSize()) - .localAddress(new InetSocketAddress(this.nettyServerConfig.getListenPort())) - .childHandler(new ChannelInitializer() { - @Override - public void initChannel(SocketChannel ch) throws Exception { - ch.pipeline() - .addLast(defaultEventExecutorGroup, HANDSHAKE_HANDLER_NAME, - new HandshakeHandler(TlsSystemConfig.tlsMode)) - .addLast(defaultEventExecutorGroup, - new NettyEncoder(), - new NettyDecoder(), - new IdleStateHandler(0, 0, nettyServerConfig.getServerChannelMaxIdleTimeSeconds()), - new NettyConnectManageHandler(), - new NettyServerHandler() - ); - } - }); - - if (nettyServerConfig.isServerPooledByteBufAllocatorEnable()) { - childHandler.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT); - } + addCustomConfig(serverBootstrap); try { - ChannelFuture sync = this.serverBootstrap.bind().sync(); + ChannelFuture sync = serverBootstrap.bind().sync(); InetSocketAddress addr = (InetSocketAddress) sync.channel().localAddress(); - this.port = addr.getPort(); - } catch (InterruptedException e1) { - throw new RuntimeException("this.serverBootstrap.bind().sync() InterruptedException", e1); + if (0 == nettyServerConfig.getListenPort()) { + this.nettyServerConfig.setListenPort(addr.getPort()); + } + log.info("RemotingServer started, listening {}:{}", this.nettyServerConfig.getBindAddress(), + this.nettyServerConfig.getListenPort()); + this.remotingServerTable.put(this.nettyServerConfig.getListenPort(), this); + } catch (Exception e) { + throw new IllegalStateException(String.format("Failed to bind to %s:%d", nettyServerConfig.getBindAddress(), + nettyServerConfig.getListenPort()), e); } if (this.channelEventListener != null) { this.nettyEventExecutor.start(); } - this.timer.scheduleAtFixedRate(new TimerTask() { - + TimerTask timerScanResponseTable = new TimerTask() { @Override - public void run() { + public void run(Timeout timeout) { try { NettyRemotingServer.this.scanResponseTable(); } catch (Throwable e) { log.error("scanResponseTable exception", e); + } finally { + timer.newTimeout(this, 1000, TimeUnit.MILLISECONDS); } } - }, 1000 * 3, 1000); + }; + this.timer.newTimeout(timerScanResponseTable, 1000 * 3, TimeUnit.MILLISECONDS); + + scheduledExecutorService.scheduleWithFixedDelay(() -> { + try { + NettyRemotingServer.this.printRemotingCodeDistribution(); + } catch (Throwable e) { + TRAFFIC_LOGGER.error("NettyRemotingServer print remoting code distribution exception", e); + } + }, 1, 1, TimeUnit.SECONDS); + } + + /** + * config channel in ChannelInitializer + * + * @param ch the SocketChannel needed to init + * @return the initialized ChannelPipeline, sub class can use it to extent in the future + */ + protected ChannelPipeline configChannel(SocketChannel ch) { + return ch.pipeline() + .addLast(defaultEventExecutorGroup, HANDSHAKE_HANDLER_NAME, new HandshakeHandler()) + .addLast(defaultEventExecutorGroup, + encoder, + new NettyDecoder(), + distributionHandler, + new IdleStateHandler(0, 0, + nettyServerConfig.getServerChannelMaxIdleTimeSeconds()), + connectionManageHandler, + serverHandler + ); + } + + private void addCustomConfig(ServerBootstrap childHandler) { + if (nettyServerConfig.getServerSocketSndBufSize() > 0) { + log.info("server set SO_SNDBUF to {}", nettyServerConfig.getServerSocketSndBufSize()); + childHandler.childOption(ChannelOption.SO_SNDBUF, nettyServerConfig.getServerSocketSndBufSize()); + } + if (nettyServerConfig.getServerSocketRcvBufSize() > 0) { + log.info("server set SO_RCVBUF to {}", nettyServerConfig.getServerSocketRcvBufSize()); + childHandler.childOption(ChannelOption.SO_RCVBUF, nettyServerConfig.getServerSocketRcvBufSize()); + } + if (nettyServerConfig.getWriteBufferLowWaterMark() > 0 && nettyServerConfig.getWriteBufferHighWaterMark() > 0) { + log.info("server set netty WRITE_BUFFER_WATER_MARK to {},{}", + nettyServerConfig.getWriteBufferLowWaterMark(), nettyServerConfig.getWriteBufferHighWaterMark()); + childHandler.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark( + nettyServerConfig.getWriteBufferLowWaterMark(), nettyServerConfig.getWriteBufferHighWaterMark())); + } + + if (nettyServerConfig.isServerPooledByteBufAllocatorEnable()) { + childHandler.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT); + } } @Override public void shutdown() { try { - if (this.timer != null) { - this.timer.cancel(); + if (nettyServerConfig.isEnableShutdownGracefully() && isShuttingDown.compareAndSet(false, true)) { + Thread.sleep(Duration.ofSeconds(nettyServerConfig.getShutdownWaitTimeSeconds()).toMillis()); } + this.timer.stop(); + this.eventLoopGroupBoss.shutdownGracefully(); this.eventLoopGroupSelector.shutdownGracefully(); - if (this.nettyEventExecutor != null) { - this.nettyEventExecutor.shutdown(); - } + this.nettyEventExecutor.shutdown(); if (this.defaultEventExecutorGroup != null) { this.defaultEventExecutorGroup.shutdownGracefully(); @@ -260,11 +334,6 @@ public void shutdown() { } } - @Override - public void registerRPCHook(RPCHook rpcHook) { - this.rpcHook = rpcHook; - } - @Override public void registerProcessor(int requestCode, NettyRequestProcessor processor, ExecutorService executor) { ExecutorService executorThis = executor; @@ -272,18 +341,18 @@ public void registerProcessor(int requestCode, NettyRequestProcessor processor, executorThis = this.publicExecutor; } - Pair pair = new Pair(processor, executorThis); + Pair pair = new Pair<>(processor, executorThis); this.processorTable.put(requestCode, pair); } @Override public void registerDefaultProcessor(NettyRequestProcessor processor, ExecutorService executor) { - this.defaultRequestProcessor = new Pair(processor, executor); + this.defaultRequestProcessorPair = new Pair<>(processor, executor); } @Override public int localListenPort() { - return this.port; + return this.nettyServerConfig.getListenPort(); } @Override @@ -291,6 +360,28 @@ public Pair getProcessorPair(int request return processorTable.get(requestCode); } + @Override + public Pair getDefaultProcessorPair() { + return defaultRequestProcessorPair; + } + + @Override + public RemotingServer newRemotingServer(final int port) { + SubRemotingServer remotingServer = new SubRemotingServer(port, + this.nettyServerConfig.getServerOnewaySemaphoreValue(), + this.nettyServerConfig.getServerAsyncSemaphoreValue()); + NettyRemotingAbstract existingServer = this.remotingServerTable.putIfAbsent(port, remotingServer); + if (existingServer != null) { + throw new RuntimeException("The port " + port + " already in use by another RemotingServer"); + } + return remotingServer; + } + + @Override + public void removeRemotingServer(final int port) { + this.remotingServerTable.remove(port); + } + @Override public RemotingCommand invokeSync(final Channel channel, final RemotingCommand request, final long timeoutMillis) throws InterruptedException, RemotingSendRequestException, RemotingTimeoutException { @@ -314,51 +405,121 @@ public ChannelEventListener getChannelEventListener() { return channelEventListener; } - @Override - public RPCHook getRPCHook() { - return this.rpcHook; - } - @Override public ExecutorService getCallbackExecutor() { return this.publicExecutor; } - class HandshakeHandler extends SimpleChannelInboundHandler { + private void prepareSharableHandlers() { + tlsModeHandler = new TlsModeHandler(TlsSystemConfig.tlsMode); + encoder = new NettyEncoder(); + connectionManageHandler = new NettyConnectManageHandler(); + serverHandler = new NettyServerHandler(); + distributionHandler = new RemotingCodeDistributionHandler(); + } + + private void printRemotingCodeDistribution() { + if (distributionHandler != null) { + String inBoundSnapshotString = distributionHandler.getInBoundSnapshotString(); + if (inBoundSnapshotString != null) { + TRAFFIC_LOGGER.info("Port: {}, RequestCode Distribution: {}", + nettyServerConfig.getListenPort(), inBoundSnapshotString); + } + + String outBoundSnapshotString = distributionHandler.getOutBoundSnapshotString(); + if (outBoundSnapshotString != null) { + TRAFFIC_LOGGER.info("Port: {}, ResponseCode Distribution: {}", + nettyServerConfig.getListenPort(), outBoundSnapshotString); + } + } + } + + public DefaultEventExecutorGroup getDefaultEventExecutorGroup() { + return defaultEventExecutorGroup; + } + + public NettyEncoder getEncoder() { + return encoder; + } + + public NettyConnectManageHandler getConnectionManageHandler() { + return connectionManageHandler; + } + + public NettyServerHandler getServerHandler() { + return serverHandler; + } + + public RemotingCodeDistributionHandler getDistributionHandler() { + return distributionHandler; + } + + public class HandshakeHandler extends ByteToMessageDecoder { + + public HandshakeHandler() { + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List out) throws Exception { + try { + ProtocolDetectionResult detectionResult = HAProxyMessageDecoder.detectProtocol(byteBuf); + if (detectionResult.state() == ProtocolDetectionState.NEEDS_MORE_DATA) { + return; + } + if (detectionResult.state() == ProtocolDetectionState.DETECTED) { + ctx.pipeline().addAfter(defaultEventExecutorGroup, ctx.name(), HA_PROXY_DECODER, new HAProxyMessageDecoder()) + .addAfter(defaultEventExecutorGroup, HA_PROXY_DECODER, HA_PROXY_HANDLER, new HAProxyMessageHandler()) + .addAfter(defaultEventExecutorGroup, HA_PROXY_HANDLER, TLS_MODE_HANDLER, tlsModeHandler); + } else { + ctx.pipeline().addAfter(defaultEventExecutorGroup, ctx.name(), TLS_MODE_HANDLER, tlsModeHandler); + } + + try { + // Remove this handler + ctx.pipeline().remove(this); + } catch (NoSuchElementException e) { + log.error("Error while removing HandshakeHandler", e); + } + } catch (Exception e) { + log.error("process proxy protocol negotiator failed.", e); + throw e; + } + } + } + + @ChannelHandler.Sharable + public class TlsModeHandler extends SimpleChannelInboundHandler { private final TlsMode tlsMode; private static final byte HANDSHAKE_MAGIC_CODE = 0x16; - HandshakeHandler(TlsMode tlsMode) { + TlsModeHandler(TlsMode tlsMode) { this.tlsMode = tlsMode; } @Override - protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { - - // mark the current position so that we can peek the first byte to determine if the content is starting with - // TLS handshake - msg.markReaderIndex(); + protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) { - byte b = msg.getByte(0); + // Peek the current read index byte to determine if the content is starting with TLS handshake + byte b = msg.getByte(msg.readerIndex()); if (b == HANDSHAKE_MAGIC_CODE) { switch (tlsMode) { case DISABLED: ctx.close(); - log.warn("Clients intend to establish a SSL connection while this server is running in SSL disabled mode"); - break; + log.warn("Clients intend to establish an SSL connection while this server is running in SSL disabled mode"); + throw new UnsupportedOperationException("The NettyRemotingServer in SSL disabled mode doesn't support ssl client"); case PERMISSIVE: case ENFORCING: if (null != sslContext) { ctx.pipeline() - .addAfter(defaultEventExecutorGroup, HANDSHAKE_HANDLER_NAME, TLS_HANDLER_NAME, sslContext.newHandler(ctx.channel().alloc())) + .addAfter(defaultEventExecutorGroup, TLS_MODE_HANDLER, TLS_HANDLER_NAME, sslContext.newHandler(ctx.channel().alloc())) .addAfter(defaultEventExecutorGroup, TLS_HANDLER_NAME, FILE_REGION_ENCODER_NAME, new FileRegionEncoder()); log.info("Handlers prepended to channel pipeline to establish SSL connection"); } else { ctx.close(); - log.error("Trying to establish a SSL connection but sslContext is null"); + log.error("Trying to establish an SSL connection but sslContext is null"); } break; @@ -371,14 +532,11 @@ protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Excep log.warn("Clients intend to establish an insecure connection while this server is running in SSL enforcing mode"); } - // reset the reader index so that handshake negotiation may proceed as normal. - msg.resetReaderIndex(); - try { // Remove this handler ctx.pipeline().remove(this); } catch (NoSuchElementException e) { - log.error("Error while removing HandshakeHandler", e); + log.error("Error while removing TlsModeHandler", e); } // Hand over this message to the next . @@ -386,15 +544,41 @@ protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Excep } } - class NettyServerHandler extends SimpleChannelInboundHandler { + @ChannelHandler.Sharable + public class NettyServerHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) { + int localPort = RemotingHelper.parseSocketAddressPort(ctx.channel().localAddress()); + NettyRemotingAbstract remotingAbstract = NettyRemotingServer.this.remotingServerTable.get(localPort); + if (localPort != -1 && remotingAbstract != null) { + remotingAbstract.processMessageReceived(ctx, msg); + return; + } + // The related remoting server has been shutdown, so close the connected channel + RemotingHelper.closeChannel(ctx.channel()); + } @Override - protected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception { - processMessageReceived(ctx, msg); + public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { + Channel channel = ctx.channel(); + if (channel.isWritable()) { + if (!channel.config().isAutoRead()) { + channel.config().setAutoRead(true); + log.info("Channel[{}] turns writable, bytes to buffer before changing channel to un-writable: {}", + RemotingHelper.parseChannelRemoteAddr(channel), channel.bytesBeforeUnwritable()); + } + } else { + channel.config().setAutoRead(false); + log.warn("Channel[{}] auto-read is disabled, bytes to drain before it turns writable: {}", + RemotingHelper.parseChannelRemoteAddr(channel), channel.bytesBeforeWritable()); + } + super.channelWritabilityChanged(ctx); } } - class NettyConnectManageHandler extends ChannelDuplexHandler { + @ChannelHandler.Sharable + public class NettyConnectManageHandler extends ChannelDuplexHandler { @Override public void channelRegistered(ChannelHandlerContext ctx) throws Exception { final String remoteAddress = RemotingHelper.parseChannelRemoteAddr(ctx.channel()); @@ -432,13 +616,13 @@ public void channelInactive(ChannelHandlerContext ctx) throws Exception { } @Override - public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { if (evt instanceof IdleStateEvent) { IdleStateEvent event = (IdleStateEvent) evt; if (event.state().equals(IdleState.ALL_IDLE)) { final String remoteAddress = RemotingHelper.parseChannelRemoteAddr(ctx.channel()); log.warn("NETTY SERVER PIPELINE: IDLE exception [{}]", remoteAddress); - RemotingUtil.closeChannel(ctx.channel()); + RemotingHelper.closeChannel(ctx.channel()); if (NettyRemotingServer.this.channelEventListener != null) { NettyRemotingServer.this .putNettyEvent(new NettyEvent(NettyEventType.IDLE, remoteAddress, ctx.channel())); @@ -450,7 +634,7 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc } @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { final String remoteAddress = RemotingHelper.parseChannelRemoteAddr(ctx.channel()); log.warn("NETTY SERVER PIPELINE: exceptionCaught {}", remoteAddress); log.warn("NETTY SERVER PIPELINE: exceptionCaught exception.", cause); @@ -459,7 +643,174 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws E NettyRemotingServer.this.putNettyEvent(new NettyEvent(NettyEventType.EXCEPTION, remoteAddress, ctx.channel())); } - RemotingUtil.closeChannel(ctx.channel()); + RemotingHelper.closeChannel(ctx.channel()); + } + } + + /** + * The NettyRemotingServer supports bind multiple ports, each port bound by a SubRemotingServer. The + * SubRemotingServer will delegate all the functions to NettyRemotingServer, so the sub server can share all the + * resources from its parent server. + */ + class SubRemotingServer extends NettyRemotingAbstract implements RemotingServer { + private volatile int listenPort; + private volatile Channel serverChannel; + + SubRemotingServer(final int port, final int permitsOnway, final int permitsAsync) { + super(permitsOnway, permitsAsync); + listenPort = port; + } + + @Override + public void registerProcessor(final int requestCode, final NettyRequestProcessor processor, + final ExecutorService executor) { + ExecutorService executorThis = executor; + if (null == executor) { + executorThis = NettyRemotingServer.this.publicExecutor; + } + + Pair pair = new Pair<>(processor, executorThis); + this.processorTable.put(requestCode, pair); + } + + @Override + public void registerDefaultProcessor(final NettyRequestProcessor processor, final ExecutorService executor) { + this.defaultRequestProcessorPair = new Pair<>(processor, executor); + } + + @Override + public int localListenPort() { + return listenPort; + } + + @Override + public Pair getProcessorPair(final int requestCode) { + return this.processorTable.get(requestCode); + } + + @Override + public Pair getDefaultProcessorPair() { + return this.defaultRequestProcessorPair; + } + + @Override + public RemotingServer newRemotingServer(final int port) { + throw new UnsupportedOperationException("The SubRemotingServer of NettyRemotingServer " + + "doesn't support new nested RemotingServer"); + } + + @Override + public void removeRemotingServer(final int port) { + throw new UnsupportedOperationException("The SubRemotingServer of NettyRemotingServer " + + "doesn't support remove nested RemotingServer"); + } + + @Override + public RemotingCommand invokeSync(final Channel channel, final RemotingCommand request, + final long timeoutMillis) throws InterruptedException, RemotingSendRequestException, RemotingTimeoutException { + return this.invokeSyncImpl(channel, request, timeoutMillis); + } + + @Override + public void invokeAsync(final Channel channel, final RemotingCommand request, final long timeoutMillis, + final InvokeCallback invokeCallback) throws InterruptedException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException { + this.invokeAsyncImpl(channel, request, timeoutMillis, invokeCallback); + } + + @Override + public void invokeOneway(final Channel channel, final RemotingCommand request, + final long timeoutMillis) throws InterruptedException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException { + this.invokeOnewayImpl(channel, request, timeoutMillis); + } + + @Override + public void start() { + try { + if (listenPort < 0) { + listenPort = 0; + } + this.serverChannel = NettyRemotingServer.this.serverBootstrap.bind(listenPort).sync().channel(); + if (0 == listenPort) { + InetSocketAddress addr = (InetSocketAddress) this.serverChannel.localAddress(); + this.listenPort = addr.getPort(); + } + } catch (InterruptedException e) { + throw new RuntimeException("this.subRemotingServer.serverBootstrap.bind().sync() InterruptedException", e); + } + } + + @Override + public void shutdown() { + isShuttingDown.set(true); + if (this.serverChannel != null) { + try { + this.serverChannel.close().await(5, TimeUnit.SECONDS); + } catch (InterruptedException ignored) { + } + } + } + + @Override + public ChannelEventListener getChannelEventListener() { + return NettyRemotingServer.this.getChannelEventListener(); + } + + @Override + public ExecutorService getCallbackExecutor() { + return NettyRemotingServer.this.getCallbackExecutor(); + } + } + + public class HAProxyMessageHandler extends ChannelInboundHandlerAdapter { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof HAProxyMessage) { + handleWithMessage((HAProxyMessage) msg, ctx.channel()); + } else { + super.channelRead(ctx, msg); + } + ctx.pipeline().remove(this); + } + + /** + * The definition of key refers to the implementation of nginx + * ngx_http_core_module + * @param msg + * @param channel + */ + private void handleWithMessage(HAProxyMessage msg, Channel channel) { + try { + if (StringUtils.isNotBlank(msg.sourceAddress())) { + channel.attr(AttributeKeys.PROXY_PROTOCOL_ADDR).set(msg.sourceAddress()); + } + if (msg.sourcePort() > 0) { + channel.attr(AttributeKeys.PROXY_PROTOCOL_PORT).set(String.valueOf(msg.sourcePort())); + } + if (StringUtils.isNotBlank(msg.destinationAddress())) { + channel.attr(AttributeKeys.PROXY_PROTOCOL_SERVER_ADDR).set(msg.destinationAddress()); + } + if (msg.destinationPort() > 0) { + channel.attr(AttributeKeys.PROXY_PROTOCOL_SERVER_PORT).set(String.valueOf(msg.destinationPort())); + } + if (CollectionUtils.isNotEmpty(msg.tlvs())) { + msg.tlvs().forEach(tlv -> { + handleHAProxyTLV(tlv, channel); + }); + } + } finally { + msg.release(); + } + } + } + + protected void handleHAProxyTLV(HAProxyTLV tlv, Channel channel) { + byte[] valueBytes = ByteBufUtil.getBytes(tlv.content()); + if (!BinaryUtil.isAscii(valueBytes)) { + return; } + AttributeKey key = AttributeKeys.valueOf( + HAProxyConstants.PROXY_PROTOCOL_TLV_PREFIX + String.format("%02x", tlv.typeByteValue())); + channel.attr(key).set(new String(valueBytes, CharsetUtil.UTF_8)); } } diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyServerConfig.java b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyServerConfig.java index a5e2a232dd5..756661f623f 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyServerConfig.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettyServerConfig.java @@ -17,7 +17,13 @@ package org.apache.rocketmq.remoting.netty; public class NettyServerConfig implements Cloneable { - private int listenPort = 8888; + + /** + * Bind address may be hostname, IPv4 or IPv6. + * By default, it's wildcard address, listening all network interfaces. + */ + private String bindAddress = "0.0.0.0"; + private int listenPort = 0; private int serverWorkerThreads = 8; private int serverCallbackExecutorThreads = 0; private int serverSelectorThreads = 3; @@ -27,10 +33,16 @@ public class NettyServerConfig implements Cloneable { private int serverSocketSndBufSize = NettySystemConfig.socketSndbufSize; private int serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; + private int writeBufferHighWaterMark = NettySystemConfig.writeBufferHighWaterMark; + private int writeBufferLowWaterMark = NettySystemConfig.writeBufferLowWaterMark; + private int serverSocketBacklog = NettySystemConfig.socketBacklog; private boolean serverPooledByteBufAllocatorEnable = true; + private boolean enableShutdownGracefully = false; + private int shutdownWaitTimeSeconds = 30; + /** - * make make install + * make install * * * ../glibc-2.10.1/configure \ --prefix=/usr \ --with-headers=/usr/include \ @@ -38,6 +50,14 @@ public class NettyServerConfig implements Cloneable { */ private boolean useEpollNativeSelector = false; + public String getBindAddress() { + return bindAddress; + } + + public void setBindAddress(String bindAddress) { + this.bindAddress = bindAddress; + } + public int getListenPort() { return listenPort; } @@ -110,6 +130,14 @@ public void setServerSocketRcvBufSize(int serverSocketRcvBufSize) { this.serverSocketRcvBufSize = serverSocketRcvBufSize; } + public int getServerSocketBacklog() { + return serverSocketBacklog; + } + + public void setServerSocketBacklog(int serverSocketBacklog) { + this.serverSocketBacklog = serverSocketBacklog; + } + public boolean isServerPooledByteBufAllocatorEnable() { return serverPooledByteBufAllocatorEnable; } @@ -130,4 +158,36 @@ public void setUseEpollNativeSelector(boolean useEpollNativeSelector) { public Object clone() throws CloneNotSupportedException { return (NettyServerConfig) super.clone(); } + + public int getWriteBufferLowWaterMark() { + return writeBufferLowWaterMark; + } + + public void setWriteBufferLowWaterMark(int writeBufferLowWaterMark) { + this.writeBufferLowWaterMark = writeBufferLowWaterMark; + } + + public int getWriteBufferHighWaterMark() { + return writeBufferHighWaterMark; + } + + public void setWriteBufferHighWaterMark(int writeBufferHighWaterMark) { + this.writeBufferHighWaterMark = writeBufferHighWaterMark; + } + + public boolean isEnableShutdownGracefully() { + return enableShutdownGracefully; + } + + public void setEnableShutdownGracefully(boolean enableShutdownGracefully) { + this.enableShutdownGracefully = enableShutdownGracefully; + } + + public int getShutdownWaitTimeSeconds() { + return shutdownWaitTimeSeconds; + } + + public void setShutdownWaitTimeSeconds(int shutdownWaitTimeSeconds) { + this.shutdownWaitTimeSeconds = shutdownWaitTimeSeconds; + } } diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettySystemConfig.java b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettySystemConfig.java index 6357c03ba8b..edad8444ed8 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettySystemConfig.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/NettySystemConfig.java @@ -24,10 +24,24 @@ public class NettySystemConfig { "com.rocketmq.remoting.socket.sndbuf.size"; public static final String COM_ROCKETMQ_REMOTING_SOCKET_RCVBUF_SIZE = "com.rocketmq.remoting.socket.rcvbuf.size"; + public static final String COM_ROCKETMQ_REMOTING_SOCKET_BACKLOG = + "com.rocketmq.remoting.socket.backlog"; public static final String COM_ROCKETMQ_REMOTING_CLIENT_ASYNC_SEMAPHORE_VALUE = "com.rocketmq.remoting.clientAsyncSemaphoreValue"; public static final String COM_ROCKETMQ_REMOTING_CLIENT_ONEWAY_SEMAPHORE_VALUE = "com.rocketmq.remoting.clientOnewaySemaphoreValue"; + public static final String COM_ROCKETMQ_REMOTING_CLIENT_WORKER_SIZE = + "com.rocketmq.remoting.client.worker.size"; + public static final String COM_ROCKETMQ_REMOTING_CLIENT_CONNECT_TIMEOUT = + "com.rocketmq.remoting.client.connect.timeout"; + public static final String COM_ROCKETMQ_REMOTING_CLIENT_CHANNEL_MAX_IDLE_SECONDS = + "com.rocketmq.remoting.client.channel.maxIdleTimeSeconds"; + public static final String COM_ROCKETMQ_REMOTING_CLIENT_CLOSE_SOCKET_IF_TIMEOUT = + "com.rocketmq.remoting.client.closeSocketIfTimeout"; + public static final String COM_ROCKETMQ_REMOTING_WRITE_BUFFER_HIGH_WATER_MARK_VALUE = + "com.rocketmq.remoting.write.buffer.high.water.mark"; + public static final String COM_ROCKETMQ_REMOTING_WRITE_BUFFER_LOW_WATER_MARK = + "com.rocketmq.remoting.write.buffer.low.water.mark"; public static final boolean NETTY_POOLED_BYTE_BUF_ALLOCATOR_ENABLE = // Boolean.parseBoolean(System.getProperty(COM_ROCKETMQ_REMOTING_NETTY_POOLED_BYTE_BUF_ALLOCATOR_ENABLE, "false")); @@ -36,7 +50,22 @@ public class NettySystemConfig { public static final int CLIENT_ONEWAY_SEMAPHORE_VALUE = Integer.parseInt(System.getProperty(COM_ROCKETMQ_REMOTING_CLIENT_ONEWAY_SEMAPHORE_VALUE, "65535")); public static int socketSndbufSize = - Integer.parseInt(System.getProperty(COM_ROCKETMQ_REMOTING_SOCKET_SNDBUF_SIZE, "65535")); + Integer.parseInt(System.getProperty(COM_ROCKETMQ_REMOTING_SOCKET_SNDBUF_SIZE, "0")); public static int socketRcvbufSize = - Integer.parseInt(System.getProperty(COM_ROCKETMQ_REMOTING_SOCKET_RCVBUF_SIZE, "65535")); + Integer.parseInt(System.getProperty(COM_ROCKETMQ_REMOTING_SOCKET_RCVBUF_SIZE, "0")); + public static int socketBacklog = + Integer.parseInt(System.getProperty(COM_ROCKETMQ_REMOTING_SOCKET_BACKLOG, "1024")); + public static int clientWorkerSize = + Integer.parseInt(System.getProperty(COM_ROCKETMQ_REMOTING_CLIENT_WORKER_SIZE, "4")); + public static int connectTimeoutMillis = + Integer.parseInt(System.getProperty(COM_ROCKETMQ_REMOTING_CLIENT_CONNECT_TIMEOUT, "3000")); + public static int clientChannelMaxIdleTimeSeconds = + Integer.parseInt(System.getProperty(COM_ROCKETMQ_REMOTING_CLIENT_CHANNEL_MAX_IDLE_SECONDS, "120")); + public static boolean clientCloseSocketIfTimeout = + Boolean.parseBoolean(System.getProperty(COM_ROCKETMQ_REMOTING_CLIENT_CLOSE_SOCKET_IF_TIMEOUT, "true")); + public static int writeBufferHighWaterMark = + Integer.parseInt(System.getProperty(COM_ROCKETMQ_REMOTING_WRITE_BUFFER_HIGH_WATER_MARK_VALUE, "0")); + public static int writeBufferLowWaterMark = + Integer.parseInt(System.getProperty(COM_ROCKETMQ_REMOTING_WRITE_BUFFER_LOW_WATER_MARK, "0")); + } diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/RemotingCodeDistributionHandler.java b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/RemotingCodeDistributionHandler.java new file mode 100644 index 00000000000..c6a97fe441b --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/RemotingCodeDistributionHandler.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.netty; + +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.LongAdder; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +@ChannelHandler.Sharable +public class RemotingCodeDistributionHandler extends ChannelDuplexHandler { + + private final ConcurrentMap inboundDistribution; + private final ConcurrentMap outboundDistribution; + + public RemotingCodeDistributionHandler() { + inboundDistribution = new ConcurrentHashMap<>(); + outboundDistribution = new ConcurrentHashMap<>(); + } + + private void countInbound(int requestCode) { + LongAdder item = inboundDistribution.computeIfAbsent(requestCode, k -> new LongAdder()); + item.increment(); + } + + private void countOutbound(int responseCode) { + LongAdder item = outboundDistribution.computeIfAbsent(responseCode, k -> new LongAdder()); + item.increment(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof RemotingCommand) { + RemotingCommand cmd = (RemotingCommand) msg; + countInbound(cmd.getCode()); + } + ctx.fireChannelRead(msg); + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (msg instanceof RemotingCommand) { + RemotingCommand cmd = (RemotingCommand) msg; + countOutbound(cmd.getCode()); + } + ctx.write(msg, promise); + } + + private Map getDistributionSnapshot(Map countMap) { + Map map = new HashMap<>(countMap.size()); + for (Map.Entry entry : countMap.entrySet()) { + map.put(entry.getKey(), entry.getValue().sumThenReset()); + } + return map; + } + + private String snapshotToString(Map distribution) { + if (null != distribution && !distribution.isEmpty()) { + StringBuilder sb = new StringBuilder("{"); + boolean first = true; + for (Map.Entry entry : distribution.entrySet()) { + if (0L == entry.getValue()) { + continue; + } + sb.append(first ? "" : ", ").append(entry.getKey()).append(":").append(entry.getValue()); + first = false; + } + if (first) { + return null; + } + sb.append("}"); + return sb.toString(); + } + return null; + } + + public String getInBoundSnapshotString() { + return this.snapshotToString(this.getDistributionSnapshot(this.inboundDistribution)); + } + + public String getOutBoundSnapshotString() { + return this.snapshotToString(this.getDistributionSnapshot(this.outboundDistribution)); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/RemotingResponseCallback.java b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/RemotingResponseCallback.java new file mode 100644 index 00000000000..7185f20d1d6 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/RemotingResponseCallback.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.netty; + +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public interface RemotingResponseCallback { + void callback(RemotingCommand response); +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/RequestTask.java b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/RequestTask.java index 737ed7426d0..57ed3606090 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/RequestTask.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/RequestTask.java @@ -25,7 +25,7 @@ public class RequestTask implements Runnable { private final long createTimestamp = System.currentTimeMillis(); private final Channel channel; private final RemotingCommand request; - private boolean stopRun = false; + private volatile boolean stopRun = false; public RequestTask(final Runnable runnable, final Channel channel, final RemotingCommand request) { this.runnable = runnable; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/ResponseFuture.java b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/ResponseFuture.java index 1157c450283..0882818feac 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/ResponseFuture.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/ResponseFuture.java @@ -16,15 +16,21 @@ */ package org.apache.rocketmq.remoting.netty; +import io.netty.channel.Channel; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.rocketmq.remoting.InvokeCallback; import org.apache.rocketmq.remoting.common.SemaphoreReleaseOnlyOnce; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.exception.RemotingSendRequestException; +import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; import org.apache.rocketmq.remoting.protocol.RemotingCommand; public class ResponseFuture { + private final Channel channel; private final int opaque; + private final RemotingCommand request; private final long timeoutMillis; private final InvokeCallback invokeCallback; private final long beginTimestamp = System.currentTimeMillis(); @@ -36,10 +42,18 @@ public class ResponseFuture { private volatile RemotingCommand responseCommand; private volatile boolean sendRequestOK = true; private volatile Throwable cause; + private volatile boolean interrupted = false; - public ResponseFuture(int opaque, long timeoutMillis, InvokeCallback invokeCallback, - SemaphoreReleaseOnlyOnce once) { + public ResponseFuture(Channel channel, int opaque, long timeoutMillis, InvokeCallback invokeCallback, + SemaphoreReleaseOnlyOnce once) { + this(channel, opaque, null, timeoutMillis, invokeCallback, once); + } + + public ResponseFuture(Channel channel, int opaque, RemotingCommand request, long timeoutMillis, InvokeCallback invokeCallback, + SemaphoreReleaseOnlyOnce once) { + this.channel = channel; this.opaque = opaque; + this.request = request; this.timeoutMillis = timeoutMillis; this.invokeCallback = invokeCallback; this.once = once; @@ -48,11 +62,28 @@ public ResponseFuture(int opaque, long timeoutMillis, InvokeCallback invokeCallb public void executeInvokeCallback() { if (invokeCallback != null) { if (this.executeCallbackOnlyOnce.compareAndSet(false, true)) { + RemotingCommand response = getResponseCommand(); + if (response != null) { + invokeCallback.operationSucceed(response); + } else { + if (!isSendRequestOK()) { + invokeCallback.operationFail(new RemotingSendRequestException(channel.remoteAddress().toString(), getCause())); + } else if (isTimeout()) { + invokeCallback.operationFail(new RemotingTimeoutException(channel.remoteAddress().toString(), getTimeoutMillis(), getCause())); + } else { + invokeCallback.operationFail(new RemotingException(getRequestCommand().toString(), getCause())); + } + } invokeCallback.operationComplete(this); } } } + public void interrupt() { + interrupted = true; + executeInvokeCallback(); + } + public void release() { if (this.once != null) { this.once.release(); @@ -114,6 +145,18 @@ public int getOpaque() { return opaque; } + public RemotingCommand getRequestCommand() { + return request; + } + + public Channel getChannel() { + return channel; + } + + public boolean isInterrupted() { + return interrupted; + } + @Override public String toString() { return "ResponseFuture [responseCommand=" + responseCommand + ", sendRequestOK=" + sendRequestOK diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/TlsHelper.java b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/TlsHelper.java index 3a74b4b67bc..9e73ad7ae0a 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/netty/TlsHelper.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/netty/TlsHelper.java @@ -30,9 +30,9 @@ import java.io.InputStream; import java.security.cert.CertificateException; import java.util.Properties; -import org.apache.rocketmq.remoting.common.RemotingHelper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_CLIENT_AUTHSERVER; import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_CLIENT_CERTPATH; @@ -73,7 +73,7 @@ public interface DecryptionStrategy { InputStream decryptPrivateKey(String privateKeyEncryptPath, boolean forClient) throws IOException; } - private static final Logger LOGGER = LoggerFactory.getLogger(RemotingHelper.ROCKETMQ_REMOTING); + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.ROCKETMQ_REMOTING_NAME); private static DecryptionStrategy decryptionStrategy = new DecryptionStrategy() { @Override @@ -133,7 +133,7 @@ public static SslContext buildSslContext(boolean forClient) throws IOException, SelfSignedCertificate selfSignedCertificate = new SelfSignedCertificate(); return SslContextBuilder .forServer(selfSignedCertificate.certificate(), selfSignedCertificate.privateKey()) - .sslProvider(SslProvider.JDK) + .sslProvider(provider) .clientAuth(ClientAuth.OPTIONAL) .build(); } else { diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/BitSetSerializerDeserializer.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/BitSetSerializerDeserializer.java new file mode 100644 index 00000000000..8f53c0250bf --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/BitSetSerializerDeserializer.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol; + +import com.alibaba.fastjson.parser.DefaultJSONParser; +import com.alibaba.fastjson.parser.JSONToken; +import com.alibaba.fastjson.parser.deserializer.ObjectDeserializer; +import com.alibaba.fastjson.serializer.JSONSerializer; +import com.alibaba.fastjson.serializer.ObjectSerializer; +import com.alibaba.fastjson.serializer.SerializeWriter; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.BitSet; + +public class BitSetSerializerDeserializer implements ObjectSerializer, ObjectDeserializer { + + @Override + public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features) throws IOException { + SerializeWriter out = serializer.out; + out.writeByteArray(((BitSet) object).toByteArray()); + } + + @SuppressWarnings("unchecked") + @Override + public T deserialze(DefaultJSONParser parser, Type type, Object fieldName) { + byte[] bytes = parser.parseObject(byte[].class); + if (bytes != null) { + return (T) BitSet.valueOf(bytes); + } + return null; + } + + @Override + public int getFastMatchToken() { + return JSONToken.LITERAL_STRING; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/BrokerSyncInfo.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/BrokerSyncInfo.java new file mode 100644 index 00000000000..9340a70e6ee --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/BrokerSyncInfo.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol; + +public class BrokerSyncInfo extends RemotingSerializable { + /** + * For slave online sync, retrieve HA address before register + */ + private String masterHaAddress; + + private long masterFlushOffset; + + private String masterAddress; + + public BrokerSyncInfo(String masterHaAddress, long masterFlushOffset, String masterAddress) { + this.masterHaAddress = masterHaAddress; + this.masterFlushOffset = masterFlushOffset; + this.masterAddress = masterAddress; + } + + public String getMasterHaAddress() { + return masterHaAddress; + } + + public void setMasterHaAddress(String masterHaAddress) { + this.masterHaAddress = masterHaAddress; + } + + public long getMasterFlushOffset() { + return masterFlushOffset; + } + + public void setMasterFlushOffset(long masterFlushOffset) { + this.masterFlushOffset = masterFlushOffset; + } + + public String getMasterAddress() { + return masterAddress; + } + + public void setMasterAddress(String masterAddress) { + this.masterAddress = masterAddress; + } + + @Override + public String toString() { + return "BrokerSyncInfo{" + + "masterHaAddress='" + masterHaAddress + '\'' + + ", masterFlushOffset=" + masterFlushOffset + + ", masterAddress=" + masterAddress + + '}'; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/DataVersion.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/DataVersion.java new file mode 100644 index 00000000000..655cf889bfa --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/DataVersion.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol; + +import java.util.concurrent.atomic.AtomicLong; + +public class DataVersion extends RemotingSerializable { + private long stateVersion = 0L; + private long timestamp = System.currentTimeMillis(); + private AtomicLong counter = new AtomicLong(0); + + public void assignNewOne(final DataVersion dataVersion) { + this.timestamp = dataVersion.timestamp; + this.stateVersion = dataVersion.stateVersion; + this.counter.set(dataVersion.counter.get()); + } + + public void nextVersion() { + this.nextVersion(0L); + } + + public void nextVersion(long stateVersion) { + this.timestamp = System.currentTimeMillis(); + this.stateVersion = stateVersion; + this.counter.incrementAndGet(); + } + + public long getStateVersion() { + return stateVersion; + } + + public void setStateVersion(long stateVersion) { + this.stateVersion = stateVersion; + } + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public AtomicLong getCounter() { + return counter; + } + + public void setCounter(AtomicLong counter) { + this.counter = counter; + } + + @Override + public boolean equals(final Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + DataVersion version = (DataVersion) o; + + if (getStateVersion() != version.getStateVersion()) + return false; + if (getTimestamp() != version.getTimestamp()) + return false; + + if (counter != null && version.counter != null) { + return counter.longValue() == version.counter.longValue(); + } + + return null == counter && null == version.counter; + + } + + @Override + public int hashCode() { + int result = (int) (getStateVersion() ^ (getStateVersion() >>> 32)); + result = 31 * result + (int) (getTimestamp() ^ (getTimestamp() >>> 32)); + if (null != counter) { + long l = counter.get(); + result = 31 * result + (int) (l ^ (l >>> 32)); + } + return result; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("DataVersion["); + sb.append("timestamp=").append(timestamp); + sb.append(", counter=").append(counter); + sb.append(']'); + return sb.toString(); + } + + public int compare(DataVersion dataVersion) { + if (this.getStateVersion() > dataVersion.getStateVersion()) { + return 1; + } else if (this.getStateVersion() < dataVersion.getStateVersion()) { + return -1; + } else if (this.getCounter().get() > dataVersion.getCounter().get()) { + return 1; + } else if (this.getCounter().get() < dataVersion.getCounter().get()) { + return -1; + } else if (this.getTimestamp() > dataVersion.getTimestamp()) { + return 1; + } else if (this.getTimestamp() < dataVersion.getTimestamp()) { + return -1; + } + return 0; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/EpochEntry.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/EpochEntry.java new file mode 100644 index 00000000000..4ff81760adb --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/EpochEntry.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol; + +import java.util.Objects; + +public class EpochEntry extends RemotingSerializable { + + private int epoch; + private long startOffset; + private long endOffset = Long.MAX_VALUE; + + public EpochEntry(EpochEntry entry) { + this.epoch = entry.getEpoch(); + this.startOffset = entry.getStartOffset(); + this.endOffset = entry.getEndOffset(); + } + + public EpochEntry(int epoch, long startOffset) { + this.epoch = epoch; + this.startOffset = startOffset; + } + + public EpochEntry(int epoch, long startOffset, long endOffset) { + this.epoch = epoch; + this.startOffset = startOffset; + this.endOffset = endOffset; + } + + public int getEpoch() { + return epoch; + } + + public void setEpoch(int epoch) { + this.epoch = epoch; + } + + public long getStartOffset() { + return startOffset; + } + + public void setStartOffset(long startOffset) { + this.startOffset = startOffset; + } + + public long getEndOffset() { + return endOffset; + } + + public void setEndOffset(long endOffset) { + this.endOffset = endOffset; + } + + @Override + public String toString() { + return "EpochEntry{" + + "epoch=" + epoch + + ", startOffset=" + startOffset + + ", endOffset=" + endOffset + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + EpochEntry entry = (EpochEntry) o; + return epoch == entry.epoch && startOffset == entry.startOffset && endOffset == entry.endOffset; + } + + @Override + public int hashCode() { + return Objects.hash(epoch, startOffset, endOffset); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/FastCodesHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/FastCodesHeader.java new file mode 100644 index 00000000000..ebf9930889a --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/FastCodesHeader.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol; + +import java.util.HashMap; + +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +import io.netty.buffer.ByteBuf; + +public interface FastCodesHeader { + + default String getAndCheckNotNull(HashMap fields, String field) { + String value = fields.get(field); + if (value == null) { + String headerClass = this.getClass().getSimpleName(); + RemotingCommand.log.error("the custom field {}.{} is null", headerClass, field); + // no exception throws, keep compatible with RemotingCommand.decodeCommandCustomHeader + } + return value; + } + + default void writeIfNotNull(ByteBuf out, String key, Object value) { + if (value != null) { + RocketMQSerializable.writeStr(out, true, key); + RocketMQSerializable.writeStr(out, false, value.toString()); + } + } + + void encode(ByteBuf out); + + void decode(HashMap fields) throws RemotingCommandException; + + +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/ForbiddenType.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/ForbiddenType.java new file mode 100644 index 00000000000..0701dc57fc5 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/ForbiddenType.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol; + +/** + * + * gives the reason for a no permission messaging pulling. + * + */ +public interface ForbiddenType { + + /** + * 1=forbidden by broker + */ + int BROKER_FORBIDDEN = 1; + /** + * 2=forbidden by groupId + */ + int GROUP_FORBIDDEN = 2; + /** + * 3=forbidden by topic + */ + int TOPIC_FORBIDDEN = 3; + /** + * 4=forbidden by brocasting mode + */ + int BROADCASTING_DISABLE_FORBIDDEN = 4; + /** + * 5=forbidden for a substription(group with a topic) + */ + int SUBSCRIPTION_FORBIDDEN = 5; + +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/LanguageCode.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/LanguageCode.java index 17ce9190037..2df9fbf0278 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/LanguageCode.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/LanguageCode.java @@ -17,6 +17,11 @@ package org.apache.rocketmq.remoting.protocol; +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + public enum LanguageCode { JAVA((byte) 0), CPP((byte) 1), @@ -28,7 +33,9 @@ public enum LanguageCode { OTHER((byte) 7), HTTP((byte) 8), GO((byte) 9), - PHP((byte) 10); + PHP((byte) 10), + OMS((byte) 11), + RUST((byte) 12); private byte code; @@ -48,4 +55,10 @@ public static LanguageCode valueOf(byte code) { public byte getCode() { return code; } + + private static final Map MAP = Arrays.stream(LanguageCode.values()).collect(Collectors.toMap(LanguageCode::name, Function.identity())); + + public static LanguageCode getCode(String language) { + return MAP.get(language); + } } diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/MQProtosHelper.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/MQProtosHelper.java similarity index 87% rename from common/src/main/java/org/apache/rocketmq/common/protocol/MQProtosHelper.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/MQProtosHelper.java index c1cd69cd6ee..918377f7e34 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/MQProtosHelper.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/MQProtosHelper.java @@ -15,14 +15,13 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol; +package org.apache.rocketmq.remoting.protocol; import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.common.protocol.header.namesrv.RegisterBrokerRequestHeader; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.remoting.common.RemotingHelper; -import org.apache.rocketmq.remoting.protocol.RemotingCommand; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.header.namesrv.RegisterBrokerRequestHeader; public class MQProtosHelper { private static final Logger log = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/NamespaceUtil.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/NamespaceUtil.java new file mode 100644 index 00000000000..54016b392d4 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/NamespaceUtil.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol; + +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.topic.TopicValidator; + +public class NamespaceUtil { + public static final char NAMESPACE_SEPARATOR = '%'; + public static final String STRING_BLANK = ""; + public static final int RETRY_PREFIX_LENGTH = MixAll.RETRY_GROUP_TOPIC_PREFIX.length(); + public static final int DLQ_PREFIX_LENGTH = MixAll.DLQ_GROUP_TOPIC_PREFIX.length(); + + /** + * Unpack namespace from resource, just like: + * (1) MQ_INST_XX%Topic_XXX --> Topic_XXX + * (2) %RETRY%MQ_INST_XX%GID_XXX --> %RETRY%GID_XXX + * + * @param resourceWithNamespace, topic/groupId with namespace. + * @return topic/groupId without namespace. + */ + public static String withoutNamespace(String resourceWithNamespace) { + if (StringUtils.isEmpty(resourceWithNamespace) || isSystemResource(resourceWithNamespace)) { + return resourceWithNamespace; + } + + StringBuilder stringBuilder = new StringBuilder(); + if (isRetryTopic(resourceWithNamespace)) { + stringBuilder.append(MixAll.RETRY_GROUP_TOPIC_PREFIX); + } + if (isDLQTopic(resourceWithNamespace)) { + stringBuilder.append(MixAll.DLQ_GROUP_TOPIC_PREFIX); + } + + String resourceWithoutRetryAndDLQ = withOutRetryAndDLQ(resourceWithNamespace); + int index = resourceWithoutRetryAndDLQ.indexOf(NAMESPACE_SEPARATOR); + if (index > 0) { + String resourceWithoutNamespace = resourceWithoutRetryAndDLQ.substring(index + 1); + return stringBuilder.append(resourceWithoutNamespace).toString(); + } + + return resourceWithNamespace; + } + + /** + * If resource contains the namespace, unpack namespace from resource, just like: + * (1) (MQ_INST_XX1%Topic_XXX1, MQ_INST_XX1) --> Topic_XXX1 + * (2) (MQ_INST_XX2%Topic_XXX2, NULL) --> MQ_INST_XX2%Topic_XXX2 + * (3) (%RETRY%MQ_INST_XX1%GID_XXX1, MQ_INST_XX1) --> %RETRY%GID_XXX1 + * (4) (%RETRY%MQ_INST_XX2%GID_XXX2, MQ_INST_XX3) --> %RETRY%MQ_INST_XX2%GID_XXX2 + * + * @param resourceWithNamespace, topic/groupId with namespace. + * @param namespace, namespace to be unpacked. + * @return topic/groupId without namespace. + */ + public static String withoutNamespace(String resourceWithNamespace, String namespace) { + if (StringUtils.isEmpty(resourceWithNamespace) || StringUtils.isEmpty(namespace)) { + return resourceWithNamespace; + } + + String resourceWithoutRetryAndDLQ = withOutRetryAndDLQ(resourceWithNamespace); + if (resourceWithoutRetryAndDLQ.startsWith(namespace + NAMESPACE_SEPARATOR)) { + return withoutNamespace(resourceWithNamespace); + } + + return resourceWithNamespace; + } + + public static String wrapNamespace(String namespace, String resourceWithOutNamespace) { + if (StringUtils.isEmpty(namespace) || StringUtils.isEmpty(resourceWithOutNamespace)) { + return resourceWithOutNamespace; + } + + if (isSystemResource(resourceWithOutNamespace) || isAlreadyWithNamespace(resourceWithOutNamespace, namespace)) { + return resourceWithOutNamespace; + } + + String resourceWithoutRetryAndDLQ = withOutRetryAndDLQ(resourceWithOutNamespace); + StringBuilder stringBuilder = new StringBuilder(); + + if (isRetryTopic(resourceWithOutNamespace)) { + stringBuilder.append(MixAll.RETRY_GROUP_TOPIC_PREFIX); + } + + if (isDLQTopic(resourceWithOutNamespace)) { + stringBuilder.append(MixAll.DLQ_GROUP_TOPIC_PREFIX); + } + + return stringBuilder.append(namespace).append(NAMESPACE_SEPARATOR).append(resourceWithoutRetryAndDLQ).toString(); + + } + + public static boolean isAlreadyWithNamespace(String resource, String namespace) { + if (StringUtils.isEmpty(namespace) || StringUtils.isEmpty(resource) || isSystemResource(resource)) { + return false; + } + + String resourceWithoutRetryAndDLQ = withOutRetryAndDLQ(resource); + + return resourceWithoutRetryAndDLQ.startsWith(namespace + NAMESPACE_SEPARATOR); + } + + public static String wrapNamespaceAndRetry(String namespace, String consumerGroup) { + if (StringUtils.isEmpty(consumerGroup)) { + return null; + } + + return new StringBuilder() + .append(MixAll.RETRY_GROUP_TOPIC_PREFIX) + .append(wrapNamespace(namespace, consumerGroup)) + .toString(); + } + + public static String getNamespaceFromResource(String resource) { + if (StringUtils.isEmpty(resource) || isSystemResource(resource)) { + return STRING_BLANK; + } + String resourceWithoutRetryAndDLQ = withOutRetryAndDLQ(resource); + int index = resourceWithoutRetryAndDLQ.indexOf(NAMESPACE_SEPARATOR); + + return index > 0 ? resourceWithoutRetryAndDLQ.substring(0, index) : STRING_BLANK; + } + + private static String withOutRetryAndDLQ(String originalResource) { + if (StringUtils.isEmpty(originalResource)) { + return STRING_BLANK; + } + if (isRetryTopic(originalResource)) { + return originalResource.substring(RETRY_PREFIX_LENGTH); + } + + if (isDLQTopic(originalResource)) { + return originalResource.substring(DLQ_PREFIX_LENGTH); + } + + return originalResource; + } + + private static boolean isSystemResource(String resource) { + if (StringUtils.isEmpty(resource)) { + return false; + } + + if (TopicValidator.isSystemTopic(resource) || MixAll.isSysConsumerGroup(resource)) { + return true; + } + + return false; + } + + public static boolean isRetryTopic(String resource) { + return StringUtils.isNotBlank(resource) && resource.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX); + } + + public static boolean isDLQTopic(String resource) { + return StringUtils.isNotBlank(resource) && resource.startsWith(MixAll.DLQ_GROUP_TOPIC_PREFIX); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RemotingCommand.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RemotingCommand.java index 2f8cb388440..0fa275e822e 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RemotingCommand.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RemotingCommand.java @@ -17,33 +17,44 @@ package org.apache.rocketmq.remoting.protocol; import com.alibaba.fastjson.annotation.JSONField; +import com.google.common.base.Stopwatch; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; import java.lang.annotation.Annotation; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; +import java.nio.Buffer; import java.nio.ByteBuffer; +import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.BoundaryType; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; -import org.apache.rocketmq.remoting.common.RemotingHelper; import org.apache.rocketmq.remoting.exception.RemotingCommandException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class RemotingCommand { public static final String SERIALIZE_TYPE_PROPERTY = "rocketmq.serialize.type"; public static final String SERIALIZE_TYPE_ENV = "ROCKETMQ_SERIALIZE_TYPE"; public static final String REMOTING_VERSION_KEY = "rocketmq.remoting.version"; - private static final Logger log = LoggerFactory.getLogger(RemotingHelper.ROCKETMQ_REMOTING); + static final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_REMOTING_NAME); private static final int RPC_TYPE = 0; // 0, REQUEST_COMMAND private static final int RPC_ONEWAY = 1; // 0, RPC private static final Map, Field[]> CLASS_HASH_MAP = - new HashMap, Field[]>(); - private static final Map CANONICAL_NAME_CACHE = new HashMap(); + new HashMap<>(); + private static final Map CANONICAL_NAME_CACHE = new HashMap<>(); // 1, Oneway // 1, RESPONSE_COMMAND - private static final Map NULLABLE_FIELD_CACHE = new HashMap(); + private static final Map NULLABLE_FIELD_CACHE = new HashMap<>(); private static final String STRING_CANONICAL_NAME = String.class.getCanonicalName(); private static final String DOUBLE_CANONICAL_NAME_1 = Double.class.getCanonicalName(); private static final String DOUBLE_CANONICAL_NAME_2 = double.class.getCanonicalName(); @@ -53,6 +64,7 @@ public class RemotingCommand { private static final String LONG_CANONICAL_NAME_2 = long.class.getCanonicalName(); private static final String BOOLEAN_CANONICAL_NAME_1 = Boolean.class.getCanonicalName(); private static final String BOOLEAN_CANONICAL_NAME_2 = boolean.class.getCanonicalName(); + private static final String BOUNDARY_TYPE_CANONICAL_NAME = BoundaryType.class.getCanonicalName(); private static volatile int configVersion = -1; private static AtomicInteger requestId = new AtomicInteger(0); @@ -60,7 +72,7 @@ public class RemotingCommand { static { final String protocol = System.getProperty(SERIALIZE_TYPE_PROPERTY, System.getenv(SERIALIZE_TYPE_ENV)); - if (!isBlank(protocol)) { + if (!StringUtils.isBlank(protocol)) { try { serializeTypeConfigInThisServer = SerializeType.valueOf(protocol); } catch (IllegalArgumentException e) { @@ -77,10 +89,13 @@ public class RemotingCommand { private String remark; private HashMap extFields; private transient CommandCustomHeader customHeader; + private transient CommandCustomHeader cachedHeader; private SerializeType serializeTypeCurrentRPC = serializeTypeConfigInThisServer; private transient byte[] body; + private boolean suspended; + private transient Stopwatch processTimer; protected RemotingCommand() { } @@ -93,7 +108,16 @@ public static RemotingCommand createRequestCommand(int code, CommandCustomHeader return cmd; } - private static void setCmdVersion(RemotingCommand cmd) { + public static RemotingCommand createResponseCommandWithHeader(int code, CommandCustomHeader customHeader) { + RemotingCommand cmd = new RemotingCommand(); + cmd.setCode(code); + cmd.markResponseType(); + cmd.customHeader = customHeader; + setCmdVersion(cmd); + return cmd; + } + + protected static void setCmdVersion(RemotingCommand cmd) { if (configVersion >= 0) { cmd.setVersion(configVersion); } else { @@ -110,6 +134,18 @@ public static RemotingCommand createResponseCommand(Class classHeader) { + final RemotingCommand response = RemotingCommand.createResponseCommand(classHeader); + response.setCode(code); + response.setRemark(remark); + return response; + } + + public static RemotingCommand buildErrorResponse(int code, String remark) { + return buildErrorResponse(code, remark, null); + } + public static RemotingCommand createResponseCommand(int code, String remark, Class classHeader) { RemotingCommand cmd = new RemotingCommand(); @@ -120,12 +156,16 @@ public static RemotingCommand createResponseCommand(int code, String remark, if (classHeader != null) { try { - CommandCustomHeader objectHeader = classHeader.newInstance(); + CommandCustomHeader objectHeader = classHeader.getDeclaredConstructor().newInstance(); cmd.customHeader = objectHeader; } catch (InstantiationException e) { return null; } catch (IllegalAccessException e) { return null; + } catch (InvocationTargetException e) { + return null; + } catch (NoSuchMethodException e) { + return null; } } @@ -136,26 +176,30 @@ public static RemotingCommand createResponseCommand(int code, String remark) { return createResponseCommand(code, remark, null); } - public static RemotingCommand decode(final byte[] array) { + public static RemotingCommand decode(final byte[] array) throws RemotingCommandException { ByteBuffer byteBuffer = ByteBuffer.wrap(array); return decode(byteBuffer); } - public static RemotingCommand decode(final ByteBuffer byteBuffer) { - int length = byteBuffer.limit(); - int oriHeaderLen = byteBuffer.getInt(); - int headerLength = getHeaderLength(oriHeaderLen); + public static RemotingCommand decode(final ByteBuffer byteBuffer) throws RemotingCommandException { + return decode(Unpooled.wrappedBuffer(byteBuffer)); + } - byte[] headerData = new byte[headerLength]; - byteBuffer.get(headerData); + public static RemotingCommand decode(final ByteBuf byteBuffer) throws RemotingCommandException { + int length = byteBuffer.readableBytes(); + int oriHeaderLen = byteBuffer.readInt(); + int headerLength = getHeaderLength(oriHeaderLen); + if (headerLength > length - 4) { + throw new RemotingCommandException("decode error, bad header length: " + headerLength); + } - RemotingCommand cmd = headerDecode(headerData, getProtocolType(oriHeaderLen)); + RemotingCommand cmd = headerDecode(byteBuffer, headerLength, getProtocolType(oriHeaderLen)); int bodyLength = length - 4 - headerLength; byte[] bodyData = null; if (bodyLength > 0) { bodyData = new byte[bodyLength]; - byteBuffer.get(bodyData); + byteBuffer.readBytes(bodyData); } cmd.body = bodyData; @@ -166,14 +210,17 @@ public static int getHeaderLength(int length) { return length & 0xFFFFFF; } - private static RemotingCommand headerDecode(byte[] headerData, SerializeType type) { + private static RemotingCommand headerDecode(ByteBuf byteBuffer, int len, + SerializeType type) throws RemotingCommandException { switch (type) { case JSON: + byte[] headerData = new byte[len]; + byteBuffer.readBytes(headerData); RemotingCommand resultJson = RemotingSerializable.decode(headerData, RemotingCommand.class); resultJson.setSerializeTypeCurrentRPC(type); return resultJson; case ROCKETMQ: - RemotingCommand resultRMQ = RocketMQSerializable.rocketMQProtocolDecode(headerData); + RemotingCommand resultRMQ = RocketMQSerializable.rocketMQProtocolDecode(byteBuffer, len); resultRMQ.setSerializeTypeCurrentRPC(type); return resultRMQ; default: @@ -188,34 +235,15 @@ public static SerializeType getProtocolType(int source) { } public static int createNewRequestId() { - return requestId.incrementAndGet(); + return requestId.getAndIncrement(); } public static SerializeType getSerializeTypeConfigInThisServer() { return serializeTypeConfigInThisServer; } - private static boolean isBlank(String str) { - int strLen; - if (str == null || (strLen = str.length()) == 0) { - return true; - } - for (int i = 0; i < strLen; i++) { - if (!Character.isWhitespace(str.charAt(i))) { - return false; - } - } - return true; - } - - public static byte[] markProtocolType(int source, SerializeType type) { - byte[] result = new byte[4]; - - result[0] = type.getCode(); - result[1] = (byte) ((source >> 16) & 0xFF); - result[2] = (byte) ((source >> 8) & 0xFF); - result[3] = (byte) (source & 0xFF); - return result; + public static int markProtocolType(int source, SerializeType type) { + return (type.getCode() << 24) | (source & 0x00FFFFFF); } public void markResponseType() { @@ -233,16 +261,39 @@ public void writeCustomHeader(CommandCustomHeader customHeader) { public CommandCustomHeader decodeCommandCustomHeader( Class classHeader) throws RemotingCommandException { + return decodeCommandCustomHeader(classHeader, false); + } + + public CommandCustomHeader decodeCommandCustomHeader( + Class classHeader, boolean isCached) throws RemotingCommandException { + if (isCached && cachedHeader != null) { + return cachedHeader; + } + cachedHeader = decodeCommandCustomHeaderDirectly(classHeader, true); + return cachedHeader; + } + + public CommandCustomHeader decodeCommandCustomHeaderDirectly(Class classHeader, + boolean useFastEncode) throws RemotingCommandException { CommandCustomHeader objectHeader; try { - objectHeader = classHeader.newInstance(); + objectHeader = classHeader.getDeclaredConstructor().newInstance(); } catch (InstantiationException e) { return null; } catch (IllegalAccessException e) { return null; + } catch (InvocationTargetException e) { + return null; + } catch (NoSuchMethodException e) { + return null; } if (this.extFields != null) { + if (objectHeader instanceof FastCodesHeader && useFastEncode) { + ((FastCodesHeader) objectHeader).decode(this.extFields); + objectHeader.checkFields(); + return objectHeader; + } Field[] fields = getClazzFields(classHeader); for (Field field : fields) { @@ -272,6 +323,8 @@ public CommandCustomHeader decodeCommandCustomHeader( valueParsed = Boolean.parseBoolean(value); } else if (type.equals(DOUBLE_CANONICAL_NAME_1) || type.equals(DOUBLE_CANONICAL_NAME_2)) { valueParsed = Double.parseDouble(value); + } else if (type.equals(BOUNDARY_TYPE_CANONICAL_NAME)) { + valueParsed = BoundaryType.getType(value); } else { throw new RemotingCommandException("the custom field <" + fieldName + "> type is not supported"); } @@ -291,11 +344,17 @@ public CommandCustomHeader decodeCommandCustomHeader( return objectHeader; } - private Field[] getClazzFields(Class classHeader) { + //make it able to test + Field[] getClazzFields(Class classHeader) { Field[] field = CLASS_HASH_MAP.get(classHeader); if (field == null) { - field = classHeader.getDeclaredFields(); + Set fieldList = new HashSet<>(); + for (Class className = classHeader; className != Object.class; className = className.getSuperclass()) { + Field[] fields = className.getDeclaredFields(); + fieldList.addAll(Arrays.asList(fields)); + } + field = fieldList.toArray(new Field[0]); synchronized (CLASS_HASH_MAP) { CLASS_HASH_MAP.put(classHeader, field); } @@ -344,7 +403,7 @@ public ByteBuffer encode() { result.putInt(length); // header length - result.put(markProtocolType(headerData.length, serializeTypeCurrentRPC)); + result.putInt(markProtocolType(headerData.length, serializeTypeCurrentRPC)); // header data result.put(headerData); @@ -372,7 +431,7 @@ public void makeCustomHeaderToNet() { if (this.customHeader != null) { Field[] fields = getClazzFields(customHeader.getClass()); if (null == this.extFields) { - this.extFields = new HashMap(); + this.extFields = new HashMap<>(); } for (Field field : fields) { @@ -396,6 +455,27 @@ public void makeCustomHeaderToNet() { } } + public void fastEncodeHeader(ByteBuf out) { + int bodySize = this.body != null ? this.body.length : 0; + int beginIndex = out.writerIndex(); + // skip 8 bytes + out.writeLong(0); + int headerSize; + if (SerializeType.ROCKETMQ == serializeTypeCurrentRPC) { + if (customHeader != null && !(customHeader instanceof FastCodesHeader)) { + this.makeCustomHeaderToNet(); + } + headerSize = RocketMQSerializable.rocketMQProtocolEncode(this, out); + } else { + this.makeCustomHeaderToNet(); + byte[] header = RemotingSerializable.encode(this); + headerSize = header.length; + out.writeBytes(header); + } + out.setInt(beginIndex, 4 + headerSize + bodySize); + out.setInt(beginIndex + 4, markProtocolType(headerSize, serializeTypeCurrentRPC)); + } + public ByteBuffer encodeHeader() { return encodeHeader(this.body != null ? this.body.length : 0); } @@ -419,12 +499,12 @@ public ByteBuffer encodeHeader(final int bodyLength) { result.putInt(length); // header length - result.put(markProtocolType(headerData.length, serializeTypeCurrentRPC)); + result.putInt(markProtocolType(headerData.length, serializeTypeCurrentRPC)); // header data result.put(headerData); - result.flip(); + ((Buffer) result).flip(); return result; } @@ -511,6 +591,16 @@ public void setBody(byte[] body) { this.body = body; } + @JSONField(serialize = false) + public boolean isSuspended() { + return suspended; + } + + @JSONField(serialize = false) + public void setSuspended(boolean suspended) { + this.suspended = suspended; + } + public HashMap getExtFields() { return extFields; } @@ -521,11 +611,15 @@ public void setExtFields(HashMap extFields) { public void addExtField(String key, String value) { if (null == extFields) { - extFields = new HashMap(); + extFields = new HashMap<>(256); } extFields.put(key, value); } + public void addExtFieldIfNotExist(String key, String value) { + extFields.putIfAbsent(key, value); + } + @Override public String toString() { return "RemotingCommand [code=" + code + ", language=" + language + ", version=" + version + ", opaque=" + opaque + ", flag(B)=" @@ -540,4 +634,12 @@ public SerializeType getSerializeTypeCurrentRPC() { public void setSerializeTypeCurrentRPC(SerializeType serializeTypeCurrentRPC) { this.serializeTypeCurrentRPC = serializeTypeCurrentRPC; } -} \ No newline at end of file + + public Stopwatch getProcessTimer() { + return processTimer; + } + + public void setProcessTimer(Stopwatch processTimer) { + this.processTimer = processTimer; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RemotingSerializable.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RemotingSerializable.java index f80ff14c107..60cc4e3e227 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RemotingSerializable.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RemotingSerializable.java @@ -17,17 +17,20 @@ package org.apache.rocketmq.remoting.protocol; import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.serializer.SerializerFeature; + import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; public abstract class RemotingSerializable { - private final static Charset CHARSET_UTF8 = Charset.forName("UTF-8"); + private final static Charset CHARSET_UTF8 = StandardCharsets.UTF_8; public static byte[] encode(final Object obj) { - final String json = toJson(obj, false); - if (json != null) { - return json.getBytes(CHARSET_UTF8); + if (obj == null) { + return null; } - return null; + final String json = toJson(obj, false); + return json.getBytes(CHARSET_UTF8); } public static String toJson(final Object obj, boolean prettyFormat) { @@ -35,14 +38,17 @@ public static String toJson(final Object obj, boolean prettyFormat) { } public static T decode(final byte[] data, Class classOfT) { - final String json = new String(data, CHARSET_UTF8); - return fromJson(json, classOfT); + return fromJson(data, classOfT); } public static T fromJson(String json, Class classOfT) { return JSON.parseObject(json, classOfT); } + private static T fromJson(byte[] data, Class classOfT) { + return JSON.parseObject(data, classOfT); + } + public byte[] encode() { final String json = this.toJson(); if (json != null) { @@ -51,6 +57,17 @@ public byte[] encode() { return null; } + /** + * Allow call-site to apply specific features according to their requirements. + * + * @param features Features to apply + * @return serialized data. + */ + public byte[] encode(SerializerFeature...features) { + final String json = JSON.toJSONString(this, features); + return json.getBytes(CHARSET_UTF8); + } + public String toJson() { return toJson(false); } diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RequestCode.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RequestCode.java new file mode 100644 index 00000000000..00d14acd223 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RequestCode.java @@ -0,0 +1,292 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol; + +public class RequestCode { + + public static final int SEND_MESSAGE = 10; + + public static final int PULL_MESSAGE = 11; + + public static final int QUERY_MESSAGE = 12; + public static final int QUERY_BROKER_OFFSET = 13; + public static final int QUERY_CONSUMER_OFFSET = 14; + public static final int UPDATE_CONSUMER_OFFSET = 15; + public static final int UPDATE_AND_CREATE_TOPIC = 17; + public static final int GET_ALL_TOPIC_CONFIG = 21; + public static final int GET_TOPIC_CONFIG_LIST = 22; + + public static final int GET_TOPIC_NAME_LIST = 23; + + public static final int UPDATE_BROKER_CONFIG = 25; + + public static final int GET_BROKER_CONFIG = 26; + + public static final int TRIGGER_DELETE_FILES = 27; + + public static final int GET_BROKER_RUNTIME_INFO = 28; + public static final int SEARCH_OFFSET_BY_TIMESTAMP = 29; + public static final int GET_MAX_OFFSET = 30; + public static final int GET_MIN_OFFSET = 31; + + public static final int GET_EARLIEST_MSG_STORETIME = 32; + + public static final int VIEW_MESSAGE_BY_ID = 33; + + public static final int HEART_BEAT = 34; + + public static final int UNREGISTER_CLIENT = 35; + + public static final int CONSUMER_SEND_MSG_BACK = 36; + + public static final int END_TRANSACTION = 37; + public static final int GET_CONSUMER_LIST_BY_GROUP = 38; + + public static final int CHECK_TRANSACTION_STATE = 39; + + public static final int NOTIFY_CONSUMER_IDS_CHANGED = 40; + + public static final int LOCK_BATCH_MQ = 41; + + public static final int UNLOCK_BATCH_MQ = 42; + public static final int GET_ALL_CONSUMER_OFFSET = 43; + + public static final int GET_ALL_DELAY_OFFSET = 45; + + public static final int CHECK_CLIENT_CONFIG = 46; + + public static final int GET_CLIENT_CONFIG = 47; + + public static final int UPDATE_AND_CREATE_ACL_CONFIG = 50; + + public static final int DELETE_ACL_CONFIG = 51; + + public static final int GET_BROKER_CLUSTER_ACL_INFO = 52; + + public static final int UPDATE_GLOBAL_WHITE_ADDRS_CONFIG = 53; + + @Deprecated + public static final int GET_BROKER_CLUSTER_ACL_CONFIG = 54; + + public static final int GET_TIMER_CHECK_POINT = 60; + + public static final int GET_TIMER_METRICS = 61; + + public static final int POP_MESSAGE = 200050; + public static final int ACK_MESSAGE = 200051; + public static final int BATCH_ACK_MESSAGE = 200151; + public static final int PEEK_MESSAGE = 200052; + public static final int CHANGE_MESSAGE_INVISIBLETIME = 200053; + public static final int NOTIFICATION = 200054; + public static final int POLLING_INFO = 200055; + + public static final int PUT_KV_CONFIG = 100; + + public static final int GET_KV_CONFIG = 101; + + public static final int DELETE_KV_CONFIG = 102; + + public static final int REGISTER_BROKER = 103; + + public static final int UNREGISTER_BROKER = 104; + public static final int GET_ROUTEINFO_BY_TOPIC = 105; + + public static final int GET_BROKER_CLUSTER_INFO = 106; + public static final int UPDATE_AND_CREATE_SUBSCRIPTIONGROUP = 200; + public static final int GET_ALL_SUBSCRIPTIONGROUP_CONFIG = 201; + public static final int GET_TOPIC_STATS_INFO = 202; + public static final int GET_CONSUMER_CONNECTION_LIST = 203; + public static final int GET_PRODUCER_CONNECTION_LIST = 204; + public static final int WIPE_WRITE_PERM_OF_BROKER = 205; + + public static final int GET_ALL_TOPIC_LIST_FROM_NAMESERVER = 206; + + public static final int DELETE_SUBSCRIPTIONGROUP = 207; + public static final int GET_CONSUME_STATS = 208; + + public static final int SUSPEND_CONSUMER = 209; + + public static final int RESUME_CONSUMER = 210; + public static final int RESET_CONSUMER_OFFSET_IN_CONSUMER = 211; + public static final int RESET_CONSUMER_OFFSET_IN_BROKER = 212; + + public static final int ADJUST_CONSUMER_THREAD_POOL = 213; + + public static final int WHO_CONSUME_THE_MESSAGE = 214; + + public static final int DELETE_TOPIC_IN_BROKER = 215; + + public static final int DELETE_TOPIC_IN_NAMESRV = 216; + public static final int REGISTER_TOPIC_IN_NAMESRV = 217; + public static final int GET_KVLIST_BY_NAMESPACE = 219; + + public static final int RESET_CONSUMER_CLIENT_OFFSET = 220; + + public static final int GET_CONSUMER_STATUS_FROM_CLIENT = 221; + + public static final int INVOKE_BROKER_TO_RESET_OFFSET = 222; + + public static final int INVOKE_BROKER_TO_GET_CONSUMER_STATUS = 223; + + public static final int QUERY_TOPIC_CONSUME_BY_WHO = 300; + + public static final int GET_TOPICS_BY_CLUSTER = 224; + + public static final int QUERY_TOPICS_BY_CONSUMER = 343; + public static final int QUERY_SUBSCRIPTION_BY_CONSUMER = 345; + + public static final int REGISTER_FILTER_SERVER = 301; + public static final int REGISTER_MESSAGE_FILTER_CLASS = 302; + + public static final int QUERY_CONSUME_TIME_SPAN = 303; + + public static final int GET_SYSTEM_TOPIC_LIST_FROM_NS = 304; + public static final int GET_SYSTEM_TOPIC_LIST_FROM_BROKER = 305; + + public static final int CLEAN_EXPIRED_CONSUMEQUEUE = 306; + + public static final int GET_CONSUMER_RUNNING_INFO = 307; + + public static final int QUERY_CORRECTION_OFFSET = 308; + public static final int CONSUME_MESSAGE_DIRECTLY = 309; + + public static final int SEND_MESSAGE_V2 = 310; + + public static final int GET_UNIT_TOPIC_LIST = 311; + + public static final int GET_HAS_UNIT_SUB_TOPIC_LIST = 312; + + public static final int GET_HAS_UNIT_SUB_UNUNIT_TOPIC_LIST = 313; + + public static final int CLONE_GROUP_OFFSET = 314; + + public static final int VIEW_BROKER_STATS_DATA = 315; + + public static final int CLEAN_UNUSED_TOPIC = 316; + + public static final int GET_BROKER_CONSUME_STATS = 317; + + /** + * update the config of name server + */ + public static final int UPDATE_NAMESRV_CONFIG = 318; + + /** + * get config from name server + */ + public static final int GET_NAMESRV_CONFIG = 319; + + public static final int SEND_BATCH_MESSAGE = 320; + + public static final int QUERY_CONSUME_QUEUE = 321; + + public static final int QUERY_DATA_VERSION = 322; + + /** + * resume logic of checking half messages that have been put in TRANS_CHECK_MAXTIME_TOPIC before + */ + public static final int RESUME_CHECK_HALF_MESSAGE = 323; + + public static final int SEND_REPLY_MESSAGE = 324; + + public static final int SEND_REPLY_MESSAGE_V2 = 325; + + public static final int PUSH_REPLY_MESSAGE_TO_CLIENT = 326; + + public static final int ADD_WRITE_PERM_OF_BROKER = 327; + + public static final int GET_TOPIC_CONFIG = 351; + + public static final int GET_SUBSCRIPTIONGROUP_CONFIG = 352; + public static final int UPDATE_AND_GET_GROUP_FORBIDDEN = 353; + + public static final int LITE_PULL_MESSAGE = 361; + + public static final int QUERY_ASSIGNMENT = 400; + public static final int SET_MESSAGE_REQUEST_MODE = 401; + public static final int GET_ALL_MESSAGE_REQUEST_MODE = 402; + + public static final int UPDATE_AND_CREATE_STATIC_TOPIC = 513; + + public static final int GET_BROKER_MEMBER_GROUP = 901; + + public static final int ADD_BROKER = 902; + + public static final int REMOVE_BROKER = 903; + + public static final int BROKER_HEARTBEAT = 904; + + public static final int NOTIFY_MIN_BROKER_ID_CHANGE = 905; + + public static final int EXCHANGE_BROKER_HA_INFO = 906; + + public static final int GET_BROKER_HA_STATUS = 907; + + public static final int RESET_MASTER_FLUSH_OFFSET = 908; + + public static final int GET_ALL_PRODUCER_INFO = 328; + + public static final int DELETE_EXPIRED_COMMITLOG = 329; + + /** + * Controller code + */ + public static final int CONTROLLER_ALTER_SYNC_STATE_SET = 1001; + + public static final int CONTROLLER_ELECT_MASTER = 1002; + + public static final int CONTROLLER_REGISTER_BROKER = 1003; + + public static final int CONTROLLER_GET_REPLICA_INFO = 1004; + + public static final int CONTROLLER_GET_METADATA_INFO = 1005; + + public static final int CONTROLLER_GET_SYNC_STATE_DATA = 1006; + + public static final int GET_BROKER_EPOCH_CACHE = 1007; + + public static final int NOTIFY_BROKER_ROLE_CHANGED = 1008; + + /** + * update the config of controller + */ + public static final int UPDATE_CONTROLLER_CONFIG = 1009; + + /** + * get config from controller + */ + public static final int GET_CONTROLLER_CONFIG = 1010; + + /** + * clean broker data + */ + public static final int CLEAN_BROKER_DATA = 1011; + public static final int CONTROLLER_GET_NEXT_BROKER_ID = 1012; + + public static final int CONTROLLER_APPLY_BROKER_ID = 1013; + public static final short BROKER_CLOSE_CHANNEL_REQUEST = 1014; + public static final short CHECK_NOT_ACTIVE_BROKER_REQUEST = 1015; + public static final short GET_BROKER_LIVE_INFO_REQUEST = 1016; + public static final short GET_SYNC_STATE_DATA_REQUEST = 1017; + public static final short RAFT_BROKER_HEART_BEAT_EVENT_REQUEST = 1018; + + public static final int UPDATE_COLD_DATA_FLOW_CTR_CONFIG = 2001; + public static final int REMOVE_COLD_DATA_FLOW_CTR_CONFIG = 2002; + public static final int GET_COLD_DATA_FLOW_CTR_INFO = 2003; + public static final int SET_COMMITLOG_READ_MODE = 2004; +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RequestSource.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RequestSource.java new file mode 100644 index 00000000000..5d811601323 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RequestSource.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol; + +public enum RequestSource { + + SDK(-1), + PROXY_FOR_ORDER(0), + PROXY_FOR_BROADCAST(1), + PROXY_FOR_STREAM(2); + + public static final String SYSTEM_PROPERTY_KEY = "rocketmq.requestSource"; + private final int value; + + RequestSource(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static boolean isValid(Integer value) { + return null != value && value >= -1 && value < RequestSource.values().length - 1; + } + + public static RequestSource parseInteger(Integer value) { + if (isValid(value)) { + return RequestSource.values()[value + 1]; + } + return SDK; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RequestType.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RequestType.java new file mode 100644 index 00000000000..65217d5b8de --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RequestType.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol; + +public enum RequestType { + STREAM((byte) 0); + + private final byte code; + + RequestType(byte code) { + this.code = code; + } + + public static RequestType valueOf(byte code) { + for (RequestType requestType : RequestType.values()) { + if (requestType.getCode() == code) { + return requestType; + } + } + return null; + } + + public byte getCode() { + return code; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/ResponseCode.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/ResponseCode.java new file mode 100644 index 00000000000..0e595fabc55 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/ResponseCode.java @@ -0,0 +1,133 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol; + +public class ResponseCode extends RemotingSysResponseCode { + + public static final int FLUSH_DISK_TIMEOUT = 10; + + public static final int SLAVE_NOT_AVAILABLE = 11; + + public static final int FLUSH_SLAVE_TIMEOUT = 12; + + public static final int MESSAGE_ILLEGAL = 13; + + public static final int SERVICE_NOT_AVAILABLE = 14; + + public static final int VERSION_NOT_SUPPORTED = 15; + + public static final int NO_PERMISSION = 16; + + public static final int TOPIC_NOT_EXIST = 17; + public static final int TOPIC_EXIST_ALREADY = 18; + public static final int PULL_NOT_FOUND = 19; + + public static final int PULL_RETRY_IMMEDIATELY = 20; + + public static final int PULL_OFFSET_MOVED = 21; + + public static final int QUERY_NOT_FOUND = 22; + + public static final int SUBSCRIPTION_PARSE_FAILED = 23; + + public static final int SUBSCRIPTION_NOT_EXIST = 24; + + public static final int SUBSCRIPTION_NOT_LATEST = 25; + + public static final int SUBSCRIPTION_GROUP_NOT_EXIST = 26; + + public static final int FILTER_DATA_NOT_EXIST = 27; + + public static final int FILTER_DATA_NOT_LATEST = 28; + + public static final int TRANSACTION_SHOULD_COMMIT = 200; + + public static final int TRANSACTION_SHOULD_ROLLBACK = 201; + + public static final int TRANSACTION_STATE_UNKNOW = 202; + + public static final int TRANSACTION_STATE_GROUP_WRONG = 203; + public static final int NO_BUYER_ID = 204; + + public static final int NOT_IN_CURRENT_UNIT = 205; + + public static final int CONSUMER_NOT_ONLINE = 206; + + public static final int CONSUME_MSG_TIMEOUT = 207; + + public static final int NO_MESSAGE = 208; + + public static final int UPDATE_AND_CREATE_ACL_CONFIG_FAILED = 209; + + public static final int DELETE_ACL_CONFIG_FAILED = 210; + + public static final int UPDATE_GLOBAL_WHITE_ADDRS_CONFIG_FAILED = 211; + + public static final int POLLING_FULL = 209; + + public static final int POLLING_TIMEOUT = 210; + + public static final int BROKER_NOT_EXIST = 211; + + public static final int BROKER_DISPATCH_NOT_COMPLETE = 212; + + public static final int BROADCAST_CONSUMPTION = 213; + + public static final int FLOW_CONTROL = 215; + + public static final int NOT_LEADER_FOR_QUEUE = 501; + + public static final int ILLEGAL_OPERATION = 604; + + public static final int RPC_UNKNOWN = -1000; + public static final int RPC_ADDR_IS_NULL = -1002; + public static final int RPC_SEND_TO_CHANNEL_FAILED = -1004; + public static final int RPC_TIME_OUT = -1006; + + public static final int GO_AWAY = 1500; + + /** + * Controller response code + */ + public static final int CONTROLLER_FENCED_MASTER_EPOCH = 2000; + public static final int CONTROLLER_FENCED_SYNC_STATE_SET_EPOCH = 2001; + public static final int CONTROLLER_INVALID_MASTER = 2002; + public static final int CONTROLLER_INVALID_REPLICAS = 2003; + public static final int CONTROLLER_MASTER_NOT_AVAILABLE = 2004; + public static final int CONTROLLER_INVALID_REQUEST = 2005; + public static final int CONTROLLER_BROKER_NOT_ALIVE = 2006; + public static final int CONTROLLER_NOT_LEADER = 2007; + + public static final int CONTROLLER_BROKER_METADATA_NOT_EXIST = 2008; + + public static final int CONTROLLER_INVALID_CLEAN_BROKER_METADATA = 2009; + + public static final int CONTROLLER_BROKER_NEED_TO_BE_REGISTERED = 2010; + + public static final int CONTROLLER_MASTER_STILL_EXIST = 2011; + + public static final int CONTROLLER_ELECT_MASTER_FAILED = 2012; + + public static final int CONTROLLER_ALTER_SYNC_STATE_SET_FAILED = 2013; + + public static final int CONTROLLER_BROKER_ID_INVALID = 2014; + + public static final int CONTROLLER_JRAFT_INTERNAL_ERROR = 2015; + + public static final int CONTROLLER_BROKER_LIVE_INFO_NOT_EXISTS = 2016; +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RocketMQSerializable.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RocketMQSerializable.java index 66119e0eacd..25ebbaafd94 100644 --- a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RocketMQSerializable.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RocketMQSerializable.java @@ -18,12 +18,81 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +import io.netty.buffer.ByteBuf; public class RocketMQSerializable { - private static final Charset CHARSET_UTF8 = Charset.forName("UTF-8"); + private static final Charset CHARSET_UTF8 = StandardCharsets.UTF_8; + + public static void writeStr(ByteBuf buf, boolean useShortLength, String str) { + int lenIndex = buf.writerIndex(); + if (useShortLength) { + buf.writeShort(0); + } else { + buf.writeInt(0); + } + int len = buf.writeCharSequence(str, StandardCharsets.UTF_8); + if (useShortLength) { + buf.setShort(lenIndex, len); + } else { + buf.setInt(lenIndex, len); + } + } + + private static String readStr(ByteBuf buf, boolean useShortLength, int limit) throws RemotingCommandException { + int len = useShortLength ? buf.readShort() : buf.readInt(); + if (len == 0) { + return null; + } + if (len > limit) { + throw new RemotingCommandException("string length exceed limit:" + limit); + } + CharSequence cs = buf.readCharSequence(len, StandardCharsets.UTF_8); + return cs == null ? null : cs.toString(); + } + + public static int rocketMQProtocolEncode(RemotingCommand cmd, ByteBuf out) { + int beginIndex = out.writerIndex(); + // int code(~32767) + out.writeShort(cmd.getCode()); + // LanguageCode language + out.writeByte(cmd.getLanguage().getCode()); + // int version(~32767) + out.writeShort(cmd.getVersion()); + // int opaque + out.writeInt(cmd.getOpaque()); + // int flag + out.writeInt(cmd.getFlag()); + // String remark + String remark = cmd.getRemark(); + if (remark != null && !remark.isEmpty()) { + writeStr(out, false, remark); + } else { + out.writeInt(0); + } + + int mapLenIndex = out.writerIndex(); + out.writeInt(0); + if (cmd.readCustomHeader() instanceof FastCodesHeader) { + ((FastCodesHeader) cmd.readCustomHeader()).encode(out); + } + HashMap map = cmd.getExtFields(); + if (map != null && !map.isEmpty()) { + map.forEach((k, v) -> { + if (k != null && v != null) { + writeStr(out, true, k); + writeStr(out, false, v); + } + }); + } + out.setInt(mapLenIndex, out.writerIndex() - mapLenIndex - 4); + return out.writerIndex() - beginIndex; + } public static byte[] rocketMQProtocolEncode(RemotingCommand cmd) { // String remark @@ -133,72 +202,43 @@ private static int calTotalLen(int remark, int ext) { return length; } - public static RemotingCommand rocketMQProtocolDecode(final byte[] headerArray) { + public static RemotingCommand rocketMQProtocolDecode(final ByteBuf headerBuffer, + int headerLen) throws RemotingCommandException { RemotingCommand cmd = new RemotingCommand(); - ByteBuffer headerBuffer = ByteBuffer.wrap(headerArray); // int code(~32767) - cmd.setCode(headerBuffer.getShort()); + cmd.setCode(headerBuffer.readShort()); // LanguageCode language - cmd.setLanguage(LanguageCode.valueOf(headerBuffer.get())); + cmd.setLanguage(LanguageCode.valueOf(headerBuffer.readByte())); // int version(~32767) - cmd.setVersion(headerBuffer.getShort()); + cmd.setVersion(headerBuffer.readShort()); // int opaque - cmd.setOpaque(headerBuffer.getInt()); + cmd.setOpaque(headerBuffer.readInt()); // int flag - cmd.setFlag(headerBuffer.getInt()); + cmd.setFlag(headerBuffer.readInt()); // String remark - int remarkLength = headerBuffer.getInt(); - if (remarkLength > 0) { - byte[] remarkContent = new byte[remarkLength]; - headerBuffer.get(remarkContent); - cmd.setRemark(new String(remarkContent, CHARSET_UTF8)); - } + cmd.setRemark(readStr(headerBuffer, false, headerLen)); // HashMap extFields - int extFieldsLength = headerBuffer.getInt(); + int extFieldsLength = headerBuffer.readInt(); if (extFieldsLength > 0) { - byte[] extFieldsBytes = new byte[extFieldsLength]; - headerBuffer.get(extFieldsBytes); - cmd.setExtFields(mapDeserialize(extFieldsBytes)); + if (extFieldsLength > headerLen) { + throw new RemotingCommandException("RocketMQ protocol decoding failed, extFields length: " + extFieldsLength + ", but header length: " + headerLen); + } + cmd.setExtFields(mapDeserialize(headerBuffer, extFieldsLength)); } return cmd; } - public static HashMap mapDeserialize(byte[] bytes) { - if (bytes == null || bytes.length <= 0) - return null; - - HashMap map = new HashMap(); - ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + public static HashMap mapDeserialize(ByteBuf byteBuffer, int len) throws RemotingCommandException { - short keySize; - byte[] keyContent; - int valSize; - byte[] valContent; - while (byteBuffer.hasRemaining()) { - keySize = byteBuffer.getShort(); - keyContent = new byte[keySize]; - byteBuffer.get(keyContent); + HashMap map = new HashMap<>(128); + int endIndex = byteBuffer.readerIndex() + len; - valSize = byteBuffer.getInt(); - valContent = new byte[valSize]; - byteBuffer.get(valContent); - - map.put(new String(keyContent, CHARSET_UTF8), new String(valContent, CHARSET_UTF8)); + while (byteBuffer.readerIndex() < endIndex) { + String k = readStr(byteBuffer, true, len); + String v = readStr(byteBuffer, false, len); + map.put(k, v); } return map; } - - public static boolean isBlank(String str) { - int strLen; - if (str == null || (strLen = str.length()) == 0) { - return true; - } - for (int i = 0; i < strLen; i++) { - if (!Character.isWhitespace(str.charAt(i))) { - return false; - } - } - return true; - } } diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/ConsumeStats.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/ConsumeStats.java new file mode 100644 index 00000000000..1ddbfe9300b --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/ConsumeStats.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.admin; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class ConsumeStats extends RemotingSerializable { + private Map offsetTable = new ConcurrentHashMap<>(); + private double consumeTps = 0; + + public long computeTotalDiff() { + long diffTotal = 0L; + for (Entry entry : this.offsetTable.entrySet()) { + diffTotal += entry.getValue().getBrokerOffset() - entry.getValue().getConsumerOffset(); + } + return diffTotal; + } + + public long computeInflightTotalDiff() { + long diffTotal = 0L; + for (Entry entry : this.offsetTable.entrySet()) { + diffTotal += entry.getValue().getPullOffset() - entry.getValue().getConsumerOffset(); + } + return diffTotal; + } + + public Map getOffsetTable() { + return offsetTable; + } + + public void setOffsetTable(Map offsetTable) { + this.offsetTable = offsetTable; + } + + public double getConsumeTps() { + return consumeTps; + } + + public void setConsumeTps(double consumeTps) { + this.consumeTps = consumeTps; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/admin/OffsetWrapper.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/OffsetWrapper.java similarity index 85% rename from common/src/main/java/org/apache/rocketmq/common/admin/OffsetWrapper.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/OffsetWrapper.java index a246da82f3f..a6153617fd4 100644 --- a/common/src/main/java/org/apache/rocketmq/common/admin/OffsetWrapper.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/OffsetWrapper.java @@ -14,12 +14,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.rocketmq.common.admin; +package org.apache.rocketmq.remoting.protocol.admin; public class OffsetWrapper { private long brokerOffset; private long consumerOffset; - + private long pullOffset; private long lastTimestamp; public long getBrokerOffset() { @@ -38,6 +38,14 @@ public void setConsumerOffset(long consumerOffset) { this.consumerOffset = consumerOffset; } + public long getPullOffset() { + return pullOffset; + } + + public void setPullOffset(long pullOffset) { + this.pullOffset = pullOffset; + } + public long getLastTimestamp() { return lastTimestamp; } diff --git a/common/src/main/java/org/apache/rocketmq/common/admin/RollbackStats.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/RollbackStats.java similarity index 97% rename from common/src/main/java/org/apache/rocketmq/common/admin/RollbackStats.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/RollbackStats.java index 4b9676e0331..467520749cb 100644 --- a/common/src/main/java/org/apache/rocketmq/common/admin/RollbackStats.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/RollbackStats.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.admin; +package org.apache.rocketmq.remoting.protocol.admin; public class RollbackStats { private String brokerName; diff --git a/common/src/main/java/org/apache/rocketmq/common/admin/TopicOffset.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/TopicOffset.java similarity index 82% rename from common/src/main/java/org/apache/rocketmq/common/admin/TopicOffset.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/TopicOffset.java index 7e667491836..be7eeeb40ae 100644 --- a/common/src/main/java/org/apache/rocketmq/common/admin/TopicOffset.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/TopicOffset.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.rocketmq.common.admin; +package org.apache.rocketmq.remoting.protocol.admin; public class TopicOffset { private long minOffset; @@ -44,4 +44,13 @@ public long getLastUpdateTimestamp() { public void setLastUpdateTimestamp(long lastUpdateTimestamp) { this.lastUpdateTimestamp = lastUpdateTimestamp; } + + @Override + public String toString() { + return "TopicOffset{" + + "minOffset=" + minOffset + + ", maxOffset=" + maxOffset + + ", lastUpdateTimestamp=" + lastUpdateTimestamp + + '}'; + } } diff --git a/common/src/main/java/org/apache/rocketmq/common/admin/TopicStatsTable.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/TopicStatsTable.java similarity index 75% rename from common/src/main/java/org/apache/rocketmq/common/admin/TopicStatsTable.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/TopicStatsTable.java index 729075c061a..9f467e7449e 100644 --- a/common/src/main/java/org/apache/rocketmq/common/admin/TopicStatsTable.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/admin/TopicStatsTable.java @@ -14,20 +14,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.rocketmq.common.admin; +package org.apache.rocketmq.remoting.protocol.admin; -import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.apache.rocketmq.common.message.MessageQueue; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; public class TopicStatsTable extends RemotingSerializable { - private HashMap offsetTable = new HashMap(); + private Map offsetTable = new ConcurrentHashMap<>(); - public HashMap getOffsetTable() { + public Map getOffsetTable() { return offsetTable; } - public void setOffsetTable(HashMap offsetTable) { + public void setOffsetTable(Map offsetTable) { this.offsetTable = offsetTable; } } diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/BatchAck.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/BatchAck.java new file mode 100644 index 00000000000..82dcd8567ea --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/BatchAck.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.body; + +import com.alibaba.fastjson.annotation.JSONField; +import org.apache.rocketmq.remoting.protocol.BitSetSerializerDeserializer; + +import java.io.Serializable; +import java.util.BitSet; + +public class BatchAck implements Serializable { + @JSONField(name = "c", alternateNames = {"consumerGroup"}) + private String consumerGroup; + @JSONField(name = "t", alternateNames = {"topic"}) + private String topic; + @JSONField(name = "r", alternateNames = {"retry"}) + private String retry; // "1" if is retry topic + @JSONField(name = "so", alternateNames = {"startOffset"}) + private long startOffset; + @JSONField(name = "q", alternateNames = {"queueId"}) + private int queueId; + @JSONField(name = "rq", alternateNames = {"reviveQueueId"}) + private int reviveQueueId; + @JSONField(name = "pt", alternateNames = {"popTime"}) + private long popTime; + @JSONField(name = "it", alternateNames = {"invisibleTime"}) + private long invisibleTime; + @JSONField(name = "b", alternateNames = {"bitSet"}, serializeUsing = BitSetSerializerDeserializer.class, deserializeUsing = BitSetSerializerDeserializer.class) + private BitSet bitSet; // ack offsets bitSet + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getRetry() { + return retry; + } + + public void setRetry(String retry) { + this.retry = retry; + } + + public long getStartOffset() { + return startOffset; + } + + public void setStartOffset(long startOffset) { + this.startOffset = startOffset; + } + + public int getQueueId() { + return queueId; + } + + public void setQueueId(int queueId) { + this.queueId = queueId; + } + + public int getReviveQueueId() { + return reviveQueueId; + } + + public void setReviveQueueId(int reviveQueueId) { + this.reviveQueueId = reviveQueueId; + } + + public long getPopTime() { + return popTime; + } + + public void setPopTime(long popTime) { + this.popTime = popTime; + } + + public long getInvisibleTime() { + return invisibleTime; + } + + public void setInvisibleTime(long invisibleTime) { + this.invisibleTime = invisibleTime; + } + + public BitSet getBitSet() { + return bitSet; + } + + public void setBitSet(BitSet bitSet) { + this.bitSet = bitSet; + } + + @Override + public String toString() { + return "BatchAck{" + + "consumerGroup='" + consumerGroup + '\'' + + ", topic='" + topic + '\'' + + ", retry='" + retry + '\'' + + ", startOffset=" + startOffset + + ", queueId=" + queueId + + ", reviveQueueId=" + reviveQueueId + + ", popTime=" + popTime + + ", invisibleTime=" + invisibleTime + + ", bitSet=" + bitSet + + '}'; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/BatchAckMessageRequestBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/BatchAckMessageRequestBody.java new file mode 100644 index 00000000000..f0e1a8c3c8d --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/BatchAckMessageRequestBody.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +import java.util.List; + +public class BatchAckMessageRequestBody extends RemotingSerializable { + private String brokerName; + private List acks; + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + public List getAcks() { + return acks; + } + + public void setAcks(List acks) { + this.acks = acks; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/BrokerMemberGroup.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/BrokerMemberGroup.java new file mode 100644 index 00000000000..32497fadc78 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/BrokerMemberGroup.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import com.google.common.base.Objects; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class BrokerMemberGroup extends RemotingSerializable { + private String cluster; + private String brokerName; + private Map brokerAddrs; + + // Provide default constructor for serializer + public BrokerMemberGroup() { + this.brokerAddrs = new HashMap<>(); + } + + public BrokerMemberGroup(final String cluster, final String brokerName) { + this.cluster = cluster; + this.brokerName = brokerName; + this.brokerAddrs = new HashMap<>(); + } + + public long minimumBrokerId() { + if (this.brokerAddrs.isEmpty()) { + return 0; + } + return Collections.min(brokerAddrs.keySet()); + } + + public String getCluster() { + return cluster; + } + + public void setCluster(final String cluster) { + this.cluster = cluster; + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(final String brokerName) { + this.brokerName = brokerName; + } + + public Map getBrokerAddrs() { + return brokerAddrs; + } + + public void setBrokerAddrs(final Map brokerAddrs) { + this.brokerAddrs = brokerAddrs; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BrokerMemberGroup that = (BrokerMemberGroup) o; + return Objects.equal(cluster, that.cluster) && + Objects.equal(brokerName, that.brokerName) && + Objects.equal(brokerAddrs, that.brokerAddrs); + } + + @Override + public int hashCode() { + return Objects.hashCode(cluster, brokerName, brokerAddrs); + } + + @Override + public String toString() { + return "BrokerMemberGroup{" + + "cluster='" + cluster + '\'' + + ", brokerName='" + brokerName + '\'' + + ", brokerAddrs=" + brokerAddrs + + '}'; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/BrokerReplicasInfo.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/BrokerReplicasInfo.java new file mode 100644 index 00000000000..a2960165ed7 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/BrokerReplicasInfo.java @@ -0,0 +1,205 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.body; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class BrokerReplicasInfo extends RemotingSerializable { + private Map replicasInfoTable; + + public BrokerReplicasInfo() { + this.replicasInfoTable = new HashMap<>(); + } + + public void addReplicaInfo(final String brokerName, final ReplicasInfo replicasInfo) { + this.replicasInfoTable.put(brokerName, replicasInfo); + } + + public Map getReplicasInfoTable() { + return replicasInfoTable; + } + + public void setReplicasInfoTable( + Map replicasInfoTable) { + this.replicasInfoTable = replicasInfoTable; + } + + public static class ReplicasInfo extends RemotingSerializable { + + private Long masterBrokerId; + + private String masterAddress; + private Integer masterEpoch; + private Integer syncStateSetEpoch; + private List inSyncReplicas; + private List notInSyncReplicas; + + public ReplicasInfo(Long masterBrokerId, String masterAddress, int masterEpoch, int syncStateSetEpoch, + List inSyncReplicas, List notInSyncReplicas) { + this.masterBrokerId = masterBrokerId; + this.masterAddress = masterAddress; + this.masterEpoch = masterEpoch; + this.syncStateSetEpoch = syncStateSetEpoch; + this.inSyncReplicas = inSyncReplicas; + this.notInSyncReplicas = notInSyncReplicas; + } + + public String getMasterAddress() { + return masterAddress; + } + + public void setMasterAddress(String masterAddress) { + this.masterAddress = masterAddress; + } + + public int getMasterEpoch() { + return masterEpoch; + } + + public void setMasterEpoch(int masterEpoch) { + this.masterEpoch = masterEpoch; + } + + public int getSyncStateSetEpoch() { + return syncStateSetEpoch; + } + + public void setSyncStateSetEpoch(int syncStateSetEpoch) { + this.syncStateSetEpoch = syncStateSetEpoch; + } + + public List getInSyncReplicas() { + return inSyncReplicas; + } + + public void setInSyncReplicas( + List inSyncReplicas) { + this.inSyncReplicas = inSyncReplicas; + } + + public List getNotInSyncReplicas() { + return notInSyncReplicas; + } + + public void setNotInSyncReplicas( + List notInSyncReplicas) { + this.notInSyncReplicas = notInSyncReplicas; + } + + public void setMasterBrokerId(Long masterBrokerId) { + this.masterBrokerId = masterBrokerId; + } + + public Long getMasterBrokerId() { + return masterBrokerId; + } + + public boolean isExistInSync(String brokerName, Long brokerId, String brokerAddress) { + return this.getInSyncReplicas().contains(new ReplicaIdentity(brokerName, brokerId, brokerAddress)); + } + + public boolean isExistInNotSync(String brokerName, Long brokerId, String brokerAddress) { + return this.getNotInSyncReplicas().contains(new ReplicaIdentity(brokerName, brokerId, brokerAddress)); + } + + public boolean isExistInAllReplicas(String brokerName, Long brokerId, String brokerAddress) { + return this.isExistInSync(brokerName, brokerId, brokerAddress) || this.isExistInNotSync(brokerName, brokerId, brokerAddress); + } + } + + public static class ReplicaIdentity extends RemotingSerializable { + private String brokerName; + private Long brokerId; + + private String brokerAddress; + private Boolean alive; + + public ReplicaIdentity(String brokerName, Long brokerId, String brokerAddress) { + this.brokerName = brokerName; + this.brokerId = brokerId; + this.brokerAddress = brokerAddress; + this.alive = false; + } + + public ReplicaIdentity(String brokerName, Long brokerId, String brokerAddress, Boolean alive) { + this.brokerName = brokerName; + this.brokerId = brokerId; + this.brokerAddress = brokerAddress; + this.alive = alive; + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + public String getBrokerAddress() { + return brokerAddress; + } + + public void setBrokerAddress(String brokerAddress) { + this.brokerAddress = brokerAddress; + } + + public Long getBrokerId() { + return brokerId; + } + + public void setBrokerId(Long brokerId) { + this.brokerId = brokerId; + } + + public Boolean getAlive() { + return alive; + } + + public void setAlive(Boolean alive) { + this.alive = alive; + } + + @Override + public String toString() { + return "ReplicaIdentity{" + + "brokerName='" + brokerName + '\'' + + ", brokerId=" + brokerId + + ", brokerAddress='" + brokerAddress + '\'' + + ", alive=" + alive + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ReplicaIdentity that = (ReplicaIdentity) o; + return brokerName.equals(that.brokerName) && brokerId.equals(that.brokerId) && brokerAddress.equals(that.brokerAddress); + } + + @Override + public int hashCode() { + return Objects.hash(brokerName, brokerId, brokerAddress); + } + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/BrokerStatsData.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/BrokerStatsData.java similarity index 96% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/BrokerStatsData.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/BrokerStatsData.java index c4ff63d0b89..f6649aa9738 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/BrokerStatsData.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/BrokerStatsData.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/BrokerStatsItem.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/BrokerStatsItem.java similarity index 96% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/BrokerStatsItem.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/BrokerStatsItem.java index e3246d0e6f7..1a339adc770 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/BrokerStatsItem.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/BrokerStatsItem.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; public class BrokerStatsItem { private long sum; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/CMResult.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/CMResult.java similarity index 94% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/CMResult.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/CMResult.java index d6dc94382d9..3e25402f0ca 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/CMResult.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/CMResult.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; public enum CMResult { CR_SUCCESS, diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/CheckClientRequestBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/CheckClientRequestBody.java similarity index 83% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/CheckClientRequestBody.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/CheckClientRequestBody.java index a78ce554560..bd482d07468 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/CheckClientRequestBody.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/CheckClientRequestBody.java @@ -15,16 +15,17 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; public class CheckClientRequestBody extends RemotingSerializable { private String clientId; private String group; private SubscriptionData subscriptionData; + private String namespace; public String getClientId() { return clientId; @@ -49,4 +50,12 @@ public SubscriptionData getSubscriptionData() { public void setSubscriptionData(SubscriptionData subscriptionData) { this.subscriptionData = subscriptionData; } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } } diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ClusterAclVersionInfo.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ClusterAclVersionInfo.java new file mode 100644 index 00000000000..3ec6cf64f32 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ClusterAclVersionInfo.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.body; + +import java.util.Map; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class ClusterAclVersionInfo extends RemotingSerializable { + + private String brokerName; + + private String brokerAddr; + + @Deprecated + private DataVersion aclConfigDataVersion; + + private Map allAclConfigDataVersion; + + private String clusterName; + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + public String getBrokerAddr() { + return brokerAddr; + } + + public void setBrokerAddr(String brokerAddr) { + this.brokerAddr = brokerAddr; + } + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public DataVersion getAclConfigDataVersion() { + return aclConfigDataVersion; + } + + public void setAclConfigDataVersion(DataVersion aclConfigDataVersion) { + this.aclConfigDataVersion = aclConfigDataVersion; + } + + public Map getAllAclConfigDataVersion() { + return allAclConfigDataVersion; + } + + public void setAllAclConfigDataVersion( + Map allAclConfigDataVersion) { + this.allAclConfigDataVersion = allAclConfigDataVersion; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ClusterInfo.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ClusterInfo.java new file mode 100644 index 00000000000..2ee73018b1b --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ClusterInfo.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import com.google.common.base.Objects; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; + +public class ClusterInfo extends RemotingSerializable { + private Map brokerAddrTable; + private Map> clusterAddrTable; + + public Map getBrokerAddrTable() { + return brokerAddrTable; + } + + public void setBrokerAddrTable(Map brokerAddrTable) { + this.brokerAddrTable = brokerAddrTable; + } + + public Map> getClusterAddrTable() { + return clusterAddrTable; + } + + public void setClusterAddrTable(Map> clusterAddrTable) { + this.clusterAddrTable = clusterAddrTable; + } + + public String[] retrieveAllAddrByCluster(String cluster) { + List addrs = new ArrayList<>(); + if (clusterAddrTable.containsKey(cluster)) { + Set brokerNames = clusterAddrTable.get(cluster); + for (String brokerName : brokerNames) { + BrokerData brokerData = brokerAddrTable.get(brokerName); + if (null != brokerData) { + addrs.addAll(brokerData.getBrokerAddrs().values()); + } + } + } + + return addrs.toArray(new String[] {}); + } + + public String[] retrieveAllClusterNames() { + return clusterAddrTable.keySet().toArray(new String[] {}); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ClusterInfo info = (ClusterInfo) o; + return Objects.equal(brokerAddrTable, info.brokerAddrTable) && Objects.equal(clusterAddrTable, info.clusterAddrTable); + } + + @Override + public int hashCode() { + return Objects.hashCode(brokerAddrTable, clusterAddrTable); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/Connection.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/Connection.java similarity index 96% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/Connection.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/Connection.java index b42737f88c5..2e804243db4 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/Connection.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/Connection.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; import org.apache.rocketmq.remoting.protocol.LanguageCode; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ConsumeByWho.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeByWho.java similarity index 90% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/ConsumeByWho.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeByWho.java index 7b20d760e64..ad62493d712 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ConsumeByWho.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeByWho.java @@ -14,14 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; import java.util.HashSet; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; public class ConsumeByWho extends RemotingSerializable { - private HashSet consumedGroup = new HashSet(); - private HashSet notConsumedGroup = new HashSet(); + private HashSet consumedGroup = new HashSet<>(); + private HashSet notConsumedGroup = new HashSet<>(); private String topic; private int queueId; private long offset; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ConsumeMessageDirectlyResult.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeMessageDirectlyResult.java similarity index 97% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/ConsumeMessageDirectlyResult.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeMessageDirectlyResult.java index 674df60229d..39da734a682 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ConsumeMessageDirectlyResult.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeMessageDirectlyResult.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ConsumeQueueData.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeQueueData.java similarity index 97% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/ConsumeQueueData.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeQueueData.java index 7268dcda56b..34ebc9af378 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ConsumeQueueData.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeQueueData.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; public class ConsumeQueueData { diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ConsumeStatsList.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeStatsList.java similarity index 80% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/ConsumeStatsList.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeStatsList.java index 7b35a8099df..11b36c86976 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ConsumeStatsList.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeStatsList.java @@ -14,18 +14,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; import java.util.ArrayList; import java.util.List; import java.util.Map; -import org.apache.rocketmq.common.admin.ConsumeStats; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.admin.ConsumeStats; public class ConsumeStatsList extends RemotingSerializable { - private List>> consumeStatsList = new ArrayList>>(); + private List>> consumeStatsList = new ArrayList<>(); private String brokerAddr; private long totalDiff; + private long totalInflightDiff; public List>> getConsumeStatsList() { return consumeStatsList; @@ -50,4 +51,12 @@ public long getTotalDiff() { public void setTotalDiff(long totalDiff) { this.totalDiff = totalDiff; } + + public long getTotalInflightDiff() { + return totalInflightDiff; + } + + public void setTotalInflightDiff(long totalInflightDiff) { + this.totalInflightDiff = totalInflightDiff; + } } diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ConsumeStatus.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeStatus.java similarity index 97% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/ConsumeStatus.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeStatus.java index 5e9a3cc88e5..6f4729cd298 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ConsumeStatus.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumeStatus.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; public class ConsumeStatus { private double pullRT; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ConsumerConnection.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumerConnection.java similarity index 87% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/ConsumerConnection.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumerConnection.java index 3a0356c7cc9..4eb5d7da4ef 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ConsumerConnection.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumerConnection.java @@ -15,21 +15,21 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; import java.util.HashSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.apache.rocketmq.common.consumer.ConsumeFromWhere; -import org.apache.rocketmq.common.protocol.heartbeat.ConsumeType; -import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; public class ConsumerConnection extends RemotingSerializable { - private HashSet connectionSet = new HashSet(); + private HashSet connectionSet = new HashSet<>(); private ConcurrentMap subscriptionTable = - new ConcurrentHashMap(); + new ConcurrentHashMap<>(); private ConsumeType consumeType; private MessageModel messageModel; private ConsumeFromWhere consumeFromWhere; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ConsumerOffsetSerializeWrapper.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumerOffsetSerializeWrapper.java similarity index 79% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/ConsumerOffsetSerializeWrapper.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumerOffsetSerializeWrapper.java index 5b08d788d9e..407be4670e8 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ConsumerOffsetSerializeWrapper.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumerOffsetSerializeWrapper.java @@ -15,15 +15,17 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import org.apache.rocketmq.remoting.protocol.DataVersion; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; public class ConsumerOffsetSerializeWrapper extends RemotingSerializable { private ConcurrentMap> offsetTable = - new ConcurrentHashMap>(512); + new ConcurrentHashMap<>(512); + private DataVersion dataVersion; public ConcurrentMap> getOffsetTable() { return offsetTable; @@ -32,4 +34,12 @@ public ConcurrentMap> getOffsetTable() { public void setOffsetTable(ConcurrentMap> offsetTable) { this.offsetTable = offsetTable; } + + public DataVersion getDataVersion() { + return dataVersion; + } + + public void setDataVersion(DataVersion dataVersion) { + this.dataVersion = dataVersion; + } } diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ConsumerRunningInfo.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumerRunningInfo.java similarity index 77% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/ConsumerRunningInfo.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumerRunningInfo.java index d7942eb4a0a..542f9300678 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ConsumerRunningInfo.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ConsumerRunningInfo.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; import java.util.Iterator; import java.util.Map.Entry; @@ -23,9 +23,9 @@ import java.util.TreeMap; import java.util.TreeSet; import org.apache.rocketmq.common.message.MessageQueue; -import org.apache.rocketmq.common.protocol.heartbeat.ConsumeType; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; public class ConsumerRunningInfo extends RemotingSerializable { public static final String PROP_NAMESERVER_ADDR = "PROP_NAMESERVER_ADDR"; @@ -37,26 +37,22 @@ public class ConsumerRunningInfo extends RemotingSerializable { private Properties properties = new Properties(); - private TreeSet subscriptionSet = new TreeSet(); + private TreeSet subscriptionSet = new TreeSet<>(); - private TreeMap mqTable = new TreeMap(); + private TreeMap mqTable = new TreeMap<>(); - private TreeMap statusTable = new TreeMap(); + private TreeMap mqPopTable = new TreeMap<>(); + + private TreeMap statusTable = new TreeMap<>(); + + private TreeMap userConsumerInfo = new TreeMap<>(); private String jstack; public static boolean analyzeSubscription(final TreeMap criTable) { ConsumerRunningInfo prev = criTable.firstEntry().getValue(); - boolean push = false; - { - String property = prev.getProperties().getProperty(ConsumerRunningInfo.PROP_CONSUME_TYPE); - - if (property == null) { - property = ((ConsumeType) prev.getProperties().get(ConsumerRunningInfo.PROP_CONSUME_TYPE)).name(); - } - push = ConsumeType.valueOf(property) == ConsumeType.CONSUME_PASSIVELY; - } + boolean push = isPushType(prev); boolean startForAWhile = false; { @@ -85,19 +81,29 @@ public static boolean analyzeSubscription(final TreeMap criTable) { return true; } @@ -135,7 +141,7 @@ public static String analyzeProcessQueue(final String clientId, ConsumerRunningI mq, System.currentTimeMillis() - pq.getLastLockTimestamp())); } else { - if (pq.isDroped() && (pq.getTryUnlockTimes() > 0)) { + if (pq.isDroped() && pq.getTryUnlockTimes() > 0) { sb.append(String.format("%s %s unlock %d times, still failed%n", clientId, mq, @@ -191,6 +197,10 @@ public void setStatusTable(TreeMap statusTable) { this.statusTable = statusTable; } + public TreeMap getUserConsumerInfo() { + return userConsumerInfo; + } + public String formatString() { StringBuilder sb = new StringBuilder(); @@ -223,7 +233,7 @@ public String formatString() { { sb.append("\n\n#Consumer Offset#\n"); - sb.append(String.format("%-32s %-32s %-4s %-20s%n", + sb.append(String.format("%-64s %-32s %-4s %-20s%n", "#Topic", "#Broker Name", "#QID", @@ -245,7 +255,7 @@ public String formatString() { { sb.append("\n\n#Consumer MQ Detail#\n"); - sb.append(String.format("%-32s %-32s %-4s %-20s%n", + sb.append(String.format("%-64s %-32s %-4s %-20s%n", "#Topic", "#Broker Name", "#QID", @@ -255,6 +265,28 @@ public String formatString() { Iterator> it = this.mqTable.entrySet().iterator(); while (it.hasNext()) { Entry next = it.next(); + String item = String.format("%-64s %-32s %-4d %s%n", + next.getKey().getTopic(), + next.getKey().getBrokerName(), + next.getKey().getQueueId(), + next.getValue().toString()); + + sb.append(item); + } + } + + { + sb.append("\n\n#Consumer Pop Detail#\n"); + sb.append(String.format("%-32s %-32s %-4s %-20s%n", + "#Topic", + "#Broker Name", + "#QID", + "#ProcessQueueInfo" + )); + + Iterator> it = this.mqPopTable.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); String item = String.format("%-32s %-32s %-4d %s%n", next.getKey().getTopic(), next.getKey().getBrokerName(), @@ -267,7 +299,7 @@ public String formatString() { { sb.append("\n\n#Consumer RT&TPS#\n"); - sb.append(String.format("%-32s %14s %14s %14s %14s %18s %25s%n", + sb.append(String.format("%-64s %14s %14s %14s %14s %18s %25s%n", "#Topic", "#Pull RT", "#Pull TPS", @@ -294,6 +326,16 @@ public String formatString() { } } + if (this.userConsumerInfo != null) { + sb.append("\n\n#User Consume Info#\n"); + Iterator> it = this.userConsumerInfo.entrySet().iterator(); + while (it.hasNext()) { + Entry next = it.next(); + String item = String.format("%-40s: %s%n", next.getKey(), next.getValue()); + sb.append(item); + } + } + if (this.jstack != null) { sb.append("\n\n#Consumer jstack#\n"); sb.append(this.jstack); @@ -310,4 +352,12 @@ public void setJstack(String jstack) { this.jstack = jstack; } + public TreeMap getMqPopTable() { + return mqPopTable; + } + + public void setMqPopTable( + TreeMap mqPopTable) { + this.mqPopTable = mqPopTable; + } } diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ElectMasterResponseBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ElectMasterResponseBody.java new file mode 100644 index 00000000000..8aef636fa4c --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ElectMasterResponseBody.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import com.google.common.base.Objects; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import java.util.HashSet; +import java.util.Set; + +public class ElectMasterResponseBody extends RemotingSerializable { + private BrokerMemberGroup brokerMemberGroup; + private Set syncStateSet; + + // Provide default constructor for serializer + public ElectMasterResponseBody() { + this.syncStateSet = new HashSet(); + this.brokerMemberGroup = null; + } + + public ElectMasterResponseBody(final Set syncStateSet) { + this.syncStateSet = syncStateSet; + this.brokerMemberGroup = null; + } + + public ElectMasterResponseBody(final BrokerMemberGroup brokerMemberGroup, final Set syncStateSet) { + this.brokerMemberGroup = brokerMemberGroup; + this.syncStateSet = syncStateSet; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ElectMasterResponseBody that = (ElectMasterResponseBody) o; + return Objects.equal(brokerMemberGroup, that.brokerMemberGroup) && + Objects.equal(syncStateSet, that.syncStateSet); + } + + @Override + public int hashCode() { + return Objects.hashCode(brokerMemberGroup, syncStateSet); + } + + @Override + public String toString() { + return "BrokerMemberGroup{" + + "brokerMemberGroup='" + brokerMemberGroup.toString() + '\'' + + ", syncStateSet='" + syncStateSet.toString() + + '}'; + } + + public void setBrokerMemberGroup(BrokerMemberGroup brokerMemberGroup) { + this.brokerMemberGroup = brokerMemberGroup; + } + + public BrokerMemberGroup getBrokerMemberGroup() { + return brokerMemberGroup; + } + + public void setSyncStateSet(Set syncStateSet) { + this.syncStateSet = syncStateSet; + } + + public Set getSyncStateSet() { + return syncStateSet; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/EpochEntryCache.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/EpochEntryCache.java new file mode 100644 index 00000000000..642331cb11f --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/EpochEntryCache.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.body; + +import java.util.List; +import org.apache.rocketmq.remoting.protocol.EpochEntry; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class EpochEntryCache extends RemotingSerializable { + private String clusterName; + private String brokerName; + private long brokerId; + private List epochList; + private long maxOffset; + + public EpochEntryCache(String clusterName, String brokerName, long brokerId, List epochList, long maxOffset) { + this.clusterName = clusterName; + this.brokerName = brokerName; + this.brokerId = brokerId; + this.epochList = epochList; + this.maxOffset = maxOffset; + } + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + public long getBrokerId() { + return brokerId; + } + + public void setBrokerId(long brokerId) { + this.brokerId = brokerId; + } + + public List getEpochList() { + return this.epochList; + } + + public void setEpochList(List epochList) { + this.epochList = epochList; + } + + public long getMaxOffset() { + return maxOffset; + } + + public void setMaxOffset(long maxOffset) { + this.maxOffset = maxOffset; + } + + @Override + public String toString() { + return "EpochEntryCache{" + + "clusterName='" + clusterName + '\'' + + ", brokerName='" + brokerName + '\'' + + ", brokerId=" + brokerId + + ", epochList=" + epochList + + ", maxOffset=" + maxOffset + + '}'; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/GetBrokerMemberGroupResponseBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/GetBrokerMemberGroupResponseBody.java new file mode 100644 index 00000000000..f3384022060 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/GetBrokerMemberGroupResponseBody.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class GetBrokerMemberGroupResponseBody extends RemotingSerializable { + // Contains the broker member info of the same broker group + private BrokerMemberGroup brokerMemberGroup; + + public BrokerMemberGroup getBrokerMemberGroup() { + return brokerMemberGroup; + } + + public void setBrokerMemberGroup(final BrokerMemberGroup brokerMemberGroup) { + this.brokerMemberGroup = brokerMemberGroup; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/GetConsumerStatusBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/GetConsumerStatusBody.java similarity index 92% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/GetConsumerStatusBody.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/GetConsumerStatusBody.java index 6234a774d4e..f69193ad10a 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/GetConsumerStatusBody.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/GetConsumerStatusBody.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; import java.util.HashMap; import java.util.Map; @@ -24,9 +24,9 @@ @Deprecated public class GetConsumerStatusBody extends RemotingSerializable { - private Map messageQueueTable = new HashMap(); + private Map messageQueueTable = new HashMap<>(); private Map> consumerTable = - new HashMap>(); + new HashMap<>(); public Map getMessageQueueTable() { return messageQueueTable; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/GroupList.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/GroupList.java similarity index 90% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/GroupList.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/GroupList.java index 862a739ec6a..f2fa43fc944 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/GroupList.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/GroupList.java @@ -14,13 +14,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; import java.util.HashSet; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; public class GroupList extends RemotingSerializable { - private HashSet groupList = new HashSet(); + private HashSet groupList = new HashSet<>(); public HashSet getGroupList() { return groupList; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/HARuntimeInfo.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/HARuntimeInfo.java new file mode 100644 index 00000000000..d0f3fb231a3 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/HARuntimeInfo.java @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import java.util.ArrayList; +import java.util.List; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class HARuntimeInfo extends RemotingSerializable { + + private boolean master; + private long masterCommitLogMaxOffset; + private int inSyncSlaveNums; + private List haConnectionInfo = new ArrayList<>(); + private HAClientRuntimeInfo haClientRuntimeInfo = new HAClientRuntimeInfo(); + + public boolean isMaster() { + return this.master; + } + + public void setMaster(boolean master) { + this.master = master; + } + + public long getMasterCommitLogMaxOffset() { + return this.masterCommitLogMaxOffset; + } + + public void setMasterCommitLogMaxOffset(long masterCommitLogMaxOffset) { + this.masterCommitLogMaxOffset = masterCommitLogMaxOffset; + } + + public int getInSyncSlaveNums() { + return this.inSyncSlaveNums; + } + + public void setInSyncSlaveNums(int inSyncSlaveNums) { + this.inSyncSlaveNums = inSyncSlaveNums; + } + + public List getHaConnectionInfo() { + return this.haConnectionInfo; + } + + public void setHaConnectionInfo(List haConnectionInfo) { + this.haConnectionInfo = haConnectionInfo; + } + + public HAClientRuntimeInfo getHaClientRuntimeInfo() { + return this.haClientRuntimeInfo; + } + + public void setHaClientRuntimeInfo(HAClientRuntimeInfo haClientRuntimeInfo) { + this.haClientRuntimeInfo = haClientRuntimeInfo; + } + + public static class HAConnectionRuntimeInfo extends RemotingSerializable { + private String addr; + private long slaveAckOffset; + private long diff; + private boolean inSync; + private long transferredByteInSecond; + private long transferFromWhere; + + public String getAddr() { + return this.addr; + } + + public void setAddr(String addr) { + this.addr = addr; + } + + public long getSlaveAckOffset() { + return this.slaveAckOffset; + } + + public void setSlaveAckOffset(long slaveAckOffset) { + this.slaveAckOffset = slaveAckOffset; + } + + public long getDiff() { + return this.diff; + } + + public void setDiff(long diff) { + this.diff = diff; + } + + public boolean isInSync() { + return this.inSync; + } + + public void setInSync(boolean inSync) { + this.inSync = inSync; + } + + public long getTransferredByteInSecond() { + return this.transferredByteInSecond; + } + + public void setTransferredByteInSecond(long transferredByteInSecond) { + this.transferredByteInSecond = transferredByteInSecond; + } + + public long getTransferFromWhere() { + return transferFromWhere; + } + + public void setTransferFromWhere(long transferFromWhere) { + this.transferFromWhere = transferFromWhere; + } + } + + public static class HAClientRuntimeInfo extends RemotingSerializable { + private String masterAddr; + private long transferredByteInSecond; + private long maxOffset; + private long lastReadTimestamp; + private long lastWriteTimestamp; + private long masterFlushOffset; + private boolean isActivated = false; + + public String getMasterAddr() { + return this.masterAddr; + } + + public void setMasterAddr(String masterAddr) { + this.masterAddr = masterAddr; + } + + public long getTransferredByteInSecond() { + return this.transferredByteInSecond; + } + + public void setTransferredByteInSecond(long transferredByteInSecond) { + this.transferredByteInSecond = transferredByteInSecond; + } + + public long getMaxOffset() { + return this.maxOffset; + } + + public void setMaxOffset(long maxOffset) { + this.maxOffset = maxOffset; + } + + public long getLastReadTimestamp() { + return this.lastReadTimestamp; + } + + public void setLastReadTimestamp(long lastReadTimestamp) { + this.lastReadTimestamp = lastReadTimestamp; + } + + public long getLastWriteTimestamp() { + return this.lastWriteTimestamp; + } + + public void setLastWriteTimestamp(long lastWriteTimestamp) { + this.lastWriteTimestamp = lastWriteTimestamp; + } + + public long getMasterFlushOffset() { + return masterFlushOffset; + } + + public void setMasterFlushOffset(long masterFlushOffset) { + this.masterFlushOffset = masterFlushOffset; + } + } + +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/KVTable.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/KVTable.java similarity index 89% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/KVTable.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/KVTable.java index 28aafc45782..73452b4c925 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/KVTable.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/KVTable.java @@ -14,13 +14,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; import java.util.HashMap; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; public class KVTable extends RemotingSerializable { - private HashMap table = new HashMap(); + private HashMap table = new HashMap<>(); public HashMap getTable() { return table; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/LockBatchRequestBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/LockBatchRequestBody.java new file mode 100644 index 00000000000..6766564bc72 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/LockBatchRequestBody.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import com.google.common.base.MoreObjects; +import java.util.HashSet; +import java.util.Set; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class LockBatchRequestBody extends RemotingSerializable { + private String consumerGroup; + private String clientId; + private boolean onlyThisBroker = false; + private Set mqSet = new HashSet<>(); + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public boolean isOnlyThisBroker() { + return onlyThisBroker; + } + + public void setOnlyThisBroker(boolean onlyThisBroker) { + this.onlyThisBroker = onlyThisBroker; + } + + public Set getMqSet() { + return mqSet; + } + + public void setMqSet(Set mqSet) { + this.mqSet = mqSet; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("consumerGroup", consumerGroup) + .add("clientId", clientId) + .add("onlyThisBroker", onlyThisBroker) + .add("mqSet", mqSet) + .toString(); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/LockBatchResponseBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/LockBatchResponseBody.java similarity index 90% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/LockBatchResponseBody.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/LockBatchResponseBody.java index 5018f203146..a46a8aac37f 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/LockBatchResponseBody.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/LockBatchResponseBody.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; import java.util.HashSet; import java.util.Set; @@ -24,7 +24,7 @@ public class LockBatchResponseBody extends RemotingSerializable { - private Set lockOKMQSet = new HashSet(); + private Set lockOKMQSet = new HashSet<>(); public Set getLockOKMQSet() { return lockOKMQSet; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/MessageRequestModeSerializeWrapper.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/MessageRequestModeSerializeWrapper.java new file mode 100644 index 00000000000..fcc0e6f893a --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/MessageRequestModeSerializeWrapper.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.body; + +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class MessageRequestModeSerializeWrapper extends RemotingSerializable { + + private ConcurrentHashMap> + messageRequestModeMap = new ConcurrentHashMap<>(); + + public ConcurrentHashMap> getMessageRequestModeMap() { + return messageRequestModeMap; + } + + public void setMessageRequestModeMap(ConcurrentHashMap> messageRequestModeMap) { + this.messageRequestModeMap = messageRequestModeMap; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/PopProcessQueueInfo.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/PopProcessQueueInfo.java new file mode 100644 index 00000000000..3a6bc3f69e4 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/PopProcessQueueInfo.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.body; + +public class PopProcessQueueInfo { + private int waitAckCount; + private boolean droped; + private long lastPopTimestamp; + + + public int getWaitAckCount() { + return waitAckCount; + } + + + public void setWaitAckCount(int waitAckCount) { + this.waitAckCount = waitAckCount; + } + + + public boolean isDroped() { + return droped; + } + + + public void setDroped(boolean droped) { + this.droped = droped; + } + + + public long getLastPopTimestamp() { + return lastPopTimestamp; + } + + + public void setLastPopTimestamp(long lastPopTimestamp) { + this.lastPopTimestamp = lastPopTimestamp; + } + + @Override + public String toString() { + return "PopProcessQueueInfo [waitAckCount:" + waitAckCount + + ", droped:" + droped + ", lastPopTimestamp:" + lastPopTimestamp + "]"; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ProcessQueueInfo.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ProcessQueueInfo.java similarity index 98% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/ProcessQueueInfo.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ProcessQueueInfo.java index 6b220b81216..075b56eb820 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ProcessQueueInfo.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ProcessQueueInfo.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; import org.apache.rocketmq.common.UtilAll; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ProducerConnection.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ProducerConnection.java similarity index 90% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/ProducerConnection.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ProducerConnection.java index 14d71104b27..91efe5a2326 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ProducerConnection.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ProducerConnection.java @@ -15,13 +15,13 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; import java.util.HashSet; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; public class ProducerConnection extends RemotingSerializable { - private HashSet connectionSet = new HashSet(); + private HashSet connectionSet = new HashSet<>(); public HashSet getConnectionSet() { return connectionSet; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ProducerInfo.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ProducerInfo.java new file mode 100644 index 00000000000..bb6d3c8c738 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ProducerInfo.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import org.apache.rocketmq.remoting.protocol.LanguageCode; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + + +public class ProducerInfo extends RemotingSerializable { + private String clientId; + private String remoteIP; + private LanguageCode language; + private int version; + private long lastUpdateTimestamp; + + public ProducerInfo(String clientId, String remoteIP, LanguageCode language, int version, long lastUpdateTimestamp) { + this.clientId = clientId; + this.remoteIP = remoteIP; + this.language = language; + this.version = version; + this.lastUpdateTimestamp = lastUpdateTimestamp; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getRemoteIP() { + return remoteIP; + } + + public void setRemoteIP(String remoteIP) { + this.remoteIP = remoteIP; + } + + public LanguageCode getLanguage() { + return language; + } + + public void setLanguage(LanguageCode language) { + this.language = language; + } + + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } + + public long getLastUpdateTimestamp() { + return lastUpdateTimestamp; + } + + public void setLastUpdateTimestamp(long lastUpdateTimestamp) { + this.lastUpdateTimestamp = lastUpdateTimestamp; + } + + @Override + public String toString() { + return String.format("clientId=%s,remoteIP=%s, language=%s, version=%d, lastUpdateTimestamp=%d", + clientId, remoteIP, language.name(), version, lastUpdateTimestamp); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ProducerTableInfo.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ProducerTableInfo.java new file mode 100644 index 00000000000..d4a1d0b3d84 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ProducerTableInfo.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import java.util.List; +import java.util.Map; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class ProducerTableInfo extends RemotingSerializable { + public ProducerTableInfo(Map> data) { + this.data = data; + } + + private Map> data; + + public Map> getData() { + return data; + } + + public void setData(Map> data) { + this.data = data; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueryAssignmentRequestBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueryAssignmentRequestBody.java new file mode 100644 index 00000000000..fc83b511340 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueryAssignmentRequestBody.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; + +public class QueryAssignmentRequestBody extends RemotingSerializable { + + private String topic; + + private String consumerGroup; + + private String clientId; + + private String strategyName; + + private MessageModel messageModel; + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getStrategyName() { + return strategyName; + } + + public void setStrategyName(String strategyName) { + this.strategyName = strategyName; + } + + public MessageModel getMessageModel() { + return messageModel; + } + + public void setMessageModel(MessageModel messageModel) { + this.messageModel = messageModel; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueryAssignmentResponseBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueryAssignmentResponseBody.java new file mode 100644 index 00000000000..8d9b53270bb --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueryAssignmentResponseBody.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import java.util.Set; +import org.apache.rocketmq.common.message.MessageQueueAssignment; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class QueryAssignmentResponseBody extends RemotingSerializable { + + private Set messageQueueAssignments; + + public Set getMessageQueueAssignments() { + return messageQueueAssignments; + } + + public void setMessageQueueAssignments( + Set messageQueueAssignments) { + this.messageQueueAssignments = messageQueueAssignments; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/QueryConsumeQueueResponseBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueryConsumeQueueResponseBody.java similarity index 94% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/QueryConsumeQueueResponseBody.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueryConsumeQueueResponseBody.java index be93da993d0..ecc84c6e8a6 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/QueryConsumeQueueResponseBody.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueryConsumeQueueResponseBody.java @@ -15,12 +15,11 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; - -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; -import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +package org.apache.rocketmq.remoting.protocol.body; import java.util.List; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; public class QueryConsumeQueueResponseBody extends RemotingSerializable { diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/QueryConsumeTimeSpanBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueryConsumeTimeSpanBody.java similarity index 90% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/QueryConsumeTimeSpanBody.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueryConsumeTimeSpanBody.java index cae21094bbb..599ccc890f8 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/QueryConsumeTimeSpanBody.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueryConsumeTimeSpanBody.java @@ -15,14 +15,14 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; import java.util.ArrayList; import java.util.List; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; public class QueryConsumeTimeSpanBody extends RemotingSerializable { - List consumeTimeSpanSet = new ArrayList(); + List consumeTimeSpanSet = new ArrayList<>(); public List getConsumeTimeSpanSet() { return consumeTimeSpanSet; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/QueryCorrectionOffsetBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueryCorrectionOffsetBody.java similarity index 90% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/QueryCorrectionOffsetBody.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueryCorrectionOffsetBody.java index f7c866ac084..85be8bc9325 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/QueryCorrectionOffsetBody.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueryCorrectionOffsetBody.java @@ -14,14 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; import java.util.HashMap; import java.util.Map; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; public class QueryCorrectionOffsetBody extends RemotingSerializable { - private Map correctionOffsets = new HashMap(); + private Map correctionOffsets = new HashMap<>(); public Map getCorrectionOffsets() { return correctionOffsets; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QuerySubscriptionResponseBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QuerySubscriptionResponseBody.java new file mode 100644 index 00000000000..e094a07234f --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QuerySubscriptionResponseBody.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; + +public class QuerySubscriptionResponseBody extends RemotingSerializable { + + private SubscriptionData subscriptionData; + private String group; + private String topic; + + public SubscriptionData getSubscriptionData() { + return subscriptionData; + } + + public void setSubscriptionData(SubscriptionData subscriptionData) { + this.subscriptionData = subscriptionData; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/QueueTimeSpan.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueueTimeSpan.java similarity index 97% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/QueueTimeSpan.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueueTimeSpan.java index e579d9162e9..6bcb2a38f93 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/QueueTimeSpan.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/QueueTimeSpan.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; import java.util.Date; import org.apache.rocketmq.common.UtilAll; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/RegisterBrokerBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/RegisterBrokerBody.java new file mode 100644 index 00000000000..99557b1d3fb --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/RegisterBrokerBody.java @@ -0,0 +1,225 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import com.alibaba.fastjson.JSON; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.InflaterInputStream; +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingInfo; + +public class RegisterBrokerBody extends RemotingSerializable { + + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); + private TopicConfigAndMappingSerializeWrapper topicConfigSerializeWrapper = new TopicConfigAndMappingSerializeWrapper(); + private List filterServerList = new ArrayList<>(); + private static final long MINIMUM_TAKE_TIME_MILLISECOND = 50; + + public byte[] encode(boolean compress) { + + if (!compress) { + return super.encode(); + } + long start = System.currentTimeMillis(); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + DeflaterOutputStream outputStream = new DeflaterOutputStream(byteArrayOutputStream, new Deflater(Deflater.BEST_COMPRESSION)); + DataVersion dataVersion = topicConfigSerializeWrapper.getDataVersion(); + ConcurrentMap topicConfigTable = cloneTopicConfigTable(topicConfigSerializeWrapper.getTopicConfigTable()); + assert topicConfigTable != null; + try { + byte[] buffer = dataVersion.encode(); + + // write data version + outputStream.write(convertIntToByteArray(buffer.length)); + outputStream.write(buffer); + + int topicNumber = topicConfigTable.size(); + + // write number of topic configs + outputStream.write(convertIntToByteArray(topicNumber)); + + // write topic config entry one by one. + for (ConcurrentMap.Entry next : topicConfigTable.entrySet()) { + buffer = next.getValue().encode().getBytes(MixAll.DEFAULT_CHARSET); + outputStream.write(convertIntToByteArray(buffer.length)); + outputStream.write(buffer); + } + + buffer = JSON.toJSONString(filterServerList).getBytes(MixAll.DEFAULT_CHARSET); + + // write filter server list json length + outputStream.write(convertIntToByteArray(buffer.length)); + + // write filter server list json + outputStream.write(buffer); + + //write the topic queue mapping + Map topicQueueMappingInfoMap = topicConfigSerializeWrapper.getTopicQueueMappingInfoMap(); + if (topicQueueMappingInfoMap == null) { + //as the placeholder + topicQueueMappingInfoMap = new ConcurrentHashMap<>(); + } + outputStream.write(convertIntToByteArray(topicQueueMappingInfoMap.size())); + for (TopicQueueMappingInfo info: topicQueueMappingInfoMap.values()) { + buffer = JSON.toJSONString(info).getBytes(MixAll.DEFAULT_CHARSET); + outputStream.write(convertIntToByteArray(buffer.length)); + // write filter server list json + outputStream.write(buffer); + } + + outputStream.finish(); + long takeTime = System.currentTimeMillis() - start; + if (takeTime > MINIMUM_TAKE_TIME_MILLISECOND) { + LOGGER.info("Compressing takes {}ms", takeTime); + } + return byteArrayOutputStream.toByteArray(); + } catch (IOException e) { + LOGGER.error("Failed to compress RegisterBrokerBody object", e); + } + + return null; + } + + public static RegisterBrokerBody decode(byte[] data, boolean compressed, MQVersion.Version brokerVersion) throws IOException { + if (!compressed) { + return RegisterBrokerBody.decode(data, RegisterBrokerBody.class); + } + long start = System.currentTimeMillis(); + InflaterInputStream inflaterInputStream = new InflaterInputStream(new ByteArrayInputStream(data)); + int dataVersionLength = readInt(inflaterInputStream); + byte[] dataVersionBytes = readBytes(inflaterInputStream, dataVersionLength); + DataVersion dataVersion = DataVersion.decode(dataVersionBytes, DataVersion.class); + + RegisterBrokerBody registerBrokerBody = new RegisterBrokerBody(); + registerBrokerBody.getTopicConfigSerializeWrapper().setDataVersion(dataVersion); + ConcurrentMap topicConfigTable = registerBrokerBody.getTopicConfigSerializeWrapper().getTopicConfigTable(); + + int topicConfigNumber = readInt(inflaterInputStream); + LOGGER.debug("{} topic configs to extract", topicConfigNumber); + + for (int i = 0; i < topicConfigNumber; i++) { + int topicConfigJsonLength = readInt(inflaterInputStream); + + byte[] buffer = readBytes(inflaterInputStream, topicConfigJsonLength); + TopicConfig topicConfig = new TopicConfig(); + String topicConfigJson = new String(buffer, MixAll.DEFAULT_CHARSET); + topicConfig.decode(topicConfigJson); + topicConfigTable.put(topicConfig.getTopicName(), topicConfig); + } + + int filterServerListJsonLength = readInt(inflaterInputStream); + + byte[] filterServerListBuffer = readBytes(inflaterInputStream, filterServerListJsonLength); + String filterServerListJson = new String(filterServerListBuffer, MixAll.DEFAULT_CHARSET); + List filterServerList = new ArrayList<>(); + try { + filterServerList = JSON.parseArray(filterServerListJson, String.class); + } catch (Exception e) { + LOGGER.error("Decompressing occur Exception {}", filterServerListJson); + } + + registerBrokerBody.setFilterServerList(filterServerList); + + if (brokerVersion.ordinal() >= MQVersion.Version.V5_0_0.ordinal()) { + int topicQueueMappingNum = readInt(inflaterInputStream); + Map topicQueueMappingInfoMap = new ConcurrentHashMap<>(); + for (int i = 0; i < topicQueueMappingNum; i++) { + int mappingJsonLen = readInt(inflaterInputStream); + byte[] buffer = readBytes(inflaterInputStream, mappingJsonLen); + TopicQueueMappingInfo info = TopicQueueMappingInfo.decode(buffer, TopicQueueMappingInfo.class); + topicQueueMappingInfoMap.put(info.getTopic(), info); + } + registerBrokerBody.getTopicConfigSerializeWrapper().setTopicQueueMappingInfoMap(topicQueueMappingInfoMap); + } + + long takeTime = System.currentTimeMillis() - start; + if (takeTime > MINIMUM_TAKE_TIME_MILLISECOND) { + LOGGER.info("Decompressing takes {}ms", takeTime); + } + return registerBrokerBody; + } + + private static byte[] convertIntToByteArray(int n) { + ByteBuffer byteBuffer = ByteBuffer.allocate(4); + byteBuffer.putInt(n); + return byteBuffer.array(); + } + + private static byte[] readBytes(InflaterInputStream inflaterInputStream, int length) throws IOException { + byte[] buffer = new byte[length]; + int bytesRead = 0; + while (bytesRead < length) { + int len = inflaterInputStream.read(buffer, bytesRead, length - bytesRead); + if (len == -1) { + throw new IOException("End of compressed data has reached"); + } else { + bytesRead += len; + } + } + return buffer; + } + + private static int readInt(InflaterInputStream inflaterInputStream) throws IOException { + byte[] buffer = readBytes(inflaterInputStream, 4); + ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); + return byteBuffer.getInt(); + } + + public TopicConfigAndMappingSerializeWrapper getTopicConfigSerializeWrapper() { + return topicConfigSerializeWrapper; + } + + public void setTopicConfigSerializeWrapper(TopicConfigAndMappingSerializeWrapper topicConfigSerializeWrapper) { + this.topicConfigSerializeWrapper = topicConfigSerializeWrapper; + } + + public List getFilterServerList() { + return filterServerList; + } + + public void setFilterServerList(List filterServerList) { + this.filterServerList = filterServerList; + } + + private ConcurrentMap cloneTopicConfigTable( + ConcurrentMap topicConfigConcurrentMap) { + if (topicConfigConcurrentMap == null) { + return null; + } + ConcurrentHashMap result = new ConcurrentHashMap<>(topicConfigConcurrentMap.size()); + result.putAll(topicConfigConcurrentMap); + return result; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ResetOffsetBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ResetOffsetBody.java similarity index 89% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/ResetOffsetBody.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ResetOffsetBody.java index b28e74b5656..840bfbf40db 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ResetOffsetBody.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ResetOffsetBody.java @@ -15,15 +15,21 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; +import java.util.HashMap; import java.util.Map; import org.apache.rocketmq.common.message.MessageQueue; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; public class ResetOffsetBody extends RemotingSerializable { + private Map offsetTable; + public ResetOffsetBody() { + offsetTable = new HashMap<>(); + } + public Map getOffsetTable() { return offsetTable; } diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ResetOffsetBodyForC.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ResetOffsetBodyForC.java similarity index 96% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/ResetOffsetBodyForC.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ResetOffsetBodyForC.java index fa812ed2cd9..24702328c97 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/ResetOffsetBodyForC.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/ResetOffsetBodyForC.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; import java.util.List; import org.apache.rocketmq.common.message.MessageQueueForC; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/RoleChangeNotifyEntry.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/RoleChangeNotifyEntry.java new file mode 100644 index 00000000000..ab25df0d1d0 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/RoleChangeNotifyEntry.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + + +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.header.controller.ElectMasterResponseHeader; + +import java.util.Set; + +public class RoleChangeNotifyEntry { + + private final BrokerMemberGroup brokerMemberGroup; + + private final String masterAddress; + + private final Long masterBrokerId; + + private final int masterEpoch; + + private final int syncStateSetEpoch; + + private final Set syncStateSet; + + public RoleChangeNotifyEntry(BrokerMemberGroup brokerMemberGroup, String masterAddress, Long masterBrokerId, int masterEpoch, int syncStateSetEpoch, Set syncStateSet) { + this.brokerMemberGroup = brokerMemberGroup; + this.masterAddress = masterAddress; + this.masterEpoch = masterEpoch; + this.syncStateSetEpoch = syncStateSetEpoch; + this.masterBrokerId = masterBrokerId; + this.syncStateSet = syncStateSet; + } + + public static RoleChangeNotifyEntry convert(RemotingCommand electMasterResponse) { + final ElectMasterResponseHeader header = (ElectMasterResponseHeader) electMasterResponse.readCustomHeader(); + BrokerMemberGroup brokerMemberGroup = null; + Set syncStateSet = null; + + if (electMasterResponse.getBody() != null && electMasterResponse.getBody().length > 0) { + ElectMasterResponseBody body = RemotingSerializable.decode(electMasterResponse.getBody(), ElectMasterResponseBody.class); + brokerMemberGroup = body.getBrokerMemberGroup(); + syncStateSet = body.getSyncStateSet(); + } + + return new RoleChangeNotifyEntry(brokerMemberGroup, header.getMasterAddress(), header.getMasterBrokerId(), header.getMasterEpoch(), header.getSyncStateSetEpoch(), syncStateSet); + } + + + public BrokerMemberGroup getBrokerMemberGroup() { + return brokerMemberGroup; + } + + public String getMasterAddress() { + return masterAddress; + } + + public int getMasterEpoch() { + return masterEpoch; + } + + public int getSyncStateSetEpoch() { + return syncStateSetEpoch; + } + + public Long getMasterBrokerId() { + return masterBrokerId; + } + + public Set getSyncStateSet() { + return syncStateSet; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/SetMessageRequestModeRequestBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/SetMessageRequestModeRequestBody.java new file mode 100644 index 00000000000..31aecd0ba63 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/SetMessageRequestModeRequestBody.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import org.apache.rocketmq.common.message.MessageRequestMode; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class SetMessageRequestModeRequestBody extends RemotingSerializable { + + private String topic; + + private String consumerGroup; + + private MessageRequestMode mode = MessageRequestMode.PULL; + + /* + consumer working in pop mode could share the MessageQueues assigned to the N (N = popShareQueueNum) consumers following it in the cid list + */ + private int popShareQueueNum = 0; + + public SetMessageRequestModeRequestBody() { + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public MessageRequestMode getMode() { + return mode; + } + + public void setMode(MessageRequestMode mode) { + this.mode = mode; + } + + public int getPopShareQueueNum() { + return popShareQueueNum; + } + + public void setPopShareQueueNum(int popShareQueueNum) { + this.popShareQueueNum = popShareQueueNum; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/SubscriptionGroupWrapper.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/SubscriptionGroupWrapper.java similarity index 87% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/SubscriptionGroupWrapper.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/SubscriptionGroupWrapper.java index e05f75961ff..7c159021aae 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/SubscriptionGroupWrapper.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/SubscriptionGroupWrapper.java @@ -15,17 +15,17 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import org.apache.rocketmq.common.DataVersion; -import org.apache.rocketmq.common.subscription.SubscriptionGroupConfig; +import org.apache.rocketmq.remoting.protocol.DataVersion; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; public class SubscriptionGroupWrapper extends RemotingSerializable { private ConcurrentMap subscriptionGroupTable = - new ConcurrentHashMap(1024); + new ConcurrentHashMap<>(1024); private DataVersion dataVersion = new DataVersion(); public ConcurrentMap getSubscriptionGroupTable() { diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/SyncStateSet.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/SyncStateSet.java new file mode 100644 index 00000000000..f0a71f8a978 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/SyncStateSet.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import java.util.HashSet; +import java.util.Set; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class SyncStateSet extends RemotingSerializable { + private Set syncStateSet; + private int syncStateSetEpoch; + + public SyncStateSet(Set syncStateSet, int syncStateSetEpoch) { + this.syncStateSet = new HashSet<>(syncStateSet); + this.syncStateSetEpoch = syncStateSetEpoch; + } + + public Set getSyncStateSet() { + return new HashSet<>(syncStateSet); + } + + public void setSyncStateSet(Set syncStateSet) { + this.syncStateSet = new HashSet<>(syncStateSet); + } + + public int getSyncStateSetEpoch() { + return syncStateSetEpoch; + } + + public void setSyncStateSetEpoch(int syncStateSetEpoch) { + this.syncStateSetEpoch = syncStateSetEpoch; + } + + @Override + public String toString() { + return "SyncStateSet{" + + "syncStateSet=" + syncStateSet + + ", syncStateSetEpoch=" + syncStateSetEpoch + + '}'; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/TopicConfigAndMappingSerializeWrapper.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/TopicConfigAndMappingSerializeWrapper.java new file mode 100644 index 00000000000..ae9a193ebb0 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/TopicConfigAndMappingSerializeWrapper.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingDetail; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingInfo; + +public class TopicConfigAndMappingSerializeWrapper extends TopicConfigSerializeWrapper { + private Map topicQueueMappingInfoMap = new ConcurrentHashMap<>(); + + private Map topicQueueMappingDetailMap = new ConcurrentHashMap<>(); + + private DataVersion mappingDataVersion = new DataVersion(); + + + public Map getTopicQueueMappingInfoMap() { + return topicQueueMappingInfoMap; + } + + public void setTopicQueueMappingInfoMap(Map topicQueueMappingInfoMap) { + this.topicQueueMappingInfoMap = topicQueueMappingInfoMap; + } + + public Map getTopicQueueMappingDetailMap() { + return topicQueueMappingDetailMap; + } + + public void setTopicQueueMappingDetailMap(Map topicQueueMappingDetailMap) { + this.topicQueueMappingDetailMap = topicQueueMappingDetailMap; + } + + public DataVersion getMappingDataVersion() { + return mappingDataVersion; + } + + public void setMappingDataVersion(DataVersion mappingDataVersion) { + this.mappingDataVersion = mappingDataVersion; + } + + public static TopicConfigAndMappingSerializeWrapper from(TopicConfigSerializeWrapper wrapper) { + if (wrapper instanceof TopicConfigAndMappingSerializeWrapper) { + return (TopicConfigAndMappingSerializeWrapper) wrapper; + } + TopicConfigAndMappingSerializeWrapper mappingSerializeWrapper = new TopicConfigAndMappingSerializeWrapper(); + mappingSerializeWrapper.setDataVersion(wrapper.getDataVersion()); + mappingSerializeWrapper.setTopicConfigTable(wrapper.getTopicConfigTable()); + return mappingSerializeWrapper; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/TopicConfigSerializeWrapper.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/TopicConfigSerializeWrapper.java similarity index 91% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/TopicConfigSerializeWrapper.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/TopicConfigSerializeWrapper.java index ce123021d45..b42a5b90a43 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/TopicConfigSerializeWrapper.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/TopicConfigSerializeWrapper.java @@ -15,17 +15,17 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import org.apache.rocketmq.common.DataVersion; import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.remoting.protocol.DataVersion; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; public class TopicConfigSerializeWrapper extends RemotingSerializable { private ConcurrentMap topicConfigTable = - new ConcurrentHashMap(); + new ConcurrentHashMap<>(); private DataVersion dataVersion = new DataVersion(); public ConcurrentMap getTopicConfigTable() { diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/body/TopicList.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/TopicList.java similarity index 88% rename from common/src/main/java/org/apache/rocketmq/common/protocol/body/TopicList.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/TopicList.java index baf8312479f..0de0bae7e3c 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/body/TopicList.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/TopicList.java @@ -14,14 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.rocketmq.common.protocol.body; +package org.apache.rocketmq.remoting.protocol.body; -import java.util.HashSet; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; public class TopicList extends RemotingSerializable { - private Set topicList = new HashSet(); + private Set topicList = ConcurrentHashMap.newKeySet(); private String brokerAddr; public Set getTopicList() { diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/TopicQueueMappingSerializeWrapper.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/TopicQueueMappingSerializeWrapper.java new file mode 100644 index 00000000000..17e16b8402f --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/TopicQueueMappingSerializeWrapper.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import java.util.Map; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingDetail; + +public class TopicQueueMappingSerializeWrapper extends RemotingSerializable { + private Map topicQueueMappingInfoMap; + private DataVersion dataVersion = new DataVersion(); + + public Map getTopicQueueMappingInfoMap() { + return topicQueueMappingInfoMap; + } + + public void setTopicQueueMappingInfoMap(Map topicQueueMappingInfoMap) { + this.topicQueueMappingInfoMap = topicQueueMappingInfoMap; + } + + public DataVersion getDataVersion() { + return dataVersion; + } + + public void setDataVersion(DataVersion dataVersion) { + this.dataVersion = dataVersion; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/UnlockBatchRequestBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/UnlockBatchRequestBody.java new file mode 100644 index 00000000000..2ad906739cc --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/UnlockBatchRequestBody.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import com.google.common.base.MoreObjects; +import java.util.HashSet; +import java.util.Set; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class UnlockBatchRequestBody extends RemotingSerializable { + private String consumerGroup; + private String clientId; + private boolean onlyThisBroker = false; + private Set mqSet = new HashSet<>(); + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public boolean isOnlyThisBroker() { + return onlyThisBroker; + } + + public void setOnlyThisBroker(boolean onlyThisBroker) { + this.onlyThisBroker = onlyThisBroker; + } + + public Set getMqSet() { + return mqSet; + } + + public void setMqSet(Set mqSet) { + this.mqSet = mqSet; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("consumerGroup", consumerGroup) + .add("clientId", clientId) + .add("onlyThisBroker", onlyThisBroker) + .add("mqSet", mqSet) + .toString(); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/filter/FilterAPI.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/filter/FilterAPI.java new file mode 100644 index 00000000000..f291bfccfeb --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/filter/FilterAPI.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.filter; + +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.filter.ExpressionType; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; + +import java.util.Arrays; + +public class FilterAPI { + + public static SubscriptionData buildSubscriptionData(String topic, String subString) throws Exception { + final SubscriptionData subscriptionData = new SubscriptionData(); + subscriptionData.setTopic(topic); + subscriptionData.setSubString(subString); + + if (StringUtils.isEmpty(subString) || subString.equals(SubscriptionData.SUB_ALL)) { + subscriptionData.setSubString(SubscriptionData.SUB_ALL); + return subscriptionData; + } + String[] tags = subString.split("\\|\\|"); + if (tags.length > 0) { + Arrays.stream(tags).map(String::trim).filter(tag -> !tag.isEmpty()).forEach(tag -> { + subscriptionData.getTagsSet().add(tag); + subscriptionData.getCodeSet().add(tag.hashCode()); + }); + } else { + throw new Exception("subString split error"); + } + + return subscriptionData; + } + + public static SubscriptionData buildSubscriptionData(String topic, String subString, String expressionType) throws Exception { + final SubscriptionData subscriptionData = buildSubscriptionData(topic, subString); + if (StringUtils.isNotBlank(expressionType)) { + subscriptionData.setExpressionType(expressionType); + } + return subscriptionData; + } + + public static SubscriptionData build(final String topic, final String subString, + final String type) throws Exception { + if (ExpressionType.TAG.equals(type) || type == null) { + return buildSubscriptionData(topic, subString); + } + + if (StringUtils.isEmpty(subString)) { + throw new IllegalArgumentException("Expression can't be null! " + type); + } + + SubscriptionData subscriptionData = new SubscriptionData(); + subscriptionData.setTopic(topic); + subscriptionData.setSubString(subString); + subscriptionData.setExpressionType(type); + + return subscriptionData; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/AckMessageRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/AckMessageRequestHeader.java new file mode 100644 index 00000000000..9c5d4d8b1c7 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/AckMessageRequestHeader.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +public class AckMessageRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + private String consumerGroup; + @CFNotNull + private String topic; + @CFNotNull + private Integer queueId; + @CFNotNull + private String extraInfo; + + @CFNotNull + private Long offset; + + + @Override + public void checkFields() throws RemotingCommandException { + } + + public void setOffset(Long offset) { + this.offset = offset; + } + + public Long getOffset() { + return offset; + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setExtraInfo(String extraInfo) { + this.extraInfo = extraInfo; + } + + public String getExtraInfo() { + return extraInfo; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public Integer getQueueId() { + return queueId; + } + + public void setQueueId(Integer queueId) { + this.queueId = queueId; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("consumerGroup", consumerGroup) + .add("topic", topic) + .add("queueId", queueId) + .add("extraInfo", extraInfo) + .add("offset", offset) + .toString(); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/AddBrokerRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/AddBrokerRequestHeader.java new file mode 100644 index 00000000000..8ec19833323 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/AddBrokerRequestHeader.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNullable; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class AddBrokerRequestHeader implements CommandCustomHeader { + @CFNullable + private String configPath; + + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getConfigPath() { + return configPath; + } + + public void setConfigPath(String configPath) { + this.configPath = configPath; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ChangeInvisibleTimeRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ChangeInvisibleTimeRequestHeader.java new file mode 100644 index 00000000000..fd63de0fb7e --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ChangeInvisibleTimeRequestHeader.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +public class ChangeInvisibleTimeRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + private String consumerGroup; + @CFNotNull + private String topic; + @CFNotNull + private Integer queueId; + /** + * startOffset popTime invisibleTime queueId + */ + @CFNotNull + private String extraInfo; + + @CFNotNull + private Long offset; + + @CFNotNull + private Long invisibleTime; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public void setOffset(Long offset) { + this.offset = offset; + } + + public Long getOffset() { + return offset; + } + + public Long getInvisibleTime() { + return invisibleTime; + } + + public void setInvisibleTime(Long invisibleTime) { + this.invisibleTime = invisibleTime; + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setExtraInfo(String extraInfo) { + this.extraInfo = extraInfo; + } + + /** + * startOffset popTime invisibleTime queueId + */ + public String getExtraInfo() { + return extraInfo; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public Integer getQueueId() { + return queueId; + } + + public void setQueueId(Integer queueId) { + this.queueId = queueId; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("consumerGroup", consumerGroup) + .add("topic", topic) + .add("queueId", queueId) + .add("extraInfo", extraInfo) + .add("offset", offset) + .add("invisibleTime", invisibleTime) + .toString(); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ChangeInvisibleTimeResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ChangeInvisibleTimeResponseHeader.java new file mode 100644 index 00000000000..c3b1cca6da2 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ChangeInvisibleTimeResponseHeader.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class ChangeInvisibleTimeResponseHeader implements CommandCustomHeader { + + + @CFNotNull + private long popTime; + @CFNotNull + private long invisibleTime; + + @CFNotNull + private int reviveQid; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public long getPopTime() { + return popTime; + } + + public void setPopTime(long popTime) { + this.popTime = popTime; + } + + public long getInvisibleTime() { + return invisibleTime; + } + + public void setInvisibleTime(long invisibleTime) { + this.invisibleTime = invisibleTime; + } + + public int getReviveQid() { + return reviveQid; + } + + public void setReviveQid(int reviveQid) { + this.reviveQid = reviveQid; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CheckTransactionStateRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CheckTransactionStateRequestHeader.java new file mode 100644 index 00000000000..8c04eaa2e2c --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CheckTransactionStateRequestHeader.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * $Id: EndTransactionRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; + +public class CheckTransactionStateRequestHeader extends RpcRequestHeader { + @CFNotNull + private Long tranStateTableOffset; + @CFNotNull + private Long commitLogOffset; + private String msgId; + private String transactionId; + private String offsetMsgId; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public Long getTranStateTableOffset() { + return tranStateTableOffset; + } + + public void setTranStateTableOffset(Long tranStateTableOffset) { + this.tranStateTableOffset = tranStateTableOffset; + } + + public Long getCommitLogOffset() { + return commitLogOffset; + } + + public void setCommitLogOffset(Long commitLogOffset) { + this.commitLogOffset = commitLogOffset; + } + + public String getMsgId() { + return msgId; + } + + public void setMsgId(String msgId) { + this.msgId = msgId; + } + + public String getTransactionId() { + return transactionId; + } + + public void setTransactionId(String transactionId) { + this.transactionId = transactionId; + } + + public String getOffsetMsgId() { + return offsetMsgId; + } + + public void setOffsetMsgId(String offsetMsgId) { + this.offsetMsgId = offsetMsgId; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("tranStateTableOffset", tranStateTableOffset) + .add("commitLogOffset", commitLogOffset) + .add("msgId", msgId) + .add("transactionId", transactionId) + .add("offsetMsgId", offsetMsgId) + .toString(); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/CheckTransactionStateResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CheckTransactionStateResponseHeader.java similarity index 98% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/CheckTransactionStateResponseHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CheckTransactionStateResponseHeader.java index d429eed724a..9aa2d7addba 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/CheckTransactionStateResponseHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CheckTransactionStateResponseHeader.java @@ -18,7 +18,7 @@ /** * $Id: EndTransactionResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; import org.apache.rocketmq.common.sysflag.MessageSysFlag; import org.apache.rocketmq.remoting.CommandCustomHeader; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/CloneGroupOffsetRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CloneGroupOffsetRequestHeader.java similarity index 78% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/CloneGroupOffsetRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CloneGroupOffsetRequestHeader.java index afc017b2a6c..0589c0fa828 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/CloneGroupOffsetRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CloneGroupOffsetRequestHeader.java @@ -18,13 +18,14 @@ /** * $Id: DeleteTopicRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.remoting.CommandCustomHeader; +import com.google.common.base.MoreObjects; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; -public class CloneGroupOffsetRequestHeader implements CommandCustomHeader { +public class CloneGroupOffsetRequestHeader extends RpcRequestHeader { @CFNotNull private String srcGroup; @CFNotNull @@ -68,4 +69,14 @@ public boolean isOffline() { public void setOffline(boolean offline) { this.offline = offline; } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("srcGroup", srcGroup) + .add("destGroup", destGroup) + .add("topic", topic) + .add("offline", offline) + .toString(); + } } diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ConsumeMessageDirectlyResultRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ConsumeMessageDirectlyResultRequestHeader.java new file mode 100644 index 00000000000..f56ad5b59da --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ConsumeMessageDirectlyResultRequestHeader.java @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.annotation.CFNullable; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; + +public class ConsumeMessageDirectlyResultRequestHeader extends TopicRequestHeader { + @CFNotNull + private String consumerGroup; + @CFNullable + private String clientId; + @CFNullable + private String msgId; + @CFNullable + private String brokerName; + @CFNullable + private String topic; + @CFNullable + private Integer topicSysFlag; + @CFNullable + private Integer groupSysFlag; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getMsgId() { + return msgId; + } + + public void setMsgId(String msgId) { + this.msgId = msgId; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public Integer getTopicSysFlag() { + return topicSysFlag; + } + + public void setTopicSysFlag(Integer topicSysFlag) { + this.topicSysFlag = topicSysFlag; + } + + public Integer getGroupSysFlag() { + return groupSysFlag; + } + + public void setGroupSysFlag(Integer groupSysFlag) { + this.groupSysFlag = groupSysFlag; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("consumerGroup", consumerGroup) + .add("clientId", clientId) + .add("msgId", msgId) + .add("brokerName", brokerName) + .add("topic", topic) + .add("topicSysFlag", topicSysFlag) + .add("groupSysFlag", groupSysFlag) + .toString(); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/ConsumerSendMsgBackRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ConsumerSendMsgBackRequestHeader.java similarity index 81% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/ConsumerSendMsgBackRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ConsumerSendMsgBackRequestHeader.java index bd8fbb44ca0..f69e016c2c9 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/ConsumerSendMsgBackRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ConsumerSendMsgBackRequestHeader.java @@ -15,14 +15,15 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.remoting.CommandCustomHeader; +import com.google.common.base.MoreObjects; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.annotation.CFNullable; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; -public class ConsumerSendMsgBackRequestHeader implements CommandCustomHeader { +public class ConsumerSendMsgBackRequestHeader extends RpcRequestHeader { @CFNotNull private Long offset; @CFNotNull @@ -98,7 +99,14 @@ public void setMaxReconsumeTimes(final Integer maxReconsumeTimes) { @Override public String toString() { - return "ConsumerSendMsgBackRequestHeader [group=" + group + ", originTopic=" + originTopic + ", originMsgId=" + originMsgId - + ", delayLevel=" + delayLevel + ", unitMode=" + unitMode + ", maxReconsumeTimes=" + maxReconsumeTimes + "]"; + return MoreObjects.toStringHelper(this) + .add("offset", offset) + .add("group", group) + .add("delayLevel", delayLevel) + .add("originMsgId", originMsgId) + .add("originTopic", originTopic) + .add("unitMode", unitMode) + .add("maxReconsumeTimes", maxReconsumeTimes) + .toString(); } } diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CreateAccessConfigRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CreateAccessConfigRequestHeader.java new file mode 100644 index 00000000000..d02a23858d1 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CreateAccessConfigRequestHeader.java @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class CreateAccessConfigRequestHeader implements CommandCustomHeader { + + @CFNotNull + private String accessKey; + + private String secretKey; + + private String whiteRemoteAddress; + + private boolean admin; + + private String defaultTopicPerm; + + private String defaultGroupPerm; + + // list string,eg: topicA=DENY,topicD=SUB + private String topicPerms; + + // list string,eg: groupD=DENY,groupD=SUB + private String groupPerms; + + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getAccessKey() { + return accessKey; + } + + public void setAccessKey(String accessKey) { + this.accessKey = accessKey; + } + + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + + public String getWhiteRemoteAddress() { + return whiteRemoteAddress; + } + + public void setWhiteRemoteAddress(String whiteRemoteAddress) { + this.whiteRemoteAddress = whiteRemoteAddress; + } + + public boolean isAdmin() { + return admin; + } + + public void setAdmin(boolean admin) { + this.admin = admin; + } + + public String getDefaultTopicPerm() { + return defaultTopicPerm; + } + + public void setDefaultTopicPerm(String defaultTopicPerm) { + this.defaultTopicPerm = defaultTopicPerm; + } + + public String getDefaultGroupPerm() { + return defaultGroupPerm; + } + + public void setDefaultGroupPerm(String defaultGroupPerm) { + this.defaultGroupPerm = defaultGroupPerm; + } + + public String getTopicPerms() { + return topicPerms; + } + + public void setTopicPerms(String topicPerms) { + this.topicPerms = topicPerms; + } + + public String getGroupPerms() { + return groupPerms; + } + + public void setGroupPerms(String groupPerms) { + this.groupPerms = groupPerms; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("accessKey", accessKey) + .add("secretKey", secretKey) + .add("whiteRemoteAddress", whiteRemoteAddress) + .add("admin", admin) + .add("defaultTopicPerm", defaultTopicPerm) + .add("defaultGroupPerm", defaultGroupPerm) + .add("topicPerms", topicPerms) + .add("groupPerms", groupPerms) + .toString(); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CreateTopicRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CreateTopicRequestHeader.java new file mode 100644 index 00000000000..faddd9f461e --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/CreateTopicRequestHeader.java @@ -0,0 +1,159 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * $Id: CreateTopicRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.common.TopicFilterType; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.annotation.CFNullable; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; + +public class CreateTopicRequestHeader extends TopicRequestHeader { + @CFNotNull + private String topic; + @CFNotNull + private String defaultTopic; + @CFNotNull + private Integer readQueueNums; + @CFNotNull + private Integer writeQueueNums; + @CFNotNull + private Integer perm; + @CFNotNull + private String topicFilterType; + private Integer topicSysFlag; + @CFNotNull + private Boolean order = false; + private String attributes; + + @CFNullable + private Boolean force = false; + + @Override + public void checkFields() throws RemotingCommandException { + try { + TopicFilterType.valueOf(this.topicFilterType); + } catch (Exception e) { + throw new RemotingCommandException("topicFilterType = [" + topicFilterType + "] value invalid", e); + } + } + + public TopicFilterType getTopicFilterTypeEnum() { + return TopicFilterType.valueOf(this.topicFilterType); + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getDefaultTopic() { + return defaultTopic; + } + + public void setDefaultTopic(String defaultTopic) { + this.defaultTopic = defaultTopic; + } + + public Integer getReadQueueNums() { + return readQueueNums; + } + + public void setReadQueueNums(Integer readQueueNums) { + this.readQueueNums = readQueueNums; + } + + public Integer getWriteQueueNums() { + return writeQueueNums; + } + + public void setWriteQueueNums(Integer writeQueueNums) { + this.writeQueueNums = writeQueueNums; + } + + public Integer getPerm() { + return perm; + } + + public void setPerm(Integer perm) { + this.perm = perm; + } + + public String getTopicFilterType() { + return topicFilterType; + } + + public void setTopicFilterType(String topicFilterType) { + this.topicFilterType = topicFilterType; + } + + public Integer getTopicSysFlag() { + return topicSysFlag; + } + + public void setTopicSysFlag(Integer topicSysFlag) { + this.topicSysFlag = topicSysFlag; + } + + public Boolean getOrder() { + return order; + } + + public void setOrder(Boolean order) { + this.order = order; + } + + public Boolean getForce() { + return force; + } + + public void setForce(Boolean force) { + this.force = force; + } + + public String getAttributes() { + return attributes; + } + + public void setAttributes(String attributes) { + this.attributes = attributes; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("topic", topic) + .add("defaultTopic", defaultTopic) + .add("readQueueNums", readQueueNums) + .add("writeQueueNums", writeQueueNums) + .add("perm", perm) + .add("topicFilterType", topicFilterType) + .add("topicSysFlag", topicSysFlag) + .add("order", order) + .add("attributes", attributes) + .add("force", force) + .toString(); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/DeleteAccessConfigRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/DeleteAccessConfigRequestHeader.java new file mode 100644 index 00000000000..ef5cbdc9f1a --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/DeleteAccessConfigRequestHeader.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class DeleteAccessConfigRequestHeader implements CommandCustomHeader { + + @CFNotNull + private String accessKey; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getAccessKey() { + return accessKey; + } + + public void setAccessKey(String accessKey) { + this.accessKey = accessKey; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/DeleteSubscriptionGroupRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/DeleteSubscriptionGroupRequestHeader.java similarity index 75% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/DeleteSubscriptionGroupRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/DeleteSubscriptionGroupRequestHeader.java index dff9e2f35ce..4548454853f 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/DeleteSubscriptionGroupRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/DeleteSubscriptionGroupRequestHeader.java @@ -15,16 +15,18 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; -public class DeleteSubscriptionGroupRequestHeader implements CommandCustomHeader { +public class DeleteSubscriptionGroupRequestHeader extends RpcRequestHeader { @CFNotNull private String groupName; + private boolean cleanOffset = false; + @Override public void checkFields() throws RemotingCommandException { } @@ -36,4 +38,12 @@ public String getGroupName() { public void setGroupName(String groupName) { this.groupName = groupName; } + + public boolean isCleanOffset() { + return cleanOffset; + } + + public void setCleanOffset(boolean cleanOffset) { + this.cleanOffset = cleanOffset; + } } diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/DeleteTopicRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/DeleteTopicRequestHeader.java similarity index 87% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/DeleteTopicRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/DeleteTopicRequestHeader.java index 54dd7f87b4a..d24c1da0786 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/DeleteTopicRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/DeleteTopicRequestHeader.java @@ -18,13 +18,13 @@ /** * $Id: DeleteTopicRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; -public class DeleteTopicRequestHeader implements CommandCustomHeader { +public class DeleteTopicRequestHeader extends TopicRequestHeader { @CFNotNull private String topic; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/EndTransactionRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/EndTransactionRequestHeader.java similarity index 83% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/EndTransactionRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/EndTransactionRequestHeader.java index 67785e23cfb..3f5515e272e 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/EndTransactionRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/EndTransactionRequestHeader.java @@ -15,18 +15,16 @@ * limitations under the License. */ -/** - * $Id: EndTransactionRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ - */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; +import com.google.common.base.MoreObjects; import org.apache.rocketmq.common.sysflag.MessageSysFlag; -import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.annotation.CFNullable; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; -public class EndTransactionRequestHeader implements CommandCustomHeader { +public class EndTransactionRequestHeader extends RpcRequestHeader { @CFNotNull private String producerGroup; @CFNotNull @@ -121,9 +119,14 @@ public void setTransactionId(String transactionId) { @Override public String toString() { - return "EndTransactionRequestHeader [producerGroup=" + producerGroup + ", tranStateTableOffset=" - + tranStateTableOffset + ", commitLogOffset=" + commitLogOffset + ", commitOrRollback=" - + commitOrRollback + ", fromTransactionCheck=" + fromTransactionCheck + ", msgId=" + msgId - + "]"; + return MoreObjects.toStringHelper(this) + .add("producerGroup", producerGroup) + .add("tranStateTableOffset", tranStateTableOffset) + .add("commitLogOffset", commitLogOffset) + .add("commitOrRollback", commitOrRollback) + .add("fromTransactionCheck", fromTransactionCheck) + .add("msgId", msgId) + .add("transactionId", transactionId) + .toString(); } } diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/EndTransactionResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/EndTransactionResponseHeader.java similarity index 95% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/EndTransactionResponseHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/EndTransactionResponseHeader.java index 1b01a6c65a9..f6aa51e877d 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/EndTransactionResponseHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/EndTransactionResponseHeader.java @@ -18,7 +18,7 @@ /** * $Id: EndTransactionResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.exception.RemotingCommandException; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ExchangeHAInfoRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ExchangeHAInfoRequestHeader.java new file mode 100644 index 00000000000..b636e36ec95 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ExchangeHAInfoRequestHeader.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNullable; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class ExchangeHAInfoRequestHeader implements CommandCustomHeader { + @CFNullable + public String masterHaAddress; + + @CFNullable + public Long masterFlushOffset; + + @CFNullable + public String masterAddress; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getMasterHaAddress() { + return masterHaAddress; + } + + public void setMasterHaAddress(String masterHaAddress) { + this.masterHaAddress = masterHaAddress; + } + + public Long getMasterFlushOffset() { + return masterFlushOffset; + } + + public void setMasterFlushOffset(Long masterFlushOffset) { + this.masterFlushOffset = masterFlushOffset; + } + + public String getMasterAddress() { + return masterAddress; + } + + public void setMasterAddress(String masterAddress) { + this.masterAddress = masterAddress; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ExchangeHAInfoResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ExchangeHAInfoResponseHeader.java new file mode 100644 index 00000000000..3bbbc4cc49a --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ExchangeHAInfoResponseHeader.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNullable; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class ExchangeHAInfoResponseHeader implements CommandCustomHeader { + @CFNullable + public String masterHaAddress; + + @CFNullable + public Long masterFlushOffset; + + @CFNullable + public String masterAddress; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getMasterHaAddress() { + return masterHaAddress; + } + + public void setMasterHaAddress(String masterHaAddress) { + this.masterHaAddress = masterHaAddress; + } + + public Long getMasterFlushOffset() { + return masterFlushOffset; + } + + public void setMasterFlushOffset(Long masterFlushOffset) { + this.masterFlushOffset = masterFlushOffset; + } + + public String getMasterAddress() { + return masterAddress; + } + + public void setMasterAddress(String masterAddress) { + this.masterAddress = masterAddress; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ExtraInfoUtil.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ExtraInfoUtil.java new file mode 100644 index 00000000000..a6a4a777675 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ExtraInfoUtil.java @@ -0,0 +1,322 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.rocketmq.common.KeyBuilder; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.message.MessageConst; + +public class ExtraInfoUtil { + private static final String NORMAL_TOPIC = "0"; + private static final String RETRY_TOPIC = "1"; + private static final String RETRY_TOPIC_V2 = "2"; + private static final String QUEUE_OFFSET = "qo"; + + public static String[] split(String extraInfo) { + if (extraInfo == null) { + throw new IllegalArgumentException("split extraInfo is null"); + } + return extraInfo.split(MessageConst.KEY_SEPARATOR); + } + + public static Long getCkQueueOffset(String[] extraInfoStrs) { + if (extraInfoStrs == null || extraInfoStrs.length < 1) { + throw new IllegalArgumentException("getCkQueueOffset fail, extraInfoStrs length " + (extraInfoStrs == null ? 0 : extraInfoStrs.length)); + } + return Long.valueOf(extraInfoStrs[0]); + } + + public static Long getPopTime(String[] extraInfoStrs) { + if (extraInfoStrs == null || extraInfoStrs.length < 2) { + throw new IllegalArgumentException("getPopTime fail, extraInfoStrs length " + (extraInfoStrs == null ? 0 : extraInfoStrs.length)); + } + return Long.valueOf(extraInfoStrs[1]); + } + + public static Long getInvisibleTime(String[] extraInfoStrs) { + if (extraInfoStrs == null || extraInfoStrs.length < 3) { + throw new IllegalArgumentException("getInvisibleTime fail, extraInfoStrs length " + (extraInfoStrs == null ? 0 : extraInfoStrs.length)); + } + return Long.valueOf(extraInfoStrs[2]); + } + + public static int getReviveQid(String[] extraInfoStrs) { + if (extraInfoStrs == null || extraInfoStrs.length < 4) { + throw new IllegalArgumentException("getReviveQid fail, extraInfoStrs length " + (extraInfoStrs == null ? 0 : extraInfoStrs.length)); + } + return Integer.parseInt(extraInfoStrs[3]); + } + + public static String getRealTopic(String[] extraInfoStrs, String topic, String cid) { + if (extraInfoStrs == null || extraInfoStrs.length < 5) { + throw new IllegalArgumentException("getRealTopic fail, extraInfoStrs length " + (extraInfoStrs == null ? 0 : extraInfoStrs.length)); + } + if (RETRY_TOPIC.equals(extraInfoStrs[4])) { + return KeyBuilder.buildPopRetryTopicV1(topic, cid); + } else if (RETRY_TOPIC_V2.equals(extraInfoStrs[4])) { + return KeyBuilder.buildPopRetryTopicV2(topic, cid); + } else { + return topic; + } + } + + public static String getRealTopic(String topic, String cid, String retry) { + if (retry.equals(NORMAL_TOPIC)) { + return topic; + } else if (retry.equals(RETRY_TOPIC)) { + return KeyBuilder.buildPopRetryTopicV1(topic, cid); + } else if (retry.equals(RETRY_TOPIC_V2)) { + return KeyBuilder.buildPopRetryTopicV2(topic, cid); + } else { + throw new IllegalArgumentException("getRetry fail, format is wrong"); + } + } + + public static String getRetry(String[] extraInfoStrs) { + if (extraInfoStrs == null || extraInfoStrs.length < 5) { + throw new IllegalArgumentException("getRetry fail, extraInfoStrs length " + (extraInfoStrs == null ? 0 : extraInfoStrs.length)); + } + return extraInfoStrs[4]; + } + + public static String getBrokerName(String[] extraInfoStrs) { + if (extraInfoStrs == null || extraInfoStrs.length < 6) { + throw new IllegalArgumentException("getBrokerName fail, extraInfoStrs length " + (extraInfoStrs == null ? 0 : extraInfoStrs.length)); + } + return extraInfoStrs[5]; + } + + public static int getQueueId(String[] extraInfoStrs) { + if (extraInfoStrs == null || extraInfoStrs.length < 7) { + throw new IllegalArgumentException("getQueueId fail, extraInfoStrs length " + (extraInfoStrs == null ? 0 : extraInfoStrs.length)); + } + return Integer.parseInt(extraInfoStrs[6]); + } + + public static long getQueueOffset(String[] extraInfoStrs) { + if (extraInfoStrs == null || extraInfoStrs.length < 8) { + throw new IllegalArgumentException("getQueueOffset fail, extraInfoStrs length " + (extraInfoStrs == null ? 0 : extraInfoStrs.length)); + } + return Long.parseLong(extraInfoStrs[7]); + } + + public static String buildExtraInfo(long ckQueueOffset, long popTime, long invisibleTime, int reviveQid, String topic, String brokerName, int queueId) { + String t = getRetry(topic); + return ckQueueOffset + MessageConst.KEY_SEPARATOR + popTime + MessageConst.KEY_SEPARATOR + invisibleTime + MessageConst.KEY_SEPARATOR + reviveQid + MessageConst.KEY_SEPARATOR + t + + MessageConst.KEY_SEPARATOR + brokerName + MessageConst.KEY_SEPARATOR + queueId; + } + + public static String buildExtraInfo(long ckQueueOffset, long popTime, long invisibleTime, int reviveQid, String topic, String brokerName, int queueId, + long msgQueueOffset) { + String t = getRetry(topic); + return ckQueueOffset + + MessageConst.KEY_SEPARATOR + popTime + MessageConst.KEY_SEPARATOR + invisibleTime + + MessageConst.KEY_SEPARATOR + reviveQid + MessageConst.KEY_SEPARATOR + t + + MessageConst.KEY_SEPARATOR + brokerName + MessageConst.KEY_SEPARATOR + queueId + + MessageConst.KEY_SEPARATOR + msgQueueOffset; + } + + public static void buildStartOffsetInfo(StringBuilder stringBuilder, String topic, int queueId, long startOffset) { + if (stringBuilder == null) { + stringBuilder = new StringBuilder(64); + } + + if (stringBuilder.length() > 0) { + stringBuilder.append(";"); + } + + stringBuilder.append(getRetry(topic)) + .append(MessageConst.KEY_SEPARATOR).append(queueId) + .append(MessageConst.KEY_SEPARATOR).append(startOffset); + } + + public static void buildQueueIdOrderCountInfo(StringBuilder stringBuilder, String topic, int queueId, int orderCount) { + if (stringBuilder == null) { + stringBuilder = new StringBuilder(64); + } + + if (stringBuilder.length() > 0) { + stringBuilder.append(";"); + } + + stringBuilder.append(getRetry(topic)) + .append(MessageConst.KEY_SEPARATOR).append(queueId) + .append(MessageConst.KEY_SEPARATOR).append(orderCount); + } + + public static void buildQueueOffsetOrderCountInfo(StringBuilder stringBuilder, String topic, long queueId, long queueOffset, int orderCount) { + if (stringBuilder == null) { + stringBuilder = new StringBuilder(64); + } + + if (stringBuilder.length() > 0) { + stringBuilder.append(";"); + } + + stringBuilder.append(getRetry(topic)) + .append(MessageConst.KEY_SEPARATOR).append(getQueueOffsetKeyValueKey(queueId, queueOffset)) + .append(MessageConst.KEY_SEPARATOR).append(orderCount); + } + + public static void buildMsgOffsetInfo(StringBuilder stringBuilder, String topic, int queueId, List msgOffsets) { + if (stringBuilder == null) { + stringBuilder = new StringBuilder(64); + } + + if (stringBuilder.length() > 0) { + stringBuilder.append(";"); + } + + stringBuilder.append(getRetry(topic)) + .append(MessageConst.KEY_SEPARATOR).append(queueId) + .append(MessageConst.KEY_SEPARATOR); + + for (int i = 0; i < msgOffsets.size(); i++) { + stringBuilder.append(msgOffsets.get(i)); + if (i < msgOffsets.size() - 1) { + stringBuilder.append(","); + } + } + } + + public static Map> parseMsgOffsetInfo(String msgOffsetInfo) { + if (msgOffsetInfo == null || msgOffsetInfo.length() == 0) { + return null; + } + + Map> msgOffsetMap = new HashMap<>(4); + String[] array; + if (msgOffsetInfo.indexOf(";") < 0) { + array = new String[]{msgOffsetInfo}; + } else { + array = msgOffsetInfo.split(";"); + } + + for (String one : array) { + String[] split = one.split(MessageConst.KEY_SEPARATOR); + if (split.length != 3) { + throw new IllegalArgumentException("parse msgOffsetMap error, " + msgOffsetMap); + } + String key = split[0] + "@" + split[1]; + if (msgOffsetMap.containsKey(key)) { + throw new IllegalArgumentException("parse msgOffsetMap error, duplicate, " + msgOffsetMap); + } + msgOffsetMap.put(key, new ArrayList<>(8)); + String[] msgOffsets = split[2].split(","); + for (String msgOffset : msgOffsets) { + msgOffsetMap.get(key).add(Long.valueOf(msgOffset)); + } + } + + return msgOffsetMap; + } + + public static Map parseStartOffsetInfo(String startOffsetInfo) { + if (startOffsetInfo == null || startOffsetInfo.length() == 0) { + return null; + } + Map startOffsetMap = new HashMap<>(4); + String[] array; + if (startOffsetInfo.indexOf(";") < 0) { + array = new String[]{startOffsetInfo}; + } else { + array = startOffsetInfo.split(";"); + } + + for (String one : array) { + String[] split = one.split(MessageConst.KEY_SEPARATOR); + if (split.length != 3) { + throw new IllegalArgumentException("parse startOffsetInfo error, " + startOffsetInfo); + } + String key = split[0] + "@" + split[1]; + if (startOffsetMap.containsKey(key)) { + throw new IllegalArgumentException("parse startOffsetInfo error, duplicate, " + startOffsetInfo); + } + startOffsetMap.put(key, Long.valueOf(split[2])); + } + + return startOffsetMap; + } + + public static Map parseOrderCountInfo(String orderCountInfo) { + if (orderCountInfo == null || orderCountInfo.length() == 0) { + return null; + } + Map startOffsetMap = new HashMap<>(4); + String[] array; + if (orderCountInfo.indexOf(";") < 0) { + array = new String[]{orderCountInfo}; + } else { + array = orderCountInfo.split(";"); + } + + for (String one : array) { + String[] split = one.split(MessageConst.KEY_SEPARATOR); + if (split.length != 3) { + throw new IllegalArgumentException("parse orderCountInfo error, " + orderCountInfo); + } + String key = split[0] + "@" + split[1]; + if (startOffsetMap.containsKey(key)) { + throw new IllegalArgumentException("parse orderCountInfo error, duplicate, " + orderCountInfo); + } + startOffsetMap.put(key, Integer.valueOf(split[2])); + } + + return startOffsetMap; + } + + public static String getStartOffsetInfoMapKey(String topic, long key) { + return getRetry(topic) + "@" + key; + } + + public static String getStartOffsetInfoMapKey(String topic, String popCk, long key) { + return getRetry(topic, popCk) + "@" + key; + } + + public static String getQueueOffsetKeyValueKey(long queueId, long queueOffset) { + return QUEUE_OFFSET + queueId + "%" + queueOffset; + } + + public static String getQueueOffsetMapKey(String topic, long queueId, long queueOffset) { + return getRetry(topic) + "@" + getQueueOffsetKeyValueKey(queueId, queueOffset); + } + + public static boolean isOrder(String[] extraInfo) { + return ExtraInfoUtil.getReviveQid(extraInfo) == KeyBuilder.POP_ORDER_REVIVE_QUEUE; + } + + private static String getRetry(String topic) { + String t = NORMAL_TOPIC; + if (KeyBuilder.isPopRetryTopicV2(topic)) { + t = RETRY_TOPIC_V2; + } else if (topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + t = RETRY_TOPIC; + } + return t; + } + + private static String getRetry(String topic, String popCk) { + if (popCk != null) { + return getRetry(split(popCk)); + } + return getRetry(topic); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllProducerInfoRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllProducerInfoRequestHeader.java new file mode 100644 index 00000000000..a24de24fd9d --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllProducerInfoRequestHeader.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class GetAllProducerInfoRequestHeader implements CommandCustomHeader { + @Override + public void checkFields() throws RemotingCommandException { + // To change body of implemented methods use File | Settings | File + // Templates. + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetAllTopicConfigResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllTopicConfigResponseHeader.java similarity index 95% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/GetAllTopicConfigResponseHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllTopicConfigResponseHeader.java index ea477e848ea..cd8da68c60c 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetAllTopicConfigResponseHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetAllTopicConfigResponseHeader.java @@ -18,7 +18,7 @@ /** * $Id: GetAllTopicConfigResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.exception.RemotingCommandException; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetBrokerAclConfigResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetBrokerAclConfigResponseHeader.java new file mode 100644 index 00000000000..50f5713b8b5 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetBrokerAclConfigResponseHeader.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class GetBrokerAclConfigResponseHeader implements CommandCustomHeader { + + @CFNotNull + private String version; + + private String allAclFileVersion; + + @CFNotNull + private String brokerName; + + @CFNotNull + private String brokerAddr; + + @CFNotNull + private String clusterName; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + public String getBrokerAddr() { + return brokerAddr; + } + + public void setBrokerAddr(String brokerAddr) { + this.brokerAddr = brokerAddr; + } + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public String getAllAclFileVersion() { + return allAclFileVersion; + } + + public void setAllAclFileVersion(String allAclFileVersion) { + this.allAclFileVersion = allAclFileVersion; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetBrokerConfigResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetBrokerConfigResponseHeader.java similarity index 96% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/GetBrokerConfigResponseHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetBrokerConfigResponseHeader.java index bc30342fccb..bcc6721ef82 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetBrokerConfigResponseHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetBrokerConfigResponseHeader.java @@ -18,7 +18,7 @@ /** * $Id: GetBrokerConfigResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetBrokerMemberGroupRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetBrokerMemberGroupRequestHeader.java new file mode 100644 index 00000000000..964025f5e6f --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetBrokerMemberGroupRequestHeader.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class GetBrokerMemberGroupRequestHeader implements CommandCustomHeader { + @CFNotNull + private String clusterName; + + @CFNotNull + private String brokerName; + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(final String clusterName) { + this.clusterName = clusterName; + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(final String brokerName) { + this.brokerName = brokerName; + } + + @Override + public void checkFields() throws RemotingCommandException { + + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetConsumeStatsInBrokerHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumeStatsInBrokerHeader.java similarity index 96% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/GetConsumeStatsInBrokerHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumeStatsInBrokerHeader.java index b18e8c52e61..156f6dbefd0 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetConsumeStatsInBrokerHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumeStatsInBrokerHeader.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetConsumeStatsRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumeStatsRequestHeader.java similarity index 76% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/GetConsumeStatsRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumeStatsRequestHeader.java index 6ba069e1ffd..474811b3bd5 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetConsumeStatsRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumeStatsRequestHeader.java @@ -14,13 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.remoting.CommandCustomHeader; +import com.google.common.base.MoreObjects; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; -public class GetConsumeStatsRequestHeader implements CommandCustomHeader { +public class GetConsumeStatsRequestHeader extends TopicRequestHeader { @CFNotNull private String consumerGroup; private String topic; @@ -44,4 +45,12 @@ public String getTopic() { public void setTopic(String topic) { this.topic = topic; } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("consumerGroup", consumerGroup) + .add("topic", topic) + .toString(); + } } diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetConsumerConnectionListRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerConnectionListRequestHeader.java similarity index 87% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/GetConsumerConnectionListRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerConnectionListRequestHeader.java index 8f7579660f7..b572c82f35b 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetConsumerConnectionListRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerConnectionListRequestHeader.java @@ -15,13 +15,13 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; -public class GetConsumerConnectionListRequestHeader implements CommandCustomHeader { +public class GetConsumerConnectionListRequestHeader extends RpcRequestHeader { @CFNotNull private String consumerGroup; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetConsumerListByGroupRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerListByGroupRequestHeader.java similarity index 75% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/GetConsumerListByGroupRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerListByGroupRequestHeader.java index 3523a52caee..43161ef3627 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetConsumerListByGroupRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerListByGroupRequestHeader.java @@ -15,13 +15,14 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.remoting.CommandCustomHeader; +import com.google.common.base.MoreObjects; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; -public class GetConsumerListByGroupRequestHeader implements CommandCustomHeader { +public class GetConsumerListByGroupRequestHeader extends RpcRequestHeader { @CFNotNull private String consumerGroup; @@ -36,4 +37,11 @@ public String getConsumerGroup() { public void setConsumerGroup(String consumerGroup) { this.consumerGroup = consumerGroup; } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("consumerGroup", consumerGroup) + .toString(); + } } diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetConsumerListByGroupResponseBody.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerListByGroupResponseBody.java similarity index 95% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/GetConsumerListByGroupResponseBody.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerListByGroupResponseBody.java index da39e7725e0..545ea12f752 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetConsumerListByGroupResponseBody.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerListByGroupResponseBody.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; import java.util.List; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetConsumerListByGroupResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerListByGroupResponseHeader.java similarity index 95% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/GetConsumerListByGroupResponseHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerListByGroupResponseHeader.java index 2eb41031fff..42ca5f1fd50 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetConsumerListByGroupResponseHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerListByGroupResponseHeader.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.exception.RemotingCommandException; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetConsumerRunningInfoRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerRunningInfoRequestHeader.java similarity index 78% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/GetConsumerRunningInfoRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerRunningInfoRequestHeader.java index 1bbbd900c53..c67e9a15eb3 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetConsumerRunningInfoRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerRunningInfoRequestHeader.java @@ -15,14 +15,15 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.remoting.CommandCustomHeader; +import com.google.common.base.MoreObjects; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.annotation.CFNullable; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; -public class GetConsumerRunningInfoRequestHeader implements CommandCustomHeader { +public class GetConsumerRunningInfoRequestHeader extends RpcRequestHeader { @CFNotNull private String consumerGroup; @CFNotNull @@ -57,4 +58,13 @@ public boolean isJstackEnable() { public void setJstackEnable(boolean jstackEnable) { this.jstackEnable = jstackEnable; } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("consumerGroup", consumerGroup) + .add("clientId", clientId) + .add("jstackEnable", jstackEnable) + .toString(); + } } diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetConsumerStatusRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerStatusRequestHeader.java similarity index 78% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/GetConsumerStatusRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerStatusRequestHeader.java index ca26a869c60..de9115a0142 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetConsumerStatusRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetConsumerStatusRequestHeader.java @@ -15,14 +15,15 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.remoting.CommandCustomHeader; +import com.google.common.base.MoreObjects; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.annotation.CFNullable; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; -public class GetConsumerStatusRequestHeader implements CommandCustomHeader { +public class GetConsumerStatusRequestHeader extends TopicRequestHeader { @CFNotNull private String topic; @CFNotNull @@ -57,4 +58,13 @@ public String getClientAddr() { public void setClientAddr(String clientAddr) { this.clientAddr = clientAddr; } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("topic", topic) + .add("group", group) + .add("clientAddr", clientAddr) + .toString(); + } } diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetEarliestMsgStoretimeRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetEarliestMsgStoretimeRequestHeader.java similarity index 85% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/GetEarliestMsgStoretimeRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetEarliestMsgStoretimeRequestHeader.java index c64381fb787..b6a3a2d47dd 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetEarliestMsgStoretimeRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetEarliestMsgStoretimeRequestHeader.java @@ -18,13 +18,13 @@ /** * $Id: GetEarliestMsgStoretimeRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; -public class GetEarliestMsgStoretimeRequestHeader implements CommandCustomHeader { +public class GetEarliestMsgStoretimeRequestHeader extends TopicQueueRequestHeader { @CFNotNull private String topic; @CFNotNull @@ -34,18 +34,22 @@ public class GetEarliestMsgStoretimeRequestHeader implements CommandCustomHeader public void checkFields() throws RemotingCommandException { } + @Override public String getTopic() { return topic; } + @Override public void setTopic(String topic) { this.topic = topic; } + @Override public Integer getQueueId() { return queueId; } + @Override public void setQueueId(Integer queueId) { this.queueId = queueId; } diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetEarliestMsgStoretimeResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetEarliestMsgStoretimeResponseHeader.java similarity index 96% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/GetEarliestMsgStoretimeResponseHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetEarliestMsgStoretimeResponseHeader.java index 6b9b3b21d20..94983527a26 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetEarliestMsgStoretimeResponseHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetEarliestMsgStoretimeResponseHeader.java @@ -18,7 +18,7 @@ /** * $Id: GetEarliestMsgStoretimeResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMaxOffsetRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMaxOffsetRequestHeader.java new file mode 100644 index 00000000000..ec4219874a6 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMaxOffsetRequestHeader.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * $Id: GetMaxOffsetRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.annotation.CFNullable; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +public class GetMaxOffsetRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + private String topic; + @CFNotNull + private Integer queueId; + + /** + * A message at committed offset has been dispatched from Topic to MessageQueue, so it can be consumed immediately, + * while a message at inflight offset is not visible for a consumer temporarily. + * Set this flag true if the max committed offset is needed, or false if the max inflight offset is preferred. + * The default value is true. + */ + @CFNullable + private boolean committed = true; + + @Override + public void checkFields() throws RemotingCommandException { + } + + @Override + public String getTopic() { + return topic; + } + + @Override + public void setTopic(String topic) { + this.topic = topic; + } + + @Override + public Integer getQueueId() { + return queueId; + } + + @Override + public void setQueueId(Integer queueId) { + this.queueId = queueId; + } + + public boolean isCommitted() { + return committed; + } + + public void setCommitted(final boolean committed) { + this.committed = committed; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("topic", topic) + .add("queueId", queueId) + .add("committed", committed) + .toString(); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetMaxOffsetResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMaxOffsetResponseHeader.java similarity index 96% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/GetMaxOffsetResponseHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMaxOffsetResponseHeader.java index fcd0a302fd8..1e2f6872063 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetMaxOffsetResponseHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMaxOffsetResponseHeader.java @@ -18,7 +18,7 @@ /** * $Id: GetMaxOffsetResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetMinOffsetRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMinOffsetRequestHeader.java similarity index 75% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/GetMinOffsetRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMinOffsetRequestHeader.java index 6fb8ed40c45..11087e6730f 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetMinOffsetRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMinOffsetRequestHeader.java @@ -18,13 +18,14 @@ /** * $Id: GetMinOffsetRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.remoting.CommandCustomHeader; +import com.google.common.base.MoreObjects; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; -public class GetMinOffsetRequestHeader implements CommandCustomHeader { +public class GetMinOffsetRequestHeader extends TopicQueueRequestHeader { @CFNotNull private String topic; @CFNotNull @@ -34,19 +35,31 @@ public class GetMinOffsetRequestHeader implements CommandCustomHeader { public void checkFields() throws RemotingCommandException { } + @Override public String getTopic() { return topic; } + @Override public void setTopic(String topic) { this.topic = topic; } + @Override public Integer getQueueId() { return queueId; } + @Override public void setQueueId(Integer queueId) { this.queueId = queueId; } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("topic", topic) + .add("queueId", queueId) + .toString(); + } } diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetMinOffsetResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMinOffsetResponseHeader.java similarity index 96% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/GetMinOffsetResponseHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMinOffsetResponseHeader.java index 6fc0fac3b05..b4e1ae92ac5 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetMinOffsetResponseHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetMinOffsetResponseHeader.java @@ -18,7 +18,7 @@ /** * $Id: GetMinOffsetResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetProducerConnectionListRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetProducerConnectionListRequestHeader.java similarity index 87% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/GetProducerConnectionListRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetProducerConnectionListRequestHeader.java index 880c4e41c38..701335191d8 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetProducerConnectionListRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetProducerConnectionListRequestHeader.java @@ -15,13 +15,13 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; -public class GetProducerConnectionListRequestHeader implements CommandCustomHeader { +public class GetProducerConnectionListRequestHeader extends RpcRequestHeader { @CFNotNull private String producerGroup; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetSubscriptionGroupConfigRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetSubscriptionGroupConfigRequestHeader.java new file mode 100644 index 00000000000..2ad06b4966e --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetSubscriptionGroupConfigRequestHeader.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * $Id: GetAllTopicConfigResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; + +public class GetSubscriptionGroupConfigRequestHeader extends RpcRequestHeader { + + @Override + public void checkFields() throws RemotingCommandException { + } + + @CFNotNull + private String group; + + /** + * @return the group + */ + public String getGroup() { + return group; + } + + /** + * @param group the group to set + */ + public void setGroup(String group) { + this.group = group; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetTopicConfigRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetTopicConfigRequestHeader.java new file mode 100644 index 00000000000..69b07f188fc --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetTopicConfigRequestHeader.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; + +public class GetTopicConfigRequestHeader extends TopicRequestHeader { + @Override + public void checkFields() throws RemotingCommandException { + } + + @CFNotNull + private String topic; + + + /** + * @return the topic + */ + public String getTopic() { + return topic; + } + + /** + * @param topic the topic to set + */ + public void setTopic(String topic) { + this.topic = topic; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetTopicStatsInfoRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetTopicStatsInfoRequestHeader.java similarity index 86% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/GetTopicStatsInfoRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetTopicStatsInfoRequestHeader.java index c4cf4decdcb..7d1e8d6b7d9 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetTopicStatsInfoRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetTopicStatsInfoRequestHeader.java @@ -15,13 +15,13 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; -public class GetTopicStatsInfoRequestHeader implements CommandCustomHeader { +public class GetTopicStatsInfoRequestHeader extends TopicRequestHeader { @CFNotNull private String topic; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetTopicsByClusterRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetTopicsByClusterRequestHeader.java similarity index 96% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/GetTopicsByClusterRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetTopicsByClusterRequestHeader.java index 9955e71ed6c..08af6f875c9 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/GetTopicsByClusterRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/GetTopicsByClusterRequestHeader.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/HeartbeatRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/HeartbeatRequestHeader.java new file mode 100644 index 00000000000..a37383f3a18 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/HeartbeatRequestHeader.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; + +public class HeartbeatRequestHeader extends RpcRequestHeader { + // for namespace + @Override + public void checkFields() throws RemotingCommandException { + + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/InitConsumerOffsetRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/InitConsumerOffsetRequestHeader.java new file mode 100644 index 00000000000..ac63b974b93 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/InitConsumerOffsetRequestHeader.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; + +public class InitConsumerOffsetRequestHeader extends TopicRequestHeader { + + private String topic; + // @see ConsumeInitMode + private int initMode; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public int getInitMode() { + return initMode; + } + + public void setInitMode(int initMode) { + this.initMode = initMode; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/LockBatchMqRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/LockBatchMqRequestHeader.java new file mode 100644 index 00000000000..3484fa7d3e7 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/LockBatchMqRequestHeader.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; + +public class LockBatchMqRequestHeader extends RpcRequestHeader { + @Override + public void checkFields() throws RemotingCommandException { + + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/NotificationRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/NotificationRequestHeader.java new file mode 100644 index 00000000000..2ccf564df56 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/NotificationRequestHeader.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + + +public class NotificationRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + private String consumerGroup; + @CFNotNull + private String topic; + @CFNotNull + private int queueId; + @CFNotNull + private long pollTime; + @CFNotNull + private long bornTime; + + private Boolean order = Boolean.FALSE; + private String attemptId; + + @CFNotNull + @Override + public void checkFields() throws RemotingCommandException { + } + + public long getPollTime() { + return pollTime; + } + + public void setPollTime(long pollTime) { + this.pollTime = pollTime; + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public long getBornTime() { + return bornTime; + } + + public void setBornTime(long bornTime) { + this.bornTime = bornTime; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public Integer getQueueId() { + if (queueId < 0) { + return -1; + } + return queueId; + } + + public void setQueueId(Integer queueId) { + this.queueId = queueId; + } + + public Boolean getOrder() { + return order; + } + + public void setOrder(Boolean order) { + this.order = order; + } + + public String getAttemptId() { + return attemptId; + } + + public void setAttemptId(String attemptId) { + this.attemptId = attemptId; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("consumerGroup", consumerGroup) + .add("topic", topic) + .add("queueId", queueId) + .add("pollTime", pollTime) + .add("bornTime", bornTime) + .add("order", order) + .add("attemptId", attemptId) + .toString(); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/NotificationResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/NotificationResponseHeader.java new file mode 100644 index 00000000000..cbab5974015 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/NotificationResponseHeader.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class NotificationResponseHeader implements CommandCustomHeader { + + + @CFNotNull + private boolean hasMsg = false; + + public boolean isHasMsg() { + return hasMsg; + } + + public void setHasMsg(boolean hasMsg) { + this.hasMsg = hasMsg; + } + + @Override + public void checkFields() throws RemotingCommandException { + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/NotifyBrokerRoleChangedRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/NotifyBrokerRoleChangedRequestHeader.java new file mode 100644 index 00000000000..3a112a57824 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/NotifyBrokerRoleChangedRequestHeader.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class NotifyBrokerRoleChangedRequestHeader implements CommandCustomHeader { + private String masterAddress; + private Integer masterEpoch; + private Integer syncStateSetEpoch; + private Long masterBrokerId; + + public NotifyBrokerRoleChangedRequestHeader() { + } + + public NotifyBrokerRoleChangedRequestHeader(String masterAddress, Long masterBrokerId, Integer masterEpoch, Integer syncStateSetEpoch) { + this.masterAddress = masterAddress; + this.masterEpoch = masterEpoch; + this.syncStateSetEpoch = syncStateSetEpoch; + this.masterBrokerId = masterBrokerId; + } + + public String getMasterAddress() { + return masterAddress; + } + + public void setMasterAddress(String masterAddress) { + this.masterAddress = masterAddress; + } + + public Integer getMasterEpoch() { + return masterEpoch; + } + + public void setMasterEpoch(Integer masterEpoch) { + this.masterEpoch = masterEpoch; + } + + public Integer getSyncStateSetEpoch() { + return syncStateSetEpoch; + } + + public void setSyncStateSetEpoch(Integer syncStateSetEpoch) { + this.syncStateSetEpoch = syncStateSetEpoch; + } + + public Long getMasterBrokerId() { + return masterBrokerId; + } + + public void setMasterBrokerId(Long masterBrokerId) { + this.masterBrokerId = masterBrokerId; + } + + @Override + public String toString() { + return "NotifyBrokerRoleChangedRequestHeader{" + + "masterAddress='" + masterAddress + '\'' + + ", masterEpoch=" + masterEpoch + + ", syncStateSetEpoch=" + syncStateSetEpoch + + ", masterBrokerId=" + masterBrokerId + + '}'; + } + + @Override + public void checkFields() throws RemotingCommandException { + + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/NotifyConsumerIdsChangedRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/NotifyConsumerIdsChangedRequestHeader.java similarity index 86% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/NotifyConsumerIdsChangedRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/NotifyConsumerIdsChangedRequestHeader.java index b9c1a176537..38271f97549 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/NotifyConsumerIdsChangedRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/NotifyConsumerIdsChangedRequestHeader.java @@ -15,13 +15,13 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; -public class NotifyConsumerIdsChangedRequestHeader implements CommandCustomHeader { +public class NotifyConsumerIdsChangedRequestHeader extends RpcRequestHeader { @CFNotNull private String consumerGroup; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/NotifyMinBrokerIdChangeRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/NotifyMinBrokerIdChangeRequestHeader.java new file mode 100644 index 00000000000..2b02006abcb --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/NotifyMinBrokerIdChangeRequestHeader.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNullable; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class NotifyMinBrokerIdChangeRequestHeader implements CommandCustomHeader { + @CFNullable + private Long minBrokerId; + + @CFNullable + private String brokerName; + + @CFNullable + private String minBrokerAddr; + + @CFNullable + private String offlineBrokerAddr; + + @CFNullable + private String haBrokerAddr; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public Long getMinBrokerId() { + return minBrokerId; + } + + public void setMinBrokerId(Long minBrokerId) { + this.minBrokerId = minBrokerId; + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + public String getMinBrokerAddr() { + return minBrokerAddr; + } + + public void setMinBrokerAddr(String minBrokerAddr) { + this.minBrokerAddr = minBrokerAddr; + } + + public String getOfflineBrokerAddr() { + return offlineBrokerAddr; + } + + public void setOfflineBrokerAddr(String offlineBrokerAddr) { + this.offlineBrokerAddr = offlineBrokerAddr; + } + + public String getHaBrokerAddr() { + return haBrokerAddr; + } + + public void setHaBrokerAddr(String haBrokerAddr) { + this.haBrokerAddr = haBrokerAddr; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PeekMessageRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PeekMessageRequestHeader.java new file mode 100644 index 00000000000..a6f23dc2ee0 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PeekMessageRequestHeader.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +public class PeekMessageRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + private String topic; + @CFNotNull + private int queueId; + @CFNotNull + private int maxMsgNums; + @CFNotNull + private String consumerGroup; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public Integer getQueueId() { + return queueId; + } + + public void setQueueId(Integer queueId) { + this.queueId = queueId; + } + + + public int getMaxMsgNums() { + return maxMsgNums; + } + + public void setMaxMsgNums(int maxMsgNums) { + this.maxMsgNums = maxMsgNums; + } + +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PollingInfoRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PollingInfoRequestHeader.java new file mode 100644 index 00000000000..558fb3f5061 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PollingInfoRequestHeader.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + + +public class PollingInfoRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + private String consumerGroup; + @CFNotNull + private String topic; + @CFNotNull + private int queueId; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public Integer getQueueId() { + if (queueId < 0) { + return -1; + } + return queueId; + } + + public void setQueueId(Integer queueId) { + this.queueId = queueId; + } + +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PollingInfoResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PollingInfoResponseHeader.java new file mode 100644 index 00000000000..7d2d852dfb1 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PollingInfoResponseHeader.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class PollingInfoResponseHeader implements CommandCustomHeader { + + + @CFNotNull + private int pollingNum; + + public int getPollingNum() { + return pollingNum; + } + + public void setPollingNum(int pollingNum) { + this.pollingNum = pollingNum; + } + + @Override + public void checkFields() throws RemotingCommandException { + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PopMessageRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PopMessageRequestHeader.java new file mode 100644 index 00000000000..34b97987ddd --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PopMessageRequestHeader.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +public class PopMessageRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + private String consumerGroup; + @CFNotNull + private String topic; + @CFNotNull + private int queueId; + @CFNotNull + private int maxMsgNums; + @CFNotNull + private long invisibleTime; + @CFNotNull + private long pollTime; + @CFNotNull + private long bornTime; + @CFNotNull + private int initMode; + + private String expType; + private String exp; + + /** + * marked as order consume, if true + * 1. not commit offset + * 2. not pop retry, because no retry + * 3. not append check point, because no retry + */ + private Boolean order = Boolean.FALSE; + + private String attemptId; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public void setInitMode(int initMode) { + this.initMode = initMode; + } + + public int getInitMode() { + return initMode; + } + + public long getInvisibleTime() { + return invisibleTime; + } + + public void setInvisibleTime(long invisibleTime) { + this.invisibleTime = invisibleTime; + } + + public long getPollTime() { + return pollTime; + } + + public void setPollTime(long pollTime) { + this.pollTime = pollTime; + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public long getBornTime() { + return bornTime; + } + + public void setBornTime(long bornTime) { + this.bornTime = bornTime; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public Integer getQueueId() { + if (queueId < 0) { + return -1; + } + return queueId; + } + + @Override + public void setQueueId(Integer queueId) { + this.queueId = queueId; + } + + public int getMaxMsgNums() { + return maxMsgNums; + } + + public void setMaxMsgNums(int maxMsgNums) { + this.maxMsgNums = maxMsgNums; + } + + public boolean isTimeoutTooMuch() { + return System.currentTimeMillis() - bornTime - pollTime > 500; + } + + public String getExpType() { + return expType; + } + + public void setExpType(String expType) { + this.expType = expType; + } + + public String getExp() { + return exp; + } + + public void setExp(String exp) { + this.exp = exp; + } + + public Boolean getOrder() { + return order; + } + + public void setOrder(Boolean order) { + this.order = order; + } + + public boolean isOrder() { + return this.order != null && this.order.booleanValue(); + } + + public String getAttemptId() { + return attemptId; + } + + public void setAttemptId(String attemptId) { + this.attemptId = attemptId; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("consumerGroup", consumerGroup) + .add("topic", topic) + .add("queueId", queueId) + .add("maxMsgNums", maxMsgNums) + .add("invisibleTime", invisibleTime) + .add("pollTime", pollTime) + .add("bornTime", bornTime) + .add("initMode", initMode) + .add("expType", expType) + .add("exp", exp) + .add("order", order) + .add("attemptId", attemptId) + .toString(); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PopMessageResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PopMessageResponseHeader.java new file mode 100644 index 00000000000..da17733abe1 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PopMessageResponseHeader.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class PopMessageResponseHeader implements CommandCustomHeader { + + + @CFNotNull + private long popTime; + @CFNotNull + private long invisibleTime; + + @CFNotNull + private int reviveQid; + /** + * the rest num in queue + */ + @CFNotNull + private long restNum; + + private String startOffsetInfo; + private String msgOffsetInfo; + private String orderCountInfo; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public long getPopTime() { + return popTime; + } + + public void setPopTime(long popTime) { + this.popTime = popTime; + } + + public long getInvisibleTime() { + return invisibleTime; + } + + public long getRestNum() { + return restNum; + } + + public void setRestNum(long restNum) { + this.restNum = restNum; + } + + public void setInvisibleTime(long invisibleTime) { + this.invisibleTime = invisibleTime; + } + + public int getReviveQid() { + return reviveQid; + } + + public void setReviveQid(int reviveQid) { + this.reviveQid = reviveQid; + } + + public String getStartOffsetInfo() { + return startOffsetInfo; + } + + public void setStartOffsetInfo(String startOffsetInfo) { + this.startOffsetInfo = startOffsetInfo; + } + + public String getMsgOffsetInfo() { + return msgOffsetInfo; + } + + public void setMsgOffsetInfo(String msgOffsetInfo) { + this.msgOffsetInfo = msgOffsetInfo; + } + + public String getOrderCountInfo() { + return orderCountInfo; + } + + public void setOrderCountInfo(String orderCountInfo) { + this.orderCountInfo = orderCountInfo; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PullMessageRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PullMessageRequestHeader.java new file mode 100644 index 00000000000..a6d6d3b64c3 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PullMessageRequestHeader.java @@ -0,0 +1,329 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * $Id: PullMessageRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import com.google.common.base.MoreObjects; +import io.netty.buffer.ByteBuf; +import java.util.HashMap; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.annotation.CFNullable; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.FastCodesHeader; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +public class PullMessageRequestHeader extends TopicQueueRequestHeader implements FastCodesHeader { + + @CFNotNull + private String consumerGroup; + @CFNotNull + private String topic; + @CFNotNull + private Integer queueId; + @CFNotNull + private Long queueOffset; + @CFNotNull + private Integer maxMsgNums; + @CFNotNull + private Integer sysFlag; + @CFNotNull + private Long commitOffset; + @CFNotNull + private Long suspendTimeoutMillis; + @CFNullable + private String subscription; + @CFNotNull + private Long subVersion; + private String expressionType; + + @CFNullable + private Integer maxMsgBytes; + + /** + * mark the source of this pull request + */ + private Integer requestSource; + + /** + * the real clientId when request from proxy + */ + private String proxyFrowardClientId; + + @Override + public void checkFields() throws RemotingCommandException { + } + + @Override + public void encode(ByteBuf out) { + writeIfNotNull(out, "consumerGroup", consumerGroup); + writeIfNotNull(out, "topic", topic); + writeIfNotNull(out, "queueId", queueId); + writeIfNotNull(out, "queueOffset", queueOffset); + writeIfNotNull(out, "maxMsgNums", maxMsgNums); + writeIfNotNull(out, "sysFlag", sysFlag); + writeIfNotNull(out, "commitOffset", commitOffset); + writeIfNotNull(out, "suspendTimeoutMillis", suspendTimeoutMillis); + writeIfNotNull(out, "subscription", subscription); + writeIfNotNull(out, "subVersion", subVersion); + writeIfNotNull(out, "expressionType", expressionType); + writeIfNotNull(out, "maxMsgBytes", maxMsgBytes); + writeIfNotNull(out, "requestSource", requestSource); + writeIfNotNull(out, "proxyFrowardClientId", proxyFrowardClientId); + writeIfNotNull(out, "lo", lo); + writeIfNotNull(out, "ns", ns); + writeIfNotNull(out, "nsd", nsd); + writeIfNotNull(out, "bname", bname); + writeIfNotNull(out, "oway", oway); + } + + @Override + public void decode(HashMap fields) throws RemotingCommandException { + String str = getAndCheckNotNull(fields, "consumerGroup"); + if (str != null) { + this.consumerGroup = str; + } + + str = getAndCheckNotNull(fields, "topic"); + if (str != null) { + this.topic = str; + } + + str = getAndCheckNotNull(fields, "queueId"); + if (str != null) { + this.queueId = Integer.parseInt(str); + } + + str = getAndCheckNotNull(fields, "queueOffset"); + if (str != null) { + this.queueOffset = Long.parseLong(str); + } + + str = getAndCheckNotNull(fields, "maxMsgNums"); + if (str != null) { + this.maxMsgNums = Integer.parseInt(str); + } + + str = getAndCheckNotNull(fields, "sysFlag"); + if (str != null) { + this.sysFlag = Integer.parseInt(str); + } + + str = getAndCheckNotNull(fields, "commitOffset"); + if (str != null) { + this.commitOffset = Long.parseLong(str); + } + + str = getAndCheckNotNull(fields, "suspendTimeoutMillis"); + if (str != null) { + this.suspendTimeoutMillis = Long.parseLong(str); + } + + str = fields.get("subscription"); + if (str != null) { + this.subscription = str; + } + + str = getAndCheckNotNull(fields, "subVersion"); + if (str != null) { + this.subVersion = Long.parseLong(str); + } + + str = fields.get("expressionType"); + if (str != null) { + this.expressionType = str; + } + + str = fields.get("maxMsgBytes"); + if (str != null) { + this.maxMsgBytes = Integer.parseInt(str); + } + + str = fields.get("requestSource"); + if (str != null) { + this.requestSource = Integer.parseInt(str); + } + + str = fields.get("proxyFrowardClientId"); + if (str != null) { + this.proxyFrowardClientId = str; + } + + str = fields.get("lo"); + if (str != null) { + this.lo = Boolean.parseBoolean(str); + } + + str = fields.get("ns"); + if (str != null) { + this.ns = str; + } + + str = fields.get("nsd"); + if (str != null) { + this.nsd = Boolean.parseBoolean(str); + } + + str = fields.get("bname"); + if (str != null) { + this.bname = str; + } + + str = fields.get("oway"); + if (str != null) { + this.oway = Boolean.parseBoolean(str); + } + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + @Override + public String getTopic() { + return topic; + } + + @Override + public void setTopic(String topic) { + this.topic = topic; + } + + @Override + public Integer getQueueId() { + return queueId; + } + + @Override + public void setQueueId(Integer queueId) { + this.queueId = queueId; + } + + public Long getQueueOffset() { + return queueOffset; + } + + public void setQueueOffset(Long queueOffset) { + this.queueOffset = queueOffset; + } + + public Integer getMaxMsgNums() { + return maxMsgNums; + } + + public void setMaxMsgNums(Integer maxMsgNums) { + this.maxMsgNums = maxMsgNums; + } + + public Integer getSysFlag() { + return sysFlag; + } + + public void setSysFlag(Integer sysFlag) { + this.sysFlag = sysFlag; + } + + public Long getCommitOffset() { + return commitOffset; + } + + public void setCommitOffset(Long commitOffset) { + this.commitOffset = commitOffset; + } + + public Long getSuspendTimeoutMillis() { + return suspendTimeoutMillis; + } + + public void setSuspendTimeoutMillis(Long suspendTimeoutMillis) { + this.suspendTimeoutMillis = suspendTimeoutMillis; + } + + public String getSubscription() { + return subscription; + } + + public void setSubscription(String subscription) { + this.subscription = subscription; + } + + public Long getSubVersion() { + return subVersion; + } + + public void setSubVersion(Long subVersion) { + this.subVersion = subVersion; + } + + public String getExpressionType() { + return expressionType; + } + + public void setExpressionType(String expressionType) { + this.expressionType = expressionType; + } + + public Integer getMaxMsgBytes() { + return maxMsgBytes; + } + + public void setMaxMsgBytes(Integer maxMsgBytes) { + this.maxMsgBytes = maxMsgBytes; + } + + public Integer getRequestSource() { + return requestSource; + } + + public void setRequestSource(Integer requestSource) { + this.requestSource = requestSource; + } + + public String getProxyFrowardClientId() { + return proxyFrowardClientId; + } + + public void setProxyFrowardClientId(String proxyFrowardClientId) { + this.proxyFrowardClientId = proxyFrowardClientId; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("consumerGroup", consumerGroup) + .add("topic", topic) + .add("queueId", queueId) + .add("queueOffset", queueOffset) + .add("maxMsgBytes", maxMsgBytes) + .add("maxMsgNums", maxMsgNums) + .add("sysFlag", sysFlag) + .add("commitOffset", commitOffset) + .add("suspendTimeoutMillis", suspendTimeoutMillis) + .add("subscription", subscription) + .add("subVersion", subVersion) + .add("expressionType", expressionType) + .add("requestSource", requestSource) + .add("proxyFrowardClientId", proxyFrowardClientId) + .toString(); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PullMessageResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PullMessageResponseHeader.java new file mode 100644 index 00000000000..bc356f2e9e1 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/PullMessageResponseHeader.java @@ -0,0 +1,172 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * $Id: PullMessageResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import io.netty.buffer.ByteBuf; +import java.util.HashMap; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.annotation.CFNullable; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.FastCodesHeader; + +public class PullMessageResponseHeader implements CommandCustomHeader, FastCodesHeader { + @CFNotNull + private Long suggestWhichBrokerId; + @CFNotNull + private Long nextBeginOffset; + @CFNotNull + private Long minOffset; + @CFNotNull + private Long maxOffset; + @CFNullable + private Long offsetDelta; + @CFNullable + private Integer topicSysFlag; + @CFNullable + private Integer groupSysFlag; + @CFNullable + private Integer forbiddenType; + + @Override + public void checkFields() throws RemotingCommandException { + } + + @Override + public void encode(ByteBuf out) { + writeIfNotNull(out, "suggestWhichBrokerId", suggestWhichBrokerId); + writeIfNotNull(out, "nextBeginOffset", nextBeginOffset); + writeIfNotNull(out, "minOffset", minOffset); + writeIfNotNull(out, "maxOffset", maxOffset); + writeIfNotNull(out, "offsetDelta", offsetDelta); + writeIfNotNull(out, "topicSysFlag", topicSysFlag); + writeIfNotNull(out, "groupSysFlag", groupSysFlag); + writeIfNotNull(out, "forbiddenType", forbiddenType); + } + + @Override + public void decode(HashMap fields) throws RemotingCommandException { + String str = getAndCheckNotNull(fields, "suggestWhichBrokerId"); + if (str != null) { + this.suggestWhichBrokerId = Long.parseLong(str); + } + + str = getAndCheckNotNull(fields, "nextBeginOffset"); + if (str != null) { + this.nextBeginOffset = Long.parseLong(str); + } + + str = getAndCheckNotNull(fields, "minOffset"); + if (str != null) { + this.minOffset = Long.parseLong(str); + } + + str = getAndCheckNotNull(fields, "maxOffset"); + if (str != null) { + this.maxOffset = Long.parseLong(str); + } + + str = fields.get("offsetDelta"); + if (str != null) { + this.offsetDelta = Long.parseLong(str); + } + + str = fields.get("topicSysFlag"); + if (str != null) { + this.topicSysFlag = Integer.parseInt(str); + } + + str = fields.get("groupSysFlag"); + if (str != null) { + this.groupSysFlag = Integer.parseInt(str); + } + + str = fields.get("forbiddenType"); + if (str != null) { + this.forbiddenType = Integer.parseInt(str); + } + + } + + public Long getNextBeginOffset() { + return nextBeginOffset; + } + + public void setNextBeginOffset(Long nextBeginOffset) { + this.nextBeginOffset = nextBeginOffset; + } + + public Long getMinOffset() { + return minOffset; + } + + public void setMinOffset(Long minOffset) { + this.minOffset = minOffset; + } + + public Long getMaxOffset() { + return maxOffset; + } + + public void setMaxOffset(Long maxOffset) { + this.maxOffset = maxOffset; + } + + public Long getSuggestWhichBrokerId() { + return suggestWhichBrokerId; + } + + public void setSuggestWhichBrokerId(Long suggestWhichBrokerId) { + this.suggestWhichBrokerId = suggestWhichBrokerId; + } + + public Integer getTopicSysFlag() { + return topicSysFlag; + } + + public void setTopicSysFlag(Integer topicSysFlag) { + this.topicSysFlag = topicSysFlag; + } + + public Integer getGroupSysFlag() { + return groupSysFlag; + } + + public void setGroupSysFlag(Integer groupSysFlag) { + this.groupSysFlag = groupSysFlag; + } + + public Integer getForbiddenType() { + return forbiddenType; + } + + public void setForbiddenType(Integer forbiddenType) { + this.forbiddenType = forbiddenType; + } + + public Long getOffsetDelta() { + return offsetDelta; + } + + public void setOffsetDelta(Long offsetDelta) { + this.offsetDelta = offsetDelta; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/QueryConsumeQueueRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryConsumeQueueRequestHeader.java similarity index 86% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/QueryConsumeQueueRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryConsumeQueueRequestHeader.java index 642fe17cb63..a1b45e9b26c 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/QueryConsumeQueueRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryConsumeQueueRequestHeader.java @@ -15,12 +15,12 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; -public class QueryConsumeQueueRequestHeader implements CommandCustomHeader { +public class QueryConsumeQueueRequestHeader extends TopicQueueRequestHeader { private String topic; private int queueId; @@ -36,11 +36,11 @@ public void setTopic(String topic) { this.topic = topic; } - public int getQueueId() { + public Integer getQueueId() { return queueId; } - public void setQueueId(int queueId) { + public void setQueueId(Integer queueId) { this.queueId = queueId; } diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/QueryConsumeTimeSpanRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryConsumeTimeSpanRequestHeader.java similarity index 87% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/QueryConsumeTimeSpanRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryConsumeTimeSpanRequestHeader.java index 5250d8bd41d..9c0aa34b5fa 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/QueryConsumeTimeSpanRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryConsumeTimeSpanRequestHeader.java @@ -15,13 +15,13 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; -public class QueryConsumeTimeSpanRequestHeader implements CommandCustomHeader { +public class QueryConsumeTimeSpanRequestHeader extends TopicRequestHeader { @CFNotNull private String topic; @CFNotNull diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryConsumerOffsetRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryConsumerOffsetRequestHeader.java new file mode 100644 index 00000000000..e16d38a7a3e --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryConsumerOffsetRequestHeader.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * $Id: QueryConsumerOffsetRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +public class QueryConsumerOffsetRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + private String consumerGroup; + @CFNotNull + private String topic; + @CFNotNull + private Integer queueId; + + private Boolean setZeroIfNotFound; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + @Override + public String getTopic() { + return topic; + } + + @Override + public void setTopic(String topic) { + this.topic = topic; + } + + @Override + public Integer getQueueId() { + return queueId; + } + + @Override + public void setQueueId(Integer queueId) { + this.queueId = queueId; + } + + public Boolean getSetZeroIfNotFound() { + return setZeroIfNotFound; + } + + public void setSetZeroIfNotFound(Boolean setZeroIfNotFound) { + this.setZeroIfNotFound = setZeroIfNotFound; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("consumerGroup", consumerGroup) + .add("topic", topic) + .add("queueId", queueId) + .add("setZeroIfNotFound", setZeroIfNotFound) + .toString(); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/QueryConsumerOffsetResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryConsumerOffsetResponseHeader.java similarity index 96% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/QueryConsumerOffsetResponseHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryConsumerOffsetResponseHeader.java index c21710a318f..1ee706fd00f 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/QueryConsumerOffsetResponseHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryConsumerOffsetResponseHeader.java @@ -18,7 +18,7 @@ /** * $Id: QueryConsumerOffsetResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/QueryCorrectionOffsetHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryCorrectionOffsetHeader.java similarity index 90% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/QueryCorrectionOffsetHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryCorrectionOffsetHeader.java index 93fa7227420..8e03ce5c6aa 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/QueryCorrectionOffsetHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryCorrectionOffsetHeader.java @@ -18,13 +18,13 @@ /** * $Id: GetMinOffsetRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; -public class QueryCorrectionOffsetHeader implements CommandCustomHeader { +public class QueryCorrectionOffsetHeader extends TopicRequestHeader { private String filterGroups; @CFNotNull private String compareGroup; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/QueryMessageRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryMessageRequestHeader.java similarity index 92% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/QueryMessageRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryMessageRequestHeader.java index 9476651899d..1a78b9e4fab 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/QueryMessageRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryMessageRequestHeader.java @@ -18,13 +18,13 @@ /** * $Id: QueryMessageRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; -public class QueryMessageRequestHeader implements CommandCustomHeader { +public class QueryMessageRequestHeader extends TopicRequestHeader { @CFNotNull private String topic; @CFNotNull diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/QueryMessageResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryMessageResponseHeader.java similarity index 97% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/QueryMessageResponseHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryMessageResponseHeader.java index a9f3f146893..b1927c5a9ba 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/QueryMessageResponseHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryMessageResponseHeader.java @@ -18,7 +18,7 @@ /** * $Id: QueryMessageResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QuerySubscriptionByConsumerRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QuerySubscriptionByConsumerRequestHeader.java new file mode 100644 index 00000000000..1e5a48c5887 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QuerySubscriptionByConsumerRequestHeader.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * $Id: QueryMessageRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; + +public class QuerySubscriptionByConsumerRequestHeader extends TopicRequestHeader { + @CFNotNull + private String group; + private String topic; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/QueryTopicConsumeByWhoRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryTopicConsumeByWhoRequestHeader.java similarity index 87% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/QueryTopicConsumeByWhoRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryTopicConsumeByWhoRequestHeader.java index 3fba2e4960e..5cd9b931c2a 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/QueryTopicConsumeByWhoRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryTopicConsumeByWhoRequestHeader.java @@ -18,13 +18,13 @@ /** * $Id: QueryMessageRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; -public class QueryTopicConsumeByWhoRequestHeader implements CommandCustomHeader { +public class QueryTopicConsumeByWhoRequestHeader extends TopicRequestHeader { @CFNotNull private String topic; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryTopicsByConsumerRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryTopicsByConsumerRequestHeader.java new file mode 100644 index 00000000000..e90afdd8ba6 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/QueryTopicsByConsumerRequestHeader.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * $Id: QueryMessageRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; + +public class QueryTopicsByConsumerRequestHeader extends RpcRequestHeader { + @CFNotNull + private String group; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/RemoveBrokerRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/RemoveBrokerRequestHeader.java new file mode 100644 index 00000000000..a641340eeb9 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/RemoveBrokerRequestHeader.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class RemoveBrokerRequestHeader implements CommandCustomHeader { + @CFNotNull + private String brokerName; + @CFNotNull + private String brokerClusterName; + @CFNotNull + private Long brokerId; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + public String getBrokerClusterName() { + return brokerClusterName; + } + + public void setBrokerClusterName(String brokerClusterName) { + this.brokerClusterName = brokerClusterName; + } + + public Long getBrokerId() { + return brokerId; + } + + public void setBrokerId(Long brokerId) { + this.brokerId = brokerId; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ReplyMessageRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ReplyMessageRequestHeader.java new file mode 100644 index 00000000000..eeb022d6d69 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ReplyMessageRequestHeader.java @@ -0,0 +1,170 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.annotation.CFNullable; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +public class ReplyMessageRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + private String producerGroup; + @CFNotNull + private String topic; + @CFNotNull + private String defaultTopic; + @CFNotNull + private Integer defaultTopicQueueNums; + @CFNotNull + private Integer queueId; + @CFNotNull + private Integer sysFlag; + @CFNotNull + private Long bornTimestamp; + @CFNotNull + private Integer flag; + @CFNullable + private String properties; + @CFNullable + private Integer reconsumeTimes; + @CFNullable + private boolean unitMode = false; + + @CFNotNull + private String bornHost; + @CFNotNull + private String storeHost; + @CFNotNull + private long storeTimestamp; + + public void checkFields() throws RemotingCommandException { + } + + public String getProducerGroup() { + return producerGroup; + } + + public void setProducerGroup(String producerGroup) { + this.producerGroup = producerGroup; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getDefaultTopic() { + return defaultTopic; + } + + public void setDefaultTopic(String defaultTopic) { + this.defaultTopic = defaultTopic; + } + + public Integer getDefaultTopicQueueNums() { + return defaultTopicQueueNums; + } + + public void setDefaultTopicQueueNums(Integer defaultTopicQueueNums) { + this.defaultTopicQueueNums = defaultTopicQueueNums; + } + + public Integer getQueueId() { + return queueId; + } + + public void setQueueId(Integer queueId) { + this.queueId = queueId; + } + + public Integer getSysFlag() { + return sysFlag; + } + + public void setSysFlag(Integer sysFlag) { + this.sysFlag = sysFlag; + } + + public Long getBornTimestamp() { + return bornTimestamp; + } + + public void setBornTimestamp(Long bornTimestamp) { + this.bornTimestamp = bornTimestamp; + } + + public Integer getFlag() { + return flag; + } + + public void setFlag(Integer flag) { + this.flag = flag; + } + + public String getProperties() { + return properties; + } + + public void setProperties(String properties) { + this.properties = properties; + } + + public Integer getReconsumeTimes() { + return reconsumeTimes; + } + + public void setReconsumeTimes(Integer reconsumeTimes) { + this.reconsumeTimes = reconsumeTimes; + } + + public boolean isUnitMode() { + return unitMode; + } + + public void setUnitMode(boolean unitMode) { + this.unitMode = unitMode; + } + + public String getBornHost() { + return bornHost; + } + + public void setBornHost(String bornHost) { + this.bornHost = bornHost; + } + + public String getStoreHost() { + return storeHost; + } + + public void setStoreHost(String storeHost) { + this.storeHost = storeHost; + } + + public long getStoreTimestamp() { + return storeTimestamp; + } + + public void setStoreTimestamp(long storeTimestamp) { + this.storeTimestamp = storeTimestamp; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ResetMasterFlushOffsetHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ResetMasterFlushOffsetHeader.java new file mode 100644 index 00000000000..ddee7224c7d --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ResetMasterFlushOffsetHeader.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class ResetMasterFlushOffsetHeader implements CommandCustomHeader { + @CFNotNull + private Long masterFlushOffset; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public Long getMasterFlushOffset() { + return masterFlushOffset; + } + + public void setMasterFlushOffset(Long masterFlushOffset) { + this.masterFlushOffset = masterFlushOffset; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/ResetOffsetRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ResetOffsetRequestHeader.java similarity index 76% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/ResetOffsetRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ResetOffsetRequestHeader.java index c3bfa218902..dd5c043ef62 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/ResetOffsetRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ResetOffsetRequestHeader.java @@ -15,19 +15,27 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +public class ResetOffsetRequestHeader extends TopicQueueRequestHeader { -public class ResetOffsetRequestHeader implements CommandCustomHeader { @CFNotNull private String topic; + @CFNotNull private String group; + + private int queueId = -1; + + private Long offset; + @CFNotNull private long timestamp; + @CFNotNull private boolean isForce; @@ -63,6 +71,22 @@ public void setForce(boolean isForce) { this.isForce = isForce; } + public Integer getQueueId() { + return queueId; + } + + public void setQueueId(Integer queueId) { + this.queueId = queueId; + } + + public Long getOffset() { + return offset; + } + + public void setOffset(Long offset) { + this.offset = offset; + } + @Override public void checkFields() throws RemotingCommandException { diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ResumeCheckHalfMessageRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ResumeCheckHalfMessageRequestHeader.java new file mode 100644 index 00000000000..6265cdfd4be --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ResumeCheckHalfMessageRequestHeader.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNullable; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class ResumeCheckHalfMessageRequestHeader implements CommandCustomHeader { + @CFNullable + private String msgId; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getMsgId() { + return msgId; + } + + public void setMsgId(String msgId) { + this.msgId = msgId; + } + + @Override + public String toString() { + return "ResumeCheckHalfMessageRequestHeader [msgId=" + msgId + "]"; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SearchOffsetRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SearchOffsetRequestHeader.java new file mode 100644 index 00000000000..79c3d337be0 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SearchOffsetRequestHeader.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * $Id: SearchOffsetRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.common.BoundaryType; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +public class SearchOffsetRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + private String topic; + @CFNotNull + private Integer queueId; + @CFNotNull + private Long timestamp; + + private BoundaryType boundaryType; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + @Override + public String getTopic() { + return topic; + } + + @Override + public void setTopic(String topic) { + this.topic = topic; + } + + @Override + public Integer getQueueId() { + return queueId; + } + + @Override + public void setQueueId(Integer queueId) { + this.queueId = queueId; + } + + public Long getTimestamp() { + return timestamp; + } + + public void setTimestamp(Long timestamp) { + this.timestamp = timestamp; + } + + public BoundaryType getBoundaryType() { + // default return LOWER + return boundaryType == null ? BoundaryType.LOWER : boundaryType; + } + + public void setBoundaryType(BoundaryType boundaryType) { + this.boundaryType = boundaryType; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("topic", topic) + .add("queueId", queueId) + .add("timestamp", timestamp) + .add("boundaryType", boundaryType.getName()) + .toString(); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/SearchOffsetResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SearchOffsetResponseHeader.java similarity index 96% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/SearchOffsetResponseHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SearchOffsetResponseHeader.java index f88ac6852f4..fe4006219eb 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/SearchOffsetResponseHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SearchOffsetResponseHeader.java @@ -18,7 +18,7 @@ /** * $Id: SearchOffsetResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SendMessageRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SendMessageRequestHeader.java new file mode 100644 index 00000000000..17ce5126313 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SendMessageRequestHeader.java @@ -0,0 +1,211 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * $Id: SendMessageRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.annotation.CFNullable; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +public class SendMessageRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + private String producerGroup; + @CFNotNull + private String topic; + @CFNotNull + private String defaultTopic; + @CFNotNull + private Integer defaultTopicQueueNums; + @CFNotNull + private Integer queueId; + @CFNotNull + private Integer sysFlag; + @CFNotNull + private Long bornTimestamp; + @CFNotNull + private Integer flag; + @CFNullable + private String properties; + @CFNullable + private Integer reconsumeTimes; + @CFNullable + private boolean unitMode = false; + @CFNullable + private boolean batch = false; + private Integer maxReconsumeTimes; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getProducerGroup() { + return producerGroup; + } + + public void setProducerGroup(String producerGroup) { + this.producerGroup = producerGroup; + } + + @Override + public String getTopic() { + return topic; + } + + @Override + public void setTopic(String topic) { + this.topic = topic; + } + + public String getDefaultTopic() { + return defaultTopic; + } + + public void setDefaultTopic(String defaultTopic) { + this.defaultTopic = defaultTopic; + } + + public Integer getDefaultTopicQueueNums() { + return defaultTopicQueueNums; + } + + public void setDefaultTopicQueueNums(Integer defaultTopicQueueNums) { + this.defaultTopicQueueNums = defaultTopicQueueNums; + } + + @Override + public Integer getQueueId() { + return queueId; + } + + @Override + public void setQueueId(Integer queueId) { + this.queueId = queueId; + } + + public Integer getSysFlag() { + return sysFlag; + } + + public void setSysFlag(Integer sysFlag) { + this.sysFlag = sysFlag; + } + + public Long getBornTimestamp() { + return bornTimestamp; + } + + public void setBornTimestamp(Long bornTimestamp) { + this.bornTimestamp = bornTimestamp; + } + + public Integer getFlag() { + return flag; + } + + public void setFlag(Integer flag) { + this.flag = flag; + } + + public String getProperties() { + return properties; + } + + public void setProperties(String properties) { + this.properties = properties; + } + + public Integer getReconsumeTimes() { + return reconsumeTimes; + } + + public void setReconsumeTimes(Integer reconsumeTimes) { + this.reconsumeTimes = reconsumeTimes; + } + + public boolean isUnitMode() { + return unitMode; + } + + public void setUnitMode(boolean isUnitMode) { + this.unitMode = isUnitMode; + } + + public Integer getMaxReconsumeTimes() { + return maxReconsumeTimes; + } + + public void setMaxReconsumeTimes(final Integer maxReconsumeTimes) { + this.maxReconsumeTimes = maxReconsumeTimes; + } + + public boolean isBatch() { + return batch; + } + + public void setBatch(boolean batch) { + this.batch = batch; + } + + public static SendMessageRequestHeader parseRequestHeader(RemotingCommand request) throws RemotingCommandException { + SendMessageRequestHeaderV2 requestHeaderV2 = null; + SendMessageRequestHeader requestHeader = null; + switch (request.getCode()) { + case RequestCode.SEND_BATCH_MESSAGE: + case RequestCode.SEND_MESSAGE_V2: + requestHeaderV2 = + (SendMessageRequestHeaderV2) request + .decodeCommandCustomHeader(SendMessageRequestHeaderV2.class); + case RequestCode.SEND_MESSAGE: + if (null == requestHeaderV2) { + requestHeader = + (SendMessageRequestHeader) request + .decodeCommandCustomHeader(SendMessageRequestHeader.class); + } else { + requestHeader = SendMessageRequestHeaderV2.createSendMessageRequestHeaderV1(requestHeaderV2); + } + default: + break; + } + return requestHeader; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("producerGroup", producerGroup) + .add("topic", topic) + .add("defaultTopic", defaultTopic) + .add("defaultTopicQueueNums", defaultTopicQueueNums) + .add("queueId", queueId) + .add("sysFlag", sysFlag) + .add("bornTimestamp", bornTimestamp) + .add("flag", flag) + .add("properties", properties) + .add("reconsumeTimes", reconsumeTimes) + .add("unitMode", unitMode) + .add("batch", batch) + .add("maxReconsumeTimes", maxReconsumeTimes) + .toString(); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SendMessageRequestHeaderV2.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SendMessageRequestHeaderV2.java new file mode 100644 index 00000000000..4b0d795bcd5 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SendMessageRequestHeaderV2.java @@ -0,0 +1,341 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header; + +import com.google.common.base.MoreObjects; +import io.netty.buffer.ByteBuf; +import java.util.HashMap; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.annotation.CFNullable; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.FastCodesHeader; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +/** + * Use short variable name to speed up FastJson deserialization process. + */ +public class SendMessageRequestHeaderV2 extends TopicQueueRequestHeader implements CommandCustomHeader, FastCodesHeader { + @CFNotNull + private String a; // producerGroup; + @CFNotNull + private String b; // topic; + @CFNotNull + private String c; // defaultTopic; + @CFNotNull + private Integer d; // defaultTopicQueueNums; + @CFNotNull + private Integer e; // queueId; + @CFNotNull + private Integer f; // sysFlag; + @CFNotNull + private Long g; // bornTimestamp; + @CFNotNull + private Integer h; // flag; + @CFNullable + private String i; // properties; + @CFNullable + private Integer j; // reconsumeTimes; + @CFNullable + private Boolean k; // unitMode = false; + + private Integer l; // consumeRetryTimes + + @CFNullable + private Boolean m; //batch + @CFNullable + private String n; // brokerName + + public static SendMessageRequestHeader createSendMessageRequestHeaderV1(final SendMessageRequestHeaderV2 v2) { + SendMessageRequestHeader v1 = new SendMessageRequestHeader(); + v1.setProducerGroup(v2.a); + v1.setTopic(v2.b); + v1.setDefaultTopic(v2.c); + v1.setDefaultTopicQueueNums(v2.d); + v1.setQueueId(v2.e); + v1.setSysFlag(v2.f); + v1.setBornTimestamp(v2.g); + v1.setFlag(v2.h); + v1.setProperties(v2.i); + v1.setReconsumeTimes(v2.j); + v1.setUnitMode(v2.k); + v1.setMaxReconsumeTimes(v2.l); + v1.setBatch(v2.m); + v1.setBrokerName(v2.n); + return v1; + } + + public static SendMessageRequestHeaderV2 createSendMessageRequestHeaderV2(final SendMessageRequestHeader v1) { + SendMessageRequestHeaderV2 v2 = new SendMessageRequestHeaderV2(); + v2.a = v1.getProducerGroup(); + v2.b = v1.getTopic(); + v2.c = v1.getDefaultTopic(); + v2.d = v1.getDefaultTopicQueueNums(); + v2.e = v1.getQueueId(); + v2.f = v1.getSysFlag(); + v2.g = v1.getBornTimestamp(); + v2.h = v1.getFlag(); + v2.i = v1.getProperties(); + v2.j = v1.getReconsumeTimes(); + v2.k = v1.isUnitMode(); + v2.l = v1.getMaxReconsumeTimes(); + v2.m = v1.isBatch(); + v2.n = v1.getBrokerName(); + return v2; + } + + @Override + public void checkFields() throws RemotingCommandException { + } + + @Override + public void encode(ByteBuf out) { + writeIfNotNull(out, "a", a); + writeIfNotNull(out, "b", b); + writeIfNotNull(out, "c", c); + writeIfNotNull(out, "d", d); + writeIfNotNull(out, "e", e); + writeIfNotNull(out, "f", f); + writeIfNotNull(out, "g", g); + writeIfNotNull(out, "h", h); + writeIfNotNull(out, "i", i); + writeIfNotNull(out, "j", j); + writeIfNotNull(out, "k", k); + writeIfNotNull(out, "l", l); + writeIfNotNull(out, "m", m); + writeIfNotNull(out, "n", n); + } + + @Override + public void decode(HashMap fields) throws RemotingCommandException { + + String str = getAndCheckNotNull(fields, "a"); + if (str != null) { + a = str; + } + + str = getAndCheckNotNull(fields, "b"); + if (str != null) { + b = str; + } + + str = getAndCheckNotNull(fields, "c"); + if (str != null) { + c = str; + } + + str = getAndCheckNotNull(fields, "d"); + if (str != null) { + d = Integer.parseInt(str); + } + + str = getAndCheckNotNull(fields, "e"); + if (str != null) { + e = Integer.parseInt(str); + } + + str = getAndCheckNotNull(fields, "f"); + if (str != null) { + f = Integer.parseInt(str); + } + + str = getAndCheckNotNull(fields, "g"); + if (str != null) { + g = Long.parseLong(str); + } + + str = getAndCheckNotNull(fields, "h"); + if (str != null) { + h = Integer.parseInt(str); + } + + str = fields.get("i"); + if (str != null) { + i = str; + } + + str = fields.get("j"); + if (str != null) { + j = Integer.parseInt(str); + } + + str = fields.get("k"); + if (str != null) { + k = Boolean.parseBoolean(str); + } + + str = fields.get("l"); + if (str != null) { + l = Integer.parseInt(str); + } + + str = fields.get("m"); + if (str != null) { + m = Boolean.parseBoolean(str); + } + + str = fields.get("n"); + if (str != null) { + n = str; + } + } + + public String getA() { + return a; + } + + public void setA(String a) { + this.a = a; + } + + public String getB() { + return b; + } + + public void setB(String b) { + this.b = b; + } + + public String getC() { + return c; + } + + public void setC(String c) { + this.c = c; + } + + public Integer getD() { + return d; + } + + public void setD(Integer d) { + this.d = d; + } + + public Integer getE() { + return e; + } + + public void setE(Integer e) { + this.e = e; + } + + public Integer getF() { + return f; + } + + public void setF(Integer f) { + this.f = f; + } + + public Long getG() { + return g; + } + + public void setG(Long g) { + this.g = g; + } + + public Integer getH() { + return h; + } + + public void setH(Integer h) { + this.h = h; + } + + public String getI() { + return i; + } + + public void setI(String i) { + this.i = i; + } + + public Integer getJ() { + return j; + } + + public void setJ(Integer j) { + this.j = j; + } + + public Boolean isK() { + return k; + } + + public void setK(Boolean k) { + this.k = k; + } + + public Integer getL() { + return l; + } + + public void setL(final Integer l) { + this.l = l; + } + + public Boolean isM() { + return m; + } + + public void setM(Boolean m) { + this.m = m; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("a", a) + .add("b", b) + .add("c", c) + .add("d", d) + .add("e", e) + .add("f", f) + .add("g", g) + .add("h", h) + .add("i", i) + .add("j", j) + .add("k", k) + .add("l", l) + .add("m", m) + .add("n", n) + .toString(); + } + + @Override + public Integer getQueueId() { + return e; + } + + @Override + public void setQueueId(Integer queueId) { + this.e = queueId; + } + + @Override + public String getTopic() { + return b; + } + + @Override + public void setTopic(String topic) { + this.b = topic; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SendMessageResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SendMessageResponseHeader.java new file mode 100644 index 00000000000..fe1e8533e54 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/SendMessageResponseHeader.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * $Id: SendMessageResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import io.netty.buffer.ByteBuf; +import java.util.HashMap; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.FastCodesHeader; + +public class SendMessageResponseHeader implements CommandCustomHeader, FastCodesHeader { + @CFNotNull + private String msgId; + @CFNotNull + private Integer queueId; + @CFNotNull + private Long queueOffset; + private String transactionId; + private String batchUniqId; + + @Override + public void checkFields() throws RemotingCommandException { + } + + @Override + public void encode(ByteBuf out) { + writeIfNotNull(out, "msgId", msgId); + writeIfNotNull(out, "queueId", queueId); + writeIfNotNull(out, "queueOffset", queueOffset); + writeIfNotNull(out, "transactionId", transactionId); + writeIfNotNull(out, "batchUniqId", batchUniqId); + } + + @Override + public void decode(HashMap fields) throws RemotingCommandException { + String str = getAndCheckNotNull(fields, "msgId"); + if (str != null) { + this.msgId = str; + } + + str = getAndCheckNotNull(fields, "queueId"); + if (str != null) { + this.queueId = Integer.parseInt(str); + } + + str = getAndCheckNotNull(fields, "queueOffset"); + if (str != null) { + this.queueOffset = Long.parseLong(str); + } + + str = fields.get("transactionId"); + if (str != null) { + this.transactionId = str; + } + + str = fields.get("batchUniqId"); + if (str != null) { + this.batchUniqId = str; + } + } + + public String getMsgId() { + return msgId; + } + + public void setMsgId(String msgId) { + this.msgId = msgId; + } + + public Integer getQueueId() { + return queueId; + } + + public void setQueueId(Integer queueId) { + this.queueId = queueId; + } + + public Long getQueueOffset() { + return queueOffset; + } + + public void setQueueOffset(Long queueOffset) { + this.queueOffset = queueOffset; + } + + public String getTransactionId() { + return transactionId; + } + + public void setTransactionId(String transactionId) { + this.transactionId = transactionId; + } + + public String getBatchUniqId() { + return batchUniqId; + } + + public void setBatchUniqId(String batchUniqId) { + this.batchUniqId = batchUniqId; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/StatisticsMessagesRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/StatisticsMessagesRequestHeader.java new file mode 100644 index 00000000000..d8c22e37651 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/StatisticsMessagesRequestHeader.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; + +public class StatisticsMessagesRequestHeader extends TopicQueueRequestHeader { + @CFNotNull + private String consumerGroup; + @CFNotNull + private String topic; + @CFNotNull + private int queueId; + + private long fromTime; + private long toTime; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public Integer getQueueId() { + if (queueId < 0) { + return -1; + } + return queueId; + } + + public void setQueueId(Integer queueId) { + this.queueId = queueId; + } + + public long getFromTime() { + return fromTime; + } + + public void setFromTime(long fromTime) { + this.fromTime = fromTime; + } + + public long getToTime() { + return toTime; + } + + public void setToTime(long toTime) { + this.toTime = toTime; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UnlockBatchMqRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UnlockBatchMqRequestHeader.java new file mode 100644 index 00000000000..e7a44f2f8b8 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UnlockBatchMqRequestHeader.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; + +public class UnlockBatchMqRequestHeader extends RpcRequestHeader { + @Override + public void checkFields() throws RemotingCommandException { + + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/UnregisterClientRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UnregisterClientRequestHeader.java similarity index 90% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/UnregisterClientRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UnregisterClientRequestHeader.java index bb0a4629170..79072195b5e 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/UnregisterClientRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UnregisterClientRequestHeader.java @@ -15,14 +15,14 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.annotation.CFNullable; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.RpcRequestHeader; -public class UnregisterClientRequestHeader implements CommandCustomHeader { +public class UnregisterClientRequestHeader extends RpcRequestHeader { @CFNotNull private String clientID; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/UnregisterClientResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UnregisterClientResponseHeader.java similarity index 95% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/UnregisterClientResponseHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UnregisterClientResponseHeader.java index 38fb87a6e23..f7347c5d477 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/UnregisterClientResponseHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UnregisterClientResponseHeader.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.exception.RemotingCommandException; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/UpdateConsumerOffsetRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UpdateConsumerOffsetRequestHeader.java similarity index 76% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/UpdateConsumerOffsetRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UpdateConsumerOffsetRequestHeader.java index 3f44db645c5..f131c36f057 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/UpdateConsumerOffsetRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UpdateConsumerOffsetRequestHeader.java @@ -18,13 +18,14 @@ /** * $Id: UpdateConsumerOffsetRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; -import org.apache.rocketmq.remoting.CommandCustomHeader; +import com.google.common.base.MoreObjects; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicQueueRequestHeader; -public class UpdateConsumerOffsetRequestHeader implements CommandCustomHeader { +public class UpdateConsumerOffsetRequestHeader extends TopicQueueRequestHeader { @CFNotNull private String consumerGroup; @CFNotNull @@ -46,18 +47,22 @@ public void setConsumerGroup(String consumerGroup) { this.consumerGroup = consumerGroup; } + @Override public String getTopic() { return topic; } + @Override public void setTopic(String topic) { this.topic = topic; } + @Override public Integer getQueueId() { return queueId; } + @Override public void setQueueId(Integer queueId) { this.queueId = queueId; } @@ -69,4 +74,14 @@ public Long getCommitOffset() { public void setCommitOffset(Long commitOffset) { this.commitOffset = commitOffset; } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("consumerGroup", consumerGroup) + .add("topic", topic) + .add("queueId", queueId) + .add("commitOffset", commitOffset) + .toString(); + } } diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/UpdateConsumerOffsetResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UpdateConsumerOffsetResponseHeader.java similarity index 95% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/UpdateConsumerOffsetResponseHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UpdateConsumerOffsetResponseHeader.java index 3cd9ebfcc2e..13b5b8752b1 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/UpdateConsumerOffsetResponseHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UpdateConsumerOffsetResponseHeader.java @@ -18,7 +18,7 @@ /** * $Id: UpdateConsumerOffsetResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.exception.RemotingCommandException; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UpdateGlobalWhiteAddrsConfigRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UpdateGlobalWhiteAddrsConfigRequestHeader.java new file mode 100644 index 00000000000..59e93d32630 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UpdateGlobalWhiteAddrsConfigRequestHeader.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class UpdateGlobalWhiteAddrsConfigRequestHeader implements CommandCustomHeader { + + @CFNotNull + private String globalWhiteAddrs; + @CFNotNull + private String aclFileFullPath; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getGlobalWhiteAddrs() { + return globalWhiteAddrs; + } + + public void setGlobalWhiteAddrs(String globalWhiteAddrs) { + this.globalWhiteAddrs = globalWhiteAddrs; + } + + public String getAclFileFullPath() { + return aclFileFullPath; + } + + public void setAclFileFullPath(String aclFileFullPath) { + this.aclFileFullPath = aclFileFullPath; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UpdateGroupForbiddenRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UpdateGroupForbiddenRequestHeader.java new file mode 100644 index 00000000000..cfb28df218d --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/UpdateGroupForbiddenRequestHeader.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * $Id: CreateTopicRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header; + +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; + +public class UpdateGroupForbiddenRequestHeader extends TopicRequestHeader { + @CFNotNull + private String group; + @CFNotNull + private String topic; + + private Boolean readable; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public Boolean getReadable() { + return readable; + } + + public void setReadable(Boolean readable) { + this.readable = readable; + } + +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/ViewBrokerStatsDataRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ViewBrokerStatsDataRequestHeader.java similarity index 96% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/ViewBrokerStatsDataRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ViewBrokerStatsDataRequestHeader.java index 646cf3a1c91..b879bfbac24 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/ViewBrokerStatsDataRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ViewBrokerStatsDataRequestHeader.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/ViewMessageRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ViewMessageRequestHeader.java similarity index 96% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/ViewMessageRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ViewMessageRequestHeader.java index c0b937363bc..79421fee4c0 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/ViewMessageRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ViewMessageRequestHeader.java @@ -18,7 +18,7 @@ /** * $Id: ViewMessageRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/ViewMessageResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ViewMessageResponseHeader.java similarity index 95% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/ViewMessageResponseHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ViewMessageResponseHeader.java index e7153dd189b..94484e04b27 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/ViewMessageResponseHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/ViewMessageResponseHeader.java @@ -18,7 +18,7 @@ /** * $Id: ViewMessageResponseHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.header; +package org.apache.rocketmq.remoting.protocol.header; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.exception.RemotingCommandException; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/AlterSyncStateSetRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/AlterSyncStateSetRequestHeader.java new file mode 100644 index 00000000000..d1c605c19a9 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/AlterSyncStateSetRequestHeader.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header.controller; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class AlterSyncStateSetRequestHeader implements CommandCustomHeader { + private String brokerName; + private Long masterBrokerId; + private Integer masterEpoch; + private long invokeTime = System.currentTimeMillis(); + + public AlterSyncStateSetRequestHeader() { + } + + public AlterSyncStateSetRequestHeader(String brokerName, Long masterBrokerId, Integer masterEpoch) { + this.brokerName = brokerName; + this.masterBrokerId = masterBrokerId; + this.masterEpoch = masterEpoch; + } + + public long getInvokeTime() { + return invokeTime; + } + + public void setInvokeTime(long invokeTime) { + this.invokeTime = invokeTime; + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + public Long getMasterBrokerId() { + return masterBrokerId; + } + + public void setMasterBrokerId(Long masterBrokerId) { + this.masterBrokerId = masterBrokerId; + } + + public Integer getMasterEpoch() { + return masterEpoch; + } + + public void setMasterEpoch(Integer masterEpoch) { + this.masterEpoch = masterEpoch; + } + + @Override + public String toString() { + return "AlterSyncStateSetRequestHeader{" + + "brokerName='" + brokerName + '\'' + + ", masterBrokerId=" + masterBrokerId + + ", masterEpoch=" + masterEpoch + + '}'; + } + + @Override + public void checkFields() throws RemotingCommandException { + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/AlterSyncStateSetResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/AlterSyncStateSetResponseHeader.java new file mode 100644 index 00000000000..012197c73b6 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/AlterSyncStateSetResponseHeader.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header.controller; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class AlterSyncStateSetResponseHeader implements CommandCustomHeader { + private int newSyncStateSetEpoch; + + public AlterSyncStateSetResponseHeader() { + } + + public int getNewSyncStateSetEpoch() { + return newSyncStateSetEpoch; + } + + public void setNewSyncStateSetEpoch(int newSyncStateSetEpoch) { + this.newSyncStateSetEpoch = newSyncStateSetEpoch; + } + + @Override + public String toString() { + return "AlterSyncStateSetResponseHeader{" + + "newSyncStateSetEpoch=" + newSyncStateSetEpoch + + '}'; + } + + @Override + public void checkFields() throws RemotingCommandException { + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/ElectMasterRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/ElectMasterRequestHeader.java new file mode 100644 index 00000000000..79000f184fb --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/ElectMasterRequestHeader.java @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header.controller; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class ElectMasterRequestHeader implements CommandCustomHeader { + + @CFNotNull + private String clusterName = ""; + + @CFNotNull + private String brokerName = ""; + + /** + * brokerId + * for brokerTrigger electMaster: this brokerId will be elected as a master when it is the first time to elect + * in this broker-set + * for adminTrigger electMaster: this brokerId is also named assignedBrokerId, which means we must prefer to elect + * it as a new master when this broker is valid. + */ + @CFNotNull + private Long brokerId = -1L; + + @CFNotNull + private Boolean designateElect = false; + + private Long invokeTime = System.currentTimeMillis(); + + public ElectMasterRequestHeader() { + } + + public ElectMasterRequestHeader(String brokerName) { + this.brokerName = brokerName; + } + + public ElectMasterRequestHeader(String clusterName, String brokerName, Long brokerId) { + this.clusterName = clusterName; + this.brokerName = brokerName; + this.brokerId = brokerId; + } + + public ElectMasterRequestHeader(String clusterName, String brokerName, Long brokerId, boolean designateElect) { + this.clusterName = clusterName; + this.brokerName = brokerName; + this.brokerId = brokerId; + this.designateElect = designateElect; + } + + public static ElectMasterRequestHeader ofBrokerTrigger(String clusterName, String brokerName, + Long brokerId) { + return new ElectMasterRequestHeader(clusterName, brokerName, brokerId); + } + + public static ElectMasterRequestHeader ofControllerTrigger(String brokerName) { + return new ElectMasterRequestHeader(brokerName); + } + + public static ElectMasterRequestHeader ofAdminTrigger(String clusterName, String brokerName, Long brokerId) { + return new ElectMasterRequestHeader(clusterName, brokerName, brokerId, true); + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + public Long getBrokerId() { + return brokerId; + } + + public void setBrokerId(Long brokerId) { + this.brokerId = brokerId; + } + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public boolean getDesignateElect() { + return this.designateElect; + } + + public Long getInvokeTime() { + return invokeTime; + } + + public void setInvokeTime(Long invokeTime) { + this.invokeTime = invokeTime; + } + + @Override + public String toString() { + return "ElectMasterRequestHeader{" + + "clusterName='" + clusterName + '\'' + + ", brokerName='" + brokerName + '\'' + + ", brokerId=" + brokerId + + ", designateElect=" + designateElect + + '}'; + } + + @Override + public void checkFields() throws RemotingCommandException { + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/ElectMasterResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/ElectMasterResponseHeader.java new file mode 100644 index 00000000000..aaf3b10b829 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/ElectMasterResponseHeader.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header.controller; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + + +public class ElectMasterResponseHeader implements CommandCustomHeader { + + private Long masterBrokerId; + private String masterAddress; + private Integer masterEpoch; + private Integer syncStateSetEpoch; + + public ElectMasterResponseHeader() { + } + + public ElectMasterResponseHeader(Long masterBrokerId, String masterAddress, Integer masterEpoch, Integer syncStateSetEpoch) { + this.masterBrokerId = masterBrokerId; + this.masterAddress = masterAddress; + this.masterEpoch = masterEpoch; + this.syncStateSetEpoch = syncStateSetEpoch; + } + + public String getMasterAddress() { + return masterAddress; + } + + public void setMasterAddress(String masterAddress) { + this.masterAddress = masterAddress; + } + + public Integer getMasterEpoch() { + return masterEpoch; + } + + public void setMasterEpoch(Integer masterEpoch) { + this.masterEpoch = masterEpoch; + } + + public Integer getSyncStateSetEpoch() { + return syncStateSetEpoch; + } + + public void setSyncStateSetEpoch(Integer syncStateSetEpoch) { + this.syncStateSetEpoch = syncStateSetEpoch; + } + + public void setMasterBrokerId(Long masterBrokerId) { + this.masterBrokerId = masterBrokerId; + } + + public Long getMasterBrokerId() { + return masterBrokerId; + } + + @Override + public String toString() { + return "ElectMasterResponseHeader{" + + "masterBrokerId=" + masterBrokerId + + ", masterAddress='" + masterAddress + '\'' + + ", masterEpoch=" + masterEpoch + + ", syncStateSetEpoch=" + syncStateSetEpoch + + '}'; + } + + @Override + public void checkFields() throws RemotingCommandException { + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/GetMetaDataResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/GetMetaDataResponseHeader.java new file mode 100644 index 00000000000..9ae6c7ca08d --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/GetMetaDataResponseHeader.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header.controller; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class GetMetaDataResponseHeader implements CommandCustomHeader { + private String group; + private String controllerLeaderId; + private String controllerLeaderAddress; + private boolean isLeader; + private String peers; + + public GetMetaDataResponseHeader() { + } + + public GetMetaDataResponseHeader(String group, String controllerLeaderId, String controllerLeaderAddress, boolean isLeader, String peers) { + this.group = group; + this.controllerLeaderId = controllerLeaderId; + this.controllerLeaderAddress = controllerLeaderAddress; + this.isLeader = isLeader; + this.peers = peers; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public String getControllerLeaderId() { + return controllerLeaderId; + } + + public void setControllerLeaderId(String controllerLeaderId) { + this.controllerLeaderId = controllerLeaderId; + } + + public String getControllerLeaderAddress() { + return controllerLeaderAddress; + } + + public void setControllerLeaderAddress(String controllerLeaderAddress) { + this.controllerLeaderAddress = controllerLeaderAddress; + } + + public boolean isLeader() { + return isLeader; + } + + public void setIsLeader(boolean leader) { + isLeader = leader; + } + + public String getPeers() { + return peers; + } + + public void setPeers(String peers) { + this.peers = peers; + } + + @Override + public String toString() { + return "GetMetaDataResponseHeader{" + + "group='" + group + '\'' + + ", controllerLeaderId='" + controllerLeaderId + '\'' + + ", controllerLeaderAddress='" + controllerLeaderAddress + '\'' + + ", isLeader=" + isLeader + + ", peers='" + peers + '\'' + + '}'; + } + + @Override + public void checkFields() throws RemotingCommandException { + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/GetReplicaInfoRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/GetReplicaInfoRequestHeader.java new file mode 100644 index 00000000000..efc7afc8498 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/GetReplicaInfoRequestHeader.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header.controller; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class GetReplicaInfoRequestHeader implements CommandCustomHeader { + private String brokerName; + + public GetReplicaInfoRequestHeader() { + } + + public GetReplicaInfoRequestHeader(String brokerName) { + this.brokerName = brokerName; + } + + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + @Override + public String toString() { + return "GetReplicaInfoRequestHeader{" + + "brokerName='" + brokerName + '\'' + + '}'; + } + + @Override + public void checkFields() throws RemotingCommandException { + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/GetReplicaInfoResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/GetReplicaInfoResponseHeader.java new file mode 100644 index 00000000000..f7aa49e6970 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/GetReplicaInfoResponseHeader.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header.controller; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class GetReplicaInfoResponseHeader implements CommandCustomHeader { + + private Long masterBrokerId; + private String masterAddress; + private Integer masterEpoch; + + public GetReplicaInfoResponseHeader() { + } + + public String getMasterAddress() { + return masterAddress; + } + + public void setMasterAddress(String masterAddress) { + this.masterAddress = masterAddress; + } + + public Integer getMasterEpoch() { + return masterEpoch; + } + + public void setMasterEpoch(Integer masterEpoch) { + this.masterEpoch = masterEpoch; + } + + public Long getMasterBrokerId() { + return masterBrokerId; + } + + public void setMasterBrokerId(Long masterBrokerId) { + this.masterBrokerId = masterBrokerId; + } + + @Override + public String toString() { + return "GetReplicaInfoResponseHeader{" + + "masterBrokerId=" + masterBrokerId + + ", masterAddress='" + masterAddress + '\'' + + ", masterEpoch=" + masterEpoch + + '}'; + } + + @Override + public void checkFields() throws RemotingCommandException { + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/admin/CleanControllerBrokerDataRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/admin/CleanControllerBrokerDataRequestHeader.java new file mode 100644 index 00000000000..9482f4af5f4 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/admin/CleanControllerBrokerDataRequestHeader.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header.controller.admin; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.annotation.CFNullable; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class CleanControllerBrokerDataRequestHeader implements CommandCustomHeader { + + @CFNullable + private String clusterName; + + @CFNotNull + private String brokerName; + + @CFNullable + private String brokerControllerIdsToClean; + + private boolean isCleanLivingBroker = false; + private long invokeTime = System.currentTimeMillis(); + + public CleanControllerBrokerDataRequestHeader() { + } + + public CleanControllerBrokerDataRequestHeader(String clusterName, String brokerName, String brokerIdSetToClean, + boolean isCleanLivingBroker) { + this.clusterName = clusterName; + this.brokerName = brokerName; + this.brokerControllerIdsToClean = brokerIdSetToClean; + this.isCleanLivingBroker = isCleanLivingBroker; + } + + public CleanControllerBrokerDataRequestHeader(String clusterName, String brokerName, String brokerIdSetToClean) { + this(clusterName, brokerName, brokerIdSetToClean, false); + } + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public long getInvokeTime() { + return invokeTime; + } + + public void setInvokeTime(long invokeTime) { + this.invokeTime = invokeTime; + } + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + public String getBrokerControllerIdsToClean() { + return brokerControllerIdsToClean; + } + + public void setBrokerControllerIdsToClean(String brokerIdSetToClean) { + this.brokerControllerIdsToClean = brokerIdSetToClean; + } + + public boolean isCleanLivingBroker() { + return isCleanLivingBroker; + } + + public void setCleanLivingBroker(boolean cleanLivingBroker) { + isCleanLivingBroker = cleanLivingBroker; + } + + @Override + public String toString() { + return "CleanControllerBrokerDataRequestHeader{" + + "clusterName='" + clusterName + '\'' + + ", brokerName='" + brokerName + '\'' + + ", brokerIdSetToClean='" + brokerControllerIdsToClean + '\'' + + ", isCleanLivingBroker=" + isCleanLivingBroker + + '}'; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/register/ApplyBrokerIdRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/register/ApplyBrokerIdRequestHeader.java new file mode 100644 index 00000000000..c577d6a736a --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/register/ApplyBrokerIdRequestHeader.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header.controller.register; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class ApplyBrokerIdRequestHeader implements CommandCustomHeader { + + private String clusterName; + + private String brokerName; + + private Long appliedBrokerId; + + private String registerCheckCode; + + public ApplyBrokerIdRequestHeader() { + + } + + public ApplyBrokerIdRequestHeader(String clusterName, String brokerName, Long appliedBrokerId, String registerCheckCode) { + this.clusterName = clusterName; + this.brokerName = brokerName; + this.appliedBrokerId = appliedBrokerId; + this.registerCheckCode = registerCheckCode; + } + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getClusterName() { + return clusterName; + } + + public String getBrokerName() { + return brokerName; + } + + public Long getAppliedBrokerId() { + return appliedBrokerId; + } + + public String getRegisterCheckCode() { + return registerCheckCode; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + public void setAppliedBrokerId(Long appliedBrokerId) { + this.appliedBrokerId = appliedBrokerId; + } + + public void setRegisterCheckCode(String registerCheckCode) { + this.registerCheckCode = registerCheckCode; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/register/ApplyBrokerIdResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/register/ApplyBrokerIdResponseHeader.java new file mode 100644 index 00000000000..a7f100f778d --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/register/ApplyBrokerIdResponseHeader.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header.controller.register; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class ApplyBrokerIdResponseHeader implements CommandCustomHeader { + + private String clusterName; + + private String brokerName; + + public ApplyBrokerIdResponseHeader() { + } + + public ApplyBrokerIdResponseHeader(String clusterName, String brokerName) { + this.clusterName = clusterName; + this.brokerName = brokerName; + } + + + @Override + public String toString() { + return "ApplyBrokerIdResponseHeader{" + + "clusterName='" + clusterName + '\'' + + ", brokerName='" + brokerName + '\'' + + '}'; + } + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/register/GetNextBrokerIdRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/register/GetNextBrokerIdRequestHeader.java new file mode 100644 index 00000000000..aeb222955e0 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/register/GetNextBrokerIdRequestHeader.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header.controller.register; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class GetNextBrokerIdRequestHeader implements CommandCustomHeader { + + private String clusterName; + + private String brokerName; + + public GetNextBrokerIdRequestHeader() { + + } + + public GetNextBrokerIdRequestHeader(String clusterName, String brokerName) { + this.clusterName = clusterName; + this.brokerName = brokerName; + } + + @Override + public String toString() { + return "GetNextBrokerIdRequestHeader{" + + "clusterName='" + clusterName + '\'' + + ", brokerName='" + brokerName + '\'' + + '}'; + } + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getClusterName() { + return clusterName; + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/register/GetNextBrokerIdResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/register/GetNextBrokerIdResponseHeader.java new file mode 100644 index 00000000000..7d62722d4df --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/register/GetNextBrokerIdResponseHeader.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header.controller.register; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class GetNextBrokerIdResponseHeader implements CommandCustomHeader { + + private String clusterName; + + private String brokerName; + + private Long nextBrokerId; + + public GetNextBrokerIdResponseHeader() { + } + + public GetNextBrokerIdResponseHeader(String clusterName, String brokerName) { + this(clusterName, brokerName, null); + } + + public GetNextBrokerIdResponseHeader(String clusterName, String brokerName, Long nextBrokerId) { + this.clusterName = clusterName; + this.brokerName = brokerName; + this.nextBrokerId = nextBrokerId; + } + + @Override + public String toString() { + return "GetNextBrokerIdResponseHeader{" + + "clusterName='" + clusterName + '\'' + + ", brokerName='" + brokerName + '\'' + + ", nextBrokerId=" + nextBrokerId + + '}'; + } + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public void setNextBrokerId(Long nextBrokerId) { + this.nextBrokerId = nextBrokerId; + } + + public Long getNextBrokerId() { + return nextBrokerId; + } + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/register/RegisterBrokerToControllerRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/register/RegisterBrokerToControllerRequestHeader.java new file mode 100644 index 00000000000..6efab166666 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/register/RegisterBrokerToControllerRequestHeader.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header.controller.register; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class RegisterBrokerToControllerRequestHeader implements CommandCustomHeader { + + private String clusterName; + + private String brokerName; + + private Long brokerId; + + private String brokerAddress; + + private long invokeTime; + + public RegisterBrokerToControllerRequestHeader() { + } + + public RegisterBrokerToControllerRequestHeader(String clusterName, String brokerName, Long brokerId, String brokerAddress) { + this.clusterName = clusterName; + this.brokerName = brokerName; + this.brokerId = brokerId; + this.brokerAddress = brokerAddress; + this.invokeTime = System.currentTimeMillis(); + } + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public long getInvokeTime() { + return invokeTime; + } + + public void setInvokeTime(long invokeTime) { + this.invokeTime = invokeTime; + } + + public String getClusterName() { + return clusterName; + } + + public String getBrokerName() { + return brokerName; + } + + public Long getBrokerId() { + return brokerId; + } + + public String getBrokerAddress() { + return brokerAddress; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + public void setBrokerId(Long brokerId) { + this.brokerId = brokerId; + } + + public void setBrokerAddress(String brokerAddress) { + this.brokerAddress = brokerAddress; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/register/RegisterBrokerToControllerResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/register/RegisterBrokerToControllerResponseHeader.java new file mode 100644 index 00000000000..66bf0e44151 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/controller/register/RegisterBrokerToControllerResponseHeader.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header.controller.register; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class RegisterBrokerToControllerResponseHeader implements CommandCustomHeader { + + private String clusterName; + + private String brokerName; + + private Long masterBrokerId; + + private String masterAddress; + + private Integer masterEpoch; + + private Integer syncStateSetEpoch; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public RegisterBrokerToControllerResponseHeader() { + } + + public RegisterBrokerToControllerResponseHeader(String clusterName, String brokerName) { + this.clusterName = clusterName; + this.brokerName = brokerName; + } + + public void setMasterBrokerId(Long masterBrokerId) { + this.masterBrokerId = masterBrokerId; + } + + public void setMasterAddress(String masterAddress) { + this.masterAddress = masterAddress; + } + + public void setMasterEpoch(Integer masterEpoch) { + this.masterEpoch = masterEpoch; + } + + public void setSyncStateSetEpoch(Integer syncStateSetEpoch) { + this.syncStateSetEpoch = syncStateSetEpoch; + } + + public Integer getMasterEpoch() { + return masterEpoch; + } + + public Integer getSyncStateSetEpoch() { + return syncStateSetEpoch; + } + + public String getClusterName() { + return clusterName; + } + + public String getBrokerName() { + return brokerName; + } + + public Long getMasterBrokerId() { + return masterBrokerId; + } + + public String getMasterAddress() { + return masterAddress; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/AddWritePermOfBrokerRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/AddWritePermOfBrokerRequestHeader.java new file mode 100644 index 00000000000..6c50916282e --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/AddWritePermOfBrokerRequestHeader.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header.namesrv; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class AddWritePermOfBrokerRequestHeader implements CommandCustomHeader { + @CFNotNull + private String brokerName; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/AddWritePermOfBrokerResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/AddWritePermOfBrokerResponseHeader.java new file mode 100644 index 00000000000..50bf6a9ed11 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/AddWritePermOfBrokerResponseHeader.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header.namesrv; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class AddWritePermOfBrokerResponseHeader implements CommandCustomHeader { + @CFNotNull + private Integer addTopicCount; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public Integer getAddTopicCount() { + return addTopicCount; + } + + public void setAddTopicCount(Integer addTopicCount) { + this.addTopicCount = addTopicCount; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/BrokerHeartbeatRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/BrokerHeartbeatRequestHeader.java new file mode 100644 index 00000000000..eb7332fdf40 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/BrokerHeartbeatRequestHeader.java @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header.namesrv; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.annotation.CFNullable; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class BrokerHeartbeatRequestHeader implements CommandCustomHeader { + @CFNotNull + private String clusterName; + @CFNotNull + private String brokerAddr; + @CFNotNull + private String brokerName; + @CFNullable + private Long brokerId; + @CFNullable + private Integer epoch; + @CFNullable + private Long maxOffset; + @CFNullable + private Long confirmOffset; + @CFNullable + private Long heartbeatTimeoutMills; + @CFNullable + private Integer electionPriority; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getBrokerAddr() { + return brokerAddr; + } + + public void setBrokerAddr(String brokerAddr) { + this.brokerAddr = brokerAddr; + } + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + public Integer getEpoch() { + return epoch; + } + + public void setEpoch(Integer epoch) { + this.epoch = epoch; + } + + public Long getMaxOffset() { + return maxOffset; + } + + public void setMaxOffset(Long maxOffset) { + this.maxOffset = maxOffset; + } + + public Long getConfirmOffset() { + return confirmOffset; + } + + public void setConfirmOffset(Long confirmOffset) { + this.confirmOffset = confirmOffset; + } + + public Long getBrokerId() { + return brokerId; + } + + public void setBrokerId(Long brokerId) { + this.brokerId = brokerId; + } + + public Long getHeartbeatTimeoutMills() { + return heartbeatTimeoutMills; + } + + public void setHeartbeatTimeoutMills(Long heartbeatTimeoutMills) { + this.heartbeatTimeoutMills = heartbeatTimeoutMills; + } + + public Integer getElectionPriority() { + return electionPriority; + } + + public void setElectionPriority(Integer electionPriority) { + this.electionPriority = electionPriority; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/DeleteKVConfigRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/DeleteKVConfigRequestHeader.java similarity index 96% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/DeleteKVConfigRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/DeleteKVConfigRequestHeader.java index 3245187cc03..7fcbe97e8db 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/DeleteKVConfigRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/DeleteKVConfigRequestHeader.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header.namesrv; +package org.apache.rocketmq.remoting.protocol.header.namesrv; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/DeleteTopicFromNamesrvRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/DeleteTopicFromNamesrvRequestHeader.java new file mode 100644 index 00000000000..5e5bd8b172e --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/DeleteTopicFromNamesrvRequestHeader.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header.namesrv; + +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; + +public class DeleteTopicFromNamesrvRequestHeader extends TopicRequestHeader { + @CFNotNull + private String topic; + + private String clusterName; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/GetKVConfigRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/GetKVConfigRequestHeader.java similarity index 96% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/GetKVConfigRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/GetKVConfigRequestHeader.java index 14a0340a06d..3a069afeb8a 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/GetKVConfigRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/GetKVConfigRequestHeader.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header.namesrv; +package org.apache.rocketmq.remoting.protocol.header.namesrv; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/GetKVConfigResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/GetKVConfigResponseHeader.java similarity index 95% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/GetKVConfigResponseHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/GetKVConfigResponseHeader.java index ef4859e521d..e5b1113870a 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/GetKVConfigResponseHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/GetKVConfigResponseHeader.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header.namesrv; +package org.apache.rocketmq.remoting.protocol.header.namesrv; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNullable; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/GetKVListByNamespaceRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/GetKVListByNamespaceRequestHeader.java similarity index 95% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/GetKVListByNamespaceRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/GetKVListByNamespaceRequestHeader.java index 6c60b2f96ee..9876161e173 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/GetKVListByNamespaceRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/GetKVListByNamespaceRequestHeader.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header.namesrv; +package org.apache.rocketmq.remoting.protocol.header.namesrv; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/GetRouteInfoRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/GetRouteInfoRequestHeader.java new file mode 100644 index 00000000000..8de15f3fd6d --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/GetRouteInfoRequestHeader.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * $Id: GetRouteInfoRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header.namesrv; + +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.annotation.CFNullable; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; + +public class GetRouteInfoRequestHeader extends TopicRequestHeader { + + @CFNotNull + private String topic; + + @CFNullable + private Boolean acceptStandardJsonOnly; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public Boolean getAcceptStandardJsonOnly() { + return acceptStandardJsonOnly; + } + + public void setAcceptStandardJsonOnly(Boolean acceptStandardJsonOnly) { + this.acceptStandardJsonOnly = acceptStandardJsonOnly; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/PutKVConfigRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/PutKVConfigRequestHeader.java similarity index 96% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/PutKVConfigRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/PutKVConfigRequestHeader.java index 22e17e06f37..60f16cbea35 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/PutKVConfigRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/PutKVConfigRequestHeader.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header.namesrv; +package org.apache.rocketmq.remoting.protocol.header.namesrv; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/QueryDataVersionRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/QueryDataVersionRequestHeader.java new file mode 100644 index 00000000000..2a1e95b2ce7 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/QueryDataVersionRequestHeader.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header.namesrv; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class QueryDataVersionRequestHeader implements CommandCustomHeader { + @CFNotNull + private String brokerName; + @CFNotNull + private String brokerAddr; + @CFNotNull + private String clusterName; + @CFNotNull + private Long brokerId; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + public String getBrokerAddr() { + return brokerAddr; + } + + public void setBrokerAddr(String brokerAddr) { + this.brokerAddr = brokerAddr; + } + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public Long getBrokerId() { + return brokerId; + } + + public void setBrokerId(Long brokerId) { + this.brokerId = brokerId; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/QueryDataVersionResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/QueryDataVersionResponseHeader.java new file mode 100644 index 00000000000..94e83ba8531 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/QueryDataVersionResponseHeader.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header.namesrv; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class QueryDataVersionResponseHeader implements CommandCustomHeader { + @CFNotNull + private Boolean changed; + + @Override + public void checkFields() throws RemotingCommandException { + + } + + public Boolean getChanged() { + return changed; + } + + public void setChanged(Boolean changed) { + this.changed = changed; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("QueryDataVersionResponseHeader{"); + sb.append("changed=").append(changed); + sb.append('}'); + return sb.toString(); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/RegisterBrokerRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/RegisterBrokerRequestHeader.java new file mode 100644 index 00000000000..f97d40daa9d --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/RegisterBrokerRequestHeader.java @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * $Id: RegisterBrokerRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.header.namesrv; + +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.annotation.CFNullable; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; + +public class RegisterBrokerRequestHeader implements CommandCustomHeader { + @CFNotNull + private String brokerName; + @CFNotNull + private String brokerAddr; + @CFNotNull + private String clusterName; + @CFNotNull + private String haServerAddr; + @CFNotNull + private Long brokerId; + @CFNullable + private Long heartbeatTimeoutMillis; + @CFNullable + private Boolean enableActingMaster; + + private boolean compressed; + + private Integer bodyCrc32 = 0; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + public String getBrokerAddr() { + return brokerAddr; + } + + public void setBrokerAddr(String brokerAddr) { + this.brokerAddr = brokerAddr; + } + + public String getClusterName() { + return clusterName; + } + + public void setClusterName(String clusterName) { + this.clusterName = clusterName; + } + + public String getHaServerAddr() { + return haServerAddr; + } + + public void setHaServerAddr(String haServerAddr) { + this.haServerAddr = haServerAddr; + } + + public Long getBrokerId() { + return brokerId; + } + + public void setBrokerId(Long brokerId) { + this.brokerId = brokerId; + } + + public Long getHeartbeatTimeoutMillis() { + return heartbeatTimeoutMillis; + } + + public void setHeartbeatTimeoutMillis(Long heartbeatTimeoutMillis) { + this.heartbeatTimeoutMillis = heartbeatTimeoutMillis; + } + + public boolean isCompressed() { + return compressed; + } + + public void setCompressed(boolean compressed) { + this.compressed = compressed; + } + + public Integer getBodyCrc32() { + return bodyCrc32; + } + + public void setBodyCrc32(Integer bodyCrc32) { + this.bodyCrc32 = bodyCrc32; + } + + public Boolean getEnableActingMaster() { + return enableActingMaster; + } + + public void setEnableActingMaster(Boolean enableActingMaster) { + this.enableActingMaster = enableActingMaster; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/RegisterBrokerResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/RegisterBrokerResponseHeader.java similarity index 96% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/RegisterBrokerResponseHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/RegisterBrokerResponseHeader.java index da4b56c8823..0e35187f3c6 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/RegisterBrokerResponseHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/RegisterBrokerResponseHeader.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header.namesrv; +package org.apache.rocketmq.remoting.protocol.header.namesrv; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNullable; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/RegisterOrderTopicRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/RegisterOrderTopicRequestHeader.java similarity index 96% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/RegisterOrderTopicRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/RegisterOrderTopicRequestHeader.java index 8307e20b712..39bb83350dd 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/RegisterOrderTopicRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/RegisterOrderTopicRequestHeader.java @@ -18,7 +18,7 @@ /** * $Id: RegisterOrderTopicRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.header.namesrv; +package org.apache.rocketmq.remoting.protocol.header.namesrv; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/RegisterTopicRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/RegisterTopicRequestHeader.java new file mode 100644 index 00000000000..d93a05824e5 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/RegisterTopicRequestHeader.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header.namesrv; + +import org.apache.rocketmq.remoting.annotation.CFNotNull; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.rpc.TopicRequestHeader; + +public class RegisterTopicRequestHeader extends TopicRequestHeader { + @CFNotNull + private String topic; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/UnRegisterBrokerRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/UnRegisterBrokerRequestHeader.java similarity index 97% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/UnRegisterBrokerRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/UnRegisterBrokerRequestHeader.java index 6787ecfa755..e0609791f77 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/UnRegisterBrokerRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/UnRegisterBrokerRequestHeader.java @@ -18,7 +18,7 @@ /** * $Id: UnRegisterBrokerRequestHeader.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.header.namesrv; +package org.apache.rocketmq.remoting.protocol.header.namesrv; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/WipeWritePermOfBrokerRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/WipeWritePermOfBrokerRequestHeader.java similarity index 95% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/WipeWritePermOfBrokerRequestHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/WipeWritePermOfBrokerRequestHeader.java index 2be8fe6cb6a..edd87d1111d 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/WipeWritePermOfBrokerRequestHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/WipeWritePermOfBrokerRequestHeader.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header.namesrv; +package org.apache.rocketmq.remoting.protocol.header.namesrv; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/WipeWritePermOfBrokerResponseHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/WipeWritePermOfBrokerResponseHeader.java similarity index 95% rename from common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/WipeWritePermOfBrokerResponseHeader.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/WipeWritePermOfBrokerResponseHeader.java index 0fc29523dc5..bd09e4b82c7 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/header/namesrv/WipeWritePermOfBrokerResponseHeader.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/namesrv/WipeWritePermOfBrokerResponseHeader.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.rocketmq.common.protocol.header.namesrv; +package org.apache.rocketmq.remoting.protocol.header.namesrv; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/heartbeat/ConsumeType.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/ConsumeType.java similarity index 90% rename from common/src/main/java/org/apache/rocketmq/common/protocol/heartbeat/ConsumeType.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/ConsumeType.java index f2554dea7a2..fbcca5d5eff 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/heartbeat/ConsumeType.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/ConsumeType.java @@ -18,13 +18,15 @@ /** * $Id: ConsumeType.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.heartbeat; +package org.apache.rocketmq.remoting.protocol.heartbeat; public enum ConsumeType { CONSUME_ACTIVELY("PULL"), - CONSUME_PASSIVELY("PUSH"); + CONSUME_PASSIVELY("PUSH"), + + CONSUME_POP("POP"); private String typeCN; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/heartbeat/ConsumerData.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/ConsumerData.java similarity index 97% rename from common/src/main/java/org/apache/rocketmq/common/protocol/heartbeat/ConsumerData.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/ConsumerData.java index d4605d069aa..fe1e8dfa8ff 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/heartbeat/ConsumerData.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/ConsumerData.java @@ -18,7 +18,7 @@ /** * $Id: ConsumerData.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.heartbeat; +package org.apache.rocketmq.remoting.protocol.heartbeat; import java.util.HashSet; import java.util.Set; @@ -29,7 +29,7 @@ public class ConsumerData { private ConsumeType consumeType; private MessageModel messageModel; private ConsumeFromWhere consumeFromWhere; - private Set subscriptionDataSet = new HashSet(); + private Set subscriptionDataSet = new HashSet<>(); private boolean unitMode; public String getGroupName() { diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/HeartbeatData.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/HeartbeatData.java new file mode 100644 index 00000000000..f7b4b9faeff --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/HeartbeatData.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * $Id: HeartbeatData.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.heartbeat; + +import java.util.HashSet; +import java.util.Set; +import com.alibaba.fastjson.JSON; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class HeartbeatData extends RemotingSerializable { + private String clientID; + private Set producerDataSet = new HashSet<>(); + private Set consumerDataSet = new HashSet<>(); + private int heartbeatFingerprint = 0; + private boolean isWithoutSub = false; + + public String getClientID() { + return clientID; + } + + public void setClientID(String clientID) { + this.clientID = clientID; + } + + public Set getProducerDataSet() { + return producerDataSet; + } + + public void setProducerDataSet(Set producerDataSet) { + this.producerDataSet = producerDataSet; + } + + public Set getConsumerDataSet() { + return consumerDataSet; + } + + public void setConsumerDataSet(Set consumerDataSet) { + this.consumerDataSet = consumerDataSet; + } + + public int getHeartbeatFingerprint() { + return heartbeatFingerprint; + } + + public void setHeartbeatFingerprint(int heartbeatFingerprint) { + this.heartbeatFingerprint = heartbeatFingerprint; + } + + public boolean isWithoutSub() { + return isWithoutSub; + } + + public void setWithoutSub(boolean withoutSub) { + isWithoutSub = withoutSub; + } + + @Override + public String toString() { + return "HeartbeatData [clientID=" + clientID + ", producerDataSet=" + producerDataSet + + ", consumerDataSet=" + consumerDataSet + "]"; + } + + public int computeHeartbeatFingerprint() { + HeartbeatData heartbeatDataCopy = JSON.parseObject(JSON.toJSONString(this), HeartbeatData.class); + for (ConsumerData consumerData : heartbeatDataCopy.getConsumerDataSet()) { + for (SubscriptionData subscriptionData : consumerData.getSubscriptionDataSet()) { + subscriptionData.setSubVersion(0L); + } + } + heartbeatDataCopy.setWithoutSub(false); + heartbeatDataCopy.setHeartbeatFingerprint(0); + heartbeatDataCopy.setClientID(""); + return JSON.toJSONString(heartbeatDataCopy).hashCode(); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/heartbeat/MessageModel.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/MessageModel.java similarity index 95% rename from common/src/main/java/org/apache/rocketmq/common/protocol/heartbeat/MessageModel.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/MessageModel.java index defe676f6ea..11f2e6c9ec4 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/heartbeat/MessageModel.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/MessageModel.java @@ -18,7 +18,7 @@ /** * $Id: MessageModel.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.heartbeat; +package org.apache.rocketmq.remoting.protocol.heartbeat; /** * Message model diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/heartbeat/ProducerData.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/ProducerData.java similarity index 95% rename from common/src/main/java/org/apache/rocketmq/common/protocol/heartbeat/ProducerData.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/ProducerData.java index 279996a7948..ebf5fc44547 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/heartbeat/ProducerData.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/ProducerData.java @@ -18,7 +18,7 @@ /** * $Id: ProducerData.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.heartbeat; +package org.apache.rocketmq.remoting.protocol.heartbeat; public class ProducerData { private String groupName; diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/heartbeat/SubscriptionData.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/SubscriptionData.java similarity index 95% rename from common/src/main/java/org/apache/rocketmq/common/protocol/heartbeat/SubscriptionData.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/SubscriptionData.java index e456b7e606a..59088fc42e5 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/heartbeat/SubscriptionData.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/heartbeat/SubscriptionData.java @@ -18,21 +18,22 @@ /** * $Id: SubscriptionData.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.heartbeat; +package org.apache.rocketmq.remoting.protocol.heartbeat; import com.alibaba.fastjson.annotation.JSONField; import java.util.HashSet; import java.util.Set; +import org.apache.rocketmq.common.filter.ExpressionType; public class SubscriptionData implements Comparable { public final static String SUB_ALL = "*"; private boolean classFilterMode = false; private String topic; private String subString; - private Set tagsSet = new HashSet(); - private Set codeSet = new HashSet(); + private Set tagsSet = new HashSet<>(); + private Set codeSet = new HashSet<>(); private long subVersion = System.currentTimeMillis(); - private String expressionType; + private String expressionType = ExpressionType.TAG; @JSONField(serialize = false) private String filterClassSource; diff --git a/common/src/main/java/org/apache/rocketmq/common/namesrv/RegisterBrokerResult.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/namesrv/RegisterBrokerResult.java similarity index 92% rename from common/src/main/java/org/apache/rocketmq/common/namesrv/RegisterBrokerResult.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/namesrv/RegisterBrokerResult.java index 5d803332db0..edbed3e843f 100644 --- a/common/src/main/java/org/apache/rocketmq/common/namesrv/RegisterBrokerResult.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/namesrv/RegisterBrokerResult.java @@ -15,9 +15,9 @@ * limitations under the License. */ -package org.apache.rocketmq.common.namesrv; +package org.apache.rocketmq.remoting.protocol.namesrv; -import org.apache.rocketmq.common.protocol.body.KVTable; +import org.apache.rocketmq.remoting.protocol.body.KVTable; public class RegisterBrokerResult { private String haServerAddr; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/route/BrokerData.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/route/BrokerData.java new file mode 100644 index 00000000000..de911d17c26 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/route/BrokerData.java @@ -0,0 +1,182 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.route; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Random; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.MixAll; + +/** + * The class describes that a typical broker cluster's (in replication) details: the cluster (in sharding) name + * that it belongs to, and all the single instance information for this cluster. + */ +public class BrokerData implements Comparable { + private String cluster; + private String brokerName; + + /** + * The container that store the all single instances for the current broker replication cluster. + * The key is the brokerId, and the value is the address of the single broker instance. + */ + private HashMap brokerAddrs; + private String zoneName; + private final Random random = new Random(); + + /** + * Enable acting master or not, used for old version HA adaption, + */ + private boolean enableActingMaster = false; + + public BrokerData() { + + } + + public BrokerData(BrokerData brokerData) { + this.cluster = brokerData.cluster; + this.brokerName = brokerData.brokerName; + if (brokerData.brokerAddrs != null) { + this.brokerAddrs = new HashMap<>(brokerData.brokerAddrs); + } + this.zoneName = brokerData.zoneName; + this.enableActingMaster = brokerData.enableActingMaster; + } + + public BrokerData(String cluster, String brokerName, HashMap brokerAddrs) { + this.cluster = cluster; + this.brokerName = brokerName; + this.brokerAddrs = brokerAddrs; + } + + public BrokerData(String cluster, String brokerName, HashMap brokerAddrs, + boolean enableActingMaster) { + this.cluster = cluster; + this.brokerName = brokerName; + this.brokerAddrs = brokerAddrs; + this.enableActingMaster = enableActingMaster; + } + + public BrokerData(String cluster, String brokerName, HashMap brokerAddrs, boolean enableActingMaster, + String zoneName) { + this.cluster = cluster; + this.brokerName = brokerName; + this.brokerAddrs = brokerAddrs; + this.enableActingMaster = enableActingMaster; + this.zoneName = zoneName; + } + + /** + * Selects a (preferably master) broker address from the registered list. If the master's address cannot be found, a + * slave broker address is selected in a random manner. + * + * @return Broker address. + */ + public String selectBrokerAddr() { + String masterAddress = this.brokerAddrs.get(MixAll.MASTER_ID); + + if (masterAddress == null) { + List addrs = new ArrayList<>(brokerAddrs.values()); + return addrs.get(random.nextInt(addrs.size())); + } + + return masterAddress; + } + + public HashMap getBrokerAddrs() { + return brokerAddrs; + } + + public void setBrokerAddrs(HashMap brokerAddrs) { + this.brokerAddrs = brokerAddrs; + } + + public String getCluster() { + return cluster; + } + + public void setCluster(String cluster) { + this.cluster = cluster; + } + + public boolean isEnableActingMaster() { + return enableActingMaster; + } + + public void setEnableActingMaster(boolean enableActingMaster) { + this.enableActingMaster = enableActingMaster; + } + + public String getZoneName() { + return zoneName; + } + + public void setZoneName(String zoneName) { + this.zoneName = zoneName; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((brokerAddrs == null) ? 0 : brokerAddrs.hashCode()); + result = prime * result + ((brokerName == null) ? 0 : brokerName.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + BrokerData other = (BrokerData) obj; + if (brokerAddrs == null) { + if (other.brokerAddrs != null) { + return false; + } + } else if (!brokerAddrs.equals(other.brokerAddrs)) { + return false; + } + return StringUtils.equals(brokerName, other.brokerName); + } + + @Override + public String toString() { + return "BrokerData [brokerName=" + brokerName + ", brokerAddrs=" + brokerAddrs + ", enableActingMaster=" + enableActingMaster + "]"; + } + + @Override + public int compareTo(BrokerData o) { + return this.brokerName.compareTo(o.getBrokerName()); + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/route/MessageQueueRouteState.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/route/MessageQueueRouteState.java new file mode 100644 index 00000000000..0e43a6f3812 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/route/MessageQueueRouteState.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.route; + +public enum MessageQueueRouteState { + // do not change below order, since ordinal() is used + Expired, + ReadOnly, + Normal, + WriteOnly, + ; +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/route/QueueData.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/route/QueueData.java similarity index 78% rename from common/src/main/java/org/apache/rocketmq/common/protocol/route/QueueData.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/route/QueueData.java index 72a21f384cb..3678e400758 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/route/QueueData.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/route/QueueData.java @@ -15,17 +15,30 @@ * limitations under the License. */ -/** - * $Id: QueueData.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ +/* + $Id: QueueData.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ */ -package org.apache.rocketmq.common.protocol.route; +package org.apache.rocketmq.remoting.protocol.route; public class QueueData implements Comparable { private String brokerName; private int readQueueNums; private int writeQueueNums; private int perm; - private int topicSynFlag; + private int topicSysFlag; + + public QueueData() { + + } + + // Deep copy QueueData + public QueueData(QueueData queueData) { + this.brokerName = queueData.brokerName; + this.readQueueNums = queueData.readQueueNums; + this.writeQueueNums = queueData.writeQueueNums; + this.perm = queueData.perm; + this.topicSysFlag = queueData.topicSysFlag; + } public int getReadQueueNums() { return readQueueNums; @@ -51,12 +64,12 @@ public void setPerm(int perm) { this.perm = perm; } - public int getTopicSynFlag() { - return topicSynFlag; + public int getTopicSysFlag() { + return topicSysFlag; } - public void setTopicSynFlag(int topicSynFlag) { - this.topicSynFlag = topicSynFlag; + public void setTopicSysFlag(int topicSysFlag) { + this.topicSysFlag = topicSysFlag; } @Override @@ -67,7 +80,7 @@ public int hashCode() { result = prime * result + perm; result = prime * result + readQueueNums; result = prime * result + writeQueueNums; - result = prime * result + topicSynFlag; + result = prime * result + topicSysFlag; return result; } @@ -91,15 +104,13 @@ public boolean equals(Object obj) { return false; if (writeQueueNums != other.writeQueueNums) return false; - if (topicSynFlag != other.topicSynFlag) - return false; - return true; + return topicSysFlag == other.topicSysFlag; } @Override public String toString() { return "QueueData [brokerName=" + brokerName + ", readQueueNums=" + readQueueNums - + ", writeQueueNums=" + writeQueueNums + ", perm=" + perm + ", topicSynFlag=" + topicSynFlag + + ", writeQueueNums=" + writeQueueNums + ", perm=" + perm + ", topicSysFlag=" + topicSysFlag + "]"; } diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/route/TopicRouteData.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/route/TopicRouteData.java new file mode 100644 index 00000000000..2ef9923eb7c --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/route/TopicRouteData.java @@ -0,0 +1,226 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * $Id: TopicRouteData.java 1835 2013-05-16 02:00:50Z vintagewang@apache.org $ + */ +package org.apache.rocketmq.remoting.protocol.route; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingInfo; + +public class TopicRouteData extends RemotingSerializable { + private String orderTopicConf; + private List queueDatas; + private List brokerDatas; + private HashMap/* Filter Server */> filterServerTable; + //It could be null or empty + private Map topicQueueMappingByBroker; + + public TopicRouteData() { + queueDatas = new ArrayList<>(); + brokerDatas = new ArrayList<>(); + filterServerTable = new HashMap<>(); + } + + public TopicRouteData(TopicRouteData topicRouteData) { + this.queueDatas = new ArrayList<>(); + this.brokerDatas = new ArrayList<>(); + this.filterServerTable = new HashMap<>(); + this.orderTopicConf = topicRouteData.orderTopicConf; + + if (topicRouteData.queueDatas != null) { + this.queueDatas.addAll(topicRouteData.queueDatas); + } + + if (topicRouteData.brokerDatas != null) { + this.brokerDatas.addAll(topicRouteData.brokerDatas); + } + + if (topicRouteData.filterServerTable != null) { + this.filterServerTable.putAll(topicRouteData.filterServerTable); + } + + if (topicRouteData.topicQueueMappingByBroker != null) { + this.topicQueueMappingByBroker = new HashMap<>(topicRouteData.topicQueueMappingByBroker); + } + } + + public TopicRouteData cloneTopicRouteData() { + TopicRouteData topicRouteData = new TopicRouteData(); + topicRouteData.setQueueDatas(new ArrayList<>()); + topicRouteData.setBrokerDatas(new ArrayList<>()); + topicRouteData.setFilterServerTable(new HashMap<>()); + topicRouteData.setOrderTopicConf(this.orderTopicConf); + + topicRouteData.getQueueDatas().addAll(this.queueDatas); + topicRouteData.getBrokerDatas().addAll(this.brokerDatas); + topicRouteData.getFilterServerTable().putAll(this.filterServerTable); + if (this.topicQueueMappingByBroker != null) { + Map cloneMap = new HashMap<>(this.topicQueueMappingByBroker); + topicRouteData.setTopicQueueMappingByBroker(cloneMap); + } + return topicRouteData; + } + + public TopicRouteData deepCloneTopicRouteData() { + TopicRouteData topicRouteData = new TopicRouteData(); + + topicRouteData.setOrderTopicConf(this.orderTopicConf); + + for (final QueueData queueData : this.queueDatas) { + topicRouteData.getQueueDatas().add(new QueueData(queueData)); + } + + for (final BrokerData brokerData : this.brokerDatas) { + topicRouteData.getBrokerDatas().add(new BrokerData(brokerData)); + } + + for (final Map.Entry> listEntry : this.filterServerTable.entrySet()) { + topicRouteData.getFilterServerTable().put(listEntry.getKey(), + new ArrayList<>(listEntry.getValue())); + } + if (this.topicQueueMappingByBroker != null) { + Map cloneMap = new HashMap<>(this.topicQueueMappingByBroker.size()); + for (final Map.Entry entry : this.getTopicQueueMappingByBroker().entrySet()) { + TopicQueueMappingInfo topicQueueMappingInfo = new TopicQueueMappingInfo(entry.getValue().getTopic(), entry.getValue().getTotalQueues(), entry.getValue().getBname(), entry.getValue().getEpoch()); + topicQueueMappingInfo.setDirty(entry.getValue().isDirty()); + topicQueueMappingInfo.setScope(entry.getValue().getScope()); + ConcurrentMap concurrentMap = new ConcurrentHashMap<>(entry.getValue().getCurrIdMap()); + topicQueueMappingInfo.setCurrIdMap(concurrentMap); + cloneMap.put(entry.getKey(), topicQueueMappingInfo); + } + topicRouteData.setTopicQueueMappingByBroker(cloneMap); + } + + return topicRouteData; + } + + public boolean topicRouteDataChanged(TopicRouteData oldData) { + if (oldData == null) + return true; + TopicRouteData old = new TopicRouteData(oldData); + TopicRouteData now = new TopicRouteData(this); + Collections.sort(old.getQueueDatas()); + Collections.sort(old.getBrokerDatas()); + Collections.sort(now.getQueueDatas()); + Collections.sort(now.getBrokerDatas()); + return !old.equals(now); + } + + public List getQueueDatas() { + return queueDatas; + } + + public void setQueueDatas(List queueDatas) { + this.queueDatas = queueDatas; + } + + public List getBrokerDatas() { + return brokerDatas; + } + + public void setBrokerDatas(List brokerDatas) { + this.brokerDatas = brokerDatas; + } + + public HashMap> getFilterServerTable() { + return filterServerTable; + } + + public void setFilterServerTable(HashMap> filterServerTable) { + this.filterServerTable = filterServerTable; + } + + public String getOrderTopicConf() { + return orderTopicConf; + } + + public void setOrderTopicConf(String orderTopicConf) { + this.orderTopicConf = orderTopicConf; + } + + public Map getTopicQueueMappingByBroker() { + return topicQueueMappingByBroker; + } + + public void setTopicQueueMappingByBroker(Map topicQueueMappingByBroker) { + this.topicQueueMappingByBroker = topicQueueMappingByBroker; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((brokerDatas == null) ? 0 : brokerDatas.hashCode()); + result = prime * result + ((orderTopicConf == null) ? 0 : orderTopicConf.hashCode()); + result = prime * result + ((queueDatas == null) ? 0 : queueDatas.hashCode()); + result = prime * result + ((filterServerTable == null) ? 0 : filterServerTable.hashCode()); + result = prime * result + ((topicQueueMappingByBroker == null) ? 0 : topicQueueMappingByBroker.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TopicRouteData other = (TopicRouteData) obj; + if (brokerDatas == null) { + if (other.brokerDatas != null) + return false; + } else if (!brokerDatas.equals(other.brokerDatas)) + return false; + if (orderTopicConf == null) { + if (other.orderTopicConf != null) + return false; + } else if (!orderTopicConf.equals(other.orderTopicConf)) + return false; + if (queueDatas == null) { + if (other.queueDatas != null) + return false; + } else if (!queueDatas.equals(other.queueDatas)) + return false; + if (filterServerTable == null) { + if (other.filterServerTable != null) + return false; + } else if (!filterServerTable.equals(other.filterServerTable)) + return false; + if (topicQueueMappingByBroker == null) { + if (other.topicQueueMappingByBroker != null) + return false; + } else if (!topicQueueMappingByBroker.equals(other.topicQueueMappingByBroker)) + return false; + return true; + } + + @Override + public String toString() { + return "TopicRouteData [orderTopicConf=" + orderTopicConf + ", queueDatas=" + queueDatas + + ", brokerDatas=" + brokerDatas + ", filterServerTable=" + filterServerTable + ", topicQueueMappingInfoTable=" + topicQueueMappingByBroker + "]"; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/statictopic/LogicQueueMappingItem.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/statictopic/LogicQueueMappingItem.java new file mode 100644 index 00000000000..0c5bbb6a974 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/statictopic/LogicQueueMappingItem.java @@ -0,0 +1,212 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.statictopic; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class LogicQueueMappingItem extends RemotingSerializable { + + private int gen; // immutable + private int queueId; //, immutable + private String bname; //important, immutable + private long logicOffset; // the start of the logic offset, important, can be changed by command only once + private long startOffset; // the start of the physical offset, should always be 0, immutable + private long endOffset = -1; // the end of the physical offset, excluded, revered -1, mutable + private long timeOfStart = -1; // mutable, reserved + private long timeOfEnd = -1; // mutable, reserved + + //make sure it has a default constructor + public LogicQueueMappingItem() { + + } + + public LogicQueueMappingItem(int gen, int queueId, String bname, long logicOffset, long startOffset, long endOffset, long timeOfStart, long timeOfEnd) { + this.gen = gen; + this.queueId = queueId; + this.bname = bname; + this.logicOffset = logicOffset; + this.startOffset = startOffset; + this.endOffset = endOffset; + this.timeOfStart = timeOfStart; + this.timeOfEnd = timeOfEnd; + } + + + //should only be user in sendMessage and getMinOffset + public long computeStaticQueueOffsetLoosely(long physicalQueueOffset) { + //consider the newly mapped item + if (logicOffset < 0) { + return -1; + } + if (physicalQueueOffset < startOffset) { + return logicOffset; + } + if (endOffset >= startOffset + && endOffset < physicalQueueOffset) { + return logicOffset + (endOffset - startOffset); + } + return logicOffset + (physicalQueueOffset - startOffset); + } + + public long computeStaticQueueOffsetStrictly(long physicalQueueOffset) { + assert logicOffset >= 0; + + if (physicalQueueOffset < startOffset) { + return logicOffset; + } + return logicOffset + (physicalQueueOffset - startOffset); + } + + public long computePhysicalQueueOffset(long staticQueueOffset) { + return (staticQueueOffset - logicOffset) + startOffset; + } + + public long computeMaxStaticQueueOffset() { + if (endOffset >= startOffset) { + return logicOffset + endOffset - startOffset; + } else { + return logicOffset; + } + } + public boolean checkIfEndOffsetDecided() { + //if the endOffset == startOffset, then the item should be deleted + return endOffset > startOffset; + } + + public boolean checkIfLogicoffsetDecided() { + return logicOffset >= 0; + } + + public long computeOffsetDelta() { + return logicOffset - startOffset; + } + + public int getGen() { + return gen; + } + + public int getQueueId() { + return queueId; + } + + public String getBname() { + return bname; + } + + public long getLogicOffset() { + return logicOffset; + } + + public long getStartOffset() { + return startOffset; + } + + public long getEndOffset() { + return endOffset; + } + + public long getTimeOfStart() { + return timeOfStart; + } + + public long getTimeOfEnd() { + return timeOfEnd; + } + + public void setLogicOffset(long logicOffset) { + this.logicOffset = logicOffset; + } + + public void setEndOffset(long endOffset) { + this.endOffset = endOffset; + } + + public void setTimeOfStart(long timeOfStart) { + this.timeOfStart = timeOfStart; + } + + public void setTimeOfEnd(long timeOfEnd) { + this.timeOfEnd = timeOfEnd; + } + + public void setGen(int gen) { + this.gen = gen; + } + + public void setQueueId(int queueId) { + this.queueId = queueId; + } + + public void setBname(String bname) { + this.bname = bname; + } + + public void setStartOffset(long startOffset) { + this.startOffset = startOffset; + } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (!(o instanceof LogicQueueMappingItem)) return false; + + LogicQueueMappingItem item = (LogicQueueMappingItem) o; + + return new EqualsBuilder() + .append(gen, item.gen) + .append(queueId, item.queueId) + .append(logicOffset, item.logicOffset) + .append(startOffset, item.startOffset) + .append(endOffset, item.endOffset) + .append(timeOfStart, item.timeOfStart) + .append(timeOfEnd, item.timeOfEnd) + .append(bname, item.bname) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(gen) + .append(queueId) + .append(bname) + .append(logicOffset) + .append(startOffset) + .append(endOffset) + .append(timeOfStart) + .append(timeOfEnd) + .toHashCode(); + } + + @Override + public String toString() { + return "LogicQueueMappingItem{" + + "gen=" + gen + + ", queueId=" + queueId + + ", bname='" + bname + '\'' + + ", logicOffset=" + logicOffset + + ", startOffset=" + startOffset + + ", endOffset=" + endOffset + + ", timeOfStart=" + timeOfStart + + ", timeOfEnd=" + timeOfEnd + + '}'; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicConfigAndQueueMapping.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicConfigAndQueueMapping.java new file mode 100644 index 00000000000..d13692735e9 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicConfigAndQueueMapping.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.statictopic; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.rocketmq.common.TopicConfig; + +public class TopicConfigAndQueueMapping extends TopicConfig { + private TopicQueueMappingDetail mappingDetail; + + public TopicConfigAndQueueMapping() { + } + + public TopicConfigAndQueueMapping(TopicConfig topicConfig, TopicQueueMappingDetail mappingDetail) { + super(topicConfig); + this.mappingDetail = mappingDetail; + } + + public TopicQueueMappingDetail getMappingDetail() { + return mappingDetail; + } + + public void setMappingDetail(TopicQueueMappingDetail mappingDetail) { + this.mappingDetail = mappingDetail; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (!(o instanceof TopicConfigAndQueueMapping)) return false; + + TopicConfigAndQueueMapping that = (TopicConfigAndQueueMapping) o; + + return new EqualsBuilder() + .appendSuper(super.equals(o)) + .append(mappingDetail, that.mappingDetail) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .appendSuper(super.hashCode()) + .append(mappingDetail) + .toHashCode(); + } + + @Override + public String toString() { + String string = super.toString(); + if (StringUtils.isNotBlank(string)) { + string = string.substring(0, string.length() - 1) + ", mappingDetail=" + mappingDetail + "]"; + } + return string; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicQueueMappingContext.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicQueueMappingContext.java new file mode 100644 index 00000000000..81718c8bc11 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicQueueMappingContext.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.statictopic; + +import com.google.common.collect.ImmutableList; +import java.util.List; + +public class TopicQueueMappingContext { + private String topic; + private Integer globalId; + private TopicQueueMappingDetail mappingDetail; + private List mappingItemList; + private LogicQueueMappingItem leaderItem; + + private LogicQueueMappingItem currentItem; + + public TopicQueueMappingContext(String topic, Integer globalId, TopicQueueMappingDetail mappingDetail, List mappingItemList, LogicQueueMappingItem leaderItem) { + this.topic = topic; + this.globalId = globalId; + this.mappingDetail = mappingDetail; + this.mappingItemList = mappingItemList; + this.leaderItem = leaderItem; + + } + + + public boolean isLeader() { + return leaderItem != null && leaderItem.getBname().equals(mappingDetail.getBname()); + } + + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public Integer getGlobalId() { + return globalId; + } + + public void setGlobalId(Integer globalId) { + this.globalId = globalId; + } + + + public TopicQueueMappingDetail getMappingDetail() { + return mappingDetail; + } + + public void setMappingDetail(TopicQueueMappingDetail mappingDetail) { + this.mappingDetail = mappingDetail; + } + + public List getMappingItemList() { + return mappingItemList; + } + + public void setMappingItemList(ImmutableList mappingItemList) { + this.mappingItemList = mappingItemList; + } + + public LogicQueueMappingItem getLeaderItem() { + return leaderItem; + } + + public void setLeaderItem(LogicQueueMappingItem leaderItem) { + this.leaderItem = leaderItem; + } + + public LogicQueueMappingItem getCurrentItem() { + return currentItem; + } + + public void setCurrentItem(LogicQueueMappingItem currentItem) { + this.currentItem = currentItem; + } + + public void setMappingItemList(List mappingItemList) { + this.mappingItemList = mappingItemList; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicQueueMappingDetail.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicQueueMappingDetail.java new file mode 100644 index 00000000000..5c6e4d29847 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicQueueMappingDetail.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.statictopic; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +public class TopicQueueMappingDetail extends TopicQueueMappingInfo { + + // the mapping info in current broker, do not register to nameserver + // make sure this value is not null + private ConcurrentMap> hostedQueues = new ConcurrentHashMap<>(); + + //make sure there is a default constructor + public TopicQueueMappingDetail() { + + } + + public TopicQueueMappingDetail(String topic, int totalQueues, String bname, long epoch) { + super(topic, totalQueues, bname, epoch); + } + + + + public static boolean putMappingInfo(TopicQueueMappingDetail mappingDetail, Integer globalId, List mappingInfo) { + if (mappingInfo.isEmpty()) { + return true; + } + mappingDetail.hostedQueues.put(globalId, mappingInfo); + return true; + } + + public static List getMappingInfo(TopicQueueMappingDetail mappingDetail, Integer globalId) { + return mappingDetail.hostedQueues.get(globalId); + } + + public static ConcurrentMap buildIdMap(TopicQueueMappingDetail mappingDetail, int level) { + //level 0 means current leader in this broker + //level 1 means previous leader in this broker, reserved for + assert level == LEVEL_0 ; + + if (mappingDetail.hostedQueues == null || mappingDetail.hostedQueues.isEmpty()) { + return new ConcurrentHashMap<>(); + } + ConcurrentMap tmpIdMap = new ConcurrentHashMap<>(); + for (Map.Entry> entry: mappingDetail.hostedQueues.entrySet()) { + Integer globalId = entry.getKey(); + List items = entry.getValue(); + if (level == LEVEL_0 + && items.size() >= 1) { + LogicQueueMappingItem curr = items.get(items.size() - 1); + if (mappingDetail.bname.equals(curr.getBname())) { + tmpIdMap.put(globalId, curr.getQueueId()); + } + } + } + return tmpIdMap; + } + + + public static long computeMaxOffsetFromMapping(TopicQueueMappingDetail mappingDetail, Integer globalId) { + List mappingItems = getMappingInfo(mappingDetail, globalId); + if (mappingItems == null + || mappingItems.isEmpty()) { + return -1; + } + LogicQueueMappingItem item = mappingItems.get(mappingItems.size() - 1); + return item.computeMaxStaticQueueOffset(); + } + + + public static TopicQueueMappingInfo cloneAsMappingInfo(TopicQueueMappingDetail mappingDetail) { + TopicQueueMappingInfo topicQueueMappingInfo = new TopicQueueMappingInfo(mappingDetail.topic, mappingDetail.totalQueues, mappingDetail.bname, mappingDetail.epoch); + topicQueueMappingInfo.currIdMap = TopicQueueMappingDetail.buildIdMap(mappingDetail, LEVEL_0); + return topicQueueMappingInfo; + } + + public static boolean checkIfAsPhysical(TopicQueueMappingDetail mappingDetail, Integer globalId) { + List mappingItems = getMappingInfo(mappingDetail, globalId); + return mappingItems == null + || mappingItems.size() == 1 + && mappingItems.get(0).getLogicOffset() == 0; + } + + public ConcurrentMap> getHostedQueues() { + return hostedQueues; + } + + public void setHostedQueues(ConcurrentMap> hostedQueues) { + this.hostedQueues = hostedQueues; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (!(o instanceof TopicQueueMappingDetail)) return false; + + TopicQueueMappingDetail that = (TopicQueueMappingDetail) o; + + return new EqualsBuilder() + .append(hostedQueues, that.hostedQueues) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(hostedQueues) + .toHashCode(); + } + + @Override + public String toString() { + return "TopicQueueMappingDetail{" + + "hostedQueues=" + hostedQueues + + ", topic='" + topic + '\'' + + ", totalQueues=" + totalQueues + + ", bname='" + bname + '\'' + + ", epoch=" + epoch + + ", dirty=" + dirty + + ", currIdMap=" + currIdMap + + '}'; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicQueueMappingInfo.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicQueueMappingInfo.java new file mode 100644 index 00000000000..4325a429d3a --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicQueueMappingInfo.java @@ -0,0 +1,145 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.statictopic; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class TopicQueueMappingInfo extends RemotingSerializable { + public static final int LEVEL_0 = 0; + + String topic; // redundant field + String scope = MixAll.METADATA_SCOPE_GLOBAL; + int totalQueues; + String bname; //identify the hosted broker name + long epoch; //important to fence the old dirty data + boolean dirty; //indicate if the data is dirty + //register to broker to construct the route + protected ConcurrentMap currIdMap = new ConcurrentHashMap<>(); + + public TopicQueueMappingInfo() { + + } + + public TopicQueueMappingInfo(String topic, int totalQueues, String bname, long epoch) { + this.topic = topic; + this.totalQueues = totalQueues; + this.bname = bname; + this.epoch = epoch; + this.dirty = false; + } + + public boolean isDirty() { + return dirty; + } + + public void setDirty(boolean dirty) { + this.dirty = dirty; + } + + public int getTotalQueues() { + return totalQueues; + } + + + public String getBname() { + return bname; + } + + public String getTopic() { + return topic; + } + + public long getEpoch() { + return epoch; + } + + public void setEpoch(long epoch) { + this.epoch = epoch; + } + + public void setTotalQueues(int totalQueues) { + this.totalQueues = totalQueues; + } + + public ConcurrentMap getCurrIdMap() { + return currIdMap; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public void setBname(String bname) { + this.bname = bname; + } + + public void setCurrIdMap(ConcurrentMap currIdMap) { + this.currIdMap = currIdMap; + } + + public String getScope() { + return scope; + } + + public void setScope(String scope) { + this.scope = scope; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof TopicQueueMappingInfo)) return false; + + TopicQueueMappingInfo info = (TopicQueueMappingInfo) o; + + if (totalQueues != info.totalQueues) return false; + if (epoch != info.epoch) return false; + if (dirty != info.dirty) return false; + if (topic != null ? !topic.equals(info.topic) : info.topic != null) return false; + if (scope != null ? !scope.equals(info.scope) : info.scope != null) return false; + if (bname != null ? !bname.equals(info.bname) : info.bname != null) return false; + return currIdMap != null ? currIdMap.equals(info.currIdMap) : info.currIdMap == null; + } + + @Override + public int hashCode() { + int result = topic != null ? topic.hashCode() : 0; + result = 31 * result + (scope != null ? scope.hashCode() : 0); + result = 31 * result + totalQueues; + result = 31 * result + (bname != null ? bname.hashCode() : 0); + result = 31 * result + (int) (epoch ^ (epoch >>> 32)); + result = 31 * result + (dirty ? 1 : 0); + result = 31 * result + (currIdMap != null ? currIdMap.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "TopicQueueMappingInfo{" + + "topic='" + topic + '\'' + + ", scope='" + scope + '\'' + + ", totalQueues=" + totalQueues + + ", bname='" + bname + '\'' + + ", epoch=" + epoch + + ", dirty=" + dirty + + ", currIdMap=" + currIdMap + + '}'; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicQueueMappingOne.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicQueueMappingOne.java new file mode 100644 index 00000000000..8cbbd59ee6c --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicQueueMappingOne.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.statictopic; + +import java.util.List; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class TopicQueueMappingOne extends RemotingSerializable { + + String topic; // redundant field + String bname; //identify the hosted broker name + Integer globalId; + List items; + TopicQueueMappingDetail mappingDetail; + + public TopicQueueMappingOne(TopicQueueMappingDetail mappingDetail, String topic, String bname, Integer globalId, List items) { + this.mappingDetail = mappingDetail; + this.topic = topic; + this.bname = bname; + this.globalId = globalId; + this.items = items; + } + + public String getTopic() { + return topic; + } + + public String getBname() { + return bname; + } + + public Integer getGlobalId() { + return globalId; + } + + public List getItems() { + return items; + } + + public TopicQueueMappingDetail getMappingDetail() { + return mappingDetail; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof TopicQueueMappingOne)) + return false; + + TopicQueueMappingOne that = (TopicQueueMappingOne) o; + + if (topic != null ? !topic.equals(that.topic) : that.topic != null) + return false; + if (bname != null ? !bname.equals(that.bname) : that.bname != null) + return false; + if (globalId != null ? !globalId.equals(that.globalId) : that.globalId != null) + return false; + if (items != null ? !items.equals(that.items) : that.items != null) + return false; + return mappingDetail != null ? mappingDetail.equals(that.mappingDetail) : that.mappingDetail == null; + } + + @Override + public int hashCode() { + int result = topic != null ? topic.hashCode() : 0; + result = 31 * result + (bname != null ? bname.hashCode() : 0); + result = 31 * result + (globalId != null ? globalId.hashCode() : 0); + result = 31 * result + (items != null ? items.hashCode() : 0); + result = 31 * result + (mappingDetail != null ? mappingDetail.hashCode() : 0); + return result; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicQueueMappingUtils.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicQueueMappingUtils.java new file mode 100644 index 00000000000..45cbed75727 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicQueueMappingUtils.java @@ -0,0 +1,684 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.statictopic; + +import java.io.File; +import java.util.AbstractMap; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; + +public class TopicQueueMappingUtils { + + public static final int DEFAULT_BLOCK_SEQ_SIZE = 10000; + + public static class MappingAllocator { + Map brokerNumMap = new HashMap<>(); + Map idToBroker = new HashMap<>(); + //used for remapping + Map brokerNumMapBeforeRemapping; + int currentIndex = 0; + List leastBrokers = new ArrayList<>(); + private MappingAllocator(Map idToBroker, Map brokerNumMap, Map brokerNumMapBeforeRemapping) { + this.idToBroker.putAll(idToBroker); + this.brokerNumMap.putAll(brokerNumMap); + this.brokerNumMapBeforeRemapping = brokerNumMapBeforeRemapping; + } + + private void freshState() { + int minNum = Integer.MAX_VALUE; + for (Map.Entry entry : brokerNumMap.entrySet()) { + if (entry.getValue() < minNum) { + leastBrokers.clear(); + leastBrokers.add(entry.getKey()); + minNum = entry.getValue(); + } else if (entry.getValue() == minNum) { + leastBrokers.add(entry.getKey()); + } + } + //reduce the remapping + if (brokerNumMapBeforeRemapping != null + && !brokerNumMapBeforeRemapping.isEmpty()) { + leastBrokers.sort((o1, o2) -> { + int i1 = 0, i2 = 0; + if (brokerNumMapBeforeRemapping.containsKey(o1)) { + i1 = brokerNumMapBeforeRemapping.get(o1); + } + if (brokerNumMapBeforeRemapping.containsKey(o2)) { + i2 = brokerNumMapBeforeRemapping.get(o2); + } + return i1 - i2; + }); + } else { + //reduce the imbalance + Collections.shuffle(leastBrokers); + } + currentIndex = leastBrokers.size() - 1; + } + private String nextBroker() { + if (leastBrokers.isEmpty()) { + freshState(); + } + int tmpIndex = currentIndex % leastBrokers.size(); + return leastBrokers.remove(tmpIndex); + } + + public Map getBrokerNumMap() { + return brokerNumMap; + } + + public void upToNum(int maxQueueNum) { + int currSize = idToBroker.size(); + if (maxQueueNum <= currSize) { + return; + } + for (int i = currSize; i < maxQueueNum; i++) { + String nextBroker = nextBroker(); + if (brokerNumMap.containsKey(nextBroker)) { + brokerNumMap.put(nextBroker, brokerNumMap.get(nextBroker) + 1); + } else { + brokerNumMap.put(nextBroker, 1); + } + idToBroker.put(i, nextBroker); + } + } + + public Map getIdToBroker() { + return idToBroker; + } + } + + + public static MappingAllocator buildMappingAllocator(Map idToBroker, Map brokerNumMap, Map brokerNumMapBeforeRemapping) { + return new MappingAllocator(idToBroker, brokerNumMap, brokerNumMapBeforeRemapping); + } + + public static Map.Entry findMaxEpochAndQueueNum(List mappingDetailList) { + long epoch = -1; + int queueNum = 0; + for (TopicQueueMappingDetail mappingDetail : mappingDetailList) { + if (mappingDetail.getEpoch() > epoch) { + epoch = mappingDetail.getEpoch(); + } + if (mappingDetail.getTotalQueues() > queueNum) { + queueNum = mappingDetail.getTotalQueues(); + } + } + return new AbstractMap.SimpleImmutableEntry<>(epoch, queueNum); + } + + public static List getMappingDetailFromConfig(Collection configs) { + List detailList = new ArrayList<>(); + for (TopicConfigAndQueueMapping configMapping : configs) { + if (configMapping.getMappingDetail() != null) { + detailList.add(configMapping.getMappingDetail()); + } + } + return detailList; + } + + public static Map.Entry checkNameEpochNumConsistence(String topic, Map brokerConfigMap) { + if (brokerConfigMap == null + || brokerConfigMap.isEmpty()) { + return null; + } + //make sure it is not null + long maxEpoch = -1; + int maxNum = -1; + String scope = null; + for (Map.Entry entry : brokerConfigMap.entrySet()) { + String broker = entry.getKey(); + TopicConfigAndQueueMapping configMapping = entry.getValue(); + if (configMapping.getMappingDetail() == null) { + throw new RuntimeException("Mapping info should not be null in broker " + broker); + } + TopicQueueMappingDetail mappingDetail = configMapping.getMappingDetail(); + if (!broker.equals(mappingDetail.getBname())) { + throw new RuntimeException(String.format("The broker name is not equal %s != %s ", broker, mappingDetail.getBname())); + } + if (mappingDetail.isDirty()) { + throw new RuntimeException("The mapping info is dirty in broker " + broker); + } + if (!configMapping.getTopicName().equals(mappingDetail.getTopic())) { + throw new RuntimeException("The topic name is inconsistent in broker " + broker); + } + if (topic != null + && !topic.equals(mappingDetail.getTopic())) { + throw new RuntimeException("The topic name is not match for broker " + broker); + } + + if (scope != null + && !scope.equals(mappingDetail.getScope())) { + throw new RuntimeException(String.format("scope dose not match %s != %s in %s", mappingDetail.getScope(), scope, broker)); + } else { + scope = mappingDetail.getScope(); + } + + if (maxEpoch != -1 + && maxEpoch != mappingDetail.getEpoch()) { + throw new RuntimeException(String.format("epoch dose not match %d != %d in %s", maxEpoch, mappingDetail.getEpoch(), mappingDetail.getBname())); + } else { + maxEpoch = mappingDetail.getEpoch(); + } + + if (maxNum != -1 + && maxNum != mappingDetail.getTotalQueues()) { + throw new RuntimeException(String.format("total queue number dose not match %d != %d in %s", maxNum, mappingDetail.getTotalQueues(), mappingDetail.getBname())); + } else { + maxNum = mappingDetail.getTotalQueues(); + } + } + return new AbstractMap.SimpleEntry<>(maxEpoch, maxNum); + } + + public static String getMockBrokerName(String scope) { + assert scope != null; + if (scope.equals(MixAll.METADATA_SCOPE_GLOBAL)) { + return MixAll.LOGICAL_QUEUE_MOCK_BROKER_PREFIX + scope.substring(2); + } else { + return MixAll.LOGICAL_QUEUE_MOCK_BROKER_PREFIX + scope; + } + } + + public static void makeSureLogicQueueMappingItemImmutable(List oldItems, List newItems, boolean epochEqual, boolean isCLean) { + if (oldItems == null || oldItems.isEmpty()) { + return; + } + if (newItems == null || newItems.isEmpty()) { + throw new RuntimeException("The new item list is null or empty"); + } + int iold = 0, inew = 0; + while (iold < oldItems.size() && inew < newItems.size()) { + LogicQueueMappingItem newItem = newItems.get(inew); + LogicQueueMappingItem oldItem = oldItems.get(iold); + if (newItem.getGen() < oldItem.getGen()) { + //the earliest item may have been deleted concurrently + inew++; + } else if (oldItem.getGen() < newItem.getGen()) { + //in the following cases, the new item-list has fewer items than old item-list + //1. the queue is mapped back to a broker which hold the logic queue before + //2. The earliest item is deleted by TopicQueueMappingCleanService + iold++; + } else { + assert oldItem.getBname().equals(newItem.getBname()); + assert oldItem.getQueueId() == newItem.getQueueId(); + assert oldItem.getStartOffset() == newItem.getStartOffset(); + if (oldItem.getLogicOffset() != -1) { + assert oldItem.getLogicOffset() == newItem.getLogicOffset(); + } + iold++; + inew++; + } + } + if (epochEqual) { + LogicQueueMappingItem oldLeader = oldItems.get(oldItems.size() - 1); + LogicQueueMappingItem newLeader = newItems.get(newItems.size() - 1); + if (newLeader.getGen() != oldLeader.getGen() + || !newLeader.getBname().equals(oldLeader.getBname()) + || newLeader.getQueueId() != oldLeader.getQueueId() + || newLeader.getStartOffset() != oldLeader.getStartOffset()) { + throw new RuntimeException("The new leader is different but epoch equal"); + } + } + } + + + public static void checkLogicQueueMappingItemOffset(List items) { + if (items == null + || items.isEmpty()) { + return; + } + int lastGen = -1; + long lastOffset = -1; + for (int i = items.size() - 1; i >= 0 ; i--) { + LogicQueueMappingItem item = items.get(i); + if (item.getStartOffset() < 0 + || item.getGen() < 0 + || item.getQueueId() < 0) { + throw new RuntimeException("The field is illegal, should not be negative"); + } + if (items.size() >= 2 + && i <= items.size() - 2 + && items.get(i).getLogicOffset() < 0) { + throw new RuntimeException("The non-latest item has negative logic offset"); + } + if (lastGen != -1 && item.getGen() >= lastGen) { + throw new RuntimeException("The gen dose not increase monotonically"); + } + + if (item.getEndOffset() != -1 + && item.getEndOffset() < item.getStartOffset()) { + throw new RuntimeException("The endOffset is smaller than the start offset"); + } + + if (lastOffset != -1 && item.getLogicOffset() != -1) { + if (item.getLogicOffset() >= lastOffset) { + throw new RuntimeException("The base logic offset dose not increase monotonically"); + } + if (item.computeMaxStaticQueueOffset() >= lastOffset) { + throw new RuntimeException("The max logic offset dose not increase monotonically"); + } + } + lastGen = item.getGen(); + lastOffset = item.getLogicOffset(); + } + } + + public static void checkIfReusePhysicalQueue(Collection mappingOnes) { + Map physicalQueueIdMap = new HashMap<>(); + for (TopicQueueMappingOne mappingOne : mappingOnes) { + for (LogicQueueMappingItem item: mappingOne.items) { + String physicalQueueId = item.getBname() + "-" + item.getQueueId(); + if (physicalQueueIdMap.containsKey(physicalQueueId)) { + throw new RuntimeException(String.format("Topic %s global queue id %d and %d shared the same physical queue %s", + mappingOne.topic, mappingOne.globalId, physicalQueueIdMap.get(physicalQueueId).globalId, physicalQueueId)); + } else { + physicalQueueIdMap.put(physicalQueueId, mappingOne); + } + } + } + } + + public static void checkLeaderInTargetBrokers(Collection mappingOnes, Set targetBrokers) { + for (TopicQueueMappingOne mappingOne : mappingOnes) { + if (!targetBrokers.contains(mappingOne.bname)) { + throw new RuntimeException("The leader broker does not in target broker"); + } + } + } + + public static void checkPhysicalQueueConsistence(Map brokerConfigMap) { + for (Map.Entry entry : brokerConfigMap.entrySet()) { + TopicConfigAndQueueMapping configMapping = entry.getValue(); + assert configMapping != null; + assert configMapping.getMappingDetail() != null; + if (configMapping.getReadQueueNums() < configMapping.getWriteQueueNums()) { + throw new RuntimeException("Read queues is smaller than write queues"); + } + for (List items: configMapping.getMappingDetail().getHostedQueues().values()) { + for (LogicQueueMappingItem item: items) { + if (item.getStartOffset() != 0) { + throw new RuntimeException("The start offset dose not begin from 0"); + } + TopicConfig topicConfig = brokerConfigMap.get(item.getBname()); + if (topicConfig == null) { + throw new RuntimeException("The broker of item dose not exist"); + } + if (item.getQueueId() >= topicConfig.getWriteQueueNums()) { + throw new RuntimeException("The physical queue id is overflow the write queues"); + } + } + } + } + } + + + + public static Map checkAndBuildMappingItems(List mappingDetailList, boolean replace, boolean checkConsistence) { + mappingDetailList.sort((o1, o2) -> (int) (o2.getEpoch() - o1.getEpoch())); + + int maxNum = 0; + Map globalIdMap = new HashMap<>(); + for (TopicQueueMappingDetail mappingDetail : mappingDetailList) { + if (mappingDetail.totalQueues > maxNum) { + maxNum = mappingDetail.totalQueues; + } + for (Map.Entry> entry : mappingDetail.getHostedQueues().entrySet()) { + Integer globalid = entry.getKey(); + checkLogicQueueMappingItemOffset(entry.getValue()); + String leaderBrokerName = getLeaderBroker(entry.getValue()); + if (!leaderBrokerName.equals(mappingDetail.getBname())) { + //not the leader + continue; + } + if (globalIdMap.containsKey(globalid)) { + if (!replace) { + throw new RuntimeException(String.format("The queue id is duplicated in broker %s %s", leaderBrokerName, mappingDetail.getBname())); + } + } else { + globalIdMap.put(globalid, new TopicQueueMappingOne(mappingDetail, mappingDetail.topic, mappingDetail.bname, globalid, entry.getValue())); + } + } + } + if (checkConsistence) { + if (maxNum != globalIdMap.size()) { + throw new RuntimeException(String.format("The total queue number in config dose not match the real hosted queues %d != %d", maxNum, globalIdMap.size())); + } + for (int i = 0; i < maxNum; i++) { + if (!globalIdMap.containsKey(i)) { + throw new RuntimeException(String.format("The queue number %s is not in globalIdMap", i)); + } + } + } + checkIfReusePhysicalQueue(globalIdMap.values()); + return globalIdMap; + } + + public static String getLeaderBroker(List items) { + return getLeaderItem(items).getBname(); + } + public static LogicQueueMappingItem getLeaderItem(List items) { + assert items.size() > 0; + return items.get(items.size() - 1); + } + + public static String writeToTemp(TopicRemappingDetailWrapper wrapper, boolean after) { + String topic = wrapper.getTopic(); + String data = wrapper.toJson(); + String suffix = TopicRemappingDetailWrapper.SUFFIX_BEFORE; + if (after) { + suffix = TopicRemappingDetailWrapper.SUFFIX_AFTER; + } + String fileName = System.getProperty("java.io.tmpdir") + File.separator + topic + "-" + wrapper.getEpoch() + suffix; + try { + MixAll.string2File(data, fileName); + return fileName; + } catch (Exception e) { + throw new RuntimeException("write file failed " + fileName,e); + } + } + + public static long blockSeqRoundUp(long offset, long blockSeqSize) { + long num = offset / blockSeqSize; + long left = offset % blockSeqSize; + if (left < blockSeqSize / 2) { + return (num + 1) * blockSeqSize; + } else { + return (num + 2) * blockSeqSize; + } + } + + public static void checkTargetBrokersComplete(Set targetBrokers, Map brokerConfigMap) { + for (String broker : brokerConfigMap.keySet()) { + if (brokerConfigMap.get(broker).getMappingDetail().getHostedQueues().isEmpty()) { + continue; + } + if (!targetBrokers.contains(broker)) { + throw new RuntimeException("The existed broker " + broker + " dose not in target brokers "); + } + } + } + + public static void checkNonTargetBrokers(Set targetBrokers, Set nonTargetBrokers) { + for (String broker : nonTargetBrokers) { + if (targetBrokers.contains(broker)) { + throw new RuntimeException("The non-target broker exist in target broker"); + } + } + } + + public static TopicRemappingDetailWrapper createTopicConfigMapping(String topic, int queueNum, Set targetBrokers, Map brokerConfigMap) { + checkTargetBrokersComplete(targetBrokers, brokerConfigMap); + Map globalIdMap = new HashMap<>(); + Map.Entry maxEpochAndNum = new AbstractMap.SimpleImmutableEntry<>(System.currentTimeMillis(), queueNum); + if (!brokerConfigMap.isEmpty()) { + maxEpochAndNum = TopicQueueMappingUtils.checkNameEpochNumConsistence(topic, brokerConfigMap); + globalIdMap = TopicQueueMappingUtils.checkAndBuildMappingItems(new ArrayList<>(TopicQueueMappingUtils.getMappingDetailFromConfig(brokerConfigMap.values())), false, true); + checkIfReusePhysicalQueue(globalIdMap.values()); + checkPhysicalQueueConsistence(brokerConfigMap); + } + if (queueNum < globalIdMap.size()) { + throw new RuntimeException(String.format("Cannot decrease the queue num for static topic %d < %d", queueNum, globalIdMap.size())); + } + //check the queue number + if (queueNum == globalIdMap.size()) { + throw new RuntimeException("The topic queue num is equal the existed queue num, do nothing"); + } + + //the check is ok, now do the mapping allocation + Map brokerNumMap = new HashMap<>(); + for (String broker: targetBrokers) { + brokerNumMap.put(broker, 0); + } + final Map oldIdToBroker = new HashMap<>(); + for (Map.Entry entry : globalIdMap.entrySet()) { + String leaderbroker = entry.getValue().getBname(); + oldIdToBroker.put(entry.getKey(), leaderbroker); + if (!brokerNumMap.containsKey(leaderbroker)) { + brokerNumMap.put(leaderbroker, 1); + } else { + brokerNumMap.put(leaderbroker, brokerNumMap.get(leaderbroker) + 1); + } + } + TopicQueueMappingUtils.MappingAllocator allocator = TopicQueueMappingUtils.buildMappingAllocator(oldIdToBroker, brokerNumMap, null); + allocator.upToNum(queueNum); + Map newIdToBroker = allocator.getIdToBroker(); + + //construct the topic configAndMapping + long newEpoch = Math.max(maxEpochAndNum.getKey() + 1000, System.currentTimeMillis()); + for (Map.Entry e : newIdToBroker.entrySet()) { + Integer queueId = e.getKey(); + String broker = e.getValue(); + if (globalIdMap.containsKey(queueId)) { + //ignore the exited + continue; + } + TopicConfigAndQueueMapping configMapping; + if (!brokerConfigMap.containsKey(broker)) { + configMapping = new TopicConfigAndQueueMapping(new TopicConfig(topic), new TopicQueueMappingDetail(topic, 0, broker, System.currentTimeMillis())); + configMapping.setWriteQueueNums(1); + configMapping.setReadQueueNums(1); + brokerConfigMap.put(broker, configMapping); + } else { + configMapping = brokerConfigMap.get(broker); + configMapping.setWriteQueueNums(configMapping.getWriteQueueNums() + 1); + configMapping.setReadQueueNums(configMapping.getReadQueueNums() + 1); + } + LogicQueueMappingItem mappingItem = new LogicQueueMappingItem(0, configMapping.getWriteQueueNums() - 1, broker, 0, 0, -1, -1, -1); + TopicQueueMappingDetail.putMappingInfo(configMapping.getMappingDetail(), queueId, new ArrayList<>(Collections.singletonList(mappingItem))); + } + + // set the topic config + for (Map.Entry entry : brokerConfigMap.entrySet()) { + TopicConfigAndQueueMapping configMapping = entry.getValue(); + configMapping.getMappingDetail().setEpoch(newEpoch); + configMapping.getMappingDetail().setTotalQueues(queueNum); + } + //double check the config + { + TopicQueueMappingUtils.checkNameEpochNumConsistence(topic, brokerConfigMap); + globalIdMap = TopicQueueMappingUtils.checkAndBuildMappingItems(getMappingDetailFromConfig(brokerConfigMap.values()), false, true); + checkIfReusePhysicalQueue(globalIdMap.values()); + checkPhysicalQueueConsistence(brokerConfigMap); + } + return new TopicRemappingDetailWrapper(topic, TopicRemappingDetailWrapper.TYPE_CREATE_OR_UPDATE, newEpoch, brokerConfigMap, new HashSet<>(), new HashSet<>()); + } + + + public static TopicRemappingDetailWrapper remappingStaticTopic(String topic, Map brokerConfigMap, Set targetBrokers) { + Map.Entry maxEpochAndNum = TopicQueueMappingUtils.checkNameEpochNumConsistence(topic, brokerConfigMap); + Map globalIdMap = TopicQueueMappingUtils.checkAndBuildMappingItems(getMappingDetailFromConfig(brokerConfigMap.values()), false, true); + TopicQueueMappingUtils.checkPhysicalQueueConsistence(brokerConfigMap); + TopicQueueMappingUtils.checkIfReusePhysicalQueue(globalIdMap.values()); + + //the check is ok, now do the mapping allocation + int maxNum = maxEpochAndNum.getValue(); + + Map brokerNumMap = new HashMap<>(); + for (String broker: targetBrokers) { + brokerNumMap.put(broker, 0); + } + Map brokerNumMapBeforeRemapping = new HashMap<>(); + for (TopicQueueMappingOne mappingOne: globalIdMap.values()) { + if (brokerNumMapBeforeRemapping.containsKey(mappingOne.bname)) { + brokerNumMapBeforeRemapping.put(mappingOne.bname, brokerNumMapBeforeRemapping.get(mappingOne.bname) + 1); + } else { + brokerNumMapBeforeRemapping.put(mappingOne.bname, 1); + } + } + + TopicQueueMappingUtils.MappingAllocator allocator = TopicQueueMappingUtils.buildMappingAllocator(new HashMap<>(), brokerNumMap, brokerNumMapBeforeRemapping); + allocator.upToNum(maxNum); + Map expectedBrokerNumMap = allocator.getBrokerNumMap(); + Queue waitAssignQueues = new ArrayDeque<>(); + //cannot directly use the idBrokerMap from allocator, for the number of globalId maybe not in the natural order + Map expectedIdToBroker = new HashMap<>(); + //the following logic will make sure that, for one broker, either "map in" or "map out" + //It can't both, map in some queues but also map out some queues. + for (Map.Entry entry : globalIdMap.entrySet()) { + Integer queueId = entry.getKey(); + TopicQueueMappingOne mappingOne = entry.getValue(); + String leaderBroker = mappingOne.getBname(); + if (expectedBrokerNumMap.containsKey(leaderBroker)) { + if (expectedBrokerNumMap.get(leaderBroker) > 0) { + expectedIdToBroker.put(queueId, leaderBroker); + expectedBrokerNumMap.put(leaderBroker, expectedBrokerNumMap.get(leaderBroker) - 1); + } else { + waitAssignQueues.add(queueId); + expectedBrokerNumMap.remove(leaderBroker); + } + } else { + waitAssignQueues.add(queueId); + } + } + + for (Map.Entry entry: expectedBrokerNumMap.entrySet()) { + String broker = entry.getKey(); + Integer queueNum = entry.getValue(); + for (int i = 0; i < queueNum; i++) { + Integer queueId = waitAssignQueues.poll(); + assert queueId != null; + expectedIdToBroker.put(queueId, broker); + } + } + long newEpoch = Math.max(maxEpochAndNum.getKey() + 1000, System.currentTimeMillis()); + + //Now construct the remapping info + Set brokersToMapOut = new HashSet<>(); + Set brokersToMapIn = new HashSet<>(); + for (Map.Entry mapEntry : expectedIdToBroker.entrySet()) { + Integer queueId = mapEntry.getKey(); + String broker = mapEntry.getValue(); + TopicQueueMappingOne topicQueueMappingOne = globalIdMap.get(queueId); + assert topicQueueMappingOne != null; + if (topicQueueMappingOne.getBname().equals(broker)) { + continue; + } + //remapping + final String mapInBroker = broker; + final String mapOutBroker = topicQueueMappingOne.getBname(); + brokersToMapIn.add(mapInBroker); + brokersToMapOut.add(mapOutBroker); + TopicConfigAndQueueMapping mapInConfig = brokerConfigMap.get(mapInBroker); + TopicConfigAndQueueMapping mapOutConfig = brokerConfigMap.get(mapOutBroker); + + if (mapInConfig == null) { + mapInConfig = new TopicConfigAndQueueMapping(new TopicConfig(topic, 0, 0), new TopicQueueMappingDetail(topic, maxNum, mapInBroker, newEpoch)); + brokerConfigMap.put(mapInBroker, mapInConfig); + } + + mapInConfig.setWriteQueueNums(mapInConfig.getWriteQueueNums() + 1); + mapInConfig.setReadQueueNums(mapInConfig.getReadQueueNums() + 1); + + List items = new ArrayList<>(topicQueueMappingOne.getItems()); + LogicQueueMappingItem last = items.get(items.size() - 1); + items.add(new LogicQueueMappingItem(last.getGen() + 1, mapInConfig.getWriteQueueNums() - 1, mapInBroker, -1, 0, -1, -1, -1)); + + //Use the same object + TopicQueueMappingDetail.putMappingInfo(mapInConfig.getMappingDetail(), queueId, items); + TopicQueueMappingDetail.putMappingInfo(mapOutConfig.getMappingDetail(), queueId, items); + } + + for (Map.Entry entry : brokerConfigMap.entrySet()) { + TopicConfigAndQueueMapping configMapping = entry.getValue(); + configMapping.getMappingDetail().setEpoch(newEpoch); + configMapping.getMappingDetail().setTotalQueues(maxNum); + } + + //double check + { + TopicQueueMappingUtils.checkNameEpochNumConsistence(topic, brokerConfigMap); + globalIdMap = TopicQueueMappingUtils.checkAndBuildMappingItems(getMappingDetailFromConfig(brokerConfigMap.values()), false, true); + TopicQueueMappingUtils.checkPhysicalQueueConsistence(brokerConfigMap); + TopicQueueMappingUtils.checkIfReusePhysicalQueue(globalIdMap.values()); + TopicQueueMappingUtils.checkLeaderInTargetBrokers(globalIdMap.values(), targetBrokers); + } + return new TopicRemappingDetailWrapper(topic, TopicRemappingDetailWrapper.TYPE_REMAPPING, newEpoch, brokerConfigMap, brokersToMapIn, brokersToMapOut); + } + + public static LogicQueueMappingItem findLogicQueueMappingItem(List mappingItems, long logicOffset, boolean ignoreNegative) { + if (mappingItems == null + || mappingItems.isEmpty()) { + return null; + } + //Could use bi-search to polish performance + for (int i = mappingItems.size() - 1; i >= 0; i--) { + LogicQueueMappingItem item = mappingItems.get(i); + if (ignoreNegative && item.getLogicOffset() < 0) { + continue; + } + if (logicOffset >= item.getLogicOffset()) { + return item; + } + } + //if not found, maybe out of range, return the first one + for (int i = 0; i < mappingItems.size(); i++) { + LogicQueueMappingItem item = mappingItems.get(i); + if (ignoreNegative && item.getLogicOffset() < 0) { + continue; + } else { + return item; + } + } + return null; + } + + public static LogicQueueMappingItem findNext(List items, LogicQueueMappingItem currentItem, boolean ignoreNegative) { + if (items == null + || currentItem == null) { + return null; + } + for (int i = 0; i < items.size(); i++) { + LogicQueueMappingItem item = items.get(i); + if (ignoreNegative && item.getLogicOffset() < 0) { + continue; + } + if (item.getGen() == currentItem.getGen()) { + if (i < items.size() - 1) { + item = items.get(i + 1); + if (ignoreNegative && item.getLogicOffset() < 0) { + return null; + } else { + return item; + } + } else { + return null; + } + } + } + return null; + } + + + public static boolean checkIfLeader(List items, TopicQueueMappingDetail mappingDetail) { + if (items == null + || mappingDetail == null + || items.isEmpty()) { + return false; + } + return items.get(items.size() - 1).getBname().equals(mappingDetail.getBname()); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicRemappingDetailWrapper.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicRemappingDetailWrapper.java new file mode 100644 index 00000000000..75522bf3d7e --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicRemappingDetailWrapper.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.statictopic; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class TopicRemappingDetailWrapper extends RemotingSerializable { + public static final String TYPE_CREATE_OR_UPDATE = "CREATE_OR_UPDATE"; + public static final String TYPE_REMAPPING = "REMAPPING"; + + public static final String SUFFIX_BEFORE = ".before"; + public static final String SUFFIX_AFTER = ".after"; + + + private String topic; + private String type; + private long epoch; + + private Map brokerConfigMap = new HashMap<>(); + + private Set brokerToMapIn = new HashSet<>(); + + private Set brokerToMapOut = new HashSet<>(); + + public TopicRemappingDetailWrapper() { + + } + + public TopicRemappingDetailWrapper(String topic, String type, long epoch, Map brokerConfigMap, Set brokerToMapIn, Set brokerToMapOut) { + this.topic = topic; + this.type = type; + this.epoch = epoch; + this.brokerConfigMap = brokerConfigMap; + this.brokerToMapIn = brokerToMapIn; + this.brokerToMapOut = brokerToMapOut; + } + + public String getTopic() { + return topic; + } + + public String getType() { + return type; + } + + public long getEpoch() { + return epoch; + } + + public Map getBrokerConfigMap() { + return brokerConfigMap; + } + + public Set getBrokerToMapIn() { + return brokerToMapIn; + } + + public Set getBrokerToMapOut() { + return brokerToMapOut; + } + + public void setBrokerConfigMap(Map brokerConfigMap) { + this.brokerConfigMap = brokerConfigMap; + } + + public void setBrokerToMapIn(Set brokerToMapIn) { + this.brokerToMapIn = brokerToMapIn; + } + + public void setBrokerToMapOut(Set brokerToMapOut) { + this.brokerToMapOut = brokerToMapOut; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public void setType(String type) { + this.type = type; + } + + public void setEpoch(long epoch) { + this.epoch = epoch; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/CustomizedRetryPolicy.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/CustomizedRetryPolicy.java new file mode 100644 index 00000000000..a8cdc748872 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/CustomizedRetryPolicy.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.subscription; + +import com.google.common.base.MoreObjects; +import java.util.concurrent.TimeUnit; + +/** + * CustomizedRetryPolicy is aim to make group's behavior compatible with messageDelayLevel + * + * @see org.apache.rocketmq.store.config.MessageStoreConfig + */ +public class CustomizedRetryPolicy implements RetryPolicy { + // 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h + private long[] next = new long[] { + TimeUnit.SECONDS.toMillis(1), + TimeUnit.SECONDS.toMillis(5), + TimeUnit.SECONDS.toMillis(10), + TimeUnit.SECONDS.toMillis(30), + TimeUnit.MINUTES.toMillis(1), + TimeUnit.MINUTES.toMillis(2), + TimeUnit.MINUTES.toMillis(3), + TimeUnit.MINUTES.toMillis(4), + TimeUnit.MINUTES.toMillis(5), + TimeUnit.MINUTES.toMillis(6), + TimeUnit.MINUTES.toMillis(7), + TimeUnit.MINUTES.toMillis(8), + TimeUnit.MINUTES.toMillis(9), + TimeUnit.MINUTES.toMillis(10), + TimeUnit.MINUTES.toMillis(20), + TimeUnit.MINUTES.toMillis(30), + TimeUnit.HOURS.toMillis(1), + TimeUnit.HOURS.toMillis(2) + }; + + public CustomizedRetryPolicy() { + } + + public CustomizedRetryPolicy(long[] next) { + this.next = next; + } + + public long[] getNext() { + return next; + } + + public void setNext(long[] next) { + this.next = next; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("next", next) + .toString(); + } + + /** + * Index = reconsumeTimes + 2 is compatible logic, cause old delayLevelTable starts from index 1, + * and old index is reconsumeTime + 3 + * + * @param reconsumeTimes Message reconsumeTimes {@link org.apache.rocketmq.common.message.MessageExt#getReconsumeTimes} + * @see org.apache.rocketmq.broker.processor.AbstractSendMessageProcessor + * @see org.apache.rocketmq.store.DefaultMessageStore + */ + @Override + public long nextDelayDuration(int reconsumeTimes) { + if (reconsumeTimes < 0) { + reconsumeTimes = 0; + } + int index = reconsumeTimes + 2; + if (index >= next.length) { + index = next.length - 1; + } + return next[index]; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/ExponentialRetryPolicy.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/ExponentialRetryPolicy.java new file mode 100644 index 00000000000..937c99d1c8c --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/ExponentialRetryPolicy.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.subscription; + +import com.google.common.base.MoreObjects; +import java.util.concurrent.TimeUnit; + +public class ExponentialRetryPolicy implements RetryPolicy { + private long initial = TimeUnit.SECONDS.toMillis(5); + private long max = TimeUnit.HOURS.toMillis(2); + private long multiplier = 2; + + public ExponentialRetryPolicy() { + } + + public ExponentialRetryPolicy(long initial, long max, long multiplier) { + this.initial = initial; + this.max = max; + this.multiplier = multiplier; + } + + public long getInitial() { + return initial; + } + + public void setInitial(long initial) { + this.initial = initial; + } + + public long getMax() { + return max; + } + + public void setMax(long max) { + this.max = max; + } + + public long getMultiplier() { + return multiplier; + } + + public void setMultiplier(long multiplier) { + this.multiplier = multiplier; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("initial", initial) + .add("max", max) + .add("multiplier", multiplier) + .toString(); + } + + @Override + public long nextDelayDuration(int reconsumeTimes) { + if (reconsumeTimes < 0) { + reconsumeTimes = 0; + } + if (reconsumeTimes > 32) { + reconsumeTimes = 32; + } + return Math.min(max, initial * (long) Math.pow(multiplier, reconsumeTimes)); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/GroupForbidden.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/GroupForbidden.java new file mode 100644 index 00000000000..5d509902c5e --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/GroupForbidden.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.subscription; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +/** + * + */ +public class GroupForbidden extends RemotingSerializable { + + private String topic; + private String group; + private Boolean readable; + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public Boolean getReadable() { + return readable; + } + + public void setReadable(Boolean readable) { + this.readable = readable; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((group == null) ? 0 : group.hashCode()); + result = prime * result + ((readable == null) ? 0 : readable.hashCode()); + result = prime * result + ((topic == null) ? 0 : topic.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + GroupForbidden other = (GroupForbidden) obj; + return new EqualsBuilder() + .append(topic, other.topic) + .append(group, other.group) + .append(readable, other.readable) + .isEquals(); + } + + @Override + public String toString() { + return "GroupForbidden [topic=" + topic + ", group=" + group + ", readable=" + readable + "]"; + } + +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/GroupRetryPolicy.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/GroupRetryPolicy.java new file mode 100644 index 00000000000..14d5e537697 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/GroupRetryPolicy.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.subscription; + +import com.alibaba.fastjson.annotation.JSONField; +import com.google.common.base.MoreObjects; + +public class GroupRetryPolicy { + private final static RetryPolicy DEFAULT_RETRY_POLICY = new CustomizedRetryPolicy(); + private GroupRetryPolicyType type = GroupRetryPolicyType.CUSTOMIZED; + private ExponentialRetryPolicy exponentialRetryPolicy; + private CustomizedRetryPolicy customizedRetryPolicy; + + public GroupRetryPolicyType getType() { + return type; + } + + public void setType(GroupRetryPolicyType type) { + this.type = type; + } + + public ExponentialRetryPolicy getExponentialRetryPolicy() { + return exponentialRetryPolicy; + } + + public void setExponentialRetryPolicy(ExponentialRetryPolicy exponentialRetryPolicy) { + this.exponentialRetryPolicy = exponentialRetryPolicy; + } + + public CustomizedRetryPolicy getCustomizedRetryPolicy() { + return customizedRetryPolicy; + } + + public void setCustomizedRetryPolicy(CustomizedRetryPolicy customizedRetryPolicy) { + this.customizedRetryPolicy = customizedRetryPolicy; + } + + @JSONField(serialize = false, deserialize = false) + public RetryPolicy getRetryPolicy() { + if (GroupRetryPolicyType.EXPONENTIAL.equals(type)) { + if (exponentialRetryPolicy == null) { + return DEFAULT_RETRY_POLICY; + } + return exponentialRetryPolicy; + } else if (GroupRetryPolicyType.CUSTOMIZED.equals(type)) { + if (customizedRetryPolicy == null) { + return DEFAULT_RETRY_POLICY; + } + return customizedRetryPolicy; + } else { + return DEFAULT_RETRY_POLICY; + } + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("type", type) + .add("exponentialRetryPolicy", exponentialRetryPolicy) + .add("customizedRetryPolicy", customizedRetryPolicy) + .toString(); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/GroupRetryPolicyType.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/GroupRetryPolicyType.java new file mode 100644 index 00000000000..f68b127f1d1 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/GroupRetryPolicyType.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.subscription; + +public enum GroupRetryPolicyType { + EXPONENTIAL, + CUSTOMIZED +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/RetryPolicy.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/RetryPolicy.java new file mode 100644 index 00000000000..2a77fa88a56 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/RetryPolicy.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.subscription; + +public interface RetryPolicy { + /** + * Compute message's next delay duration by specify reconsumeTimes + * + * @param reconsumeTimes Message reconsumeTimes + * @return Message's nextDelayDuration in milliseconds + */ + long nextDelayDuration(int reconsumeTimes); +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/SimpleSubscriptionData.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/SimpleSubscriptionData.java new file mode 100644 index 00000000000..ec2b51e0b96 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/SimpleSubscriptionData.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.subscription; + +import com.google.common.base.MoreObjects; +import java.util.Objects; + +public class SimpleSubscriptionData { + private String topic; + private String expressionType; + private String expression; + private long version; + + public SimpleSubscriptionData(String topic, String expressionType, String expression, long version) { + this.topic = topic; + this.expressionType = expressionType; + this.expression = expression; + this.version = version; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getExpressionType() { + return expressionType; + } + + public void setExpressionType(String expressionType) { + this.expressionType = expressionType; + } + + public String getExpression() { + return expression; + } + + public void setExpression(String expression) { + this.expression = expression; + } + + public long getVersion() { + return version; + } + + public void setVersion(long version) { + this.version = version; + } + + @Override public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SimpleSubscriptionData that = (SimpleSubscriptionData) o; + return version == that.version && Objects.equals(topic, that.topic); + } + + @Override public int hashCode() { + return Objects.hash(topic, version); + } + + @Override public String toString() { + return MoreObjects.toStringHelper(this) + .add("topic", topic) + .add("expressionType", expressionType) + .add("expression", expression) + .add("version", version) + .toString(); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/SubscriptionGroupConfig.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/SubscriptionGroupConfig.java new file mode 100644 index 00000000000..5522059aaf5 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/subscription/SubscriptionGroupConfig.java @@ -0,0 +1,242 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.subscription; + +import com.google.common.base.MoreObjects; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.rocketmq.common.MixAll; + +public class SubscriptionGroupConfig { + + private String groupName; + + private boolean consumeEnable = true; + private boolean consumeFromMinEnable = true; + private boolean consumeBroadcastEnable = true; + private boolean consumeMessageOrderly = false; + + private int retryQueueNums = 1; + + private int retryMaxTimes = 16; + private GroupRetryPolicy groupRetryPolicy = new GroupRetryPolicy(); + + private long brokerId = MixAll.MASTER_ID; + + private long whichBrokerWhenConsumeSlowly = 1; + + private boolean notifyConsumerIdsChangedEnable = true; + + private int groupSysFlag = 0; + + // Only valid for push consumer + private int consumeTimeoutMinute = 15; + + private Set subscriptionDataSet; + + private Map attributes = new HashMap<>(); + + public String getGroupName() { + return groupName; + } + + public void setGroupName(String groupName) { + this.groupName = groupName; + } + + public boolean isConsumeEnable() { + return consumeEnable; + } + + public void setConsumeEnable(boolean consumeEnable) { + this.consumeEnable = consumeEnable; + } + + public boolean isConsumeFromMinEnable() { + return consumeFromMinEnable; + } + + public void setConsumeFromMinEnable(boolean consumeFromMinEnable) { + this.consumeFromMinEnable = consumeFromMinEnable; + } + + public boolean isConsumeBroadcastEnable() { + return consumeBroadcastEnable; + } + + public void setConsumeBroadcastEnable(boolean consumeBroadcastEnable) { + this.consumeBroadcastEnable = consumeBroadcastEnable; + } + + public boolean isConsumeMessageOrderly() { + return consumeMessageOrderly; + } + + public void setConsumeMessageOrderly(boolean consumeMessageOrderly) { + this.consumeMessageOrderly = consumeMessageOrderly; + } + + public int getRetryQueueNums() { + return retryQueueNums; + } + + public void setRetryQueueNums(int retryQueueNums) { + this.retryQueueNums = retryQueueNums; + } + + public int getRetryMaxTimes() { + return retryMaxTimes; + } + + public void setRetryMaxTimes(int retryMaxTimes) { + this.retryMaxTimes = retryMaxTimes; + } + + public GroupRetryPolicy getGroupRetryPolicy() { + return groupRetryPolicy; + } + + public void setGroupRetryPolicy(GroupRetryPolicy groupRetryPolicy) { + this.groupRetryPolicy = groupRetryPolicy; + } + + public long getBrokerId() { + return brokerId; + } + + public void setBrokerId(long brokerId) { + this.brokerId = brokerId; + } + + public long getWhichBrokerWhenConsumeSlowly() { + return whichBrokerWhenConsumeSlowly; + } + + public void setWhichBrokerWhenConsumeSlowly(long whichBrokerWhenConsumeSlowly) { + this.whichBrokerWhenConsumeSlowly = whichBrokerWhenConsumeSlowly; + } + + public boolean isNotifyConsumerIdsChangedEnable() { + return notifyConsumerIdsChangedEnable; + } + + public void setNotifyConsumerIdsChangedEnable(final boolean notifyConsumerIdsChangedEnable) { + this.notifyConsumerIdsChangedEnable = notifyConsumerIdsChangedEnable; + } + + public int getGroupSysFlag() { + return groupSysFlag; + } + + public void setGroupSysFlag(int groupSysFlag) { + this.groupSysFlag = groupSysFlag; + } + + public int getConsumeTimeoutMinute() { + return consumeTimeoutMinute; + } + + public void setConsumeTimeoutMinute(int consumeTimeoutMinute) { + this.consumeTimeoutMinute = consumeTimeoutMinute; + } + + public Set getSubscriptionDataSet() { + return subscriptionDataSet; + } + + public void setSubscriptionDataSet(Set subscriptionDataSet) { + this.subscriptionDataSet = subscriptionDataSet; + } + + public Map getAttributes() { + return attributes; + } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (int) (brokerId ^ (brokerId >>> 32)); + result = prime * result + (consumeBroadcastEnable ? 1231 : 1237); + result = prime * result + (consumeEnable ? 1231 : 1237); + result = prime * result + (consumeFromMinEnable ? 1231 : 1237); + result = prime * result + (notifyConsumerIdsChangedEnable ? 1231 : 1237); + result = prime * result + ((groupName == null) ? 0 : groupName.hashCode()); + result = prime * result + retryMaxTimes; + result = prime * result + retryQueueNums; + result = + prime * result + (int) (whichBrokerWhenConsumeSlowly ^ (whichBrokerWhenConsumeSlowly >>> 32)); + result = prime * result + groupSysFlag; + result = prime * result + consumeTimeoutMinute; + result = prime * result + subscriptionDataSet.hashCode(); + result = prime * result + attributes.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + SubscriptionGroupConfig other = (SubscriptionGroupConfig) obj; + return new EqualsBuilder() + .append(groupName, other.groupName) + .append(consumeEnable, other.consumeEnable) + .append(consumeFromMinEnable, other.consumeFromMinEnable) + .append(consumeBroadcastEnable, other.consumeBroadcastEnable) + .append(retryQueueNums, other.retryQueueNums) + .append(retryMaxTimes, other.retryMaxTimes) + .append(whichBrokerWhenConsumeSlowly, other.whichBrokerWhenConsumeSlowly) + .append(notifyConsumerIdsChangedEnable, other.notifyConsumerIdsChangedEnable) + .append(groupSysFlag, other.groupSysFlag) + .append(consumeTimeoutMinute, other.consumeTimeoutMinute) + .append(subscriptionDataSet, other.subscriptionDataSet) + .append(attributes, other.attributes) + .isEquals(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("groupName", groupName) + .add("consumeEnable", consumeEnable) + .add("consumeFromMinEnable", consumeFromMinEnable) + .add("consumeBroadcastEnable", consumeBroadcastEnable) + .add("consumeMessageOrderly", consumeMessageOrderly) + .add("retryQueueNums", retryQueueNums) + .add("retryMaxTimes", retryMaxTimes) + .add("groupRetryPolicy", groupRetryPolicy) + .add("brokerId", brokerId) + .add("whichBrokerWhenConsumeSlowly", whichBrokerWhenConsumeSlowly) + .add("notifyConsumerIdsChangedEnable", notifyConsumerIdsChangedEnable) + .add("groupSysFlag", groupSysFlag) + .add("consumeTimeoutMinute", consumeTimeoutMinute) + .add("subscriptionDataSet", subscriptionDataSet) + .add("attributes", attributes) + .toString(); + } +} diff --git a/common/src/main/java/org/apache/rocketmq/common/protocol/topic/OffsetMovedEvent.java b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/topic/OffsetMovedEvent.java similarity index 97% rename from common/src/main/java/org/apache/rocketmq/common/protocol/topic/OffsetMovedEvent.java rename to remoting/src/main/java/org/apache/rocketmq/remoting/protocol/topic/OffsetMovedEvent.java index 1c03c0713d0..ee877161fe5 100644 --- a/common/src/main/java/org/apache/rocketmq/common/protocol/topic/OffsetMovedEvent.java +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/topic/OffsetMovedEvent.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol.topic; +package org.apache.rocketmq.remoting.protocol.topic; import org.apache.rocketmq.common.message.MessageQueue; import org.apache.rocketmq.remoting.protocol.RemotingSerializable; diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/proxy/SocksProxyConfig.java b/remoting/src/main/java/org/apache/rocketmq/remoting/proxy/SocksProxyConfig.java new file mode 100644 index 00000000000..f59b5dd873d --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/proxy/SocksProxyConfig.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.proxy; + +public class SocksProxyConfig { + private String addr; + private String username; + private String password; + + public SocksProxyConfig() { + } + + public SocksProxyConfig(String addr) { + this.addr = addr; + } + + public SocksProxyConfig(String addr, String username, String password) { + this.addr = addr; + this.username = username; + this.password = password; + } + + public String getAddr() { + return addr; + } + + public void setAddr(String addr) { + this.addr = addr; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + @Override + public String toString() { + return String.format("SocksProxy address: %s, username: %s, password: %s", addr, username, password); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/ClientMetadata.java b/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/ClientMetadata.java new file mode 100644 index 00000000000..d4962e00a58 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/ClientMetadata.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.rpc; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.apache.rocketmq.remoting.protocol.route.TopicRouteData; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingInfo; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicQueueMappingUtils; + +public class ClientMetadata { + private static final Logger log = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); + + private final ConcurrentMap topicRouteTable = new ConcurrentHashMap<>(); + private final ConcurrentMap> topicEndPointsTable = new ConcurrentHashMap<>(); + private final ConcurrentMap> brokerAddrTable = + new ConcurrentHashMap<>(); + private final ConcurrentMap> brokerVersionTable = + new ConcurrentHashMap<>(); + + public void freshTopicRoute(String topic, TopicRouteData topicRouteData) { + if (topic == null + || topicRouteData == null) { + return; + } + TopicRouteData old = this.topicRouteTable.get(topic); + if (!topicRouteData.topicRouteDataChanged(old)) { + return ; + } + { + for (BrokerData bd : topicRouteData.getBrokerDatas()) { + this.brokerAddrTable.put(bd.getBrokerName(), bd.getBrokerAddrs()); + } + } + { + ConcurrentMap mqEndPoints = topicRouteData2EndpointsForStaticTopic(topic, topicRouteData); + if (mqEndPoints != null + && !mqEndPoints.isEmpty()) { + topicEndPointsTable.put(topic, mqEndPoints); + } + } + } + + public String getBrokerNameFromMessageQueue(final MessageQueue mq) { + if (topicEndPointsTable.get(mq.getTopic()) != null + && !topicEndPointsTable.get(mq.getTopic()).isEmpty()) { + return topicEndPointsTable.get(mq.getTopic()).get(mq); + } + return mq.getBrokerName(); + } + + public void refreshClusterInfo(ClusterInfo clusterInfo) { + if (clusterInfo == null + || clusterInfo.getBrokerAddrTable() == null) { + return; + } + for (Map.Entry entry : clusterInfo.getBrokerAddrTable().entrySet()) { + brokerAddrTable.put(entry.getKey(), entry.getValue().getBrokerAddrs()); + } + } + + public String findMasterBrokerAddr(String brokerName) { + if (!brokerAddrTable.containsKey(brokerName)) { + return null; + } + return brokerAddrTable.get(brokerName).get(MixAll.MASTER_ID); + } + + public ConcurrentMap> getBrokerAddrTable() { + return brokerAddrTable; + } + + public static ConcurrentMap topicRouteData2EndpointsForStaticTopic(final String topic, final TopicRouteData route) { + if (route.getTopicQueueMappingByBroker() == null + || route.getTopicQueueMappingByBroker().isEmpty()) { + return new ConcurrentHashMap<>(); + } + ConcurrentMap mqEndPointsOfBroker = new ConcurrentHashMap<>(); + + Map> mappingInfosByScope = new HashMap<>(); + for (Map.Entry entry : route.getTopicQueueMappingByBroker().entrySet()) { + TopicQueueMappingInfo info = entry.getValue(); + String scope = info.getScope(); + if (scope != null) { + if (!mappingInfosByScope.containsKey(scope)) { + mappingInfosByScope.put(scope, new HashMap<>()); + } + mappingInfosByScope.get(scope).put(entry.getKey(), entry.getValue()); + } + } + + for (Map.Entry> mapEntry : mappingInfosByScope.entrySet()) { + String scope = mapEntry.getKey(); + Map topicQueueMappingInfoMap = mapEntry.getValue(); + ConcurrentMap mqEndPoints = new ConcurrentHashMap<>(); + List> mappingInfos = new ArrayList<>(topicQueueMappingInfoMap.entrySet()); + mappingInfos.sort((o1, o2) -> (int) (o2.getValue().getEpoch() - o1.getValue().getEpoch())); + int maxTotalNums = 0; + long maxTotalNumOfEpoch = -1; + for (Map.Entry entry : mappingInfos) { + TopicQueueMappingInfo info = entry.getValue(); + if (info.getEpoch() >= maxTotalNumOfEpoch && info.getTotalQueues() > maxTotalNums) { + maxTotalNums = info.getTotalQueues(); + } + for (Map.Entry idEntry : entry.getValue().getCurrIdMap().entrySet()) { + int globalId = idEntry.getKey(); + MessageQueue mq = new MessageQueue(topic, TopicQueueMappingUtils.getMockBrokerName(info.getScope()), globalId); + TopicQueueMappingInfo oldInfo = mqEndPoints.get(mq); + if (oldInfo == null || oldInfo.getEpoch() <= info.getEpoch()) { + mqEndPoints.put(mq, info); + } + } + } + + + //accomplish the static logic queues + for (int i = 0; i < maxTotalNums; i++) { + MessageQueue mq = new MessageQueue(topic, TopicQueueMappingUtils.getMockBrokerName(scope), i); + if (!mqEndPoints.containsKey(mq)) { + mqEndPointsOfBroker.put(mq, MixAll.LOGICAL_QUEUE_MOCK_BROKER_NAME_NOT_EXIST); + } else { + mqEndPointsOfBroker.put(mq, mqEndPoints.get(mq).getBname()); + } + } + } + return mqEndPointsOfBroker; + } + +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RequestBuilder.java b/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RequestBuilder.java new file mode 100644 index 00000000000..e2e2c52fd32 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RequestBuilder.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.rpc; + +import java.util.HashMap; +import java.util.Map; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; + +public class RequestBuilder { + + private static Map requestCodeMap = new HashMap<>(); + static { + requestCodeMap.put(RequestCode.PULL_MESSAGE, PullMessageRequestHeader.class); + } + + public static RpcRequestHeader buildCommonRpcHeader(int requestCode, String destBrokerName) { + return buildCommonRpcHeader(requestCode, null, destBrokerName); + } + + public static RpcRequestHeader buildCommonRpcHeader(int requestCode, Boolean oneway, String destBrokerName) { + Class requestHeaderClass = requestCodeMap.get(requestCode); + if (requestHeaderClass == null) { + throw new UnsupportedOperationException("unknown " + requestCode); + } + try { + RpcRequestHeader requestHeader = (RpcRequestHeader) requestHeaderClass.newInstance(); + requestHeader.setOneway(oneway); + requestHeader.setBrokerName(destBrokerName); + return requestHeader; + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + public static TopicQueueRequestHeader buildTopicQueueRequestHeader(int requestCode, MessageQueue mq) { + return buildTopicQueueRequestHeader(requestCode, null, mq.getBrokerName(), mq.getTopic(), mq.getQueueId(), null); + } + + public static TopicQueueRequestHeader buildTopicQueueRequestHeader(int requestCode, MessageQueue mq, Boolean logic) { + return buildTopicQueueRequestHeader(requestCode, null, mq.getBrokerName(), mq.getTopic(), mq.getQueueId(), logic); + } + + public static TopicQueueRequestHeader buildTopicQueueRequestHeader(int requestCode, Boolean oneway, MessageQueue mq, Boolean logic) { + return buildTopicQueueRequestHeader(requestCode, oneway, mq.getBrokerName(), mq.getTopic(), mq.getQueueId(), logic); + } + + public static TopicQueueRequestHeader buildTopicQueueRequestHeader(int requestCode, Boolean oneway, String destBrokerName, String topic, int queueId, Boolean logic) { + Class requestHeaderClass = requestCodeMap.get(requestCode); + if (requestHeaderClass == null) { + throw new UnsupportedOperationException("unknown " + requestCode); + } + try { + TopicQueueRequestHeader requestHeader = (TopicQueueRequestHeader) requestHeaderClass.newInstance(); + requestHeader.setOneway(oneway); + requestHeader.setBrokerName(destBrokerName); + requestHeader.setTopic(topic); + requestHeader.setQueueId(queueId); + requestHeader.setLo(logic); + return requestHeader; + } catch (Throwable t) { + throw new RuntimeException(t); + } + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RpcClient.java b/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RpcClient.java new file mode 100644 index 00000000000..f1df83bc7d8 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RpcClient.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.rpc; + +import java.util.concurrent.Future; +import org.apache.rocketmq.common.message.MessageQueue; + +public interface RpcClient { + + + //common invoke paradigm, the logic remote addr is defined in "bname" field of request + //For oneway request, the sign is labeled in request, and do not need an another method named "invokeOneway" + //For one + Future invoke(RpcRequest request, long timeoutMs) throws RpcException; + + //For rocketmq, most requests are corresponded to MessageQueue + //And for LogicQueue, the broker name is mocked, the physical addr could only be defined by MessageQueue + Future invoke(MessageQueue mq, RpcRequest request, long timeoutMs) throws RpcException; + +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RpcClientHook.java b/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RpcClientHook.java new file mode 100644 index 00000000000..56751483481 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RpcClientHook.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.rpc; + +public abstract class RpcClientHook { + + //if the return is not null, return it + public abstract RpcResponse beforeRequest(RpcRequest rpcRequest) throws RpcException; + + //if the return is not null, return it + public abstract RpcResponse afterResponse(RpcResponse rpcResponse) throws RpcException; + +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RpcClientImpl.java b/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RpcClientImpl.java new file mode 100644 index 00000000000..bca2d79d995 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RpcClientImpl.java @@ -0,0 +1,347 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.rpc; + +import io.netty.util.concurrent.ImmediateEventExecutor; +import io.netty.util.concurrent.Promise; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Future; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.InvokeCallback; +import org.apache.rocketmq.remoting.RemotingClient; +import org.apache.rocketmq.remoting.netty.ResponseFuture; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.admin.TopicStatsTable; +import org.apache.rocketmq.remoting.protocol.header.GetEarliestMsgStoretimeResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMaxOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.GetMinOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.PullMessageResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.QueryConsumerOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.SearchOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.UpdateConsumerOffsetResponseHeader; +import org.apache.rocketmq.remoting.protocol.statictopic.TopicConfigAndQueueMapping; + +public class RpcClientImpl implements RpcClient { + + private ClientMetadata clientMetadata; + + private RemotingClient remotingClient; + + private List clientHookList = new ArrayList<>(); + + public RpcClientImpl(ClientMetadata clientMetadata, RemotingClient remotingClient) { + this.clientMetadata = clientMetadata; + this.remotingClient = remotingClient; + } + + public void registerHook(RpcClientHook hook) { + clientHookList.add(hook); + } + + @Override + public Future invoke(MessageQueue mq, RpcRequest request, long timeoutMs) throws RpcException { + String bname = clientMetadata.getBrokerNameFromMessageQueue(mq); + request.getHeader().setBrokerName(bname); + return invoke(request, timeoutMs); + } + + + public Promise createResponseFuture() { + return ImmediateEventExecutor.INSTANCE.newPromise(); + } + + @Override + public Future invoke(RpcRequest request, long timeoutMs) throws RpcException { + if (clientHookList.size() > 0) { + for (RpcClientHook rpcClientHook: clientHookList) { + RpcResponse response = rpcClientHook.beforeRequest(request); + if (response != null) { + //For 1.6, there is not easy-to-use future impl + return createResponseFuture().setSuccess(response); + } + } + } + String addr = getBrokerAddrByNameOrException(request.getHeader().bname); + Promise rpcResponsePromise = null; + try { + switch (request.getCode()) { + case RequestCode.PULL_MESSAGE: + rpcResponsePromise = handlePullMessage(addr, request, timeoutMs); + break; + case RequestCode.GET_MIN_OFFSET: + rpcResponsePromise = handleGetMinOffset(addr, request, timeoutMs); + break; + case RequestCode.GET_MAX_OFFSET: + rpcResponsePromise = handleGetMaxOffset(addr, request, timeoutMs); + break; + case RequestCode.SEARCH_OFFSET_BY_TIMESTAMP: + rpcResponsePromise = handleSearchOffset(addr, request, timeoutMs); + break; + case RequestCode.GET_EARLIEST_MSG_STORETIME: + rpcResponsePromise = handleGetEarliestMsgStoretime(addr, request, timeoutMs); + break; + case RequestCode.QUERY_CONSUMER_OFFSET: + rpcResponsePromise = handleQueryConsumerOffset(addr, request, timeoutMs); + break; + case RequestCode.UPDATE_CONSUMER_OFFSET: + rpcResponsePromise = handleUpdateConsumerOffset(addr, request, timeoutMs); + break; + case RequestCode.GET_TOPIC_STATS_INFO: + rpcResponsePromise = handleCommonBodyRequest(addr, request, timeoutMs, TopicStatsTable.class); + break; + case RequestCode.GET_TOPIC_CONFIG: + rpcResponsePromise = handleCommonBodyRequest(addr, request, timeoutMs, TopicConfigAndQueueMapping.class); + break; + default: + throw new RpcException(ResponseCode.REQUEST_CODE_NOT_SUPPORTED, "Unknown request code " + request.getCode()); + } + } catch (RpcException rpcException) { + throw rpcException; + } catch (Exception e) { + throw new RpcException(ResponseCode.RPC_UNKNOWN, "error from remoting layer", e); + } + return rpcResponsePromise; + } + + + private String getBrokerAddrByNameOrException(String bname) throws RpcException { + String addr = this.clientMetadata.findMasterBrokerAddr(bname); + if (addr == null) { + throw new RpcException(ResponseCode.SYSTEM_ERROR, "cannot find addr for broker " + bname); + } + return addr; + } + + + private void processFailedResponse(String addr, RemotingCommand requestCommand, ResponseFuture responseFuture, Promise rpcResponsePromise) { + RemotingCommand responseCommand = responseFuture.getResponseCommand(); + if (responseCommand != null) { + //this should not happen + return; + } + int errorCode = ResponseCode.RPC_UNKNOWN; + String errorMessage = null; + if (!responseFuture.isSendRequestOK()) { + errorCode = ResponseCode.RPC_SEND_TO_CHANNEL_FAILED; + errorMessage = "send request failed to " + addr + ". Request: " + requestCommand; + } else if (responseFuture.isTimeout()) { + errorCode = ResponseCode.RPC_TIME_OUT; + errorMessage = "wait response from " + addr + " timeout :" + responseFuture.getTimeoutMillis() + "ms" + ". Request: " + requestCommand; + } else { + errorMessage = "unknown reason. addr: " + addr + ", timeoutMillis: " + responseFuture.getTimeoutMillis() + ". Request: " + requestCommand; + } + rpcResponsePromise.setSuccess(new RpcResponse(new RpcException(errorCode, errorMessage))); + } + + + public Promise handlePullMessage(final String addr, RpcRequest rpcRequest, long timeoutMillis) throws Exception { + final RemotingCommand requestCommand = RpcClientUtils.createCommandForRpcRequest(rpcRequest); + + final Promise rpcResponsePromise = createResponseFuture(); + + InvokeCallback callback = new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { + + } + + @Override + public void operationSucceed(RemotingCommand response) { + try { + switch (response.getCode()) { + case ResponseCode.SUCCESS: + case ResponseCode.PULL_NOT_FOUND: + case ResponseCode.PULL_RETRY_IMMEDIATELY: + case ResponseCode.PULL_OFFSET_MOVED: + PullMessageResponseHeader responseHeader = + (PullMessageResponseHeader) response.decodeCommandCustomHeader(PullMessageResponseHeader.class); + rpcResponsePromise.setSuccess(new RpcResponse(response.getCode(), responseHeader, response.getBody())); + default: + RpcResponse rpcResponse = new RpcResponse(new RpcException(response.getCode(), "unexpected remote response code")); + rpcResponsePromise.setSuccess(rpcResponse); + + } + } catch (Exception e) { + String errorMessage = "process failed. addr: " + addr + ", timeoutMillis: " + timeoutMillis + ". Request: " + requestCommand; + RpcResponse rpcResponse = new RpcResponse(new RpcException(ResponseCode.RPC_UNKNOWN, errorMessage, e)); + rpcResponsePromise.setSuccess(rpcResponse); + } + } + + @Override + public void operationFail(Throwable throwable) { + String errorMessage = "process failed. addr: " + addr + ". Request: " + requestCommand; + RpcResponse rpcResponse = new RpcResponse(new RpcException(ResponseCode.RPC_UNKNOWN, errorMessage, throwable)); + rpcResponsePromise.setSuccess(rpcResponse); + } + }; + + this.remotingClient.invokeAsync(addr, requestCommand, timeoutMillis, callback); + return rpcResponsePromise; + } + + public Promise handleSearchOffset(String addr, RpcRequest rpcRequest, long timeoutMillis) throws Exception { + final Promise rpcResponsePromise = createResponseFuture(); + + RemotingCommand requestCommand = RpcClientUtils.createCommandForRpcRequest(rpcRequest); + RemotingCommand responseCommand = this.remotingClient.invokeSync(addr, requestCommand, timeoutMillis); + assert responseCommand != null; + switch (responseCommand.getCode()) { + case ResponseCode.SUCCESS: { + SearchOffsetResponseHeader responseHeader = + (SearchOffsetResponseHeader) responseCommand.decodeCommandCustomHeader(SearchOffsetResponseHeader.class); + rpcResponsePromise.setSuccess(new RpcResponse(responseCommand.getCode(), responseHeader, responseCommand.getBody())); + break; + } + default: { + rpcResponsePromise.setSuccess(new RpcResponse(new RpcException(responseCommand.getCode(), "unknown remote error"))); + } + } + return rpcResponsePromise; + } + + + + public Promise handleQueryConsumerOffset(String addr, RpcRequest rpcRequest, long timeoutMillis) throws Exception { + final Promise rpcResponsePromise = createResponseFuture(); + + RemotingCommand requestCommand = RpcClientUtils.createCommandForRpcRequest(rpcRequest); + RemotingCommand responseCommand = this.remotingClient.invokeSync(addr, requestCommand, timeoutMillis); + assert responseCommand != null; + switch (responseCommand.getCode()) { + case ResponseCode.SUCCESS: { + QueryConsumerOffsetResponseHeader responseHeader = + (QueryConsumerOffsetResponseHeader) responseCommand.decodeCommandCustomHeader(QueryConsumerOffsetResponseHeader.class); + rpcResponsePromise.setSuccess(new RpcResponse(responseCommand.getCode(), responseHeader, responseCommand.getBody())); + break; + } + case ResponseCode.QUERY_NOT_FOUND: { + rpcResponsePromise.setSuccess(new RpcResponse(responseCommand.getCode(), null, null)); + break; + } + default: { + rpcResponsePromise.setSuccess(new RpcResponse(new RpcException(responseCommand.getCode(), "unknown remote error"))); + } + } + return rpcResponsePromise; + } + + public Promise handleUpdateConsumerOffset(String addr, RpcRequest rpcRequest, long timeoutMillis) throws Exception { + final Promise rpcResponsePromise = createResponseFuture(); + + RemotingCommand requestCommand = RpcClientUtils.createCommandForRpcRequest(rpcRequest); + RemotingCommand responseCommand = this.remotingClient.invokeSync(addr, requestCommand, timeoutMillis); + assert responseCommand != null; + switch (responseCommand.getCode()) { + case ResponseCode.SUCCESS: { + UpdateConsumerOffsetResponseHeader responseHeader = + (UpdateConsumerOffsetResponseHeader) responseCommand.decodeCommandCustomHeader(UpdateConsumerOffsetResponseHeader.class); + rpcResponsePromise.setSuccess(new RpcResponse(responseCommand.getCode(), responseHeader, responseCommand.getBody())); + break; + } + default: { + rpcResponsePromise.setSuccess(new RpcResponse(new RpcException(responseCommand.getCode(), "unknown remote error"))); + } + } + return rpcResponsePromise; + } + + public Promise handleCommonBodyRequest(final String addr, RpcRequest rpcRequest, long timeoutMillis, Class bodyClass) throws Exception { + final Promise rpcResponsePromise = createResponseFuture(); + RemotingCommand requestCommand = RpcClientUtils.createCommandForRpcRequest(rpcRequest); + RemotingCommand responseCommand = this.remotingClient.invokeSync(addr, requestCommand, timeoutMillis); + assert responseCommand != null; + switch (responseCommand.getCode()) { + case ResponseCode.SUCCESS: { + rpcResponsePromise.setSuccess(new RpcResponse(ResponseCode.SUCCESS, null, RemotingSerializable.decode(responseCommand.getBody(), bodyClass))); + break; + } + default: { + rpcResponsePromise.setSuccess(new RpcResponse(new RpcException(responseCommand.getCode(), "unknown remote error"))); + } + } + return rpcResponsePromise; + } + + public Promise handleGetMinOffset(String addr, RpcRequest rpcRequest, long timeoutMillis) throws Exception { + final Promise rpcResponsePromise = createResponseFuture(); + + RemotingCommand requestCommand = RpcClientUtils.createCommandForRpcRequest(rpcRequest); + + RemotingCommand responseCommand = this.remotingClient.invokeSync(addr, requestCommand, timeoutMillis); + assert responseCommand != null; + switch (responseCommand.getCode()) { + case ResponseCode.SUCCESS: { + GetMinOffsetResponseHeader responseHeader = + (GetMinOffsetResponseHeader) responseCommand.decodeCommandCustomHeader(GetMinOffsetResponseHeader.class); + rpcResponsePromise.setSuccess(new RpcResponse(responseCommand.getCode(), responseHeader, responseCommand.getBody())); + break; + } + default: { + rpcResponsePromise.setSuccess(new RpcResponse(new RpcException(responseCommand.getCode(), "unknown remote error"))); + } + } + return rpcResponsePromise; + } + + public Promise handleGetMaxOffset(String addr, RpcRequest rpcRequest, long timeoutMillis) throws Exception { + final Promise rpcResponsePromise = createResponseFuture(); + + RemotingCommand requestCommand = RpcClientUtils.createCommandForRpcRequest(rpcRequest); + + RemotingCommand responseCommand = this.remotingClient.invokeSync(addr, requestCommand, timeoutMillis); + assert responseCommand != null; + switch (responseCommand.getCode()) { + case ResponseCode.SUCCESS: { + GetMaxOffsetResponseHeader responseHeader = + (GetMaxOffsetResponseHeader) responseCommand.decodeCommandCustomHeader(GetMaxOffsetResponseHeader.class); + rpcResponsePromise.setSuccess(new RpcResponse(responseCommand.getCode(), responseHeader, responseCommand.getBody())); + break; + } + default: { + rpcResponsePromise.setSuccess(new RpcResponse(new RpcException(responseCommand.getCode(), "unknown remote error"))); + } + } + return rpcResponsePromise; + } + + public Promise handleGetEarliestMsgStoretime(String addr, RpcRequest rpcRequest, long timeoutMillis) throws Exception { + final Promise rpcResponsePromise = createResponseFuture(); + + RemotingCommand requestCommand = RpcClientUtils.createCommandForRpcRequest(rpcRequest); + + RemotingCommand responseCommand = this.remotingClient.invokeSync(addr, requestCommand, timeoutMillis); + assert responseCommand != null; + switch (responseCommand.getCode()) { + case ResponseCode.SUCCESS: { + GetEarliestMsgStoretimeResponseHeader responseHeader = + (GetEarliestMsgStoretimeResponseHeader) responseCommand.decodeCommandCustomHeader(GetEarliestMsgStoretimeResponseHeader.class); + rpcResponsePromise.setSuccess(new RpcResponse(responseCommand.getCode(), responseHeader, responseCommand.getBody())); + break; + } + default: { + rpcResponsePromise.setSuccess(new RpcResponse(new RpcException(responseCommand.getCode(), "unknown remote error"))); + } + } + return rpcResponsePromise; + } + +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RpcClientUtils.java b/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RpcClientUtils.java new file mode 100644 index 00000000000..78a33b72c59 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RpcClientUtils.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.rpc; + +import java.nio.ByteBuffer; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class RpcClientUtils { + + public static RemotingCommand createCommandForRpcRequest(RpcRequest rpcRequest) { + RemotingCommand cmd = RemotingCommand.createRequestCommand(rpcRequest.getCode(), rpcRequest.getHeader()); + cmd.setBody(encodeBody(rpcRequest.getBody())); + return cmd; + } + + public static RemotingCommand createCommandForRpcResponse(RpcResponse rpcResponse) { + RemotingCommand cmd = RemotingCommand.createResponseCommandWithHeader(rpcResponse.getCode(), rpcResponse.getHeader()); + cmd.setRemark(rpcResponse.getException() == null ? "" : rpcResponse.getException().getMessage()); + cmd.setBody(encodeBody(rpcResponse.getBody())); + return cmd; + } + + public static byte[] encodeBody(Object body) { + if (body == null) { + return null; + } + if (body instanceof byte[]) { + return (byte[])body; + } else if (body instanceof RemotingSerializable) { + return ((RemotingSerializable) body).encode(); + } else if (body instanceof ByteBuffer) { + ByteBuffer buffer = (ByteBuffer)body; + buffer.mark(); + byte[] data = new byte[buffer.remaining()]; + buffer.get(data); + buffer.reset(); + return data; + } else { + throw new RuntimeException("Unsupported body type " + body.getClass()); + } + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RpcException.java b/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RpcException.java new file mode 100644 index 00000000000..dda918b33ec --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RpcException.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.rpc; + +import org.apache.rocketmq.remoting.exception.RemotingException; + +public class RpcException extends RemotingException { + private int errorCode; + public RpcException(int errorCode, String message) { + super(message); + this.errorCode = errorCode; + } + + public RpcException(int errorCode, String message, Throwable cause) { + super(message, cause); + this.errorCode = errorCode; + } + + public int getErrorCode() { + return errorCode; + } + + public void setErrorCode(int errorCode) { + this.errorCode = errorCode; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RpcRequest.java b/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RpcRequest.java new file mode 100644 index 00000000000..3bf06c1f985 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RpcRequest.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.rpc; + +public class RpcRequest { + int code; + private RpcRequestHeader header; + private Object body; + + public RpcRequest(int code, RpcRequestHeader header, Object body) { + this.code = code; + this.header = header; + this.body = body; + } + + public RpcRequestHeader getHeader() { + return header; + } + + public Object getBody() { + return body; + } + + public int getCode() { + return code; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RpcRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RpcRequestHeader.java new file mode 100644 index 00000000000..810d87648a4 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RpcRequestHeader.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.rpc; + +import com.google.common.base.MoreObjects; +import java.util.Objects; +import org.apache.rocketmq.remoting.CommandCustomHeader; + +public abstract class RpcRequestHeader implements CommandCustomHeader { + //the namespace name + protected String ns; + //if the data has been namespaced + protected Boolean nsd; + //the abstract remote addr name, usually the physical broker name + protected String bname; + //oneway + protected Boolean oway; + + @Deprecated + public String getBname() { + return bname; + } + + @Deprecated + public void setBname(String brokerName) { + this.bname = brokerName; + } + + public String getBrokerName() { + return bname; + } + + public void setBrokerName(String brokerName) { + this.bname = brokerName; + } + + public String getNamespace() { + return ns; + } + + public void setNamespace(String namespace) { + this.ns = namespace; + } + + public Boolean getNamespaced() { + return nsd; + } + + public void setNamespaced(Boolean namespaced) { + this.nsd = namespaced; + } + + public Boolean getOneway() { + return oway; + } + + public void setOneway(Boolean oneway) { + this.oway = oneway; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RpcRequestHeader header = (RpcRequestHeader) o; + return Objects.equals(ns, header.ns) && Objects.equals(nsd, header.nsd) && Objects.equals(bname, header.bname) && Objects.equals(oway, header.oway); + } + + @Override + public int hashCode() { + return Objects.hash(ns, nsd, bname, oway); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("namespace", ns) + .add("namespaced", nsd) + .add("brokerName", bname) + .add("oneway", oway) + .toString(); + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RpcResponse.java b/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RpcResponse.java new file mode 100644 index 00000000000..d7e7b17a642 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/RpcResponse.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.rpc; + +import org.apache.rocketmq.remoting.CommandCustomHeader; + +public class RpcResponse { + private int code; + private CommandCustomHeader header; + private Object body; + public RpcException exception; + + public RpcResponse() { + + } + + public RpcResponse(int code, CommandCustomHeader header, Object body) { + this.code = code; + this.header = header; + this.body = body; + } + + public RpcResponse(RpcException rpcException) { + this.code = rpcException.getErrorCode(); + this.exception = rpcException; + } + + public int getCode() { + return code; + } + + public CommandCustomHeader getHeader() { + return header; + } + + public void setHeader(CommandCustomHeader header) { + this.header = header; + } + + public Object getBody() { + return body; + } + + public void setBody(Object body) { + this.body = body; + } + + public RpcException getException() { + return exception; + } + + public void setException(RpcException exception) { + this.exception = exception; + } + +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/TopicQueueRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/TopicQueueRequestHeader.java new file mode 100644 index 00000000000..f265dd5c349 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/TopicQueueRequestHeader.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.rpc; + +public abstract class TopicQueueRequestHeader extends TopicRequestHeader { + + public abstract Integer getQueueId(); + public abstract void setQueueId(Integer queueId); + +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/TopicRequestHeader.java b/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/TopicRequestHeader.java new file mode 100644 index 00000000000..9f21c07eefa --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/rpc/TopicRequestHeader.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.rpc; + +public abstract class TopicRequestHeader extends RpcRequestHeader { + //logical + protected Boolean lo; + + public abstract String getTopic(); + public abstract void setTopic(String topic); + + public Boolean getLo() { + return lo; + } + public void setLo(Boolean lo) { + this.lo = lo; + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/rpchook/DynamicalExtFieldRPCHook.java b/remoting/src/main/java/org/apache/rocketmq/remoting/rpchook/DynamicalExtFieldRPCHook.java new file mode 100644 index 00000000000..25a189e6dd5 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/rpchook/DynamicalExtFieldRPCHook.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.rpchook; + +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; + +public class DynamicalExtFieldRPCHook implements RPCHook { + + @Override + public void doBeforeRequest(String remoteAddr, RemotingCommand request) { + String zoneName = System.getProperty(MixAll.ROCKETMQ_ZONE_PROPERTY, System.getenv(MixAll.ROCKETMQ_ZONE_ENV)); + if (StringUtils.isNotBlank(zoneName)) { + request.addExtField(MixAll.ZONE_NAME, zoneName); + } + String zoneMode = System.getProperty(MixAll.ROCKETMQ_ZONE_MODE_PROPERTY, System.getenv(MixAll.ROCKETMQ_ZONE_MODE_ENV)); + if (StringUtils.isNotBlank(zoneMode)) { + request.addExtField(MixAll.ZONE_MODE, zoneMode); + } + } + + @Override + public void doAfterResponse(String remoteAddr, RemotingCommand request, RemotingCommand response) { + + } +} diff --git a/remoting/src/main/java/org/apache/rocketmq/remoting/rpchook/StreamTypeRPCHook.java b/remoting/src/main/java/org/apache/rocketmq/remoting/rpchook/StreamTypeRPCHook.java new file mode 100644 index 00000000000..501247a7f95 --- /dev/null +++ b/remoting/src/main/java/org/apache/rocketmq/remoting/rpchook/StreamTypeRPCHook.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.rpchook; + +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestType; + +public class StreamTypeRPCHook implements RPCHook { + @Override + public void doBeforeRequest(String remoteAddr, RemotingCommand request) { + request.addExtField(MixAll.REQ_T, String.valueOf(RequestType.STREAM.getCode())); + } + + @Override + public void doAfterResponse(String remoteAddr, RemotingCommand request, + RemotingCommand response) { + + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/ProxyProtocolTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/ProxyProtocolTest.java new file mode 100644 index 00000000000..c39fd2132b9 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/ProxyProtocolTest.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.handler.codec.haproxy.HAProxyCommand; +import io.netty.handler.codec.haproxy.HAProxyMessage; +import io.netty.handler.codec.haproxy.HAProxyMessageEncoder; +import io.netty.handler.codec.haproxy.HAProxyProtocolVersion; +import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyRemotingClient; +import org.apache.rocketmq.remoting.protocol.LanguageCode; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.Socket; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertNotNull; + +@RunWith(MockitoJUnitRunner.class) +public class ProxyProtocolTest { + + private RemotingServer remotingServer; + private RemotingClient remotingClient; + + @Before + public void setUp() throws Exception { + NettyClientConfig clientConfig = new NettyClientConfig(); + clientConfig.setUseTLS(false); + + remotingServer = RemotingServerTest.createRemotingServer(); + remotingClient = RemotingServerTest.createRemotingClient(clientConfig); + + await().pollDelay(Duration.ofMillis(10)) + .pollInterval(Duration.ofMillis(10)) + .atMost(20, TimeUnit.SECONDS).until(() -> isHostConnectable(getServerAddress())); + } + + @Test + public void testProxyProtocol() throws Exception { + sendHAProxyMessage(remotingClient); + requestThenAssertResponse(remotingClient); + } + + private void requestThenAssertResponse(RemotingClient remotingClient) throws Exception { + RemotingCommand response = remotingClient.invokeSync(getServerAddress(), createRequest(), 10000 * 3); + assertNotNull(response); + assertThat(response.getLanguage()).isEqualTo(LanguageCode.JAVA); + assertThat(response.getExtFields()).hasSize(2); + assertThat(response.getExtFields().get("messageTitle")).isEqualTo("Welcome"); + } + + private void sendHAProxyMessage(RemotingClient remotingClient) throws Exception { + Method getAndCreateChannel = NettyRemotingClient.class.getDeclaredMethod("getAndCreateChannel", String.class); + getAndCreateChannel.setAccessible(true); + NettyRemotingClient nettyRemotingClient = (NettyRemotingClient) remotingClient; + Channel channel = (Channel) getAndCreateChannel.invoke(nettyRemotingClient, getServerAddress()); + HAProxyMessage message = new HAProxyMessage(HAProxyProtocolVersion.V2, HAProxyCommand.PROXY, + HAProxyProxiedProtocol.TCP4, "127.0.0.1", "127.0.0.2", 8000, 9000); + + ByteBuf byteBuf = Unpooled.directBuffer(); + Method encode = HAProxyMessageEncoder.class.getDeclaredMethod("encodeV2", HAProxyMessage.class, ByteBuf.class); + encode.setAccessible(true); + encode.invoke(HAProxyMessageEncoder.INSTANCE, message, byteBuf); + channel.writeAndFlush(byteBuf).sync(); + } + + private static RemotingCommand createRequest() { + RequestHeader requestHeader = new RequestHeader(); + requestHeader.setCount(1); + requestHeader.setMessageTitle("Welcome"); + return RemotingCommand.createRequestCommand(0, requestHeader); + } + + + private String getServerAddress() { + return "localhost:" + remotingServer.localListenPort(); + } + + private boolean isHostConnectable(String addr) { + try (Socket socket = new Socket()) { + socket.connect(NetworkUtil.string2SocketAddress(addr)); + return true; + } catch (IOException ignored) { + } + return false; + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/RemotingServerTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/RemotingServerTest.java index 0ecfaaa5aa0..d0da0eb2ef7 100644 --- a/remoting/src/test/java/org/apache/rocketmq/remoting/RemotingServerTest.java +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/RemotingServerTest.java @@ -39,7 +39,7 @@ import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertNotNull; public class RemotingServerTest { private static RemotingServer remotingServer; @@ -95,8 +95,8 @@ public void testInvokeSync() throws InterruptedException, RemotingConnectExcepti requestHeader.setCount(1); requestHeader.setMessageTitle("Welcome"); RemotingCommand request = RemotingCommand.createRequestCommand(0, requestHeader); - RemotingCommand response = remotingClient.invokeSync("localhost:8888", request, 1000 * 3); - assertTrue(response != null); + RemotingCommand response = remotingClient.invokeSync("localhost:" + remotingServer.localListenPort(), request, 1000 * 3); + assertNotNull(response); assertThat(response.getLanguage()).isEqualTo(LanguageCode.JAVA); assertThat(response.getExtFields()).hasSize(2); @@ -108,7 +108,7 @@ public void testInvokeOneway() throws InterruptedException, RemotingConnectExcep RemotingCommand request = RemotingCommand.createRequestCommand(0, null); request.setRemark("messi"); - remotingClient.invokeOneway("localhost:8888", request, 1000 * 3); + remotingClient.invokeOneway("localhost:" + remotingServer.localListenPort(), request, 1000 * 3); } @Test @@ -118,13 +118,22 @@ public void testInvokeAsync() throws InterruptedException, RemotingConnectExcept final CountDownLatch latch = new CountDownLatch(1); RemotingCommand request = RemotingCommand.createRequestCommand(0, null); request.setRemark("messi"); - remotingClient.invokeAsync("localhost:8888", request, 1000 * 3, new InvokeCallback() { + remotingClient.invokeAsync("localhost:" + remotingServer.localListenPort(), request, 1000 * 3, new InvokeCallback() { @Override public void operationComplete(ResponseFuture responseFuture) { + + } + + @Override + public void operationSucceed(RemotingCommand response) { latch.countDown(); - assertTrue(responseFuture != null); - assertThat(responseFuture.getResponseCommand().getLanguage()).isEqualTo(LanguageCode.JAVA); - assertThat(responseFuture.getResponseCommand().getExtFields()).hasSize(2); + assertThat(response.getLanguage()).isEqualTo(LanguageCode.JAVA); + assertThat(response.getExtFields()).hasSize(2); + } + + @Override + public void operationFail(Throwable throwable) { + } }); latch.await(); diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/SubRemotingServerTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/SubRemotingServerTest.java new file mode 100644 index 00000000000..43ff1e9c0f6 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/SubRemotingServerTest.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting; + +import io.netty.channel.ChannelHandlerContext; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.exception.RemotingConnectException; +import org.apache.rocketmq.remoting.exception.RemotingSendRequestException; +import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; +import org.apache.rocketmq.remoting.netty.NettyRequestProcessor; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RemotingSysResponseCode; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Fail.failBecauseExceptionWasNotThrown; + +public class SubRemotingServerTest { + private static final int SUB_SERVER_PORT = 1234; + + private static RemotingServer remotingServer; + private static RemotingClient remotingClient; + private static RemotingServer subServer; + + @BeforeClass + public static void setup() throws InterruptedException { + remotingServer = RemotingServerTest.createRemotingServer(); + remotingClient = RemotingServerTest.createRemotingClient(); + subServer = createSubRemotingServer(remotingServer); + } + + @AfterClass + public static void destroy() { + remotingClient.shutdown(); + remotingServer.shutdown(); + } + + public static RemotingServer createSubRemotingServer(RemotingServer parentServer) { + RemotingServer subServer = parentServer.newRemotingServer(SUB_SERVER_PORT); + subServer.registerProcessor(1, new NettyRequestProcessor() { + @Override + public RemotingCommand processRequest(final ChannelHandlerContext ctx, + final RemotingCommand request) throws Exception { + request.setRemark(String.valueOf(RemotingHelper.parseSocketAddressPort(ctx.channel().localAddress()))); + return request; + } + + @Override + public boolean rejectRequest() { + return false; + } + }, null); + subServer.start(); + return subServer; + } + + @Test + public void testInvokeSubRemotingServer() throws InterruptedException, RemotingTimeoutException, + RemotingConnectException, RemotingSendRequestException { + RequestHeader requestHeader = new RequestHeader(); + requestHeader.setCount(1); + requestHeader.setMessageTitle("Welcome"); + + // Parent remoting server doesn't support RequestCode 1 + RemotingCommand request = RemotingCommand.createRequestCommand(1, requestHeader); + RemotingCommand response = remotingClient.invokeSync("localhost:" + remotingServer.localListenPort(), request, + 1000 * 3); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(RemotingSysResponseCode.REQUEST_CODE_NOT_SUPPORTED); + + // Issue request to SubRemotingServer + response = remotingClient.invokeSync("localhost:1234", request, 1000 * 3); + assertThat(response).isNotNull(); + assertThat(response.getExtFields()).hasSize(2); + assertThat(response.getRemark()).isEqualTo(String.valueOf(SUB_SERVER_PORT)); + + // Issue unsupported request to SubRemotingServer + request.setCode(0); + response = remotingClient.invokeSync("localhost:1234", request, 1000 * 3); + assertThat(response).isNotNull(); + assertThat(response.getCode()).isEqualTo(RemotingSysResponseCode.REQUEST_CODE_NOT_SUPPORTED); + + // Issue request to a closed SubRemotingServer + request.setCode(1); + remotingServer.removeRemotingServer(SUB_SERVER_PORT); + subServer.shutdown(); + try { + remotingClient.invokeSync("localhost:1234", request, 1000 * 3); + failBecauseExceptionWasNotThrown(RemotingTimeoutException.class); + } catch (Exception e) { + assertThat(e).isInstanceOfAny(RemotingTimeoutException.class, RemotingSendRequestException.class); + } + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/TlsTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/TlsTest.java index 5e516dd74bd..a4890d73d5a 100644 --- a/remoting/src/test/java/org/apache/rocketmq/remoting/TlsTest.java +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/TlsTest.java @@ -17,14 +17,11 @@ package org.apache.rocketmq.remoting; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.io.PrintWriter; +import org.apache.rocketmq.common.utils.NetworkUtil; import org.apache.rocketmq.remoting.common.TlsMode; import org.apache.rocketmq.remoting.exception.RemotingSendRequestException; import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyRemotingServer; import org.apache.rocketmq.remoting.netty.TlsHelper; import org.apache.rocketmq.remoting.protocol.LanguageCode; import org.apache.rocketmq.remoting.protocol.RemotingCommand; @@ -37,6 +34,20 @@ import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.net.Socket; +import java.time.Duration; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_CLIENT_AUTHSERVER; import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_CLIENT_CERTPATH; import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.TLS_CLIENT_KEYPASSWORD; @@ -64,7 +75,8 @@ import static org.apache.rocketmq.remoting.netty.TlsSystemConfig.tlsTestModeEnable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; -import static org.junit.Assert.assertTrue; +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertNotNull; @RunWith(MockitoJUnitRunner.class) public class TlsTest { @@ -132,12 +144,24 @@ else if ("noClientAuthFailure".equals(name.getMethodName())) { tlsClientKeyPath = ""; tlsClientCertPath = ""; clientConfig.setUseTLS(false); - } else if ("serverRejectsSSLClient".equals(name.getMethodName())) { + } else if ("disabledServerRejectsSSLClient".equals(name.getMethodName())) { + tlsMode = TlsMode.DISABLED; + } else if ("disabledServerAcceptUnAuthClient".equals(name.getMethodName())) { tlsMode = TlsMode.DISABLED; + tlsClientKeyPath = ""; + tlsClientCertPath = ""; + clientConfig.setUseTLS(false); + } else if ("reloadSslContextForServer".equals(name.getMethodName())) { + tlsClientAuthServer = false; + tlsServerNeedClientAuth = "none"; } remotingServer = RemotingServerTest.createRemotingServer(); remotingClient = RemotingServerTest.createRemotingClient(clientConfig); + + await().pollDelay(Duration.ofMillis(10)) + .pollInterval(Duration.ofMillis(10)) + .atMost(20, TimeUnit.SECONDS).until(() -> isHostConnectable(getServerAddress())); } @After @@ -156,6 +180,26 @@ public void basicClientServerIntegrationTest() throws Exception { requestThenAssertResponse(); } + @Test + public void reloadSslContextForServer() throws Exception { + requestThenAssertResponse(); + + //Use new cert and private key + tlsClientKeyPath = getCertsPath("badClient.key"); + tlsClientCertPath = getCertsPath("badClient.pem"); + + ((NettyRemotingServer) remotingServer).loadSslContext(); + + //Request Again + requestThenAssertResponse(); + + //Start another client + NettyClientConfig clientConfig = new NettyClientConfig(); + clientConfig.setUseTLS(true); + RemotingClient remotingClient = RemotingServerTest.createRemotingClient(clientConfig); + requestThenAssertResponse(remotingClient); + } + @Test public void serverNotNeedClientAuth() throws Exception { requestThenAssertResponse(); @@ -172,14 +216,19 @@ public void serverAcceptsUnAuthClient() throws Exception { } @Test - public void serverRejectsSSLClient() throws Exception { + public void disabledServerRejectsSSLClient() throws Exception { try { - RemotingCommand response = remotingClient.invokeSync("localhost:8888", createRequest(), 1000 * 5); + RemotingCommand response = remotingClient.invokeSync(getServerAddress(), createRequest(), 1000 * 5); failBecauseExceptionWasNotThrown(RemotingSendRequestException.class); } catch (RemotingSendRequestException ignore) { } } + @Test + public void disabledServerAcceptUnAuthClient() throws Exception { + requestThenAssertResponse(); + } + /** * Tests that a server configured to require client authentication refuses to accept connections * from a client that has an untrusted certificate. @@ -187,7 +236,7 @@ public void serverRejectsSSLClient() throws Exception { @Test public void serverRejectsUntrustedClientCert() throws Exception { try { - RemotingCommand response = remotingClient.invokeSync("localhost:8888", createRequest(), 1000 * 5); + RemotingCommand response = remotingClient.invokeSync(getServerAddress(), createRequest(), 1000 * 5); failBecauseExceptionWasNotThrown(RemotingSendRequestException.class); } catch (RemotingSendRequestException ignore) { } @@ -196,6 +245,7 @@ public void serverRejectsUntrustedClientCert() throws Exception { @Test public void serverAcceptsUntrustedClientCert() throws Exception { requestThenAssertResponse(); +// Thread.sleep(1000000L); } /** @@ -205,7 +255,7 @@ public void serverAcceptsUntrustedClientCert() throws Exception { @Test public void noClientAuthFailure() throws Exception { try { - RemotingCommand response = remotingClient.invokeSync("localhost:8888", createRequest(), 1000 * 3); + RemotingCommand response = remotingClient.invokeSync(getServerAddress(), createRequest(), 1000 * 3); failBecauseExceptionWasNotThrown(RemotingSendRequestException.class); } catch (RemotingSendRequestException ignore) { } @@ -218,7 +268,7 @@ public void noClientAuthFailure() throws Exception { @Test public void clientRejectsUntrustedServerCert() throws Exception { try { - RemotingCommand response = remotingClient.invokeSync("localhost:8888", createRequest(), 1000 * 3); + RemotingCommand response = remotingClient.invokeSync(getServerAddress(), createRequest(), 1000 * 3); failBecauseExceptionWasNotThrown(RemotingSendRequestException.class); } catch (RemotingSendRequestException ignore) { } @@ -277,8 +327,35 @@ private static void writeStringToFile(String path, String content) { } private static String getCertsPath(String fileName) { - File resourcesDirectory = new File("src/test/resources/certs"); - return resourcesDirectory.getAbsolutePath() + "/" + fileName; + ClassLoader loader = TlsTest.class.getClassLoader(); + InputStream stream = loader.getResourceAsStream("certs/" + fileName); + if (null == stream) { + throw new RuntimeException("File: " + fileName + " is not found"); + } + + try { + String[] segments = fileName.split("\\."); + File f = File.createTempFile(UUID.randomUUID().toString(), segments[1]); + f.deleteOnExit(); + + try (BufferedInputStream bis = new BufferedInputStream(stream); + BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(f))) { + byte[] buffer = new byte[1024]; + int len; + while ((len = bis.read(buffer)) > 0) { + bos.write(buffer, 0, len); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return f.getAbsolutePath(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private String getServerAddress() { + return "localhost:" + remotingServer.localListenPort(); } private static RemotingCommand createRequest() { @@ -289,10 +366,23 @@ private static RemotingCommand createRequest() { } private void requestThenAssertResponse() throws Exception { - RemotingCommand response = remotingClient.invokeSync("localhost:8888", createRequest(), 1000 * 3); - assertTrue(response != null); + requestThenAssertResponse(remotingClient); + } + + private void requestThenAssertResponse(RemotingClient remotingClient) throws Exception { + RemotingCommand response = remotingClient.invokeSync(getServerAddress(), createRequest(), 1000 * 3); + assertNotNull(response); assertThat(response.getLanguage()).isEqualTo(LanguageCode.JAVA); assertThat(response.getExtFields()).hasSize(2); assertThat(response.getExtFields().get("messageTitle")).isEqualTo("Welcome"); } + + private boolean isHostConnectable(String addr) { + try (Socket socket = new Socket()) { + socket.connect(NetworkUtil.string2SocketAddress(addr)); + return true; + } catch (IOException ignored) { + } + return false; + } } diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/netty/FileRegionEncoderTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/netty/FileRegionEncoderTest.java index 6c7327f258e..0cbe627d801 100644 --- a/remoting/src/test/java/org/apache/rocketmq/remoting/netty/FileRegionEncoderTest.java +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/netty/FileRegionEncoderTest.java @@ -21,14 +21,15 @@ import io.netty.channel.DefaultFileRegion; import io.netty.channel.FileRegion; import io.netty.channel.embedded.EmbeddedChannel; +import org.junit.Assert; +import org.junit.Test; + import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.Random; import java.util.UUID; -import org.junit.Assert; -import org.junit.Test; public class FileRegionEncoderTest { diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/netty/MockChannel.java b/remoting/src/test/java/org/apache/rocketmq/remoting/netty/MockChannel.java new file mode 100644 index 00000000000..8ddcdf35df0 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/netty/MockChannel.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.netty; + +import io.netty.channel.ChannelFuture; +import io.netty.channel.local.LocalChannel; + +public class MockChannel extends LocalChannel { + @Override + public ChannelFuture writeAndFlush(Object msg) { + return new MockChannelPromise(MockChannel.this); + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/netty/MockChannelPromise.java b/remoting/src/test/java/org/apache/rocketmq/remoting/netty/MockChannelPromise.java new file mode 100644 index 00000000000..9c3a354871b --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/netty/MockChannelPromise.java @@ -0,0 +1,191 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.netty; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelPromise; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.jetbrains.annotations.NotNull; + +public class MockChannelPromise implements ChannelPromise { + protected Channel channel; + + public MockChannelPromise(Channel channel) { + this.channel = channel; + } + + @Override + public Channel channel() { + return channel; + } + + @Override + public ChannelPromise setSuccess(Void result) { + return this; + } + + @Override + public ChannelPromise setSuccess() { + return this; + } + + @Override + public boolean trySuccess() { + return false; + } + + @Override + public ChannelPromise setFailure(Throwable cause) { + return this; + } + + @Override + public ChannelPromise addListener(GenericFutureListener> listener) { + return this; + } + + @Override + public ChannelPromise addListeners(GenericFutureListener>... listeners) { + return this; + } + + @Override + public ChannelPromise removeListener(GenericFutureListener> listener) { + return this; + } + + @Override + public ChannelPromise removeListeners(GenericFutureListener>... listeners) { + return this; + } + + @Override + public ChannelPromise sync() throws InterruptedException { + return this; + } + + @Override + public ChannelPromise syncUninterruptibly() { + return this; + } + + @Override + public ChannelPromise await() throws InterruptedException { + return this; + } + + @Override + public ChannelPromise awaitUninterruptibly() { + return this; + } + + @Override + public ChannelPromise unvoid() { + return this; + } + + @Override + public boolean isVoid() { + return false; + } + + @Override + public boolean trySuccess(Void result) { + return false; + } + + @Override + public boolean tryFailure(Throwable cause) { + return false; + } + + @Override + public boolean setUncancellable() { + return false; + } + + @Override + public boolean isSuccess() { + return false; + } + + @Override + public boolean isCancellable() { + return false; + } + + @Override + public Throwable cause() { + return null; + } + + @Override + public boolean await(long timeout, TimeUnit unit) throws InterruptedException { + return false; + } + + @Override + public boolean await(long timeoutMillis) throws InterruptedException { + return false; + } + + @Override + public boolean awaitUninterruptibly(long timeout, TimeUnit unit) { + return false; + } + + @Override + public boolean awaitUninterruptibly(long timeoutMillis) { + return false; + } + + @Override + public Void getNow() { + return null; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return false; + } + + @Override + public Void get() throws InterruptedException, ExecutionException { + return null; + } + + @Override + public Void get(long timeout, + @NotNull java.util.concurrent.TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return null; + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/netty/NettyClientConfigTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/netty/NettyClientConfigTest.java new file mode 100644 index 00000000000..bc74950829b --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/netty/NettyClientConfigTest.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.netty; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(MockitoJUnitRunner.class) +public class NettyClientConfigTest { + + @Test + public void testChangeConfigBySystemProperty() { + + + System.setProperty(NettySystemConfig.COM_ROCKETMQ_REMOTING_CLIENT_WORKER_SIZE, "1"); + System.setProperty(NettySystemConfig.COM_ROCKETMQ_REMOTING_CLIENT_CONNECT_TIMEOUT, "2000"); + System.setProperty(NettySystemConfig.COM_ROCKETMQ_REMOTING_CLIENT_CHANNEL_MAX_IDLE_SECONDS, "60"); + System.setProperty(NettySystemConfig.COM_ROCKETMQ_REMOTING_SOCKET_SNDBUF_SIZE, "16383"); + System.setProperty(NettySystemConfig.COM_ROCKETMQ_REMOTING_SOCKET_RCVBUF_SIZE, "16384"); + System.setProperty(NettySystemConfig.COM_ROCKETMQ_REMOTING_CLIENT_CLOSE_SOCKET_IF_TIMEOUT, "false"); + System.setProperty(TlsSystemConfig.TLS_ENABLE, "true"); + + + NettySystemConfig.socketSndbufSize = + Integer.parseInt(System.getProperty(NettySystemConfig.COM_ROCKETMQ_REMOTING_SOCKET_SNDBUF_SIZE, "65535")); + NettySystemConfig.socketRcvbufSize = + Integer.parseInt(System.getProperty(NettySystemConfig.COM_ROCKETMQ_REMOTING_SOCKET_RCVBUF_SIZE, "65535")); + NettySystemConfig.clientWorkerSize = + Integer.parseInt(System.getProperty(NettySystemConfig.COM_ROCKETMQ_REMOTING_CLIENT_WORKER_SIZE, "4")); + NettySystemConfig.connectTimeoutMillis = + Integer.parseInt(System.getProperty(NettySystemConfig.COM_ROCKETMQ_REMOTING_CLIENT_CONNECT_TIMEOUT, "3000")); + NettySystemConfig.clientChannelMaxIdleTimeSeconds = + Integer.parseInt(System.getProperty(NettySystemConfig.COM_ROCKETMQ_REMOTING_CLIENT_CHANNEL_MAX_IDLE_SECONDS, "120")); + NettySystemConfig.clientCloseSocketIfTimeout = + Boolean.parseBoolean(System.getProperty(NettySystemConfig.COM_ROCKETMQ_REMOTING_CLIENT_CLOSE_SOCKET_IF_TIMEOUT, "true")); + + NettyClientConfig changedConfig = new NettyClientConfig(); + assertThat(changedConfig.getClientWorkerThreads()).isEqualTo(1); + assertThat(changedConfig.getClientOnewaySemaphoreValue()).isEqualTo(65535); + assertThat(changedConfig.getClientAsyncSemaphoreValue()).isEqualTo(65535); + assertThat(changedConfig.getConnectTimeoutMillis()).isEqualTo(2000); + assertThat(changedConfig.getClientChannelMaxIdleTimeSeconds()).isEqualTo(60); + assertThat(changedConfig.getClientSocketSndBufSize()).isEqualTo(16383); + assertThat(changedConfig.getClientSocketRcvBufSize()).isEqualTo(16384); + assertThat(changedConfig.isClientCloseSocketIfTimeout()).isEqualTo(false); + assertThat(changedConfig.isUseTLS()).isEqualTo(true); + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/netty/NettyRemotingAbstractTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/netty/NettyRemotingAbstractTest.java index 99aab9e0757..dbbea86ea2f 100644 --- a/remoting/src/test/java/org/apache/rocketmq/remoting/netty/NettyRemotingAbstractTest.java +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/netty/NettyRemotingAbstractTest.java @@ -26,6 +26,7 @@ import org.mockito.junit.MockitoJUnitRunner; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertNull; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) @@ -36,11 +37,21 @@ public class NettyRemotingAbstractTest { @Test public void testProcessResponseCommand() throws InterruptedException { final Semaphore semaphore = new Semaphore(0); - ResponseFuture responseFuture = new ResponseFuture(1, 3000, new InvokeCallback() { + ResponseFuture responseFuture = new ResponseFuture(null, 1, 3000, new InvokeCallback() { @Override - public void operationComplete(final ResponseFuture responseFuture) { + public void operationComplete(ResponseFuture responseFuture) { + + } + + @Override + public void operationSucceed(RemotingCommand response) { assertThat(semaphore.availablePermits()).isEqualTo(0); } + + @Override + public void operationFail(Throwable throwable) { + + } }, new SemaphoreReleaseOnlyOnce(semaphore)); remotingAbstract.responseTable.putIfAbsent(1, responseFuture); @@ -57,7 +68,7 @@ public void operationComplete(final ResponseFuture responseFuture) { @Test public void testProcessResponseCommand_NullCallBack() throws InterruptedException { final Semaphore semaphore = new Semaphore(0); - ResponseFuture responseFuture = new ResponseFuture(1, 3000, null, + ResponseFuture responseFuture = new ResponseFuture(null, 1, 3000, null, new SemaphoreReleaseOnlyOnce(semaphore)); remotingAbstract.responseTable.putIfAbsent(1, responseFuture); @@ -72,11 +83,21 @@ public void testProcessResponseCommand_NullCallBack() throws InterruptedExceptio @Test public void testProcessResponseCommand_RunCallBackInCurrentThread() throws InterruptedException { final Semaphore semaphore = new Semaphore(0); - ResponseFuture responseFuture = new ResponseFuture(1, 3000, new InvokeCallback() { + ResponseFuture responseFuture = new ResponseFuture(null, 1, 3000, new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { + + } + @Override - public void operationComplete(final ResponseFuture responseFuture) { + public void operationSucceed(RemotingCommand response) { assertThat(semaphore.availablePermits()).isEqualTo(0); } + + @Override + public void operationFail(Throwable throwable) { + + } }, new SemaphoreReleaseOnlyOnce(semaphore)); remotingAbstract.responseTable.putIfAbsent(1, responseFuture); @@ -90,4 +111,61 @@ public void operationComplete(final ResponseFuture responseFuture) { semaphore.acquire(1); assertThat(semaphore.availablePermits()).isEqualTo(0); } + + @Test + public void testScanResponseTable() { + int dummyId = 1; + // mock timeout + ResponseFuture responseFuture = new ResponseFuture(null, dummyId, -1000, new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { + + } + + @Override + public void operationSucceed(RemotingCommand response) { + + } + + @Override + public void operationFail(Throwable throwable) { + + } + }, null); + remotingAbstract.responseTable.putIfAbsent(dummyId, responseFuture); + remotingAbstract.scanResponseTable(); + assertNull(remotingAbstract.responseTable.get(dummyId)); + } + + @Test + public void testProcessRequestCommand() throws InterruptedException { + final Semaphore semaphore = new Semaphore(0); + RemotingCommand request = RemotingCommand.createRequestCommand(1, null); + ResponseFuture responseFuture = new ResponseFuture(null, 1, request, 3000, + new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { + + } + + @Override + public void operationSucceed(RemotingCommand response) { + assertThat(semaphore.availablePermits()).isEqualTo(0); + } + + @Override + public void operationFail(Throwable throwable) { + + } + }, new SemaphoreReleaseOnlyOnce(semaphore)); + + remotingAbstract.responseTable.putIfAbsent(1, responseFuture); + RemotingCommand response = RemotingCommand.createResponseCommand(0, "Foo"); + response.setOpaque(1); + remotingAbstract.processResponseCommand(null, response); + + // Acquire the release permit after call back + semaphore.acquire(1); + assertThat(semaphore.availablePermits()).isEqualTo(0); + } } \ No newline at end of file diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/netty/NettyRemotingClientTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/netty/NettyRemotingClientTest.java index 04a3bebbb57..456e7ecdd59 100644 --- a/remoting/src/test/java/org/apache/rocketmq/remoting/netty/NettyRemotingClientTest.java +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/netty/NettyRemotingClientTest.java @@ -16,28 +16,294 @@ */ package org.apache.rocketmq.remoting.netty; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.local.LocalChannel; + import java.lang.reflect.Field; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Semaphore; +import org.apache.rocketmq.remoting.InvokeCallback; +import org.apache.rocketmq.remoting.RPCHook; +import org.apache.rocketmq.remoting.common.SemaphoreReleaseOnlyOnce; +import org.apache.rocketmq.remoting.exception.RemotingConnectException; +import org.apache.rocketmq.remoting.exception.RemotingException; +import org.apache.rocketmq.remoting.exception.RemotingSendRequestException; +import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForClassTypes.catchThrowable; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class NettyRemotingClientTest { + @Spy private NettyRemotingClient remotingClient = new NettyRemotingClient(new NettyClientConfig()); + @Mock + private RPCHook rpcHookMock; @Test - public void testSetCallbackExecutor() throws NoSuchFieldException, IllegalAccessException { - Field field = NettyRemotingClient.class.getDeclaredField("publicExecutor"); - field.setAccessible(true); - assertThat(remotingClient.getCallbackExecutor()).isEqualTo(field.get(remotingClient)); - + public void testSetCallbackExecutor() { ExecutorService customized = Executors.newCachedThreadPool(); remotingClient.setCallbackExecutor(customized); - assertThat(remotingClient.getCallbackExecutor()).isEqualTo(customized); } -} \ No newline at end of file + + @Test + public void testInvokeResponse() throws Exception { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, null); + + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + ResponseFuture responseFuture = new ResponseFuture(null, request.getOpaque(), 3 * 1000, null, null); + responseFuture.setResponseCommand(response); + CompletableFuture future0 = new CompletableFuture<>(); + future0.complete(responseFuture.getResponseCommand()); + doReturn(future0).when(remotingClient).invoke(anyString(), any(RemotingCommand.class), anyLong()); + + CompletableFuture future = remotingClient.invoke("0.0.0.0", request, 1000); + RemotingCommand actual = future.get(); + assertThat(actual).isEqualTo(response); + } + + @Test + public void testRemotingSendRequestException() throws Exception { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, null); + + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + CompletableFuture future0 = new CompletableFuture<>(); + future0.completeExceptionally(new RemotingSendRequestException(null)); + doReturn(future0).when(remotingClient).invoke(anyString(), any(RemotingCommand.class), anyLong()); + + CompletableFuture future = remotingClient.invoke("0.0.0.0", request, 1000); + Throwable thrown = catchThrowable(future::get); + assertThat(thrown.getCause()).isInstanceOf(RemotingSendRequestException.class); + } + + @Test + public void testRemotingTimeoutException() throws Exception { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, null); + + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + CompletableFuture future0 = new CompletableFuture<>(); + future0.completeExceptionally(new RemotingTimeoutException("")); + doReturn(future0).when(remotingClient).invoke(anyString(), any(RemotingCommand.class), anyLong()); + + CompletableFuture future = remotingClient.invoke("0.0.0.0", request, 1000); + Throwable thrown = catchThrowable(future::get); + assertThat(thrown.getCause()).isInstanceOf(RemotingTimeoutException.class); + } + + @Test + public void testRemotingException() throws Exception { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, null); + + CompletableFuture future0 = new CompletableFuture<>(); + future0.completeExceptionally(new RemotingException("")); + doReturn(future0).when(remotingClient).invoke(anyString(), any(RemotingCommand.class), anyLong()); + + CompletableFuture future = remotingClient.invoke("0.0.0.0", request, 1000); + Throwable thrown = catchThrowable(future::get); + assertThat(thrown.getCause()).isInstanceOf(RemotingException.class); + } + + @Test + public void testInvokeOnewayException() throws Exception { + String addr = "0.0.0.0"; + try { + remotingClient.invokeOneway(addr, null, 1000); + } catch (RemotingConnectException e) { + assertThat(e.getMessage()).contains(addr); + } + } + + @Test + public void testInvoke0() throws ExecutionException, InterruptedException { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, null); + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + Channel channel = new MockChannel() { + @Override + public ChannelFuture writeAndFlush(Object msg) { + ResponseFuture responseFuture = remotingClient.responseTable.get(request.getOpaque()); + responseFuture.setResponseCommand(response); + responseFuture.executeInvokeCallback(); + return super.writeAndFlush(msg); + } + }; + CompletableFuture future = remotingClient.invoke0(channel, request, 1000L); + assertThat(future.get().getResponseCommand()).isEqualTo(response); + } + + @Test + public void testInvoke0WithException() { + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, null); + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + Channel channel = new MockChannel() { + @Override + public ChannelFuture writeAndFlush(Object msg) { + ResponseFuture responseFuture = remotingClient.responseTable.get(request.getOpaque()); + responseFuture.executeInvokeCallback(); + return super.writeAndFlush(msg); + } + }; + CompletableFuture future = remotingClient.invoke0(channel, request, 1000L); + assertThatThrownBy(future::get).getCause().isInstanceOf(RemotingException.class); + } + + @Test + public void testInvokeSync() throws RemotingSendRequestException, RemotingTimeoutException, InterruptedException { + remotingClient.registerRPCHook(rpcHookMock); + + Channel channel = new LocalChannel(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, null); + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + ResponseFuture responseFuture = new ResponseFuture(channel, request.getOpaque(), request, 1000, new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { + + } + }, new SemaphoreReleaseOnlyOnce(new Semaphore(1))); + responseFuture.setResponseCommand(response); + CompletableFuture future = new CompletableFuture<>(); + future.complete(responseFuture); + + doReturn(future).when(remotingClient).invoke0(any(Channel.class), any(RemotingCommand.class), anyLong()); + RemotingCommand actual = remotingClient.invokeSyncImpl(channel, request, 1000); + assertThat(actual).isEqualTo(response); + + verify(rpcHookMock).doBeforeRequest(anyString(), eq(request)); + verify(rpcHookMock).doAfterResponse(anyString(), eq(request), eq(response)); + } + + @Test + public void testInvokeAsync() { + remotingClient.registerRPCHook(rpcHookMock); + Channel channel = new LocalChannel(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, null); + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + ResponseFuture responseFuture = new ResponseFuture(channel, request.getOpaque(), request, 1000, new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { + + } + }, new SemaphoreReleaseOnlyOnce(new Semaphore(1))); + responseFuture.setResponseCommand(response); + CompletableFuture future = new CompletableFuture<>(); + future.complete(responseFuture); + + doReturn(future).when(remotingClient).invoke0(any(Channel.class), any(RemotingCommand.class), anyLong()); + + InvokeCallback callback = mock(InvokeCallback.class); + remotingClient.invokeAsyncImpl(channel, request, 1000, callback); + verify(callback, times(1)).operationSucceed(eq(response)); + verify(callback, times(1)).operationComplete(eq(responseFuture)); + verify(callback, never()).operationFail(any()); + + verify(rpcHookMock).doBeforeRequest(anyString(), eq(request)); + verify(rpcHookMock).doAfterResponse(anyString(), eq(request), eq(response)); + } + + @Test + public void testInvokeAsyncFail() { + remotingClient.registerRPCHook(rpcHookMock); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, null); + + Channel channel = new LocalChannel(); + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new RemotingException(null)); + + doReturn(future).when(remotingClient).invoke0(any(Channel.class), any(RemotingCommand.class), anyLong()); + + InvokeCallback callback = mock(InvokeCallback.class); + remotingClient.invokeAsyncImpl(channel, request, 1000, callback); + verify(callback, never()).operationSucceed(any()); + verify(callback, times(1)).operationComplete(any()); + verify(callback, times(1)).operationFail(any()); + + verify(rpcHookMock).doBeforeRequest(anyString(), eq(request)); + verify(rpcHookMock, never()).doAfterResponse(anyString(), eq(request), any()); + } + + @Test + public void testInvokeImpl() throws ExecutionException, InterruptedException { + remotingClient.registerRPCHook(rpcHookMock); + Channel channel = new LocalChannel(); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, null); + RemotingCommand response = RemotingCommand.createResponseCommand(null); + response.setCode(ResponseCode.SUCCESS); + ResponseFuture responseFuture = new ResponseFuture(channel, request.getOpaque(), request, 1000, new InvokeCallback() { + @Override + public void operationComplete(ResponseFuture responseFuture) { + + } + }, new SemaphoreReleaseOnlyOnce(new Semaphore(1))); + responseFuture.setResponseCommand(response); + CompletableFuture future = new CompletableFuture<>(); + future.complete(responseFuture); + + doReturn(future).when(remotingClient).invoke0(any(Channel.class), any(RemotingCommand.class), anyLong()); + + CompletableFuture future0 = remotingClient.invokeImpl(channel, request, 1000); + assertThat(future0.get()).isEqualTo(responseFuture); + + verify(rpcHookMock).doBeforeRequest(anyString(), eq(request)); + verify(rpcHookMock).doAfterResponse(anyString(), eq(request), eq(response)); + } + + @Test + public void testInvokeImplFail() { + remotingClient.registerRPCHook(rpcHookMock); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, null); + + Channel channel = new LocalChannel(); + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new RemotingException(null)); + + doReturn(future).when(remotingClient).invoke0(any(Channel.class), any(RemotingCommand.class), anyLong()); + + assertThatThrownBy(() -> remotingClient.invokeImpl(channel, request, 1000).get()).getCause().isInstanceOf(RemotingException.class); + + verify(rpcHookMock).doBeforeRequest(anyString(), eq(request)); + verify(rpcHookMock, never()).doAfterResponse(anyString(), eq(request), any()); + } + + @Test + public void testIsAddressReachableFail() throws NoSuchFieldException, IllegalAccessException { + Bootstrap bootstrap = spy(Bootstrap.class); + Field field = NettyRemotingClient.class.getDeclaredField("bootstrap"); + field.setAccessible(true); + field.set(remotingClient, bootstrap); + assertThat(remotingClient.isAddressReachable("0.0.0.0:8080")).isFalse(); + verify(bootstrap).connect(eq("0.0.0.0"), eq(8080)); + assertThat(remotingClient.isAddressReachable("[fe80::]:8080")).isFalse(); + verify(bootstrap).connect(eq("[fe80::]"), eq(8080)); + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/netty/NettyRemotingServerTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/netty/NettyRemotingServerTest.java new file mode 100644 index 00000000000..c69fcebd453 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/netty/NettyRemotingServerTest.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.netty; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.handler.codec.haproxy.HAProxyTLV; +import io.netty.util.Attribute; +import io.netty.util.AttributeKey; +import java.nio.charset.StandardCharsets; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class NettyRemotingServerTest { + + private NettyRemotingServer nettyRemotingServer; + + @Mock + private Channel channel; + + @Mock + private Attribute attribute; + + @Before + public void setUp() throws Exception { + NettyServerConfig nettyServerConfig = new NettyServerConfig(); + nettyRemotingServer = new NettyRemotingServer(nettyServerConfig); + } + + @Test + public void handleHAProxyTLV() { + when(channel.attr(any(AttributeKey.class))).thenReturn(attribute); + doNothing().when(attribute).set(any()); + + ByteBuf content = Unpooled.buffer(); + content.writeBytes("xxxx".getBytes(StandardCharsets.UTF_8)); + HAProxyTLV haProxyTLV = new HAProxyTLV((byte) 0xE1, content); + nettyRemotingServer.handleHAProxyTLV(haProxyTLV, channel); + } +} \ No newline at end of file diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/netty/NettyServerConfigTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/netty/NettyServerConfigTest.java new file mode 100644 index 00000000000..0ab0d193e8a --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/netty/NettyServerConfigTest.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.netty; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(MockitoJUnitRunner.class) +public class NettyServerConfigTest { + + @Test + public void testChangeConfigBySystemProperty() { + System.setProperty(NettySystemConfig.COM_ROCKETMQ_REMOTING_SOCKET_BACKLOG, "65535"); + NettySystemConfig.socketBacklog = + Integer.parseInt(System.getProperty(NettySystemConfig.COM_ROCKETMQ_REMOTING_SOCKET_BACKLOG, "1024")); + NettyServerConfig changedConfig = new NettyServerConfig(); + assertThat(changedConfig.getServerSocketBacklog()).isEqualTo(65535); + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/netty/RemotingCodeDistributionHandlerTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/netty/RemotingCodeDistributionHandlerTest.java new file mode 100644 index 00000000000..eb623a9de92 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/netty/RemotingCodeDistributionHandlerTest.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.netty; + +import java.lang.reflect.Method; +import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.junit.Assert; +import org.junit.Test; + +import static org.awaitility.Awaitility.await; + +public class RemotingCodeDistributionHandlerTest { + + private final RemotingCodeDistributionHandler distributionHandler = new RemotingCodeDistributionHandler(); + + @Test + public void remotingCodeCountTest() throws Exception { + Class clazz = RemotingCodeDistributionHandler.class; + Method methodIn = clazz.getDeclaredMethod("countInbound", int.class); + Method methodOut = clazz.getDeclaredMethod("countOutbound", int.class); + methodIn.setAccessible(true); + methodOut.setAccessible(true); + + int threadCount = 4; + int count = 1000 * 1000; + CountDownLatch latch = new CountDownLatch(threadCount); + AtomicBoolean result = new AtomicBoolean(true); + ExecutorService executorService = Executors.newFixedThreadPool(threadCount, new ThreadFactoryImpl("RemotingCodeTest_")); + + for (int i = 0; i < threadCount; i++) { + executorService.submit(() -> { + try { + for (int j = 0; j < count; j++) { + methodIn.invoke(distributionHandler, 1); + methodOut.invoke(distributionHandler, 2); + } + } catch (Exception e) { + result.set(false); + } finally { + latch.countDown(); + } + }); + } + + latch.await(); + Assert.assertTrue(result.get()); + await().pollInterval(Duration.ofMillis(100)).atMost(Duration.ofSeconds(10)).until(() -> { + boolean f1 = ("{1:" + count * threadCount + "}").equals(distributionHandler.getInBoundSnapshotString()); + boolean f2 = ("{2:" + count * threadCount + "}").equals(distributionHandler.getOutBoundSnapshotString()); + return f1 && f2; + }); + } +} \ No newline at end of file diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/CheckpointFileTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/CheckpointFileTest.java new file mode 100644 index 00000000000..658f59e4806 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/CheckpointFileTest.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.utils.CheckpointFile; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class CheckpointFileTest { + + private static final String FILE_PATH = + Paths.get(System.getProperty("java.io.tmpdir"), "store-test", "epoch.ckpt").toString(); + + private List entryList; + private CheckpointFile checkpoint; + + static class EpochEntrySerializer implements CheckpointFile.CheckpointSerializer { + + @Override + public String toLine(EpochEntry entry) { + if (entry != null) { + return String.format("%d-%d", entry.getEpoch(), entry.getStartOffset()); + } else { + return null; + } + } + + @Override + public EpochEntry fromLine(String line) { + final String[] arr = line.split("-"); + if (arr.length == 2) { + final int epoch = Integer.parseInt(arr[0]); + final long startOffset = Long.parseLong(arr[1]); + return new EpochEntry(epoch, startOffset); + } + return null; + } + } + + @Before + public void init() throws IOException { + this.entryList = new ArrayList<>(); + entryList.add(new EpochEntry(7, 7000)); + entryList.add(new EpochEntry(8, 8000)); + this.checkpoint = new CheckpointFile<>(FILE_PATH, new EpochEntrySerializer()); + this.checkpoint.write(entryList); + } + + @After + public void destroy() { + UtilAll.deleteFile(new File(FILE_PATH)); + UtilAll.deleteFile(new File(FILE_PATH + ".bak")); + } + + @Test + public void testNormalWriteAndRead() throws IOException { + List listFromFile = checkpoint.read(); + Assert.assertEquals(entryList, listFromFile); + checkpoint.write(entryList); + listFromFile = checkpoint.read(); + Assert.assertEquals(entryList, listFromFile); + } + + @Test + public void testAbNormalWriteAndRead() throws IOException { + this.checkpoint.write(entryList); + UtilAll.deleteFile(new File(FILE_PATH)); + List listFromFile = checkpoint.read(); + Assert.assertEquals(entryList, listFromFile); + checkpoint.write(entryList); + listFromFile = checkpoint.read(); + Assert.assertEquals(entryList, listFromFile); + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/ClusterInfoTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/ClusterInfoTest.java new file mode 100644 index 00000000000..7d31931d5e8 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/ClusterInfoTest.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.protocol.body.ClusterInfo; +import org.apache.rocketmq.remoting.protocol.route.BrokerData; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class ClusterInfoTest { + + @Test + public void testFormJson() throws Exception { + ClusterInfo clusterInfo = buildClusterInfo(); + byte[] data = clusterInfo.encode(); + ClusterInfo json = RemotingSerializable.decode(data, ClusterInfo.class); + + assertNotNull(json); + assertNotNull(json.getClusterAddrTable()); + assertTrue(json.getClusterAddrTable().containsKey("DEFAULT_CLUSTER")); + assertTrue(json.getClusterAddrTable().get("DEFAULT_CLUSTER").contains("master")); + assertNotNull(json.getBrokerAddrTable()); + assertTrue(json.getBrokerAddrTable().containsKey("master")); + assertEquals(json.getBrokerAddrTable().get("master").getBrokerName(), "master"); + assertEquals(json.getBrokerAddrTable().get("master").getCluster(), "DEFAULT_CLUSTER"); + assertEquals(json.getBrokerAddrTable().get("master").getBrokerAddrs().get(MixAll.MASTER_ID), MixAll.getLocalhostByNetworkInterface()); + } + + @Test + public void testRetrieveAllClusterNames() throws Exception { + ClusterInfo clusterInfo = buildClusterInfo(); + byte[] data = clusterInfo.encode(); + ClusterInfo json = RemotingSerializable.decode(data, ClusterInfo.class); + + assertArrayEquals(new String[]{"DEFAULT_CLUSTER"}, json.retrieveAllClusterNames()); + } + + + @Test + public void testRetrieveAllAddrByCluster() throws Exception { + ClusterInfo clusterInfo = buildClusterInfo(); + byte[] data = clusterInfo.encode(); + ClusterInfo json = RemotingSerializable.decode(data, ClusterInfo.class); + + assertArrayEquals(new String[]{MixAll.getLocalhostByNetworkInterface()}, json.retrieveAllAddrByCluster("DEFAULT_CLUSTER")); + } + + + private ClusterInfo buildClusterInfo() throws Exception { + ClusterInfo clusterInfo = new ClusterInfo(); + HashMap brokerAddrTable = new HashMap<>(); + HashMap> clusterAddrTable = new HashMap<>(); + + //build brokerData + BrokerData brokerData = new BrokerData(); + brokerData.setBrokerName("master"); + brokerData.setCluster("DEFAULT_CLUSTER"); + + //build brokerAddrs + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(MixAll.MASTER_ID, MixAll.getLocalhostByNetworkInterface()); + + brokerData.setBrokerAddrs(brokerAddrs); + brokerAddrTable.put("master", brokerData); + + Set brokerNames = new HashSet<>(); + brokerNames.add("master"); + + clusterAddrTable.put("DEFAULT_CLUSTER", brokerNames); + + clusterInfo.setBrokerAddrTable(brokerAddrTable); + clusterInfo.setClusterAddrTable(clusterAddrTable); + return clusterInfo; + } +} diff --git a/common/src/test/java/org/apache/rocketmq/common/protocol/ConsumeStatusTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/ConsumeStatusTest.java similarity index 90% rename from common/src/test/java/org/apache/rocketmq/common/protocol/ConsumeStatusTest.java rename to remoting/src/test/java/org/apache/rocketmq/remoting/protocol/ConsumeStatusTest.java index 4a2e790fe66..b685d31311a 100644 --- a/common/src/test/java/org/apache/rocketmq/common/protocol/ConsumeStatusTest.java +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/ConsumeStatusTest.java @@ -15,10 +15,9 @@ * limitations under the License. */ -package org.apache.rocketmq.common.protocol; +package org.apache.rocketmq.remoting.protocol; -import org.apache.rocketmq.common.protocol.body.ConsumeStatus; -import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.body.ConsumeStatus; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; diff --git a/common/src/test/java/org/apache/rocketmq/common/DataVersionTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/DataVersionTest.java similarity index 90% rename from common/src/test/java/org/apache/rocketmq/common/DataVersionTest.java rename to remoting/src/test/java/org/apache/rocketmq/remoting/protocol/DataVersionTest.java index f4d14e5614f..dccedde491c 100644 --- a/common/src/test/java/org/apache/rocketmq/common/DataVersionTest.java +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/DataVersionTest.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.rocketmq.common; +package org.apache.rocketmq.remoting.protocol; import java.util.concurrent.atomic.AtomicLong; import org.junit.Assert; @@ -67,4 +67,11 @@ public void testEquals_trueWhenCountersBothNull() { other.setTimestamp(dataVersion.getTimestamp()); Assert.assertTrue(dataVersion.equals(other)); } -} \ No newline at end of file + + @Test + public void testEncode() { + DataVersion dataVersion = new DataVersion(); + Assert.assertTrue(dataVersion.encode().length > 0); + Assert.assertNotNull(dataVersion.toJson()); + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/GroupListTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/GroupListTest.java new file mode 100644 index 00000000000..e0fba128d17 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/GroupListTest.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol; + +import java.util.HashSet; +import java.util.UUID; +import org.apache.rocketmq.remoting.protocol.body.GroupList; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Created by guoyao on 2019/2/18. + */ +public class GroupListTest { + + @Test + public void testSetGet() throws Exception { + HashSet fisrtUniqueSet = createUniqueNewSet(); + HashSet secondUniqueSet = createUniqueNewSet(); + assertThat(fisrtUniqueSet).isNotEqualTo(secondUniqueSet); + GroupList gl = new GroupList(); + gl.setGroupList(fisrtUniqueSet); + assertThat(gl.getGroupList()).isEqualTo(fisrtUniqueSet); + assertThat(gl.getGroupList()).isNotEqualTo(secondUniqueSet); + gl.setGroupList(secondUniqueSet); + assertThat(gl.getGroupList()).isNotEqualTo(fisrtUniqueSet); + assertThat(gl.getGroupList()).isEqualTo(secondUniqueSet); + } + + private HashSet createUniqueNewSet() { + HashSet groups = new HashSet<>(); + groups.add(UUID.randomUUID().toString()); + return groups; + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/LanguageCodeTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/LanguageCodeTest.java new file mode 100644 index 00000000000..e1f9016cc38 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/LanguageCodeTest.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol; + +import org.junit.Test; +import static org.assertj.core.api.Assertions.assertThat; + +public class LanguageCodeTest { + + @Test + public void testLanguageCodeRust() { + LanguageCode code = LanguageCode.valueOf((byte) 12); + assertThat(code).isEqualTo(LanguageCode.RUST); + + code = LanguageCode.valueOf("RUST"); + assertThat(code).isEqualTo(LanguageCode.RUST); + } + +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/NamespaceUtilTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/NamespaceUtilTest.java new file mode 100644 index 00000000000..2f7af7adff6 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/NamespaceUtilTest.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol; + +import org.apache.rocketmq.common.MixAll; +import org.junit.Assert; +import org.junit.Test; + +/** + * MQDevelopers + */ +public class NamespaceUtilTest { + + private static final String INSTANCE_ID = "MQ_INST_XXX"; + private static final String INSTANCE_ID_WRONG = "MQ_INST_XXX1"; + private static final String TOPIC = "TOPIC_XXX"; + private static final String GROUP_ID = "GID_XXX"; + private static final String SYSTEM_TOPIC = "rmq_sys_topic"; + private static final String GROUP_ID_WITH_NAMESPACE = INSTANCE_ID + NamespaceUtil.NAMESPACE_SEPARATOR + GROUP_ID; + private static final String TOPIC_WITH_NAMESPACE = INSTANCE_ID + NamespaceUtil.NAMESPACE_SEPARATOR + TOPIC; + private static final String RETRY_TOPIC = MixAll.RETRY_GROUP_TOPIC_PREFIX + GROUP_ID; + private static final String RETRY_TOPIC_WITH_NAMESPACE = + MixAll.RETRY_GROUP_TOPIC_PREFIX + INSTANCE_ID + NamespaceUtil.NAMESPACE_SEPARATOR + GROUP_ID; + private static final String DLQ_TOPIC = MixAll.DLQ_GROUP_TOPIC_PREFIX + GROUP_ID; + private static final String DLQ_TOPIC_WITH_NAMESPACE = + MixAll.DLQ_GROUP_TOPIC_PREFIX + INSTANCE_ID + NamespaceUtil.NAMESPACE_SEPARATOR + GROUP_ID; + + @Test + public void testWithoutNamespace() { + String topic = NamespaceUtil.withoutNamespace(TOPIC_WITH_NAMESPACE, INSTANCE_ID); + Assert.assertEquals(topic, TOPIC); + String topic1 = NamespaceUtil.withoutNamespace(TOPIC_WITH_NAMESPACE); + Assert.assertEquals(topic1, TOPIC); + String groupId = NamespaceUtil.withoutNamespace(GROUP_ID_WITH_NAMESPACE, INSTANCE_ID); + Assert.assertEquals(groupId, GROUP_ID); + String groupId1 = NamespaceUtil.withoutNamespace(GROUP_ID_WITH_NAMESPACE); + Assert.assertEquals(groupId1, GROUP_ID); + String consumerId = NamespaceUtil.withoutNamespace(RETRY_TOPIC_WITH_NAMESPACE, INSTANCE_ID); + Assert.assertEquals(consumerId, RETRY_TOPIC); + String consumerId1 = NamespaceUtil.withoutNamespace(RETRY_TOPIC_WITH_NAMESPACE); + Assert.assertEquals(consumerId1, RETRY_TOPIC); + String consumerId2 = NamespaceUtil.withoutNamespace(RETRY_TOPIC_WITH_NAMESPACE, INSTANCE_ID_WRONG); + Assert.assertEquals(consumerId2, RETRY_TOPIC_WITH_NAMESPACE); + Assert.assertNotEquals(consumerId2, RETRY_TOPIC); + } + + @Test + public void testWrapNamespace() { + String topic1 = NamespaceUtil.wrapNamespace(INSTANCE_ID, TOPIC); + Assert.assertEquals(topic1, TOPIC_WITH_NAMESPACE); + String topicWithNamespaceAgain = NamespaceUtil.wrapNamespace(INSTANCE_ID, topic1); + Assert.assertEquals(topicWithNamespaceAgain, TOPIC_WITH_NAMESPACE); + //Wrap retry topic + String retryTopicWithNamespace = NamespaceUtil.wrapNamespace(INSTANCE_ID, RETRY_TOPIC); + Assert.assertEquals(retryTopicWithNamespace, RETRY_TOPIC_WITH_NAMESPACE); + String retryTopicWithNamespaceAgain = NamespaceUtil.wrapNamespace(INSTANCE_ID, retryTopicWithNamespace); + Assert.assertEquals(retryTopicWithNamespaceAgain, retryTopicWithNamespace); + //Wrap DLQ topic + String dlqTopicWithNamespace = NamespaceUtil.wrapNamespace(INSTANCE_ID, DLQ_TOPIC); + Assert.assertEquals(dlqTopicWithNamespace, DLQ_TOPIC_WITH_NAMESPACE); + String dlqTopicWithNamespaceAgain = NamespaceUtil.wrapNamespace(INSTANCE_ID, dlqTopicWithNamespace); + Assert.assertEquals(dlqTopicWithNamespaceAgain, dlqTopicWithNamespace); + Assert.assertEquals(dlqTopicWithNamespaceAgain, DLQ_TOPIC_WITH_NAMESPACE); + //test system topic + String systemTopic = NamespaceUtil.wrapNamespace(INSTANCE_ID, SYSTEM_TOPIC); + Assert.assertEquals(systemTopic, SYSTEM_TOPIC); + } + + @Test + public void testGetNamespaceFromResource() { + String namespaceExpectBlank = NamespaceUtil.getNamespaceFromResource(TOPIC); + Assert.assertEquals(namespaceExpectBlank, NamespaceUtil.STRING_BLANK); + String namespace = NamespaceUtil.getNamespaceFromResource(TOPIC_WITH_NAMESPACE); + Assert.assertEquals(namespace, INSTANCE_ID); + String namespaceFromRetryTopic = NamespaceUtil.getNamespaceFromResource(RETRY_TOPIC_WITH_NAMESPACE); + Assert.assertEquals(namespaceFromRetryTopic, INSTANCE_ID); + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/QueryConsumeTimeSpanBodyTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/QueryConsumeTimeSpanBodyTest.java new file mode 100644 index 00000000000..6460d80a2cd --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/QueryConsumeTimeSpanBodyTest.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.UUID; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.body.QueryConsumeTimeSpanBody; +import org.apache.rocketmq.remoting.protocol.body.QueueTimeSpan; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class QueryConsumeTimeSpanBodyTest { + + @Test + public void testSetGet() throws Exception { + QueryConsumeTimeSpanBody queryConsumeTimeSpanBody = new QueryConsumeTimeSpanBody(); + List firstQueueTimeSpans = newUniqueConsumeTimeSpanSet(); + List secondQueueTimeSpans = newUniqueConsumeTimeSpanSet(); + queryConsumeTimeSpanBody.setConsumeTimeSpanSet(firstQueueTimeSpans); + assertThat(queryConsumeTimeSpanBody.getConsumeTimeSpanSet()).isEqualTo(firstQueueTimeSpans); + assertThat(queryConsumeTimeSpanBody.getConsumeTimeSpanSet()).isNotEqualTo(secondQueueTimeSpans); + queryConsumeTimeSpanBody.setConsumeTimeSpanSet(secondQueueTimeSpans); + assertThat(queryConsumeTimeSpanBody.getConsumeTimeSpanSet()).isEqualTo(secondQueueTimeSpans); + assertThat(queryConsumeTimeSpanBody.getConsumeTimeSpanSet()).isNotEqualTo(firstQueueTimeSpans); + } + + @Test + public void testFromJson() throws Exception { + QueryConsumeTimeSpanBody qctsb = new QueryConsumeTimeSpanBody(); + List queueTimeSpans = new ArrayList<>(); + QueueTimeSpan queueTimeSpan = new QueueTimeSpan(); + queueTimeSpan.setMinTimeStamp(1550825710000L); + queueTimeSpan.setMaxTimeStamp(1550825790000L); + queueTimeSpan.setConsumeTimeStamp(1550825760000L); + queueTimeSpan.setDelayTime(5000L); + MessageQueue messageQueue = new MessageQueue("topicName", "brokerName", 1); + queueTimeSpan.setMessageQueue(messageQueue); + queueTimeSpans.add(queueTimeSpan); + qctsb.setConsumeTimeSpanSet(queueTimeSpans); + String json = RemotingSerializable.toJson(qctsb, true); + QueryConsumeTimeSpanBody fromJson = RemotingSerializable.fromJson(json, QueryConsumeTimeSpanBody.class); + assertThat(fromJson.getConsumeTimeSpanSet().get(0).getMaxTimeStamp()).isEqualTo(1550825790000L); + assertThat(fromJson.getConsumeTimeSpanSet().get(0).getMinTimeStamp()).isEqualTo(1550825710000L); + assertThat(fromJson.getConsumeTimeSpanSet().get(0).getDelayTime()).isEqualTo(5000L); + assertThat(fromJson.getConsumeTimeSpanSet().get(0).getMessageQueue()).isEqualTo(messageQueue); + } + + @Test + public void testFromJsonRandom() throws Exception { + QueryConsumeTimeSpanBody origin = new QueryConsumeTimeSpanBody(); + List queueTimeSpans = newUniqueConsumeTimeSpanSet(); + origin.setConsumeTimeSpanSet(queueTimeSpans); + String json = origin.toJson(true); + QueryConsumeTimeSpanBody fromJson = RemotingSerializable.fromJson(json, QueryConsumeTimeSpanBody.class); + assertThat(fromJson.getConsumeTimeSpanSet().get(0).getMinTimeStamp()).isEqualTo(origin.getConsumeTimeSpanSet().get(0).getMinTimeStamp()); + assertThat(fromJson.getConsumeTimeSpanSet().get(0).getMaxTimeStamp()).isEqualTo(origin.getConsumeTimeSpanSet().get(0).getMaxTimeStamp()); + assertThat(fromJson.getConsumeTimeSpanSet().get(0).getConsumeTimeStamp()).isEqualTo(origin.getConsumeTimeSpanSet().get(0).getConsumeTimeStamp()); + assertThat(fromJson.getConsumeTimeSpanSet().get(0).getDelayTime()).isEqualTo(origin.getConsumeTimeSpanSet().get(0).getDelayTime()); + assertThat(fromJson.getConsumeTimeSpanSet().get(0).getMessageQueue().getBrokerName()).isEqualTo(origin.getConsumeTimeSpanSet().get(0).getMessageQueue().getBrokerName()); + assertThat(fromJson.getConsumeTimeSpanSet().get(0).getMessageQueue().getTopic()).isEqualTo(origin.getConsumeTimeSpanSet().get(0).getMessageQueue().getTopic()); + assertThat(fromJson.getConsumeTimeSpanSet().get(0).getMessageQueue().getQueueId()).isEqualTo(origin.getConsumeTimeSpanSet().get(0).getMessageQueue().getQueueId()); + } + + @Test + public void testEncode() throws Exception { + QueryConsumeTimeSpanBody origin = new QueryConsumeTimeSpanBody(); + List queueTimeSpans = newUniqueConsumeTimeSpanSet(); + origin.setConsumeTimeSpanSet(queueTimeSpans); + byte[] data = origin.encode(); + QueryConsumeTimeSpanBody fromData = RemotingSerializable.decode(data, QueryConsumeTimeSpanBody.class); + assertThat(fromData.getConsumeTimeSpanSet().get(0).getMinTimeStamp()).isEqualTo(origin.getConsumeTimeSpanSet().get(0).getMinTimeStamp()); + assertThat(fromData.getConsumeTimeSpanSet().get(0).getMaxTimeStamp()).isEqualTo(origin.getConsumeTimeSpanSet().get(0).getMaxTimeStamp()); + assertThat(fromData.getConsumeTimeSpanSet().get(0).getConsumeTimeStamp()).isEqualTo(origin.getConsumeTimeSpanSet().get(0).getConsumeTimeStamp()); + assertThat(fromData.getConsumeTimeSpanSet().get(0).getDelayTime()).isEqualTo(origin.getConsumeTimeSpanSet().get(0).getDelayTime()); + assertThat(fromData.getConsumeTimeSpanSet().get(0).getMessageQueue().getBrokerName()).isEqualTo(origin.getConsumeTimeSpanSet().get(0).getMessageQueue().getBrokerName()); + assertThat(fromData.getConsumeTimeSpanSet().get(0).getMessageQueue().getTopic()).isEqualTo(origin.getConsumeTimeSpanSet().get(0).getMessageQueue().getTopic()); + assertThat(fromData.getConsumeTimeSpanSet().get(0).getMessageQueue().getQueueId()).isEqualTo(origin.getConsumeTimeSpanSet().get(0).getMessageQueue().getQueueId()); + } + + private List newUniqueConsumeTimeSpanSet() { + List queueTimeSpans = new ArrayList<>(); + QueueTimeSpan queueTimeSpan = new QueueTimeSpan(); + queueTimeSpan.setMinTimeStamp(System.currentTimeMillis()); + queueTimeSpan.setMaxTimeStamp(UtilAll.computeNextHourTimeMillis()); + queueTimeSpan.setConsumeTimeStamp(UtilAll.computeNextMinutesTimeMillis()); + queueTimeSpan.setDelayTime(5000L); + MessageQueue messageQueue = new MessageQueue(UUID.randomUUID().toString(), UUID.randomUUID().toString(), new Random().nextInt()); + queueTimeSpan.setMessageQueue(messageQueue); + queueTimeSpans.add(queueTimeSpan); + return queueTimeSpans; + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/RegisterBrokerBodyTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/RegisterBrokerBodyTest.java new file mode 100644 index 00000000000..e63552f9f75 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/RegisterBrokerBodyTest.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol; + +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.rocketmq.common.MQVersion; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.remoting.protocol.body.RegisterBrokerBody; +import org.apache.rocketmq.remoting.protocol.body.TopicConfigAndMappingSerializeWrapper; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class RegisterBrokerBodyTest { + @Test + public void test_encode_decode() throws IOException { + RegisterBrokerBody registerBrokerBody = new RegisterBrokerBody(); + TopicConfigAndMappingSerializeWrapper topicConfigSerializeWrapper = new TopicConfigAndMappingSerializeWrapper(); + registerBrokerBody.setTopicConfigSerializeWrapper(topicConfigSerializeWrapper); + + ConcurrentMap topicConfigTable = new ConcurrentHashMap<>(); + for (int i = 0; i < 10000; i++) { + topicConfigTable.put(String.valueOf(i), new TopicConfig(String.valueOf(i))); + } + + topicConfigSerializeWrapper.setTopicConfigTable(topicConfigTable); + + byte[] compareEncode = registerBrokerBody.encode(true); + byte[] encode2 = registerBrokerBody.encode(false); + RegisterBrokerBody decodeRegisterBrokerBody = RegisterBrokerBody.decode(compareEncode, true, MQVersion.Version.V5_0_0); + + assertEquals(registerBrokerBody.getTopicConfigSerializeWrapper().getTopicConfigTable().size(), decodeRegisterBrokerBody.getTopicConfigSerializeWrapper().getTopicConfigTable().size()); + + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/RemotingCommandTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/RemotingCommandTest.java index 2bd41cec839..b5a0d003ebc 100644 --- a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/RemotingCommandTest.java +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/RemotingCommandTest.java @@ -19,9 +19,12 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.nio.ByteBuffer; +import java.util.HashSet; +import java.util.Set; import org.apache.rocketmq.remoting.CommandCustomHeader; import org.apache.rocketmq.remoting.annotation.CFNotNull; import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.junit.Assert; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -31,7 +34,13 @@ public class RemotingCommandTest { public void testMarkProtocolType_JSONProtocolType() { int source = 261; SerializeType type = SerializeType.JSON; - byte[] result = RemotingCommand.markProtocolType(source, type); + + byte[] result = new byte[4]; + int x = RemotingCommand.markProtocolType(source, type); + result[0] = (byte) (x >> 24); + result[1] = (byte) (x >> 16); + result[2] = (byte) (x >> 8); + result[3] = (byte) x; assertThat(result).isEqualTo(new byte[] {0, 0, 1, 5}); } @@ -39,7 +48,12 @@ public void testMarkProtocolType_JSONProtocolType() { public void testMarkProtocolType_ROCKETMQProtocolType() { int source = 16777215; SerializeType type = SerializeType.ROCKETMQ; - byte[] result = RemotingCommand.markProtocolType(source, type); + byte[] result = new byte[4]; + int x = RemotingCommand.markProtocolType(source, type); + result[0] = (byte) (x >> 24); + result[1] = (byte) (x >> 16); + result[2] = (byte) (x >> 8); + result[3] = (byte) x; assertThat(result).isEqualTo(new byte[] {1, -1, -1, -1}); } @@ -47,7 +61,7 @@ public void testMarkProtocolType_ROCKETMQProtocolType() { public void testCreateRequestCommand_RegisterBroker() { System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, "2333"); - int code = 103; //org.apache.rocketmq.common.protocol.RequestCode.REGISTER_BROKER + int code = 103; //org.apache.rocketmq.remoting.protocol.RequestCode.REGISTER_BROKER CommandCustomHeader header = new SampleCommandCustomHeader(); RemotingCommand cmd = RemotingCommand.createRequestCommand(code, header); assertThat(cmd.getCode()).isEqualTo(code); @@ -106,7 +120,7 @@ public void testCreateResponseCommand_SystemError() { public void testEncodeAndDecode_EmptyBody() { System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, "2333"); - int code = 103; //org.apache.rocketmq.common.protocol.RequestCode.REGISTER_BROKER + int code = 103; //org.apache.rocketmq.remoting.protocol.RequestCode.REGISTER_BROKER CommandCustomHeader header = new SampleCommandCustomHeader(); RemotingCommand cmd = RemotingCommand.createRequestCommand(code, header); @@ -118,17 +132,24 @@ public void testEncodeAndDecode_EmptyBody() { buffer.get(bytes, 0, buffer.limit() - 4); buffer = ByteBuffer.wrap(bytes); - RemotingCommand decodedCommand = RemotingCommand.decode(buffer); + RemotingCommand decodedCommand = null; + try { + decodedCommand = RemotingCommand.decode(buffer); + + assertThat(decodedCommand.getSerializeTypeCurrentRPC()).isEqualTo(SerializeType.JSON); + assertThat(decodedCommand.getBody()).isNull(); + } catch (RemotingCommandException e) { + e.printStackTrace(); + Assert.fail("Should not throw IOException"); + } - assertThat(decodedCommand.getSerializeTypeCurrentRPC()).isEqualTo(SerializeType.JSON); - assertThat(decodedCommand.getBody()).isNull(); } @Test public void testEncodeAndDecode_FilledBody() { System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, "2333"); - int code = 103; //org.apache.rocketmq.common.protocol.RequestCode.REGISTER_BROKER + int code = 103; //org.apache.rocketmq.remoting.protocol.RequestCode.REGISTER_BROKER CommandCustomHeader header = new SampleCommandCustomHeader(); RemotingCommand cmd = RemotingCommand.createRequestCommand(code, header); cmd.setBody(new byte[] {0, 1, 2, 3, 4}); @@ -141,17 +162,23 @@ public void testEncodeAndDecode_FilledBody() { buffer.get(bytes, 0, buffer.limit() - 4); buffer = ByteBuffer.wrap(bytes); - RemotingCommand decodedCommand = RemotingCommand.decode(buffer); + RemotingCommand decodedCommand = null; + try { + decodedCommand = RemotingCommand.decode(buffer); - assertThat(decodedCommand.getSerializeTypeCurrentRPC()).isEqualTo(SerializeType.JSON); - assertThat(decodedCommand.getBody()).isEqualTo(new byte[] {0, 1, 2, 3, 4}); + assertThat(decodedCommand.getSerializeTypeCurrentRPC()).isEqualTo(SerializeType.JSON); + assertThat(decodedCommand.getBody()).isEqualTo(new byte[] {0, 1, 2, 3, 4}); + } catch (RemotingCommandException e) { + e.printStackTrace(); + Assert.fail("Should not throw IOException"); + } } @Test public void testEncodeAndDecode_FilledBodyWithExtFields() throws RemotingCommandException { System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, "2333"); - int code = 103; //org.apache.rocketmq.common.protocol.RequestCode.REGISTER_BROKER + int code = 103; //org.apache.rocketmq.remoting.protocol.RequestCode.REGISTER_BROKER CommandCustomHeader header = new ExtFieldsHeader(); RemotingCommand cmd = RemotingCommand.createRequestCommand(code, header); @@ -165,22 +192,30 @@ public void testEncodeAndDecode_FilledBodyWithExtFields() throws RemotingCommand buffer.get(bytes, 0, buffer.limit() - 4); buffer = ByteBuffer.wrap(bytes); - RemotingCommand decodedCommand = RemotingCommand.decode(buffer); + RemotingCommand decodedCommand = null; + try { + decodedCommand = RemotingCommand.decode(buffer); + + assertThat(decodedCommand.getExtFields().get("stringValue")).isEqualTo("bilibili"); + assertThat(decodedCommand.getExtFields().get("intValue")).isEqualTo("2333"); + assertThat(decodedCommand.getExtFields().get("longValue")).isEqualTo("23333333"); + assertThat(decodedCommand.getExtFields().get("booleanValue")).isEqualTo("true"); + assertThat(decodedCommand.getExtFields().get("doubleValue")).isEqualTo("0.618"); - assertThat(decodedCommand.getExtFields().get("stringValue")).isEqualTo("bilibili"); - assertThat(decodedCommand.getExtFields().get("intValue")).isEqualTo("2333"); - assertThat(decodedCommand.getExtFields().get("longValue")).isEqualTo("23333333"); - assertThat(decodedCommand.getExtFields().get("booleanValue")).isEqualTo("true"); - assertThat(decodedCommand.getExtFields().get("doubleValue")).isEqualTo("0.618"); + assertThat(decodedCommand.getExtFields().get("key")).isEqualTo("value"); - assertThat(decodedCommand.getExtFields().get("key")).isEqualTo("value"); + CommandCustomHeader decodedHeader = decodedCommand.decodeCommandCustomHeader(ExtFieldsHeader.class); + assertThat(((ExtFieldsHeader) decodedHeader).getStringValue()).isEqualTo("bilibili"); + assertThat(((ExtFieldsHeader) decodedHeader).getIntValue()).isEqualTo(2333); + assertThat(((ExtFieldsHeader) decodedHeader).getLongValue()).isEqualTo(23333333L); + assertThat(((ExtFieldsHeader) decodedHeader).isBooleanValue()).isEqualTo(true); + assertThat(((ExtFieldsHeader) decodedHeader).getDoubleValue()).isBetween(0.617, 0.619); + } catch (RemotingCommandException e) { + e.printStackTrace(); + + Assert.fail("Should not throw IOException"); + } - CommandCustomHeader decodedHeader = decodedCommand.decodeCommandCustomHeader(ExtFieldsHeader.class); - assertThat(((ExtFieldsHeader) decodedHeader).getStringValue()).isEqualTo("bilibili"); - assertThat(((ExtFieldsHeader) decodedHeader).getIntValue()).isEqualTo(2333); - assertThat(((ExtFieldsHeader) decodedHeader).getLongValue()).isEqualTo(23333333l); - assertThat(((ExtFieldsHeader) decodedHeader).isBooleanValue()).isEqualTo(true); - assertThat(((ExtFieldsHeader) decodedHeader).getDoubleValue()).isBetween(0.617, 0.619); } @Test @@ -198,6 +233,32 @@ public void testNotNullField() throws Exception { Field value = FieldTestClass.class.getDeclaredField("value"); assertThat(method.invoke(remotingCommand, value)).isEqualTo(false); } + + @Test + public void testParentField() throws Exception { + SubExtFieldsHeader subExtFieldsHeader = new SubExtFieldsHeader(); + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(1, subExtFieldsHeader); + Field[] fields = remotingCommand.getClazzFields(subExtFieldsHeader.getClass()); + Set fieldNames = new HashSet<>(); + for (Field field: fields) { + fieldNames.add(field.getName()); + } + Assert.assertTrue(fields.length >= 7); + Set names = new HashSet<>(); + names.add("stringValue"); + names.add("intValue"); + names.add("longValue"); + names.add("booleanValue"); + names.add("doubleValue"); + names.add("name"); + names.add("value"); + for (String name: names) { + Assert.assertTrue(fieldNames.contains(name)); + } + remotingCommand.makeCustomHeaderToNet(); + SubExtFieldsHeader other = (SubExtFieldsHeader) remotingCommand.decodeCommandCustomHeader(subExtFieldsHeader.getClass()); + Assert.assertEquals(other, subExtFieldsHeader); + } } class FieldTestClass { @@ -219,7 +280,7 @@ public void checkFields() throws RemotingCommandException { class ExtFieldsHeader implements CommandCustomHeader { private String stringValue = "bilibili"; private int intValue = 2333; - private long longValue = 23333333l; + private long longValue = 23333333L; private boolean booleanValue = true; private double doubleValue = 0.618; @@ -246,4 +307,72 @@ public boolean isBooleanValue() { public double getDoubleValue() { return doubleValue; } -} \ No newline at end of file + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ExtFieldsHeader)) return false; + + ExtFieldsHeader that = (ExtFieldsHeader) o; + + if (intValue != that.intValue) return false; + if (longValue != that.longValue) return false; + if (booleanValue != that.booleanValue) return false; + if (Double.compare(that.doubleValue, doubleValue) != 0) return false; + return stringValue != null ? stringValue.equals(that.stringValue) : that.stringValue == null; + } + + @Override + public int hashCode() { + int result; + long temp; + result = stringValue != null ? stringValue.hashCode() : 0; + result = 31 * result + intValue; + result = 31 * result + (int) (longValue ^ (longValue >>> 32)); + result = 31 * result + (booleanValue ? 1 : 0); + temp = Double.doubleToLongBits(doubleValue); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } +} + + +class SubExtFieldsHeader extends ExtFieldsHeader { + private String name = "12321"; + private int value = 111; + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SubExtFieldsHeader)) return false; + if (!super.equals(o)) return false; + + SubExtFieldsHeader that = (SubExtFieldsHeader) o; + + if (value != that.value) return false; + return name != null ? name.equals(that.name) : that.name == null; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (name != null ? name.hashCode() : 0); + result = 31 * result + value; + return result; + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/RemotingSerializableTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/RemotingSerializableTest.java index 3e8b7a90ae6..6bd80217da0 100644 --- a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/RemotingSerializableTest.java +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/RemotingSerializableTest.java @@ -16,9 +16,19 @@ */ package org.apache.rocketmq.remoting.protocol; +import com.alibaba.fastjson.serializer.SerializerFeature; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.TypeAdapter; +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.nio.charset.Charset; import java.util.Arrays; +import java.util.HashMap; import java.util.List; -import org.junit.Test; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; @@ -80,6 +90,38 @@ public void setStringList(List stringList) { "}"); } + @Test + public void testEncode() { + class Foo extends RemotingSerializable { + Map map = new HashMap<>(); + + Foo() { + map.put(0L, "Test"); + } + + public Map getMap() { + return map; + } + } + Foo foo = new Foo(); + String invalid = new String(foo.encode(), Charset.defaultCharset()); + String valid = new String(foo.encode(SerializerFeature.BrowserCompatible, SerializerFeature.QuoteFieldNames, + SerializerFeature.MapSortField), Charset.defaultCharset()); + + Gson gson = new Gson(); + final TypeAdapter strictAdapter = gson.getAdapter(JsonElement.class); + try { + strictAdapter.fromJson(invalid); + Assert.fail("Should have thrown"); + } catch (IOException ignore) { + } + + try { + strictAdapter.fromJson(valid); + } catch (IOException ignore) { + Assert.fail("Should not throw"); + } + } } class Sample { diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/RequestSourceTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/RequestSourceTest.java new file mode 100644 index 00000000000..b2ed1e3419c --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/RequestSourceTest.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol; + +import junit.framework.TestCase; + +public class RequestSourceTest extends TestCase { + + public void testIsValid() { + assertEquals(4, RequestSource.values().length); + + assertTrue(RequestSource.isValid(-1)); + assertTrue(RequestSource.isValid(0)); + assertTrue(RequestSource.isValid(1)); + assertTrue(RequestSource.isValid(2)); + + assertFalse(RequestSource.isValid(-2)); + assertFalse(RequestSource.isValid(3)); + } + + public void testParseInteger() { + assertEquals(RequestSource.SDK, RequestSource.parseInteger(-1)); + assertEquals(RequestSource.PROXY_FOR_ORDER, RequestSource.parseInteger(0)); + assertEquals(RequestSource.PROXY_FOR_BROADCAST, RequestSource.parseInteger(1)); + assertEquals(RequestSource.PROXY_FOR_STREAM, RequestSource.parseInteger(2)); + + assertEquals(RequestSource.SDK, RequestSource.parseInteger(-10)); + assertEquals(RequestSource.SDK, RequestSource.parseInteger(10)); + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/RequestTypeTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/RequestTypeTest.java new file mode 100644 index 00000000000..7457926d721 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/RequestTypeTest.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RequestTypeTest { + @Test + public void testValueOf() { + RequestType requestType = RequestType.valueOf(RequestType.STREAM.getCode()); + assertThat(requestType).isEqualTo(RequestType.STREAM); + + requestType = RequestType.valueOf((byte) 1); + assertThat(requestType).isNull(); + } +} \ No newline at end of file diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/RocketMQSerializableTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/RocketMQSerializableTest.java index f1db54fa303..7cf32d70c34 100644 --- a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/RocketMQSerializableTest.java +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/RocketMQSerializableTest.java @@ -16,7 +16,13 @@ */ package org.apache.rocketmq.remoting.protocol; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; import java.util.HashMap; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.junit.Assert; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -26,7 +32,7 @@ public class RocketMQSerializableTest { public void testRocketMQProtocolEncodeAndDecode_WithoutRemarkWithoutExtFields() { System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, "2333"); - //org.apache.rocketmq.common.protocol.RequestCode.REGISTER_BROKER + //org.apache.rocketmq.remoting.protocol.RequestCode.REGISTER_BROKER int code = 103; RemotingCommand cmd = RemotingCommand.createRequestCommand(code, new SampleCommandCustomHeader()); cmd.setSerializeTypeCurrentRPC(SerializeType.ROCKETMQ); @@ -42,22 +48,29 @@ public void testRocketMQProtocolEncodeAndDecode_WithoutRemarkWithoutExtFields() assertThat(parseToInt(result, 13)).isEqualTo(0); //empty remark assertThat(parseToInt(result, 17)).isEqualTo(0); //empty extFields - RemotingCommand decodedCommand = RocketMQSerializable.rocketMQProtocolDecode(result); - - assertThat(decodedCommand.getCode()).isEqualTo(code); - assertThat(decodedCommand.getLanguage()).isEqualTo(LanguageCode.JAVA); - assertThat(decodedCommand.getVersion()).isEqualTo(2333); - assertThat(decodedCommand.getOpaque()).isEqualTo(opaque); - assertThat(decodedCommand.getFlag()).isEqualTo(0); - assertThat(decodedCommand.getRemark()).isNull(); - assertThat(decodedCommand.getExtFields()).isNull(); + RemotingCommand decodedCommand = null; + try { + decodedCommand = RocketMQSerializable.rocketMQProtocolDecode(Unpooled.wrappedBuffer(result), result.length); + + assertThat(decodedCommand.getCode()).isEqualTo(code); + assertThat(decodedCommand.getLanguage()).isEqualTo(LanguageCode.JAVA); + assertThat(decodedCommand.getVersion()).isEqualTo(2333); + assertThat(decodedCommand.getOpaque()).isEqualTo(opaque); + assertThat(decodedCommand.getFlag()).isEqualTo(0); + assertThat(decodedCommand.getRemark()).isNull(); + assertThat(decodedCommand.getExtFields()).isNull(); + } catch (RemotingCommandException e) { + e.printStackTrace(); + + Assert.fail("Should not throw IOException"); + } } @Test public void testRocketMQProtocolEncodeAndDecode_WithRemarkWithoutExtFields() { System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, "2333"); - //org.apache.rocketmq.common.protocol.RequestCode.REGISTER_BROKER + //org.apache.rocketmq.remoting.protocol.RequestCode.REGISTER_BROKER int code = 103; RemotingCommand cmd = RemotingCommand.createRequestCommand(code, new SampleCommandCustomHeader()); @@ -80,22 +93,28 @@ public void testRocketMQProtocolEncodeAndDecode_WithRemarkWithoutExtFields() { assertThat(parseToInt(result, 30)).isEqualTo(0); //empty extFields - RemotingCommand decodedCommand = RocketMQSerializable.rocketMQProtocolDecode(result); - - assertThat(decodedCommand.getCode()).isEqualTo(code); - assertThat(decodedCommand.getLanguage()).isEqualTo(LanguageCode.JAVA); - assertThat(decodedCommand.getVersion()).isEqualTo(2333); - assertThat(decodedCommand.getOpaque()).isEqualTo(opaque); - assertThat(decodedCommand.getFlag()).isEqualTo(0); - assertThat(decodedCommand.getRemark()).contains("Sample Remark"); - assertThat(decodedCommand.getExtFields()).isNull(); + try { + RemotingCommand decodedCommand = RocketMQSerializable.rocketMQProtocolDecode(Unpooled.wrappedBuffer(result), result.length); + + assertThat(decodedCommand.getCode()).isEqualTo(code); + assertThat(decodedCommand.getLanguage()).isEqualTo(LanguageCode.JAVA); + assertThat(decodedCommand.getVersion()).isEqualTo(2333); + assertThat(decodedCommand.getOpaque()).isEqualTo(opaque); + assertThat(decodedCommand.getFlag()).isEqualTo(0); + assertThat(decodedCommand.getRemark()).contains("Sample Remark"); + assertThat(decodedCommand.getExtFields()).isNull(); + } catch (RemotingCommandException e) { + e.printStackTrace(); + + Assert.fail("Should not throw IOException"); + } } @Test - public void testRocketMQProtocolEncodeAndDecode_WithoutRemarkWithExtFields() { + public void testRocketMQProtocolEncodeAndDecode_WithoutRemarkWithExtFields() throws Exception { System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, "2333"); - //org.apache.rocketmq.common.protocol.RequestCode.REGISTER_BROKER + //org.apache.rocketmq.remoting.protocol.RequestCode.REGISTER_BROKER int code = 103; RemotingCommand cmd = RemotingCommand.createRequestCommand(code, new SampleCommandCustomHeader()); @@ -115,31 +134,24 @@ public void testRocketMQProtocolEncodeAndDecode_WithoutRemarkWithExtFields() { byte[] extFieldsArray = new byte[14]; System.arraycopy(result, 21, extFieldsArray, 0, 14); - HashMap extFields = RocketMQSerializable.mapDeserialize(extFieldsArray); + HashMap extFields = + RocketMQSerializable.mapDeserialize(Unpooled.wrappedBuffer(extFieldsArray), extFieldsArray.length); assertThat(extFields).contains(new HashMap.SimpleEntry("key", "value")); - RemotingCommand decodedCommand = RocketMQSerializable.rocketMQProtocolDecode(result); - - assertThat(decodedCommand.getCode()).isEqualTo(code); - assertThat(decodedCommand.getLanguage()).isEqualTo(LanguageCode.JAVA); - assertThat(decodedCommand.getVersion()).isEqualTo(2333); - assertThat(decodedCommand.getOpaque()).isEqualTo(opaque); - assertThat(decodedCommand.getFlag()).isEqualTo(0); - assertThat(decodedCommand.getRemark()).isNull(); - assertThat(decodedCommand.getExtFields()).contains(new HashMap.SimpleEntry("key", "value")); - } - - @Test - public void testIsBlank_NotBlank() { - assertThat(RocketMQSerializable.isBlank("bar")).isFalse(); - assertThat(RocketMQSerializable.isBlank(" A ")).isFalse(); - } - - @Test - public void testIsBlank_Blank() { - assertThat(RocketMQSerializable.isBlank(null)).isTrue(); - assertThat(RocketMQSerializable.isBlank("")).isTrue(); - assertThat(RocketMQSerializable.isBlank(" ")).isTrue(); + try { + RemotingCommand decodedCommand = RocketMQSerializable.rocketMQProtocolDecode(Unpooled.wrappedBuffer(result), result.length); + assertThat(decodedCommand.getCode()).isEqualTo(code); + assertThat(decodedCommand.getLanguage()).isEqualTo(LanguageCode.JAVA); + assertThat(decodedCommand.getVersion()).isEqualTo(2333); + assertThat(decodedCommand.getOpaque()).isEqualTo(opaque); + assertThat(decodedCommand.getFlag()).isEqualTo(0); + assertThat(decodedCommand.getRemark()).isNull(); + assertThat(decodedCommand.getExtFields()).contains(new HashMap.SimpleEntry("key", "value")); + } catch (RemotingCommandException e) { + e.printStackTrace(); + + Assert.fail("Should not throw IOException"); + } } private short parseToShort(byte[] array, int index) { @@ -150,4 +162,56 @@ private int parseToInt(byte[] array, int index) { return array[index] * 16777216 + array[++index] * 65536 + array[++index] * 256 + array[++index]; } -} \ No newline at end of file + + public static class MyHeader1 implements CommandCustomHeader { + private String str; + private int num; + + @Override + public void checkFields() throws RemotingCommandException { + } + + public String getStr() { + return str; + } + + public void setStr(String str) { + this.str = str; + } + + public int getNum() { + return num; + } + + public void setNum(int num) { + this.num = num; + } + } + + @Test + public void testFastEncode() throws Exception { + ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(16); + MyHeader1 header1 = new MyHeader1(); + header1.setStr("s1"); + header1.setNum(100); + RemotingCommand cmd = RemotingCommand.createRequestCommand(1, header1); + cmd.setRemark("remark"); + cmd.setOpaque(1001); + cmd.setVersion(99); + cmd.setLanguage(LanguageCode.JAVA); + cmd.setFlag(3); + cmd.makeCustomHeaderToNet(); + RocketMQSerializable.rocketMQProtocolEncode(cmd, buf); + RemotingCommand cmd2 = RocketMQSerializable.rocketMQProtocolDecode(buf, buf.readableBytes()); + assertThat(cmd2.getRemark()).isEqualTo("remark"); + assertThat(cmd2.getCode()).isEqualTo(1); + assertThat(cmd2.getOpaque()).isEqualTo(1001); + assertThat(cmd2.getVersion()).isEqualTo(99); + assertThat(cmd2.getLanguage()).isEqualTo(LanguageCode.JAVA); + assertThat(cmd2.getFlag()).isEqualTo(3); + + MyHeader1 h2 = (MyHeader1) cmd2.decodeCommandCustomHeader(MyHeader1.class); + assertThat(h2.getStr()).isEqualTo("s1"); + assertThat(h2.getNum()).isEqualTo(100); + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/admin/ConsumeStatsTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/admin/ConsumeStatsTest.java new file mode 100644 index 00000000000..10021c40805 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/admin/ConsumeStatsTest.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.admin; + +import org.apache.rocketmq.common.message.MessageQueue; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +public class ConsumeStatsTest { + + @Test + public void testComputeTotalDiff() { + ConsumeStats stats = new ConsumeStats(); + MessageQueue messageQueue = Mockito.mock(MessageQueue.class); + OffsetWrapper offsetWrapper = Mockito.mock(OffsetWrapper.class); + Mockito.when(offsetWrapper.getConsumerOffset()).thenReturn(1L); + Mockito.when(offsetWrapper.getBrokerOffset()).thenReturn(2L); + stats.getOffsetTable().put(messageQueue, offsetWrapper); + + MessageQueue messageQueue2 = Mockito.mock(MessageQueue.class); + OffsetWrapper offsetWrapper2 = Mockito.mock(OffsetWrapper.class); + Mockito.when(offsetWrapper2.getConsumerOffset()).thenReturn(2L); + Mockito.when(offsetWrapper2.getBrokerOffset()).thenReturn(3L); + stats.getOffsetTable().put(messageQueue2, offsetWrapper2); + Assert.assertEquals(2L, stats.computeTotalDiff()); + } + + @Test + public void testComputeInflightTotalDiff() { + ConsumeStats stats = new ConsumeStats(); + MessageQueue messageQueue = Mockito.mock(MessageQueue.class); + OffsetWrapper offsetWrapper = Mockito.mock(OffsetWrapper.class); + Mockito.when(offsetWrapper.getBrokerOffset()).thenReturn(3L); + Mockito.when(offsetWrapper.getPullOffset()).thenReturn(2L); + stats.getOffsetTable().put(messageQueue, offsetWrapper); + + MessageQueue messageQueue2 = Mockito.mock(MessageQueue.class); + OffsetWrapper offsetWrapper2 = Mockito.mock(OffsetWrapper.class); + Mockito.when(offsetWrapper.getBrokerOffset()).thenReturn(3L); + Mockito.when(offsetWrapper.getPullOffset()).thenReturn(2L); + stats.getOffsetTable().put(messageQueue2, offsetWrapper2); + Assert.assertEquals(2L, stats.computeInflightTotalDiff()); + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/admin/TopicStatsTableTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/admin/TopicStatsTableTest.java new file mode 100644 index 00000000000..bb828dbf8bd --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/admin/TopicStatsTableTest.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.admin; + +import java.util.HashMap; +import java.util.Map; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + + +public class TopicStatsTableTest { + + private volatile TopicStatsTable topicStatsTable; + + private static final String TEST_TOPIC = "test_topic"; + + private static final String TEST_BROKER = "test_broker"; + + private static final int QUEUE_ID = 1; + + private static final long CURRENT_TIME_MILLIS = System.currentTimeMillis(); + + private static final long MAX_OFFSET = CURRENT_TIME_MILLIS + 100; + + private static final long MIN_OFFSET = CURRENT_TIME_MILLIS - 100; + + @Before + public void buildTopicStatsTable() { + HashMap offsetTableMap = new HashMap<>(); + + MessageQueue messageQueue = new MessageQueue(TEST_TOPIC, TEST_BROKER, QUEUE_ID); + + TopicOffset topicOffset = new TopicOffset(); + topicOffset.setLastUpdateTimestamp(CURRENT_TIME_MILLIS); + topicOffset.setMinOffset(MIN_OFFSET); + topicOffset.setMaxOffset(MAX_OFFSET); + + offsetTableMap.put(messageQueue, topicOffset); + + topicStatsTable = new TopicStatsTable(); + topicStatsTable.setOffsetTable(offsetTableMap); + } + + @Test + public void testGetOffsetTable() throws Exception { + validateTopicStatsTable(topicStatsTable); + } + + @Test + public void testFromJson() throws Exception { + String json = RemotingSerializable.toJson(topicStatsTable, true); + TopicStatsTable fromJson = RemotingSerializable.fromJson(json, TopicStatsTable.class); + + validateTopicStatsTable(fromJson); + } + + private static void validateTopicStatsTable(TopicStatsTable topicStatsTable) throws Exception { + Map.Entry savedTopicStatsTableMap = topicStatsTable.getOffsetTable().entrySet().iterator().next(); + MessageQueue savedMessageQueue = savedTopicStatsTableMap.getKey(); + TopicOffset savedTopicOffset = savedTopicStatsTableMap.getValue(); + + Assert.assertTrue(savedMessageQueue.getTopic().equals(TEST_TOPIC)); + Assert.assertTrue(savedMessageQueue.getBrokerName().equals(TEST_BROKER)); + Assert.assertTrue(savedMessageQueue.getQueueId() == QUEUE_ID); + + Assert.assertTrue(savedTopicOffset.getLastUpdateTimestamp() == CURRENT_TIME_MILLIS); + Assert.assertTrue(savedTopicOffset.getMaxOffset() == MAX_OFFSET); + Assert.assertTrue(savedTopicOffset.getMinOffset() == MIN_OFFSET); + } + +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/BatchAckTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/BatchAckTest.java new file mode 100644 index 00000000000..427a132d646 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/BatchAckTest.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.body; + +import com.alibaba.fastjson.JSON; +import org.apache.rocketmq.common.MixAll; +import org.junit.Test; + +import java.util.Arrays; +import java.util.BitSet; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BatchAckTest { + private static String topic = "myTopic"; + private static String cid = MixAll.DEFAULT_CONSUMER_GROUP; + private static long startOffset = 100; + private static int qId = 1; + private static int rqId = 2; + private static long popTime = System.currentTimeMillis(); + private static long invisibleTime = 5000; + + @Test + public void testBatchAckSerializerDeserializer() { + List ackOffsetList = Arrays.asList(startOffset + 1, startOffset + 3, startOffset + 5); + BatchAck batchAck = new BatchAck(); + batchAck.setConsumerGroup(cid); + batchAck.setTopic(topic); + batchAck.setRetry("0"); + batchAck.setStartOffset(startOffset); + batchAck.setQueueId(qId); + batchAck.setReviveQueueId(rqId); + batchAck.setPopTime(popTime); + batchAck.setInvisibleTime(invisibleTime); + batchAck.setBitSet(new BitSet()); + for (Long offset : ackOffsetList) { + batchAck.getBitSet().set((int) (offset - startOffset)); + } + String jsonStr = JSON.toJSONString(batchAck); + + BatchAck bAck = JSON.parseObject(jsonStr, BatchAck.class); + assertThat(bAck.getConsumerGroup()).isEqualTo(cid); + assertThat(bAck.getTopic()).isEqualTo(topic); + assertThat(bAck.getStartOffset()).isEqualTo(startOffset); + assertThat(bAck.getQueueId()).isEqualTo(qId); + assertThat(bAck.getReviveQueueId()).isEqualTo(rqId); + assertThat(bAck.getPopTime()).isEqualTo(popTime); + assertThat(bAck.getInvisibleTime()).isEqualTo(invisibleTime); + for (int i = 0; i < bAck.getBitSet().length(); i++) { + long ackOffset = startOffset + i; + if (ackOffsetList.contains(ackOffset)) { + assertThat(bAck.getBitSet().get(i)).isTrue(); + } else { + assertThat(bAck.getBitSet().get(i)).isFalse(); + } + } + } + + @Test + public void testWithBatchAckMessageRequestBody() { + List ackOffsetList = Arrays.asList(startOffset + 1, startOffset + 3, startOffset + 5); + BatchAck batchAck = new BatchAck(); + batchAck.setConsumerGroup(cid); + batchAck.setTopic(topic); + batchAck.setRetry("0"); + batchAck.setStartOffset(startOffset); + batchAck.setQueueId(qId); + batchAck.setReviveQueueId(rqId); + batchAck.setPopTime(popTime); + batchAck.setInvisibleTime(invisibleTime); + batchAck.setBitSet(new BitSet()); + for (Long offset : ackOffsetList) { + batchAck.getBitSet().set((int) (offset - startOffset)); + } + + BatchAckMessageRequestBody batchAckMessageRequestBody = new BatchAckMessageRequestBody(); + batchAckMessageRequestBody.setAcks(Arrays.asList(batchAck)); + byte[] bytes = batchAckMessageRequestBody.encode(); + BatchAckMessageRequestBody reqBody = BatchAckMessageRequestBody.decode(bytes, BatchAckMessageRequestBody.class); + BatchAck bAck = reqBody.getAcks().get(0); + assertThat(bAck.getConsumerGroup()).isEqualTo(cid); + assertThat(bAck.getTopic()).isEqualTo(topic); + assertThat(bAck.getStartOffset()).isEqualTo(startOffset); + assertThat(bAck.getQueueId()).isEqualTo(qId); + assertThat(bAck.getReviveQueueId()).isEqualTo(rqId); + assertThat(bAck.getPopTime()).isEqualTo(popTime); + assertThat(bAck.getInvisibleTime()).isEqualTo(invisibleTime); + for (int i = 0; i < bAck.getBitSet().length(); i++) { + long ackOffset = startOffset + i; + if (ackOffsetList.contains(ackOffset)) { + assertThat(bAck.getBitSet().get(i)).isTrue(); + } else { + assertThat(bAck.getBitSet().get(i)).isFalse(); + } + } + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/BrokerStatsDataTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/BrokerStatsDataTest.java new file mode 100644 index 00000000000..cb1faef8681 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/BrokerStatsDataTest.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.within; + +public class BrokerStatsDataTest { + + @Test + public void testFromJson() throws Exception { + BrokerStatsData brokerStatsData = new BrokerStatsData(); + + { + BrokerStatsItem brokerStatsItem = new BrokerStatsItem(); + brokerStatsItem.setAvgpt(10.0); + brokerStatsItem.setSum(100L); + brokerStatsItem.setTps(100.0); + brokerStatsData.setStatsDay(brokerStatsItem); + } + + { + BrokerStatsItem brokerStatsItem = new BrokerStatsItem(); + brokerStatsItem.setAvgpt(10.0); + brokerStatsItem.setSum(100L); + brokerStatsItem.setTps(100.0); + brokerStatsData.setStatsHour(brokerStatsItem); + } + + { + BrokerStatsItem brokerStatsItem = new BrokerStatsItem(); + brokerStatsItem.setAvgpt(10.0); + brokerStatsItem.setSum(100L); + brokerStatsItem.setTps(100.0); + brokerStatsData.setStatsMinute(brokerStatsItem); + } + + String json = RemotingSerializable.toJson(brokerStatsData, true); + BrokerStatsData brokerStatsDataResult = RemotingSerializable.fromJson(json, BrokerStatsData.class); + + assertThat(brokerStatsDataResult.getStatsMinute().getAvgpt()).isCloseTo(brokerStatsData.getStatsMinute().getAvgpt(), within(0.0001)); + assertThat(brokerStatsDataResult.getStatsMinute().getTps()).isCloseTo(brokerStatsData.getStatsMinute().getTps(), within(0.0001)); + assertThat(brokerStatsDataResult.getStatsMinute().getSum()).isEqualTo(brokerStatsData.getStatsMinute().getSum()); + + assertThat(brokerStatsDataResult.getStatsHour().getAvgpt()).isCloseTo(brokerStatsData.getStatsHour().getAvgpt(), within(0.0001)); + assertThat(brokerStatsDataResult.getStatsHour().getTps()).isCloseTo(brokerStatsData.getStatsHour().getTps(), within(0.0001)); + assertThat(brokerStatsDataResult.getStatsHour().getSum()).isEqualTo(brokerStatsData.getStatsHour().getSum()); + + assertThat(brokerStatsDataResult.getStatsDay().getAvgpt()).isCloseTo(brokerStatsData.getStatsDay().getAvgpt(), within(0.0001)); + assertThat(brokerStatsDataResult.getStatsDay().getTps()).isCloseTo(brokerStatsData.getStatsDay().getTps(), within(0.0001)); + assertThat(brokerStatsDataResult.getStatsDay().getSum()).isEqualTo(brokerStatsData.getStatsDay().getSum()); + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/CheckClientRequestBodyTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/CheckClientRequestBodyTest.java new file mode 100644 index 00000000000..402ca58f2b8 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/CheckClientRequestBodyTest.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CheckClientRequestBodyTest { + + @Test + public void testFromJson() { + SubscriptionData subscriptionData = new SubscriptionData(); + String expectedClientId = "defalutId"; + String expectedGroup = "defaultGroup"; + CheckClientRequestBody checkClientRequestBody = new CheckClientRequestBody(); + checkClientRequestBody.setClientId(expectedClientId); + checkClientRequestBody.setGroup(expectedGroup); + checkClientRequestBody.setSubscriptionData(subscriptionData); + String json = RemotingSerializable.toJson(checkClientRequestBody, true); + CheckClientRequestBody fromJson = RemotingSerializable.fromJson(json, CheckClientRequestBody.class); + assertThat(fromJson.getClientId()).isEqualTo(expectedClientId); + assertThat(fromJson.getGroup()).isEqualTo(expectedGroup); + assertThat(fromJson.getSubscriptionData()).isEqualTo(subscriptionData); + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/ConsumeMessageDirectlyResultTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/ConsumeMessageDirectlyResultTest.java new file mode 100644 index 00000000000..5e9d385eaec --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/ConsumeMessageDirectlyResultTest.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + + +public class ConsumeMessageDirectlyResultTest { + @Test + public void testFromJson() throws Exception { + ConsumeMessageDirectlyResult result = new ConsumeMessageDirectlyResult(); + boolean defaultAutoCommit = true; + boolean defaultOrder = false; + long defaultSpentTimeMills = 1234567L; + String defaultRemark = "defaultMark"; + CMResult defaultCMResult = CMResult.CR_COMMIT; + + result.setAutoCommit(defaultAutoCommit); + result.setOrder(defaultOrder); + result.setRemark(defaultRemark); + result.setSpentTimeMills(defaultSpentTimeMills); + result.setConsumeResult(defaultCMResult); + + String json = RemotingSerializable.toJson(result, true); + ConsumeMessageDirectlyResult fromJson = RemotingSerializable.fromJson(json, ConsumeMessageDirectlyResult.class); + assertThat(fromJson).isNotNull(); + + assertThat(fromJson.getRemark()).isEqualTo(defaultRemark); + assertThat(fromJson.getSpentTimeMills()).isEqualTo(defaultSpentTimeMills); + assertThat(fromJson.getConsumeResult()).isEqualTo(defaultCMResult); + assertThat(fromJson.isOrder()).isEqualTo(defaultOrder); + + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/ConsumeStatsListTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/ConsumeStatsListTest.java new file mode 100644 index 00000000000..01a4506bfbf --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/ConsumeStatsListTest.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.admin.ConsumeStats; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ConsumeStatsListTest { + + @Test + public void testFromJson() { + ConsumeStats consumeStats = new ConsumeStats(); + ArrayList consumeStatsListValue = new ArrayList<>(); + consumeStatsListValue.add(consumeStats); + HashMap> map = new HashMap<>(); + map.put("subscriptionGroupName", consumeStatsListValue); + List>> consumeStatsListValue2 = new ArrayList<>(); + consumeStatsListValue2.add(map); + + String brokerAddr = "brokerAddr"; + long totalDiff = 12352L; + ConsumeStatsList consumeStatsList = new ConsumeStatsList(); + consumeStatsList.setBrokerAddr(brokerAddr); + consumeStatsList.setTotalDiff(totalDiff); + consumeStatsList.setConsumeStatsList(consumeStatsListValue2); + + String toJson = RemotingSerializable.toJson(consumeStatsList, true); + ConsumeStatsList fromJson = RemotingSerializable.fromJson(toJson, ConsumeStatsList.class); + + assertThat(fromJson.getBrokerAddr()).isEqualTo(brokerAddr); + assertThat(fromJson.getTotalDiff()).isEqualTo(totalDiff); + + List>> fromJsonConsumeStatsList = fromJson.getConsumeStatsList(); + assertThat(fromJsonConsumeStatsList).isInstanceOf(List.class); + + ConsumeStats fromJsonConsumeStats = fromJsonConsumeStatsList.get(0).get("subscriptionGroupName").get(0); + assertThat(fromJsonConsumeStats).isExactlyInstanceOf(ConsumeStats.class); + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/ConsumerConnectionTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/ConsumerConnectionTest.java new file mode 100644 index 00000000000..e9d2fee1232 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/ConsumerConnectionTest.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.assertj.core.api.Assertions; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ConsumerConnectionTest { + + @Test + public void testFromJson() { + ConsumerConnection consumerConnection = new ConsumerConnection(); + HashSet connections = new HashSet<>(); + Connection conn = new Connection(); + connections.add(conn); + + ConcurrentHashMap subscriptionTable = new ConcurrentHashMap<>(); + SubscriptionData subscriptionData = new SubscriptionData(); + subscriptionTable.put("topicA", subscriptionData); + + ConsumeType consumeType = ConsumeType.CONSUME_ACTIVELY; + MessageModel messageModel = MessageModel.CLUSTERING; + ConsumeFromWhere consumeFromWhere = ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET; + + consumerConnection.setConnectionSet(connections); + consumerConnection.setSubscriptionTable(subscriptionTable); + consumerConnection.setConsumeType(consumeType); + consumerConnection.setMessageModel(messageModel); + consumerConnection.setConsumeFromWhere(consumeFromWhere); + + String json = RemotingSerializable.toJson(consumerConnection, true); + ConsumerConnection fromJson = RemotingSerializable.fromJson(json, ConsumerConnection.class); + assertThat(fromJson.getConsumeType()).isEqualTo(ConsumeType.CONSUME_ACTIVELY); + assertThat(fromJson.getMessageModel()).isEqualTo(MessageModel.CLUSTERING); + + HashSet connectionSet = fromJson.getConnectionSet(); + Assertions.assertThat(connectionSet).isInstanceOf(Set.class); + + SubscriptionData data = fromJson.getSubscriptionTable().get("topicA"); + assertThat(data).isExactlyInstanceOf(SubscriptionData.class); + } + + @Test + public void testComputeMinVersion() { + ConsumerConnection consumerConnection = new ConsumerConnection(); + HashSet connections = new HashSet<>(); + Connection conn1 = new Connection(); + conn1.setVersion(1); + connections.add(conn1); + Connection conn2 = new Connection(); + conn2.setVersion(10); + connections.add(conn2); + consumerConnection.setConnectionSet(connections); + + int version = consumerConnection.computeMinVersion(); + assertThat(version).isEqualTo(1); + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/ConsumerRunningInfoTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/ConsumerRunningInfoTest.java new file mode 100644 index 00000000000..f05de389df3 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/ConsumerRunningInfoTest.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import java.util.Properties; +import java.util.TreeMap; +import java.util.TreeSet; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.junit.Before; +import org.junit.Test; + +import static org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType.CONSUME_ACTIVELY; +import static org.assertj.core.api.Assertions.assertThat; + +public class ConsumerRunningInfoTest { + + private ConsumerRunningInfo consumerRunningInfo; + + private TreeMap criTable; + + private MessageQueue messageQueue; + + @Before + public void init() { + consumerRunningInfo = new ConsumerRunningInfo(); + consumerRunningInfo.setJstack("test"); + + TreeMap mqTable = new TreeMap<>(); + messageQueue = new MessageQueue("topicA","broker", 1); + mqTable.put(messageQueue, new ProcessQueueInfo()); + consumerRunningInfo.setMqTable(mqTable); + + TreeMap statusTable = new TreeMap<>(); + statusTable.put("topicA", new ConsumeStatus()); + consumerRunningInfo.setStatusTable(statusTable); + + TreeSet subscriptionSet = new TreeSet<>(); + subscriptionSet.add(new SubscriptionData()); + consumerRunningInfo.setSubscriptionSet(subscriptionSet); + + Properties properties = new Properties(); + properties.put(ConsumerRunningInfo.PROP_CONSUME_TYPE, CONSUME_ACTIVELY); + properties.put(ConsumerRunningInfo.PROP_CONSUMER_START_TIMESTAMP, System.currentTimeMillis()); + consumerRunningInfo.setProperties(properties); + + criTable = new TreeMap<>(); + criTable.put("client_id", consumerRunningInfo); + } + + @Test + public void testFromJson() { + String toJson = RemotingSerializable.toJson(consumerRunningInfo, true); + ConsumerRunningInfo fromJson = RemotingSerializable.fromJson(toJson, ConsumerRunningInfo.class); + + assertThat(fromJson.getJstack()).isEqualTo("test"); + assertThat(fromJson.getProperties().get(ConsumerRunningInfo.PROP_CONSUME_TYPE)).isEqualTo(ConsumeType.CONSUME_ACTIVELY.name()); + + ConsumeStatus consumeStatus = fromJson.getStatusTable().get("topicA"); + assertThat(consumeStatus).isExactlyInstanceOf(ConsumeStatus.class); + + SubscriptionData subscription = fromJson.getSubscriptionSet().first(); + assertThat(subscription).isExactlyInstanceOf(SubscriptionData.class); + + ProcessQueueInfo processQueueInfo = fromJson.getMqTable().get(messageQueue); + assertThat(processQueueInfo).isExactlyInstanceOf(ProcessQueueInfo.class); + } + + @Test + public void testAnalyzeRebalance() { + boolean result = ConsumerRunningInfo.analyzeRebalance(criTable); + assertThat(result).isTrue(); + } + + @Test + public void testAnalyzeProcessQueue() { + String result = ConsumerRunningInfo.analyzeProcessQueue("client_id", consumerRunningInfo); + assertThat(result).isEmpty(); + + } + + @Test + public void testAnalyzeSubscription() { + boolean result = ConsumerRunningInfo.analyzeSubscription(criTable); + assertThat(result).isTrue(); + } + + +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/KVTableTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/KVTableTest.java new file mode 100644 index 00000000000..61482132e30 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/KVTableTest.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import java.util.HashMap; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class KVTableTest { + + @Test + public void testFromJson() throws Exception { + HashMap table = new HashMap<>(); + table.put("key1", "value1"); + table.put("key2", "value2"); + + KVTable kvTable = new KVTable(); + kvTable.setTable(table); + + String json = RemotingSerializable.toJson(kvTable, true); + KVTable fromJson = RemotingSerializable.fromJson(json, KVTable.class); + + assertThat(fromJson).isNotEqualTo(kvTable); + assertThat(fromJson.getTable().get("key1")).isEqualTo(kvTable.getTable().get("key1")); + assertThat(fromJson.getTable().get("key2")).isEqualTo(kvTable.getTable().get("key2")); + } + +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/MessageRequestModeSerializeWrapperTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/MessageRequestModeSerializeWrapperTest.java new file mode 100644 index 00000000000..6ae3dbd3a0b --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/MessageRequestModeSerializeWrapperTest.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.body; + +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.common.message.MessageRequestMode; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MessageRequestModeSerializeWrapperTest { + + @Test + public void testFromJson() { + MessageRequestModeSerializeWrapper messageRequestModeSerializeWrapper = new MessageRequestModeSerializeWrapper(); + ConcurrentHashMap> + messageRequestModeMap = new ConcurrentHashMap<>(); + String topic = "TopicTest"; + String group = "Consumer"; + MessageRequestMode requestMode = MessageRequestMode.POP; + int popShareQueueNum = -1; + SetMessageRequestModeRequestBody requestBody = new SetMessageRequestModeRequestBody(); + requestBody.setTopic(topic); + requestBody.setConsumerGroup(group); + requestBody.setMode(requestMode); + requestBody.setPopShareQueueNum(popShareQueueNum); + ConcurrentHashMap map = new ConcurrentHashMap<>(); + map.put(group, requestBody); + messageRequestModeMap.put(topic, map); + + messageRequestModeSerializeWrapper.setMessageRequestModeMap(messageRequestModeMap); + + String json = RemotingSerializable.toJson(messageRequestModeSerializeWrapper, true); + MessageRequestModeSerializeWrapper fromJson = RemotingSerializable.fromJson(json, MessageRequestModeSerializeWrapper.class); + assertThat(fromJson.getMessageRequestModeMap()).containsKey(topic); + assertThat(fromJson.getMessageRequestModeMap().get(topic)).containsKey(group); + assertThat(fromJson.getMessageRequestModeMap().get(topic).get(group).getTopic()).isEqualTo(topic); + assertThat(fromJson.getMessageRequestModeMap().get(topic).get(group).getConsumerGroup()).isEqualTo(group); + assertThat(fromJson.getMessageRequestModeMap().get(topic).get(group).getMode()).isEqualTo(requestMode); + assertThat(fromJson.getMessageRequestModeMap().get(topic).get(group).getPopShareQueueNum()).isEqualTo(popShareQueueNum); + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/QueryConsumeQueueResponseBodyTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/QueryConsumeQueueResponseBodyTest.java new file mode 100644 index 00000000000..7e44d710b4e --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/QueryConsumeQueueResponseBodyTest.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import java.util.ArrayList; +import java.util.List; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class QueryConsumeQueueResponseBodyTest { + + @Test + public void test() { + QueryConsumeQueueResponseBody body = new QueryConsumeQueueResponseBody(); + + SubscriptionData subscriptionData = new SubscriptionData(); + ConsumeQueueData data = new ConsumeQueueData(); + data.setBitMap("defaultBitMap"); + data.setEval(false); + data.setMsg("this is default msg"); + data.setPhysicOffset(10L); + data.setPhysicSize(1); + data.setTagsCode(1L); + List list = new ArrayList<>(); + list.add(data); + + body.setQueueData(list); + body.setFilterData("default filter data"); + body.setMaxQueueIndex(100L); + body.setMinQueueIndex(1L); + body.setSubscriptionData(subscriptionData); + + String json = RemotingSerializable.toJson(body, true); + QueryConsumeQueueResponseBody fromJson = RemotingSerializable.fromJson(json, QueryConsumeQueueResponseBody.class); + //test ConsumeQueue + ConsumeQueueData jsonData = fromJson.getQueueData().get(0); + assertThat(jsonData.getMsg()).isEqualTo("this is default msg"); + assertThat(jsonData.getPhysicSize()).isEqualTo(1); + assertThat(jsonData.getBitMap()).isEqualTo("defaultBitMap"); + assertThat(jsonData.getTagsCode()).isEqualTo(1L); + assertThat(jsonData.getPhysicSize()).isEqualTo(1); + + //test QueryConsumeQueueResponseBody + assertThat(fromJson.getFilterData()).isEqualTo("default filter data"); + assertThat(fromJson.getMaxQueueIndex()).isEqualTo(100L); + assertThat(fromJson.getMinQueueIndex()).isEqualTo(1L); + assertThat(fromJson.getSubscriptionData()).isEqualTo(subscriptionData); + + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/QueryCorrectionOffsetBodyTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/QueryCorrectionOffsetBodyTest.java new file mode 100644 index 00000000000..6d62b07f7ac --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/QueryCorrectionOffsetBodyTest.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import java.util.HashMap; +import java.util.Map; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class QueryCorrectionOffsetBodyTest { + + @Test + public void testFromJson() throws Exception { + QueryCorrectionOffsetBody qcob = new QueryCorrectionOffsetBody(); + Map offsetMap = new HashMap<>(); + offsetMap.put(1, 100L); + offsetMap.put(2, 200L); + qcob.setCorrectionOffsets(offsetMap); + String json = RemotingSerializable.toJson(qcob, true); + QueryCorrectionOffsetBody fromJson = RemotingSerializable.fromJson(json, QueryCorrectionOffsetBody.class); + assertThat(fromJson.getCorrectionOffsets().get(1)).isEqualTo(100L); + assertThat(fromJson.getCorrectionOffsets().get(2)).isEqualTo(200L); + assertThat(fromJson.getCorrectionOffsets().size()).isEqualTo(2); + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/ResetOffsetBodyTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/ResetOffsetBodyTest.java new file mode 100644 index 00000000000..115749947b6 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/ResetOffsetBodyTest.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import java.util.HashMap; +import java.util.Map; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ResetOffsetBodyTest { + + @Test + public void testFromJson() throws Exception { + ResetOffsetBody rob = new ResetOffsetBody(); + Map offsetMap = new HashMap<>(); + MessageQueue queue = new MessageQueue(); + queue.setQueueId(1); + queue.setBrokerName("brokerName"); + queue.setTopic("topic"); + offsetMap.put(queue, 100L); + rob.setOffsetTable(offsetMap); + String json = RemotingSerializable.toJson(rob, true); + ResetOffsetBody fromJson = RemotingSerializable.fromJson(json, ResetOffsetBody.class); + assertThat(fromJson.getOffsetTable().get(queue)).isEqualTo(100L); + assertThat(fromJson.getOffsetTable().size()).isEqualTo(1); + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/SubscriptionGroupWrapperTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/SubscriptionGroupWrapperTest.java new file mode 100644 index 00000000000..b7d89a21bfb --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/body/SubscriptionGroupWrapperTest.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.body; + +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SubscriptionGroupWrapperTest { + + @Test + public void testFromJson() { + SubscriptionGroupWrapper subscriptionGroupWrapper = new SubscriptionGroupWrapper(); + ConcurrentHashMap subscriptions = new ConcurrentHashMap<>(); + SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig(); + subscriptionGroupConfig.setConsumeBroadcastEnable(true); + subscriptionGroupConfig.setBrokerId(1234); + subscriptionGroupConfig.setGroupName("Consumer-group-one"); + subscriptions.put("Consumer-group-one", subscriptionGroupConfig); + subscriptionGroupWrapper.setSubscriptionGroupTable(subscriptions); + DataVersion dataVersion = new DataVersion(); + dataVersion.nextVersion(); + subscriptionGroupWrapper.setDataVersion(dataVersion); + String json = RemotingSerializable.toJson(subscriptionGroupWrapper, true); + SubscriptionGroupWrapper fromJson = RemotingSerializable.fromJson(json, SubscriptionGroupWrapper.class); + assertThat(fromJson.getSubscriptionGroupTable()).containsKey("Consumer-group-one"); + assertThat(fromJson.getSubscriptionGroupTable().get("Consumer-group-one").getGroupName()).isEqualTo("Consumer-group-one"); + assertThat(fromJson.getSubscriptionGroupTable().get("Consumer-group-one").getBrokerId()).isEqualTo(1234); + } + +} diff --git a/common/src/test/java/org/apache/rocketmq/common/filter/FilterAPITest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/filter/FilterAPITest.java similarity index 90% rename from common/src/test/java/org/apache/rocketmq/common/filter/FilterAPITest.java rename to remoting/src/test/java/org/apache/rocketmq/remoting/protocol/filter/FilterAPITest.java index 73ab09e61c4..002a1badc3e 100644 --- a/common/src/test/java/org/apache/rocketmq/common/filter/FilterAPITest.java +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/filter/FilterAPITest.java @@ -15,13 +15,13 @@ * limitations under the License. */ -package org.apache.rocketmq.common.filter; - -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; -import org.junit.Test; +package org.apache.rocketmq.remoting.protocol.filter; import java.util.HashSet; import java.util.Set; +import org.apache.rocketmq.common.filter.ExpressionType; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; +import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -33,11 +33,11 @@ public class FilterAPITest { @Test public void testBuildSubscriptionData() throws Exception { SubscriptionData subscriptionData = - FilterAPI.buildSubscriptionData(group, topic, subString); + FilterAPI.buildSubscriptionData(topic, subString); assertThat(subscriptionData.getTopic()).isEqualTo(topic); assertThat(subscriptionData.getSubString()).isEqualTo(subString); String[] tags = subString.split("\\|\\|"); - Set tagSet = new HashSet(); + Set tagSet = new HashSet<>(); for (String tag : tags) { tagSet.add(tag.trim()); } @@ -57,7 +57,7 @@ public void testBuildTagSome() { assertThat(ExpressionType.isTagType(subscriptionData.getExpressionType())).isTrue(); assertThat(subscriptionData.getTagsSet()).isNotNull(); - assertThat(subscriptionData.getTagsSet()).containsExactly("A", "B"); + assertThat(subscriptionData.getTagsSet()).containsExactlyInAnyOrder("A", "B"); } catch (Exception e) { e.printStackTrace(); assertThat(Boolean.FALSE).isTrue(); diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/header/ExtraInfoUtilTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/header/ExtraInfoUtilTest.java new file mode 100644 index 00000000000..8081f386cb6 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/header/ExtraInfoUtilTest.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header; + +import java.util.Map; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class ExtraInfoUtilTest { + + @Test + public void testOrderCountInfo() { + String topic = "TOPIC"; + int queueId = 0; + long queueOffset = 1234; + + Integer queueIdCount = 1; + Integer queueOffsetCount = 2; + + String queueIdKey = ExtraInfoUtil.getStartOffsetInfoMapKey(topic, queueId); + String queueOffsetKey = ExtraInfoUtil.getQueueOffsetMapKey(topic, queueId, queueOffset); + + StringBuilder sb = new StringBuilder(); + ExtraInfoUtil.buildQueueIdOrderCountInfo(sb, topic, queueId, queueIdCount); + ExtraInfoUtil.buildQueueOffsetOrderCountInfo(sb, topic, queueId, queueOffset, queueOffsetCount); + Map orderCountInfo = ExtraInfoUtil.parseOrderCountInfo(sb.toString()); + + assertEquals(queueIdCount, orderCountInfo.get(queueIdKey)); + assertEquals(queueOffsetCount, orderCountInfo.get(queueOffsetKey)); + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/header/FastCodesHeaderTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/header/FastCodesHeaderTest.java new file mode 100644 index 00000000000..b6a0d631129 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/header/FastCodesHeaderTest.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.remoting.protocol.header; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import org.apache.rocketmq.remoting.CommandCustomHeader; +import org.apache.rocketmq.remoting.protocol.FastCodesHeader; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.junit.Assert; +import org.junit.Test; + +public class FastCodesHeaderTest { + + @Test + public void testFastDecode() throws Exception { + testFastDecode(SendMessageRequestHeaderV2.class); + testFastDecode(SendMessageResponseHeader.class); + testFastDecode(PullMessageRequestHeader.class); + testFastDecode(PullMessageResponseHeader.class); + } + + private void testFastDecode(Class classHeader) throws Exception { + Field[] declaredFields = classHeader.getDeclaredFields(); + List declaredFieldsList = new ArrayList<>(); + for (Field f : declaredFields) { + if (f.getName().startsWith("$")) { + continue; + } + f.setAccessible(true); + declaredFieldsList.add(f); + } + RemotingCommand command = RemotingCommand.createRequestCommand(0, null); + HashMap m = buildExtFields(declaredFieldsList); + command.setExtFields(m); + check(command, declaredFieldsList, classHeader); + } + + private HashMap buildExtFields(List fields) { + HashMap extFields = new HashMap<>(); + for (Field f: fields) { + Class c = f.getType(); + if (c.equals(String.class)) { + extFields.put(f.getName(), "str"); + } else if (c.equals(Integer.class) || c.equals(int.class)) { + extFields.put(f.getName(), "123"); + } else if (c.equals(Long.class) || c.equals(long.class)) { + extFields.put(f.getName(), "1234"); + } else if (c.equals(Boolean.class) || c.equals(boolean.class)) { + extFields.put(f.getName(), "true"); + } else { + throw new RuntimeException(f.getName() + ":" + f.getType().getName()); + } + } + return extFields; + } + + private void check(RemotingCommand command, List fields, + Class classHeader) throws Exception { + CommandCustomHeader o1 = command.decodeCommandCustomHeaderDirectly(classHeader, false); + CommandCustomHeader o2 = classHeader.getDeclaredConstructor().newInstance(); + ((FastCodesHeader)o2).decode(command.getExtFields()); + for (Field f : fields) { + Object value1 = f.get(o1); + Object value2 = f.get(o2); + if (value1 == null) { + Assert.assertNull(value2); + } else { + Assert.assertEquals(value1, value2); + } + } + } + +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/header/SendMessageRequestHeaderV2Test.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/header/SendMessageRequestHeaderV2Test.java new file mode 100644 index 00000000000..c817d8cabe5 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/header/SendMessageRequestHeaderV2Test.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.header; + +import java.nio.ByteBuffer; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SendMessageRequestHeaderV2Test { + SendMessageRequestHeaderV2 header = new SendMessageRequestHeaderV2(); + String topic = "test"; + int queueId = 5; + + @Test + public void testEncodeDecode() throws RemotingCommandException { + header.setQueueId(queueId); + header.setTopic(topic); + + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE_V2, header); + ByteBuffer buffer = remotingCommand.encode(); + + //Simulate buffer being read in NettyDecoder + buffer.getInt(); + byte[] bytes = new byte[buffer.limit() - 4]; + buffer.get(bytes, 0, buffer.limit() - 4); + buffer = ByteBuffer.wrap(bytes); + + RemotingCommand decodeRequest = RemotingCommand.decode(buffer); + assertThat(decodeRequest.getExtFields().get("e")).isEqualTo(String.valueOf(queueId)); + assertThat(decodeRequest.getExtFields().get("b")).isEqualTo(topic); + } +} \ No newline at end of file diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/heartbeat/SubscriptionDataTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/heartbeat/SubscriptionDataTest.java new file mode 100644 index 00000000000..a0dd665d911 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/heartbeat/SubscriptionDataTest.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.heartbeat; + +import org.apache.rocketmq.common.filter.ExpressionType; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.assertj.core.util.Sets; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SubscriptionDataTest { + + @Test + public void testConstructor1() { + SubscriptionData subscriptionData = new SubscriptionData(); + assertThat(subscriptionData.getTopic()).isNull(); + assertThat(subscriptionData.getSubString()).isNull(); + assertThat(subscriptionData.getSubVersion()).isLessThanOrEqualTo(System.currentTimeMillis()); + assertThat(subscriptionData.getExpressionType()).isEqualTo(ExpressionType.TAG); + assertThat(subscriptionData.getFilterClassSource()).isNull(); + assertThat(subscriptionData.getCodeSet()).isEmpty(); + assertThat(subscriptionData.getTagsSet()).isEmpty(); + assertThat(subscriptionData.isClassFilterMode()).isFalse(); + } + + @Test + public void testConstructor2() { + SubscriptionData subscriptionData = new SubscriptionData("TOPICA", "*"); + assertThat(subscriptionData.getTopic()).isEqualTo("TOPICA"); + assertThat(subscriptionData.getSubString()).isEqualTo("*"); + assertThat(subscriptionData.getSubVersion()).isLessThanOrEqualTo(System.currentTimeMillis()); + assertThat(subscriptionData.getExpressionType()).isEqualTo(ExpressionType.TAG); + assertThat(subscriptionData.getFilterClassSource()).isNull(); + assertThat(subscriptionData.getCodeSet()).isEmpty(); + assertThat(subscriptionData.getTagsSet()).isEmpty(); + assertThat(subscriptionData.isClassFilterMode()).isFalse(); + } + + + @Test + public void testHashCodeNotEquals() { + SubscriptionData subscriptionData = new SubscriptionData("TOPICA", "*"); + subscriptionData.setCodeSet(Sets.newLinkedHashSet(1, 2, 3)); + subscriptionData.setTagsSet(Sets.newLinkedHashSet("TAGA", "TAGB", "TAG3")); + assertThat(subscriptionData.hashCode()).isNotEqualTo(System.identityHashCode(subscriptionData)); + } + + @Test + public void testFromJson() throws Exception { + SubscriptionData subscriptionData = new SubscriptionData("TOPICA", "*"); + subscriptionData.setFilterClassSource("TestFilterClassSource"); + subscriptionData.setCodeSet(Sets.newLinkedHashSet(1, 2, 3)); + subscriptionData.setTagsSet(Sets.newLinkedHashSet("TAGA", "TAGB", "TAG3")); + String json = RemotingSerializable.toJson(subscriptionData, true); + SubscriptionData fromJson = RemotingSerializable.fromJson(json, SubscriptionData.class); + assertThat(subscriptionData).isEqualTo(fromJson); + assertThat(subscriptionData).isEqualByComparingTo(fromJson); + assertThat(subscriptionData.getFilterClassSource()).isEqualTo("TestFilterClassSource"); + assertThat(fromJson.getFilterClassSource()).isNull(); + } + + + @Test + public void testCompareTo() { + SubscriptionData subscriptionData = new SubscriptionData("TOPICA", "*"); + SubscriptionData subscriptionData1 = new SubscriptionData("TOPICBA", "*"); + assertThat(subscriptionData.compareTo(subscriptionData1)).isEqualTo("TOPICA@*".compareTo("TOPICB@*")); + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/route/TopicRouteDataTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/route/TopicRouteDataTest.java new file mode 100644 index 00000000000..9bee83a26d2 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/route/TopicRouteDataTest.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.route; + + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + + +public class TopicRouteDataTest { + @Test + public void testTopicRouteDataClone() throws Exception { + + TopicRouteData topicRouteData = new TopicRouteData(); + + QueueData queueData = new QueueData(); + queueData.setBrokerName("broker-a"); + queueData.setPerm(6); + queueData.setReadQueueNums(8); + queueData.setWriteQueueNums(8); + queueData.setTopicSysFlag(0); + + List queueDataList = new ArrayList<>(); + queueDataList.add(queueData); + + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(0L, "192.168.0.47:10911"); + brokerAddrs.put(1L, "192.168.0.47:10921"); + + BrokerData brokerData = new BrokerData(); + brokerData.setBrokerAddrs(brokerAddrs); + brokerData.setBrokerName("broker-a"); + brokerData.setCluster("TestCluster"); + + List brokerDataList = new ArrayList<>(); + brokerDataList.add(brokerData); + + topicRouteData.setBrokerDatas(brokerDataList); + topicRouteData.setFilterServerTable(new HashMap<>()); + topicRouteData.setQueueDatas(queueDataList); + + assertThat(new TopicRouteData(topicRouteData)).isEqualTo(topicRouteData); + + } + + @Test + public void testTopicRouteDataJsonSerialize() throws Exception { + + TopicRouteData topicRouteData = new TopicRouteData(); + + QueueData queueData = new QueueData(); + queueData.setBrokerName("broker-a"); + queueData.setPerm(6); + queueData.setReadQueueNums(8); + queueData.setWriteQueueNums(8); + queueData.setTopicSysFlag(0); + + List queueDataList = new ArrayList<>(); + queueDataList.add(queueData); + + HashMap brokerAddrs = new HashMap<>(); + brokerAddrs.put(0L, "192.168.0.47:10911"); + brokerAddrs.put(1L, "192.168.0.47:10921"); + + BrokerData brokerData = new BrokerData(); + brokerData.setBrokerAddrs(brokerAddrs); + brokerData.setBrokerName("broker-a"); + brokerData.setCluster("TestCluster"); + + List brokerDataList = new ArrayList<>(); + brokerDataList.add(brokerData); + + topicRouteData.setBrokerDatas(brokerDataList); + topicRouteData.setFilterServerTable(new HashMap<>()); + topicRouteData.setQueueDatas(queueDataList); + + String topicRouteDataJsonStr = RemotingSerializable.toJson(topicRouteData, true); + TopicRouteData topicRouteDataFromJson = RemotingSerializable.fromJson(topicRouteDataJsonStr, TopicRouteData.class); + + assertThat(topicRouteDataJsonStr).isNotEqualTo(topicRouteDataFromJson); + assertThat(topicRouteDataFromJson.getBrokerDatas()).isEqualTo(topicRouteData.getBrokerDatas()); + assertThat(topicRouteDataFromJson.getFilterServerTable()).isEqualTo(topicRouteData.getFilterServerTable()); + assertThat(topicRouteDataFromJson.getQueueDatas()).isEqualTo(topicRouteData.getQueueDatas()); + + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicQueueMappingTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicQueueMappingTest.java new file mode 100644 index 00000000000..6b8a1392f5b --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicQueueMappingTest.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.statictopic; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.google.common.collect.ImmutableList; +import java.util.Map; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.junit.Assert; +import org.junit.Test; + +public class TopicQueueMappingTest { + + @Test + public void testJsonSerialize() { + LogicQueueMappingItem mappingItem = new LogicQueueMappingItem(1, 2, "broker01", 33333333333333333L, 44444444444444444L, 555555555555555555L, 6666666666666666L, 77777777777777777L); + String mappingItemJson = JSON.toJSONString(mappingItem) ; + { + Map mappingItemMap = JSON.parseObject(mappingItemJson, Map.class); + Assert.assertEquals(8, mappingItemMap.size()); + Assert.assertEquals(mappingItemMap.get("bname"), mappingItem.getBname()); + Assert.assertEquals(mappingItemMap.get("gen"), mappingItem.getGen()); + Assert.assertEquals(mappingItemMap.get("logicOffset"), mappingItem.getLogicOffset()); + Assert.assertEquals(mappingItemMap.get("startOffset"), mappingItem.getStartOffset()); + Assert.assertEquals(mappingItemMap.get("endOffset"), mappingItem.getEndOffset()); + Assert.assertEquals(mappingItemMap.get("timeOfStart"), mappingItem.getTimeOfStart()); + Assert.assertEquals(mappingItemMap.get("timeOfEnd"), mappingItem.getTimeOfEnd()); + + } + //test the decode encode + { + LogicQueueMappingItem mappingItemFromJson = RemotingSerializable.fromJson(mappingItemJson, LogicQueueMappingItem.class); + Assert.assertEquals(mappingItem, mappingItemFromJson); + Assert.assertEquals(mappingItemJson, RemotingSerializable.toJson(mappingItemFromJson, false)); + } + TopicQueueMappingDetail mappingDetail = new TopicQueueMappingDetail("test", 1, "broker01", System.currentTimeMillis()); + TopicQueueMappingDetail.putMappingInfo(mappingDetail, 0, ImmutableList.of(mappingItem)); + + String mappingDetailJson = JSON.toJSONString(mappingDetail); + { + Map mappingDetailMap = JSON.parseObject(mappingDetailJson); + Assert.assertTrue(mappingDetailMap.containsKey("currIdMap")); + Assert.assertEquals(8, mappingDetailMap.size()); + Assert.assertEquals(1, ((JSONObject) mappingDetailMap.get("hostedQueues")).size()); + Assert.assertEquals(1, ((JSONArray)((JSONObject) mappingDetailMap.get("hostedQueues")).get("0")).size()); + } + { + TopicQueueMappingDetail mappingDetailFromJson = RemotingSerializable.decode(mappingDetailJson.getBytes(), TopicQueueMappingDetail.class); + Assert.assertEquals(1, mappingDetailFromJson.getHostedQueues().size()); + Assert.assertEquals(1, mappingDetailFromJson.getHostedQueues().get(0).size()); + Assert.assertEquals(mappingItem, mappingDetailFromJson.getHostedQueues().get(0).get(0)); + Assert.assertEquals(mappingDetailJson, RemotingSerializable.toJson(mappingDetailFromJson, false)); + } + } + + @Test + public void test() { + + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicQueueMappingUtilsTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicQueueMappingUtilsTest.java new file mode 100644 index 00000000000..a12c9f89200 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/statictopic/TopicQueueMappingUtilsTest.java @@ -0,0 +1,319 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.statictopic; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import org.apache.rocketmq.common.TopicConfig; +import org.junit.Assert; +import org.junit.Test; + +public class TopicQueueMappingUtilsTest { + + + private Set buildTargetBrokers(int num) { + return buildTargetBrokers(num, ""); + } + + private Set buildTargetBrokers(int num, String suffix) { + Set brokers = new HashSet<>(); + for (int i = 0; i < num; i++) { + brokers.add("broker" + suffix + i); + } + return brokers; + } + + private Map buildBrokerNumMap(int num) { + Map map = new HashMap<>(); + for (int i = 0; i < num; i++) { + map.put("broker" + i, 0); + } + return map; + } + + private Map buildBrokerNumMap(int num, int queues) { + Map map = new HashMap<>(); + int random = new Random().nextInt(num); + for (int i = 0; i < num; i++) { + map.put("broker" + i, queues); + if (i == random) { + map.put("broker" + i, queues + 1); + } + } + return map; + } + + private void testIdToBroker(Map idToBroker, Map brokerNumMap) { + Map brokerNumOther = new HashMap<>(); + for (int i = 0; i < idToBroker.size(); i++) { + Assert.assertTrue(idToBroker.containsKey(i)); + String broker = idToBroker.get(i); + if (brokerNumOther.containsKey(broker)) { + brokerNumOther.put(broker, brokerNumOther.get(broker) + 1); + } else { + brokerNumOther.put(broker, 1); + } + } + Assert.assertEquals(brokerNumMap.size(), brokerNumOther.size()); + for (Map.Entry entry: brokerNumOther.entrySet()) { + Assert.assertEquals(entry.getValue(), brokerNumMap.get(entry.getKey())); + } + } + + @Test + public void testAllocator() { + //stability + for (int i = 0; i < 10; i++) { + int num = 3; + Map brokerNumMap = buildBrokerNumMap(num); + TopicQueueMappingUtils.MappingAllocator allocator = TopicQueueMappingUtils.buildMappingAllocator(new HashMap<>(), brokerNumMap, null); + allocator.upToNum(num * 2); + for (Map.Entry entry: allocator.getBrokerNumMap().entrySet()) { + Assert.assertEquals(2L, entry.getValue().longValue()); + } + Assert.assertEquals(num * 2, allocator.getIdToBroker().size()); + testIdToBroker(allocator.idToBroker, allocator.getBrokerNumMap()); + + allocator.upToNum(num * 3 - 1); + + for (Map.Entry entry: allocator.getBrokerNumMap().entrySet()) { + Assert.assertTrue(entry.getValue() >= 2); + Assert.assertTrue(entry.getValue() <= 3); + } + Assert.assertEquals(num * 3 - 1, allocator.getIdToBroker().size()); + testIdToBroker(allocator.idToBroker, allocator.getBrokerNumMap()); + } + } + + @Test + public void testRemappingAllocator() { + for (int i = 0; i < 10; i++) { + int num = (i + 2) * 2; + Map brokerNumMap = buildBrokerNumMap(num); + Map brokerNumMapBeforeRemapping = buildBrokerNumMap(num, num); + TopicQueueMappingUtils.MappingAllocator allocator = TopicQueueMappingUtils.buildMappingAllocator(new HashMap<>(), brokerNumMap, brokerNumMapBeforeRemapping); + allocator.upToNum(num * num + 1); + Assert.assertEquals(brokerNumMapBeforeRemapping, allocator.getBrokerNumMap()); + } + } + + + @Test(expected = RuntimeException.class) + public void testTargetBrokersComplete() { + String topic = "static"; + String broker1 = "broker1"; + String broker2 = "broker2"; + Set targetBrokers = new HashSet<>(); + targetBrokers.add(broker1); + Map brokerConfigMap = new HashMap<>(); + TopicQueueMappingDetail mappingDetail = new TopicQueueMappingDetail(topic, 0, broker2, 0); + mappingDetail.getHostedQueues().put(1, new ArrayList<>()); + brokerConfigMap.put(broker2, new TopicConfigAndQueueMapping(new TopicConfig(topic, 0, 0), mappingDetail)); + TopicQueueMappingUtils.checkTargetBrokersComplete(targetBrokers, brokerConfigMap); + } + + + + @Test + public void testCreateStaticTopic() { + String topic = "static"; + int queueNum; + Map brokerConfigMap = new HashMap<>(); + for (int i = 1; i < 10; i++) { + Set targetBrokers = buildTargetBrokers(2 * i); + Set nonTargetBrokers = buildTargetBrokers(2 * i, "test"); + queueNum = 10 * i; + TopicRemappingDetailWrapper wrapper = TopicQueueMappingUtils.createTopicConfigMapping(topic, queueNum, targetBrokers, brokerConfigMap); + Assert.assertEquals(wrapper.getBrokerConfigMap(), brokerConfigMap); + Assert.assertEquals(2 * i, brokerConfigMap.size()); + + //do the check manually + Map.Entry maxEpochAndNum = TopicQueueMappingUtils.checkNameEpochNumConsistence(topic, brokerConfigMap); + Assert.assertEquals(queueNum, maxEpochAndNum.getValue().longValue()); + Map globalIdMap = TopicQueueMappingUtils.checkAndBuildMappingItems(new ArrayList<>(TopicQueueMappingUtils.getMappingDetailFromConfig(brokerConfigMap.values())), false, true); + TopicQueueMappingUtils.checkIfReusePhysicalQueue(globalIdMap.values()); + TopicQueueMappingUtils.checkPhysicalQueueConsistence(brokerConfigMap); + + for (Map.Entry entry : brokerConfigMap.entrySet()) { + TopicConfigAndQueueMapping configMapping = entry.getValue(); + if (nonTargetBrokers.contains(configMapping.getMappingDetail().bname)) { + Assert.assertEquals(0, configMapping.getReadQueueNums()); + Assert.assertEquals(0, configMapping.getWriteQueueNums()); + Assert.assertEquals(0, configMapping.getMappingDetail().getHostedQueues().size()); + } else { + Assert.assertEquals(5, configMapping.getReadQueueNums()); + Assert.assertEquals(5, configMapping.getWriteQueueNums()); + Assert.assertTrue(configMapping.getMappingDetail().epoch > System.currentTimeMillis()); + for (List items: configMapping.getMappingDetail().getHostedQueues().values()) { + for (LogicQueueMappingItem item: items) { + Assert.assertEquals(0, item.getStartOffset()); + Assert.assertEquals(0, item.getLogicOffset()); + TopicConfig topicConfig = brokerConfigMap.get(item.getBname()); + Assert.assertTrue(item.getQueueId() < topicConfig.getWriteQueueNums()); + } + } + } + } + } + } + + @Test + public void testRemappingStaticTopic() { + String topic = "static"; + int queueNum = 7; + Map brokerConfigMap = new HashMap<>(); + Set originalBrokers = buildTargetBrokers(2); + TopicRemappingDetailWrapper wrapper = TopicQueueMappingUtils.createTopicConfigMapping(topic, queueNum, originalBrokers, brokerConfigMap); + Assert.assertEquals(wrapper.getBrokerConfigMap(), brokerConfigMap); + Assert.assertEquals(2, brokerConfigMap.size()); + + { + //do the check manually + TopicQueueMappingUtils.checkNameEpochNumConsistence(topic, brokerConfigMap); + TopicQueueMappingUtils.checkPhysicalQueueConsistence(brokerConfigMap); + Map globalIdMap = TopicQueueMappingUtils.checkAndBuildMappingItems(new ArrayList<>(TopicQueueMappingUtils.getMappingDetailFromConfig(brokerConfigMap.values())), false, true); + TopicQueueMappingUtils.checkIfReusePhysicalQueue(globalIdMap.values()); + } + + for (int i = 0; i < 10; i++) { + Set targetBrokers = buildTargetBrokers(2, "test" + i); + TopicQueueMappingUtils.remappingStaticTopic(topic, brokerConfigMap, targetBrokers); + //do the check manually + TopicQueueMappingUtils.checkNameEpochNumConsistence(topic, brokerConfigMap); + TopicQueueMappingUtils.checkPhysicalQueueConsistence(brokerConfigMap); + Map globalIdMap = TopicQueueMappingUtils.checkAndBuildMappingItems(new ArrayList<>(TopicQueueMappingUtils.getMappingDetailFromConfig(brokerConfigMap.values())), false, true); + TopicQueueMappingUtils.checkIfReusePhysicalQueue(globalIdMap.values()); + TopicQueueMappingUtils.checkLeaderInTargetBrokers(globalIdMap.values(), targetBrokers); + + Assert.assertEquals((i + 2) * 2, brokerConfigMap.size()); + + //check and complete the logicOffset + for (Map.Entry entry : brokerConfigMap.entrySet()) { + TopicConfigAndQueueMapping configMapping = entry.getValue(); + if (!targetBrokers.contains(configMapping.getMappingDetail().bname)) { + continue; + } + for (List items: configMapping.getMappingDetail().getHostedQueues().values()) { + Assert.assertEquals(i + 2, items.size()); + items.get(items.size() - 1).setLogicOffset(i + 1); + } + } + } + } + + @Test + public void testRemappingStaticTopicStability() { + String topic = "static"; + int queueNum = 7; + Map brokerConfigMap = new HashMap<>(); + Set originalBrokers = buildTargetBrokers(2); + { + TopicRemappingDetailWrapper wrapper = TopicQueueMappingUtils.createTopicConfigMapping(topic, queueNum, originalBrokers, brokerConfigMap); + Assert.assertEquals(wrapper.getBrokerConfigMap(), brokerConfigMap); + Assert.assertEquals(2, brokerConfigMap.size()); + } + for (int i = 0; i < 10; i++) { + TopicRemappingDetailWrapper wrapper = TopicQueueMappingUtils.remappingStaticTopic(topic, brokerConfigMap, originalBrokers); + Assert.assertEquals(wrapper.getBrokerConfigMap(), brokerConfigMap); + Assert.assertEquals(2, brokerConfigMap.size()); + Assert.assertTrue(wrapper.getBrokerToMapIn().isEmpty()); + Assert.assertTrue(wrapper.getBrokerToMapOut().isEmpty()); + } + } + + + + @Test + public void testUtilsCheck() { + String topic = "static"; + int queueNum = 10; + Map brokerConfigMap = new HashMap<>(); + Set targetBrokers = buildTargetBrokers(2); + TopicRemappingDetailWrapper wrapper = TopicQueueMappingUtils.createTopicConfigMapping(topic, queueNum, targetBrokers, brokerConfigMap); + Assert.assertEquals(wrapper.getBrokerConfigMap(), brokerConfigMap); + Assert.assertEquals(2, brokerConfigMap.size()); + TopicConfigAndQueueMapping configMapping = brokerConfigMap.values().iterator().next(); + List items = configMapping.getMappingDetail().getHostedQueues().values().iterator().next(); + Map.Entry maxEpochNum = TopicQueueMappingUtils.checkNameEpochNumConsistence(topic, brokerConfigMap); + int exceptionNum = 0; + try { + configMapping.getMappingDetail().setTopic("xxxx"); + TopicQueueMappingUtils.checkNameEpochNumConsistence(topic, brokerConfigMap); + } catch (RuntimeException ignore) { + exceptionNum++; + configMapping.getMappingDetail().setTopic(topic); + TopicQueueMappingUtils.checkNameEpochNumConsistence(topic, brokerConfigMap); + } + + try { + configMapping.getMappingDetail().setTotalQueues(1); + TopicQueueMappingUtils.checkNameEpochNumConsistence(topic, brokerConfigMap); + } catch (RuntimeException ignore) { + exceptionNum++; + configMapping.getMappingDetail().setTotalQueues(10); + TopicQueueMappingUtils.checkNameEpochNumConsistence(topic, brokerConfigMap); + } + + try { + configMapping.getMappingDetail().setEpoch(0); + TopicQueueMappingUtils.checkNameEpochNumConsistence(topic, brokerConfigMap); + } catch (RuntimeException ignore) { + exceptionNum++; + configMapping.getMappingDetail().setEpoch(maxEpochNum.getKey()); + TopicQueueMappingUtils.checkNameEpochNumConsistence(topic, brokerConfigMap); + } + + + try { + configMapping.getMappingDetail().getHostedQueues().put(10000, new ArrayList<>(Collections.singletonList(new LogicQueueMappingItem(1, 1, targetBrokers.iterator().next(), 0, 0, -1, -1, -1)))); + TopicQueueMappingUtils.checkAndBuildMappingItems(TopicQueueMappingUtils.getMappingDetailFromConfig(brokerConfigMap.values()), false, true); + } catch (RuntimeException ignore) { + exceptionNum++; + configMapping.getMappingDetail().getHostedQueues().remove(10000); + TopicQueueMappingUtils.checkAndBuildMappingItems(TopicQueueMappingUtils.getMappingDetailFromConfig(brokerConfigMap.values()), false, true); + } + + try { + configMapping.setWriteQueueNums(1); + TopicQueueMappingUtils.checkPhysicalQueueConsistence(brokerConfigMap); + } catch (RuntimeException ignore) { + exceptionNum++; + configMapping.setWriteQueueNums(5); + TopicQueueMappingUtils.checkPhysicalQueueConsistence(brokerConfigMap); + } + + try { + items.add(new LogicQueueMappingItem(1, 1, targetBrokers.iterator().next(), 0, 0, -1, -1, -1)); + Map map = TopicQueueMappingUtils.checkAndBuildMappingItems(TopicQueueMappingUtils.getMappingDetailFromConfig(brokerConfigMap.values()), false, true); + TopicQueueMappingUtils.checkIfReusePhysicalQueue(map.values()); + } catch (RuntimeException ignore) { + exceptionNum++; + items.remove(items.size() - 1); + Map map = TopicQueueMappingUtils.checkAndBuildMappingItems(TopicQueueMappingUtils.getMappingDetailFromConfig(brokerConfigMap.values()), false, true); + TopicQueueMappingUtils.checkIfReusePhysicalQueue(map.values()); + } + Assert.assertEquals(6, exceptionNum); + + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/subscription/CustomizedRetryPolicyTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/subscription/CustomizedRetryPolicyTest.java new file mode 100644 index 00000000000..eb215b32528 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/subscription/CustomizedRetryPolicyTest.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.subscription; + +import java.util.concurrent.TimeUnit; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CustomizedRetryPolicyTest { + + @Test + public void testNextDelayDuration() { + CustomizedRetryPolicy customizedRetryPolicy = new CustomizedRetryPolicy(); + long actual = customizedRetryPolicy.nextDelayDuration(0); + assertThat(actual).isEqualTo(TimeUnit.SECONDS.toMillis(10)); + actual = customizedRetryPolicy.nextDelayDuration(10); + assertThat(actual).isEqualTo(TimeUnit.MINUTES.toMillis(9)); + } + + @Test + public void testNextDelayDurationOutOfRange() { + CustomizedRetryPolicy customizedRetryPolicy = new CustomizedRetryPolicy(); + long actual = customizedRetryPolicy.nextDelayDuration(-1); + assertThat(actual).isEqualTo(TimeUnit.SECONDS.toMillis(10)); + actual = customizedRetryPolicy.nextDelayDuration(100); + assertThat(actual).isEqualTo(TimeUnit.HOURS.toMillis(2)); + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/subscription/ExponentialRetryPolicyTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/subscription/ExponentialRetryPolicyTest.java new file mode 100644 index 00000000000..0a0421be52d --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/subscription/ExponentialRetryPolicyTest.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.subscription; + +import java.util.concurrent.TimeUnit; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ExponentialRetryPolicyTest { + + @Test + public void testNextDelayDuration() { + ExponentialRetryPolicy exponentialRetryPolicy = new ExponentialRetryPolicy(); + long actual = exponentialRetryPolicy.nextDelayDuration(0); + assertThat(actual).isEqualTo(TimeUnit.SECONDS.toMillis(5)); + actual = exponentialRetryPolicy.nextDelayDuration(10); + assertThat(actual).isEqualTo(TimeUnit.SECONDS.toMillis(1024 * 5)); + } + + @Test + public void testNextDelayDurationOutOfRange() { + ExponentialRetryPolicy exponentialRetryPolicy = new ExponentialRetryPolicy(); + long actual = exponentialRetryPolicy.nextDelayDuration(-1); + assertThat(actual).isEqualTo(TimeUnit.SECONDS.toMillis(5)); + actual = exponentialRetryPolicy.nextDelayDuration(100); + assertThat(actual).isEqualTo(TimeUnit.HOURS.toMillis(2)); + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/subscription/GroupRetryPolicyTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/subscription/GroupRetryPolicyTest.java new file mode 100644 index 00000000000..c449e564f94 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/subscription/GroupRetryPolicyTest.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.subscription; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GroupRetryPolicyTest { + + @Test + public void testGetRetryPolicy() { + GroupRetryPolicy groupRetryPolicy = new GroupRetryPolicy(); + RetryPolicy retryPolicy = groupRetryPolicy.getRetryPolicy(); + assertThat(retryPolicy).isInstanceOf(CustomizedRetryPolicy.class); + groupRetryPolicy.setType(GroupRetryPolicyType.EXPONENTIAL); + retryPolicy = groupRetryPolicy.getRetryPolicy(); + assertThat(retryPolicy).isInstanceOf(CustomizedRetryPolicy.class); + + groupRetryPolicy.setType(GroupRetryPolicyType.CUSTOMIZED); + groupRetryPolicy.setCustomizedRetryPolicy(new CustomizedRetryPolicy()); + retryPolicy = groupRetryPolicy.getRetryPolicy(); + assertThat(retryPolicy).isInstanceOf(CustomizedRetryPolicy.class); + + groupRetryPolicy.setType(GroupRetryPolicyType.EXPONENTIAL); + groupRetryPolicy.setExponentialRetryPolicy(new ExponentialRetryPolicy()); + retryPolicy = groupRetryPolicy.getRetryPolicy(); + assertThat(retryPolicy).isInstanceOf(ExponentialRetryPolicy.class); + + groupRetryPolicy.setType(null); + retryPolicy = groupRetryPolicy.getRetryPolicy(); + assertThat(retryPolicy).isInstanceOf(CustomizedRetryPolicy.class); + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/topic/OffsetMovedEventTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/topic/OffsetMovedEventTest.java new file mode 100644 index 00000000000..c8d4ef9d7b5 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/protocol/topic/OffsetMovedEventTest.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.protocol.topic; + +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OffsetMovedEventTest { + + @Test + public void testFromJson() throws Exception { + OffsetMovedEvent event = mockOffsetMovedEvent(); + + String json = event.toJson(); + OffsetMovedEvent fromJson = RemotingSerializable.fromJson(json, OffsetMovedEvent.class); + + assertEquals(event, fromJson); + } + + @Test + public void testFromBytes() throws Exception { + OffsetMovedEvent event = mockOffsetMovedEvent(); + + byte[] encodeData = event.encode(); + OffsetMovedEvent decodeData = RemotingSerializable.decode(encodeData, OffsetMovedEvent.class); + + assertEquals(event, decodeData); + } + + private void assertEquals(OffsetMovedEvent srcData, OffsetMovedEvent decodeData) { + assertThat(decodeData.getConsumerGroup()).isEqualTo(srcData.getConsumerGroup()); + assertThat(decodeData.getMessageQueue().getTopic()) + .isEqualTo(srcData.getMessageQueue().getTopic()); + assertThat(decodeData.getMessageQueue().getBrokerName()) + .isEqualTo(srcData.getMessageQueue().getBrokerName()); + assertThat(decodeData.getMessageQueue().getQueueId()) + .isEqualTo(srcData.getMessageQueue().getQueueId()); + assertThat(decodeData.getOffsetRequest()).isEqualTo(srcData.getOffsetRequest()); + assertThat(decodeData.getOffsetNew()).isEqualTo(srcData.getOffsetNew()); + } + + private OffsetMovedEvent mockOffsetMovedEvent() { + OffsetMovedEvent event = new OffsetMovedEvent(); + event.setConsumerGroup("test-group"); + event.setMessageQueue(new MessageQueue("test-topic", "test-broker", 0)); + event.setOffsetRequest(3000L); + event.setOffsetNew(1000L); + return event; + } +} diff --git a/remoting/src/test/java/org/apache/rocketmq/remoting/rpc/RpcRequestHeaderTest.java b/remoting/src/test/java/org/apache/rocketmq/remoting/rpc/RpcRequestHeaderTest.java new file mode 100644 index 00000000000..78047845910 --- /dev/null +++ b/remoting/src/test/java/org/apache/rocketmq/remoting/rpc/RpcRequestHeaderTest.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.remoting.rpc; + +import java.nio.ByteBuffer; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RpcRequestHeaderTest { + String brokerName = "brokerName1"; + String namespace = "namespace1"; + boolean namespaced = true; + boolean oneway = false; + static class TestRequestHeader extends RpcRequestHeader { + + @Override + public void checkFields() throws RemotingCommandException { + + } + } + + @Test + public void testEncodeDecode() throws RemotingCommandException { + TestRequestHeader requestHeader = new TestRequestHeader(); + requestHeader.setBrokerName(brokerName); + requestHeader.setNamespace(namespace); + requestHeader.setNamespaced(namespaced); + requestHeader.setOneway(oneway); + + RemotingCommand remotingCommand = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, requestHeader); + ByteBuffer buffer = remotingCommand.encode(); + + //Simulate buffer being read in NettyDecoder + buffer.getInt(); + byte[] bytes = new byte[buffer.limit() - 4]; + buffer.get(bytes, 0, buffer.limit() - 4); + buffer = ByteBuffer.wrap(bytes); + + RemotingCommand decodeRequest = RemotingCommand.decode(buffer); + assertThat(decodeRequest.getExtFields().get("bname")).isEqualTo(brokerName); + assertThat(decodeRequest.getExtFields().get("nsd")).isEqualTo(String.valueOf(namespaced)); + assertThat(decodeRequest.getExtFields().get("ns")).isEqualTo(namespace); + assertThat(decodeRequest.getExtFields().get("oway")).isEqualTo(String.valueOf(oneway)); + } +} \ No newline at end of file diff --git a/remoting/src/test/resources/rmq.logback-test.xml b/remoting/src/test/resources/rmq.logback-test.xml new file mode 100644 index 00000000000..8695d52d57c --- /dev/null +++ b/remoting/src/test/resources/rmq.logback-test.xml @@ -0,0 +1,36 @@ + + + + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + \ No newline at end of file diff --git a/srvutil/BUILD.bazel b/srvutil/BUILD.bazel new file mode 100644 index 00000000000..a47a60cb160 --- /dev/null +++ b/srvutil/BUILD.bazel @@ -0,0 +1,60 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +load("//bazel:GenTestRules.bzl", "GenTestRules") + +java_library( + name = "srvutil", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//common", + "//remoting", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:commons_validator_commons_validator", + "@maven//:com_github_luben_zstd_jni", + "@maven//:org_lz4_lz4_java", + "@maven//:com_alibaba_fastjson", + "@maven//:io_netty_netty_all", + "@maven//:commons_cli_commons_cli", + "@maven//:com_googlecode_concurrentlinkedhashmap_concurrentlinkedhashmap_lru", + "@maven//:com_google_guava_guava", + "@maven//:io_github_aliyunmq_rocketmq_slf4j_api", + "@maven//:io_github_aliyunmq_rocketmq_logback_classic", + ], +) + +java_library( + name = "tests", + srcs = glob(["src/test/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + ":srvutil", + "//common", + "//remoting", + "//:test_deps", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:io_netty_netty_all", + ], +) + +GenTestRules( + name = "GeneratedTestRules", + test_files = glob(["src/test/java/**/*Test.java"]), + deps = [ + ":tests", + ], +) diff --git a/srvutil/pom.xml b/srvutil/pom.xml index 7965966c6d0..becadace52d 100644 --- a/srvutil/pom.xml +++ b/srvutil/pom.xml @@ -19,7 +19,7 @@ org.apache.rocketmq rocketmq-all - 4.2.0 + 5.2.0 4.0.0 @@ -27,6 +27,9 @@ rocketmq-srvutil rocketmq-srvutil ${project.version} + + ${basedir}/.. + @@ -45,5 +48,9 @@ com.google.guava guava + + com.googlecode.concurrentlinkedhashmap + concurrentlinkedhashmap-lru + diff --git a/srvutil/src/main/java/org/apache/rocketmq/srvutil/AclFileWatchService.java b/srvutil/src/main/java/org/apache/rocketmq/srvutil/AclFileWatchService.java new file mode 100644 index 00000000000..eff9b422857 --- /dev/null +++ b/srvutil/src/main/java/org/apache/rocketmq/srvutil/AclFileWatchService.java @@ -0,0 +1,162 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.srvutil; + +import java.util.ArrayList; +import java.util.List; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.util.HashMap; +import java.util.Map; + +public class AclFileWatchService extends ServiceThread { + private static final Logger log = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); + + private final String aclPath; + private int aclFilesNum; + @Deprecated + private final Map fileCurrentHash; + private Map fileLastModifiedTime; + private List fileList = new ArrayList<>(); + private final AclFileWatchService.Listener listener; + private static final int WATCH_INTERVAL = 5000; + private MessageDigest md = MessageDigest.getInstance("MD5"); + private String defaultAclFile; + + public AclFileWatchService(String path, String defaultAclFile, final AclFileWatchService.Listener listener) throws Exception { + this.aclPath = path; + this.defaultAclFile = defaultAclFile; + this.fileCurrentHash = new HashMap<>(); + this.fileLastModifiedTime = new HashMap<>(); + this.listener = listener; + + getAllAclFiles(path); + if (new File(this.defaultAclFile).exists() && !fileList.contains(this.defaultAclFile)) { + fileList.add(this.defaultAclFile); + } + this.aclFilesNum = fileList.size(); + for (int i = 0; i < aclFilesNum; i++) { + String fileAbsolutePath = fileList.get(i); + this.fileLastModifiedTime.put(fileAbsolutePath, new File(fileAbsolutePath).lastModified()); + } + + } + + public void getAllAclFiles(String path) { + File file = new File(path); + if (!file.exists()) { + log.info("The default acl dir {} is not exist", path); + return; + } + File[] files = file.listFiles(); + for (int i = 0; i < files.length; i++) { + String fileName = files[i].getAbsolutePath(); + File f = new File(fileName); + if (fileName.equals(aclPath + File.separator + "tools.yml")) { + continue; + } else if (fileName.endsWith(".yml") || fileName.endsWith(".yaml")) { + fileList.add(fileName); + } else if (f.isDirectory()) { + getAllAclFiles(fileName); + } + } + } + + @Override + public String getServiceName() { + return "AclFileWatchService"; + } + + @Override + public void run() { + log.info(this.getServiceName() + " service started"); + + while (!this.isStopped()) { + try { + this.waitForRunning(WATCH_INTERVAL); + + if (fileList.size() > 0) { + fileList.clear(); + } + getAllAclFiles(aclPath); + if (new File(defaultAclFile).exists() && !fileList.contains(defaultAclFile)) { + fileList.add(defaultAclFile); + } + int realAclFilesNum = fileList.size(); + + if (aclFilesNum != realAclFilesNum) { + log.info("aclFilesNum: " + aclFilesNum + " realAclFilesNum: " + realAclFilesNum); + aclFilesNum = realAclFilesNum; + log.info("aclFilesNum: " + aclFilesNum + " realAclFilesNum: " + realAclFilesNum); + Map fileLastModifiedTime = new HashMap<>(realAclFilesNum); + for (int i = 0; i < realAclFilesNum; i++) { + String fileAbsolutePath = fileList.get(i); + fileLastModifiedTime.put(fileAbsolutePath, new File(fileAbsolutePath).lastModified()); + } + this.fileLastModifiedTime = fileLastModifiedTime; + listener.onFileNumChanged(aclPath); + } else { + for (int i = 0; i < aclFilesNum; i++) { + String fileName = fileList.get(i); + Long newLastModifiedTime = new File(fileName).lastModified(); + if (!newLastModifiedTime.equals(fileLastModifiedTime.get(fileName))) { + fileLastModifiedTime.put(fileName, newLastModifiedTime); + listener.onFileChanged(fileName); + } + } + } + } catch (Exception e) { + log.warn(this.getServiceName() + " service has exception. ", e); + } + } + log.info(this.getServiceName() + " service end"); + } + + @Deprecated + private String hash(String filePath) throws IOException { + Path path = Paths.get(filePath); + md.update(Files.readAllBytes(path)); + byte[] hash = md.digest(); + return UtilAll.bytes2string(hash); + } + + public interface Listener { + /** + * Will be called when the target file is changed + * + * @param aclFileName the changed file absolute path + */ + void onFileChanged(String aclFileName); + + /** + * Will be called when the number of the acl file is changed + * + * @param path the path of the acl dir + */ + void onFileNumChanged(String path); + } +} diff --git a/srvutil/src/main/java/org/apache/rocketmq/srvutil/FileWatchService.java b/srvutil/src/main/java/org/apache/rocketmq/srvutil/FileWatchService.java new file mode 100644 index 00000000000..06c301bec9c --- /dev/null +++ b/srvutil/src/main/java/org/apache/rocketmq/srvutil/FileWatchService.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.srvutil; + +import com.google.common.base.Strings; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.util.HashMap; +import java.util.Map; +import org.apache.rocketmq.common.LifecycleAwareServiceThread; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class FileWatchService extends LifecycleAwareServiceThread { + private static final Logger log = LoggerFactory.getLogger(LoggerName.COMMON_LOGGER_NAME); + + private final Map currentHash = new HashMap<>(); + private final Listener listener; + private static final int WATCH_INTERVAL = 500; + private final MessageDigest md = MessageDigest.getInstance("MD5"); + + public FileWatchService(final String[] watchFiles, + final Listener listener) throws Exception { + this.listener = listener; + for (String file : watchFiles) { + if (!Strings.isNullOrEmpty(file) && new File(file).exists()) { + currentHash.put(file, md5Digest(file)); + } + } + } + + @Override + public String getServiceName() { + return "FileWatchService"; + } + + @Override + public void run0() { + log.info(this.getServiceName() + " service started"); + + while (!this.isStopped()) { + try { + this.waitForRunning(WATCH_INTERVAL); + for (Map.Entry entry : currentHash.entrySet()) { + String newHash = md5Digest(entry.getKey()); + if (!newHash.equals(currentHash.get(entry.getKey()))) { + entry.setValue(newHash); + listener.onChanged(entry.getKey()); + } + } + } catch (Exception e) { + log.warn(this.getServiceName() + " service raised an unexpected exception.", e); + } + } + log.info(this.getServiceName() + " service end"); + } + + /** + * Note: we ignore DELETE event on purpose. This is useful when application renew CA file. + * When the operator delete/rename the old CA file and copy a new one, this ensures the old CA file is used during + * the operation. + *

    + * As we know exactly what to do when file does not exist or when IO exception is raised, there is no need to + * propagate the exception up. + * + * @param filePath Absolute path of the file to calculate its MD5 digest. + * @return Hash of the file content if exists; empty string otherwise. + */ + private String md5Digest(String filePath) { + Path path = Paths.get(filePath); + if (!path.toFile().exists()) { + // Reuse previous hash result + return currentHash.getOrDefault(filePath, ""); + } + byte[] raw; + try { + raw = Files.readAllBytes(path); + } catch (IOException e) { + log.info("Failed to read content of {}", filePath); + // Reuse previous hash result + return currentHash.getOrDefault(filePath, ""); + } + md.update(raw); + byte[] hash = md.digest(); + return UtilAll.bytes2string(hash); + } + + public interface Listener { + /** + * Will be called when the target files are changed + * + * @param path the changed file path + */ + void onChanged(String path); + } +} diff --git a/srvutil/src/main/java/org/apache/rocketmq/srvutil/ServerUtil.java b/srvutil/src/main/java/org/apache/rocketmq/srvutil/ServerUtil.java index 066d36cedd9..5a8a7cd5476 100644 --- a/srvutil/src/main/java/org/apache/rocketmq/srvutil/ServerUtil.java +++ b/srvutil/src/main/java/org/apache/rocketmq/srvutil/ServerUtil.java @@ -33,7 +33,7 @@ public static Options buildCommandlineOptions(final Options options) { opt = new Option("n", "namesrvAddr", true, - "Name server address list, eg: 192.168.0.1:9876;192.168.0.2:9876"); + "Name server address list, eg: '192.168.0.1:9876;192.168.0.2:9876'"); opt.setRequired(false); options.addOption(opt); @@ -49,10 +49,12 @@ public static CommandLine parseCmdLine(final String appName, String[] args, Opti commandLine = parser.parse(options, args); if (commandLine.hasOption('h')) { hf.printHelp(appName, options, true); - return null; + System.exit(0); } } catch (ParseException e) { + System.err.println(e.getMessage()); hf.printHelp(appName, options, true); + System.exit(1); } return commandLine; diff --git a/srvutil/src/main/java/org/apache/rocketmq/srvutil/ShutdownHookThread.java b/srvutil/src/main/java/org/apache/rocketmq/srvutil/ShutdownHookThread.java index 11f9b2c3365..15b57bf0c97 100644 --- a/srvutil/src/main/java/org/apache/rocketmq/srvutil/ShutdownHookThread.java +++ b/srvutil/src/main/java/org/apache/rocketmq/srvutil/ShutdownHookThread.java @@ -17,9 +17,10 @@ package org.apache.rocketmq.srvutil; +import org.apache.rocketmq.logging.org.slf4j.Logger; + import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicInteger; -import org.slf4j.Logger; /** * {@link ShutdownHookThread} is the standard hook for filtersrv and namesrv modules. diff --git a/srvutil/src/test/java/org/apache/rocketmq/srvutil/FileWatchServiceTest.java b/srvutil/src/test/java/org/apache/rocketmq/srvutil/FileWatchServiceTest.java new file mode 100644 index 00000000000..aeae6253141 --- /dev/null +++ b/srvutil/src/test/java/org/apache/rocketmq/srvutil/FileWatchServiceTest.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.srvutil; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(MockitoJUnitRunner.class) +public class FileWatchServiceTest { + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Test + public void watchSingleFile() throws Exception { + final File file = tempFolder.newFile(); + final Semaphore waitSemaphore = new Semaphore(0); + FileWatchService fileWatchService = new FileWatchService(new String[] {file.getAbsolutePath()}, path -> { + assertThat(file.getAbsolutePath()).isEqualTo(path); + waitSemaphore.release(); + }); + fileWatchService.start(); + fileWatchService.awaitStarted(1000); + modifyFile(file); + boolean result = waitSemaphore.tryAcquire(1, 1000, TimeUnit.MILLISECONDS); + assertThat(result).isTrue(); + fileWatchService.shutdown(); + } + + @Test + public void watchSingleFile_FileDeleted() throws Exception { + File file = tempFolder.newFile(); + final Semaphore waitSemaphore = new Semaphore(0); + FileWatchService fileWatchService = new FileWatchService(new String[] {file.getAbsolutePath()}, + path -> waitSemaphore.release()); + fileWatchService.start(); + fileWatchService.awaitStarted(1000); + assertThat(file.delete()).isTrue(); + boolean result = waitSemaphore.tryAcquire(1, 1000, TimeUnit.MILLISECONDS); + assertThat(result).isFalse(); + assertThat(file.createNewFile()).isTrue(); + modifyFile(file); + result = waitSemaphore.tryAcquire(1, 2000, TimeUnit.MILLISECONDS); + assertThat(result).isTrue(); + fileWatchService.shutdown(); + } + + @Test + public void watchTwoFile_FileDeleted() throws Exception { + File fileA = tempFolder.newFile(); + File fileB = tempFolder.newFile(); + Files.write(fileA.toPath(), "Hello, World!".getBytes(StandardCharsets.UTF_8)); + Files.write(fileB.toPath(), "Hello, World!".getBytes(StandardCharsets.UTF_8)); + final Semaphore waitSemaphore = new Semaphore(0); + FileWatchService fileWatchService = new FileWatchService( + new String[] {fileA.getAbsolutePath(), fileB.getAbsolutePath()}, + path -> waitSemaphore.release()); + fileWatchService.start(); + fileWatchService.awaitStarted(1000); + assertThat(fileA.delete()).isTrue(); + boolean result = waitSemaphore.tryAcquire(1, 1000, TimeUnit.MILLISECONDS); + assertThat(result).isFalse(); + modifyFile(fileB); + result = waitSemaphore.tryAcquire(1, 1000, TimeUnit.MILLISECONDS); + assertThat(result).isTrue(); + assertThat(fileA.createNewFile()).isTrue(); + modifyFile(fileA); + result = waitSemaphore.tryAcquire(1, 1000, TimeUnit.MILLISECONDS); + assertThat(result).isTrue(); + fileWatchService.shutdown(); + } + + @Test + public void watchTwoFiles_ModifyOne() throws Exception { + final File fileA = tempFolder.newFile(); + File fileB = tempFolder.newFile(); + final Semaphore waitSemaphore = new Semaphore(0); + FileWatchService fileWatchService = new FileWatchService( + new String[] {fileA.getAbsolutePath(), fileB.getAbsolutePath()}, + path -> { + assertThat(path).isEqualTo(fileA.getAbsolutePath()); + waitSemaphore.release(); + }); + fileWatchService.start(); + fileWatchService.awaitStarted(1000); + modifyFile(fileA); + boolean result = waitSemaphore.tryAcquire(1, 2000, TimeUnit.MILLISECONDS); + assertThat(result).isTrue(); + fileWatchService.shutdown(); + } + + @Test + public void watchTwoFiles() throws Exception { + File fileA = tempFolder.newFile(); + File fileB = tempFolder.newFile(); + final Semaphore waitSemaphore = new Semaphore(0); + FileWatchService fileWatchService = new FileWatchService( + new String[] {fileA.getAbsolutePath(), fileB.getAbsolutePath()}, + path -> waitSemaphore.release()); + fileWatchService.start(); + fileWatchService.awaitStarted(1000); + modifyFile(fileA); + modifyFile(fileB); + boolean result = waitSemaphore.tryAcquire(2, 1000, TimeUnit.MILLISECONDS); + assertThat(result).isTrue(); + } + + private static void modifyFile(File file) { + try { + PrintWriter out = new PrintWriter(file); + out.println(System.nanoTime()); + out.flush(); + out.close(); + } catch (IOException ignore) { + } + } +} \ No newline at end of file diff --git a/srvutil/src/test/resources/rmq.logback-test.xml b/srvutil/src/test/resources/rmq.logback-test.xml new file mode 100644 index 00000000000..8695d52d57c --- /dev/null +++ b/srvutil/src/test/resources/rmq.logback-test.xml @@ -0,0 +1,36 @@ + + + + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + \ No newline at end of file diff --git a/store/BUILD.bazel b/store/BUILD.bazel new file mode 100644 index 00000000000..8364a239c9a --- /dev/null +++ b/store/BUILD.bazel @@ -0,0 +1,89 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +load("//bazel:GenTestRules.bzl", "GenTestRules") + +java_library( + name = "store", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//common", + "//remoting", + "@maven//:com_alibaba_fastjson", + "@maven//:com_conversantmedia_disruptor", + "@maven//:com_google_guava_guava", + "@maven//:commons_collections_commons_collections", + "@maven//:commons_io_commons_io", + "@maven//:io_netty_netty_all", + "@maven//:io_openmessaging_storage_dledger", + "@maven//:io_opentelemetry_opentelemetry_api", + "@maven//:io_opentelemetry_opentelemetry_context", + "@maven//:io_opentelemetry_opentelemetry_exporter_otlp", + "@maven//:io_opentelemetry_opentelemetry_exporter_prometheus", + "@maven//:io_opentelemetry_opentelemetry_sdk", + "@maven//:io_opentelemetry_opentelemetry_sdk_common", + "@maven//:io_opentelemetry_opentelemetry_sdk_metrics", + "@maven//:net_java_dev_jna_jna", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:io_github_aliyunmq_rocketmq_slf4j_api", + "@maven//:io_github_aliyunmq_rocketmq_logback_classic", + "@maven//:org_apache_rocketmq_rocketmq_rocksdb", + ], +) + +java_library( + name = "tests", + srcs = glob(["src/test/java/**/*.java"]), + resources = glob(["src/test/resources/certs/*.pem"]) + glob(["src/test/resources/certs/*.key"]), + visibility = ["//visibility:public"], + deps = [ + ":store", + "//:test_deps", + "//common", + "//remoting", + "@maven//:com_alibaba_fastjson", + "@maven//:com_conversantmedia_disruptor", + "@maven//:io_openmessaging_storage_dledger", + "@maven//:org_apache_commons_commons_lang3", + "@maven//:com_google_guava_guava", + "@maven//:io_github_aliyunmq_rocketmq_slf4j_api", + "@maven//:io_github_aliyunmq_rocketmq_logback_classic", + "@maven//:org_apache_rocketmq_rocketmq_rocksdb", + ], +) + +GenTestRules( + name = "GeneratedTestRules", + exclude_tests = [ + # These tests are extremely slow and flaky, exclude them before they are properly fixed. + "src/test/java/org/apache/rocketmq/store/ha/autoswitch/AutoSwitchHATest", + "src/test/java/org/apache/rocketmq/store/dledger/DLedgerCommitlogTest", + ], + medium_tests = [ + "src/test/java/org/apache/rocketmq/store/DefaultMessageStoreTest", + "src/test/java/org/apache/rocketmq/store/HATest", + "src/test/java/org/apache/rocketmq/store/dledger/DLedgerCommitlogTest", + "src/test/java/org/apache/rocketmq/store/MappedFileQueueTest", + "src/test/java/org/apache/rocketmq/store/queue/BatchConsumeMessageTest", + "src/test/java/org/apache/rocketmq/store/dledger/MixCommitlogTest", + "src/test/java/org/apache/rocketmq/store/RocksDBMessageStoreTest", + ], + test_files = glob(["src/test/java/**/*Test.java"]), + deps = [ + ":tests", + ], +) diff --git a/store/pom.xml b/store/pom.xml index ca2eb1adfe1..ab6977b9daa 100644 --- a/store/pom.xml +++ b/store/pom.xml @@ -19,7 +19,7 @@ org.apache.rocketmq rocketmq-all - 4.2.0 + 5.2.0 4.0.0 @@ -27,24 +27,49 @@ rocketmq-store rocketmq-store ${project.version} + + ${basedir}/.. + + + + io.openmessaging.storage + dledger + + + org.apache.rocketmq + rocketmq-remoting + + + ${project.groupId} - rocketmq-common + rocketmq-remoting net.java.dev.jna jna - ch.qos.logback - logback-classic - test + com.conversantmedia + disruptor + + + com.google.guava + guava + + + commons-io + commons-io + + + + org.slf4j + slf4j-api - ch.qos.logback - logback-core - test + io.github.aliyunmq + rocketmq-shaded-slf4j-api-bridge diff --git a/store/src/main/java/org/apache/rocketmq/store/AllocateMappedFileService.java b/store/src/main/java/org/apache/rocketmq/store/AllocateMappedFileService.java index ad8e65dca40..c8420fea11f 100644 --- a/store/src/main/java/org/apache/rocketmq/store/AllocateMappedFileService.java +++ b/store/src/main/java/org/apache/rocketmq/store/AllocateMappedFileService.java @@ -27,9 +27,11 @@ import org.apache.rocketmq.common.ServiceThread; import org.apache.rocketmq.common.UtilAll; import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.store.config.BrokerRole; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.logfile.DefaultMappedFile; +import org.apache.rocketmq.store.logfile.MappedFile; /** * Create MappedFile in advance @@ -38,9 +40,9 @@ public class AllocateMappedFileService extends ServiceThread { private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); private static int waitTimeOut = 1000 * 5; private ConcurrentMap requestTable = - new ConcurrentHashMap(); + new ConcurrentHashMap<>(); private PriorityBlockingQueue requestQueue = - new PriorityBlockingQueue(); + new PriorityBlockingQueue<>(); private volatile boolean hasException = false; private DefaultMessageStore messageStore; @@ -50,10 +52,10 @@ public AllocateMappedFileService(DefaultMessageStore messageStore) { public MappedFile putRequestAndReturnMappedFile(String nextFilePath, String nextNextFilePath, int fileSize) { int canSubmitRequests = 2; - if (this.messageStore.getMessageStoreConfig().isTransientStorePoolEnable()) { + if (this.messageStore.isTransientStorePoolEnable()) { if (this.messageStore.getMessageStoreConfig().isFastFailIfNoBufferInStorePool() && BrokerRole.SLAVE != this.messageStore.getMessageStoreConfig().getBrokerRole()) { //if broker is slave, don't fast fail even no buffer in pool - canSubmitRequests = this.messageStore.getTransientStorePool().remainBufferNumbs() - this.requestQueue.size(); + canSubmitRequests = this.messageStore.remainTransientStoreBufferNumbs() - this.requestQueue.size(); } } @@ -63,7 +65,7 @@ public MappedFile putRequestAndReturnMappedFile(String nextFilePath, String next if (nextPutOK) { if (canSubmitRequests <= 0) { log.warn("[NOTIFYME]TransientStorePool is not enough, so create mapped file error, " + - "RequestQueueSize : {}, StorePoolSize: {}", this.requestQueue.size(), this.messageStore.getTransientStorePool().remainBufferNumbs()); + "RequestQueueSize : {}, StorePoolSize: {}", this.requestQueue.size(), this.messageStore.remainTransientStoreBufferNumbs()); this.requestTable.remove(nextFilePath); return null; } @@ -79,7 +81,7 @@ public MappedFile putRequestAndReturnMappedFile(String nextFilePath, String next if (nextNextPutOK) { if (canSubmitRequests <= 0) { log.warn("[NOTIFYME]TransientStorePool is not enough, so skip preallocate mapped file, " + - "RequestQueueSize : {}, StorePoolSize: {}", this.requestQueue.size(), this.messageStore.getTransientStorePool().remainBufferNumbs()); + "RequestQueueSize : {}, StorePoolSize: {}", this.requestQueue.size(), this.messageStore.remainTransientStoreBufferNumbs()); this.requestTable.remove(nextNextFilePath); } else { boolean offerOK = this.requestQueue.offer(nextNextReq); @@ -97,7 +99,9 @@ public MappedFile putRequestAndReturnMappedFile(String nextFilePath, String next AllocateRequest result = this.requestTable.get(nextFilePath); try { if (result != null) { + messageStore.getPerfCounter().startTick("WAIT_MAPFILE_TIME_MS"); boolean waitOK = result.getCountDownLatch().await(waitTimeOut, TimeUnit.MILLISECONDS); + messageStore.getPerfCounter().endTick("WAIT_MAPFILE_TIME_MS"); if (!waitOK) { log.warn("create mmap timeout " + result.getFilePath() + " " + result.getFileSize()); return null; @@ -117,19 +121,15 @@ public MappedFile putRequestAndReturnMappedFile(String nextFilePath, String next @Override public String getServiceName() { + if (messageStore != null && messageStore.getBrokerConfig().isInBrokerContainer()) { + return messageStore.getBrokerIdentity().getIdentifier() + AllocateMappedFileService.class.getSimpleName(); + } return AllocateMappedFileService.class.getSimpleName(); } + @Override public void shutdown() { - this.stopped = true; - this.thread.interrupt(); - - try { - this.thread.join(this.getJointime()); - } catch (InterruptedException e) { - log.error("Interrupted", e); - } - + super.shutdown(true); for (AllocateRequest req : this.requestTable.values()) { if (req.mappedFile != null) { log.info("delete pre allocated maped file, {}", req.mappedFile.getFileName()); @@ -171,28 +171,28 @@ private boolean mmapOperation() { long beginTime = System.currentTimeMillis(); MappedFile mappedFile; - if (messageStore.getMessageStoreConfig().isTransientStorePoolEnable()) { + if (messageStore.isTransientStorePoolEnable()) { try { mappedFile = ServiceLoader.load(MappedFile.class).iterator().next(); mappedFile.init(req.getFilePath(), req.getFileSize(), messageStore.getTransientStorePool()); } catch (RuntimeException e) { log.warn("Use default implementation."); - mappedFile = new MappedFile(req.getFilePath(), req.getFileSize(), messageStore.getTransientStorePool()); + mappedFile = new DefaultMappedFile(req.getFilePath(), req.getFileSize(), messageStore.getTransientStorePool()); } } else { - mappedFile = new MappedFile(req.getFilePath(), req.getFileSize()); + mappedFile = new DefaultMappedFile(req.getFilePath(), req.getFileSize()); } - long eclipseTime = UtilAll.computeEclipseTimeMilliseconds(beginTime); - if (eclipseTime > 10) { + long elapsedTime = UtilAll.computeElapsedTimeMilliseconds(beginTime); + if (elapsedTime > 10) { int queueSize = this.requestQueue.size(); - log.warn("create mappedFile spent time(ms) " + eclipseTime + " queue size " + queueSize + log.warn("create mappedFile spent time(ms) " + elapsedTime + " queue size " + queueSize + " " + req.getFilePath() + " " + req.getFileSize()); } // pre write mappedFile if (mappedFile.getFileSize() >= this.messageStore.getMessageStoreConfig() - .getMapedFileSizeCommitLog() + .getMappedFileSizeCommitLog() && this.messageStore.getMessageStoreConfig().isWarmMapedFileEnable()) { mappedFile.warmMappedFile(this.messageStore.getMessageStoreConfig().getFlushDiskType(), diff --git a/store/src/main/java/org/apache/rocketmq/store/AppendMessageCallback.java b/store/src/main/java/org/apache/rocketmq/store/AppendMessageCallback.java index d33763847f9..1cbccdf8776 100644 --- a/store/src/main/java/org/apache/rocketmq/store/AppendMessageCallback.java +++ b/store/src/main/java/org/apache/rocketmq/store/AppendMessageCallback.java @@ -18,6 +18,7 @@ import java.nio.ByteBuffer; import org.apache.rocketmq.common.message.MessageExtBatch; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; /** * Write messages callback interface @@ -25,19 +26,19 @@ public interface AppendMessageCallback { /** - * After message serialization, write MapedByteBuffer + * After message serialization, write MappedByteBuffer * * @return How many bytes to write */ AppendMessageResult doAppend(final long fileFromOffset, final ByteBuffer byteBuffer, - final int maxBlank, final MessageExtBrokerInner msg); + final int maxBlank, final MessageExtBrokerInner msg, PutMessageContext putMessageContext); /** - * After batched message serialization, write MapedByteBuffer + * After batched message serialization, write MappedByteBuffer * * @param messageExtBatch, backed up by a byte array * @return How many bytes to write */ AppendMessageResult doAppend(final long fileFromOffset, final ByteBuffer byteBuffer, - final int maxBlank, final MessageExtBatch messageExtBatch); + final int maxBlank, final MessageExtBatch messageExtBatch, PutMessageContext putMessageContext); } diff --git a/store/src/main/java/org/apache/rocketmq/store/AppendMessageResult.java b/store/src/main/java/org/apache/rocketmq/store/AppendMessageResult.java index d6d1aa6a31c..98bf203ad5d 100644 --- a/store/src/main/java/org/apache/rocketmq/store/AppendMessageResult.java +++ b/store/src/main/java/org/apache/rocketmq/store/AppendMessageResult.java @@ -16,6 +16,8 @@ */ package org.apache.rocketmq.store; +import java.util.function.Supplier; + /** * When write a message to the commit log, returns results */ @@ -28,6 +30,7 @@ public class AppendMessageResult { private int wroteBytes; // Message ID private String msgId; + private Supplier msgIdSupplier; // Message storage timestamp private long storeTimestamp; // Consume queue's offset(step by one) @@ -51,6 +54,36 @@ public AppendMessageResult(AppendMessageStatus status, long wroteOffset, int wro this.pagecacheRT = pagecacheRT; } + public AppendMessageResult(AppendMessageStatus status, long wroteOffset, int wroteBytes, long storeTimestamp) { + this.status = status; + this.wroteOffset = wroteOffset; + this.wroteBytes = wroteBytes; + this.storeTimestamp = storeTimestamp; + } + + public AppendMessageResult(AppendMessageStatus status, long wroteOffset, int wroteBytes, Supplier msgIdSupplier, + long storeTimestamp, long logicsOffset, long pagecacheRT) { + this.status = status; + this.wroteOffset = wroteOffset; + this.wroteBytes = wroteBytes; + this.msgIdSupplier = msgIdSupplier; + this.storeTimestamp = storeTimestamp; + this.logicsOffset = logicsOffset; + this.pagecacheRT = pagecacheRT; + } + + public AppendMessageResult(AppendMessageStatus status, long wroteOffset, int wroteBytes, Supplier msgIdSupplier, + long storeTimestamp, long logicsOffset, long pagecacheRT, int msgNum) { + this.status = status; + this.wroteOffset = wroteOffset; + this.wroteBytes = wroteBytes; + this.msgIdSupplier = msgIdSupplier; + this.storeTimestamp = storeTimestamp; + this.logicsOffset = logicsOffset; + this.pagecacheRT = pagecacheRT; + this.msgNum = msgNum; + } + public long getPagecacheRT() { return pagecacheRT; } @@ -88,6 +121,9 @@ public void setWroteBytes(int wroteBytes) { } public String getMsgId() { + if (msgId == null && msgIdSupplier != null) { + msgId = msgIdSupplier.get(); + } return msgId; } diff --git a/store/src/main/java/org/apache/rocketmq/store/CommitLog.java b/store/src/main/java/org/apache/rocketmq/store/CommitLog.java index 03d98d31925..cc29cca5d94 100644 --- a/store/src/main/java/org/apache/rocketmq/store/CommitLog.java +++ b/store/src/main/java/org/apache/rocketmq/store/CommitLog.java @@ -16,97 +16,175 @@ */ package org.apache.rocketmq.store; +import java.net.Inet6Address; +import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.HashMap; +import java.util.Collections; +import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.concurrent.CountDownLatch; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import com.sun.jna.NativeLong; +import com.sun.jna.Pointer; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.SystemClock; +import org.apache.rocketmq.common.TopicConfig; import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.attribute.CQType; import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.common.message.MessageAccessor; import org.apache.rocketmq.common.message.MessageConst; import org.apache.rocketmq.common.message.MessageDecoder; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageExtBatch; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.message.MessageVersion; import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.common.utils.QueueTypeUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.MessageExtEncoder.PutMessageThreadLocal; import org.apache.rocketmq.store.config.BrokerRole; import org.apache.rocketmq.store.config.FlushDiskType; +import org.apache.rocketmq.store.config.MessageStoreConfig; import org.apache.rocketmq.store.ha.HAService; -import org.apache.rocketmq.store.schedule.ScheduleMessageService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.ha.autoswitch.AutoSwitchHAService; +import org.apache.rocketmq.store.logfile.MappedFile; +import org.apache.rocketmq.store.util.LibC; +import org.rocksdb.RocksDBException; + +import sun.nio.ch.DirectBuffer; /** * Store all metadata downtime for recovery, data protection reliability */ -public class CommitLog { +public class CommitLog implements Swappable { // Message's MAGIC CODE daa320a7 - public final static int MESSAGE_MAGIC_CODE = 0xAABBCCDD ^ 1880681586 + 8; - private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + public final static int MESSAGE_MAGIC_CODE = -626843481; + protected static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); // End of file empty MAGIC CODE cbd43194 - private final static int BLANK_MAGIC_CODE = 0xBBCCDDEE ^ 1880681586 + 8; - private final MappedFileQueue mappedFileQueue; - private final DefaultMessageStore defaultMessageStore; - private final FlushCommitLogService flushCommitLogService; + public final static int BLANK_MAGIC_CODE = -875286124; + /** + * CRC32 Format: [PROPERTY_CRC32 + NAME_VALUE_SEPARATOR + 10-digit fixed-length string + PROPERTY_SEPARATOR] + */ + public static final int CRC32_RESERVED_LEN = MessageConst.PROPERTY_CRC32.length() + 1 + 10 + 1; + protected final MappedFileQueue mappedFileQueue; + protected final DefaultMessageStore defaultMessageStore; - //If TransientStorePool enabled, we must flush message to FileChannel at fixed periods - private final FlushCommitLogService commitLogService; + private final FlushManager flushManager; + private final ColdDataCheckService coldDataCheckService; private final AppendMessageCallback appendMessageCallback; - private final ThreadLocal batchEncoderThreadLocal; - private HashMap topicQueueTable = new HashMap(1024); - private volatile long confirmOffset = -1L; + private final ThreadLocal putMessageThreadLocal; + + protected volatile long confirmOffset = -1L; private volatile long beginTimeInLock = 0; - private final PutMessageLock putMessageLock; - public CommitLog(final DefaultMessageStore defaultMessageStore) { - this.mappedFileQueue = new MappedFileQueue(defaultMessageStore.getMessageStoreConfig().getStorePathCommitLog(), - defaultMessageStore.getMessageStoreConfig().getMapedFileSizeCommitLog(), defaultMessageStore.getAllocateMappedFileService()); - this.defaultMessageStore = defaultMessageStore; + protected final PutMessageLock putMessageLock; + + protected final TopicQueueLock topicQueueLock; + + private volatile Set fullStorePaths = Collections.emptySet(); + + private final FlushDiskWatcher flushDiskWatcher; + + protected int commitLogSize; - if (FlushDiskType.SYNC_FLUSH == defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) { - this.flushCommitLogService = new GroupCommitService(); + private final boolean enabledAppendPropCRC; + protected final MultiDispatch multiDispatch; + + public CommitLog(final DefaultMessageStore messageStore) { + String storePath = messageStore.getMessageStoreConfig().getStorePathCommitLog(); + if (storePath.contains(MixAll.MULTI_PATH_SPLITTER)) { + this.mappedFileQueue = new MultiPathMappedFileQueue(messageStore.getMessageStoreConfig(), + messageStore.getMessageStoreConfig().getMappedFileSizeCommitLog(), + messageStore.getAllocateMappedFileService(), this::getFullStorePaths); } else { - this.flushCommitLogService = new FlushRealTimeService(); + this.mappedFileQueue = new MappedFileQueue(storePath, + messageStore.getMessageStoreConfig().getMappedFileSizeCommitLog(), + messageStore.getAllocateMappedFileService()); } - this.commitLogService = new CommitRealTimeService(); + this.defaultMessageStore = messageStore; + + this.flushManager = new DefaultFlushManager(); + this.coldDataCheckService = new ColdDataCheckService(); - this.appendMessageCallback = new DefaultAppendMessageCallback(defaultMessageStore.getMessageStoreConfig().getMaxMessageSize()); - batchEncoderThreadLocal = new ThreadLocal() { + this.appendMessageCallback = new DefaultAppendMessageCallback(defaultMessageStore.getMessageStoreConfig()); + putMessageThreadLocal = new ThreadLocal() { @Override - protected MessageExtBatchEncoder initialValue() { - return new MessageExtBatchEncoder(defaultMessageStore.getMessageStoreConfig().getMaxMessageSize()); + protected PutMessageThreadLocal initialValue() { + return new PutMessageThreadLocal(defaultMessageStore.getMessageStoreConfig()); } }; - this.putMessageLock = defaultMessageStore.getMessageStoreConfig().isUseReentrantLockWhenPutMessage() ? new PutMessageReentrantLock() : new PutMessageSpinLock(); + this.putMessageLock = messageStore.getMessageStoreConfig().isUseReentrantLockWhenPutMessage() ? new PutMessageReentrantLock() : new PutMessageSpinLock(); + + this.flushDiskWatcher = new FlushDiskWatcher(); + + this.topicQueueLock = new TopicQueueLock(messageStore.getMessageStoreConfig().getTopicQueueLockNum()); + + this.commitLogSize = messageStore.getMessageStoreConfig().getMappedFileSizeCommitLog(); + + this.enabledAppendPropCRC = messageStore.getMessageStoreConfig().isEnabledAppendPropCRC(); + + this.multiDispatch = new MultiDispatch(defaultMessageStore); + } + public void setFullStorePaths(Set fullStorePaths) { + this.fullStorePaths = fullStorePaths; + } + + public Set getFullStorePaths() { + return fullStorePaths; + } + + public long getTotalSize() { + return this.mappedFileQueue.getTotalFileSize(); + } + + public ThreadLocal getPutMessageThreadLocal() { + return putMessageThreadLocal; } public boolean load() { boolean result = this.mappedFileQueue.load(); + if (result && !defaultMessageStore.getMessageStoreConfig().isDataReadAheadEnable()) { + scanFileAndSetReadMode(LibC.MADV_RANDOM); + } + this.mappedFileQueue.checkSelf(); log.info("load commit log " + (result ? "OK" : "Failed")); return result; } public void start() { - this.flushCommitLogService.start(); - - if (defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) { - this.commitLogService.start(); + this.flushManager.start(); + log.info("start commitLog successfully. storeRoot: {}", this.defaultMessageStore.getMessageStoreConfig().getStorePathRootDir()); + flushDiskWatcher.setDaemon(true); + flushDiskWatcher.start(); + if (this.coldDataCheckService != null) { + this.coldDataCheckService.start(); } } public void shutdown() { - if (defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) { - this.commitLogService.shutdown(); + this.flushManager.shutdown(); + log.info("shutdown commitLog successfully. storeRoot: {}", this.defaultMessageStore.getMessageStoreConfig().getStorePathRootDir()); + flushDiskWatcher.shutdown(true); + if (this.coldDataCheckService != null) { + this.coldDataCheckService.shutdown(); } - - this.flushCommitLogService.shutdown(); } public long flush() { @@ -115,6 +193,10 @@ public long flush() { return this.mappedFileQueue.getFlushedWhere(); } + public long getFlushedWhere() { + return this.mappedFileQueue.getFlushedWhere(); + } + public long getMaxOffset() { return this.mappedFileQueue.getMaxOffset(); } @@ -133,7 +215,17 @@ public int deleteExpiredFile( final long intervalForcibly, final boolean cleanImmediately ) { - return this.mappedFileQueue.deleteExpiredFileByTime(expiredTime, deleteFilesInterval, intervalForcibly, cleanImmediately); + return deleteExpiredFile(expiredTime, deleteFilesInterval, intervalForcibly, cleanImmediately, 0); + } + + public int deleteExpiredFile( + final long expiredTime, + final int deleteFilesInterval, + final long intervalForcibly, + final boolean cleanImmediately, + final int deleteFileBatchMax + ) { + return this.mappedFileQueue.deleteExpiredFileByTime(expiredTime, deleteFilesInterval, intervalForcibly, cleanImmediately, deleteFileBatchMax); } /** @@ -144,7 +236,7 @@ public SelectMappedBufferResult getData(final long offset) { } public SelectMappedBufferResult getData(final long offset, final boolean returnFirstOnNotFound) { - int mappedFileSize = this.defaultMessageStore.getMessageStoreConfig().getMapedFileSizeCommitLog(); + int mappedFileSize = this.defaultMessageStore.getMessageStoreConfig().getMappedFileSizeCommitLog(); MappedFile mappedFile = this.mappedFileQueue.findMappedFileByOffset(offset, returnFirstOnNotFound); if (mappedFile != null) { int pos = (int) (offset % mappedFileSize); @@ -155,33 +247,108 @@ public SelectMappedBufferResult getData(final long offset, final boolean returnF return null; } + public boolean getData(final long offset, final int size, final ByteBuffer byteBuffer) { + int mappedFileSize = this.defaultMessageStore.getMessageStoreConfig().getMappedFileSizeCommitLog(); + MappedFile mappedFile = this.mappedFileQueue.findMappedFileByOffset(offset, offset == 0); + if (mappedFile != null) { + int pos = (int) (offset % mappedFileSize); + return mappedFile.getData(pos, size, byteBuffer); + } + return false; + } + + public List getBulkData(final long offset, final int size) { + List bufferResultList = new ArrayList<>(); + + int mappedFileSize = this.defaultMessageStore.getMessageStoreConfig().getMappedFileSizeCommitLog(); + int remainSize = size; + long startOffset = offset; + long maxOffset = this.getMaxOffset(); + if (offset + size > maxOffset) { + remainSize = (int) (maxOffset - offset); + log.warn("get bulk data size out of range, correct to max offset. offset: {}, size: {}, max: {}", offset, remainSize, maxOffset); + } + + while (remainSize > 0) { + MappedFile mappedFile = this.mappedFileQueue.findMappedFileByOffset(startOffset, startOffset == 0); + if (mappedFile != null) { + int pos = (int) (startOffset % mappedFileSize); + int readableSize = mappedFile.getReadPosition() - pos; + int readSize = Math.min(remainSize, readableSize); + + SelectMappedBufferResult bufferResult = mappedFile.selectMappedBuffer(pos, readSize); + if (bufferResult == null) { + break; + } + bufferResultList.add(bufferResult); + remainSize -= readSize; + startOffset += readSize; + } + } + + return bufferResultList; + } + + public SelectMappedFileResult getFile(final long offset) { + int mappedFileSize = this.defaultMessageStore.getMessageStoreConfig().getMappedFileSizeCommitLog(); + MappedFile mappedFile = this.mappedFileQueue.findMappedFileByOffset(offset, offset == 0); + if (mappedFile != null) { + int size = (int) (mappedFile.getReadPosition() - offset % mappedFileSize); + if (size > 0) { + return new SelectMappedFileResult(size, mappedFile); + } + } + return null; + } + + //Create new mappedFile if not exits. + public boolean getLastMappedFile(final long startOffset) { + MappedFile lastMappedFile = this.mappedFileQueue.getLastMappedFile(startOffset); + if (null == lastMappedFile) { + log.error("getLastMappedFile error. offset:{}", startOffset); + return false; + } + + return true; + } + /** * When the normal exit, data recovery, all memory data have been flush + * + * @throws RocksDBException only in rocksdb mode */ - public void recoverNormally() { + public void recoverNormally(long maxPhyOffsetOfConsumeQueue) throws RocksDBException { boolean checkCRCOnRecover = this.defaultMessageStore.getMessageStoreConfig().isCheckCRCOnRecover(); + boolean checkDupInfo = this.defaultMessageStore.getMessageStoreConfig().isDuplicationEnable(); final List mappedFiles = this.mappedFileQueue.getMappedFiles(); if (!mappedFiles.isEmpty()) { // Began to recover from the last third file int index = mappedFiles.size() - 3; - if (index < 0) + if (index < 0) { index = 0; + } MappedFile mappedFile = mappedFiles.get(index); ByteBuffer byteBuffer = mappedFile.sliceByteBuffer(); long processOffset = mappedFile.getFileFromOffset(); long mappedFileOffset = 0; + long lastValidMsgPhyOffset = this.getConfirmOffset(); + // normal recover doesn't require dispatching + boolean doDispatch = false; while (true) { - DispatchRequest dispatchRequest = this.checkMessageAndReturnSize(byteBuffer, checkCRCOnRecover); + DispatchRequest dispatchRequest = this.checkMessageAndReturnSize(byteBuffer, checkCRCOnRecover, checkDupInfo); int size = dispatchRequest.getMsgSize(); // Normal data if (dispatchRequest.isSuccess() && size > 0) { + lastValidMsgPhyOffset = processOffset + mappedFileOffset; mappedFileOffset += size; + this.getMessageStore().onCommitLogDispatch(dispatchRequest, doDispatch, mappedFile, true, false); } // Come the end of the file, switch to the next file Since the // return 0 representatives met last hole, // this can not be included in truncate offset else if (dispatchRequest.isSuccess() && size == 0) { + this.getMessageStore().onCommitLogDispatch(dispatchRequest, doDispatch, mappedFile, true, true); index++; if (index >= mappedFiles.size()) { // Current branch can not happen @@ -197,27 +364,55 @@ else if (dispatchRequest.isSuccess() && size == 0) { } // Intermediate file read error else if (!dispatchRequest.isSuccess()) { + if (size > 0) { + log.warn("found a half message at {}, it will be truncated.", processOffset + mappedFileOffset); + } log.info("recover physics file end, " + mappedFile.getFileName()); break; } } processOffset += mappedFileOffset; + + if (this.defaultMessageStore.getBrokerConfig().isEnableControllerMode()) { + if (this.defaultMessageStore.getConfirmOffset() < this.defaultMessageStore.getMinPhyOffset()) { + log.error("confirmOffset {} is less than minPhyOffset {}, correct confirmOffset to minPhyOffset", this.defaultMessageStore.getConfirmOffset(), this.defaultMessageStore.getMinPhyOffset()); + this.defaultMessageStore.setConfirmOffset(this.defaultMessageStore.getMinPhyOffset()); + } else if (this.defaultMessageStore.getConfirmOffset() > processOffset) { + log.error("confirmOffset {} is larger than processOffset {}, correct confirmOffset to processOffset", this.defaultMessageStore.getConfirmOffset(), processOffset); + this.defaultMessageStore.setConfirmOffset(processOffset); + } + } else { + this.setConfirmOffset(lastValidMsgPhyOffset); + } + + // Clear ConsumeQueue redundant data + if (maxPhyOffsetOfConsumeQueue >= processOffset) { + log.warn("maxPhyOffsetOfConsumeQueue({}) >= processOffset({}), truncate dirty logic files", maxPhyOffsetOfConsumeQueue, processOffset); + this.defaultMessageStore.truncateDirtyLogicFiles(processOffset); + } + this.mappedFileQueue.setFlushedWhere(processOffset); this.mappedFileQueue.setCommittedWhere(processOffset); this.mappedFileQueue.truncateDirtyFiles(processOffset); + } else { + // Commitlog case files are deleted + log.warn("The commitlog files are deleted, and delete the consume queue files"); + this.mappedFileQueue.setFlushedWhere(0); + this.mappedFileQueue.setCommittedWhere(0); + this.defaultMessageStore.getQueueStore().destroy(); + this.defaultMessageStore.getQueueStore().loadAfterDestroy(); } } - public DispatchRequest checkMessageAndReturnSize(java.nio.ByteBuffer byteBuffer, final boolean checkCRC) { - return this.checkMessageAndReturnSize(byteBuffer, checkCRC, true); + public DispatchRequest checkMessageAndReturnSize(java.nio.ByteBuffer byteBuffer, final boolean checkCRC, + final boolean checkDupInfo) { + return this.checkMessageAndReturnSize(byteBuffer, checkCRC, checkDupInfo, true); } private void doNothingForDeadCode(final Object obj) { if (obj != null) { - if (log.isDebugEnabled()) { - log.debug(String.valueOf(obj.hashCode())); - } + log.debug(String.valueOf(obj.hashCode())); } } @@ -227,7 +422,7 @@ private void doNothingForDeadCode(final Object obj) { * @return 0 Come the end of the file // >0 Normal messages // -1 Message checksum failure */ public DispatchRequest checkMessageAndReturnSize(java.nio.ByteBuffer byteBuffer, final boolean checkCRC, - final boolean readBody) { + final boolean checkDupInfo, final boolean readBody) { try { // 1 TOTAL SIZE int totalSize = byteBuffer.getInt(); @@ -235,7 +430,8 @@ public DispatchRequest checkMessageAndReturnSize(java.nio.ByteBuffer byteBuffer, // 2 MAGIC CODE int magicCode = byteBuffer.getInt(); switch (magicCode) { - case MESSAGE_MAGIC_CODE: + case MessageDecoder.MESSAGE_MAGIC_CODE: + case MessageDecoder.MESSAGE_MAGIC_CODE_V2: break; case BLANK_MAGIC_CODE: return new DispatchRequest(0, true /* success */); @@ -244,6 +440,8 @@ public DispatchRequest checkMessageAndReturnSize(java.nio.ByteBuffer byteBuffer, return new DispatchRequest(-1, false /* success */); } + MessageVersion messageVersion = MessageVersion.valueOfMagicCode(magicCode); + byte[] bytesContent = new byte[totalSize]; int bodyCRC = byteBuffer.getInt(); @@ -260,11 +458,21 @@ public DispatchRequest checkMessageAndReturnSize(java.nio.ByteBuffer byteBuffer, long bornTimeStamp = byteBuffer.getLong(); - ByteBuffer byteBuffer1 = byteBuffer.get(bytesContent, 0, 8); + ByteBuffer byteBuffer1; + if ((sysFlag & MessageSysFlag.BORNHOST_V6_FLAG) == 0) { + byteBuffer1 = byteBuffer.get(bytesContent, 0, 4 + 4); + } else { + byteBuffer1 = byteBuffer.get(bytesContent, 0, 16 + 4); + } long storeTimestamp = byteBuffer.getLong(); - ByteBuffer byteBuffer2 = byteBuffer.get(bytesContent, 0, 8); + ByteBuffer byteBuffer2; + if ((sysFlag & MessageSysFlag.STOREHOSTADDRESS_V6_FLAG) == 0) { + byteBuffer2 = byteBuffer.get(bytesContent, 0, 4 + 4); + } else { + byteBuffer2 = byteBuffer.get(bytesContent, 0, 16 + 4); + } int reconsumeTimes = byteBuffer.getInt(); @@ -276,10 +484,16 @@ public DispatchRequest checkMessageAndReturnSize(java.nio.ByteBuffer byteBuffer, byteBuffer.get(bytesContent, 0, bodyLen); if (checkCRC) { - int crc = UtilAll.crc32(bytesContent, 0, bodyLen); - if (crc != bodyCRC) { - log.warn("CRC check failed. bodyCRC={}, currentCRC={}", crc, bodyCRC); - return new DispatchRequest(-1, false/* success */); + /** + * When the forceVerifyPropCRC = false, + * use original bodyCrc validation. + */ + if (!this.defaultMessageStore.getMessageStoreConfig().isForceVerifyPropCRC()) { + int crc = UtilAll.crc32(bytesContent, 0, bodyLen); + if (crc != bodyCRC) { + log.warn("CRC check failed. bodyCRC={}, currentCRC={}", crc, bodyCRC); + return new DispatchRequest(-1, false/* success */); + } } } } else { @@ -287,7 +501,7 @@ public DispatchRequest checkMessageAndReturnSize(java.nio.ByteBuffer byteBuffer, } } - byte topicLen = byteBuffer.get(); + int topicLen = messageVersion.getTopicLength(byteBuffer); byteBuffer.get(bytesContent, 0, topicLen); String topic = new String(bytesContent, 0, topicLen, MessageDecoder.CHARSET_UTF8); @@ -306,6 +520,14 @@ public DispatchRequest checkMessageAndReturnSize(java.nio.ByteBuffer byteBuffer, uniqKey = propertiesMap.get(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX); + if (checkDupInfo) { + String dupInfo = propertiesMap.get(MessageConst.DUP_INFO); + if (null == dupInfo || dupInfo.split("_").length != 2) { + log.warn("DupInfo in properties check failed. dupInfo={}", dupInfo); + return new DispatchRequest(-1, false); + } + } + String tags = propertiesMap.get(MessageConst.PROPERTY_TAGS); if (tags != null && tags.length() > 0) { tagsCode = MessageExtBrokerInner.tagsString2tagsCode(MessageExt.parseTopicFilterType(sysFlag), tags); @@ -314,22 +536,59 @@ public DispatchRequest checkMessageAndReturnSize(java.nio.ByteBuffer byteBuffer, // Timing message processing { String t = propertiesMap.get(MessageConst.PROPERTY_DELAY_TIME_LEVEL); - if (ScheduleMessageService.SCHEDULE_TOPIC.equals(topic) && t != null) { + if (TopicValidator.RMQ_SYS_SCHEDULE_TOPIC.equals(topic) && t != null) { int delayLevel = Integer.parseInt(t); - if (delayLevel > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) { - delayLevel = this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel(); + if (delayLevel > this.defaultMessageStore.getMaxDelayLevel()) { + delayLevel = this.defaultMessageStore.getMaxDelayLevel(); } if (delayLevel > 0) { - tagsCode = this.defaultMessageStore.getScheduleMessageService().computeDeliverTimestamp(delayLevel, + tagsCode = this.defaultMessageStore.computeDeliverTimestamp(delayLevel, storeTimestamp); } } } } - int readLength = calMsgLength(bodyLen, topicLen, propertiesLength); + if (checkCRC) { + /** + * When the forceVerifyPropCRC = true, + * Crc verification needs to be performed on the entire message data (excluding the length reserved at the tail) + */ + if (this.defaultMessageStore.getMessageStoreConfig().isForceVerifyPropCRC()) { + int expectedCRC = -1; + if (propertiesMap != null) { + String crc32Str = propertiesMap.get(MessageConst.PROPERTY_CRC32); + if (crc32Str != null) { + expectedCRC = 0; + for (int i = crc32Str.length() - 1; i >= 0; i--) { + int num = crc32Str.charAt(i) - '0'; + expectedCRC *= 10; + expectedCRC += num; + } + } + } + if (expectedCRC > 0) { + ByteBuffer tmpBuffer = byteBuffer.duplicate(); + tmpBuffer.position(tmpBuffer.position() - totalSize); + tmpBuffer.limit(tmpBuffer.position() + totalSize - CommitLog.CRC32_RESERVED_LEN); + int crc = UtilAll.crc32(tmpBuffer); + if (crc != expectedCRC) { + log.warn( + "CommitLog#checkAndDispatchMessage: failed to check message CRC, expected " + + "CRC={}, actual CRC={}", bodyCRC, crc); + return new DispatchRequest(-1, false/* success */); + } + } else { + log.warn( + "CommitLog#checkAndDispatchMessage: failed to check message CRC, not found CRC in properties"); + return new DispatchRequest(-1, false/* success */); + } + } + } + + int readLength = MessageExtEncoder.calMsgLength(messageVersion, sysFlag, bodyLen, topicLen, propertiesLength); if (totalSize != readLength) { doNothingForDeadCode(reconsumeTimes); doNothingForDeadCode(flag); @@ -342,7 +601,7 @@ public DispatchRequest checkMessageAndReturnSize(java.nio.ByteBuffer byteBuffer, return new DispatchRequest(totalSize, false/* success */); } - return new DispatchRequest( + DispatchRequest dispatchRequest = new DispatchRequest( topic, queueId, physicOffset, @@ -356,45 +615,86 @@ public DispatchRequest checkMessageAndReturnSize(java.nio.ByteBuffer byteBuffer, preparedTransactionOffset, propertiesMap ); + + setBatchSizeIfNeeded(propertiesMap, dispatchRequest); + + return dispatchRequest; } catch (Exception e) { } return new DispatchRequest(-1, false /* success */); } - private static int calMsgLength(int bodyLength, int topicLength, int propertiesLength) { - final int msgLen = 4 //TOTALSIZE - + 4 //MAGICCODE - + 4 //BODYCRC - + 4 //QUEUEID - + 4 //FLAG - + 8 //QUEUEOFFSET - + 8 //PHYSICALOFFSET - + 4 //SYSFLAG - + 8 //BORNTIMESTAMP - + 8 //BORNHOST - + 8 //STORETIMESTAMP - + 8 //STOREHOSTADDRESS - + 4 //RECONSUMETIMES - + 8 //Prepared Transaction Offset - + 4 + (bodyLength > 0 ? bodyLength : 0) //BODY - + 1 + topicLength //TOPIC - + 2 + (propertiesLength > 0 ? propertiesLength : 0) //propertiesLength - + 0; - return msgLen; + private void setBatchSizeIfNeeded(Map propertiesMap, DispatchRequest dispatchRequest) { + if (null != propertiesMap && propertiesMap.containsKey(MessageConst.PROPERTY_INNER_NUM) && propertiesMap.containsKey(MessageConst.PROPERTY_INNER_BASE)) { + dispatchRequest.setMsgBaseOffset(Long.parseLong(propertiesMap.get(MessageConst.PROPERTY_INNER_BASE))); + dispatchRequest.setBatchSize(Short.parseShort(propertiesMap.get(MessageConst.PROPERTY_INNER_NUM))); + } } + // Fetch and compute the newest confirmOffset. + // Even if it is just inited. public long getConfirmOffset() { - return this.confirmOffset; + if (this.defaultMessageStore.getBrokerConfig().isEnableControllerMode()) { + if (this.defaultMessageStore.getMessageStoreConfig().getBrokerRole() != BrokerRole.SLAVE && !this.defaultMessageStore.getRunningFlags().isFenced()) { + if (((AutoSwitchHAService) this.defaultMessageStore.getHaService()).getLocalSyncStateSet().size() == 1 + || !this.defaultMessageStore.getMessageStoreConfig().isAllAckInSyncStateSet()) { + return this.defaultMessageStore.getMaxPhyOffset(); + } + // First time it will compute the confirmOffset. + if (this.confirmOffset < 0) { + setConfirmOffset(((AutoSwitchHAService) this.defaultMessageStore.getHaService()).computeConfirmOffset()); + log.info("Init the confirmOffset to {}.", this.confirmOffset); + } + } + return this.confirmOffset; + } else if (this.defaultMessageStore.getMessageStoreConfig().isDuplicationEnable()) { + return this.confirmOffset; + } else { + return getMaxOffset(); + } + } + + // Fetch the original confirmOffset's value. + // Without checking and re-computing. + public long getConfirmOffsetDirectly() { + if (this.defaultMessageStore.getBrokerConfig().isEnableControllerMode()) { + if (this.defaultMessageStore.getMessageStoreConfig().getBrokerRole() != BrokerRole.SLAVE && !this.defaultMessageStore.getRunningFlags().isFenced()) { + if (((AutoSwitchHAService) this.defaultMessageStore.getHaService()).getLocalSyncStateSet().size() == 1) { + return this.defaultMessageStore.getMaxPhyOffset(); + } + } + return this.confirmOffset; + } else if (this.defaultMessageStore.getMessageStoreConfig().isDuplicationEnable()) { + return this.confirmOffset; + } else { + return getMaxOffset(); + } } public void setConfirmOffset(long phyOffset) { this.confirmOffset = phyOffset; + this.defaultMessageStore.getStoreCheckpoint().setConfirmPhyOffset(confirmOffset); + } + + public long getLastFileFromOffset() { + MappedFile lastMappedFile = this.mappedFileQueue.getLastMappedFile(); + if (lastMappedFile != null) { + if (lastMappedFile.isAvailable()) { + return lastMappedFile.getFileFromOffset(); + } + } + + return -1; } - public void recoverAbnormally() { + /** + * @throws RocksDBException only in rocksdb mode + */ + public void recoverAbnormally(long maxPhyOffsetOfConsumeQueue) throws RocksDBException { // recover by the minimum time stamp boolean checkCRCOnRecover = this.defaultMessageStore.getMessageStoreConfig().isCheckCRCOnRecover(); + boolean checkDupInfo = this.defaultMessageStore.getMessageStoreConfig().isDuplicationEnable(); final List mappedFiles = this.mappedFileQueue.getMappedFiles(); if (!mappedFiles.isEmpty()) { // Looking beginning to recover from which file @@ -416,100 +716,159 @@ public void recoverAbnormally() { ByteBuffer byteBuffer = mappedFile.sliceByteBuffer(); long processOffset = mappedFile.getFileFromOffset(); long mappedFileOffset = 0; + long lastValidMsgPhyOffset = processOffset; + long lastConfirmValidMsgPhyOffset = processOffset; + // abnormal recover require dispatching + boolean doDispatch = true; while (true) { - DispatchRequest dispatchRequest = this.checkMessageAndReturnSize(byteBuffer, checkCRCOnRecover); + DispatchRequest dispatchRequest = this.checkMessageAndReturnSize(byteBuffer, checkCRCOnRecover, checkDupInfo); int size = dispatchRequest.getMsgSize(); - // Normal data - if (size > 0) { - mappedFileOffset += size; + if (dispatchRequest.isSuccess()) { + // Normal data + if (size > 0) { + lastValidMsgPhyOffset = processOffset + mappedFileOffset; + mappedFileOffset += size; - if (this.defaultMessageStore.getMessageStoreConfig().isDuplicationEnable()) { - if (dispatchRequest.getCommitLogOffset() < this.defaultMessageStore.getConfirmOffset()) { - this.defaultMessageStore.doDispatch(dispatchRequest); + if (this.defaultMessageStore.getMessageStoreConfig().isDuplicationEnable() || this.defaultMessageStore.getBrokerConfig().isEnableControllerMode()) { + if (dispatchRequest.getCommitLogOffset() + size <= this.defaultMessageStore.getCommitLog().getConfirmOffset()) { + this.getMessageStore().onCommitLogDispatch(dispatchRequest, doDispatch, mappedFile, true, false); + lastConfirmValidMsgPhyOffset = dispatchRequest.getCommitLogOffset() + size; + } + } else { + this.getMessageStore().onCommitLogDispatch(dispatchRequest, doDispatch, mappedFile, true, false); } - } else { - this.defaultMessageStore.doDispatch(dispatchRequest); } - } - // Intermediate file read error - else if (size == -1) { - log.info("recover physics file end, " + mappedFile.getFileName()); - break; - } - // Come the end of the file, switch to the next file - // Since the return 0 representatives met last hole, this can - // not be included in truncate offset - else if (size == 0) { - index++; - if (index >= mappedFiles.size()) { - // The current branch under normal circumstances should - // not happen - log.info("recover physics file over, last mapped file " + mappedFile.getFileName()); - break; - } else { - mappedFile = mappedFiles.get(index); - byteBuffer = mappedFile.sliceByteBuffer(); - processOffset = mappedFile.getFileFromOffset(); - mappedFileOffset = 0; - log.info("recover next physics file, " + mappedFile.getFileName()); + // Come the end of the file, switch to the next file + // Since the return 0 representatives met last hole, this can + // not be included in truncate offset + else if (size == 0) { + this.getMessageStore().onCommitLogDispatch(dispatchRequest, doDispatch, mappedFile, true, true); + index++; + if (index >= mappedFiles.size()) { + // The current branch under normal circumstances should + // not happen + log.info("recover physics file over, last mapped file " + mappedFile.getFileName()); + break; + } else { + mappedFile = mappedFiles.get(index); + byteBuffer = mappedFile.sliceByteBuffer(); + processOffset = mappedFile.getFileFromOffset(); + mappedFileOffset = 0; + log.info("recover next physics file, " + mappedFile.getFileName()); + } + } + } else { + + if (size > 0) { + log.warn("found a half message at {}, it will be truncated.", processOffset + mappedFileOffset); } + + log.info("recover physics file end, " + mappedFile.getFileName() + " pos=" + byteBuffer.position()); + break; } } + // only for rocksdb mode + this.getMessageStore().finishCommitLogDispatch(); + processOffset += mappedFileOffset; + if (this.defaultMessageStore.getBrokerConfig().isEnableControllerMode()) { + if (this.defaultMessageStore.getConfirmOffset() < this.defaultMessageStore.getMinPhyOffset()) { + log.error("confirmOffset {} is less than minPhyOffset {}, correct confirmOffset to minPhyOffset", this.defaultMessageStore.getConfirmOffset(), this.defaultMessageStore.getMinPhyOffset()); + this.defaultMessageStore.setConfirmOffset(this.defaultMessageStore.getMinPhyOffset()); + } else if (this.defaultMessageStore.getConfirmOffset() > lastConfirmValidMsgPhyOffset) { + log.error("confirmOffset {} is larger than lastConfirmValidMsgPhyOffset {}, correct confirmOffset to lastConfirmValidMsgPhyOffset", this.defaultMessageStore.getConfirmOffset(), lastConfirmValidMsgPhyOffset); + this.defaultMessageStore.setConfirmOffset(lastConfirmValidMsgPhyOffset); + } + } else { + this.setConfirmOffset(lastValidMsgPhyOffset); + } + + // Clear ConsumeQueue redundant data + if (maxPhyOffsetOfConsumeQueue >= processOffset) { + log.warn("maxPhyOffsetOfConsumeQueue({}) >= processOffset({}), truncate dirty logic files", maxPhyOffsetOfConsumeQueue, processOffset); + this.defaultMessageStore.truncateDirtyLogicFiles(processOffset); + } + this.mappedFileQueue.setFlushedWhere(processOffset); this.mappedFileQueue.setCommittedWhere(processOffset); this.mappedFileQueue.truncateDirtyFiles(processOffset); - - // Clear ConsumeQueue redundant data - this.defaultMessageStore.truncateDirtyLogicFiles(processOffset); } // Commitlog case files are deleted else { + log.warn("The commitlog files are deleted, and delete the consume queue files"); this.mappedFileQueue.setFlushedWhere(0); this.mappedFileQueue.setCommittedWhere(0); - this.defaultMessageStore.destroyLogics(); + this.defaultMessageStore.getQueueStore().destroy(); + this.defaultMessageStore.getQueueStore().loadAfterDestroy(); } } - private boolean isMappedFileMatchedRecover(final MappedFile mappedFile) { - ByteBuffer byteBuffer = mappedFile.sliceByteBuffer(); + public void truncateDirtyFiles(long phyOffset) { + if (phyOffset <= this.getFlushedWhere()) { + this.mappedFileQueue.setFlushedWhere(phyOffset); + } - int magicCode = byteBuffer.getInt(MessageDecoder.MESSAGE_MAGIC_CODE_POSTION); - if (magicCode != MESSAGE_MAGIC_CODE) { - return false; + if (phyOffset <= this.mappedFileQueue.getCommittedWhere()) { + this.mappedFileQueue.setCommittedWhere(phyOffset); + } + + this.mappedFileQueue.truncateDirtyFiles(phyOffset); + if (this.confirmOffset > phyOffset) { + this.setConfirmOffset(phyOffset); } + } + + protected void onCommitLogAppend(MessageExtBrokerInner msg, AppendMessageResult result, MappedFile commitLogFile) { + this.getMessageStore().onCommitLogAppend(msg, result, commitLogFile); + } + + private boolean isMappedFileMatchedRecover(final MappedFile mappedFile) throws RocksDBException { + ByteBuffer byteBuffer = mappedFile.sliceByteBuffer(); - long storeTimestamp = byteBuffer.getLong(MessageDecoder.MESSAGE_STORE_TIMESTAMP_POSTION); - if (0 == storeTimestamp) { + int magicCode = byteBuffer.getInt(MessageDecoder.MESSAGE_MAGIC_CODE_POSITION); + if (magicCode != MessageDecoder.MESSAGE_MAGIC_CODE && magicCode != MessageDecoder.MESSAGE_MAGIC_CODE_V2) { return false; } - if (this.defaultMessageStore.getMessageStoreConfig().isMessageIndexEnable() - && this.defaultMessageStore.getMessageStoreConfig().isMessageIndexSafe()) { - if (storeTimestamp <= this.defaultMessageStore.getStoreCheckpoint().getMinTimestampIndex()) { - log.info("find check timestamp, {} {}", - storeTimestamp, - UtilAll.timeMillisToHumanString(storeTimestamp)); + if (this.defaultMessageStore.getMessageStoreConfig().isEnableRocksDBStore()) { + final long maxPhyOffsetInConsumeQueue = this.defaultMessageStore.getQueueStore().getMaxPhyOffsetInConsumeQueue(); + long phyOffset = byteBuffer.getLong(MessageDecoder.MESSAGE_PHYSIC_OFFSET_POSITION); + if (phyOffset <= maxPhyOffsetInConsumeQueue) { + log.info("find check. beginPhyOffset: {}, maxPhyOffsetInConsumeQueue: {}", phyOffset, maxPhyOffsetInConsumeQueue); return true; } } else { - if (storeTimestamp <= this.defaultMessageStore.getStoreCheckpoint().getMinTimestamp()) { - log.info("find check timestamp, {} {}", - storeTimestamp, - UtilAll.timeMillisToHumanString(storeTimestamp)); - return true; + int sysFlag = byteBuffer.getInt(MessageDecoder.SYSFLAG_POSITION); + int bornHostLength = (sysFlag & MessageSysFlag.BORNHOST_V6_FLAG) == 0 ? 8 : 20; + int msgStoreTimePos = 4 + 4 + 4 + 4 + 4 + 8 + 8 + 4 + 8 + bornHostLength; + long storeTimestamp = byteBuffer.getLong(msgStoreTimePos); + if (0 == storeTimestamp) { + return false; + } + + if (this.defaultMessageStore.getMessageStoreConfig().isMessageIndexEnable() + && this.defaultMessageStore.getMessageStoreConfig().isMessageIndexSafe()) { + if (storeTimestamp <= this.defaultMessageStore.getStoreCheckpoint().getMinTimestampIndex()) { + log.info("find check timestamp, {} {}", + storeTimestamp, + UtilAll.timeMillisToHumanString(storeTimestamp)); + return true; + } + } else { + if (storeTimestamp <= this.defaultMessageStore.getStoreCheckpoint().getMinTimestamp()) { + log.info("find check timestamp, {} {}", + storeTimestamp, + UtilAll.timeMillisToHumanString(storeTimestamp)); + return true; + } } } return false; } - private void notifyMessageArriving() { - - } - public boolean resetOffset(long offset) { return this.mappedFileQueue.resetOffset(offset); } @@ -518,100 +877,190 @@ public long getBeginTimeInLock() { return beginTimeInLock; } - public PutMessageResult putMessage(final MessageExtBrokerInner msg) { + public String generateKey(StringBuilder keyBuilder, MessageExt messageExt) { + keyBuilder.setLength(0); + keyBuilder.append(messageExt.getTopic()); + keyBuilder.append('-'); + keyBuilder.append(messageExt.getQueueId()); + return keyBuilder.toString(); + } + + public void setMappedFileQueueOffset(final long phyOffset) { + this.mappedFileQueue.setFlushedWhere(phyOffset); + this.mappedFileQueue.setCommittedWhere(phyOffset); + } + + public void updateMaxMessageSize(PutMessageThreadLocal putMessageThreadLocal) { + // dynamically adjust maxMessageSize, but not support DLedger mode temporarily + int newMaxMessageSize = this.defaultMessageStore.getMessageStoreConfig().getMaxMessageSize(); + if (newMaxMessageSize >= 10 && + putMessageThreadLocal.getEncoder().getMaxMessageBodySize() != newMaxMessageSize) { + putMessageThreadLocal.getEncoder().updateEncoderBufferCapacity(newMaxMessageSize); + } + } + + public CompletableFuture asyncPutMessage(final MessageExtBrokerInner msg) { // Set the storage time - msg.setStoreTimestamp(System.currentTimeMillis()); - // Set the message body BODY CRC (consider the most appropriate setting - // on the client) + if (!defaultMessageStore.getMessageStoreConfig().isDuplicationEnable()) { + msg.setStoreTimestamp(System.currentTimeMillis()); + } + // Set the message body CRC (consider the most appropriate setting on the client) msg.setBodyCRC(UtilAll.crc32(msg.getBody())); + if (enabledAppendPropCRC) { + // delete crc32 properties if exist + msg.deleteProperty(MessageConst.PROPERTY_CRC32); + } // Back to Results AppendMessageResult result = null; StoreStatsService storeStatsService = this.defaultMessageStore.getStoreStatsService(); String topic = msg.getTopic(); - int queueId = msg.getQueueId(); - - final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag()); - if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE - || tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) { - // Delay Delivery - if (msg.getDelayTimeLevel() > 0) { - if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) { - msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()); - } - - topic = ScheduleMessageService.SCHEDULE_TOPIC; - queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel()); + msg.setVersion(MessageVersion.MESSAGE_VERSION_V1); + boolean autoMessageVersionOnTopicLen = + this.defaultMessageStore.getMessageStoreConfig().isAutoMessageVersionOnTopicLen(); + if (autoMessageVersionOnTopicLen && topic.length() > Byte.MAX_VALUE) { + msg.setVersion(MessageVersion.MESSAGE_VERSION_V2); + } - // Backup real topic, queueId - MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic()); - MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId())); - msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); + InetSocketAddress bornSocketAddress = (InetSocketAddress) msg.getBornHost(); + if (bornSocketAddress.getAddress() instanceof Inet6Address) { + msg.setBornHostV6Flag(); + } - msg.setTopic(topic); - msg.setQueueId(queueId); - } + InetSocketAddress storeSocketAddress = (InetSocketAddress) msg.getStoreHost(); + if (storeSocketAddress.getAddress() instanceof Inet6Address) { + msg.setStoreHostAddressV6Flag(); } - long eclipseTimeInLock = 0; + PutMessageThreadLocal putMessageThreadLocal = this.putMessageThreadLocal.get(); + updateMaxMessageSize(putMessageThreadLocal); + String topicQueueKey = generateKey(putMessageThreadLocal.getKeyBuilder(), msg); + long elapsedTimeInLock = 0; MappedFile unlockMappedFile = null; MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(); - putMessageLock.lock(); //spin or ReentrantLock ,depending on store config - try { - long beginLockTimestamp = this.defaultMessageStore.getSystemClock().now(); - this.beginTimeInLock = beginLockTimestamp; + long currOffset; + if (mappedFile == null) { + currOffset = 0; + } else { + currOffset = mappedFile.getFileFromOffset() + mappedFile.getWrotePosition(); + } - // Here settings are stored timestamp, in order to ensure an orderly - // global - msg.setStoreTimestamp(beginLockTimestamp); + int needAckNums = this.defaultMessageStore.getMessageStoreConfig().getInSyncReplicas(); + boolean needHandleHA = needHandleHA(msg); - if (null == mappedFile || mappedFile.isFull()) { - mappedFile = this.mappedFileQueue.getLastMappedFile(0); // Mark: NewFile may be cause noise + if (needHandleHA && this.defaultMessageStore.getBrokerConfig().isEnableControllerMode()) { + if (this.defaultMessageStore.getHaService().inSyncReplicasNums(currOffset) < this.defaultMessageStore.getMessageStoreConfig().getMinInSyncReplicas()) { + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.IN_SYNC_REPLICAS_NOT_ENOUGH, null)); } - if (null == mappedFile) { - log.error("create mapped file1 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString()); - beginTimeInLock = 0; - return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, null); + if (this.defaultMessageStore.getMessageStoreConfig().isAllAckInSyncStateSet()) { + // -1 means all ack in SyncStateSet + needAckNums = MixAll.ALL_ACK_IN_SYNC_STATE_SET; + } + } else if (needHandleHA && this.defaultMessageStore.getBrokerConfig().isEnableSlaveActingMaster()) { + int inSyncReplicas = Math.min(this.defaultMessageStore.getAliveReplicaNumInGroup(), + this.defaultMessageStore.getHaService().inSyncReplicasNums(currOffset)); + needAckNums = calcNeedAckNums(inSyncReplicas); + if (needAckNums > inSyncReplicas) { + // Tell the producer, don't have enough slaves to handle the send request + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.IN_SYNC_REPLICAS_NOT_ENOUGH, null)); } + } - result = mappedFile.appendMessage(msg, this.appendMessageCallback); - switch (result.getStatus()) { - case PUT_OK: - break; - case END_OF_FILE: - unlockMappedFile = mappedFile; - // Create a new file, re-write the message - mappedFile = this.mappedFileQueue.getLastMappedFile(0); - if (null == mappedFile) { - // XXX: warn and notify me - log.error("create mapped file2 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString()); - beginTimeInLock = 0; - return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, result); + topicQueueLock.lock(topicQueueKey); + try { + + boolean needAssignOffset = true; + if (defaultMessageStore.getMessageStoreConfig().isDuplicationEnable() + && defaultMessageStore.getMessageStoreConfig().getBrokerRole() != BrokerRole.SLAVE) { + needAssignOffset = false; + } + if (needAssignOffset) { + defaultMessageStore.assignOffset(msg); + } + + PutMessageResult encodeResult = putMessageThreadLocal.getEncoder().encode(msg); + if (encodeResult != null) { + return CompletableFuture.completedFuture(encodeResult); + } + msg.setEncodedBuff(putMessageThreadLocal.getEncoder().getEncoderBuffer()); + PutMessageContext putMessageContext = new PutMessageContext(topicQueueKey); + + putMessageLock.lock(); //spin or ReentrantLock ,depending on store config + try { + long beginLockTimestamp = this.defaultMessageStore.getSystemClock().now(); + this.beginTimeInLock = beginLockTimestamp; + + // Here settings are stored timestamp, in order to ensure an orderly + // global + if (!defaultMessageStore.getMessageStoreConfig().isDuplicationEnable()) { + msg.setStoreTimestamp(beginLockTimestamp); + } + + if (null == mappedFile || mappedFile.isFull()) { + mappedFile = this.mappedFileQueue.getLastMappedFile(0); // Mark: NewFile may be cause noise + if (isCloseReadAhead()) { + setFileReadMode(mappedFile, LibC.MADV_RANDOM); } - result = mappedFile.appendMessage(msg, this.appendMessageCallback); - break; - case MESSAGE_SIZE_EXCEEDED: - case PROPERTIES_SIZE_EXCEEDED: - beginTimeInLock = 0; - return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, result); - case UNKNOWN_ERROR: - beginTimeInLock = 0; - return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result); - default: + } + if (null == mappedFile) { + log.error("create mapped file1 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString()); beginTimeInLock = 0; - return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result); - } + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.CREATE_MAPPED_FILE_FAILED, null)); + } + + result = mappedFile.appendMessage(msg, this.appendMessageCallback, putMessageContext); + switch (result.getStatus()) { + case PUT_OK: + onCommitLogAppend(msg, result, mappedFile); + break; + case END_OF_FILE: + onCommitLogAppend(msg, result, mappedFile); + unlockMappedFile = mappedFile; + // Create a new file, re-write the message + mappedFile = this.mappedFileQueue.getLastMappedFile(0); + if (null == mappedFile) { + // XXX: warn and notify me + log.error("create mapped file2 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString()); + beginTimeInLock = 0; + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.CREATE_MAPPED_FILE_FAILED, result)); + } + if (isCloseReadAhead()) { + setFileReadMode(mappedFile, LibC.MADV_RANDOM); + } + result = mappedFile.appendMessage(msg, this.appendMessageCallback, putMessageContext); + if (AppendMessageStatus.PUT_OK.equals(result.getStatus())) { + onCommitLogAppend(msg, result, mappedFile); + } + break; + case MESSAGE_SIZE_EXCEEDED: + case PROPERTIES_SIZE_EXCEEDED: + beginTimeInLock = 0; + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, result)); + case UNKNOWN_ERROR: + default: + beginTimeInLock = 0; + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result)); + } - eclipseTimeInLock = this.defaultMessageStore.getSystemClock().now() - beginLockTimestamp; - beginTimeInLock = 0; + elapsedTimeInLock = this.defaultMessageStore.getSystemClock().now() - beginLockTimestamp; + beginTimeInLock = 0; + } finally { + putMessageLock.unlock(); + } + // Increase queue offset when messages are successfully written + if (AppendMessageStatus.PUT_OK.equals(result.getStatus())) { + this.defaultMessageStore.increaseOffset(msg, getMessageNum(msg)); + } + } catch (RocksDBException e) { + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result)); } finally { - putMessageLock.unlock(); + topicQueueLock.unlock(topicQueueKey); } - if (eclipseTimeInLock > 500) { - log.warn("[NOTIFYME]putMessage in lock cost time(ms)={}, bodyLength={} AppendMessageResult={}", eclipseTimeInLock, msg.getBody().length, result); + if (elapsedTimeInLock > 500) { + log.warn("[NOTIFYME]putMessage in lock cost time(ms)={}, bodyLength={} AppendMessageResult={}", elapsedTimeInLock, msg.getBody().length, result); } if (null != unlockMappedFile && this.defaultMessageStore.getMessageStoreConfig().isWarmMapedFileEnable()) { @@ -621,147 +1070,158 @@ public PutMessageResult putMessage(final MessageExtBrokerInner msg) { PutMessageResult putMessageResult = new PutMessageResult(PutMessageStatus.PUT_OK, result); // Statistics - storeStatsService.getSinglePutMessageTopicTimesTotal(msg.getTopic()).incrementAndGet(); - storeStatsService.getSinglePutMessageTopicSizeTotal(topic).addAndGet(result.getWroteBytes()); - - handleDiskFlush(result, putMessageResult, msg); - handleHA(result, putMessageResult, msg); - - return putMessageResult; - } - - public void handleDiskFlush(AppendMessageResult result, PutMessageResult putMessageResult, MessageExt messageExt) { - // Synchronization flush - if (FlushDiskType.SYNC_FLUSH == this.defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) { - final GroupCommitService service = (GroupCommitService) this.flushCommitLogService; - if (messageExt.isWaitStoreMsgOK()) { - GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes()); - service.putRequest(request); - boolean flushOK = request.waitForFlush(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout()); - if (!flushOK) { - log.error("do groupcommit, wait for flush failed, topic: " + messageExt.getTopic() + " tags: " + messageExt.getTags() - + " client address: " + messageExt.getBornHostString()); - putMessageResult.setPutMessageStatus(PutMessageStatus.FLUSH_DISK_TIMEOUT); - } - } else { - service.wakeup(); - } - } - // Asynchronous flush - else { - if (!this.defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) { - flushCommitLogService.wakeup(); - } else { - commitLogService.wakeup(); - } - } - } - - public void handleHA(AppendMessageResult result, PutMessageResult putMessageResult, MessageExt messageExt) { - if (BrokerRole.SYNC_MASTER == this.defaultMessageStore.getMessageStoreConfig().getBrokerRole()) { - HAService service = this.defaultMessageStore.getHaService(); - if (messageExt.isWaitStoreMsgOK()) { - // Determine whether to wait - if (service.isSlaveOK(result.getWroteOffset() + result.getWroteBytes())) { - GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes()); - service.putRequest(request); - service.getWaitNotifyObject().wakeupAll(); - boolean flushOK = - request.waitForFlush(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout()); - if (!flushOK) { - log.error("do sync transfer other node, wait return, but failed, topic: " + messageExt.getTopic() + " tags: " - + messageExt.getTags() + " client address: " + messageExt.getBornHostNameString()); - putMessageResult.setPutMessageStatus(PutMessageStatus.FLUSH_SLAVE_TIMEOUT); - } - } - // Slave problem - else { - // Tell the producer, slave not available - putMessageResult.setPutMessageStatus(PutMessageStatus.SLAVE_NOT_AVAILABLE); - } - } - } + storeStatsService.getSinglePutMessageTopicTimesTotal(msg.getTopic()).add(result.getMsgNum()); + storeStatsService.getSinglePutMessageTopicSizeTotal(topic).add(result.getWroteBytes()); + return handleDiskFlushAndHA(putMessageResult, msg, needAckNums, needHandleHA); } - public PutMessageResult putMessages(final MessageExtBatch messageExtBatch) { + public CompletableFuture asyncPutMessages(final MessageExtBatch messageExtBatch) { messageExtBatch.setStoreTimestamp(System.currentTimeMillis()); - AppendMessageResult result; + AppendMessageResult result = null; StoreStatsService storeStatsService = this.defaultMessageStore.getStoreStatsService(); final int tranType = MessageSysFlag.getTransactionValue(messageExtBatch.getSysFlag()); if (tranType != MessageSysFlag.TRANSACTION_NOT_TYPE) { - return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null); + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null)); } if (messageExtBatch.getDelayTimeLevel() > 0) { - return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null); + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null)); + } + + InetSocketAddress bornSocketAddress = (InetSocketAddress) messageExtBatch.getBornHost(); + if (bornSocketAddress.getAddress() instanceof Inet6Address) { + messageExtBatch.setBornHostV6Flag(); + } + + InetSocketAddress storeSocketAddress = (InetSocketAddress) messageExtBatch.getStoreHost(); + if (storeSocketAddress.getAddress() instanceof Inet6Address) { + messageExtBatch.setStoreHostAddressV6Flag(); } - long eclipseTimeInLock = 0; + long elapsedTimeInLock = 0; MappedFile unlockMappedFile = null; MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(); - //fine-grained lock instead of the coarse-grained - MessageExtBatchEncoder batchEncoder = batchEncoderThreadLocal.get(); + long currOffset; + if (mappedFile == null) { + currOffset = 0; + } else { + currOffset = mappedFile.getFileFromOffset() + mappedFile.getWrotePosition(); + } - messageExtBatch.setEncodedBuff(batchEncoder.encode(messageExtBatch)); + int needAckNums = this.defaultMessageStore.getMessageStoreConfig().getInSyncReplicas(); + boolean needHandleHA = needHandleHA(messageExtBatch); - putMessageLock.lock(); + if (needHandleHA && this.defaultMessageStore.getBrokerConfig().isEnableControllerMode()) { + if (this.defaultMessageStore.getHaService().inSyncReplicasNums(currOffset) < this.defaultMessageStore.getMessageStoreConfig().getMinInSyncReplicas()) { + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.IN_SYNC_REPLICAS_NOT_ENOUGH, null)); + } + if (this.defaultMessageStore.getMessageStoreConfig().isAllAckInSyncStateSet()) { + // -1 means all ack in SyncStateSet + needAckNums = MixAll.ALL_ACK_IN_SYNC_STATE_SET; + } + } else if (needHandleHA && this.defaultMessageStore.getBrokerConfig().isEnableSlaveActingMaster()) { + int inSyncReplicas = Math.min(this.defaultMessageStore.getAliveReplicaNumInGroup(), + this.defaultMessageStore.getHaService().inSyncReplicasNums(currOffset)); + needAckNums = calcNeedAckNums(inSyncReplicas); + if (needAckNums > inSyncReplicas) { + // Tell the producer, don't have enough slaves to handle the send request + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.IN_SYNC_REPLICAS_NOT_ENOUGH, null)); + } + } + + messageExtBatch.setVersion(MessageVersion.MESSAGE_VERSION_V1); + boolean autoMessageVersionOnTopicLen = + this.defaultMessageStore.getMessageStoreConfig().isAutoMessageVersionOnTopicLen(); + if (autoMessageVersionOnTopicLen && messageExtBatch.getTopic().length() > Byte.MAX_VALUE) { + messageExtBatch.setVersion(MessageVersion.MESSAGE_VERSION_V2); + } + + //fine-grained lock instead of the coarse-grained + PutMessageThreadLocal pmThreadLocal = this.putMessageThreadLocal.get(); + updateMaxMessageSize(pmThreadLocal); + MessageExtEncoder batchEncoder = pmThreadLocal.getEncoder(); + + String topicQueueKey = generateKey(pmThreadLocal.getKeyBuilder(), messageExtBatch); + + PutMessageContext putMessageContext = new PutMessageContext(topicQueueKey); + messageExtBatch.setEncodedBuff(batchEncoder.encode(messageExtBatch, putMessageContext)); + + topicQueueLock.lock(topicQueueKey); try { - long beginLockTimestamp = this.defaultMessageStore.getSystemClock().now(); - this.beginTimeInLock = beginLockTimestamp; + defaultMessageStore.assignOffset(messageExtBatch); - // Here settings are stored timestamp, in order to ensure an orderly - // global - messageExtBatch.setStoreTimestamp(beginLockTimestamp); + putMessageLock.lock(); + try { + long beginLockTimestamp = this.defaultMessageStore.getSystemClock().now(); + this.beginTimeInLock = beginLockTimestamp; - if (null == mappedFile || mappedFile.isFull()) { - mappedFile = this.mappedFileQueue.getLastMappedFile(0); // Mark: NewFile may be cause noise - } - if (null == mappedFile) { - log.error("Create mapped file1 error, topic: {} clientAddr: {}", messageExtBatch.getTopic(), messageExtBatch.getBornHostString()); - beginTimeInLock = 0; - return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, null); - } + // Here settings are stored timestamp, in order to ensure an orderly + // global + messageExtBatch.setStoreTimestamp(beginLockTimestamp); - result = mappedFile.appendMessages(messageExtBatch, this.appendMessageCallback); - switch (result.getStatus()) { - case PUT_OK: - break; - case END_OF_FILE: - unlockMappedFile = mappedFile; - // Create a new file, re-write the message - mappedFile = this.mappedFileQueue.getLastMappedFile(0); - if (null == mappedFile) { - // XXX: warn and notify me - log.error("Create mapped file2 error, topic: {} clientAddr: {}", messageExtBatch.getTopic(), messageExtBatch.getBornHostString()); - beginTimeInLock = 0; - return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, result); + if (null == mappedFile || mappedFile.isFull()) { + mappedFile = this.mappedFileQueue.getLastMappedFile(0); // Mark: NewFile may be cause noise + if (isCloseReadAhead()) { + setFileReadMode(mappedFile, LibC.MADV_RANDOM); } - result = mappedFile.appendMessages(messageExtBatch, this.appendMessageCallback); - break; - case MESSAGE_SIZE_EXCEEDED: - case PROPERTIES_SIZE_EXCEEDED: - beginTimeInLock = 0; - return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, result); - case UNKNOWN_ERROR: - beginTimeInLock = 0; - return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result); - default: + } + if (null == mappedFile) { + log.error("Create mapped file1 error, topic: {} clientAddr: {}", messageExtBatch.getTopic(), messageExtBatch.getBornHostString()); beginTimeInLock = 0; - return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result); + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.CREATE_MAPPED_FILE_FAILED, null)); + } + + result = mappedFile.appendMessages(messageExtBatch, this.appendMessageCallback, putMessageContext); + switch (result.getStatus()) { + case PUT_OK: + break; + case END_OF_FILE: + unlockMappedFile = mappedFile; + // Create a new file, re-write the message + mappedFile = this.mappedFileQueue.getLastMappedFile(0); + if (null == mappedFile) { + // XXX: warn and notify me + log.error("Create mapped file2 error, topic: {} clientAddr: {}", messageExtBatch.getTopic(), messageExtBatch.getBornHostString()); + beginTimeInLock = 0; + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.CREATE_MAPPED_FILE_FAILED, result)); + } + if (isCloseReadAhead()) { + setFileReadMode(mappedFile, LibC.MADV_RANDOM); + } + result = mappedFile.appendMessages(messageExtBatch, this.appendMessageCallback, putMessageContext); + break; + case MESSAGE_SIZE_EXCEEDED: + case PROPERTIES_SIZE_EXCEEDED: + beginTimeInLock = 0; + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, result)); + case UNKNOWN_ERROR: + default: + beginTimeInLock = 0; + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result)); + } + + elapsedTimeInLock = this.defaultMessageStore.getSystemClock().now() - beginLockTimestamp; + beginTimeInLock = 0; + } finally { + putMessageLock.unlock(); } - eclipseTimeInLock = this.defaultMessageStore.getSystemClock().now() - beginLockTimestamp; - beginTimeInLock = 0; + // Increase queue offset when messages are successfully written + if (AppendMessageStatus.PUT_OK.equals(result.getStatus())) { + this.defaultMessageStore.increaseOffset(messageExtBatch, (short) putMessageContext.getBatchSize()); + } + } catch (RocksDBException e) { + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result)); } finally { - putMessageLock.unlock(); + topicQueueLock.unlock(topicQueueKey); } - if (eclipseTimeInLock > 500) { - log.warn("[NOTIFYME]putMessages in lock cost time(ms)={}, bodyLength={} AppendMessageResult={}", eclipseTimeInLock, messageExtBatch.getBody().length, result); + if (elapsedTimeInLock > 500) { + log.warn("[NOTIFYME]putMessages in lock cost time(ms)={}, bodyLength={} AppendMessageResult={}", elapsedTimeInLock, messageExtBatch.getBody().length, result); } if (null != unlockMappedFile && this.defaultMessageStore.getMessageStoreConfig().isWarmMapedFileEnable()) { @@ -771,25 +1231,97 @@ public PutMessageResult putMessages(final MessageExtBatch messageExtBatch) { PutMessageResult putMessageResult = new PutMessageResult(PutMessageStatus.PUT_OK, result); // Statistics - storeStatsService.getSinglePutMessageTopicTimesTotal(messageExtBatch.getTopic()).addAndGet(result.getMsgNum()); - storeStatsService.getSinglePutMessageTopicSizeTotal(messageExtBatch.getTopic()).addAndGet(result.getWroteBytes()); + storeStatsService.getSinglePutMessageTopicTimesTotal(messageExtBatch.getTopic()).add(result.getMsgNum()); + storeStatsService.getSinglePutMessageTopicSizeTotal(messageExtBatch.getTopic()).add(result.getWroteBytes()); - handleDiskFlush(result, putMessageResult, messageExtBatch); + return handleDiskFlushAndHA(putMessageResult, messageExtBatch, needAckNums, needHandleHA); + } - handleHA(result, putMessageResult, messageExtBatch); + private int calcNeedAckNums(int inSyncReplicas) { + int needAckNums = this.defaultMessageStore.getMessageStoreConfig().getInSyncReplicas(); + if (this.defaultMessageStore.getMessageStoreConfig().isEnableAutoInSyncReplicas()) { + needAckNums = Math.min(needAckNums, inSyncReplicas); + needAckNums = Math.max(needAckNums, this.defaultMessageStore.getMessageStoreConfig().getMinInSyncReplicas()); + } + return needAckNums; + } + + private boolean needHandleHA(MessageExt messageExt) { + + if (!messageExt.isWaitStoreMsgOK()) { + /* + No need to sync messages that special config to extra broker slaves. + @see MessageConst.PROPERTY_WAIT_STORE_MSG_OK + */ + return false; + } + + if (this.defaultMessageStore.getMessageStoreConfig().isDuplicationEnable()) { + return false; + } - return putMessageResult; + if (BrokerRole.SYNC_MASTER != this.defaultMessageStore.getMessageStoreConfig().getBrokerRole()) { + // No need to check ha in async or slave broker + return false; + } + + return true; + } + + private CompletableFuture handleDiskFlushAndHA(PutMessageResult putMessageResult, + MessageExt messageExt, int needAckNums, boolean needHandleHA) { + CompletableFuture flushResultFuture = handleDiskFlush(putMessageResult.getAppendMessageResult(), messageExt); + CompletableFuture replicaResultFuture; + if (!needHandleHA) { + replicaResultFuture = CompletableFuture.completedFuture(PutMessageStatus.PUT_OK); + } else { + replicaResultFuture = handleHA(putMessageResult.getAppendMessageResult(), putMessageResult, needAckNums); + } + + return flushResultFuture.thenCombine(replicaResultFuture, (flushStatus, replicaStatus) -> { + if (flushStatus != PutMessageStatus.PUT_OK) { + putMessageResult.setPutMessageStatus(flushStatus); + } + if (replicaStatus != PutMessageStatus.PUT_OK) { + putMessageResult.setPutMessageStatus(replicaStatus); + } + return putMessageResult; + }); + } + + private CompletableFuture handleDiskFlush(AppendMessageResult result, MessageExt messageExt) { + return this.flushManager.handleDiskFlush(result, messageExt); + } + + private CompletableFuture handleHA(AppendMessageResult result, PutMessageResult putMessageResult, + int needAckNums) { + if (needAckNums >= 0 && needAckNums <= 1) { + return CompletableFuture.completedFuture(PutMessageStatus.PUT_OK); + } + + HAService haService = this.defaultMessageStore.getHaService(); + + long nextOffset = result.getWroteOffset() + result.getWroteBytes(); + + // Wait enough acks from different slaves + GroupCommitRequest request = new GroupCommitRequest(nextOffset, this.defaultMessageStore.getMessageStoreConfig().getSlaveTimeout(), needAckNums); + haService.putRequest(request); + haService.getWaitNotifyObject().wakeupAll(); + return request.future(); } /** * According to receive certain message or offset storage time if an error occurs, it returns -1 */ public long pickupStoreTimestamp(final long offset, final int size) { - if (offset >= this.getMinOffset()) { + if (offset >= this.getMinOffset() && offset + size <= this.getMaxOffset()) { SelectMappedBufferResult result = this.getMessage(offset, size); if (null != result) { try { - return result.getByteBuffer().getLong(MessageDecoder.MESSAGE_STORE_TIMESTAMP_POSTION); + int sysFlag = result.getByteBuffer().getInt(MessageDecoder.SYSFLAG_POSITION); + int bornhostLength = (sysFlag & MessageSysFlag.BORNHOST_V6_FLAG) == 0 ? 8 : 20; + int msgStoreTimePos = 4 + 4 + 4 + 4 + 4 + 8 + 8 + 4 + 8 + bornhostLength; + return result.getByteBuffer().getLong(msgStoreTimePos); } finally { result.release(); } @@ -813,33 +1345,29 @@ public long getMinOffset() { } public SelectMappedBufferResult getMessage(final long offset, final int size) { - int mappedFileSize = this.defaultMessageStore.getMessageStoreConfig().getMapedFileSizeCommitLog(); + int mappedFileSize = this.defaultMessageStore.getMessageStoreConfig().getMappedFileSizeCommitLog(); MappedFile mappedFile = this.mappedFileQueue.findMappedFileByOffset(offset, offset == 0); if (mappedFile != null) { int pos = (int) (offset % mappedFileSize); - return mappedFile.selectMappedBuffer(pos, size); + SelectMappedBufferResult selectMappedBufferResult = mappedFile.selectMappedBuffer(pos, size); + if (null != selectMappedBufferResult) { + selectMappedBufferResult.setInCache(coldDataCheckService.isDataInPageCache(offset)); + return selectMappedBufferResult; + } } return null; } public long rollNextFile(final long offset) { - int mappedFileSize = this.defaultMessageStore.getMessageStoreConfig().getMapedFileSizeCommitLog(); + int mappedFileSize = this.defaultMessageStore.getMessageStoreConfig().getMappedFileSizeCommitLog(); return offset + mappedFileSize - offset % mappedFileSize; } - public HashMap getTopicQueueTable() { - return topicQueueTable; - } - - public void setTopicQueueTable(HashMap topicQueueTable) { - this.topicQueueTable = topicQueueTable; - } - public void destroy() { this.mappedFileQueue.destroy(); } - public boolean appendData(long startOffset, byte[] data) { + public boolean appendData(long startOffset, byte[] data, int dataStart, int dataLength) { putMessageLock.lock(); try { MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(startOffset); @@ -848,7 +1376,7 @@ public boolean appendData(long startOffset, byte[] data) { return false; } - return mappedFile.appendMessage(data); + return mappedFile.appendMessage(data, dataStart, dataLength); } finally { putMessageLock.unlock(); } @@ -858,15 +1386,6 @@ public boolean retryDeleteFirstFile(final long intervalForcibly) { return this.mappedFileQueue.retryDeleteFirstFile(intervalForcibly); } - public void removeQueueFromTopicQueueTable(final String topic, final int queueId) { - String key = topic + "-" + queueId; - synchronized (this) { - this.topicQueueTable.remove(key); - } - - log.info("removeQueueFromTopicQueueTable OK Topic: {} QueueId: {}", topic, queueId); - } - public void checkSelf() { mappedFileQueue.checkSelf(); } @@ -885,6 +1404,26 @@ public long lockTimeMills() { return diff; } + protected short getMessageNum(MessageExtBrokerInner msgInner) { + short messageNum = 1; + // IF inner batch, build batchQueueOffset and batchNum property. + CQType cqType = getCqType(msgInner); + + if (MessageSysFlag.check(msgInner.getSysFlag(), MessageSysFlag.INNER_BATCH_FLAG) || CQType.BatchCQ.equals(cqType)) { + if (msgInner.getProperty(MessageConst.PROPERTY_INNER_NUM) != null) { + messageNum = Short.parseShort(msgInner.getProperty(MessageConst.PROPERTY_INNER_NUM)); + messageNum = messageNum >= 1 ? messageNum : 1; + } + } + + return messageNum; + } + + private CQType getCqType(MessageExtBrokerInner msgInner) { + Optional topicConfig = this.defaultMessageStore.getTopicConfig(msgInner.getTopic()); + return QueueTypeUtils.getCQType(topicConfig); + } + abstract class FlushCommitLogService extends ServiceThread { protected static final int RETRY_TIMES_OVER = 10; } @@ -895,6 +1434,9 @@ class CommitRealTimeService extends FlushCommitLogService { @Override public String getServiceName() { + if (CommitLog.this.defaultMessageStore.getBrokerConfig().isInBrokerContainer()) { + return CommitLog.this.defaultMessageStore.getBrokerIdentity().getIdentifier() + CommitRealTimeService.class.getSimpleName(); + } return CommitRealTimeService.class.getSimpleName(); } @@ -920,10 +1462,9 @@ public void run() { long end = System.currentTimeMillis(); if (!result) { this.lastCommitTimestamp = end; // result = false means some data committed. - //now wake up flush thread. - flushCommitLogService.wakeup(); + CommitLog.this.flushManager.wakeUpFlush(); } - + CommitLog.this.getMessageStore().getPerfCounter().flowOnce("COMMIT_DATA_TIME_MS", (int) (end - begin)); if (end - begin > 500) { log.info("Commit data to file costs {} ms", end - begin); } @@ -946,6 +1487,7 @@ class FlushRealTimeService extends FlushCommitLogService { private long lastFlushTimestamp = 0; private long printTimes = 0; + @Override public void run() { CommitLog.log.info(this.getServiceName() + " service started"); @@ -986,6 +1528,7 @@ public void run() { CommitLog.this.defaultMessageStore.getStoreCheckpoint().setPhysicMsgTimestamp(storeTimestamp); } long past = System.currentTimeMillis() - begin; + CommitLog.this.getMessageStore().getPerfCounter().flowOnce("FLUSH_DATA_TIME_MS", (int) past); if (past > 500) { log.info("Flush data to disk costs {} ms", past); } @@ -1009,6 +1552,9 @@ public void run() { @Override public String getServiceName() { + if (CommitLog.this.defaultMessageStore.getBrokerConfig().isInBrokerContainer()) { + return CommitLog.this.defaultMessageStore.getBrokerConfig().getIdentifier() + FlushRealTimeService.class.getSimpleName(); + } return FlushRealTimeService.class.getSimpleName(); } @@ -1018,37 +1564,46 @@ private void printFlushProgress() { } @Override - public long getJointime() { + public long getJoinTime() { return 1000 * 60 * 5; } } public static class GroupCommitRequest { private final long nextOffset; - private final CountDownLatch countDownLatch = new CountDownLatch(1); - private volatile boolean flushOK = false; + // Indicate the GroupCommitRequest result: true or false + private final CompletableFuture flushOKFuture = new CompletableFuture<>(); + private volatile int ackNums = 1; + private final long deadLine; - public GroupCommitRequest(long nextOffset) { + public GroupCommitRequest(long nextOffset, long timeoutMillis) { this.nextOffset = nextOffset; + this.deadLine = System.nanoTime() + (timeoutMillis * 1_000_000); + } + + public GroupCommitRequest(long nextOffset, long timeoutMillis, int ackNums) { + this(nextOffset, timeoutMillis); + this.ackNums = ackNums; } public long getNextOffset() { return nextOffset; } - public void wakeupCustomer(final boolean flushOK) { - this.flushOK = flushOK; - this.countDownLatch.countDown(); + public int getAckNums() { + return ackNums; } - public boolean waitForFlush(long timeout) { - try { - this.countDownLatch.await(timeout, TimeUnit.MILLISECONDS); - return this.flushOK; - } catch (InterruptedException e) { - log.error("Interrupted", e); - return false; - } + public long getDeadLine() { + return deadLine; + } + + public void wakeupCustomer(final PutMessageStatus status) { + this.flushOKFuture.complete(status); + } + + public CompletableFuture future() { + return flushOKFuture; } } @@ -1056,16 +1611,136 @@ public boolean waitForFlush(long timeout) { * GroupCommit Service */ class GroupCommitService extends FlushCommitLogService { - private volatile List requestsWrite = new ArrayList(); - private volatile List requestsRead = new ArrayList(); + private volatile LinkedList requestsWrite = new LinkedList<>(); + private volatile LinkedList requestsRead = new LinkedList<>(); + private final PutMessageSpinLock lock = new PutMessageSpinLock(); - public synchronized void putRequest(final GroupCommitRequest request) { + public void putRequest(final GroupCommitRequest request) { + lock.lock(); + try { + this.requestsWrite.add(request); + } finally { + lock.unlock(); + } + this.wakeup(); + } + + private void swapRequests() { + lock.lock(); + try { + LinkedList tmp = this.requestsWrite; + this.requestsWrite = this.requestsRead; + this.requestsRead = tmp; + } finally { + lock.unlock(); + } + } + + private void doCommit() { + if (!this.requestsRead.isEmpty()) { + for (GroupCommitRequest req : this.requestsRead) { + boolean flushOK = CommitLog.this.mappedFileQueue.getFlushedWhere() >= req.getNextOffset(); + for (int i = 0; i < 1000 && !flushOK; i++) { + CommitLog.this.mappedFileQueue.flush(0); + flushOK = CommitLog.this.mappedFileQueue.getFlushedWhere() >= req.getNextOffset(); + if (flushOK) { + break; + } else { + // When transientStorePoolEnable is true, the messages in writeBuffer may not be committed + // to pageCache very quickly, and flushOk here may almost be false, so we can sleep 1ms to + // wait for the messages to be committed to pageCache. + try { + Thread.sleep(1); + } catch (InterruptedException ignored) { + } + } + } + + req.wakeupCustomer(flushOK ? PutMessageStatus.PUT_OK : PutMessageStatus.FLUSH_DISK_TIMEOUT); + } + + long storeTimestamp = CommitLog.this.mappedFileQueue.getStoreTimestamp(); + if (storeTimestamp > 0) { + CommitLog.this.defaultMessageStore.getStoreCheckpoint().setPhysicMsgTimestamp(storeTimestamp); + } + + this.requestsRead = new LinkedList<>(); + } else { + // Because of individual messages is set to not sync flush, it + // will come to this process + CommitLog.this.mappedFileQueue.flush(0); + } + } + + @Override + public void run() { + CommitLog.log.info(this.getServiceName() + " service started"); + + while (!this.isStopped()) { + try { + this.waitForRunning(10); + this.doCommit(); + } catch (Exception e) { + CommitLog.log.warn(this.getServiceName() + " service has exception. ", e); + } + } + + // Under normal circumstances shutdown, wait for the arrival of the + // request, and then flush + try { + Thread.sleep(10); + } catch (InterruptedException e) { + CommitLog.log.warn("GroupCommitService Exception, ", e); + } + + this.swapRequests(); + this.doCommit(); + + CommitLog.log.info(this.getServiceName() + " service end"); + } + + @Override + protected void onWaitEnd() { + this.swapRequests(); + } + + @Override + public String getServiceName() { + if (CommitLog.this.defaultMessageStore.getBrokerConfig().isInBrokerContainer()) { + return CommitLog.this.defaultMessageStore.getBrokerConfig().getIdentifier() + GroupCommitService.class.getSimpleName(); + } + return GroupCommitService.class.getSimpleName(); + } + + @Override + public long getJoinTime() { + return 1000 * 60 * 5; + } + } + + class GroupCheckService extends FlushCommitLogService { + private volatile List requestsWrite = new ArrayList<>(); + private volatile List requestsRead = new ArrayList<>(); + + public boolean isAsyncRequestsFull() { + return requestsWrite.size() > CommitLog.this.defaultMessageStore.getMessageStoreConfig().getMaxAsyncPutMessageRequests() * 2; + } + + public synchronized boolean putRequest(final GroupCommitRequest request) { synchronized (this.requestsWrite) { this.requestsWrite.add(request); } if (hasNotified.compareAndSet(false, true)) { waitPoint.countDown(); // notify } + boolean flag = this.requestsWrite.size() > + CommitLog.this.defaultMessageStore.getMessageStoreConfig().getMaxAsyncPutMessageRequests(); + if (flag) { + log.info("Async requests {} exceeded the threshold {}", requestsWrite.size(), + CommitLog.this.defaultMessageStore.getMessageStoreConfig().getMaxAsyncPutMessageRequests()); + } + + return flag; } private void swapRequests() { @@ -1081,15 +1756,19 @@ private void doCommit() { // There may be a message in the next file, so a maximum of // two times the flush boolean flushOK = false; - for (int i = 0; i < 2 && !flushOK; i++) { + for (int i = 0; i < 1000; i++) { flushOK = CommitLog.this.mappedFileQueue.getFlushedWhere() >= req.getNextOffset(); - - if (!flushOK) { - CommitLog.this.mappedFileQueue.flush(0); + if (flushOK) { + break; + } else { + try { + Thread.sleep(1); + } catch (Throwable ignored) { + + } } } - - req.wakeupCustomer(flushOK); + req.wakeupCustomer(flushOK ? PutMessageStatus.PUT_OK : PutMessageStatus.FLUSH_DISK_TIMEOUT); } long storeTimestamp = CommitLog.this.mappedFileQueue.getStoreTimestamp(); @@ -1098,10 +1777,6 @@ private void doCommit() { } this.requestsRead.clear(); - } else { - // Because of individual messages is set to not sync flush, it - // will come to this process - CommitLog.this.mappedFileQueue.flush(0); } } } @@ -1111,7 +1786,7 @@ public void run() { while (!this.isStopped()) { try { - this.waitForRunning(10); + this.waitForRunning(1); this.doCommit(); } catch (Exception e) { CommitLog.log.warn(this.getServiceName() + " service has exception. ", e); @@ -1142,11 +1817,14 @@ protected void onWaitEnd() { @Override public String getServiceName() { - return GroupCommitService.class.getSimpleName(); + if (CommitLog.this.defaultMessageStore.getBrokerConfig().isInBrokerContainer()) { + return CommitLog.this.defaultMessageStore.getBrokerConfig().getIdentifier() + GroupCheckService.class.getSimpleName(); + } + return GroupCheckService.class.getSimpleName(); } @Override - public long getJointime() { + public long getJoinTime() { return 1000 * 60 * 5; } } @@ -1154,55 +1832,111 @@ public long getJointime() { class DefaultAppendMessageCallback implements AppendMessageCallback { // File at the end of the minimum fixed length empty private static final int END_FILE_MIN_BLANK_LENGTH = 4 + 4; - private final ByteBuffer msgIdMemory; // Store the message content private final ByteBuffer msgStoreItemMemory; - // The maximum length of the message - private final int maxMessageSize; - // Build Message Key - private final StringBuilder keyBuilder = new StringBuilder(); + private final int crc32ReservedLength = CommitLog.CRC32_RESERVED_LEN; + private final MessageStoreConfig messageStoreConfig; + + DefaultAppendMessageCallback(MessageStoreConfig messageStoreConfig) { + this.msgStoreItemMemory = ByteBuffer.allocate(END_FILE_MIN_BLANK_LENGTH); + this.messageStoreConfig = messageStoreConfig; + } - private final StringBuilder msgIdBuilder = new StringBuilder(); + public AppendMessageResult handlePropertiesForLmqMsg(ByteBuffer preEncodeBuffer, + final MessageExtBrokerInner msgInner) { + if (msgInner.isEncodeCompleted()) { + return null; + } - private final ByteBuffer hostHolder = ByteBuffer.allocate(8); + multiDispatch.wrapMultiDispatch(msgInner); - DefaultAppendMessageCallback(final int size) { - this.msgIdMemory = ByteBuffer.allocate(MessageDecoder.MSG_ID_LENGTH); - this.msgStoreItemMemory = ByteBuffer.allocate(size + END_FILE_MIN_BLANK_LENGTH); - this.maxMessageSize = size; - } + msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); + + final byte[] propertiesData = + msgInner.getPropertiesString() == null ? null : msgInner.getPropertiesString().getBytes(MessageDecoder.CHARSET_UTF8); + + boolean needAppendLastPropertySeparator = enabledAppendPropCRC && propertiesData != null && propertiesData.length > 0 + && propertiesData[propertiesData.length - 1] != MessageDecoder.PROPERTY_SEPARATOR; + + final int propertiesLength = (propertiesData == null ? 0 : propertiesData.length) + (needAppendLastPropertySeparator ? 1 : 0) + crc32ReservedLength; + + if (propertiesLength > Short.MAX_VALUE) { + log.warn("putMessage message properties length too long. length={}", propertiesData.length); + return new AppendMessageResult(AppendMessageStatus.PROPERTIES_SIZE_EXCEEDED); + } + + int msgLenWithoutProperties = preEncodeBuffer.getInt(0); + + int msgLen = msgLenWithoutProperties + 2 + propertiesLength; + + // Exceeds the maximum message + if (msgLen > this.messageStoreConfig.getMaxMessageSize()) { + log.warn("message size exceeded, msg total size: " + msgLen + ", maxMessageSize: " + this.messageStoreConfig.getMaxMessageSize()); + return new AppendMessageResult(AppendMessageStatus.MESSAGE_SIZE_EXCEEDED); + } + + // Back filling total message length + preEncodeBuffer.putInt(0, msgLen); + // Modify position to msgLenWithoutProperties + preEncodeBuffer.position(msgLenWithoutProperties); + + preEncodeBuffer.putShort((short) propertiesLength); - public ByteBuffer getMsgStoreItemMemory() { - return msgStoreItemMemory; + if (propertiesLength > crc32ReservedLength) { + preEncodeBuffer.put(propertiesData); + } + + if (needAppendLastPropertySeparator) { + preEncodeBuffer.put((byte) MessageDecoder.PROPERTY_SEPARATOR); + } + // 18 CRC32 + preEncodeBuffer.position(preEncodeBuffer.position() + crc32ReservedLength); + + msgInner.setEncodeCompleted(true); + + return null; } public AppendMessageResult doAppend(final long fileFromOffset, final ByteBuffer byteBuffer, final int maxBlank, - final MessageExtBrokerInner msgInner) { + final MessageExtBrokerInner msgInner, PutMessageContext putMessageContext) { // STORETIMESTAMP + STOREHOSTADDRESS + OFFSET
    + ByteBuffer preEncodeBuffer = msgInner.getEncodedBuff(); + boolean isMultiDispatchMsg = messageStoreConfig.isEnableMultiDispatch() && CommitLog.isMultiDispatchMsg(msgInner); + if (isMultiDispatchMsg) { + AppendMessageResult appendMessageResult = handlePropertiesForLmqMsg(preEncodeBuffer, msgInner); + if (appendMessageResult != null) { + return appendMessageResult; + } + } + + final int msgLen = preEncodeBuffer.getInt(0); + preEncodeBuffer.position(0); + preEncodeBuffer.limit(msgLen); + // PHY OFFSET long wroteOffset = fileFromOffset + byteBuffer.position(); - this.resetByteBuffer(hostHolder, 8); - String msgId = MessageDecoder.createMessageId(this.msgIdMemory, msgInner.getStoreHostBytes(hostHolder), wroteOffset); + Supplier msgIdSupplier = () -> { + int sysflag = msgInner.getSysFlag(); + int msgIdLen = (sysflag & MessageSysFlag.STOREHOSTADDRESS_V6_FLAG) == 0 ? 4 + 4 + 8 : 16 + 4 + 8; + ByteBuffer msgIdBuffer = ByteBuffer.allocate(msgIdLen); + MessageExt.socketAddress2ByteBuffer(msgInner.getStoreHost(), msgIdBuffer); + msgIdBuffer.clear();//because socketAddress2ByteBuffer flip the buffer + msgIdBuffer.putLong(msgIdLen - 8, wroteOffset); + return UtilAll.bytes2string(msgIdBuffer.array()); + }; // Record ConsumeQueue information - keyBuilder.setLength(0); - keyBuilder.append(msgInner.getTopic()); - keyBuilder.append('-'); - keyBuilder.append(msgInner.getQueueId()); - String key = keyBuilder.toString(); - Long queueOffset = CommitLog.this.topicQueueTable.get(key); - if (null == queueOffset) { - queueOffset = 0L; - CommitLog.this.topicQueueTable.put(key, queueOffset); - } + Long queueOffset = msgInner.getQueueOffset(); + + // this msg maybe an inner-batch msg. + short messageNum = getMessageNum(msgInner); // Transaction messages that require special handling final int tranType = MessageSysFlag.getTransactionValue(msgInner.getSysFlag()); switch (tranType) { - // Prepared and Rollback message is not consumed, will not enter the - // consumer queuec + // Prepared and Rollback message is not consumed, will not enter the consume queue case MessageSysFlag.TRANSACTION_PREPARED_TYPE: case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE: queueOffset = 0L; @@ -1213,36 +1947,9 @@ public AppendMessageResult doAppend(final long fileFromOffset, final ByteBuffer break; } - /** - * Serialize message - */ - final byte[] propertiesData = - msgInner.getPropertiesString() == null ? null : msgInner.getPropertiesString().getBytes(MessageDecoder.CHARSET_UTF8); - - final int propertiesLength = propertiesData == null ? 0 : propertiesData.length; - - if (propertiesLength > Short.MAX_VALUE) { - log.warn("putMessage message properties length too long. length={}", propertiesData.length); - return new AppendMessageResult(AppendMessageStatus.PROPERTIES_SIZE_EXCEEDED); - } - - final byte[] topicData = msgInner.getTopic().getBytes(MessageDecoder.CHARSET_UTF8); - final int topicLength = topicData.length; - - final int bodyLength = msgInner.getBody() == null ? 0 : msgInner.getBody().length; - - final int msgLen = calMsgLength(bodyLength, topicLength, propertiesLength); - - // Exceeds the maximum message - if (msgLen > this.maxMessageSize) { - CommitLog.log.warn("message size exceeded, msg total size: " + msgLen + ", msg body size: " + bodyLength - + ", maxMessageSize: " + this.maxMessageSize); - return new AppendMessageResult(AppendMessageStatus.MESSAGE_SIZE_EXCEEDED); - } - // Determines whether there is sufficient free space if ((msgLen + END_FILE_MIN_BLANK_LENGTH) > maxBlank) { - this.resetByteBuffer(this.msgStoreItemMemory, maxBlank); + this.msgStoreItemMemory.clear(); // 1 TOTALSIZE this.msgStoreItemMemory.putInt(maxBlank); // 2 MAGICCODE @@ -1250,118 +1957,97 @@ public AppendMessageResult doAppend(final long fileFromOffset, final ByteBuffer // 3 The remaining space may be any value // Here the length of the specially set maxBlank final long beginTimeMills = CommitLog.this.defaultMessageStore.now(); - byteBuffer.put(this.msgStoreItemMemory.array(), 0, maxBlank); - return new AppendMessageResult(AppendMessageStatus.END_OF_FILE, wroteOffset, maxBlank, msgId, msgInner.getStoreTimestamp(), + byteBuffer.put(this.msgStoreItemMemory.array(), 0, 8); + return new AppendMessageResult(AppendMessageStatus.END_OF_FILE, wroteOffset, + maxBlank, /* only wrote 8 bytes, but declare wrote maxBlank for compute write position */ + msgIdSupplier, msgInner.getStoreTimestamp(), queueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills); } - // Initialization of storage space - this.resetByteBuffer(msgStoreItemMemory, msgLen); - // 1 TOTALSIZE - this.msgStoreItemMemory.putInt(msgLen); - // 2 MAGICCODE - this.msgStoreItemMemory.putInt(CommitLog.MESSAGE_MAGIC_CODE); - // 3 BODYCRC - this.msgStoreItemMemory.putInt(msgInner.getBodyCRC()); - // 4 QUEUEID - this.msgStoreItemMemory.putInt(msgInner.getQueueId()); - // 5 FLAG - this.msgStoreItemMemory.putInt(msgInner.getFlag()); + int pos = 4 + 4 + 4 + 4 + 4; // 6 QUEUEOFFSET - this.msgStoreItemMemory.putLong(queueOffset); + preEncodeBuffer.putLong(pos, queueOffset); + pos += 8; // 7 PHYSICALOFFSET - this.msgStoreItemMemory.putLong(fileFromOffset + byteBuffer.position()); - // 8 SYSFLAG - this.msgStoreItemMemory.putInt(msgInner.getSysFlag()); - // 9 BORNTIMESTAMP - this.msgStoreItemMemory.putLong(msgInner.getBornTimestamp()); - // 10 BORNHOST - this.resetByteBuffer(hostHolder, 8); - this.msgStoreItemMemory.put(msgInner.getBornHostBytes(hostHolder)); - // 11 STORETIMESTAMP - this.msgStoreItemMemory.putLong(msgInner.getStoreTimestamp()); - // 12 STOREHOSTADDRESS - this.resetByteBuffer(hostHolder, 8); - this.msgStoreItemMemory.put(msgInner.getStoreHostBytes(hostHolder)); - //this.msgBatchMemory.put(msgInner.getStoreHostBytes()); - // 13 RECONSUMETIMES - this.msgStoreItemMemory.putInt(msgInner.getReconsumeTimes()); - // 14 Prepared Transaction Offset - this.msgStoreItemMemory.putLong(msgInner.getPreparedTransactionOffset()); - // 15 BODY - this.msgStoreItemMemory.putInt(bodyLength); - if (bodyLength > 0) - this.msgStoreItemMemory.put(msgInner.getBody()); - // 16 TOPIC - this.msgStoreItemMemory.put((byte) topicLength); - this.msgStoreItemMemory.put(topicData); - // 17 PROPERTIES - this.msgStoreItemMemory.putShort((short) propertiesLength); - if (propertiesLength > 0) - this.msgStoreItemMemory.put(propertiesData); + preEncodeBuffer.putLong(pos, fileFromOffset + byteBuffer.position()); + int ipLen = (msgInner.getSysFlag() & MessageSysFlag.BORNHOST_V6_FLAG) == 0 ? 4 + 4 : 16 + 4; + // 8 SYSFLAG, 9 BORNTIMESTAMP, 10 BORNHOST, 11 STORETIMESTAMP + pos += 8 + 4 + 8 + ipLen; + // refresh store time stamp in lock + preEncodeBuffer.putLong(pos, msgInner.getStoreTimestamp()); + if (enabledAppendPropCRC) { + // 18 CRC32 + int checkSize = msgLen - crc32ReservedLength; + ByteBuffer tmpBuffer = preEncodeBuffer.duplicate(); + tmpBuffer.limit(tmpBuffer.position() + checkSize); + int crc32 = UtilAll.crc32(tmpBuffer); + tmpBuffer.limit(tmpBuffer.position() + crc32ReservedLength); + MessageDecoder.createCrc32(tmpBuffer, crc32); + } final long beginTimeMills = CommitLog.this.defaultMessageStore.now(); + CommitLog.this.getMessageStore().getPerfCounter().startTick("WRITE_MEMORY_TIME_MS"); // Write messages to the queue buffer - byteBuffer.put(this.msgStoreItemMemory.array(), 0, msgLen); - - AppendMessageResult result = new AppendMessageResult(AppendMessageStatus.PUT_OK, wroteOffset, msgLen, msgId, - msgInner.getStoreTimestamp(), queueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills); + byteBuffer.put(preEncodeBuffer); + CommitLog.this.getMessageStore().getPerfCounter().endTick("WRITE_MEMORY_TIME_MS"); + msgInner.setEncodedBuff(null); - switch (tranType) { - case MessageSysFlag.TRANSACTION_PREPARED_TYPE: - case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE: - break; - case MessageSysFlag.TRANSACTION_NOT_TYPE: - case MessageSysFlag.TRANSACTION_COMMIT_TYPE: - // The next update ConsumeQueue information - CommitLog.this.topicQueueTable.put(key, ++queueOffset); - break; - default: - break; + if (isMultiDispatchMsg) { + CommitLog.this.multiDispatch.updateMultiQueueOffset(msgInner); } - return result; + + return new AppendMessageResult(AppendMessageStatus.PUT_OK, wroteOffset, msgLen, msgIdSupplier, + msgInner.getStoreTimestamp(), queueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills, messageNum); } public AppendMessageResult doAppend(final long fileFromOffset, final ByteBuffer byteBuffer, final int maxBlank, - final MessageExtBatch messageExtBatch) { + final MessageExtBatch messageExtBatch, PutMessageContext putMessageContext) { byteBuffer.mark(); //physical offset long wroteOffset = fileFromOffset + byteBuffer.position(); // Record ConsumeQueue information - keyBuilder.setLength(0); - keyBuilder.append(messageExtBatch.getTopic()); - keyBuilder.append('-'); - keyBuilder.append(messageExtBatch.getQueueId()); - String key = keyBuilder.toString(); - Long queueOffset = CommitLog.this.topicQueueTable.get(key); - if (null == queueOffset) { - queueOffset = 0L; - CommitLog.this.topicQueueTable.put(key, queueOffset); - } + Long queueOffset = messageExtBatch.getQueueOffset(); long beginQueueOffset = queueOffset; int totalMsgLen = 0; int msgNum = 0; - msgIdBuilder.setLength(0); + final long beginTimeMills = CommitLog.this.defaultMessageStore.now(); ByteBuffer messagesByteBuff = messageExtBatch.getEncodedBuff(); - this.resetByteBuffer(hostHolder, 8); - ByteBuffer storeHostBytes = messageExtBatch.getStoreHostBytes(hostHolder); + + int sysFlag = messageExtBatch.getSysFlag(); + int bornHostLength = (sysFlag & MessageSysFlag.BORNHOST_V6_FLAG) == 0 ? 4 + 4 : 16 + 4; + int storeHostLength = (sysFlag & MessageSysFlag.STOREHOSTADDRESS_V6_FLAG) == 0 ? 4 + 4 : 16 + 4; + Supplier msgIdSupplier = () -> { + int msgIdLen = storeHostLength + 8; + int batchCount = putMessageContext.getBatchSize(); + long[] phyPosArray = putMessageContext.getPhyPos(); + ByteBuffer msgIdBuffer = ByteBuffer.allocate(msgIdLen); + MessageExt.socketAddress2ByteBuffer(messageExtBatch.getStoreHost(), msgIdBuffer); + msgIdBuffer.clear();//because socketAddress2ByteBuffer flip the buffer + + StringBuilder buffer = new StringBuilder(batchCount * msgIdLen * 2 + batchCount - 1); + for (int i = 0; i < phyPosArray.length; i++) { + msgIdBuffer.putLong(msgIdLen - 8, phyPosArray[i]); + String msgId = UtilAll.bytes2string(msgIdBuffer.array()); + if (i != 0) { + buffer.append(','); + } + buffer.append(msgId); + } + return buffer.toString(); + }; + messagesByteBuff.mark(); + int index = 0; while (messagesByteBuff.hasRemaining()) { // 1 TOTALSIZE final int msgPos = messagesByteBuff.position(); final int msgLen = messagesByteBuff.getInt(); - final int bodyLen = msgLen - 40; //only for log, just estimate it - // Exceeds the maximum message - if (msgLen > this.maxMessageSize) { - CommitLog.log.warn("message size exceeded, msg total size: " + msgLen + ", msg body size: " + bodyLen - + ", maxMessageSize: " + this.maxMessageSize); - return new AppendMessageResult(AppendMessageStatus.MESSAGE_SIZE_EXCEEDED); - } + totalMsgLen += msgLen; // Determines whether there is sufficient free space if ((totalMsgLen + END_FILE_MIN_BLANK_LENGTH) > maxBlank) { - this.resetByteBuffer(this.msgStoreItemMemory, 8); + this.msgStoreItemMemory.clear(); // 1 TOTALSIZE this.msgStoreItemMemory.putInt(maxBlank); // 2 MAGICCODE @@ -1372,21 +2058,29 @@ public AppendMessageResult doAppend(final long fileFromOffset, final ByteBuffer // Here the length of the specially set maxBlank byteBuffer.reset(); //ignore the previous appended messages byteBuffer.put(this.msgStoreItemMemory.array(), 0, 8); - return new AppendMessageResult(AppendMessageStatus.END_OF_FILE, wroteOffset, maxBlank, msgIdBuilder.toString(), messageExtBatch.getStoreTimestamp(), + return new AppendMessageResult(AppendMessageStatus.END_OF_FILE, wroteOffset, maxBlank, msgIdSupplier, messageExtBatch.getStoreTimestamp(), beginQueueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills); } //move to add queue offset and commitlog offset - messagesByteBuff.position(msgPos + 20); - messagesByteBuff.putLong(queueOffset); - messagesByteBuff.putLong(wroteOffset + totalMsgLen - msgLen); - - storeHostBytes.rewind(); - String msgId = MessageDecoder.createMessageId(this.msgIdMemory, storeHostBytes, wroteOffset + totalMsgLen - msgLen); - if (msgIdBuilder.length() > 0) { - msgIdBuilder.append(',').append(msgId); - } else { - msgIdBuilder.append(msgId); + int pos = msgPos + 20; + messagesByteBuff.putLong(pos, queueOffset); + pos += 8; + messagesByteBuff.putLong(pos, wroteOffset + totalMsgLen - msgLen); + // 8 SYSFLAG, 9 BORNTIMESTAMP, 10 BORNHOST, 11 STORETIMESTAMP + pos += 8 + 4 + 8 + bornHostLength; + // refresh store time stamp in lock + messagesByteBuff.putLong(pos, messageExtBatch.getStoreTimestamp()); + if (enabledAppendPropCRC) { + //append crc32 + int checkSize = msgLen - crc32ReservedLength; + ByteBuffer tmpBuffer = messagesByteBuff.duplicate(); + tmpBuffer.position(msgPos).limit(msgPos + checkSize); + int crc32 = UtilAll.crc32(tmpBuffer); + messagesByteBuff.position(msgPos + checkSize); + MessageDecoder.createCrc32(messagesByteBuff, crc32); } + + putMessageContext.getPhyPos()[index++] = wroteOffset + totalMsgLen - msgLen; queueOffset++; msgNum++; messagesByteBuff.position(msgPos + msgLen); @@ -1396,126 +2090,361 @@ public AppendMessageResult doAppend(final long fileFromOffset, final ByteBuffer messagesByteBuff.limit(totalMsgLen); byteBuffer.put(messagesByteBuff); messageExtBatch.setEncodedBuff(null); - AppendMessageResult result = new AppendMessageResult(AppendMessageStatus.PUT_OK, wroteOffset, totalMsgLen, msgIdBuilder.toString(), + AppendMessageResult result = new AppendMessageResult(AppendMessageStatus.PUT_OK, wroteOffset, totalMsgLen, msgIdSupplier, messageExtBatch.getStoreTimestamp(), beginQueueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills); result.setMsgNum(msgNum); - CommitLog.this.topicQueueTable.put(key, queueOffset); return result; } - private void resetByteBuffer(final ByteBuffer byteBuffer, final int limit) { - byteBuffer.flip(); - byteBuffer.limit(limit); + } + + class DefaultFlushManager implements FlushManager { + + private final FlushCommitLogService flushCommitLogService; + + //If TransientStorePool enabled, we must flush message to FileChannel at fixed periods + private final FlushCommitLogService commitRealTimeService; + + public DefaultFlushManager() { + if (FlushDiskType.SYNC_FLUSH == CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) { + this.flushCommitLogService = new CommitLog.GroupCommitService(); + } else { + this.flushCommitLogService = new CommitLog.FlushRealTimeService(); + } + + this.commitRealTimeService = new CommitLog.CommitRealTimeService(); + } + + @Override public void start() { + this.flushCommitLogService.start(); + + if (defaultMessageStore.isTransientStorePoolEnable()) { + this.commitRealTimeService.start(); + } + } + + public void handleDiskFlush(AppendMessageResult result, PutMessageResult putMessageResult, + MessageExt messageExt) { + // Synchronization flush + if (FlushDiskType.SYNC_FLUSH == CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) { + final GroupCommitService service = (GroupCommitService) this.flushCommitLogService; + if (messageExt.isWaitStoreMsgOK()) { + GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes(), CommitLog.this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout()); + service.putRequest(request); + CompletableFuture flushOkFuture = request.future(); + PutMessageStatus flushStatus = null; + try { + flushStatus = flushOkFuture.get(CommitLog.this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout(), TimeUnit.MILLISECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + //flushOK=false; + } + if (flushStatus != PutMessageStatus.PUT_OK) { + log.error("do groupcommit, wait for flush failed, topic: " + messageExt.getTopic() + " tags: " + messageExt.getTags() + " client address: " + messageExt.getBornHostString()); + putMessageResult.setPutMessageStatus(PutMessageStatus.FLUSH_DISK_TIMEOUT); + } + } else { + service.wakeup(); + } + } + // Asynchronous flush + else { + if (!CommitLog.this.defaultMessageStore.isTransientStorePoolEnable()) { + flushCommitLogService.wakeup(); + } else { + commitRealTimeService.wakeup(); + } + } + } + + @Override + public CompletableFuture handleDiskFlush(AppendMessageResult result, MessageExt messageExt) { + // Synchronization flush + if (FlushDiskType.SYNC_FLUSH == CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) { + final GroupCommitService service = (GroupCommitService) this.flushCommitLogService; + if (messageExt.isWaitStoreMsgOK()) { + GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes(), CommitLog.this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout()); + flushDiskWatcher.add(request); + service.putRequest(request); + return request.future(); + } else { + service.wakeup(); + return CompletableFuture.completedFuture(PutMessageStatus.PUT_OK); + } + } + // Asynchronous flush + else { + if (!CommitLog.this.defaultMessageStore.isTransientStorePoolEnable()) { + flushCommitLogService.wakeup(); + } else { + commitRealTimeService.wakeup(); + } + return CompletableFuture.completedFuture(PutMessageStatus.PUT_OK); + } + } + + @Override + public void wakeUpFlush() { + // now wake up flush thread. + flushCommitLogService.wakeup(); + } + + @Override + public void wakeUpCommit() { + // now wake up commit log thread. + commitRealTimeService.wakeup(); + } + + @Override + public void shutdown() { + if (defaultMessageStore.isTransientStorePoolEnable()) { + this.commitRealTimeService.shutdown(); + } + + this.flushCommitLogService.shutdown(); } } - public static class MessageExtBatchEncoder { - // Store the message content - private final ByteBuffer msgBatchMemory; - // The maximum length of the message - private final int maxMessageSize; + public int getCommitLogSize() { + return commitLogSize; + } - private final ByteBuffer hostHolder = ByteBuffer.allocate(8); + public MappedFileQueue getMappedFileQueue() { + return mappedFileQueue; + } + + public MessageStore getMessageStore() { + return defaultMessageStore; + } + + @Override + public void swapMap(int reserveNum, long forceSwapIntervalMs, long normalSwapIntervalMs) { + this.getMappedFileQueue().swapMap(reserveNum, forceSwapIntervalMs, normalSwapIntervalMs); + } + + public boolean isMappedFilesEmpty() { + return this.mappedFileQueue.isMappedFilesEmpty(); + } + + @Override + public void cleanSwappedMap(long forceCleanSwapIntervalMs) { + this.getMappedFileQueue().cleanSwappedMap(forceCleanSwapIntervalMs); + } + + public FlushManager getFlushManager() { + return flushManager; + } + + public static boolean isMultiDispatchMsg(MessageExtBrokerInner msg) { + return StringUtils.isNoneBlank(msg.getProperty(MessageConst.PROPERTY_INNER_MULTI_DISPATCH)) && !msg.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX); + } - MessageExtBatchEncoder(final int size) { - this.msgBatchMemory = ByteBuffer.allocateDirect(size); - this.maxMessageSize = size; + private boolean isCloseReadAhead() { + return !MixAll.isWindows() && !defaultMessageStore.getMessageStoreConfig().isDataReadAheadEnable(); + } + + public class ColdDataCheckService extends ServiceThread { + private final SystemClock systemClock = new SystemClock(); + private final ConcurrentHashMap pageCacheMap = new ConcurrentHashMap<>(); + private int pageSize = -1; + private int sampleSteps = 32; + + public ColdDataCheckService() { + sampleSteps = defaultMessageStore.getMessageStoreConfig().getSampleSteps(); + if (sampleSteps <= 0) { + sampleSteps = 32; + } + initPageSize(); + scanFilesInPageCache(); } - public ByteBuffer encode(final MessageExtBatch messageExtBatch) { - msgBatchMemory.clear(); //not thread-safe - int totalMsgLen = 0; - ByteBuffer messagesByteBuff = messageExtBatch.wrap(); - while (messagesByteBuff.hasRemaining()) { - // 1 TOTALSIZE - messagesByteBuff.getInt(); - // 2 MAGICCODE - messagesByteBuff.getInt(); - // 3 BODYCRC - messagesByteBuff.getInt(); - // 4 FLAG - int flag = messagesByteBuff.getInt(); - // 5 BODY - int bodyLen = messagesByteBuff.getInt(); - int bodyPos = messagesByteBuff.position(); - int bodyCrc = UtilAll.crc32(messagesByteBuff.array(), bodyPos, bodyLen); - messagesByteBuff.position(bodyPos + bodyLen); - // 6 properties - short propertiesLen = messagesByteBuff.getShort(); - int propertiesPos = messagesByteBuff.position(); - messagesByteBuff.position(propertiesPos + propertiesLen); - - final byte[] topicData = messageExtBatch.getTopic().getBytes(MessageDecoder.CHARSET_UTF8); - - final int topicLength = topicData.length; - - final int msgLen = calMsgLength(bodyLen, topicLength, propertiesLen); - - // Exceeds the maximum message - if (msgLen > this.maxMessageSize) { - CommitLog.log.warn("message size exceeded, msg total size: " + msgLen + ", msg body size: " + bodyLen - + ", maxMessageSize: " + this.maxMessageSize); - throw new RuntimeException("message size exceeded"); + @Override + public String getServiceName() { + return ColdDataCheckService.class.getSimpleName(); + } + + @Override + public void run() { + log.info("{} service started", this.getServiceName()); + while (!this.isStopped()) { + try { + if (MixAll.isWindows() || !defaultMessageStore.getMessageStoreConfig().isColdDataFlowControlEnable() || !defaultMessageStore.getMessageStoreConfig().isColdDataScanEnable()) { + pageCacheMap.clear(); + this.waitForRunning(180 * 1000); + continue; + } else { + this.waitForRunning(defaultMessageStore.getMessageStoreConfig().getTimerColdDataCheckIntervalMs()); + } + + if (pageSize < 0) { + initPageSize(); + } + + long beginClockTimestamp = this.systemClock.now(); + scanFilesInPageCache(); + long costTime = this.systemClock.now() - beginClockTimestamp; + log.info("[{}] scanFilesInPageCache-cost {} ms.", costTime > 30 * 1000 ? "NOTIFYME" : "OK", costTime); + } catch (Throwable e) { + log.warn(this.getServiceName() + " service has e: {}", e); } + } + log.info("{} service end", this.getServiceName()); + } - totalMsgLen += msgLen; - // Determines whether there is sufficient free space - if (totalMsgLen > maxMessageSize) { - throw new RuntimeException("message size exceeded"); + public boolean isDataInPageCache(final long offset) { + if (!defaultMessageStore.getMessageStoreConfig().isColdDataFlowControlEnable()) { + return true; + } + if (pageSize <= 0 || sampleSteps <= 0) { + return true; + } + if (!defaultMessageStore.checkInColdAreaByCommitOffset(offset, getMaxOffset())) { + return true; + } + if (!defaultMessageStore.getMessageStoreConfig().isColdDataScanEnable()) { + return false; + } + + MappedFile mappedFile = mappedFileQueue.findMappedFileByOffset(offset, offset == 0); + if (null == mappedFile) { + return true; + } + byte[] bytes = pageCacheMap.get(mappedFile.getFileName()); + if (null == bytes) { + return true; + } + + int pos = (int) (offset % defaultMessageStore.getMessageStoreConfig().getMappedFileSizeCommitLog()); + int realIndex = pos / pageSize / sampleSteps; + return bytes.length - 1 >= realIndex && bytes[realIndex] != 0; + } + + private void scanFilesInPageCache() { + if (MixAll.isWindows() || !defaultMessageStore.getMessageStoreConfig().isColdDataFlowControlEnable() || !defaultMessageStore.getMessageStoreConfig().isColdDataScanEnable() || pageSize <= 0) { + return; + } + try { + log.info("pageCacheMap key size: {}", pageCacheMap.size()); + clearExpireMappedFile(); + mappedFileQueue.getMappedFiles().forEach(mappedFile -> { + byte[] pageCacheTable = checkFileInPageCache(mappedFile); + if (sampleSteps > 1) { + pageCacheTable = sampling(pageCacheTable, sampleSteps); + } + pageCacheMap.put(mappedFile.getFileName(), pageCacheTable); + }); + } catch (Exception e) { + log.error("scanFilesInPageCache exception", e); + } + } + + private void clearExpireMappedFile() { + Set currentFileSet = mappedFileQueue.getMappedFiles().stream().map(MappedFile::getFileName).collect(Collectors.toSet()); + pageCacheMap.forEach((key, value) -> { + if (!currentFileSet.contains(key)) { + pageCacheMap.remove(key); + log.info("clearExpireMappedFile fileName: {}, has been clear", key); } + }); + } - // 1 TOTALSIZE - this.msgBatchMemory.putInt(msgLen); - // 2 MAGICCODE - this.msgBatchMemory.putInt(CommitLog.MESSAGE_MAGIC_CODE); - // 3 BODYCRC - this.msgBatchMemory.putInt(bodyCrc); - // 4 QUEUEID - this.msgBatchMemory.putInt(messageExtBatch.getQueueId()); - // 5 FLAG - this.msgBatchMemory.putInt(flag); - // 6 QUEUEOFFSET - this.msgBatchMemory.putLong(0); - // 7 PHYSICALOFFSET - this.msgBatchMemory.putLong(0); - // 8 SYSFLAG - this.msgBatchMemory.putInt(messageExtBatch.getSysFlag()); - // 9 BORNTIMESTAMP - this.msgBatchMemory.putLong(messageExtBatch.getBornTimestamp()); - // 10 BORNHOST - this.resetByteBuffer(hostHolder, 8); - this.msgBatchMemory.put(messageExtBatch.getBornHostBytes(hostHolder)); - // 11 STORETIMESTAMP - this.msgBatchMemory.putLong(messageExtBatch.getStoreTimestamp()); - // 12 STOREHOSTADDRESS - this.resetByteBuffer(hostHolder, 8); - this.msgBatchMemory.put(messageExtBatch.getStoreHostBytes(hostHolder)); - // 13 RECONSUMETIMES - this.msgBatchMemory.putInt(messageExtBatch.getReconsumeTimes()); - // 14 Prepared Transaction Offset, batch does not support transaction - this.msgBatchMemory.putLong(0); - // 15 BODY - this.msgBatchMemory.putInt(bodyLen); - if (bodyLen > 0) - this.msgBatchMemory.put(messagesByteBuff.array(), bodyPos, bodyLen); - // 16 TOPIC - this.msgBatchMemory.put((byte) topicLength); - this.msgBatchMemory.put(topicData); - // 17 PROPERTIES - this.msgBatchMemory.putShort(propertiesLen); - if (propertiesLen > 0) - this.msgBatchMemory.put(messagesByteBuff.array(), propertiesPos, propertiesLen); - } - msgBatchMemory.flip(); - return msgBatchMemory; - } - - private void resetByteBuffer(final ByteBuffer byteBuffer, final int limit) { - byteBuffer.flip(); - byteBuffer.limit(limit); + private byte[] sampling(byte[] pageCacheTable, int sampleStep) { + byte[] sample = new byte[(pageCacheTable.length + sampleStep - 1) / sampleStep]; + for (int i = 0, j = 0; i < pageCacheTable.length && j < sample.length; i += sampleStep) { + sample[j++] = pageCacheTable[i]; + } + return sample; + } + + private byte[] checkFileInPageCache(MappedFile mappedFile) { + long fileSize = mappedFile.getFileSize(); + final long address = ((DirectBuffer) mappedFile.getMappedByteBuffer()).address(); + int pageNums = (int) (fileSize + this.pageSize - 1) / this.pageSize; + byte[] pageCacheRst = new byte[pageNums]; + int mincore = LibC.INSTANCE.mincore(new Pointer(address), new NativeLong(fileSize), pageCacheRst); + if (mincore != 0) { + log.error("checkFileInPageCache call the LibC.INSTANCE.mincore error, fileName: {}, fileSize: {}", + mappedFile.getFileName(), fileSize); + for (int i = 0; i < pageNums; i++) { + pageCacheRst[i] = 1; + } + } + return pageCacheRst; } + private void initPageSize() { + if (pageSize < 0 && defaultMessageStore.getMessageStoreConfig().isColdDataFlowControlEnable()) { + try { + if (!MixAll.isWindows()) { + pageSize = LibC.INSTANCE.getpagesize(); + } else { + defaultMessageStore.getMessageStoreConfig().setColdDataFlowControlEnable(false); + log.info("windows os, coldDataCheckEnable force setting to be false"); + } + log.info("initPageSize pageSize: {}", pageSize); + } catch (Exception e) { + defaultMessageStore.getMessageStoreConfig().setColdDataFlowControlEnable(false); + log.error("initPageSize error, coldDataCheckEnable force setting to be false ", e); + } + } + } + + /** + * this result is not high accurate. + */ + public boolean isMsgInColdArea(String group, String topic, int queueId, long offset) { + if (!defaultMessageStore.getMessageStoreConfig().isColdDataFlowControlEnable()) { + return false; + } + try { + ConsumeQueue consumeQueue = (ConsumeQueue) defaultMessageStore.findConsumeQueue(topic, queueId); + if (null == consumeQueue) { + return false; + } + SelectMappedBufferResult bufferConsumeQueue = consumeQueue.getIndexBuffer(offset); + if (null == bufferConsumeQueue || null == bufferConsumeQueue.getByteBuffer()) { + return false; + } + long offsetPy = bufferConsumeQueue.getByteBuffer().getLong(); + return defaultMessageStore.checkInColdAreaByCommitOffset(offsetPy, getMaxOffset()); + } catch (Exception e) { + log.error("isMsgInColdArea group: {}, topic: {}, queueId: {}, offset: {}", + group, topic, queueId, offset, e); + } + return false; + } + } + + public void scanFileAndSetReadMode(int mode) { + if (MixAll.isWindows()) { + log.info("windows os stop scanFileAndSetReadMode"); + return; + } + try { + log.info("scanFileAndSetReadMode mode: {}", mode); + mappedFileQueue.getMappedFiles().forEach(mappedFile -> { + setFileReadMode(mappedFile, mode); + }); + } catch (Exception e) { + log.error("scanFileAndSetReadMode exception", e); + } + } + + private int setFileReadMode(MappedFile mappedFile, int mode) { + if (null == mappedFile) { + log.error("setFileReadMode mappedFile is null"); + return -1; + } + final long address = ((DirectBuffer) mappedFile.getMappedByteBuffer()).address(); + int madvise = LibC.INSTANCE.madvise(new Pointer(address), new NativeLong(mappedFile.getFileSize()), mode); + if (madvise != 0) { + log.error("setFileReadMode error fileName: {}, madvise: {}, mode:{}", mappedFile.getFileName(), madvise, mode); + } + return madvise; + } + + public ColdDataCheckService getColdDataCheckService() { + return coldDataCheckService; } } diff --git a/store/src/main/java/org/apache/rocketmq/store/CommitLogDispatcher.java b/store/src/main/java/org/apache/rocketmq/store/CommitLogDispatcher.java index e1564a9ef58..f3a7b7c5c43 100644 --- a/store/src/main/java/org/apache/rocketmq/store/CommitLogDispatcher.java +++ b/store/src/main/java/org/apache/rocketmq/store/CommitLogDispatcher.java @@ -17,10 +17,17 @@ package org.apache.rocketmq.store; +import org.rocksdb.RocksDBException; + /** * Dispatcher of commit log. */ public interface CommitLogDispatcher { - void dispatch(final DispatchRequest request); + /** + * Dispatch messages from store to build consume queues, indexes, and filter data + * @param request dispatch message request + * @throws RocksDBException only in rocksdb mode + */ + void dispatch(final DispatchRequest request) throws RocksDBException; } diff --git a/store/src/main/java/org/apache/rocketmq/store/CompactionAppendMsgCallback.java b/store/src/main/java/org/apache/rocketmq/store/CompactionAppendMsgCallback.java new file mode 100644 index 00000000000..1667144401e --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/CompactionAppendMsgCallback.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store; + +import java.nio.ByteBuffer; + +public interface CompactionAppendMsgCallback { + AppendMessageResult doAppend(ByteBuffer bbDest, long fileFromOffset, int maxBlank, ByteBuffer bbSrc); +} diff --git a/store/src/main/java/org/apache/rocketmq/store/ConsumeQueue.java b/store/src/main/java/org/apache/rocketmq/store/ConsumeQueue.java index 09807c36e12..453c9d1dc72 100644 --- a/store/src/main/java/org/apache/rocketmq/store/ConsumeQueue.java +++ b/store/src/main/java/org/apache/rocketmq/store/ConsumeQueue.java @@ -18,19 +18,49 @@ import java.io.File; import java.nio.ByteBuffer; +import java.util.Collections; import java.util.List; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.BoundaryType; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.attribute.CQType; import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.config.BrokerRole; import org.apache.rocketmq.store.config.StorePathConfigHelper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class ConsumeQueue { +import org.apache.rocketmq.store.logfile.MappedFile; +import org.apache.rocketmq.store.queue.ConsumeQueueInterface; +import org.apache.rocketmq.store.queue.CqUnit; +import org.apache.rocketmq.store.queue.FileQueueLifeCycle; +import org.apache.rocketmq.store.queue.MultiDispatchUtils; +import org.apache.rocketmq.store.queue.QueueOffsetOperator; +import org.apache.rocketmq.store.queue.ReferredIterator; + +public class ConsumeQueue implements ConsumeQueueInterface, FileQueueLifeCycle { private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + /** + * ConsumeQueue's store unit. Format: + *

    +     * ┌───────────────────────────────┬───────────────────┬───────────────────────────────┐
    +     * │    CommitLog Physical Offset  │      Body Size    │            Tag HashCode       │
    +     * │          (8 Bytes)            │      (4 Bytes)    │             (8 Bytes)         │
    +     * ├───────────────────────────────┴───────────────────┴───────────────────────────────┤
    +     * │                                     Store Unit                                    │
    +     * │                                                                                   │
    +     * 
    + * ConsumeQueue's store unit. Size: CommitLog Physical Offset(8) + Body Size(4) + Tag HashCode(8) = 20 Bytes + */ public static final int CQ_STORE_UNIT_SIZE = 20; + public static final int MSG_TAG_OFFSET_INDEX = 12; private static final Logger LOG_ERROR = LoggerFactory.getLogger(LoggerName.STORE_ERROR_LOGGER_NAME); - private final DefaultMessageStore defaultMessageStore; + private final MessageStore messageStore; private final MappedFileQueue mappedFileQueue; private final String topic; @@ -40,6 +70,10 @@ public class ConsumeQueue { private final String storePath; private final int mappedFileSize; private long maxPhysicOffset = -1; + + /** + * Minimum offset of the consume file queue that points to valid commit log record. + */ private volatile long minLogicOffset = 0; private ConsumeQueueExt consumeQueueExt = null; @@ -48,10 +82,10 @@ public ConsumeQueue( final int queueId, final String storePath, final int mappedFileSize, - final DefaultMessageStore defaultMessageStore) { + final MessageStore messageStore) { this.storePath = storePath; this.mappedFileSize = mappedFileSize; - this.defaultMessageStore = defaultMessageStore; + this.messageStore = messageStore; this.topic = topic; this.queueId = queueId; @@ -64,17 +98,18 @@ public ConsumeQueue( this.byteBufferIndex = ByteBuffer.allocate(CQ_STORE_UNIT_SIZE); - if (defaultMessageStore.getMessageStoreConfig().isEnableConsumeQueueExt()) { + if (messageStore.getMessageStoreConfig().isEnableConsumeQueueExt()) { this.consumeQueueExt = new ConsumeQueueExt( topic, queueId, - StorePathConfigHelper.getStorePathConsumeQueueExt(defaultMessageStore.getMessageStoreConfig().getStorePathRootDir()), - defaultMessageStore.getMessageStoreConfig().getMappedFileSizeConsumeQueueExt(), - defaultMessageStore.getMessageStoreConfig().getBitMapLengthConsumeQueueExt() + StorePathConfigHelper.getStorePathConsumeQueueExt(messageStore.getMessageStoreConfig().getStorePathRootDir()), + messageStore.getMessageStoreConfig().getMappedFileSizeConsumeQueueExt(), + messageStore.getMessageStoreConfig().getBitMapLengthConsumeQueueExt() ); } } + @Override public boolean load() { boolean result = this.mappedFileQueue.load(); log.info("load consume queue " + this.topic + "-" + this.queueId + " " + (result ? "OK" : "Failed")); @@ -84,13 +119,15 @@ public boolean load() { return result; } + @Override public void recover() { final List mappedFiles = this.mappedFileQueue.getMappedFiles(); if (!mappedFiles.isEmpty()) { int index = mappedFiles.size() - 3; - if (index < 0) + if (index < 0) { index = 0; + } int mappedFileSizeLogics = this.mappedFileSize; MappedFile mappedFile = mappedFiles.get(index); @@ -106,7 +143,7 @@ public void recover() { if (offset >= 0 && size > 0) { mappedFileOffset = i + CQ_STORE_UNIT_SIZE; - this.maxPhysicOffset = offset; + this.setMaxPhysicOffset(offset + size); if (isExtAddr(tagsCode)) { maxExtAddr = tagsCode; } @@ -132,7 +169,7 @@ public void recover() { log.info("recover next consume queue file, " + mappedFile.getFileName()); } } else { - log.info("recover current consume queue queue over " + mappedFile.getFileName() + " " + log.info("recover current consume queue over " + mappedFile.getFileName() + " " + (processOffset + mappedFileOffset)); break; } @@ -151,34 +188,110 @@ public void recover() { } } + @Override + public long getTotalSize() { + long totalSize = this.mappedFileQueue.getTotalFileSize(); + if (isExtReadEnable()) { + totalSize += this.consumeQueueExt.getTotalSize(); + } + return totalSize; + } + + @Override + public int getUnitSize() { + return CQ_STORE_UNIT_SIZE; + } + + @Deprecated + @Override public long getOffsetInQueueByTime(final long timestamp) { - MappedFile mappedFile = this.mappedFileQueue.getMappedFileByTime(timestamp); + MappedFile mappedFile = this.mappedFileQueue.getConsumeQueueMappedFileByTime(timestamp, + messageStore.getCommitLog(), BoundaryType.LOWER); + return binarySearchInQueueByTime(mappedFile, timestamp, BoundaryType.LOWER); + } + + @Override + public long getOffsetInQueueByTime(final long timestamp, final BoundaryType boundaryType) { + MappedFile mappedFile = this.mappedFileQueue.getConsumeQueueMappedFileByTime(timestamp, + messageStore.getCommitLog(), boundaryType); + return binarySearchInQueueByTime(mappedFile, timestamp, boundaryType); + } + + private long binarySearchInQueueByTime(final MappedFile mappedFile, final long timestamp, + BoundaryType boundaryType) { if (mappedFile != null) { long offset = 0; int low = minLogicOffset > mappedFile.getFileFromOffset() ? (int) (minLogicOffset - mappedFile.getFileFromOffset()) : 0; int high = 0; int midOffset = -1, targetOffset = -1, leftOffset = -1, rightOffset = -1; - long leftIndexValue = -1L, rightIndexValue = -1L; - long minPhysicOffset = this.defaultMessageStore.getMinPhyOffset(); - SelectMappedBufferResult sbr = mappedFile.selectMappedBuffer(0); + long minPhysicOffset = this.messageStore.getMinPhyOffset(); + int range = mappedFile.getFileSize(); + if (mappedFile.getWrotePosition() != 0 && mappedFile.getWrotePosition() != mappedFile.getFileSize()) { + // mappedFile is the last one and is currently being written. + range = mappedFile.getWrotePosition(); + } + SelectMappedBufferResult sbr = mappedFile.selectMappedBuffer(0, range); if (null != sbr) { ByteBuffer byteBuffer = sbr.getByteBuffer(); - high = byteBuffer.limit() - CQ_STORE_UNIT_SIZE; + int ceiling = byteBuffer.limit() - CQ_STORE_UNIT_SIZE; + int floor = low; + high = ceiling; try { + // Handle the following corner cases first: + // 1. store time of (high) < timestamp + // 2. store time of (low) > timestamp + long storeTime; + long phyOffset; + int size; + // Handle case 1 + byteBuffer.position(ceiling); + phyOffset = byteBuffer.getLong(); + size = byteBuffer.getInt(); + storeTime = messageStore.getCommitLog().pickupStoreTimestamp(phyOffset, size); + if (storeTime < timestamp) { + switch (boundaryType) { + case LOWER: + return (mappedFile.getFileFromOffset() + ceiling + CQ_STORE_UNIT_SIZE) / CQ_STORE_UNIT_SIZE; + case UPPER: + return (mappedFile.getFileFromOffset() + ceiling) / CQ_STORE_UNIT_SIZE; + default: + log.warn("Unknown boundary type"); + break; + } + } + + // Handle case 2 + byteBuffer.position(floor); + phyOffset = byteBuffer.getLong(); + size = byteBuffer.getInt(); + storeTime = messageStore.getCommitLog().pickupStoreTimestamp(phyOffset, size); + if (storeTime > timestamp) { + switch (boundaryType) { + case LOWER: + return mappedFile.getFileFromOffset() / CQ_STORE_UNIT_SIZE; + case UPPER: + return 0; + default: + log.warn("Unknown boundary type"); + break; + } + } + + // Perform binary search while (high >= low) { midOffset = (low + high) / (2 * CQ_STORE_UNIT_SIZE) * CQ_STORE_UNIT_SIZE; byteBuffer.position(midOffset); - long phyOffset = byteBuffer.getLong(); - int size = byteBuffer.getInt(); + phyOffset = byteBuffer.getLong(); + size = byteBuffer.getInt(); if (phyOffset < minPhysicOffset) { low = midOffset + CQ_STORE_UNIT_SIZE; leftOffset = midOffset; continue; } - long storeTime = - this.defaultMessageStore.getCommitLog().pickupStoreTimestamp(phyOffset, size); + storeTime = this.messageStore.getCommitLog().pickupStoreTimestamp(phyOffset, size); if (storeTime < 0) { + log.warn("Failed to query store timestamp for commit log offset: {}", phyOffset); return 0; } else if (storeTime == timestamp) { targetOffset = midOffset; @@ -186,31 +299,96 @@ public long getOffsetInQueueByTime(final long timestamp) { } else if (storeTime > timestamp) { high = midOffset - CQ_STORE_UNIT_SIZE; rightOffset = midOffset; - rightIndexValue = storeTime; } else { low = midOffset + CQ_STORE_UNIT_SIZE; leftOffset = midOffset; - leftIndexValue = storeTime; } } if (targetOffset != -1) { - + // We just found ONE matched record. These next to it might also share the same store-timestamp. offset = targetOffset; + switch (boundaryType) { + case LOWER: { + int previousAttempt = targetOffset; + while (true) { + int attempt = previousAttempt - CQ_STORE_UNIT_SIZE; + if (attempt < floor) { + break; + } + byteBuffer.position(attempt); + long physicalOffset = byteBuffer.getLong(); + int messageSize = byteBuffer.getInt(); + long messageStoreTimestamp = messageStore.getCommitLog() + .pickupStoreTimestamp(physicalOffset, messageSize); + if (messageStoreTimestamp == timestamp) { + previousAttempt = attempt; + continue; + } + break; + } + offset = previousAttempt; + break; + } + case UPPER: { + int previousAttempt = targetOffset; + while (true) { + int attempt = previousAttempt + CQ_STORE_UNIT_SIZE; + if (attempt > ceiling) { + break; + } + byteBuffer.position(attempt); + long physicalOffset = byteBuffer.getLong(); + int messageSize = byteBuffer.getInt(); + long messageStoreTimestamp = messageStore.getCommitLog() + .pickupStoreTimestamp(physicalOffset, messageSize); + if (messageStoreTimestamp == timestamp) { + previousAttempt = attempt; + continue; + } + break; + } + offset = previousAttempt; + break; + } + default: { + log.warn("Unknown boundary type"); + break; + } + } } else { - if (leftIndexValue == -1) { - - offset = rightOffset; - } else if (rightIndexValue == -1) { + // Given timestamp does not have any message records. But we have a range enclosing the + // timestamp. + /* + * Consider the follow case: t2 has no consume queue entry and we are searching offset of + * t2 for lower and upper boundaries. + * -------------------------- + * timestamp Consume Queue + * t1 1 + * t1 2 + * t1 3 + * t3 4 + * t3 5 + * -------------------------- + * Now, we return 3 as upper boundary of t2 and 4 as its lower boundary. It looks + * contradictory at first sight, but it does make sense when performing range queries. + */ + switch (boundaryType) { + case LOWER: { + offset = rightOffset; + break; + } - offset = leftOffset; - } else { - offset = - Math.abs(timestamp - leftIndexValue) > Math.abs(timestamp - - rightIndexValue) ? rightOffset : leftOffset; + case UPPER: { + offset = leftOffset; + break; + } + default: { + log.warn("Unknown boundary type"); + break; + } } } - return (mappedFile.getFileFromOffset() + offset) / CQ_STORE_UNIT_SIZE; } finally { sbr.release(); @@ -220,12 +398,18 @@ public long getOffsetInQueueByTime(final long timestamp) { return 0; } - public void truncateDirtyLogicFiles(long phyOffet) { + @Override + public void truncateDirtyLogicFiles(long phyOffset) { + truncateDirtyLogicFiles(phyOffset, true); + } + + public void truncateDirtyLogicFiles(long phyOffset, boolean deleteFile) { int logicFileSize = this.mappedFileSize; - this.maxPhysicOffset = phyOffet - 1; + this.setMaxPhysicOffset(phyOffset); long maxExtAddr = 1; + boolean shouldDeleteFile = false; while (true) { MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(); if (mappedFile != null) { @@ -241,15 +425,15 @@ public void truncateDirtyLogicFiles(long phyOffet) { long tagsCode = byteBuffer.getLong(); if (0 == i) { - if (offset >= phyOffet) { - this.mappedFileQueue.deleteLastMappedFile(); + if (offset >= phyOffset) { + shouldDeleteFile = true; break; } else { int pos = i + CQ_STORE_UNIT_SIZE; mappedFile.setWrotePosition(pos); mappedFile.setCommittedPosition(pos); mappedFile.setFlushedPosition(pos); - this.maxPhysicOffset = offset; + this.setMaxPhysicOffset(offset + size); // This maybe not take effect, when not every consume queue has extend file. if (isExtAddr(tagsCode)) { maxExtAddr = tagsCode; @@ -259,7 +443,7 @@ public void truncateDirtyLogicFiles(long phyOffet) { if (offset >= 0 && size > 0) { - if (offset >= phyOffet) { + if (offset >= phyOffset) { return; } @@ -267,7 +451,7 @@ public void truncateDirtyLogicFiles(long phyOffet) { mappedFile.setWrotePosition(pos); mappedFile.setCommittedPosition(pos); mappedFile.setFlushedPosition(pos); - this.maxPhysicOffset = offset; + this.setMaxPhysicOffset(offset + size); if (isExtAddr(tagsCode)) { maxExtAddr = tagsCode; } @@ -280,6 +464,15 @@ public void truncateDirtyLogicFiles(long phyOffet) { } } } + + if (shouldDeleteFile) { + if (deleteFile) { + this.mappedFileQueue.deleteLastMappedFile(); + } else { + this.mappedFileQueue.deleteExpiredFile(Collections.singletonList(this.mappedFileQueue.getLastMappedFile())); + } + } + } else { break; } @@ -290,6 +483,7 @@ public void truncateDirtyLogicFiles(long phyOffet) { } } + @Override public long getLastOffset() { long lastOffset = -1; @@ -320,6 +514,7 @@ public long getLastOffset() { return lastOffset; } + @Override public boolean flush(final int flushLeastPages) { boolean result = this.mappedFileQueue.flush(flushLeastPages); if (isExtReadEnable()) { @@ -329,40 +524,142 @@ public boolean flush(final int flushLeastPages) { return result; } + @Override public int deleteExpiredFile(long offset) { int cnt = this.mappedFileQueue.deleteExpiredFileByOffset(offset, CQ_STORE_UNIT_SIZE); this.correctMinOffset(offset); return cnt; } - public void correctMinOffset(long phyMinOffset) { + /** + * Update minLogicOffset such that entries after it would point to valid commit log address. + * + * @param minCommitLogOffset Minimum commit log offset + */ + @Override + public void correctMinOffset(long minCommitLogOffset) { + // Check if the consume queue is the state of deprecation. + if (minLogicOffset >= mappedFileQueue.getMaxOffset()) { + log.info("ConsumeQueue[Topic={}, queue-id={}] contains no valid entries", topic, queueId); + return; + } + + // Check whether the consume queue maps no valid data at all. This check may cost 1 IO operation. + // The rationale is that consume queue always preserves the last file. In case there are many deprecated topics, + // This check would save a lot of efforts. + MappedFile lastMappedFile = this.mappedFileQueue.getLastMappedFile(); + if (null == lastMappedFile) { + return; + } + + SelectMappedBufferResult lastRecord = null; + try { + int maxReadablePosition = lastMappedFile.getReadPosition(); + lastRecord = lastMappedFile.selectMappedBuffer(maxReadablePosition - ConsumeQueue.CQ_STORE_UNIT_SIZE, + ConsumeQueue.CQ_STORE_UNIT_SIZE); + if (null != lastRecord) { + ByteBuffer buffer = lastRecord.getByteBuffer(); + long commitLogOffset = buffer.getLong(); + if (commitLogOffset < minCommitLogOffset) { + // Keep the largest known consume offset, even if this consume-queue contains no valid entries at + // all. Let minLogicOffset point to a future slot. + this.minLogicOffset = lastMappedFile.getFileFromOffset() + maxReadablePosition; + log.info("ConsumeQueue[topic={}, queue-id={}] contains no valid entries. Min-offset is assigned as: {}.", + topic, queueId, getMinOffsetInQueue()); + return; + } + } + } finally { + if (null != lastRecord) { + lastRecord.release(); + } + } + MappedFile mappedFile = this.mappedFileQueue.getFirstMappedFile(); long minExtAddr = 1; if (mappedFile != null) { - SelectMappedBufferResult result = mappedFile.selectMappedBuffer(0); - if (result != null) { - try { - for (int i = 0; i < result.getSize(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE) { - long offsetPy = result.getByteBuffer().getLong(); - result.getByteBuffer().getInt(); - long tagsCode = result.getByteBuffer().getLong(); - - if (offsetPy >= phyMinOffset) { - this.minLogicOffset = result.getMappedFile().getFileFromOffset() + i; - log.info("Compute logical min offset: {}, topic: {}, queueId: {}", - this.getMinOffsetInQueue(), this.topic, this.queueId); - // This maybe not take effect, when not every consume queue has extend file. - if (isExtAddr(tagsCode)) { - minExtAddr = tagsCode; - } - break; + // Search from previous min logical offset. Typically, a consume queue file segment contains 300,000 entries + // searching from previous position saves significant amount of comparisons and IOs + boolean intact = true; // Assume previous value is still valid + long start = this.minLogicOffset - mappedFile.getFileFromOffset(); + if (start < 0) { + intact = false; + start = 0; + } + + if (start > mappedFile.getReadPosition()) { + log.error("[Bug][InconsistentState] ConsumeQueue file {} should have been deleted", + mappedFile.getFileName()); + return; + } + + SelectMappedBufferResult result = mappedFile.selectMappedBuffer((int) start); + if (result == null) { + log.warn("[Bug] Failed to scan consume queue entries from file on correcting min offset: {}", + mappedFile.getFileName()); + return; + } + + try { + // No valid consume entries + if (result.getSize() == 0) { + log.debug("ConsumeQueue[topic={}, queue-id={}] contains no valid entries", topic, queueId); + return; + } + + ByteBuffer buffer = result.getByteBuffer().slice(); + // Verify whether the previous value is still valid or not before conducting binary search + long commitLogOffset = buffer.getLong(); + if (intact && commitLogOffset >= minCommitLogOffset) { + log.info("Abort correction as previous min-offset points to {}, which is greater than {}", + commitLogOffset, minCommitLogOffset); + return; + } + + // Binary search between range [previous_min_logic_offset, first_file_from_offset + file_size) + // Note the consume-queue deletion procedure ensures the last entry points to somewhere valid. + int low = 0; + int high = result.getSize() - ConsumeQueue.CQ_STORE_UNIT_SIZE; + while (true) { + if (high - low <= ConsumeQueue.CQ_STORE_UNIT_SIZE) { + break; + } + int mid = (low + high) / 2 / ConsumeQueue.CQ_STORE_UNIT_SIZE * ConsumeQueue.CQ_STORE_UNIT_SIZE; + buffer.position(mid); + commitLogOffset = buffer.getLong(); + if (commitLogOffset > minCommitLogOffset) { + high = mid; + } else if (commitLogOffset == minCommitLogOffset) { + low = mid; + high = mid; + break; + } else { + low = mid; + } + } + + // Examine the last one or two entries + for (int i = low; i <= high; i += ConsumeQueue.CQ_STORE_UNIT_SIZE) { + buffer.position(i); + long offsetPy = buffer.getLong(); + buffer.position(i + 12); + long tagsCode = buffer.getLong(); + + if (offsetPy >= minCommitLogOffset) { + this.minLogicOffset = mappedFile.getFileFromOffset() + start + i; + log.info("Compute logical min offset: {}, topic: {}, queueId: {}", + this.getMinOffsetInQueue(), this.topic, this.queueId); + // This maybe not take effect, when not every consume queue has an extended file. + if (isExtAddr(tagsCode)) { + minExtAddr = tagsCode; } + break; } - } catch (Exception e) { - log.error("Exception thrown when correctMinOffset", e); - } finally { - result.release(); } + } catch (Exception e) { + log.error("Exception thrown when correctMinOffset", e); + } finally { + result.release(); } } @@ -371,13 +668,15 @@ public void correctMinOffset(long phyMinOffset) { } } + @Override public long getMinOffsetInQueue() { return this.minLogicOffset / CQ_STORE_UNIT_SIZE; } + @Override public void putMessagePositionInfoWrapper(DispatchRequest request) { final int maxRetries = 30; - boolean canWrite = this.defaultMessageStore.getRunningFlags().isCQWriteable(); + boolean canWrite = this.messageStore.getRunningFlags().isCQWriteable(); for (int i = 0; i < maxRetries && canWrite; i++) { long tagsCode = request.getTagsCode(); if (isExtWriteEnable()) { @@ -397,7 +696,14 @@ public void putMessagePositionInfoWrapper(DispatchRequest request) { boolean result = this.putMessagePositionInfo(request.getCommitLogOffset(), request.getMsgSize(), tagsCode, request.getConsumeQueueOffset()); if (result) { - this.defaultMessageStore.getStoreCheckpoint().setLogicsMsgTimestamp(request.getStoreTimestamp()); + if (this.messageStore.getMessageStoreConfig().getBrokerRole() == BrokerRole.SLAVE || + this.messageStore.getMessageStoreConfig().isEnableDLegerCommitLog()) { + this.messageStore.getStoreCheckpoint().setPhysicMsgTimestamp(request.getStoreTimestamp()); + } + this.messageStore.getStoreCheckpoint().setLogicsMsgTimestamp(request.getStoreTimestamp()); + if (MultiDispatchUtils.checkMultiDispatchQueue(this.messageStore.getMessageStoreConfig(), request)) { + multiDispatchLmqQueue(request, maxRetries); + } return; } else { // XXX: warn and notify me @@ -414,13 +720,75 @@ public void putMessagePositionInfoWrapper(DispatchRequest request) { // XXX: warn and notify me log.error("[BUG]consume queue can not write, {} {}", this.topic, this.queueId); - this.defaultMessageStore.getRunningFlags().makeLogicsQueueError(); + this.messageStore.getRunningFlags().makeLogicsQueueError(); + } + + private void multiDispatchLmqQueue(DispatchRequest request, int maxRetries) { + Map prop = request.getPropertiesMap(); + String multiDispatchQueue = prop.get(MessageConst.PROPERTY_INNER_MULTI_DISPATCH); + String multiQueueOffset = prop.get(MessageConst.PROPERTY_INNER_MULTI_QUEUE_OFFSET); + String[] queues = multiDispatchQueue.split(MixAll.MULTI_DISPATCH_QUEUE_SPLITTER); + String[] queueOffsets = multiQueueOffset.split(MixAll.MULTI_DISPATCH_QUEUE_SPLITTER); + if (queues.length != queueOffsets.length) { + log.error("[bug] queues.length!=queueOffsets.length ", request.getTopic()); + return; + } + for (int i = 0; i < queues.length; i++) { + String queueName = queues[i]; + if (StringUtils.contains(queueName, File.separator)) { + continue; + } + long queueOffset = Long.parseLong(queueOffsets[i]); + int queueId = request.getQueueId(); + if (this.messageStore.getMessageStoreConfig().isEnableLmq() && MixAll.isLmq(queueName)) { + queueId = 0; + } + doDispatchLmqQueue(request, maxRetries, queueName, queueOffset, queueId); + } + } + + private void doDispatchLmqQueue(DispatchRequest request, int maxRetries, String queueName, long queueOffset, + int queueId) { + ConsumeQueueInterface cq = this.messageStore.findConsumeQueue(queueName, queueId); + boolean canWrite = this.messageStore.getRunningFlags().isCQWriteable(); + for (int i = 0; i < maxRetries && canWrite; i++) { + boolean result = ((ConsumeQueue) cq).putMessagePositionInfo(request.getCommitLogOffset(), request.getMsgSize(), + request.getTagsCode(), + queueOffset); + if (result) { + break; + } else { + log.warn("[BUG]put commit log position info to " + queueName + ":" + queueId + " " + request.getCommitLogOffset() + + " failed, retry " + i + " times"); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + log.warn("", e); + } + } + } + } + + @Override + public void assignQueueOffset(QueueOffsetOperator queueOffsetOperator, MessageExtBrokerInner msg) { + String topicQueueKey = getTopic() + "-" + getQueueId(); + long queueOffset = queueOffsetOperator.getQueueOffset(topicQueueKey); + msg.setQueueOffset(queueOffset); + } + + @Override + public void increaseQueueOffset(QueueOffsetOperator queueOffsetOperator, MessageExtBrokerInner msg, + short messageNum) { + String topicQueueKey = getTopic() + "-" + getQueueId(); + queueOffsetOperator.increaseQueueOffset(topicQueueKey, messageNum); } private boolean putMessagePositionInfo(final long offset, final int size, final long tagsCode, final long cqOffset) { - if (offset <= this.maxPhysicOffset) { + if (offset + size <= this.getMaxPhysicOffset()) { + log.warn("Maybe try to build consume queue repeatedly maxPhysicOffset={} phyOffset={}", this.getMaxPhysicOffset(), offset); return true; } @@ -464,7 +832,7 @@ private boolean putMessagePositionInfo(final long offset, final int size, final ); } } - this.maxPhysicOffset = offset; + this.setMaxPhysicOffset(offset + size); return mappedFile.appendMessage(this.byteBufferIndex.array()); } return false; @@ -488,13 +856,149 @@ public SelectMappedBufferResult getIndexBuffer(final long startIndex) { if (offset >= this.getMinLogicOffset()) { MappedFile mappedFile = this.mappedFileQueue.findMappedFileByOffset(offset); if (mappedFile != null) { - SelectMappedBufferResult result = mappedFile.selectMappedBuffer((int) (offset % mappedFileSize)); - return result; + return mappedFile.selectMappedBuffer((int) (offset % mappedFileSize)); } } return null; } + @Override + public ReferredIterator iterateFrom(long startOffset) { + SelectMappedBufferResult sbr = getIndexBuffer(startOffset); + if (sbr == null) { + return null; + } + return new ConsumeQueueIterator(sbr); + } + + @Override + public ReferredIterator iterateFrom(long startIndex, int count) { + return iterateFrom(startIndex); + } + + @Override + public CqUnit get(long offset) { + ReferredIterator it = iterateFrom(offset); + if (it == null) { + return null; + } + return it.nextAndRelease(); + } + + @Override + public Pair getCqUnitAndStoreTime(long index) { + CqUnit cqUnit = get(index); + Long messageStoreTime = this.messageStore.getQueueStore().getStoreTime(cqUnit); + return new Pair<>(cqUnit, messageStoreTime); + } + + @Override + public Pair getEarliestUnitAndStoreTime() { + CqUnit cqUnit = getEarliestUnit(); + Long messageStoreTime = this.messageStore.getQueueStore().getStoreTime(cqUnit); + return new Pair<>(cqUnit, messageStoreTime); + } + + @Override + public CqUnit getEarliestUnit() { + /** + * here maybe should not return null + */ + ReferredIterator it = iterateFrom(minLogicOffset / CQ_STORE_UNIT_SIZE); + if (it == null) { + return null; + } + return it.nextAndRelease(); + } + + @Override + public CqUnit getLatestUnit() { + ReferredIterator it = iterateFrom((mappedFileQueue.getMaxOffset() / CQ_STORE_UNIT_SIZE) - 1); + if (it == null) { + return null; + } + return it.nextAndRelease(); + } + + @Override + public boolean isFirstFileAvailable() { + return false; + } + + @Override + public boolean isFirstFileExist() { + return false; + } + + private class ConsumeQueueIterator implements ReferredIterator { + private SelectMappedBufferResult sbr; + private int relativePos = 0; + + public ConsumeQueueIterator(SelectMappedBufferResult sbr) { + this.sbr = sbr; + if (sbr != null && sbr.getByteBuffer() != null) { + relativePos = sbr.getByteBuffer().position(); + } + } + + @Override + public boolean hasNext() { + if (sbr == null || sbr.getByteBuffer() == null) { + return false; + } + + return sbr.getByteBuffer().hasRemaining(); + } + + @Override + public CqUnit next() { + if (!hasNext()) { + return null; + } + long queueOffset = (sbr.getStartOffset() + sbr.getByteBuffer().position() - relativePos) / CQ_STORE_UNIT_SIZE; + CqUnit cqUnit = new CqUnit(queueOffset, + sbr.getByteBuffer().getLong(), + sbr.getByteBuffer().getInt(), + sbr.getByteBuffer().getLong()); + + if (isExtAddr(cqUnit.getTagsCode())) { + ConsumeQueueExt.CqExtUnit cqExtUnit = new ConsumeQueueExt.CqExtUnit(); + boolean extRet = getExt(cqUnit.getTagsCode(), cqExtUnit); + if (extRet) { + cqUnit.setTagsCode(cqExtUnit.getTagsCode()); + cqUnit.setCqExtUnit(cqExtUnit); + } else { + // can't find ext content.Client will filter messages by tag also. + log.error("[BUG] can't find consume queue extend file content! addr={}, offsetPy={}, sizePy={}, topic={}", + cqUnit.getTagsCode(), cqUnit.getPos(), cqUnit.getPos(), getTopic()); + } + } + return cqUnit; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove"); + } + + @Override + public void release() { + if (sbr != null) { + sbr.release(); + sbr = null; + } + } + + @Override + public CqUnit nextAndRelease() { + try { + return next(); + } finally { + release(); + } + } + } + public ConsumeQueueExt.CqExtUnit getExt(final long offset) { if (isExtReadEnable()) { return this.consumeQueueExt.get(offset); @@ -509,6 +1013,7 @@ public boolean getExt(final long offset, ConsumeQueueExt.CqExtUnit cqExtUnit) { return false; } + @Override public long getMinLogicOffset() { return minLogicOffset; } @@ -517,20 +1022,29 @@ public void setMinLogicOffset(long minLogicOffset) { this.minLogicOffset = minLogicOffset; } - public long rollNextFile(final long index) { + @Override + public long rollNextFile(final long nextBeginOffset) { int mappedFileSize = this.mappedFileSize; int totalUnitsInFile = mappedFileSize / CQ_STORE_UNIT_SIZE; - return index + totalUnitsInFile - index % totalUnitsInFile; + return nextBeginOffset + totalUnitsInFile - nextBeginOffset % totalUnitsInFile; } + @Override public String getTopic() { return topic; } + @Override public int getQueueId() { return queueId; } + @Override + public CQType getCQType() { + return CQType.SimpleCQ; + } + + @Override public long getMaxPhysicOffset() { return maxPhysicOffset; } @@ -539,8 +1053,9 @@ public void setMaxPhysicOffset(long maxPhysicOffset) { this.maxPhysicOffset = maxPhysicOffset; } + @Override public void destroy() { - this.maxPhysicOffset = -1; + this.setMaxPhysicOffset(-1); this.minLogicOffset = 0; this.mappedFileQueue.destroy(); if (isExtReadEnable()) { @@ -548,14 +1063,17 @@ public void destroy() { } } + @Override public long getMessageTotalInQueue() { return this.getMaxOffsetInQueue() - this.getMinOffsetInQueue(); } + @Override public long getMaxOffsetInQueue() { return this.mappedFileQueue.getMaxOffset() / CQ_STORE_UNIT_SIZE; } + @Override public void checkSelf() { mappedFileQueue.checkSelf(); if (isExtReadEnable()) { @@ -569,7 +1087,7 @@ protected boolean isExtReadEnable() { protected boolean isExtWriteEnable() { return this.consumeQueueExt != null - && this.defaultMessageStore.getMessageStoreConfig().isEnableConsumeQueueExt(); + && this.messageStore.getMessageStoreConfig().isEnableConsumeQueueExt(); } /** @@ -578,4 +1096,101 @@ protected boolean isExtWriteEnable() { public boolean isExtAddr(long tagsCode) { return ConsumeQueueExt.isExtAddr(tagsCode); } + + @Override + public void swapMap(int reserveNum, long forceSwapIntervalMs, long normalSwapIntervalMs) { + mappedFileQueue.swapMap(reserveNum, forceSwapIntervalMs, normalSwapIntervalMs); + } + + @Override + public void cleanSwappedMap(long forceCleanSwapIntervalMs) { + mappedFileQueue.cleanSwappedMap(forceCleanSwapIntervalMs); + } + + @Override + public long estimateMessageCount(long from, long to, MessageFilter filter) { + long physicalOffsetFrom = from * CQ_STORE_UNIT_SIZE; + long physicalOffsetTo = to * CQ_STORE_UNIT_SIZE; + List mappedFiles = mappedFileQueue.range(physicalOffsetFrom, physicalOffsetTo); + if (mappedFiles.isEmpty()) { + return -1; + } + + boolean sample = false; + long match = 0; + long raw = 0; + + for (MappedFile mappedFile : mappedFiles) { + int start = 0; + int len = mappedFile.getFileSize(); + + // calculate start and len for first segment and last segment to reduce scanning + // first file segment + if (mappedFile.getFileFromOffset() <= physicalOffsetFrom) { + start = (int) (physicalOffsetFrom - mappedFile.getFileFromOffset()); + if (mappedFile.getFileFromOffset() + mappedFile.getFileSize() >= physicalOffsetTo) { + // current mapped file covers search range completely. + len = (int) (physicalOffsetTo - physicalOffsetFrom); + } else { + len = mappedFile.getFileSize() - start; + } + } + + // last file segment + if (0 == start && mappedFile.getFileFromOffset() + mappedFile.getFileSize() > physicalOffsetTo) { + len = (int) (physicalOffsetTo - mappedFile.getFileFromOffset()); + } + + // select partial data to scan + SelectMappedBufferResult slice = mappedFile.selectMappedBuffer(start, len); + if (null != slice) { + try { + ByteBuffer buffer = slice.getByteBuffer(); + int current = 0; + while (current < len) { + // skip physicalOffset and message length fields. + buffer.position(current + MSG_TAG_OFFSET_INDEX); + long tagCode = buffer.getLong(); + ConsumeQueueExt.CqExtUnit ext = null; + if (isExtWriteEnable()) { + ext = consumeQueueExt.get(tagCode); + tagCode = ext.getTagsCode(); + } + if (filter.isMatchedByConsumeQueue(tagCode, ext)) { + match++; + } + raw++; + current += CQ_STORE_UNIT_SIZE; + + if (raw >= messageStore.getMessageStoreConfig().getMaxConsumeQueueScan()) { + sample = true; + break; + } + + if (match > messageStore.getMessageStoreConfig().getSampleCountThreshold()) { + sample = true; + break; + } + } + } finally { + slice.release(); + } + } + // we have scanned enough entries, now is the time to return an educated guess. + if (sample) { + break; + } + } + + long result = match; + if (sample) { + if (0 == raw) { + log.error("[BUG]. Raw should NOT be 0"); + return 0; + } + result = (long) (match * (to - from) * 1.0 / raw); + } + log.debug("Result={}, raw={}, match={}, sample={}", result, raw, match, sample); + return result; + } } diff --git a/store/src/main/java/org/apache/rocketmq/store/ConsumeQueueExt.java b/store/src/main/java/org/apache/rocketmq/store/ConsumeQueueExt.java index aeb2803e23f..780505c53d1 100644 --- a/store/src/main/java/org/apache/rocketmq/store/ConsumeQueueExt.java +++ b/store/src/main/java/org/apache/rocketmq/store/ConsumeQueueExt.java @@ -18,14 +18,15 @@ package org.apache.rocketmq.store; import org.apache.rocketmq.common.constant.LoggerName; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import java.io.File; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.apache.rocketmq.store.logfile.MappedFile; /** * Extend of consume queue, to store something not important, @@ -89,6 +90,10 @@ public ConsumeQueueExt(final String topic, } } + public long getTotalSize() { + return this.mappedFileQueue.getTotalFileSize(); + } + /** * Check whether {@code address} point to extend file. *

    @@ -321,7 +326,7 @@ public void truncateByMinAddress(final long minAddress) { log.info("Truncate consume queue ext by min {}.", minAddress); - List willRemoveFiles = new ArrayList(); + List willRemoveFiles = new ArrayList<>(); List mappedFiles = this.mappedFileQueue.getMappedFiles(); final long realOffset = unDecorate(minAddress); diff --git a/store/src/main/java/org/apache/rocketmq/store/DefaultMessageFilter.java b/store/src/main/java/org/apache/rocketmq/store/DefaultMessageFilter.java index 9db87f31960..fff6966c22d 100644 --- a/store/src/main/java/org/apache/rocketmq/store/DefaultMessageFilter.java +++ b/store/src/main/java/org/apache/rocketmq/store/DefaultMessageFilter.java @@ -16,10 +16,9 @@ */ package org.apache.rocketmq.store; -import org.apache.rocketmq.common.protocol.heartbeat.SubscriptionData; - import java.nio.ByteBuffer; import java.util.Map; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; public class DefaultMessageFilter implements MessageFilter { diff --git a/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java b/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java index 7a5647c3e0a..48a3adcc086 100644 --- a/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java +++ b/store/src/main/java/org/apache/rocketmq/store/DefaultMessageStore.java @@ -16,93 +16,154 @@ */ package org.apache.rocketmq.store; +import com.google.common.collect.Sets; +import com.google.common.hash.Hashing; +import io.openmessaging.storage.dledger.entry.DLedgerEntry; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.sdk.metrics.InstrumentSelector; +import io.opentelemetry.sdk.metrics.ViewBuilder; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; +import java.net.Inet6Address; +import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.channels.FileLock; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.Iterator; +import java.util.HashSet; import java.util.LinkedList; +import java.util.List; import java.util.Map; -import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.Executors; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.AbstractBrokerRunnable; +import org.apache.rocketmq.common.BoundaryType; import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.BrokerIdentity; import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.Pair; import org.apache.rocketmq.common.ServiceThread; import org.apache.rocketmq.common.SystemClock; import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.TopicConfig; import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.attribute.CQType; +import org.apache.rocketmq.common.attribute.CleanupPolicy; import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageConst; import org.apache.rocketmq.common.message.MessageDecoder; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageExtBatch; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; import org.apache.rocketmq.common.running.RunningStats; import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.common.utils.CleanupPolicyUtils; +import org.apache.rocketmq.common.utils.QueueTypeUtils; +import org.apache.rocketmq.common.utils.ServiceProvider; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.body.HARuntimeInfo; import org.apache.rocketmq.store.config.BrokerRole; +import org.apache.rocketmq.store.config.FlushDiskType; import org.apache.rocketmq.store.config.MessageStoreConfig; import org.apache.rocketmq.store.config.StorePathConfigHelper; +import org.apache.rocketmq.store.dledger.DLedgerCommitLog; +import org.apache.rocketmq.store.ha.DefaultHAService; import org.apache.rocketmq.store.ha.HAService; +import org.apache.rocketmq.store.ha.autoswitch.AutoSwitchHAService; +import org.apache.rocketmq.store.hook.PutMessageHook; +import org.apache.rocketmq.store.hook.SendMessageBackHook; import org.apache.rocketmq.store.index.IndexService; import org.apache.rocketmq.store.index.QueryOffsetResult; -import org.apache.rocketmq.store.schedule.ScheduleMessageService; +import org.apache.rocketmq.store.kv.CommitLogDispatcherCompaction; +import org.apache.rocketmq.store.kv.CompactionService; +import org.apache.rocketmq.store.kv.CompactionStore; +import org.apache.rocketmq.store.logfile.MappedFile; +import org.apache.rocketmq.store.metrics.DefaultStoreMetricsManager; +import org.apache.rocketmq.store.queue.ConsumeQueueInterface; +import org.apache.rocketmq.store.queue.ConsumeQueueStore; +import org.apache.rocketmq.store.queue.ConsumeQueueStoreInterface; +import org.apache.rocketmq.store.queue.CqUnit; +import org.apache.rocketmq.store.queue.ReferredIterator; import org.apache.rocketmq.store.stats.BrokerStatsManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static org.apache.rocketmq.store.config.BrokerRole.SLAVE; +import org.apache.rocketmq.store.timer.TimerMessageStore; +import org.apache.rocketmq.store.util.PerfCounter; +import org.rocksdb.RocksDBException; public class DefaultMessageStore implements MessageStore { - private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + protected static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + protected static final Logger ERROR_LOG = LoggerFactory.getLogger(LoggerName.STORE_ERROR_LOGGER_NAME); + + public final PerfCounter.Ticks perfs = new PerfCounter.Ticks(LOGGER); private final MessageStoreConfig messageStoreConfig; // CommitLog - private final CommitLog commitLog; + protected final CommitLog commitLog; - private final ConcurrentMap> consumeQueueTable; + protected final ConsumeQueueStoreInterface consumeQueueStore; private final FlushConsumeQueueService flushConsumeQueueService; - private final CleanCommitLogService cleanCommitLogService; + protected final CleanCommitLogService cleanCommitLogService; private final CleanConsumeQueueService cleanConsumeQueueService; - private final IndexService indexService; + private final CorrectLogicOffsetService correctLogicOffsetService; + + protected final IndexService indexService; private final AllocateMappedFileService allocateMappedFileService; - private final ReputMessageService reputMessageService; + private ReputMessageService reputMessageService; + + private HAService haService; - private final HAService haService; + // CompactionLog + private CompactionStore compactionStore; - private final ScheduleMessageService scheduleMessageService; + private CompactionService compactionService; private final StoreStatsService storeStatsService; private final TransientStorePool transientStorePool; - private final RunningFlags runningFlags = new RunningFlags(); + protected final RunningFlags runningFlags = new RunningFlags(); private final SystemClock systemClock = new SystemClock(); - private final ScheduledExecutorService scheduledExecutorService = - Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("StoreScheduledThread")); + private final ScheduledExecutorService scheduledExecutorService; private final BrokerStatsManager brokerStatsManager; private final MessageArrivingListener messageArrivingListener; private final BrokerConfig brokerConfig; private volatile boolean shutdown = true; + protected boolean notifyMessageArriveInBatch = false; private StoreCheckpoint storeCheckpoint; - - private AtomicLong printTimes = new AtomicLong(0); + private TimerMessageStore timerMessageStore; private final LinkedList dispatcherList; @@ -111,89 +172,204 @@ public class DefaultMessageStore implements MessageStore { private FileLock lock; boolean shutDownNormal = false; + // Max pull msg size + private final static int MAX_PULL_MSG_SIZE = 128 * 1024 * 1024; + + private volatile int aliveReplicasNum = 1; + + // Refer the MessageStore of MasterBroker in the same process. + // If current broker is master, this reference point to null or itself. + // If current broker is slave, this reference point to the store of master broker, and the two stores belong to + // different broker groups. + private MessageStore masterStoreInProcess = null; + + private volatile long masterFlushedOffset = -1L; + + private volatile long brokerInitMaxOffset = -1L; + + private List putMessageHookList = new ArrayList<>(); + + private SendMessageBackHook sendMessageBackHook; + + private final ConcurrentSkipListMap delayLevelTable = + new ConcurrentSkipListMap<>(); + + private int maxDelayLevel; + + private final AtomicInteger mappedPageHoldCount = new AtomicInteger(0); + + private final ConcurrentLinkedQueue batchDispatchRequestQueue = new ConcurrentLinkedQueue<>(); + + private int dispatchRequestOrderlyQueueSize = 16; + + private final DispatchRequestOrderlyQueue dispatchRequestOrderlyQueue = new DispatchRequestOrderlyQueue(dispatchRequestOrderlyQueueSize); + + private long stateMachineVersion = 0L; + + // this is a unmodifiableMap + private ConcurrentMap topicConfigTable; + + private final ScheduledExecutorService scheduledCleanQueueExecutorService = + ThreadUtils.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("StoreCleanQueueScheduledThread")); public DefaultMessageStore(final MessageStoreConfig messageStoreConfig, final BrokerStatsManager brokerStatsManager, - final MessageArrivingListener messageArrivingListener, final BrokerConfig brokerConfig) throws IOException { + final MessageArrivingListener messageArrivingListener, final BrokerConfig brokerConfig, final ConcurrentMap topicConfigTable) throws IOException { this.messageArrivingListener = messageArrivingListener; this.brokerConfig = brokerConfig; this.messageStoreConfig = messageStoreConfig; + this.aliveReplicasNum = messageStoreConfig.getTotalReplicas(); this.brokerStatsManager = brokerStatsManager; + this.topicConfigTable = topicConfigTable; this.allocateMappedFileService = new AllocateMappedFileService(this); - this.commitLog = new CommitLog(this); - this.consumeQueueTable = new ConcurrentHashMap<>(32); + if (messageStoreConfig.isEnableDLegerCommitLog()) { + this.commitLog = new DLedgerCommitLog(this); + } else { + this.commitLog = new CommitLog(this); + } - this.flushConsumeQueueService = new FlushConsumeQueueService(); + this.consumeQueueStore = createConsumeQueueStore(); + + this.flushConsumeQueueService = createFlushConsumeQueueService(); this.cleanCommitLogService = new CleanCommitLogService(); - this.cleanConsumeQueueService = new CleanConsumeQueueService(); - this.storeStatsService = new StoreStatsService(); + this.cleanConsumeQueueService = createCleanConsumeQueueService(); + this.correctLogicOffsetService = createCorrectLogicOffsetService(); + this.storeStatsService = new StoreStatsService(getBrokerIdentity()); this.indexService = new IndexService(this); - this.haService = new HAService(this); - - this.reputMessageService = new ReputMessageService(); - this.scheduleMessageService = new ScheduleMessageService(this); - - this.transientStorePool = new TransientStorePool(messageStoreConfig); + if (!messageStoreConfig.isEnableDLegerCommitLog() && !this.messageStoreConfig.isDuplicationEnable()) { + if (brokerConfig.isEnableControllerMode()) { + this.haService = new AutoSwitchHAService(); + LOGGER.warn("Load AutoSwitch HA Service: {}", AutoSwitchHAService.class.getSimpleName()); + } else { + this.haService = ServiceProvider.loadClass(HAService.class); + if (null == this.haService) { + this.haService = new DefaultHAService(); + LOGGER.warn("Load default HA Service: {}", DefaultHAService.class.getSimpleName()); + } + } + } - if (messageStoreConfig.isTransientStorePoolEnable()) { - this.transientStorePool.init(); + if (!messageStoreConfig.isEnableBuildConsumeQueueConcurrently()) { + this.reputMessageService = new ReputMessageService(); + } else { + this.reputMessageService = new ConcurrentReputMessageService(); } - this.allocateMappedFileService.start(); + this.transientStorePool = new TransientStorePool(messageStoreConfig.getTransientStorePoolSize(), messageStoreConfig.getMappedFileSizeCommitLog()); - this.indexService.start(); + this.scheduledExecutorService = + ThreadUtils.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("StoreScheduledThread", getBrokerIdentity())); this.dispatcherList = new LinkedList<>(); this.dispatcherList.addLast(new CommitLogDispatcherBuildConsumeQueue()); this.dispatcherList.addLast(new CommitLogDispatcherBuildIndex()); + if (messageStoreConfig.isEnableCompaction()) { + this.compactionStore = new CompactionStore(this); + this.compactionService = new CompactionService(commitLog, this, compactionStore); + this.dispatcherList.addLast(new CommitLogDispatcherCompaction(compactionService)); + } File file = new File(StorePathConfigHelper.getLockFile(messageStoreConfig.getStorePathRootDir())); - MappedFile.ensureDirOK(file.getParent()); + UtilAll.ensureDirOK(file.getParent()); + UtilAll.ensureDirOK(getStorePathPhysic()); + UtilAll.ensureDirOK(getStorePathLogic()); lockFile = new RandomAccessFile(file, "rw"); + + parseDelayLevel(); + } + + public ConsumeQueueStoreInterface createConsumeQueueStore() { + return new ConsumeQueueStore(this); + } + + public CleanConsumeQueueService createCleanConsumeQueueService() { + return new CleanConsumeQueueService(); + } + + public FlushConsumeQueueService createFlushConsumeQueueService() { + return new FlushConsumeQueueService(); + } + + public CorrectLogicOffsetService createCorrectLogicOffsetService() { + return new CorrectLogicOffsetService(); } - public void truncateDirtyLogicFiles(long phyOffset) { - ConcurrentMap> tables = DefaultMessageStore.this.consumeQueueTable; + public boolean parseDelayLevel() { + HashMap timeUnitTable = new HashMap<>(); + timeUnitTable.put("s", 1000L); + timeUnitTable.put("m", 1000L * 60); + timeUnitTable.put("h", 1000L * 60 * 60); + timeUnitTable.put("d", 1000L * 60 * 60 * 24); - for (ConcurrentMap maps : tables.values()) { - for (ConsumeQueue logic : maps.values()) { - logic.truncateDirtyLogicFiles(phyOffset); + String levelString = messageStoreConfig.getMessageDelayLevel(); + try { + String[] levelArray = levelString.split(" "); + for (int i = 0; i < levelArray.length; i++) { + String value = levelArray[i]; + String ch = value.substring(value.length() - 1); + Long tu = timeUnitTable.get(ch); + + int level = i + 1; + if (level > this.maxDelayLevel) { + this.maxDelayLevel = level; + } + long num = Long.parseLong(value.substring(0, value.length() - 1)); + long delayTimeMillis = tu * num; + this.delayLevelTable.put(level, delayTimeMillis); } + } catch (Exception e) { + LOGGER.error("parse message delay level failed. messageDelayLevel = {}", levelString, e); + return false; } + + return true; + } + + @Override + public void truncateDirtyLogicFiles(long phyOffset) throws RocksDBException { + this.consumeQueueStore.truncateDirty(phyOffset); } /** * @throws IOException */ + @Override public boolean load() { boolean result = true; try { boolean lastExitOK = !this.isTempFileExist(); - log.info("last shutdown {}", lastExitOK ? "normally" : "abnormally"); - - if (null != scheduleMessageService) { - result = result && this.scheduleMessageService.load(); - } + LOGGER.info("last shutdown {}, store path root dir: {}", + lastExitOK ? "normally" : "abnormally", messageStoreConfig.getStorePathRootDir()); // load Commit Log - result = result && this.commitLog.load(); + result = this.commitLog.load(); // load Consume Queue - result = result && this.loadConsumeQueue(); + result = result && this.consumeQueueStore.load(); + + if (messageStoreConfig.isEnableCompaction()) { + result = result && this.compactionService.load(lastExitOK); + } if (result) { this.storeCheckpoint = - new StoreCheckpoint(StorePathConfigHelper.getStoreCheckpoint(this.messageStoreConfig.getStorePathRootDir())); - - this.indexService.load(lastExitOK); + new StoreCheckpoint( + StorePathConfigHelper.getStoreCheckpoint(this.messageStoreConfig.getStorePathRootDir())); + this.masterFlushedOffset = this.storeCheckpoint.getMasterFlushedOffset(); + setConfirmOffset(this.storeCheckpoint.getConfirmPhyOffset()); + result = this.indexService.load(lastExitOK); this.recover(lastExitOK); - - log.info("load over, and the max phy offset = {}", this.getMaxPhyOffset()); + LOGGER.info("message store recover end, and the max phy offset = {}", this.getMaxPhyOffset()); } + + + long maxOffset = this.getMaxPhyOffset(); + this.setBrokerInitMaxOffset(maxOffset); + LOGGER.info("load over, and the max phy offset = {}", maxOffset); } catch (Exception e) { - log.error("load exception", e); + LOGGER.error("load exception", e); result = false; } @@ -207,71 +383,145 @@ public boolean load() { /** * @throws Exception */ + @Override public void start() throws Exception { + if (!messageStoreConfig.isEnableDLegerCommitLog() && !this.messageStoreConfig.isDuplicationEnable()) { + this.haService.init(this); + } + + if (this.isTransientStorePoolEnable()) { + this.transientStorePool.init(); + } + + this.allocateMappedFileService.start(); + + this.indexService.start(); lock = lockFile.getChannel().tryLock(0, 1, false); if (lock == null || lock.isShared() || !lock.isValid()) { throw new RuntimeException("Lock failed,MQ already started"); } - lockFile.getChannel().write(ByteBuffer.wrap("lock".getBytes())); + lockFile.getChannel().write(ByteBuffer.wrap("lock".getBytes(StandardCharsets.UTF_8))); lockFile.getChannel().force(true); + this.reputMessageService.setReputFromOffset(this.commitLog.getConfirmOffset()); + this.reputMessageService.start(); + + // Checking is not necessary, as long as the dLedger's implementation exactly follows the definition of Recover, + // which is eliminating the dispatch inconsistency between the commitLog and consumeQueue at the end of recovery. + this.doRecheckReputOffsetFromCq(); + this.flushConsumeQueueService.start(); this.commitLog.start(); + this.consumeQueueStore.start(); this.storeStatsService.start(); - if (this.scheduleMessageService != null && SLAVE != messageStoreConfig.getBrokerRole()) { - this.scheduleMessageService.start(); - } - - if (this.getMessageStoreConfig().isDuplicationEnable()) { - this.reputMessageService.setReputFromOffset(this.commitLog.getConfirmOffset()); - } else { - this.reputMessageService.setReputFromOffset(this.commitLog.getMaxOffset()); + if (this.haService != null) { + this.haService.start(); } - this.reputMessageService.start(); - - this.haService.start(); this.createTempFile(); this.addScheduleTask(); + this.perfs.start(); this.shutdown = false; } + private void doRecheckReputOffsetFromCq() throws InterruptedException { + if (!messageStoreConfig.isRecheckReputOffsetFromCq()) { + return; + } + + /** + * 1. Make sure the fast-forward messages to be truncated during the recovering according to the max physical offset of the commitlog; + * 2. DLedger committedPos may be missing, so the maxPhysicalPosInLogicQueue maybe bigger that maxOffset returned by DLedgerCommitLog, just let it go; + * 3. Calculate the reput offset according to the consume queue; + * 4. Make sure the fall-behind messages to be dispatched before starting the commitlog, especially when the broker role are automatically changed. + */ + long maxPhysicalPosInLogicQueue = commitLog.getMinOffset(); + for (ConcurrentMap maps : this.getConsumeQueueTable().values()) { + for (ConsumeQueueInterface logic : maps.values()) { + if (logic.getMaxPhysicOffset() > maxPhysicalPosInLogicQueue) { + maxPhysicalPosInLogicQueue = logic.getMaxPhysicOffset(); + } + } + } + // If maxPhyPos(CQs) < minPhyPos(CommitLog), some newly deleted topics may be re-dispatched into cqs mistakenly. + if (maxPhysicalPosInLogicQueue < 0) { + maxPhysicalPosInLogicQueue = 0; + } + if (maxPhysicalPosInLogicQueue < this.commitLog.getMinOffset()) { + maxPhysicalPosInLogicQueue = this.commitLog.getMinOffset(); + /** + * This happens in following conditions: + * 1. If someone removes all the consumequeue files or the disk get damaged. + * 2. Launch a new broker, and copy the commitlog from other brokers. + * + * All the conditions has the same in common that the maxPhysicalPosInLogicQueue should be 0. + * If the maxPhysicalPosInLogicQueue is gt 0, there maybe something wrong. + */ + LOGGER.warn("[TooSmallCqOffset] maxPhysicalPosInLogicQueue={} clMinOffset={}", maxPhysicalPosInLogicQueue, this.commitLog.getMinOffset()); + } + LOGGER.info("[SetReputOffset] maxPhysicalPosInLogicQueue={} clMinOffset={} clMaxOffset={} clConfirmedOffset={}", + maxPhysicalPosInLogicQueue, this.commitLog.getMinOffset(), this.commitLog.getMaxOffset(), this.commitLog.getConfirmOffset()); + this.reputMessageService.setReputFromOffset(maxPhysicalPosInLogicQueue); + + /** + * 1. Finish dispatching the messages fall behind, then to start other services. + * 2. DLedger committedPos may be missing, so here just require dispatchBehindBytes <= 0 + */ + while (true) { + if (dispatchBehindBytes() <= 0) { + break; + } + Thread.sleep(1000); + LOGGER.info("Try to finish doing reput the messages fall behind during the starting, reputOffset={} maxOffset={} behind={}", this.reputMessageService.getReputFromOffset(), this.getMaxPhyOffset(), this.dispatchBehindBytes()); + } + this.recoverTopicQueueTable(); + } + + @Override public void shutdown() { if (!this.shutdown) { this.shutdown = true; this.scheduledExecutorService.shutdown(); + this.scheduledCleanQueueExecutorService.shutdown(); try { - + this.scheduledExecutorService.awaitTermination(3, TimeUnit.SECONDS); + this.scheduledCleanQueueExecutorService.awaitTermination(3, TimeUnit.SECONDS); Thread.sleep(1000 * 3); } catch (InterruptedException e) { - log.error("shutdown Exception, ", e); + LOGGER.error("shutdown Exception, ", e); } - if (this.scheduleMessageService != null) { - this.scheduleMessageService.shutdown(); + if (this.haService != null) { + this.haService.shutdown(); } - this.haService.shutdown(); - this.storeStatsService.shutdown(); - this.indexService.shutdown(); this.commitLog.shutdown(); this.reputMessageService.shutdown(); + this.consumeQueueStore.shutdown(); + // dispatch-related services must be shut down after reputMessageService + this.indexService.shutdown(); + if (this.compactionService != null) { + this.compactionService.shutdown(); + } + this.flushConsumeQueueService.shutdown(); this.allocateMappedFileService.shutdown(); this.storeCheckpoint.flush(); this.storeCheckpoint.shutdown(); + this.perfs.shutdown(); + if (this.runningFlags.isWriteable() && dispatchBehindBytes() == 0) { this.deleteFile(StorePathConfigHelper.getAbortFile(this.messageStoreConfig.getStorePathRootDir())); shutDownNormal = true; } else { - log.warn("the store may be wrong, so shutdown abnormally, and keep abort file."); + LOGGER.warn("the store may be wrong, so shutdown abnormally, and keep abort file."); } } @@ -286,132 +536,129 @@ public void shutdown() { } } + @Override public void destroy() { - this.destroyLogics(); + this.consumeQueueStore.destroy(); this.commitLog.destroy(); this.indexService.destroy(); this.deleteFile(StorePathConfigHelper.getAbortFile(this.messageStoreConfig.getStorePathRootDir())); this.deleteFile(StorePathConfigHelper.getStoreCheckpoint(this.messageStoreConfig.getStorePathRootDir())); } - public void destroyLogics() { - for (ConcurrentMap maps : this.consumeQueueTable.values()) { - for (ConsumeQueue logic : maps.values()) { - logic.destroy(); - } + public long getMajorFileSize() { + long commitLogSize = 0; + if (this.commitLog != null) { + commitLogSize = this.commitLog.getTotalSize(); } - } - public PutMessageResult putMessage(MessageExtBrokerInner msg) { - if (this.shutdown) { - log.warn("message store has shutdown, so putMessage is forbidden"); - return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null); + long consumeQueueSize = 0; + if (this.consumeQueueStore != null) { + consumeQueueSize = this.consumeQueueStore.getTotalSize(); } - if (BrokerRole.SLAVE == this.messageStoreConfig.getBrokerRole()) { - long value = this.printTimes.getAndIncrement(); - if ((value % 50000) == 0) { - log.warn("message store is slave mode, so putMessage is forbidden "); - } - - return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null); + long indexFileSize = 0; + if (this.indexService != null) { + indexFileSize = this.indexService.getTotalSize(); } - if (!this.runningFlags.isWriteable()) { - long value = this.printTimes.getAndIncrement(); - if ((value % 50000) == 0) { - log.warn("message store is not writeable, so putMessage is forbidden " + this.runningFlags.getFlagBits()); - } + return commitLogSize + consumeQueueSize + indexFileSize; + } - return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null); - } else { - this.printTimes.set(0); - } + @Override + public CompletableFuture asyncPutMessage(MessageExtBrokerInner msg) { - if (msg.getTopic().length() > Byte.MAX_VALUE) { - log.warn("putMessage message topic length too long " + msg.getTopic().length()); - return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null); + for (PutMessageHook putMessageHook : putMessageHookList) { + PutMessageResult handleResult = putMessageHook.executeBeforePutMessage(msg); + if (handleResult != null) { + return CompletableFuture.completedFuture(handleResult); + } } - if (msg.getPropertiesString() != null && msg.getPropertiesString().length() > Short.MAX_VALUE) { - log.warn("putMessage message properties length too long " + msg.getPropertiesString().length()); - return new PutMessageResult(PutMessageStatus.PROPERTIES_SIZE_EXCEEDED, null); + if (msg.getProperties().containsKey(MessageConst.PROPERTY_INNER_NUM) + && !MessageSysFlag.check(msg.getSysFlag(), MessageSysFlag.INNER_BATCH_FLAG)) { + LOGGER.warn("[BUG]The message had property {} but is not an inner batch", MessageConst.PROPERTY_INNER_NUM); + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null)); } - if (this.isOSPageCacheBusy()) { - return new PutMessageResult(PutMessageStatus.OS_PAGECACHE_BUSY, null); + if (MessageSysFlag.check(msg.getSysFlag(), MessageSysFlag.INNER_BATCH_FLAG)) { + Optional topicConfig = this.getTopicConfig(msg.getTopic()); + if (!QueueTypeUtils.isBatchCq(topicConfig)) { + LOGGER.error("[BUG]The message is an inner batch but cq type is not batch cq"); + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null)); + } } long beginTime = this.getSystemClock().now(); - PutMessageResult result = this.commitLog.putMessage(msg); + CompletableFuture putResultFuture = this.commitLog.asyncPutMessage(msg); - long eclipseTime = this.getSystemClock().now() - beginTime; - if (eclipseTime > 500) { - log.warn("putMessage not in lock eclipse time(ms)={}, bodyLength={}", eclipseTime, msg.getBody().length); - } - this.storeStatsService.setPutMessageEntireTimeMax(eclipseTime); + putResultFuture.thenAccept(result -> { + long elapsedTime = this.getSystemClock().now() - beginTime; + if (elapsedTime > 500) { + LOGGER.warn("DefaultMessageStore#putMessage: CommitLog#putMessage cost {}ms, topic={}, bodyLength={}", + elapsedTime, msg.getTopic(), msg.getBody().length); + } + this.storeStatsService.setPutMessageEntireTimeMax(elapsedTime); - if (null == result || !result.isOk()) { - this.storeStatsService.getPutMessageFailedTimes().incrementAndGet(); - } + if (null == result || !result.isOk()) { + this.storeStatsService.getPutMessageFailedTimes().add(1); + } + }); - return result; + return putResultFuture; } - public PutMessageResult putMessages(MessageExtBatch messageExtBatch) { - if (this.shutdown) { - log.warn("DefaultMessageStore has shutdown, so putMessages is forbidden"); - return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null); - } + @Override + public CompletableFuture asyncPutMessages(MessageExtBatch messageExtBatch) { - if (BrokerRole.SLAVE == this.messageStoreConfig.getBrokerRole()) { - long value = this.printTimes.getAndIncrement(); - if ((value % 50000) == 0) { - log.warn("DefaultMessageStore is in slave mode, so putMessages is forbidden "); + for (PutMessageHook putMessageHook : putMessageHookList) { + PutMessageResult handleResult = putMessageHook.executeBeforePutMessage(messageExtBatch); + if (handleResult != null) { + return CompletableFuture.completedFuture(handleResult); } - - return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null); } - if (!this.runningFlags.isWriteable()) { - long value = this.printTimes.getAndIncrement(); - if ((value % 50000) == 0) { - log.warn("DefaultMessageStore is not writable, so putMessages is forbidden " + this.runningFlags.getFlagBits()); - } - - return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null); - } else { - this.printTimes.set(0); - } + long beginTime = this.getSystemClock().now(); + CompletableFuture putResultFuture = this.commitLog.asyncPutMessages(messageExtBatch); - if (messageExtBatch.getTopic().length() > Byte.MAX_VALUE) { - log.warn("PutMessages topic length too long " + messageExtBatch.getTopic().length()); - return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null); - } + putResultFuture.thenAccept(result -> { + long eclipseTime = this.getSystemClock().now() - beginTime; + if (eclipseTime > 500) { + LOGGER.warn("not in lock eclipse time(ms)={}, bodyLength={}", eclipseTime, messageExtBatch.getBody().length); + } + this.storeStatsService.setPutMessageEntireTimeMax(eclipseTime); - if (messageExtBatch.getBody().length > messageStoreConfig.getMaxMessageSize()) { - log.warn("PutMessages body length too long " + messageExtBatch.getBody().length); - return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null); - } + if (null == result || !result.isOk()) { + this.storeStatsService.getPutMessageFailedTimes().add(1); + } + }); - if (this.isOSPageCacheBusy()) { - return new PutMessageResult(PutMessageStatus.OS_PAGECACHE_BUSY, null); - } + return putResultFuture; + } - long beginTime = this.getSystemClock().now(); - PutMessageResult result = this.commitLog.putMessages(messageExtBatch); + @Override + public PutMessageResult putMessage(MessageExtBrokerInner msg) { + return waitForPutResult(asyncPutMessage(msg)); + } - long eclipseTime = this.getSystemClock().now() - beginTime; - if (eclipseTime > 500) { - log.warn("not in lock eclipse time(ms)={}, bodyLength={}", eclipseTime, messageExtBatch.getBody().length); - } - this.storeStatsService.setPutMessageEntireTimeMax(eclipseTime); + @Override + public PutMessageResult putMessages(MessageExtBatch messageExtBatch) { + return waitForPutResult(asyncPutMessages(messageExtBatch)); + } - if (null == result || !result.isOk()) { - this.storeStatsService.getPutMessageFailedTimes().incrementAndGet(); + private PutMessageResult waitForPutResult(CompletableFuture putMessageResultFuture) { + try { + int putMessageTimeout = + Math.max(this.messageStoreConfig.getSyncFlushTimeout(), + this.messageStoreConfig.getSlaveTimeout()) + 5000; + return putMessageResultFuture.get(putMessageTimeout, TimeUnit.MILLISECONDS); + } catch (ExecutionException | InterruptedException e) { + return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, null); + } catch (TimeoutException e) { + LOGGER.error("usually it will never timeout, putMessageTimeout is much bigger than slaveTimeout and " + + "flushTimeout so the result can be got anyway, but in some situations timeout will happen like full gc " + + "process hangs or other unexpected situations."); + return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, null); } - - return result; } @Override @@ -420,7 +667,7 @@ public boolean isOSPageCacheBusy() { long diff = this.systemClock.now() - begin; return diff < 10000000 - && diff > this.messageStoreConfig.getOsPageCacheBusyTimeOutMills(); + && diff > this.messageStoreConfig.getOsPageCacheBusyTimeOutMills(); } @Override @@ -428,27 +675,130 @@ public long lockTimeMills() { return this.commitLog.lockTimeMills(); } + @Override + public long getMasterFlushedOffset() { + return this.masterFlushedOffset; + } + + @Override + public void setMasterFlushedOffset(long masterFlushedOffset) { + this.masterFlushedOffset = masterFlushedOffset; + this.storeCheckpoint.setMasterFlushedOffset(masterFlushedOffset); + } + + @Override + public long getBrokerInitMaxOffset() { + return this.brokerInitMaxOffset; + } + + @Override + public void setBrokerInitMaxOffset(long brokerInitMaxOffset) { + this.brokerInitMaxOffset = brokerInitMaxOffset; + } + public SystemClock getSystemClock() { return systemClock; } + @Override public CommitLog getCommitLog() { return commitLog; } + public void truncateDirtyFiles(long offsetToTruncate) throws RocksDBException { + + LOGGER.info("truncate dirty files to {}", offsetToTruncate); + + if (offsetToTruncate >= this.getMaxPhyOffset()) { + LOGGER.info("no need to truncate files, truncate offset is {}, max physical offset is {}", offsetToTruncate, this.getMaxPhyOffset()); + return; + } + + this.reputMessageService.shutdown(); + + long oldReputFromOffset = this.reputMessageService.getReputFromOffset(); + + // truncate consume queue + this.truncateDirtyLogicFiles(offsetToTruncate); + + // truncate commitLog + this.commitLog.truncateDirtyFiles(offsetToTruncate); + + this.recoverTopicQueueTable(); + + if (!messageStoreConfig.isEnableBuildConsumeQueueConcurrently()) { + this.reputMessageService = new ReputMessageService(); + } else { + this.reputMessageService = new ConcurrentReputMessageService(); + } + + long resetReputOffset = Math.min(oldReputFromOffset, offsetToTruncate); + + LOGGER.info("oldReputFromOffset is {}, reset reput from offset to {}", oldReputFromOffset, resetReputOffset); + + this.reputMessageService.setReputFromOffset(resetReputOffset); + this.reputMessageService.start(); + } + + @Override + public boolean truncateFiles(long offsetToTruncate) throws RocksDBException { + if (offsetToTruncate >= this.getMaxPhyOffset()) { + LOGGER.info("no need to truncate files, truncate offset is {}, max physical offset is {}", offsetToTruncate, this.getMaxPhyOffset()); + return true; + } + + if (!isOffsetAligned(offsetToTruncate)) { + LOGGER.error("offset {} is not align, truncate failed, need manual fix", offsetToTruncate); + return false; + } + truncateDirtyFiles(offsetToTruncate); + return true; + } + + @Override + public boolean isOffsetAligned(long offset) { + SelectMappedBufferResult mappedBufferResult = this.getCommitLogData(offset); + + if (mappedBufferResult == null) { + return true; + } + + DispatchRequest dispatchRequest = this.commitLog.checkMessageAndReturnSize(mappedBufferResult.getByteBuffer(), true, false); + return dispatchRequest.isSuccess(); + } + + @Override + public GetMessageResult getMessage(final String group, final String topic, final int queueId, final long offset, + final int maxMsgNums, final MessageFilter messageFilter) { + return getMessage(group, topic, queueId, offset, maxMsgNums, MAX_PULL_MSG_SIZE, messageFilter); + } + + @Override + public CompletableFuture getMessageAsync(String group, String topic, + int queueId, long offset, int maxMsgNums, MessageFilter messageFilter) { + return CompletableFuture.completedFuture(getMessage(group, topic, queueId, offset, maxMsgNums, messageFilter)); + } + + @Override public GetMessageResult getMessage(final String group, final String topic, final int queueId, final long offset, - final int maxMsgNums, - final MessageFilter messageFilter) { + final int maxMsgNums, final int maxTotalMsgSize, final MessageFilter messageFilter) { if (this.shutdown) { - log.warn("message store has shutdown, so getMessage is forbidden"); + LOGGER.warn("message store has shutdown, so getMessage is forbidden"); return null; } if (!this.runningFlags.isReadable()) { - log.warn("message store is not readable, so getMessage is forbidden " + this.runningFlags.getFlagBits()); + LOGGER.warn("message store is not readable, so getMessage is forbidden " + this.runningFlags.getFlagBits()); return null; } + Optional topicConfig = getTopicConfig(topic); + CleanupPolicy policy = CleanupPolicyUtils.getDeletePolicy(topicConfig); + //check request topic flag + if (Objects.equals(policy, CleanupPolicy.COMPACTION) && messageStoreConfig.isEnableCompaction()) { + return compactionStore.getMessage(group, topic, queueId, offset, maxMsgNums, maxTotalMsgSize); + } // else skip + long beginTime = this.getSystemClock().now(); GetMessageStatus status = GetMessageStatus.NO_MESSAGE_IN_QUEUE; @@ -460,7 +810,7 @@ public GetMessageResult getMessage(final String group, final String topic, final final long maxOffsetPy = this.commitLog.getMaxOffset(); - ConsumeQueue consumeQueue = findConsumeQueue(topic, queueId); + ConsumeQueueInterface consumeQueue = findConsumeQueue(topic, queueId); if (consumeQueue != null) { minOffset = consumeQueue.getMinOffsetInQueue(); maxOffset = consumeQueue.getMaxOffsetInQueue(); @@ -476,58 +826,70 @@ public GetMessageResult getMessage(final String group, final String topic, final nextBeginOffset = nextOffsetCorrection(offset, offset); } else if (offset > maxOffset) { status = GetMessageStatus.OFFSET_OVERFLOW_BADLY; - if (0 == minOffset) { - nextBeginOffset = nextOffsetCorrection(offset, minOffset); - } else { - nextBeginOffset = nextOffsetCorrection(offset, maxOffset); - } + nextBeginOffset = nextOffsetCorrection(offset, maxOffset); } else { - SelectMappedBufferResult bufferConsumeQueue = consumeQueue.getIndexBuffer(offset); - if (bufferConsumeQueue != null) { + final int maxFilterMessageSize = Math.max(this.messageStoreConfig.getMaxFilterMessageSize(), maxMsgNums * consumeQueue.getUnitSize()); + final boolean diskFallRecorded = this.messageStoreConfig.isDiskFallRecorded(); + + long maxPullSize = Math.max(maxTotalMsgSize, 100); + if (maxPullSize > MAX_PULL_MSG_SIZE) { + LOGGER.warn("The max pull size is too large maxPullSize={} topic={} queueId={}", maxPullSize, topic, queueId); + maxPullSize = MAX_PULL_MSG_SIZE; + } + status = GetMessageStatus.NO_MATCHED_MESSAGE; + long maxPhyOffsetPulling = 0; + int cqFileNum = 0; + + while (getResult.getBufferTotalSize() <= 0 + && nextBeginOffset < maxOffset + && cqFileNum++ < this.messageStoreConfig.getTravelCqFileNumWhenGetMessage()) { + ReferredIterator bufferConsumeQueue = null; + try { - status = GetMessageStatus.NO_MATCHED_MESSAGE; + bufferConsumeQueue = consumeQueue.iterateFrom(nextBeginOffset, maxMsgNums); + + if (bufferConsumeQueue == null) { + status = GetMessageStatus.OFFSET_FOUND_NULL; + nextBeginOffset = nextOffsetCorrection(nextBeginOffset, this.consumeQueueStore.rollNextFile(consumeQueue, nextBeginOffset)); + LOGGER.warn("consumer request topic: " + topic + ", offset: " + offset + ", minOffset: " + minOffset + ", maxOffset: " + + maxOffset + ", but access logic queue failed. Correct nextBeginOffset to " + nextBeginOffset); + break; + } long nextPhyFileStartOffset = Long.MIN_VALUE; - long maxPhyOffsetPulling = 0; + while (bufferConsumeQueue.hasNext() + && nextBeginOffset < maxOffset) { + CqUnit cqUnit = bufferConsumeQueue.next(); + long offsetPy = cqUnit.getPos(); + int sizePy = cqUnit.getSize(); - int i = 0; - final int maxFilterMessageCount = Math.max(16000, maxMsgNums * ConsumeQueue.CQ_STORE_UNIT_SIZE); - final boolean diskFallRecorded = this.messageStoreConfig.isDiskFallRecorded(); - ConsumeQueueExt.CqExtUnit cqExtUnit = new ConsumeQueueExt.CqExtUnit(); - for (; i < bufferConsumeQueue.getSize() && i < maxFilterMessageCount; i += ConsumeQueue.CQ_STORE_UNIT_SIZE) { - long offsetPy = bufferConsumeQueue.getByteBuffer().getLong(); - int sizePy = bufferConsumeQueue.getByteBuffer().getInt(); - long tagsCode = bufferConsumeQueue.getByteBuffer().getLong(); - - maxPhyOffsetPulling = offsetPy; + boolean isInMem = estimateInMemByCommitOffset(offsetPy, maxOffsetPy); - if (nextPhyFileStartOffset != Long.MIN_VALUE) { - if (offsetPy < nextPhyFileStartOffset) - continue; + if ((cqUnit.getQueueOffset() - offset) * consumeQueue.getUnitSize() > maxFilterMessageSize) { + break; } - boolean isInDisk = checkInDiskByCommitOffset(offsetPy, maxOffsetPy); + if (this.isTheBatchFull(sizePy, cqUnit.getBatchNum(), maxMsgNums, maxPullSize, getResult.getBufferTotalSize(), getResult.getMessageCount(), isInMem)) { + break; + } - if (this.isTheBatchFull(sizePy, maxMsgNums, getResult.getBufferTotalSize(), getResult.getMessageCount(), - isInDisk)) { + if (getResult.getBufferTotalSize() >= maxPullSize) { break; } - boolean extRet = false, isTagsCodeLegal = true; - if (consumeQueue.isExtAddr(tagsCode)) { - extRet = consumeQueue.getExt(tagsCode, cqExtUnit); - if (extRet) { - tagsCode = cqExtUnit.getTagsCode(); - } else { - // can't find ext content.Client will filter messages by tag also. - log.error("[BUG] can't find consume queue extend file content!addr={}, offsetPy={}, sizePy={}, topic={}, group={}", - tagsCode, offsetPy, sizePy, topic, group); - isTagsCodeLegal = false; + maxPhyOffsetPulling = offsetPy; + + //Be careful, here should before the isTheBatchFull + nextBeginOffset = cqUnit.getQueueOffset() + cqUnit.getBatchNum(); + + if (nextPhyFileStartOffset != Long.MIN_VALUE) { + if (offsetPy < nextPhyFileStartOffset) { + continue; } } if (messageFilter != null - && !messageFilter.isMatchedByConsumeQueue(isTagsCodeLegal ? tagsCode : null, extRet ? cqExtUnit : null)) { + && !messageFilter.isMatchedByConsumeQueue(cqUnit.getValidTagsCodeAsLong(), cqUnit.getCqExtUnit())) { if (getResult.getBufferTotalSize() == 0) { status = GetMessageStatus.NO_MATCHED_MESSAGE; } @@ -545,6 +907,10 @@ public GetMessageResult getMessage(final String group, final String topic, final continue; } + if (messageStoreConfig.isColdDataFlowControlEnable() && !MixAll.isSysConsumerGroupForNoColdReadLimit(group) && !selectResult.isInCache()) { + getResult.setColdDataSum(getResult.getColdDataSum() + sizePy); + } + if (messageFilter != null && !messageFilter.isMatchedByCommitLog(selectResult.getByteBuffer().slice(), null)) { if (getResult.getBufferTotalSize() == 0) { @@ -554,34 +920,30 @@ public GetMessageResult getMessage(final String group, final String topic, final selectResult.release(); continue; } - - this.storeStatsService.getGetMessageTransferedMsgCount().incrementAndGet(); - getResult.addMessage(selectResult); + this.storeStatsService.getGetMessageTransferredMsgCount().add(cqUnit.getBatchNum()); + getResult.addMessage(selectResult, cqUnit.getQueueOffset(), cqUnit.getBatchNum()); status = GetMessageStatus.FOUND; nextPhyFileStartOffset = Long.MIN_VALUE; } - - if (diskFallRecorded) { - long fallBehind = maxOffsetPy - maxPhyOffsetPulling; - brokerStatsManager.recordDiskFallBehindSize(group, topic, queueId, fallBehind); - } - - nextBeginOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE); - - long diff = maxOffsetPy - maxPhyOffsetPulling; - long memory = (long) (StoreUtil.TOTAL_PHYSICAL_MEMORY_SIZE - * (this.messageStoreConfig.getAccessMessageInMemoryMaxRatio() / 100.0)); - getResult.setSuggestPullingFromSlave(diff > memory); + } catch (RocksDBException e) { + ERROR_LOG.error("getMessage Failed. cid: {}, topic: {}, queueId: {}, offset: {}, minOffset: {}, maxOffset: {}, {}", + group, topic, queueId, offset, minOffset, maxOffset, e.getMessage()); } finally { - - bufferConsumeQueue.release(); + if (bufferConsumeQueue != null) { + bufferConsumeQueue.release(); + } } - } else { - status = GetMessageStatus.OFFSET_FOUND_NULL; - nextBeginOffset = nextOffsetCorrection(offset, consumeQueue.rollNextFile(offset)); - log.warn("consumer request topic: " + topic + "offset: " + offset + " minOffset: " + minOffset + " maxOffset: " - + maxOffset + ", but access logic queue failed."); } + + if (diskFallRecorded) { + long fallBehind = maxOffsetPy - maxPhyOffsetPulling; + brokerStatsManager.recordDiskFallBehindSize(group, topic, queueId, fallBehind); + } + + long diff = maxOffsetPy - maxPhyOffsetPulling; + long memory = (long) (StoreUtil.TOTAL_PHYSICAL_MEMORY_SIZE + * (this.messageStoreConfig.getAccessMessageInMemoryMaxRatio() / 100.0)); + getResult.setSuggestPullingFromSlave(diff > memory); } } else { status = GetMessageStatus.NO_MATCHED_LOGIC_QUEUE; @@ -589,12 +951,17 @@ public GetMessageResult getMessage(final String group, final String topic, final } if (GetMessageStatus.FOUND == status) { - this.storeStatsService.getGetMessageTimesTotalFound().incrementAndGet(); + this.storeStatsService.getGetMessageTimesTotalFound().add(1); } else { - this.storeStatsService.getGetMessageTimesTotalMiss().incrementAndGet(); + this.storeStatsService.getGetMessageTimesTotalMiss().add(1); + } + long elapsedTime = this.getSystemClock().now() - beginTime; + this.storeStatsService.setGetMessageEntireTimeMax(elapsedTime); + + // lazy init no data found. + if (getResult == null) { + getResult = new GetMessageResult(0); } - long eclipseTime = this.getSystemClock().now() - beginTime; - this.storeStatsService.setGetMessageEntireTimeMax(eclipseTime); getResult.setStatus(status); getResult.setNextBeginOffset(nextBeginOffset); @@ -603,52 +970,83 @@ public GetMessageResult getMessage(final String group, final String topic, final return getResult; } + @Override + public CompletableFuture getMessageAsync(String group, String topic, + int queueId, long offset, int maxMsgNums, int maxTotalMsgSize, MessageFilter messageFilter) { + return CompletableFuture.completedFuture(getMessage(group, topic, queueId, offset, maxMsgNums, maxTotalMsgSize, messageFilter)); + } + + @Override public long getMaxOffsetInQueue(String topic, int queueId) { - ConsumeQueue logic = this.findConsumeQueue(topic, queueId); - if (logic != null) { - long offset = logic.getMaxOffsetInQueue(); - return offset; + return getMaxOffsetInQueue(topic, queueId, true); + } + + @Override + public long getMaxOffsetInQueue(String topic, int queueId, boolean committed) { + if (committed) { + ConsumeQueueInterface logic = this.findConsumeQueue(topic, queueId); + if (logic != null) { + return logic.getMaxOffsetInQueue(); + } + } else { + Long offset = this.consumeQueueStore.getMaxOffset(topic, queueId); + if (offset != null) { + return offset; + } } return 0; } + @Override public long getMinOffsetInQueue(String topic, int queueId) { - ConsumeQueue logic = this.findConsumeQueue(topic, queueId); - if (logic != null) { - return logic.getMinOffsetInQueue(); + try { + return this.consumeQueueStore.getMinOffsetInQueue(topic, queueId); + } catch (RocksDBException e) { + ERROR_LOG.error("getMinOffsetInQueue Failed. topic: {}, queueId: {}", topic, queueId, e); + return -1; } + } - return -1; + @Override + public TimerMessageStore getTimerMessageStore() { + return this.timerMessageStore; + } + + @Override + public void setTimerMessageStore(TimerMessageStore timerMessageStore) { + this.timerMessageStore = timerMessageStore; } @Override public long getCommitLogOffsetInQueue(String topic, int queueId, long consumeQueueOffset) { - ConsumeQueue consumeQueue = findConsumeQueue(topic, queueId); + ConsumeQueueInterface consumeQueue = findConsumeQueue(topic, queueId); if (consumeQueue != null) { - SelectMappedBufferResult bufferConsumeQueue = consumeQueue.getIndexBuffer(consumeQueueOffset); - if (bufferConsumeQueue != null) { - try { - long offsetPy = bufferConsumeQueue.getByteBuffer().getLong(); - return offsetPy; - } finally { - bufferConsumeQueue.release(); - } + CqUnit cqUnit = consumeQueue.get(consumeQueueOffset); + if (cqUnit != null) { + return cqUnit.getPos(); } } - return 0; } + @Override public long getOffsetInQueueByTime(String topic, int queueId, long timestamp) { - ConsumeQueue logic = this.findConsumeQueue(topic, queueId); - if (logic != null) { - return logic.getOffsetInQueueByTime(timestamp); - } + return this.getOffsetInQueueByTime(topic, queueId, timestamp, BoundaryType.LOWER); + } + @Override + public long getOffsetInQueueByTime(String topic, int queueId, long timestamp, BoundaryType boundaryType) { + try { + return this.consumeQueueStore.getOffsetInQueueByTime(topic, queueId, timestamp, boundaryType); + } catch (RocksDBException e) { + ERROR_LOG.error("getOffsetInQueueByTime Failed. topic: {}, queueId: {}, timestamp: {} boundaryType: {}, {}", + topic, queueId, timestamp, boundaryType, e.getMessage()); + } return 0; } + @Override public MessageExt lookMessageByOffset(long commitLogOffset) { SelectMappedBufferResult sbr = this.commitLog.getMessage(commitLogOffset, 4); if (null != sbr) { @@ -685,34 +1083,51 @@ public SelectMappedBufferResult selectOneMessageByOffset(long commitLogOffset, i return this.commitLog.getMessage(commitLogOffset, msgSize); } + @Override public String getRunningDataInfo() { return this.storeStatsService.toString(); } + public String getStorePathPhysic() { + String storePathPhysic; + if (DefaultMessageStore.this.getMessageStoreConfig().isEnableDLegerCommitLog()) { + storePathPhysic = ((DLedgerCommitLog) DefaultMessageStore.this.getCommitLog()).getdLedgerServer().getdLedgerConfig().getDataStorePath(); + } else { + storePathPhysic = DefaultMessageStore.this.getMessageStoreConfig().getStorePathCommitLog(); + } + return storePathPhysic; + } + + public String getStorePathLogic() { + return StorePathConfigHelper.getStorePathConsumeQueue(this.messageStoreConfig.getStorePathRootDir()); + } + + public MessageArrivingListener getMessageArrivingListener() { + return messageArrivingListener; + } + @Override public HashMap getRuntimeInfo() { HashMap result = this.storeStatsService.getRuntimeInfo(); { - String storePathPhysic = DefaultMessageStore.this.getMessageStoreConfig().getStorePathCommitLog(); - double physicRatio = UtilAll.getDiskPartitionSpaceUsedPercent(storePathPhysic); - result.put(RunningStats.commitLogDiskRatio.name(), String.valueOf(physicRatio)); - + double minPhysicsUsedRatio = Double.MAX_VALUE; + String commitLogStorePath = getStorePathPhysic(); + String[] paths = commitLogStorePath.trim().split(MixAll.MULTI_PATH_SPLITTER); + for (String clPath : paths) { + double physicRatio = UtilAll.isPathExists(clPath) ? + UtilAll.getDiskPartitionSpaceUsedPercent(clPath) : -1; + result.put(RunningStats.commitLogDiskRatio.name() + "_" + clPath, String.valueOf(physicRatio)); + minPhysicsUsedRatio = Math.min(minPhysicsUsedRatio, physicRatio); + } + result.put(RunningStats.commitLogDiskRatio.name(), String.valueOf(minPhysicsUsedRatio)); } { - - String storePathLogics = StorePathConfigHelper.getStorePathConsumeQueue(this.messageStoreConfig.getStorePathRootDir()); - double logicsRatio = UtilAll.getDiskPartitionSpaceUsedPercent(storePathLogics); + double logicsRatio = UtilAll.getDiskPartitionSpaceUsedPercent(getStorePathLogic()); result.put(RunningStats.consumeQueueDiskRatio.name(), String.valueOf(logicsRatio)); } - { - if (this.scheduleMessageService != null) { - this.scheduleMessageService.buildRunningStats(result); - } - } - result.put(RunningStats.commitLogMinOffset.name(), String.valueOf(DefaultMessageStore.this.getMinPhyOffset())); result.put(RunningStats.commitLogMaxOffset.name(), String.valueOf(DefaultMessageStore.this.getMaxPhyOffset())); @@ -729,55 +1144,65 @@ public long getMinPhyOffset() { return this.commitLog.getMinOffset(); } + @Override + public long getLastFileFromOffset() { + return this.commitLog.getLastFileFromOffset(); + } + + @Override + public boolean getLastMappedFile(long startOffset) { + return this.commitLog.getLastMappedFile(startOffset); + } + @Override public long getEarliestMessageTime(String topic, int queueId) { - ConsumeQueue logicQueue = this.findConsumeQueue(topic, queueId); + ConsumeQueueInterface logicQueue = this.findConsumeQueue(topic, queueId); if (logicQueue != null) { - long minLogicOffset = logicQueue.getMinLogicOffset(); - - SelectMappedBufferResult result = logicQueue.getIndexBuffer(minLogicOffset / ConsumeQueue.CQ_STORE_UNIT_SIZE); - return getStoreTime(result); + Pair pair = logicQueue.getEarliestUnitAndStoreTime(); + if (pair != null && pair.getObject2() != null) { + return pair.getObject2(); + } } return -1; } - private long getStoreTime(SelectMappedBufferResult result) { - if (result != null) { - try { - final long phyOffset = result.getByteBuffer().getLong(); - final int size = result.getByteBuffer().getInt(); - long storeTime = this.getCommitLog().pickupStoreTimestamp(phyOffset, size); - return storeTime; - } catch (Exception e) { - } finally { - result.release(); - } - } - return -1; + @Override + public CompletableFuture getEarliestMessageTimeAsync(String topic, int queueId) { + return CompletableFuture.completedFuture(getEarliestMessageTime(topic, queueId)); } @Override public long getEarliestMessageTime() { - final long minPhyOffset = this.getMinPhyOffset(); - final int size = this.messageStoreConfig.getMaxMessageSize() * 2; + long minPhyOffset = this.getMinPhyOffset(); + if (this.getCommitLog() instanceof DLedgerCommitLog) { + minPhyOffset += DLedgerEntry.BODY_OFFSET; + } + final int size = MessageDecoder.MESSAGE_STORE_TIMESTAMP_POSITION + 8; return this.getCommitLog().pickupStoreTimestamp(minPhyOffset, size); } @Override public long getMessageStoreTimeStamp(String topic, int queueId, long consumeQueueOffset) { - ConsumeQueue logicQueue = this.findConsumeQueue(topic, queueId); + ConsumeQueueInterface logicQueue = this.findConsumeQueue(topic, queueId); if (logicQueue != null) { - SelectMappedBufferResult result = logicQueue.getIndexBuffer(consumeQueueOffset); - return getStoreTime(result); + Pair pair = logicQueue.getCqUnitAndStoreTime(consumeQueueOffset); + if (pair != null && pair.getObject2() != null) { + return pair.getObject2(); + } } - return -1; } + @Override + public CompletableFuture getMessageStoreTimeStampAsync(String topic, int queueId, + long consumeQueueOffset) { + return CompletableFuture.completedFuture(getMessageStoreTimeStamp(topic, queueId, consumeQueueOffset)); + } + @Override public long getMessageTotalInQueue(String topic, int queueId) { - ConsumeQueue logicQueue = this.findConsumeQueue(topic, queueId); + ConsumeQueueInterface logicQueue = this.findConsumeQueue(topic, queueId); if (logicQueue != null) { return logicQueue.getMessageTotalInQueue(); } @@ -788,7 +1213,7 @@ public long getMessageTotalInQueue(String topic, int queueId) { @Override public SelectMappedBufferResult getCommitLogData(final long offset) { if (this.shutdown) { - log.warn("message store has shutdown, so getPhyQueueData is forbidden"); + LOGGER.warn("message store has shutdown, so getPhyQueueData is forbidden"); return null; } @@ -796,17 +1221,29 @@ public SelectMappedBufferResult getCommitLogData(final long offset) { } @Override - public boolean appendToCommitLog(long startOffset, byte[] data) { + public List getBulkCommitLogData(final long offset, final int size) { + if (this.shutdown) { + LOGGER.warn("message store has shutdown, so getBulkCommitLogData is forbidden"); + return null; + } + + return this.commitLog.getBulkData(offset, size); + } + + @Override + public boolean appendToCommitLog(long startOffset, byte[] data, int dataStart, int dataLength) { if (this.shutdown) { - log.warn("message store has shutdown, so appendToPhyQueue is forbidden"); + LOGGER.warn("message store has shutdown, so appendToCommitLog is forbidden"); return false; } - boolean result = this.commitLog.appendData(startOffset, data); + boolean result = this.commitLog.appendData(startOffset, data, dataStart, dataLength); if (result) { this.reputMessageService.wakeup(); } else { - log.error("appendToPhyQueue failed " + startOffset + " " + data.length); + LOGGER.error( + "DefaultMessageStore#appendToCommitLog: failed to append data to commitLog, physical offset={}, data " + + "length={}", startOffset, data.length); } return result; @@ -814,7 +1251,7 @@ public boolean appendToCommitLog(long startOffset, byte[] data) { @Override public void executeDeleteFilesManually() { - this.cleanCommitLogService.excuteDeleteFilesManualy(); + this.cleanCommitLogService.executeDeleteFilesManually(); } @Override @@ -838,36 +1275,20 @@ public QueryMessageResult queryMessage(String topic, String key, int maxNum, lon long offset = queryOffsetResult.getPhyOffsets().get(m); try { - - boolean match = true; MessageExt msg = this.lookMessageByOffset(offset); if (0 == m) { lastQueryMsgTime = msg.getStoreTimestamp(); } -// String[] keyArray = msg.getKeys().split(MessageConst.KEY_SEPARATOR); -// if (topic.equals(msg.getTopic())) { -// for (String k : keyArray) { -// if (k.equals(key)) { -// match = true; -// break; -// } -// } -// } - - if (match) { - SelectMappedBufferResult result = this.commitLog.getData(offset, false); - if (result != null) { - int size = result.getByteBuffer().getInt(0); - result.getByteBuffer().limit(size); - result.setSize(size); - queryMessageResult.addMessage(result); - } - } else { - log.warn("queryMessage hash duplicate, {} {}", topic, key); + SelectMappedBufferResult result = this.commitLog.getData(offset, false); + if (result != null) { + int size = result.getByteBuffer().getInt(0); + result.getByteBuffer().limit(size); + result.setSize(size); + queryMessageResult.addMessage(result); } } catch (Exception e) { - log.error("queryMessage exception", e); + LOGGER.error("queryMessage exception", e); } } @@ -883,14 +1304,54 @@ public QueryMessageResult queryMessage(String topic, String key, int maxNum, lon return queryMessageResult; } + @Override public CompletableFuture queryMessageAsync(String topic, String key, + int maxNum, long begin, long end) { + return CompletableFuture.completedFuture(queryMessage(topic, key, maxNum, begin, end)); + } + @Override public void updateHaMasterAddress(String newAddr) { - this.haService.updateMasterAddress(newAddr); + if (this.haService != null) { + this.haService.updateHaMasterAddress(newAddr); + } + } + + @Override + public void updateMasterAddress(String newAddr) { + if (this.haService != null) { + this.haService.updateMasterAddress(newAddr); + } + if (this.compactionService != null) { + this.compactionService.updateMasterAddress(newAddr); + } + } + + @Override + public void setAliveReplicaNumInGroup(int aliveReplicaNums) { + this.aliveReplicasNum = aliveReplicaNums; + } + + @Override + public void wakeupHAClient() { + if (this.haService != null) { + this.haService.getHAClient().wakeup(); + } + } + + @Override + public int getAliveReplicaNumInGroup() { + return this.aliveReplicasNum; } @Override public long slaveFallBehindMuch() { - return this.commitLog.getMaxOffset() - this.haService.getPush2SlaveMaxOffset().get(); + if (this.haService == null || this.messageStoreConfig.isDuplicationEnable() || this.messageStoreConfig.isEnableDLegerCommitLog()) { + LOGGER.warn("haServer is null or duplication is enable or enableDLegerCommitLog is true"); + return -1; + } else { + return this.commitLog.getMaxOffset() - this.haService.getPush2SlaveMaxOffset().get(); + } + } @Override @@ -898,85 +1359,92 @@ public long now() { return this.systemClock.now(); } + /** + * Lazy clean queue offset table. + * If offset table is cleaned, and old messages are dispatching after the old consume queue is cleaned, + * consume queue will be created with old offset, then later message with new offset table can not be + * dispatched to consume queue. + * @throws RocksDBException only in rocksdb mode + */ @Override - public int cleanUnusedTopic(Set topics) { - Iterator>> it = this.consumeQueueTable.entrySet().iterator(); - while (it.hasNext()) { - Entry> next = it.next(); - String topic = next.getKey(); + public int deleteTopics(final Set deleteTopics) { + if (deleteTopics == null || deleteTopics.isEmpty()) { + return 0; + } - if (!topics.contains(topic) && !topic.equals(ScheduleMessageService.SCHEDULE_TOPIC)) { - ConcurrentMap queueTable = next.getValue(); - for (ConsumeQueue cq : queueTable.values()) { - cq.destroy(); - log.info("cleanUnusedTopic: {} {} ConsumeQueue cleaned", - cq.getTopic(), - cq.getQueueId() - ); + int deleteCount = 0; + for (String topic : deleteTopics) { + ConcurrentMap queueTable = this.consumeQueueStore.findConsumeQueueMap(topic); - this.commitLog.removeQueueFromTopicQueueTable(cq.getTopic(), cq.getQueueId()); + if (queueTable == null || queueTable.isEmpty()) { + continue; + } + + for (ConsumeQueueInterface cq : queueTable.values()) { + try { + this.consumeQueueStore.destroy(cq); + } catch (RocksDBException e) { + LOGGER.error("DeleteTopic: ConsumeQueue cleans error!, topic={}, queueId={}", cq.getTopic(), cq.getQueueId(), e); } - it.remove(); + LOGGER.info("DeleteTopic: ConsumeQueue has been cleaned, topic={}, queueId={}", cq.getTopic(), cq.getQueueId()); + this.consumeQueueStore.removeTopicQueueTable(cq.getTopic(), cq.getQueueId()); + } - log.info("cleanUnusedTopic: {},topic destroyed", topic); + // remove topic from cq table + this.consumeQueueStore.getConsumeQueueTable().remove(topic); + + if (this.brokerConfig.isAutoDeleteUnusedStats()) { + this.brokerStatsManager.onTopicDeleted(topic); } + + // destroy consume queue dir + String consumeQueueDir = StorePathConfigHelper.getStorePathConsumeQueue( + this.messageStoreConfig.getStorePathRootDir()) + File.separator + topic; + String consumeQueueExtDir = StorePathConfigHelper.getStorePathConsumeQueueExt( + this.messageStoreConfig.getStorePathRootDir()) + File.separator + topic; + String batchConsumeQueueDir = StorePathConfigHelper.getStorePathBatchConsumeQueue( + this.messageStoreConfig.getStorePathRootDir()) + File.separator + topic; + + UtilAll.deleteEmptyDirectory(new File(consumeQueueDir)); + UtilAll.deleteEmptyDirectory(new File(consumeQueueExtDir)); + UtilAll.deleteEmptyDirectory(new File(batchConsumeQueueDir)); + + LOGGER.info("DeleteTopic: Topic has been destroyed, topic={}", topic); + deleteCount++; } + return deleteCount; + } - return 0; + @Override + public int cleanUnusedTopic(final Set retainTopics) { + Set consumeQueueTopicSet = this.getConsumeQueueTable().keySet(); + int deleteCount = 0; + for (String topicName : Sets.difference(consumeQueueTopicSet, retainTopics)) { + if (retainTopics.contains(topicName) || + TopicValidator.isSystemTopic(topicName) || + MixAll.isLmq(topicName)) { + continue; + } + deleteCount += this.deleteTopics(Sets.newHashSet(topicName)); + } + return deleteCount; } + @Override public void cleanExpiredConsumerQueue() { long minCommitLogOffset = this.commitLog.getMinOffset(); - Iterator>> it = this.consumeQueueTable.entrySet().iterator(); - while (it.hasNext()) { - Entry> next = it.next(); - String topic = next.getKey(); - if (!topic.equals(ScheduleMessageService.SCHEDULE_TOPIC)) { - ConcurrentMap queueTable = next.getValue(); - Iterator> itQT = queueTable.entrySet().iterator(); - while (itQT.hasNext()) { - Entry nextQT = itQT.next(); - long maxCLOffsetInConsumeQueue = nextQT.getValue().getLastOffset(); - - if (maxCLOffsetInConsumeQueue == -1) { - log.warn("maybe ConsumeQueue was created just now. topic={} queueId={} maxPhysicOffset={} minLogicOffset={}.", - nextQT.getValue().getTopic(), - nextQT.getValue().getQueueId(), - nextQT.getValue().getMaxPhysicOffset(), - nextQT.getValue().getMinLogicOffset()); - } else if (maxCLOffsetInConsumeQueue < minCommitLogOffset) { - log.info( - "cleanExpiredConsumerQueue: {} {} consumer queue destroyed, minCommitLogOffset: {} maxCLOffsetInConsumeQueue: {}", - topic, - nextQT.getKey(), - minCommitLogOffset, - maxCLOffsetInConsumeQueue); - - DefaultMessageStore.this.commitLog.removeQueueFromTopicQueueTable(nextQT.getValue().getTopic(), - nextQT.getValue().getQueueId()); - - nextQT.getValue().destroy(); - itQT.remove(); - } - } - - if (queueTable.isEmpty()) { - log.info("cleanExpiredConsumerQueue: {},topic destroyed", topic); - it.remove(); - } - } - } + this.consumeQueueStore.cleanExpired(minCommitLogOffset); } public Map getMessageIds(final String topic, final int queueId, long minOffset, long maxOffset, SocketAddress storeHost) { - Map messageIds = new HashMap(); + Map messageIds = new HashMap<>(); if (this.shutdown) { return messageIds; } - ConsumeQueue consumeQueue = findConsumeQueue(topic, queueId); + ConsumeQueueInterface consumeQueue = findConsumeQueue(topic, queueId); if (consumeQueue != null) { minOffset = Math.max(minOffset, consumeQueue.getMinOffsetInQueue()); maxOffset = Math.min(maxOffset, consumeQueue.getMaxOffsetInQueue()); @@ -987,26 +1455,30 @@ public Map getMessageIds(final String topic, final int queueId, lo long nextOffset = minOffset; while (nextOffset < maxOffset) { - SelectMappedBufferResult bufferConsumeQueue = consumeQueue.getIndexBuffer(nextOffset); - if (bufferConsumeQueue != null) { - try { - int i = 0; - for (; i < bufferConsumeQueue.getSize(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE) { - long offsetPy = bufferConsumeQueue.getByteBuffer().getLong(); - final ByteBuffer msgIdMemory = ByteBuffer.allocate(MessageDecoder.MSG_ID_LENGTH); + ReferredIterator bufferConsumeQueue = consumeQueue.iterateFrom(nextOffset); + try { + if (bufferConsumeQueue != null && bufferConsumeQueue.hasNext()) { + while (bufferConsumeQueue.hasNext()) { + CqUnit cqUnit = bufferConsumeQueue.next(); + long offsetPy = cqUnit.getPos(); + InetSocketAddress inetSocketAddress = (InetSocketAddress) storeHost; + int msgIdLength = (inetSocketAddress.getAddress() instanceof Inet6Address) ? 16 + 4 + 8 : 4 + 4 + 8; + final ByteBuffer msgIdMemory = ByteBuffer.allocate(msgIdLength); String msgId = MessageDecoder.createMessageId(msgIdMemory, MessageExt.socketAddress2ByteBuffer(storeHost), offsetPy); - messageIds.put(msgId, nextOffset++); - if (nextOffset > maxOffset) { + messageIds.put(msgId, cqUnit.getQueueOffset()); + nextOffset = cqUnit.getQueueOffset() + cqUnit.getBatchNum(); + if (nextOffset >= maxOffset) { return messageIds; } } - } finally { - + } else { + return messageIds; + } + } finally { + if (bufferConsumeQueue != null) { bufferConsumeQueue.release(); } - } else { - return messageIds; } } } @@ -1014,24 +1486,18 @@ public Map getMessageIds(final String topic, final int queueId, lo } @Override + @Deprecated public boolean checkInDiskByConsumeOffset(final String topic, final int queueId, long consumeOffset) { final long maxOffsetPy = this.commitLog.getMaxOffset(); - ConsumeQueue consumeQueue = findConsumeQueue(topic, queueId); + ConsumeQueueInterface consumeQueue = findConsumeQueue(topic, queueId); if (consumeQueue != null) { - SelectMappedBufferResult bufferConsumeQueue = consumeQueue.getIndexBuffer(consumeOffset); - if (bufferConsumeQueue != null) { - try { - for (int i = 0; i < bufferConsumeQueue.getSize(); ) { - i += ConsumeQueue.CQ_STORE_UNIT_SIZE; - long offsetPy = bufferConsumeQueue.getByteBuffer().getLong(); - return checkInDiskByCommitOffset(offsetPy, maxOffsetPy); - } - } finally { + CqUnit cqUnit = consumeQueue.get(consumeOffset); - bufferConsumeQueue.release(); - } + if (cqUnit != null) { + long offsetPy = cqUnit.getPos(); + return !estimateInMemByCommitOffset(offsetPy, maxOffsetPy); } else { return false; } @@ -1039,30 +1505,180 @@ public boolean checkInDiskByConsumeOffset(final String topic, final int queueId, return false; } + @Override + public boolean checkInMemByConsumeOffset(final String topic, final int queueId, long consumeOffset, int batchSize) { + ConsumeQueueInterface consumeQueue = findConsumeQueue(topic, queueId); + if (consumeQueue != null) { + CqUnit firstCQItem = consumeQueue.get(consumeOffset); + if (firstCQItem == null) { + return false; + } + long startOffsetPy = firstCQItem.getPos(); + if (batchSize <= 1) { + int size = firstCQItem.getSize(); + return checkInMemByCommitOffset(startOffsetPy, size); + } + + CqUnit lastCQItem = consumeQueue.get(consumeOffset + batchSize); + if (lastCQItem == null) { + int size = firstCQItem.getSize(); + return checkInMemByCommitOffset(startOffsetPy, size); + } + long endOffsetPy = lastCQItem.getPos(); + int size = (int) (endOffsetPy - startOffsetPy) + lastCQItem.getSize(); + return checkInMemByCommitOffset(startOffsetPy, size); + } + return false; + } + + @Override + public boolean checkInStoreByConsumeOffset(String topic, int queueId, long consumeOffset) { + long commitLogOffset = getCommitLogOffsetInQueue(topic, queueId, consumeOffset); + return checkInDiskByCommitOffset(commitLogOffset); + } + + @Override public long dispatchBehindBytes() { return this.reputMessageService.behind(); } + public long flushBehindBytes() { + return this.commitLog.remainHowManyDataToCommit() + this.commitLog.remainHowManyDataToFlush(); + } + @Override public long flush() { return this.commitLog.flush(); } + @Override + public long getFlushedWhere() { + return this.commitLog.getFlushedWhere(); + } + @Override public boolean resetWriteOffset(long phyOffset) { - return this.commitLog.resetOffset(phyOffset); + //copy a new map + ConcurrentHashMap newMap = new ConcurrentHashMap<>(consumeQueueStore.getTopicQueueTable()); + SelectMappedBufferResult lastBuffer = null; + long startReadOffset = phyOffset == -1 ? 0 : phyOffset; + while ((lastBuffer = selectOneMessageByOffset(startReadOffset)) != null) { + try { + if (lastBuffer.getStartOffset() > startReadOffset) { + startReadOffset = lastBuffer.getStartOffset(); + continue; + } + + ByteBuffer bb = lastBuffer.getByteBuffer(); + int magicCode = bb.getInt(bb.position() + 4); + if (magicCode == CommitLog.BLANK_MAGIC_CODE) { + startReadOffset += bb.getInt(bb.position()); + continue; + } else if (magicCode != MessageDecoder.MESSAGE_MAGIC_CODE) { + throw new RuntimeException("Unknown magicCode: " + magicCode); + } + + lastBuffer.getByteBuffer().mark(); + + DispatchRequest dispatchRequest = checkMessageAndReturnSize(lastBuffer.getByteBuffer(), true, messageStoreConfig.isDuplicationEnable(), true); + if (!dispatchRequest.isSuccess()) + break; + + lastBuffer.getByteBuffer().reset(); + + MessageExt msg = MessageDecoder.decode(lastBuffer.getByteBuffer(), true, false, false, false, true); + if (msg == null) { + break; + } + String key = msg.getTopic() + "-" + msg.getQueueId(); + Long cur = newMap.get(key); + if (cur != null && cur > msg.getQueueOffset()) { + newMap.put(key, msg.getQueueOffset()); + } + startReadOffset += msg.getStoreSize(); + } catch (Throwable e) { + LOGGER.error("resetWriteOffset error.", e); + } finally { + if (lastBuffer != null) + lastBuffer.release(); + } + } + if (this.commitLog.resetOffset(phyOffset)) { + this.consumeQueueStore.setTopicQueueTable(newMap); + return true; + } else { + return false; + } } + // Fetch and compute the newest confirmOffset. + // Even if it is just inited. @Override public long getConfirmOffset() { return this.commitLog.getConfirmOffset(); } + // Fetch the original confirmOffset's value. + // Without checking and re-computing. + public long getConfirmOffsetDirectly() { + return this.commitLog.getConfirmOffsetDirectly(); + } + @Override public void setConfirmOffset(long phyOffset) { this.commitLog.setConfirmOffset(phyOffset); } + @Override + public byte[] calcDeltaChecksum(long from, long to) { + if (from < 0 || to <= from) { + return new byte[0]; + } + + int size = (int) (to - from); + + if (size > this.messageStoreConfig.getMaxChecksumRange()) { + LOGGER.error("Checksum range from {}, size {} exceeds threshold {}", from, size, this.messageStoreConfig.getMaxChecksumRange()); + return null; + } + + List msgList = new ArrayList<>(); + List bufferResultList = this.getBulkCommitLogData(from, size); + if (bufferResultList.isEmpty()) { + return new byte[0]; + } + + for (SelectMappedBufferResult bufferResult : bufferResultList) { + msgList.addAll(MessageDecoder.decodesBatch(bufferResult.getByteBuffer(), true, false, false)); + bufferResult.release(); + } + + if (msgList.isEmpty()) { + return new byte[0]; + } + + ByteBuffer byteBuffer = ByteBuffer.allocate(size); + for (MessageExt msg : msgList) { + try { + byteBuffer.put(MessageDecoder.encodeUniquely(msg, false)); + } catch (IOException ignore) { + } + } + + return Hashing.murmur3_128().hashBytes(byteBuffer.array()).asBytes(); + } + + @Override + public void setPhysicalOffset(long phyOffset) { + this.commitLog.setMappedFileQueueOffset(phyOffset); + } + + @Override + public boolean isMappedFilesEmpty() { + return this.commitLog.isMappedFilesEmpty(); + } + + @Override public MessageExt lookMessageByOffset(long commitLogOffset, int size) { SelectMappedBufferResult sbr = this.commitLog.getMessage(commitLogOffset, size); if (null != sbr) { @@ -1076,85 +1692,85 @@ public MessageExt lookMessageByOffset(long commitLogOffset, int size) { return null; } - public ConsumeQueue findConsumeQueue(String topic, int queueId) { - ConcurrentMap map = consumeQueueTable.get(topic); - if (null == map) { - ConcurrentMap newMap = new ConcurrentHashMap(128); - ConcurrentMap oldMap = consumeQueueTable.putIfAbsent(topic, newMap); - if (oldMap != null) { - map = oldMap; - } else { - map = newMap; - } - } - - ConsumeQueue logic = map.get(queueId); - if (null == logic) { - ConsumeQueue newLogic = new ConsumeQueue( - topic, - queueId, - StorePathConfigHelper.getStorePathConsumeQueue(this.messageStoreConfig.getStorePathRootDir()), - this.getMessageStoreConfig().getMapedFileSizeConsumeQueue(), - this); - ConsumeQueue oldLogic = map.putIfAbsent(queueId, newLogic); - if (oldLogic != null) { - logic = oldLogic; - } else { - logic = newLogic; - } - } - - return logic; + @Override + public ConsumeQueueInterface findConsumeQueue(String topic, int queueId) { + return this.consumeQueueStore.findOrCreateConsumeQueue(topic, queueId); } private long nextOffsetCorrection(long oldOffset, long newOffset) { long nextOffset = oldOffset; - if (this.getMessageStoreConfig().getBrokerRole() != BrokerRole.SLAVE || this.getMessageStoreConfig().isOffsetCheckInSlave()) { + if (this.getMessageStoreConfig().getBrokerRole() != BrokerRole.SLAVE || + this.getMessageStoreConfig().isOffsetCheckInSlave()) { nextOffset = newOffset; } return nextOffset; } - private boolean checkInDiskByCommitOffset(long offsetPy, long maxOffsetPy) { + private boolean estimateInMemByCommitOffset(long offsetPy, long maxOffsetPy) { long memory = (long) (StoreUtil.TOTAL_PHYSICAL_MEMORY_SIZE * (this.messageStoreConfig.getAccessMessageInMemoryMaxRatio() / 100.0)); + return (maxOffsetPy - offsetPy) <= memory; + } + + private boolean checkInMemByCommitOffset(long offsetPy, int size) { + SelectMappedBufferResult message = this.commitLog.getMessage(offsetPy, size); + if (message != null) { + try { + return message.isInMem(); + } finally { + message.release(); + } + } + return false; + } + + public boolean checkInDiskByCommitOffset(long offsetPy) { + return offsetPy >= commitLog.getMinOffset(); + } + + /** + * The ratio val is estimated by the experiment and experience + * so that the result is not high accurate for different business + * @return + */ + public boolean checkInColdAreaByCommitOffset(long offsetPy, long maxOffsetPy) { + long memory = (long)(StoreUtil.TOTAL_PHYSICAL_MEMORY_SIZE * (this.messageStoreConfig.getAccessMessageInMemoryHotRatio() / 100.0)); return (maxOffsetPy - offsetPy) > memory; } - private boolean isTheBatchFull(int sizePy, int maxMsgNums, int bufferTotal, int messageTotal, boolean isInDisk) { + private boolean isTheBatchFull(int sizePy, int unitBatchNum, int maxMsgNums, long maxMsgSize, int bufferTotal, + int messageTotal, boolean isInMem) { if (0 == bufferTotal || 0 == messageTotal) { return false; } - if (maxMsgNums <= messageTotal) { + if (messageTotal + unitBatchNum > maxMsgNums) { return true; } - if (isInDisk) { - if ((bufferTotal + sizePy) > this.messageStoreConfig.getMaxTransferBytesOnMessageInDisk()) { - return true; - } + if (bufferTotal + sizePy > maxMsgSize) { + return true; + } - if (messageTotal > this.messageStoreConfig.getMaxTransferCountOnMessageInDisk() - 1) { - return true; - } - } else { + if (isInMem) { if ((bufferTotal + sizePy) > this.messageStoreConfig.getMaxTransferBytesOnMessageInMemory()) { return true; } - if (messageTotal > this.messageStoreConfig.getMaxTransferCountOnMessageInMemory() - 1) { + return messageTotal > this.messageStoreConfig.getMaxTransferCountOnMessageInMemory() - 1; + } else { + if ((bufferTotal + sizePy) > this.messageStoreConfig.getMaxTransferBytesOnMessageInDisk()) { return true; } - } - return false; + return messageTotal > this.messageStoreConfig.getMaxTransferCountOnMessageInDisk() - 1; + } } private void deleteFile(final String fileName) { File file = new File(fileName); boolean result = file.delete(); - log.info(fileName + (result ? " delete OK" : " delete Failed")); + LOGGER.info(fileName + (result ? " delete OK" : " delete Failed")); } /** @@ -1163,30 +1779,31 @@ private void deleteFile(final String fileName) { private void createTempFile() throws IOException { String fileName = StorePathConfigHelper.getAbortFile(this.messageStoreConfig.getStorePathRootDir()); File file = new File(fileName); - MappedFile.ensureDirOK(file.getParent()); + UtilAll.ensureDirOK(file.getParent()); boolean result = file.createNewFile(); - log.info(fileName + (result ? " create OK" : " already exists")); + LOGGER.info(fileName + (result ? " create OK" : " already exists")); + MixAll.string2File(Long.toString(MixAll.getPID()), file.getAbsolutePath()); } private void addScheduleTask() { - this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + this.scheduledExecutorService.scheduleAtFixedRate(new AbstractBrokerRunnable(this.getBrokerIdentity()) { @Override - public void run() { + public void run0() { DefaultMessageStore.this.cleanFilesPeriodically(); } }, 1000 * 60, this.messageStoreConfig.getCleanResourceInterval(), TimeUnit.MILLISECONDS); - this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + this.scheduledExecutorService.scheduleAtFixedRate(new AbstractBrokerRunnable(this.getBrokerIdentity()) { @Override - public void run() { + public void run0() { DefaultMessageStore.this.checkSelf(); } }, 1, 10, TimeUnit.MINUTES); - this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + this.scheduledExecutorService.scheduleAtFixedRate(new AbstractBrokerRunnable(this.getBrokerIdentity()) { @Override - public void run() { + public void run0() { if (DefaultMessageStore.this.getMessageStoreConfig().isDebugLockEnable()) { try { if (DefaultMessageStore.this.commitLog.getBeginTimeInLock() != 0) { @@ -1205,6 +1822,21 @@ public void run() { } }, 1, 1, TimeUnit.SECONDS); + this.scheduledExecutorService.scheduleAtFixedRate(new AbstractBrokerRunnable(this.getBrokerIdentity()) { + @Override + public void run0() { + DefaultMessageStore.this.storeCheckpoint.flush(); + } + }, 1, 1, TimeUnit.SECONDS); + + this.scheduledCleanQueueExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + DefaultMessageStore.this.cleanQueueFilesPeriodically(); + } + }, 1000 * 60, this.messageStoreConfig.getCleanResourceInterval(), TimeUnit.MILLISECONDS); + + // this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { // @Override // public void run() { @@ -1215,21 +1847,16 @@ public void run() { private void cleanFilesPeriodically() { this.cleanCommitLogService.run(); + } + + private void cleanQueueFilesPeriodically() { + this.correctLogicOffsetService.run(); this.cleanConsumeQueueService.run(); } private void checkSelf() { this.commitLog.checkSelf(); - - Iterator>> it = this.consumeQueueTable.entrySet().iterator(); - while (it.hasNext()) { - Entry> next = it.next(); - Iterator> itNext = next.getValue().entrySet().iterator(); - while (itNext.hasNext()) { - Entry cq = itNext.next(); - cq.getValue().checkSelf(); - } - } + this.consumeQueueStore.checkSelf(); } private boolean isTempFileExist() { @@ -1238,100 +1865,82 @@ private boolean isTempFileExist() { return file.exists(); } - private boolean loadConsumeQueue() { - File dirLogic = new File(StorePathConfigHelper.getStorePathConsumeQueue(this.messageStoreConfig.getStorePathRootDir())); - File[] fileTopicList = dirLogic.listFiles(); - if (fileTopicList != null) { - - for (File fileTopic : fileTopicList) { - String topic = fileTopic.getName(); - - File[] fileQueueIdList = fileTopic.listFiles(); - if (fileQueueIdList != null) { - for (File fileQueueId : fileQueueIdList) { - int queueId; - try { - queueId = Integer.parseInt(fileQueueId.getName()); - } catch (NumberFormatException e) { - continue; - } - ConsumeQueue logic = new ConsumeQueue( - topic, - queueId, - StorePathConfigHelper.getStorePathConsumeQueue(this.messageStoreConfig.getStorePathRootDir()), - this.getMessageStoreConfig().getMapedFileSizeConsumeQueue(), - this); - this.putConsumeQueue(topic, queueId, logic); - if (!logic.load()) { - return false; - } - } - } - } - } - - log.info("load logics queue all over, OK"); - - return true; + private boolean isRecoverConcurrently() { + return this.brokerConfig.isRecoverConcurrently() && !this.messageStoreConfig.isEnableRocksDBStore(); } - private void recover(final boolean lastExitOK) { + private void recover(final boolean lastExitOK) throws RocksDBException { + boolean recoverConcurrently = this.isRecoverConcurrently(); + LOGGER.info("message store recover mode: {}", recoverConcurrently ? "concurrent" : "normal"); + + // recover consume queue + long recoverConsumeQueueStart = System.currentTimeMillis(); this.recoverConsumeQueue(); + long maxPhyOffsetOfConsumeQueue = this.consumeQueueStore.getMaxPhyOffsetInConsumeQueue(); + long recoverConsumeQueueEnd = System.currentTimeMillis(); + // recover commitlog if (lastExitOK) { - this.commitLog.recoverNormally(); + this.commitLog.recoverNormally(maxPhyOffsetOfConsumeQueue); } else { - this.commitLog.recoverAbnormally(); + this.commitLog.recoverAbnormally(maxPhyOffsetOfConsumeQueue); } + // recover consume offset table + long recoverCommitLogEnd = System.currentTimeMillis(); this.recoverTopicQueueTable(); + long recoverConsumeOffsetEnd = System.currentTimeMillis(); + + LOGGER.info("message store recover total cost: {} ms, " + + "recoverConsumeQueue: {} ms, recoverCommitLog: {} ms, recoverOffsetTable: {} ms", + recoverConsumeOffsetEnd - recoverConsumeQueueStart, recoverConsumeQueueEnd - recoverConsumeQueueStart, + recoverCommitLogEnd - recoverConsumeQueueEnd, recoverConsumeOffsetEnd - recoverCommitLogEnd); + } + + @Override + public long getTimingMessageCount(String topic) { + if (null == timerMessageStore) { + return 0L; + } else { + return timerMessageStore.getTimerMetrics().getTimingCount(topic); + } } + @Override public MessageStoreConfig getMessageStoreConfig() { return messageStoreConfig; } - public TransientStorePool getTransientStorePool() { - return transientStorePool; + @Override + public void finishCommitLogDispatch() { + // ignore } - private void putConsumeQueue(final String topic, final int queueId, final ConsumeQueue consumeQueue) { - ConcurrentMap map = this.consumeQueueTable.get(topic); - if (null == map) { - map = new ConcurrentHashMap(); - map.put(queueId, consumeQueue); - this.consumeQueueTable.put(topic, map); - } else { - map.put(queueId, consumeQueue); - } + @Override + public TransientStorePool getTransientStorePool() { + return transientStorePool; } private void recoverConsumeQueue() { - for (ConcurrentMap maps : this.consumeQueueTable.values()) { - for (ConsumeQueue logic : maps.values()) { - logic.recover(); - } + if (!this.isRecoverConcurrently()) { + this.consumeQueueStore.recover(); + } else { + this.consumeQueueStore.recoverConcurrently(); } } - private void recoverTopicQueueTable() { - HashMap table = new HashMap(1024); + @Override + public void recoverTopicQueueTable() { long minPhyOffset = this.commitLog.getMinOffset(); - for (ConcurrentMap maps : this.consumeQueueTable.values()) { - for (ConsumeQueue logic : maps.values()) { - String key = logic.getTopic() + "-" + logic.getQueueId(); - table.put(key, logic.getMaxOffsetInQueue()); - logic.correctMinOffset(minPhyOffset); - } - } - - this.commitLog.setTopicQueueTable(table); + this.consumeQueueStore.recoverOffsetTable(minPhyOffset); } + @Override public AllocateMappedFileService getAllocateMappedFileService() { return allocateMappedFileService; } + @Override public StoreStatsService getStoreStatsService() { return storeStatsService; } @@ -1340,43 +1949,67 @@ public RunningFlags getAccessRights() { return runningFlags; } - public ConcurrentMap> getConsumeQueueTable() { - return consumeQueueTable; + public ConcurrentMap> getConsumeQueueTable() { + return consumeQueueStore.getConsumeQueueTable(); } + @Override public StoreCheckpoint getStoreCheckpoint() { return storeCheckpoint; } + @Override public HAService getHaService() { return haService; } - public ScheduleMessageService getScheduleMessageService() { - return scheduleMessageService; - } - + @Override public RunningFlags getRunningFlags() { return runningFlags; } - public void doDispatch(DispatchRequest req) { + public void doDispatch(DispatchRequest req) throws RocksDBException { for (CommitLogDispatcher dispatcher : this.dispatcherList) { dispatcher.dispatch(req); } } - public void putMessagePositionInfo(DispatchRequest dispatchRequest) { - ConsumeQueue cq = this.findConsumeQueue(dispatchRequest.getTopic(), dispatchRequest.getQueueId()); - cq.putMessagePositionInfoWrapper(dispatchRequest); + /** + * @param dispatchRequest + * @throws RocksDBException only in rocksdb mode + */ + protected void putMessagePositionInfo(DispatchRequest dispatchRequest) throws RocksDBException { + this.consumeQueueStore.putMessagePositionInfoWrapper(dispatchRequest); + } + + @Override + public DispatchRequest checkMessageAndReturnSize(final ByteBuffer byteBuffer, final boolean checkCRC, + final boolean checkDupInfo, final boolean readBody) { + return this.commitLog.checkMessageAndReturnSize(byteBuffer, checkCRC, checkDupInfo, readBody); + } + + @Override + public long getStateMachineVersion() { + return stateMachineVersion; + } + + public void setStateMachineVersion(long stateMachineVersion) { + this.stateMachineVersion = stateMachineVersion; } public BrokerStatsManager getBrokerStatsManager() { return brokerStatsManager; } + public BrokerConfig getBrokerConfig() { + return brokerConfig; + } + public int remainTransientStoreBufferNumbs() { - return this.transientStorePool.remainBufferNumbs(); + if (this.isTransientStorePoolEnable()) { + return this.transientStorePool.availableBufferNums(); + } + return Integer.MAX_VALUE; } @Override @@ -1384,20 +2017,51 @@ public boolean isTransientStorePoolDeficient() { return remainTransientStoreBufferNumbs() == 0; } + @Override + public long remainHowManyDataToCommit() { + return this.commitLog.remainHowManyDataToCommit(); + } + + @Override + public long remainHowManyDataToFlush() { + return this.commitLog.remainHowManyDataToFlush(); + } + @Override public LinkedList getDispatcherList() { return this.dispatcherList; } @Override - public ConsumeQueue getConsumeQueue(String topic, int queueId) { - ConcurrentMap map = consumeQueueTable.get(topic); + public void addDispatcher(CommitLogDispatcher dispatcher) { + this.dispatcherList.add(dispatcher); + } + + @Override + public void setMasterStoreInProcess(MessageStore masterStoreInProcess) { + this.masterStoreInProcess = masterStoreInProcess; + } + + @Override + public MessageStore getMasterStoreInProcess() { + return this.masterStoreInProcess; + } + + @Override + public boolean getData(long offset, int size, ByteBuffer byteBuffer) { + return this.commitLog.getData(offset, size, byteBuffer); + } + + @Override + public ConsumeQueueInterface getConsumeQueue(String topic, int queueId) { + ConcurrentMap map = this.getConsumeQueueTable().get(topic); if (map == null) { return null; } return map.get(queueId); } + @Override public void unlockMappedFile(final MappedFile mappedFile) { this.scheduledExecutorService.schedule(new Runnable() { @Override @@ -1407,15 +2071,90 @@ public void run() { }, 6, TimeUnit.SECONDS); } + @Override + public PerfCounter.Ticks getPerfCounter() { + return perfs; + } + + @Override + public ConsumeQueueStoreInterface getQueueStore() { + return consumeQueueStore; + } + + @Override + public void onCommitLogAppend(MessageExtBrokerInner msg, AppendMessageResult result, MappedFile commitLogFile) { + // empty + } + + @Override + public void onCommitLogDispatch(DispatchRequest dispatchRequest, boolean doDispatch, MappedFile commitLogFile, + boolean isRecover, boolean isFileEnd) throws RocksDBException { + if (doDispatch && !isFileEnd) { + this.doDispatch(dispatchRequest); + } + } + + @Override + public boolean isSyncDiskFlush() { + return FlushDiskType.SYNC_FLUSH == this.getMessageStoreConfig().getFlushDiskType(); + } + + @Override + public boolean isSyncMaster() { + return BrokerRole.SYNC_MASTER == this.getMessageStoreConfig().getBrokerRole(); + } + + @Override + public void assignOffset(MessageExtBrokerInner msg) throws RocksDBException { + final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag()); + + if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE || tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) { + this.consumeQueueStore.assignQueueOffset(msg); + } + } + + @Override + public void increaseOffset(MessageExtBrokerInner msg, short messageNum) { + final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag()); + + if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE || tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) { + this.consumeQueueStore.increaseQueueOffset(msg, messageNum); + } + } + + public ConcurrentMap getTopicConfigs() { + return this.topicConfigTable; + } + + public Optional getTopicConfig(String topic) { + if (this.topicConfigTable == null) { + return Optional.empty(); + } + + return Optional.ofNullable(this.topicConfigTable.get(topic)); + } + + public BrokerIdentity getBrokerIdentity() { + if (messageStoreConfig.isEnableDLegerCommitLog()) { + return new BrokerIdentity( + brokerConfig.getBrokerClusterName(), brokerConfig.getBrokerName(), + Integer.parseInt(messageStoreConfig.getdLegerSelfId().substring(1)), brokerConfig.isInBrokerContainer()); + } else { + return new BrokerIdentity( + brokerConfig.getBrokerClusterName(), brokerConfig.getBrokerName(), + brokerConfig.getBrokerId(), brokerConfig.isInBrokerContainer()); + } + } + class CommitLogDispatcherBuildConsumeQueue implements CommitLogDispatcher { @Override - public void dispatch(DispatchRequest request) { + public void dispatch(DispatchRequest request) throws RocksDBException { final int tranType = MessageSysFlag.getTransactionValue(request.getSysFlag()); switch (tranType) { case MessageSysFlag.TRANSACTION_NOT_TYPE: case MessageSysFlag.TRANSACTION_COMMIT_TYPE: - DefaultMessageStore.this.putMessagePositionInfo(request); + putMessagePositionInfo(request); break; case MessageSysFlag.TRANSACTION_PREPARED_TYPE: case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE: @@ -1437,29 +2176,66 @@ public void dispatch(DispatchRequest request) { class CleanCommitLogService { private final static int MAX_MANUAL_DELETE_FILE_TIMES = 20; - private final double diskSpaceWarningLevelRatio = - Double.parseDouble(System.getProperty("rocketmq.broker.diskSpaceWarningLevelRatio", "0.90")); + private final String diskSpaceWarningLevelRatio = + System.getProperty("rocketmq.broker.diskSpaceWarningLevelRatio", ""); - private final double diskSpaceCleanForciblyRatio = - Double.parseDouble(System.getProperty("rocketmq.broker.diskSpaceCleanForciblyRatio", "0.85")); + private final String diskSpaceCleanForciblyRatio = + System.getProperty("rocketmq.broker.diskSpaceCleanForciblyRatio", ""); private long lastRedeleteTimestamp = 0; private volatile int manualDeleteFileSeveralTimes = 0; private volatile boolean cleanImmediately = false; - public void excuteDeleteFilesManualy() { + private int forceCleanFailedTimes = 0; + + double getDiskSpaceWarningLevelRatio() { + double finalDiskSpaceWarningLevelRatio; + if ("".equals(diskSpaceWarningLevelRatio)) { + finalDiskSpaceWarningLevelRatio = DefaultMessageStore.this.getMessageStoreConfig().getDiskSpaceWarningLevelRatio() / 100.0; + } else { + finalDiskSpaceWarningLevelRatio = Double.parseDouble(diskSpaceWarningLevelRatio); + } + + if (finalDiskSpaceWarningLevelRatio > 0.90) { + finalDiskSpaceWarningLevelRatio = 0.90; + } + if (finalDiskSpaceWarningLevelRatio < 0.35) { + finalDiskSpaceWarningLevelRatio = 0.35; + } + + return finalDiskSpaceWarningLevelRatio; + } + + double getDiskSpaceCleanForciblyRatio() { + double finalDiskSpaceCleanForciblyRatio; + if ("".equals(diskSpaceCleanForciblyRatio)) { + finalDiskSpaceCleanForciblyRatio = DefaultMessageStore.this.getMessageStoreConfig().getDiskSpaceCleanForciblyRatio() / 100.0; + } else { + finalDiskSpaceCleanForciblyRatio = Double.parseDouble(diskSpaceCleanForciblyRatio); + } + + if (finalDiskSpaceCleanForciblyRatio > 0.85) { + finalDiskSpaceCleanForciblyRatio = 0.85; + } + if (finalDiskSpaceCleanForciblyRatio < 0.30) { + finalDiskSpaceCleanForciblyRatio = 0.30; + } + + return finalDiskSpaceCleanForciblyRatio; + } + + public void executeDeleteFilesManually() { this.manualDeleteFileSeveralTimes = MAX_MANUAL_DELETE_FILE_TIMES; - DefaultMessageStore.log.info("executeDeleteFilesManually was invoked"); + DefaultMessageStore.LOGGER.info("executeDeleteFilesManually was invoked"); } public void run() { try { this.deleteExpiredFiles(); - - this.redeleteHangedFile(); + this.reDeleteHangedFile(); } catch (Throwable e) { - DefaultMessageStore.log.warn(this.getServiceName() + " service has exception. ", e); + DefaultMessageStore.LOGGER.warn(this.getServiceName() + " service has exception. ", e); } } @@ -1467,57 +2243,67 @@ private void deleteExpiredFiles() { int deleteCount = 0; long fileReservedTime = DefaultMessageStore.this.getMessageStoreConfig().getFileReservedTime(); int deletePhysicFilesInterval = DefaultMessageStore.this.getMessageStoreConfig().getDeleteCommitLogFilesInterval(); - int destroyMapedFileIntervalForcibly = DefaultMessageStore.this.getMessageStoreConfig().getDestroyMapedFileIntervalForcibly(); + int destroyMappedFileIntervalForcibly = DefaultMessageStore.this.getMessageStoreConfig().getDestroyMapedFileIntervalForcibly(); + int deleteFileBatchMax = DefaultMessageStore.this.getMessageStoreConfig().getDeleteFileBatchMax(); - boolean timeup = this.isTimeToDelete(); - boolean spacefull = this.isSpaceToDelete(); - boolean manualDelete = this.manualDeleteFileSeveralTimes > 0; + boolean isTimeUp = this.isTimeToDelete(); + boolean isUsageExceedsThreshold = this.isSpaceToDelete(); + boolean isManualDelete = this.manualDeleteFileSeveralTimes > 0; - if (timeup || spacefull || manualDelete) { + if (isTimeUp || isUsageExceedsThreshold || isManualDelete) { - if (manualDelete) + if (isManualDelete) { this.manualDeleteFileSeveralTimes--; + } boolean cleanAtOnce = DefaultMessageStore.this.getMessageStoreConfig().isCleanFileForciblyEnable() && this.cleanImmediately; - log.info("begin to delete before {} hours file. timeup: {} spacefull: {} manualDeleteFileSeveralTimes: {} cleanAtOnce: {}", + LOGGER.info("begin to delete before {} hours file. isTimeUp: {} isUsageExceedsThreshold: {} manualDeleteFileSeveralTimes: {} cleanAtOnce: {} deleteFileBatchMax: {}", fileReservedTime, - timeup, - spacefull, + isTimeUp, + isUsageExceedsThreshold, manualDeleteFileSeveralTimes, - cleanAtOnce); + cleanAtOnce, + deleteFileBatchMax); fileReservedTime *= 60 * 60 * 1000; deleteCount = DefaultMessageStore.this.commitLog.deleteExpiredFile(fileReservedTime, deletePhysicFilesInterval, - destroyMapedFileIntervalForcibly, cleanAtOnce); + destroyMappedFileIntervalForcibly, cleanAtOnce, deleteFileBatchMax); if (deleteCount > 0) { - } else if (spacefull) { - log.warn("disk space will be full soon, but delete file failed."); + // If in the controller mode, we should notify the AutoSwitchHaService to truncateEpochFile + if (DefaultMessageStore.this.brokerConfig.isEnableControllerMode()) { + if (DefaultMessageStore.this.haService instanceof AutoSwitchHAService) { + final long minPhyOffset = getMinPhyOffset(); + ((AutoSwitchHAService) DefaultMessageStore.this.haService).truncateEpochFilePrefix(minPhyOffset - 1); + } + } + } else if (isUsageExceedsThreshold) { + LOGGER.warn("disk space will be full soon, but delete file failed."); } } } - private void redeleteHangedFile() { + private void reDeleteHangedFile() { int interval = DefaultMessageStore.this.getMessageStoreConfig().getRedeleteHangedFileInterval(); long currentTimestamp = System.currentTimeMillis(); if ((currentTimestamp - this.lastRedeleteTimestamp) > interval) { this.lastRedeleteTimestamp = currentTimestamp; - int destroyMapedFileIntervalForcibly = + int destroyMappedFileIntervalForcibly = DefaultMessageStore.this.getMessageStoreConfig().getDestroyMapedFileIntervalForcibly(); - if (DefaultMessageStore.this.commitLog.retryDeleteFirstFile(destroyMapedFileIntervalForcibly)) { + if (DefaultMessageStore.this.commitLog.retryDeleteFirstFile(destroyMappedFileIntervalForcibly)) { } } } public String getServiceName() { - return CleanCommitLogService.class.getSimpleName(); + return DefaultMessageStore.this.brokerConfig.getIdentifier() + CleanCommitLogService.class.getSimpleName(); } - private boolean isTimeToDelete() { + protected boolean isTimeToDelete() { String when = DefaultMessageStore.this.getMessageStoreConfig().getDeleteWhen(); if (UtilAll.isItTimeToDo(when)) { - DefaultMessageStore.log.info("it's time to reclaim disk space, " + when); + DefaultMessageStore.LOGGER.info("it's time to reclaim disk space, " + when); return true; } @@ -1525,62 +2311,99 @@ private boolean isTimeToDelete() { } private boolean isSpaceToDelete() { - double ratio = DefaultMessageStore.this.getMessageStoreConfig().getDiskMaxUsedSpaceRatio() / 100.0; - cleanImmediately = false; - { - String storePathPhysic = DefaultMessageStore.this.getMessageStoreConfig().getStorePathCommitLog(); + String commitLogStorePath = DefaultMessageStore.this.getMessageStoreConfig().getStorePathCommitLog(); + String[] storePaths = commitLogStorePath.trim().split(MixAll.MULTI_PATH_SPLITTER); + Set fullStorePath = new HashSet<>(); + double minPhysicRatio = 100; + String minStorePath = null; + for (String storePathPhysic : storePaths) { double physicRatio = UtilAll.getDiskPartitionSpaceUsedPercent(storePathPhysic); - if (physicRatio > diskSpaceWarningLevelRatio) { - boolean diskok = DefaultMessageStore.this.runningFlags.getAndMakeDiskFull(); - if (diskok) { - DefaultMessageStore.log.error("physic disk maybe full soon " + physicRatio + ", so mark disk full"); - } - - cleanImmediately = true; - } else if (physicRatio > diskSpaceCleanForciblyRatio) { - cleanImmediately = true; - } else { - boolean diskok = DefaultMessageStore.this.runningFlags.getAndMakeDiskOK(); - if (!diskok) { - DefaultMessageStore.log.info("physic disk space OK " + physicRatio + ", so mark disk ok"); - } + if (minPhysicRatio > physicRatio) { + minPhysicRatio = physicRatio; + minStorePath = storePathPhysic; + } + if (physicRatio > getDiskSpaceCleanForciblyRatio()) { + fullStorePath.add(storePathPhysic); + } + } + DefaultMessageStore.this.commitLog.setFullStorePaths(fullStorePath); + if (minPhysicRatio > getDiskSpaceWarningLevelRatio()) { + boolean diskFull = DefaultMessageStore.this.runningFlags.getAndMakeDiskFull(); + if (diskFull) { + DefaultMessageStore.LOGGER.error("physic disk maybe full soon " + minPhysicRatio + + ", so mark disk full, storePathPhysic=" + minStorePath); } - if (physicRatio < 0 || physicRatio > ratio) { - DefaultMessageStore.log.info("physic disk maybe full soon, so reclaim space, " + physicRatio); - return true; + cleanImmediately = true; + return true; + } else if (minPhysicRatio > getDiskSpaceCleanForciblyRatio()) { + cleanImmediately = true; + return true; + } else { + boolean diskOK = DefaultMessageStore.this.runningFlags.getAndMakeDiskOK(); + if (!diskOK) { + DefaultMessageStore.LOGGER.info("physic disk space OK " + minPhysicRatio + + ", so mark disk ok, storePathPhysic=" + minStorePath); } } - { - String storePathLogics = StorePathConfigHelper - .getStorePathConsumeQueue(DefaultMessageStore.this.getMessageStoreConfig().getStorePathRootDir()); - double logicsRatio = UtilAll.getDiskPartitionSpaceUsedPercent(storePathLogics); - if (logicsRatio > diskSpaceWarningLevelRatio) { - boolean diskok = DefaultMessageStore.this.runningFlags.getAndMakeDiskFull(); - if (diskok) { - DefaultMessageStore.log.error("logics disk maybe full soon " + logicsRatio + ", so mark disk full"); - } + String storePathLogics = StorePathConfigHelper + .getStorePathConsumeQueue(DefaultMessageStore.this.getMessageStoreConfig().getStorePathRootDir()); + double logicsRatio = UtilAll.getDiskPartitionSpaceUsedPercent(storePathLogics); + if (logicsRatio > getDiskSpaceWarningLevelRatio()) { + boolean diskOK = DefaultMessageStore.this.runningFlags.getAndMakeDiskFull(); + if (diskOK) { + DefaultMessageStore.LOGGER.error("logics disk maybe full soon " + logicsRatio + ", so mark disk full"); + } - cleanImmediately = true; - } else if (logicsRatio > diskSpaceCleanForciblyRatio) { - cleanImmediately = true; - } else { - boolean diskok = DefaultMessageStore.this.runningFlags.getAndMakeDiskOK(); - if (!diskok) { - DefaultMessageStore.log.info("logics disk space OK " + logicsRatio + ", so mark disk ok"); - } + cleanImmediately = true; + return true; + } else if (logicsRatio > getDiskSpaceCleanForciblyRatio()) { + cleanImmediately = true; + return true; + } else { + boolean diskOK = DefaultMessageStore.this.runningFlags.getAndMakeDiskOK(); + if (!diskOK) { + DefaultMessageStore.LOGGER.info("logics disk space OK " + logicsRatio + ", so mark disk ok"); + } + } + + double ratio = DefaultMessageStore.this.getMessageStoreConfig().getDiskMaxUsedSpaceRatio() / 100.0; + int replicasPerPartition = DefaultMessageStore.this.getMessageStoreConfig().getReplicasPerDiskPartition(); + // Only one commitLog in node + if (replicasPerPartition <= 1) { + if (minPhysicRatio < 0 || minPhysicRatio > ratio) { + DefaultMessageStore.LOGGER.info("commitLog disk maybe full soon, so reclaim space, " + minPhysicRatio); + return true; } if (logicsRatio < 0 || logicsRatio > ratio) { - DefaultMessageStore.log.info("logics disk maybe full soon, so reclaim space, " + logicsRatio); + DefaultMessageStore.LOGGER.info("consumeQueue disk maybe full soon, so reclaim space, " + logicsRatio); + return true; + } + return false; + } else { + long majorFileSize = DefaultMessageStore.this.getMajorFileSize(); + long partitionLogicalSize = UtilAll.getDiskPartitionTotalSpace(minStorePath) / replicasPerPartition; + double logicalRatio = 1.0 * majorFileSize / partitionLogicalSize; + + if (logicalRatio > DefaultMessageStore.this.getMessageStoreConfig().getLogicalDiskSpaceCleanForciblyThreshold()) { + // if logical ratio exceeds 0.80, then clean immediately + DefaultMessageStore.LOGGER.info("Logical disk usage {} exceeds logical disk space clean forcibly threshold {}, forcibly: {}", + logicalRatio, minPhysicRatio, cleanImmediately); + cleanImmediately = true; return true; } - } - return false; + boolean isUsageExceedsThreshold = logicalRatio > ratio; + if (isUsageExceedsThreshold) { + DefaultMessageStore.LOGGER.info("Logical disk usage {} exceeds clean threshold {}, forcibly: {}", + logicalRatio, ratio, cleanImmediately); + } + return isUsageExceedsThreshold; + } } public int getManualDeleteFileSeveralTimes() { @@ -1590,32 +2413,73 @@ public int getManualDeleteFileSeveralTimes() { public void setManualDeleteFileSeveralTimes(int manualDeleteFileSeveralTimes) { this.manualDeleteFileSeveralTimes = manualDeleteFileSeveralTimes; } + + public double calcStorePathPhysicRatio() { + Set fullStorePath = new HashSet<>(); + String storePath = getStorePathPhysic(); + String[] paths = storePath.trim().split(MixAll.MULTI_PATH_SPLITTER); + double minPhysicRatio = 100; + for (String path : paths) { + double physicRatio = UtilAll.isPathExists(path) ? + UtilAll.getDiskPartitionSpaceUsedPercent(path) : -1; + minPhysicRatio = Math.min(minPhysicRatio, physicRatio); + if (physicRatio > getDiskSpaceCleanForciblyRatio()) { + fullStorePath.add(path); + } + } + DefaultMessageStore.this.commitLog.setFullStorePaths(fullStorePath); + return minPhysicRatio; + + } + + public boolean isSpaceFull() { + double physicRatio = calcStorePathPhysicRatio(); + double ratio = DefaultMessageStore.this.getMessageStoreConfig().getDiskMaxUsedSpaceRatio() / 100.0; + if (physicRatio > ratio) { + DefaultMessageStore.LOGGER.info("physic disk of commitLog used: " + physicRatio); + } + if (physicRatio > this.getDiskSpaceWarningLevelRatio()) { + boolean diskok = DefaultMessageStore.this.runningFlags.getAndMakeDiskFull(); + if (diskok) { + DefaultMessageStore.LOGGER.error("physic disk of commitLog maybe full soon, used " + physicRatio + ", so mark disk full"); + } + + return true; + } else { + boolean diskok = DefaultMessageStore.this.runningFlags.getAndMakeDiskOK(); + + if (!diskok) { + DefaultMessageStore.LOGGER.info("physic disk space of commitLog OK " + physicRatio + ", so mark disk ok"); + } + + return false; + } + } } class CleanConsumeQueueService { - private long lastPhysicalMinOffset = 0; + protected long lastPhysicalMinOffset = 0; public void run() { try { this.deleteExpiredFiles(); } catch (Throwable e) { - DefaultMessageStore.log.warn(this.getServiceName() + " service has exception. ", e); + DefaultMessageStore.LOGGER.warn(this.getServiceName() + " service has exception. ", e); } } - private void deleteExpiredFiles() { + protected void deleteExpiredFiles() { int deleteLogicsFilesInterval = DefaultMessageStore.this.getMessageStoreConfig().getDeleteConsumeQueueFilesInterval(); long minOffset = DefaultMessageStore.this.commitLog.getMinOffset(); if (minOffset > this.lastPhysicalMinOffset) { this.lastPhysicalMinOffset = minOffset; - ConcurrentMap> tables = DefaultMessageStore.this.consumeQueueTable; - - for (ConcurrentMap maps : tables.values()) { - for (ConsumeQueue logic : maps.values()) { - int deleteCount = logic.deleteExpiredFile(minOffset); + ConcurrentMap> tables = DefaultMessageStore.this.getConsumeQueueTable(); + for (ConcurrentMap maps : tables.values()) { + for (ConsumeQueueInterface logic : maps.values()) { + int deleteCount = DefaultMessageStore.this.consumeQueueStore.deleteExpiredFile(logic, minOffset); if (deleteCount > 0 && deleteLogicsFilesInterval > 0) { try { Thread.sleep(deleteLogicsFilesInterval); @@ -1630,7 +2494,127 @@ private void deleteExpiredFiles() { } public String getServiceName() { - return CleanConsumeQueueService.class.getSimpleName(); + return DefaultMessageStore.this.brokerConfig.getIdentifier() + CleanConsumeQueueService.class.getSimpleName(); + } + } + + class CorrectLogicOffsetService { + private long lastForceCorrectTime = -1L; + + public void run() { + try { + this.correctLogicMinOffset(); + } catch (Throwable e) { + LOGGER.warn(this.getServiceName() + " service has exception. ", e); + } + } + + private boolean needCorrect(ConsumeQueueInterface logic, long minPhyOffset, long lastForeCorrectTimeCurRun) { + if (logic == null) { + return false; + } + // If first exist and not available, it means first file may destroy failed, delete it. + if (DefaultMessageStore.this.consumeQueueStore.isFirstFileExist(logic) && !DefaultMessageStore.this.consumeQueueStore.isFirstFileAvailable(logic)) { + LOGGER.error("CorrectLogicOffsetService.needCorrect. first file not available, trigger correct." + + " topic:{}, queue:{}, maxPhyOffset in queue:{}, minPhyOffset " + + "in commit log:{}, minOffset in queue:{}, maxOffset in queue:{}, cqType:{}" + , logic.getTopic(), logic.getQueueId(), logic.getMaxPhysicOffset() + , minPhyOffset, logic.getMinOffsetInQueue(), logic.getMaxOffsetInQueue(), logic.getCQType()); + return true; + } + + // logic.getMaxPhysicOffset() or minPhyOffset = -1 + // means there is no message in current queue, so no need to correct. + if (logic.getMaxPhysicOffset() == -1 || minPhyOffset == -1) { + return false; + } + + if (logic.getMaxPhysicOffset() < minPhyOffset) { + if (logic.getMinOffsetInQueue() < logic.getMaxOffsetInQueue()) { + LOGGER.error("CorrectLogicOffsetService.needCorrect. logic max phy offset: {} is less than min phy offset: {}, " + + "but min offset: {} is less than max offset: {}. topic:{}, queue:{}, cqType:{}." + , logic.getMaxPhysicOffset(), minPhyOffset, logic.getMinOffsetInQueue() + , logic.getMaxOffsetInQueue(), logic.getTopic(), logic.getQueueId(), logic.getCQType()); + return true; + } else if (logic.getMinOffsetInQueue() == logic.getMaxOffsetInQueue()) { + return false; + } else { + LOGGER.error("CorrectLogicOffsetService.needCorrect. It should not happen, logic max phy offset: {} is less than min phy offset: {}," + + " but min offset: {} is larger than max offset: {}. topic:{}, queue:{}, cqType:{}" + , logic.getMaxPhysicOffset(), minPhyOffset, logic.getMinOffsetInQueue() + , logic.getMaxOffsetInQueue(), logic.getTopic(), logic.getQueueId(), logic.getCQType()); + return false; + } + } + //the logic.getMaxPhysicOffset() >= minPhyOffset + int forceCorrectInterval = DefaultMessageStore.this.getMessageStoreConfig().getCorrectLogicMinOffsetForceInterval(); + if ((System.currentTimeMillis() - lastForeCorrectTimeCurRun) > forceCorrectInterval) { + lastForceCorrectTime = System.currentTimeMillis(); + CqUnit cqUnit = logic.getEarliestUnit(); + if (cqUnit == null) { + if (logic.getMinOffsetInQueue() == logic.getMaxOffsetInQueue()) { + return false; + } else { + LOGGER.error("CorrectLogicOffsetService.needCorrect. cqUnit is null, logic max phy offset: {} is greater than min phy offset: {}, " + + "but min offset: {} is not equal to max offset: {}. topic:{}, queue:{}, cqType:{}." + , logic.getMaxPhysicOffset(), minPhyOffset, logic.getMinOffsetInQueue() + , logic.getMaxOffsetInQueue(), logic.getTopic(), logic.getQueueId(), logic.getCQType()); + return true; + } + } + + if (cqUnit.getPos() < minPhyOffset) { + LOGGER.error("CorrectLogicOffsetService.needCorrect. logic max phy offset: {} is greater than min phy offset: {}, " + + "but minPhyPos in cq is: {}. min offset in queue: {}, max offset in queue: {}, topic:{}, queue:{}, cqType:{}." + , logic.getMaxPhysicOffset(), minPhyOffset, cqUnit.getPos(), logic.getMinOffsetInQueue() + , logic.getMaxOffsetInQueue(), logic.getTopic(), logic.getQueueId(), logic.getCQType()); + return true; + } + + if (cqUnit.getPos() >= minPhyOffset) { + + // Normal case, do not need to correct. + return false; + } + } + + return false; + } + + private void correctLogicMinOffset() { + + long lastForeCorrectTimeCurRun = lastForceCorrectTime; + long minPhyOffset = getMinPhyOffset(); + ConcurrentMap> tables = DefaultMessageStore.this.getConsumeQueueTable(); + for (ConcurrentMap maps : tables.values()) { + for (ConsumeQueueInterface logic : maps.values()) { + if (Objects.equals(CQType.SimpleCQ, logic.getCQType())) { + // cq is not supported for now. + continue; + } + if (needCorrect(logic, minPhyOffset, lastForeCorrectTimeCurRun)) { + doCorrect(logic, minPhyOffset); + } + } + } + } + + private void doCorrect(ConsumeQueueInterface logic, long minPhyOffset) { + DefaultMessageStore.this.consumeQueueStore.deleteExpiredFile(logic, minPhyOffset); + int sleepIntervalWhenCorrectMinOffset = DefaultMessageStore.this.getMessageStoreConfig().getCorrectLogicMinOffsetSleepInterval(); + if (sleepIntervalWhenCorrectMinOffset > 0) { + try { + Thread.sleep(sleepIntervalWhenCorrectMinOffset); + } catch (InterruptedException ignored) { + } + } + } + + public String getServiceName() { + if (brokerConfig.isInBrokerContainer()) { + return brokerConfig.getIdentifier() + CorrectLogicOffsetService.class.getSimpleName(); + } + return CorrectLogicOffsetService.class.getSimpleName(); } } @@ -1655,17 +2639,21 @@ private void doFlush(int retryTimes) { logicsMsgTimestamp = DefaultMessageStore.this.getStoreCheckpoint().getLogicsMsgTimestamp(); } - ConcurrentMap> tables = DefaultMessageStore.this.consumeQueueTable; + ConcurrentMap> tables = DefaultMessageStore.this.getConsumeQueueTable(); - for (ConcurrentMap maps : tables.values()) { - for (ConsumeQueue cq : maps.values()) { + for (ConcurrentMap maps : tables.values()) { + for (ConsumeQueueInterface cq : maps.values()) { boolean result = false; for (int i = 0; i < retryTimes && !result; i++) { - result = cq.flush(flushConsumeQueueLeastPages); + result = DefaultMessageStore.this.consumeQueueStore.flush(cq, flushConsumeQueueLeastPages); } } } + if (messageStoreConfig.isEnableCompaction()) { + compactionStore.flush(flushConsumeQueueLeastPages); + } + if (0 == flushConsumeQueueLeastPages) { if (logicsMsgTimestamp > 0) { DefaultMessageStore.this.getStoreCheckpoint().setLogicsMsgTimestamp(logicsMsgTimestamp); @@ -1674,8 +2662,9 @@ private void doFlush(int retryTimes) { } } + @Override public void run() { - DefaultMessageStore.log.info(this.getServiceName() + " service started"); + DefaultMessageStore.LOGGER.info(this.getServiceName() + " service started"); while (!this.isStopped()) { try { @@ -1683,29 +2672,112 @@ public void run() { this.waitForRunning(interval); this.doFlush(1); } catch (Exception e) { - DefaultMessageStore.log.warn(this.getServiceName() + " service has exception. ", e); + DefaultMessageStore.LOGGER.warn(this.getServiceName() + " service has exception. ", e); } } this.doFlush(RETRY_TIMES_OVER); - DefaultMessageStore.log.info(this.getServiceName() + " service end"); + DefaultMessageStore.LOGGER.info(this.getServiceName() + " service end"); } @Override public String getServiceName() { + if (DefaultMessageStore.this.brokerConfig.isInBrokerContainer()) { + return DefaultMessageStore.this.getBrokerIdentity().getIdentifier() + FlushConsumeQueueService.class.getSimpleName(); + } return FlushConsumeQueueService.class.getSimpleName(); } @Override - public long getJointime() { + public long getJoinTime() { return 1000 * 60; } } + class BatchDispatchRequest { + + private ByteBuffer byteBuffer; + + private int position; + + private int size; + + private long id; + + public BatchDispatchRequest(ByteBuffer byteBuffer, int position, int size, long id) { + this.byteBuffer = byteBuffer; + this.position = position; + this.size = size; + this.id = id; + } + } + + class DispatchRequestOrderlyQueue { + + DispatchRequest[][] buffer; + + long ptr = 0; + + AtomicLong maxPtr = new AtomicLong(); + + public DispatchRequestOrderlyQueue(int bufferNum) { + this.buffer = new DispatchRequest[bufferNum][]; + } + + public void put(long index, DispatchRequest[] dispatchRequests) { + while (ptr + this.buffer.length <= index) { + synchronized (this) { + try { + this.wait(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + int mod = (int) (index % this.buffer.length); + this.buffer[mod] = dispatchRequests; + maxPtr.incrementAndGet(); + } + + public DispatchRequest[] get(List dispatchRequestsList) { + synchronized (this) { + for (int i = 0; i < this.buffer.length; i++) { + int mod = (int) (ptr % this.buffer.length); + DispatchRequest[] ret = this.buffer[mod]; + if (ret == null) { + this.notifyAll(); + return null; + } + dispatchRequestsList.add(ret); + this.buffer[mod] = null; + ptr++; + } + } + return null; + } + + public synchronized boolean isEmpty() { + return maxPtr.get() == ptr; + } + + } + + @Override + public void notifyMessageArriveIfNecessary(DispatchRequest dispatchRequest) { + if (DefaultMessageStore.this.brokerConfig.isLongPollingEnable() + && DefaultMessageStore.this.messageArrivingListener != null) { + DefaultMessageStore.this.messageArrivingListener.arriving(dispatchRequest.getTopic(), + dispatchRequest.getQueueId(), dispatchRequest.getConsumeQueueOffset() + 1, + dispatchRequest.getTagsCode(), dispatchRequest.getStoreTimestamp(), + dispatchRequest.getBitMap(), dispatchRequest.getPropertiesMap()); + DefaultMessageStore.this.reputMessageService.notifyMessageArrive4MultiQueue(dispatchRequest); + } + } + class ReputMessageService extends ServiceThread { - private volatile long reputFromOffset = 0; + protected volatile long reputFromOffset = 0; public long getReputFromOffset() { return reputFromOffset; @@ -1725,109 +2797,532 @@ public void shutdown() { } if (this.isCommitLogAvailable()) { - log.warn("shutdown ReputMessageService, but commitlog have not finish to be dispatched, CL: {} reputFromOffset: {}", - DefaultMessageStore.this.commitLog.getMaxOffset(), this.reputFromOffset); + LOGGER.warn("shutdown ReputMessageService, but CommitLog have not finish to be dispatched, CommitLog max" + + " offset={}, reputFromOffset={}", DefaultMessageStore.this.commitLog.getMaxOffset(), + this.reputFromOffset); } super.shutdown(); } public long behind() { - return DefaultMessageStore.this.commitLog.getMaxOffset() - this.reputFromOffset; + return DefaultMessageStore.this.getConfirmOffset() - this.reputFromOffset; } - private boolean isCommitLogAvailable() { - return this.reputFromOffset < DefaultMessageStore.this.commitLog.getMaxOffset(); + public boolean isCommitLogAvailable() { + return this.reputFromOffset < DefaultMessageStore.this.getConfirmOffset(); } - private void doReput() { + public void doReput() { + if (this.reputFromOffset < DefaultMessageStore.this.commitLog.getMinOffset()) { + LOGGER.warn("The reputFromOffset={} is smaller than minPyOffset={}, this usually indicate that the dispatch behind too much and the commitlog has expired.", + this.reputFromOffset, DefaultMessageStore.this.commitLog.getMinOffset()); + this.reputFromOffset = DefaultMessageStore.this.commitLog.getMinOffset(); + } for (boolean doNext = true; this.isCommitLogAvailable() && doNext; ) { - if (DefaultMessageStore.this.getMessageStoreConfig().isDuplicationEnable() - && this.reputFromOffset >= DefaultMessageStore.this.getConfirmOffset()) { + SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset); + + if (result == null) { break; } - SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset); - if (result != null) { - try { - this.reputFromOffset = result.getStartOffset(); - - for (int readSize = 0; readSize < result.getSize() && doNext; ) { - DispatchRequest dispatchRequest = - DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false); - int size = dispatchRequest.getMsgSize(); - - if (dispatchRequest.isSuccess()) { - if (size > 0) { - DefaultMessageStore.this.doDispatch(dispatchRequest); - - if (BrokerRole.SLAVE != DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole() - && DefaultMessageStore.this.brokerConfig.isLongPollingEnable()) { - DefaultMessageStore.this.messageArrivingListener.arriving(dispatchRequest.getTopic(), - dispatchRequest.getQueueId(), dispatchRequest.getConsumeQueueOffset() + 1, - dispatchRequest.getTagsCode(), dispatchRequest.getStoreTimestamp(), - dispatchRequest.getBitMap(), dispatchRequest.getPropertiesMap()); - } - - this.reputFromOffset += size; - readSize += size; - if (DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole() == BrokerRole.SLAVE) { - DefaultMessageStore.this.storeStatsService - .getSinglePutMessageTopicTimesTotal(dispatchRequest.getTopic()).incrementAndGet(); - DefaultMessageStore.this.storeStatsService - .getSinglePutMessageTopicSizeTotal(dispatchRequest.getTopic()) - .addAndGet(dispatchRequest.getMsgSize()); - } - } else if (size == 0) { - this.reputFromOffset = DefaultMessageStore.this.commitLog.rollNextFile(this.reputFromOffset); - readSize = result.getSize(); - } - } else if (!dispatchRequest.isSuccess()) { + try { + this.reputFromOffset = result.getStartOffset(); - if (size > 0) { - log.error("[BUG]read total count not equals msg total size. reputFromOffset={}", reputFromOffset); - this.reputFromOffset += size; - } else { - doNext = false; - if (DefaultMessageStore.this.brokerConfig.getBrokerId() == MixAll.MASTER_ID) { - log.error("[BUG]the master dispatch message to consume queue error, COMMITLOG OFFSET: {}", - this.reputFromOffset); + for (int readSize = 0; readSize < result.getSize() && reputFromOffset < DefaultMessageStore.this.getConfirmOffset() && doNext; ) { + DispatchRequest dispatchRequest = + DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false, false); + int size = dispatchRequest.getBufferSize() == -1 ? dispatchRequest.getMsgSize() : dispatchRequest.getBufferSize(); + + if (reputFromOffset + size > DefaultMessageStore.this.getConfirmOffset()) { + doNext = false; + break; + } - this.reputFromOffset += result.getSize() - readSize; - } + if (dispatchRequest.isSuccess()) { + if (size > 0) { + DefaultMessageStore.this.doDispatch(dispatchRequest); + + if (!notifyMessageArriveInBatch) { + notifyMessageArriveIfNecessary(dispatchRequest); + } + + this.reputFromOffset += size; + readSize += size; + if (!DefaultMessageStore.this.getMessageStoreConfig().isDuplicationEnable() && + DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole() == BrokerRole.SLAVE) { + DefaultMessageStore.this.storeStatsService + .getSinglePutMessageTopicTimesTotal(dispatchRequest.getTopic()).add(dispatchRequest.getBatchSize()); + DefaultMessageStore.this.storeStatsService + .getSinglePutMessageTopicSizeTotal(dispatchRequest.getTopic()) + .add(dispatchRequest.getMsgSize()); + } + } else if (size == 0) { + this.reputFromOffset = DefaultMessageStore.this.commitLog.rollNextFile(this.reputFromOffset); + readSize = result.getSize(); + } + } else { + if (size > 0) { + LOGGER.error("[BUG]read total count not equals msg total size. reputFromOffset={}", reputFromOffset); + this.reputFromOffset += size; + } else { + doNext = false; + // If user open the dledger pattern or the broker is master node, + // it will not ignore the exception and fix the reputFromOffset variable + if (DefaultMessageStore.this.getMessageStoreConfig().isEnableDLegerCommitLog() || + DefaultMessageStore.this.brokerConfig.getBrokerId() == MixAll.MASTER_ID) { + LOGGER.error("[BUG]dispatch message to consume queue error, COMMITLOG OFFSET: {}", + this.reputFromOffset); + this.reputFromOffset += result.getSize() - readSize; } } } - } finally { - result.release(); } - } else { - doNext = false; + } catch (RocksDBException e) { + ERROR_LOG.info("dispatch message to cq exception. reputFromOffset: {}", this.reputFromOffset, e); + return; + } finally { + result.release(); + } + + finishCommitLogDispatch(); + } + } + + private void notifyMessageArrive4MultiQueue(DispatchRequest dispatchRequest) { + Map prop = dispatchRequest.getPropertiesMap(); + if (prop == null || dispatchRequest.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { + return; + } + String multiDispatchQueue = prop.get(MessageConst.PROPERTY_INNER_MULTI_DISPATCH); + String multiQueueOffset = prop.get(MessageConst.PROPERTY_INNER_MULTI_QUEUE_OFFSET); + if (StringUtils.isBlank(multiDispatchQueue) || StringUtils.isBlank(multiQueueOffset)) { + return; + } + String[] queues = multiDispatchQueue.split(MixAll.MULTI_DISPATCH_QUEUE_SPLITTER); + String[] queueOffsets = multiQueueOffset.split(MixAll.MULTI_DISPATCH_QUEUE_SPLITTER); + if (queues.length != queueOffsets.length) { + return; + } + for (int i = 0; i < queues.length; i++) { + String queueName = queues[i]; + long queueOffset = Long.parseLong(queueOffsets[i]); + int queueId = dispatchRequest.getQueueId(); + if (DefaultMessageStore.this.getMessageStoreConfig().isEnableLmq() && MixAll.isLmq(queueName)) { + queueId = 0; } + DefaultMessageStore.this.messageArrivingListener.arriving( + queueName, queueId, queueOffset + 1, dispatchRequest.getTagsCode(), + dispatchRequest.getStoreTimestamp(), dispatchRequest.getBitMap(), dispatchRequest.getPropertiesMap()); } } @Override public void run() { - DefaultMessageStore.log.info(this.getServiceName() + " service started"); + DefaultMessageStore.LOGGER.info(this.getServiceName() + " service started"); while (!this.isStopped()) { try { - Thread.sleep(1); + TimeUnit.MILLISECONDS.sleep(1); this.doReput(); } catch (Exception e) { - DefaultMessageStore.log.warn(this.getServiceName() + " service has exception. ", e); + DefaultMessageStore.LOGGER.warn(this.getServiceName() + " service has exception. ", e); } } - DefaultMessageStore.log.info(this.getServiceName() + " service end"); + DefaultMessageStore.LOGGER.info(this.getServiceName() + " service end"); } @Override public String getServiceName() { + if (DefaultMessageStore.this.getBrokerConfig().isInBrokerContainer()) { + return DefaultMessageStore.this.getBrokerIdentity().getIdentifier() + ReputMessageService.class.getSimpleName(); + } return ReputMessageService.class.getSimpleName(); } } + + class MainBatchDispatchRequestService extends ServiceThread { + + private final ExecutorService batchDispatchRequestExecutor; + + public MainBatchDispatchRequestService() { + batchDispatchRequestExecutor = ThreadUtils.newThreadPoolExecutor( + DefaultMessageStore.this.getMessageStoreConfig().getBatchDispatchRequestThreadPoolNums(), + DefaultMessageStore.this.getMessageStoreConfig().getBatchDispatchRequestThreadPoolNums(), + 1000 * 60, + TimeUnit.MICROSECONDS, + new LinkedBlockingQueue<>(4096), + new ThreadFactoryImpl("BatchDispatchRequestServiceThread_"), + new ThreadPoolExecutor.AbortPolicy()); + } + + private void pollBatchDispatchRequest() { + try { + if (!batchDispatchRequestQueue.isEmpty()) { + BatchDispatchRequest task = batchDispatchRequestQueue.peek(); + batchDispatchRequestExecutor.execute(() -> { + try { + ByteBuffer tmpByteBuffer = task.byteBuffer; + tmpByteBuffer.position(task.position); + tmpByteBuffer.limit(task.position + task.size); + List dispatchRequestList = new ArrayList<>(); + while (tmpByteBuffer.hasRemaining()) { + DispatchRequest dispatchRequest = DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(tmpByteBuffer, false, false, false); + if (dispatchRequest.isSuccess()) { + dispatchRequestList.add(dispatchRequest); + } else { + LOGGER.error("[BUG]read total count not equals msg total size."); + } + } + dispatchRequestOrderlyQueue.put(task.id, dispatchRequestList.toArray(new DispatchRequest[dispatchRequestList.size()])); + mappedPageHoldCount.getAndDecrement(); + } catch (Exception e) { + LOGGER.error("There is an exception in task execution.", e); + } + }); + batchDispatchRequestQueue.poll(); + } + } catch (Exception e) { + DefaultMessageStore.LOGGER.warn(this.getServiceName() + " service has exception. ", e); + } + } + + @Override + public void run() { + DefaultMessageStore.LOGGER.info(this.getServiceName() + " service started"); + + while (!this.isStopped()) { + try { + TimeUnit.MILLISECONDS.sleep(1); + pollBatchDispatchRequest(); + } catch (Exception e) { + DefaultMessageStore.LOGGER.warn(this.getServiceName() + " service has exception. ", e); + } + } + + DefaultMessageStore.LOGGER.info(this.getServiceName() + " service end"); + } + + @Override + public String getServiceName() { + if (DefaultMessageStore.this.getBrokerConfig().isInBrokerContainer()) { + return DefaultMessageStore.this.getBrokerIdentity().getIdentifier() + MainBatchDispatchRequestService.class.getSimpleName(); + } + return MainBatchDispatchRequestService.class.getSimpleName(); + } + + } + + class DispatchService extends ServiceThread { + + private final List dispatchRequestsList = new ArrayList<>(); + + // dispatchRequestsList:[ + // {dispatchRequests:[{dispatchRequest}, {dispatchRequest}]}, + // {dispatchRequests:[{dispatchRequest}, {dispatchRequest}]}] + private void dispatch() throws Exception { + dispatchRequestsList.clear(); + dispatchRequestOrderlyQueue.get(dispatchRequestsList); + if (!dispatchRequestsList.isEmpty()) { + for (DispatchRequest[] dispatchRequests : dispatchRequestsList) { + for (DispatchRequest dispatchRequest : dispatchRequests) { + DefaultMessageStore.this.doDispatch(dispatchRequest); + // wake up long-polling + DefaultMessageStore.this.notifyMessageArriveIfNecessary(dispatchRequest); + + if (!DefaultMessageStore.this.getMessageStoreConfig().isDuplicationEnable() && + DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole() == BrokerRole.SLAVE) { + DefaultMessageStore.this.storeStatsService + .getSinglePutMessageTopicTimesTotal(dispatchRequest.getTopic()).add(1); + DefaultMessageStore.this.storeStatsService + .getSinglePutMessageTopicSizeTotal(dispatchRequest.getTopic()) + .add(dispatchRequest.getMsgSize()); + } + } + } + } + } + + @Override + public void run() { + DefaultMessageStore.LOGGER.info(this.getServiceName() + " service started"); + + while (!this.isStopped()) { + try { + TimeUnit.MILLISECONDS.sleep(1); + dispatch(); + } catch (Exception e) { + DefaultMessageStore.LOGGER.warn(this.getServiceName() + " service has exception. ", e); + } + } + + DefaultMessageStore.LOGGER.info(this.getServiceName() + " service end"); + } + + @Override + public String getServiceName() { + if (DefaultMessageStore.this.getBrokerConfig().isInBrokerContainer()) { + return DefaultMessageStore.this.getBrokerIdentity().getIdentifier() + DispatchService.class.getSimpleName(); + } + return DispatchService.class.getSimpleName(); + } + } + + class ConcurrentReputMessageService extends ReputMessageService { + + private static final int BATCH_SIZE = 1024 * 1024 * 4; + + private long batchId = 0; + + private MainBatchDispatchRequestService mainBatchDispatchRequestService; + + private DispatchService dispatchService; + + public ConcurrentReputMessageService() { + super(); + this.mainBatchDispatchRequestService = new MainBatchDispatchRequestService(); + this.dispatchService = new DispatchService(); + } + + public void createBatchDispatchRequest(ByteBuffer byteBuffer, int position, int size) { + if (position < 0) { + return; + } + mappedPageHoldCount.getAndIncrement(); + BatchDispatchRequest task = new BatchDispatchRequest(byteBuffer.duplicate(), position, size, batchId++); + batchDispatchRequestQueue.offer(task); + } + + @Override + public void start() { + super.start(); + this.mainBatchDispatchRequestService.start(); + this.dispatchService.start(); + } + + @Override + public void doReput() { + if (this.reputFromOffset < DefaultMessageStore.this.commitLog.getMinOffset()) { + LOGGER.warn("The reputFromOffset={} is smaller than minPyOffset={}, this usually indicate that the dispatch behind too much and the commitlog has expired.", + this.reputFromOffset, DefaultMessageStore.this.commitLog.getMinOffset()); + this.reputFromOffset = DefaultMessageStore.this.commitLog.getMinOffset(); + } + for (boolean doNext = true; this.isCommitLogAvailable() && doNext; ) { + + SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset); + + if (result == null) { + break; + } + + int batchDispatchRequestStart = -1; + int batchDispatchRequestSize = -1; + try { + this.reputFromOffset = result.getStartOffset(); + + for (int readSize = 0; readSize < result.getSize() && reputFromOffset < DefaultMessageStore.this.getConfirmOffset() && doNext; ) { + ByteBuffer byteBuffer = result.getByteBuffer(); + + int totalSize = preCheckMessageAndReturnSize(byteBuffer); + + if (totalSize > 0) { + if (batchDispatchRequestStart == -1) { + batchDispatchRequestStart = byteBuffer.position(); + batchDispatchRequestSize = 0; + } + batchDispatchRequestSize += totalSize; + if (batchDispatchRequestSize > BATCH_SIZE) { + this.createBatchDispatchRequest(byteBuffer, batchDispatchRequestStart, batchDispatchRequestSize); + batchDispatchRequestStart = -1; + batchDispatchRequestSize = -1; + } + byteBuffer.position(byteBuffer.position() + totalSize); + this.reputFromOffset += totalSize; + readSize += totalSize; + } else { + doNext = false; + if (totalSize == 0) { + this.reputFromOffset = DefaultMessageStore.this.commitLog.rollNextFile(this.reputFromOffset); + } + this.createBatchDispatchRequest(byteBuffer, batchDispatchRequestStart, batchDispatchRequestSize); + batchDispatchRequestStart = -1; + batchDispatchRequestSize = -1; + } + } + } finally { + this.createBatchDispatchRequest(result.getByteBuffer(), batchDispatchRequestStart, batchDispatchRequestSize); + boolean over = mappedPageHoldCount.get() == 0; + while (!over) { + try { + TimeUnit.MILLISECONDS.sleep(1); + } catch (Exception e) { + e.printStackTrace(); + } + over = mappedPageHoldCount.get() == 0; + } + result.release(); + } + } + + // only for rocksdb mode + finishCommitLogDispatch(); + } + + /** + * pre-check the message and returns the message size + * + * @return 0 Come to the end of file // >0 Normal messages // -1 Message checksum failure + */ + public int preCheckMessageAndReturnSize(ByteBuffer byteBuffer) { + byteBuffer.mark(); + + int totalSize = byteBuffer.getInt(); + if (reputFromOffset + totalSize > DefaultMessageStore.this.getConfirmOffset()) { + return -1; + } + + int magicCode = byteBuffer.getInt(); + switch (magicCode) { + case MessageDecoder.MESSAGE_MAGIC_CODE: + case MessageDecoder.MESSAGE_MAGIC_CODE_V2: + break; + case MessageDecoder.BLANK_MAGIC_CODE: + return 0; + default: + return -1; + } + + byteBuffer.reset(); + + return totalSize; + } + + @Override + public void shutdown() { + for (int i = 0; i < 50 && this.isCommitLogAvailable(); i++) { + try { + TimeUnit.MILLISECONDS.sleep(100); + } catch (InterruptedException ignored) { + } + } + + if (this.isCommitLogAvailable()) { + LOGGER.warn("shutdown concurrentReputMessageService, but CommitLog have not finish to be dispatched, CommitLog max" + + " offset={}, reputFromOffset={}", DefaultMessageStore.this.commitLog.getMaxOffset(), + this.reputFromOffset); + } + + this.mainBatchDispatchRequestService.shutdown(); + this.dispatchService.shutdown(); + super.shutdown(); + } + + @Override + public String getServiceName() { + if (DefaultMessageStore.this.getBrokerConfig().isInBrokerContainer()) { + return DefaultMessageStore.this.getBrokerIdentity().getIdentifier() + ConcurrentReputMessageService.class.getSimpleName(); + } + return ConcurrentReputMessageService.class.getSimpleName(); + } + } + + @Override + public HARuntimeInfo getHARuntimeInfo() { + if (haService != null) { + return this.haService.getRuntimeInfo(this.commitLog.getMaxOffset()); + } else { + return null; + } + } + + public int getMaxDelayLevel() { + return maxDelayLevel; + } + + public long computeDeliverTimestamp(final int delayLevel, final long storeTimestamp) { + Long time = this.delayLevelTable.get(delayLevel); + if (time != null) { + return time + storeTimestamp; + } + + return storeTimestamp + 1000; + } + + public List getPutMessageHookList() { + return putMessageHookList; + } + + @Override + public void setSendMessageBackHook(SendMessageBackHook sendMessageBackHook) { + this.sendMessageBackHook = sendMessageBackHook; + } + + @Override + public SendMessageBackHook getSendMessageBackHook() { + return sendMessageBackHook; + } + + @Override + public boolean isShutdown() { + return shutdown; + } + + @Override + public long estimateMessageCount(String topic, int queueId, long from, long to, MessageFilter filter) { + if (from < 0) { + from = 0; + } + + if (from >= to) { + return 0; + } + + if (null == filter) { + return to - from; + } + + ConsumeQueueInterface consumeQueue = findConsumeQueue(topic, queueId); + if (null == consumeQueue) { + return 0; + } + + // correct the "from" argument to min offset in queue if it is too small + long minOffset = consumeQueue.getMinOffsetInQueue(); + if (from < minOffset) { + long diff = to - from; + from = minOffset; + to = from + diff; + } + + long msgCount = consumeQueue.estimateMessageCount(from, to, filter); + return msgCount == -1 ? to - from : msgCount; + } + + @Override + public List> getMetricsView() { + return DefaultStoreMetricsManager.getMetricsView(); + } + + @Override + public void initMetrics(Meter meter, Supplier attributesBuilderSupplier) { + DefaultStoreMetricsManager.init(meter, attributesBuilderSupplier, this); + } + + /** + * Enable transient commitLog store pool only if transientStorePoolEnable is true and broker role is not SLAVE or + * enableControllerMode is true + * + * @return true or false + */ + public boolean isTransientStorePoolEnable() { + return this.messageStoreConfig.isTransientStorePoolEnable() && + (this.brokerConfig.isEnableControllerMode() || this.messageStoreConfig.getBrokerRole() != BrokerRole.SLAVE); + } + + public long getReputFromOffset() { + return this.reputMessageService.getReputFromOffset(); + } } diff --git a/store/src/main/java/org/apache/rocketmq/store/DispatchRequest.java b/store/src/main/java/org/apache/rocketmq/store/DispatchRequest.java index 819bb948c75..79d006bafc3 100644 --- a/store/src/main/java/org/apache/rocketmq/store/DispatchRequest.java +++ b/store/src/main/java/org/apache/rocketmq/store/DispatchRequest.java @@ -22,7 +22,7 @@ public class DispatchRequest { private final String topic; private final int queueId; private final long commitLogOffset; - private final int msgSize; + private int msgSize; private final long tagsCode; private final long storeTimestamp; private final long consumeQueueOffset; @@ -35,6 +35,16 @@ public class DispatchRequest { private final Map propertiesMap; private byte[] bitMap; + private int bufferSize = -1;//the buffer size maybe larger than the msg size if the message is wrapped by something + + // for batch consume queue + private long msgBaseOffset = -1; + private short batchSize = 1; + + private long nextReputFromOffset = -1; + + private String offsetId; + public DispatchRequest( final String topic, final int queueId, @@ -56,6 +66,7 @@ public DispatchRequest( this.tagsCode = tagsCode; this.storeTimestamp = storeTimestamp; this.consumeQueueOffset = consumeQueueOffset; + this.msgBaseOffset = consumeQueueOffset; this.keys = keys; this.uniqKey = uniqKey; @@ -65,6 +76,22 @@ public DispatchRequest( this.propertiesMap = propertiesMap; } + public DispatchRequest(String topic, int queueId, long consumeQueueOffset, long commitLogOffset, int size, long tagsCode) { + this.topic = topic; + this.queueId = queueId; + this.commitLogOffset = commitLogOffset; + this.msgSize = size; + this.tagsCode = tagsCode; + this.storeTimestamp = 0; + this.consumeQueueOffset = consumeQueueOffset; + this.keys = ""; + this.uniqKey = null; + this.sysFlag = 0; + this.preparedTransactionOffset = 0; + this.success = false; + this.propertiesMap = null; + } + public DispatchRequest(int size) { this.topic = ""; this.queueId = 0; @@ -156,4 +183,62 @@ public byte[] getBitMap() { public void setBitMap(byte[] bitMap) { this.bitMap = bitMap; } + + public short getBatchSize() { + return batchSize; + } + + public void setBatchSize(short batchSize) { + this.batchSize = batchSize; + } + + public void setMsgSize(int msgSize) { + this.msgSize = msgSize; + } + + public long getMsgBaseOffset() { + return msgBaseOffset; + } + + public void setMsgBaseOffset(long msgBaseOffset) { + this.msgBaseOffset = msgBaseOffset; + } + + public int getBufferSize() { + return bufferSize; + } + + public void setBufferSize(int bufferSize) { + this.bufferSize = bufferSize; + } + + public long getNextReputFromOffset() { + return nextReputFromOffset; + } + + public void setNextReputFromOffset(long nextReputFromOffset) { + this.nextReputFromOffset = nextReputFromOffset; + } + + public String getOffsetId() { + return offsetId; + } + + public void setOffsetId(String offsetId) { + this.offsetId = offsetId; + } + + @Override + public String toString() { + return "DispatchRequest{" + + "topic='" + topic + '\'' + + ", queueId=" + queueId + + ", commitLogOffset=" + commitLogOffset + + ", msgSize=" + msgSize + + ", success=" + success + + ", msgBaseOffset=" + msgBaseOffset + + ", batchSize=" + batchSize + + ", nextReputFromOffset=" + nextReputFromOffset + + '}'; + } } diff --git a/store/src/main/java/org/apache/rocketmq/store/FileQueueSnapshot.java b/store/src/main/java/org/apache/rocketmq/store/FileQueueSnapshot.java new file mode 100644 index 00000000000..6521a76b70d --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/FileQueueSnapshot.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store; + +import org.apache.rocketmq.store.logfile.MappedFile; + +public class FileQueueSnapshot { + private MappedFile firstFile; + private long firstFileIndex; + private MappedFile lastFile; + private long lastFileIndex; + private long currentFile; + private long currentFileIndex; + private long behindCount; + private boolean exist; + + public FileQueueSnapshot() { + } + + public FileQueueSnapshot(MappedFile firstFile, long firstFileIndex, MappedFile lastFile, long lastFileIndex, long currentFile, long currentFileIndex, long behindCount, boolean exist) { + this.firstFile = firstFile; + this.firstFileIndex = firstFileIndex; + this.lastFile = lastFile; + this.lastFileIndex = lastFileIndex; + this.currentFile = currentFile; + this.currentFileIndex = currentFileIndex; + this.behindCount = behindCount; + this.exist = exist; + } + + public MappedFile getFirstFile() { + return firstFile; + } + + public long getFirstFileIndex() { + return firstFileIndex; + } + + public MappedFile getLastFile() { + return lastFile; + } + + public long getLastFileIndex() { + return lastFileIndex; + } + + public long getCurrentFile() { + return currentFile; + } + + public long getCurrentFileIndex() { + return currentFileIndex; + } + + public long getBehindCount() { + return behindCount; + } + + public boolean isExist() { + return exist; + } + + @Override + public String toString() { + return "FileQueueSnapshot{" + + "firstFile=" + firstFile + + ", firstFileIndex=" + firstFileIndex + + ", lastFile=" + lastFile + + ", lastFileIndex=" + lastFileIndex + + ", currentFile=" + currentFile + + ", currentFileIndex=" + currentFileIndex + + ", behindCount=" + behindCount + + ", exist=" + exist + + '}'; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/FlushDiskWatcher.java b/store/src/main/java/org/apache/rocketmq/store/FlushDiskWatcher.java new file mode 100644 index 00000000000..a75efd6258e --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/FlushDiskWatcher.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store; + + +import java.util.concurrent.LinkedBlockingQueue; + +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.CommitLog.GroupCommitRequest; + +public class FlushDiskWatcher extends ServiceThread { + private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + private final LinkedBlockingQueue commitRequests = new LinkedBlockingQueue<>(); + + @Override + public String getServiceName() { + return FlushDiskWatcher.class.getSimpleName(); + } + + @Override + public void run() { + while (!isStopped()) { + GroupCommitRequest request = null; + try { + request = commitRequests.take(); + } catch (InterruptedException e) { + log.warn("take flush disk commit request, but interrupted, this may caused by shutdown"); + continue; + } + while (!request.future().isDone()) { + long now = System.nanoTime(); + if (now - request.getDeadLine() >= 0) { + request.wakeupCustomer(PutMessageStatus.FLUSH_DISK_TIMEOUT); + break; + } + // To avoid frequent thread switching, replace future.get with sleep here, + long sleepTime = (request.getDeadLine() - now) / 1_000_000; + sleepTime = Math.min(10, sleepTime); + if (sleepTime == 0) { + request.wakeupCustomer(PutMessageStatus.FLUSH_DISK_TIMEOUT); + break; + } + try { + Thread.sleep(sleepTime); + } catch (InterruptedException e) { + log.warn( + "An exception occurred while waiting for flushing disk to complete. this may caused by shutdown"); + break; + } + } + } + } + + public void add(GroupCommitRequest request) { + commitRequests.add(request); + } + + public int queueSize() { + return commitRequests.size(); + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/FlushManager.java b/store/src/main/java/org/apache/rocketmq/store/FlushManager.java new file mode 100644 index 00000000000..fe3951ae7f7 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/FlushManager.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store; + +import java.util.concurrent.CompletableFuture; +import org.apache.rocketmq.common.message.MessageExt; + +public interface FlushManager { + + void start(); + + void shutdown(); + + void wakeUpFlush(); + + void wakeUpCommit(); + + void handleDiskFlush(AppendMessageResult result, PutMessageResult putMessageResult, MessageExt messageExt); + + CompletableFuture handleDiskFlush(AppendMessageResult result, MessageExt messageExt); +} diff --git a/store/src/main/java/org/apache/rocketmq/store/GetMessageResult.java b/store/src/main/java/org/apache/rocketmq/store/GetMessageResult.java index 996e24d8058..a7556dfb855 100644 --- a/store/src/main/java/org/apache/rocketmq/store/GetMessageResult.java +++ b/store/src/main/java/org/apache/rocketmq/store/GetMessageResult.java @@ -18,15 +18,14 @@ import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import org.apache.rocketmq.store.stats.BrokerStatsManager; public class GetMessageResult { - private final List messageMapedList = - new ArrayList(100); - - private final List messageBufferList = new ArrayList(100); + private final List messageMapedList; + private final List messageBufferList; + private final List messageQueueOffset; private GetMessageStatus status; private long nextBeginOffset; @@ -35,11 +34,40 @@ public class GetMessageResult { private int bufferTotalSize = 0; + private int messageCount = 0; + private boolean suggestPullingFromSlave = false; private int msgCount4Commercial = 0; + private int commercialSizePerMsg = 4 * 1024; + + private long coldDataSum = 0L; + + public static final GetMessageResult NO_MATCH_LOGIC_QUEUE = + new GetMessageResult(GetMessageStatus.NO_MATCHED_LOGIC_QUEUE, 0, 0, 0, Collections.emptyList(), + Collections.emptyList(), Collections.emptyList()); public GetMessageResult() { + messageMapedList = new ArrayList<>(100); + messageBufferList = new ArrayList<>(100); + messageQueueOffset = new ArrayList<>(100); + } + + public GetMessageResult(int resultSize) { + messageMapedList = new ArrayList<>(resultSize); + messageBufferList = new ArrayList<>(resultSize); + messageQueueOffset = new ArrayList<>(resultSize); + } + + private GetMessageResult(GetMessageStatus status, long nextBeginOffset, long minOffset, long maxOffset, + List messageMapedList, List messageBufferList, List messageQueueOffset) { + this.status = status; + this.nextBeginOffset = nextBeginOffset; + this.minOffset = minOffset; + this.maxOffset = maxOffset; + this.messageMapedList = messageMapedList; + this.messageBufferList = messageBufferList; + this.messageQueueOffset = messageQueueOffset; } public GetMessageStatus getStatus() { @@ -87,7 +115,24 @@ public void addMessage(final SelectMappedBufferResult mapedBuffer) { this.messageBufferList.add(mapedBuffer.getByteBuffer()); this.bufferTotalSize += mapedBuffer.getSize(); this.msgCount4Commercial += (int) Math.ceil( - mapedBuffer.getSize() / BrokerStatsManager.SIZE_PER_COUNT); + mapedBuffer.getSize() / (double)commercialSizePerMsg); + this.messageCount++; + } + + public void addMessage(final SelectMappedBufferResult mapedBuffer, final long queueOffset) { + this.messageMapedList.add(mapedBuffer); + this.messageBufferList.add(mapedBuffer.getByteBuffer()); + this.bufferTotalSize += mapedBuffer.getSize(); + this.msgCount4Commercial += (int) Math.ceil( + mapedBuffer.getSize() / (double)commercialSizePerMsg); + this.messageCount++; + this.messageQueueOffset.add(queueOffset); + } + + + public void addMessage(final SelectMappedBufferResult mapedBuffer, final long queueOffset, final int batchNum) { + addMessage(mapedBuffer, queueOffset); + messageCount += batchNum - 1; } public void release() { @@ -100,12 +145,8 @@ public int getBufferTotalSize() { return bufferTotalSize; } - public void setBufferTotalSize(int bufferTotalSize) { - this.bufferTotalSize = bufferTotalSize; - } - public int getMessageCount() { - return this.messageMapedList.size(); + return messageCount; } public boolean isSuggestPullingFromSlave() { @@ -124,11 +165,22 @@ public void setMsgCount4Commercial(int msgCount4Commercial) { this.msgCount4Commercial = msgCount4Commercial; } + public List getMessageQueueOffset() { + return messageQueueOffset; + } + + public long getColdDataSum() { + return coldDataSum; + } + + public void setColdDataSum(long coldDataSum) { + this.coldDataSum = coldDataSum; + } + @Override public String toString() { return "GetMessageResult [status=" + status + ", nextBeginOffset=" + nextBeginOffset + ", minOffset=" - + minOffset + ", maxOffset=" + maxOffset + ", bufferTotalSize=" + bufferTotalSize + + minOffset + ", maxOffset=" + maxOffset + ", bufferTotalSize=" + bufferTotalSize + ", messageCount=" + messageCount + ", suggestPullingFromSlave=" + suggestPullingFromSlave + "]"; } - } diff --git a/store/src/main/java/org/apache/rocketmq/store/GetMessageStatus.java b/store/src/main/java/org/apache/rocketmq/store/GetMessageStatus.java index 6a824b8981b..bc244865ffe 100644 --- a/store/src/main/java/org/apache/rocketmq/store/GetMessageStatus.java +++ b/store/src/main/java/org/apache/rocketmq/store/GetMessageStatus.java @@ -35,4 +35,6 @@ public enum GetMessageStatus { NO_MATCHED_LOGIC_QUEUE, NO_MESSAGE_IN_QUEUE, + + OFFSET_RESET } diff --git a/store/src/main/java/org/apache/rocketmq/store/MappedFile.java b/store/src/main/java/org/apache/rocketmq/store/MappedFile.java deleted file mode 100644 index 0a43d47f8ff..00000000000 --- a/store/src/main/java/org/apache/rocketmq/store/MappedFile.java +++ /dev/null @@ -1,582 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.store; - -import com.sun.jna.NativeLong; -import com.sun.jna.Pointer; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.lang.reflect.Method; -import java.nio.ByteBuffer; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.channels.FileChannel.MapMode; -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import org.apache.rocketmq.common.UtilAll; -import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.common.message.MessageExt; -import org.apache.rocketmq.common.message.MessageExtBatch; -import org.apache.rocketmq.store.config.FlushDiskType; -import org.apache.rocketmq.store.util.LibC; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sun.nio.ch.DirectBuffer; - -public class MappedFile extends ReferenceResource { - public static final int OS_PAGE_SIZE = 1024 * 4; - protected static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); - - private static final AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY = new AtomicLong(0); - - private static final AtomicInteger TOTAL_MAPPED_FILES = new AtomicInteger(0); - protected final AtomicInteger wrotePosition = new AtomicInteger(0); - //ADD BY ChenYang - protected final AtomicInteger committedPosition = new AtomicInteger(0); - private final AtomicInteger flushedPosition = new AtomicInteger(0); - protected int fileSize; - protected FileChannel fileChannel; - /** - * Message will put to here first, and then reput to FileChannel if writeBuffer is not null. - */ - protected ByteBuffer writeBuffer = null; - protected TransientStorePool transientStorePool = null; - private String fileName; - private long fileFromOffset; - private File file; - private MappedByteBuffer mappedByteBuffer; - private volatile long storeTimestamp = 0; - private boolean firstCreateInQueue = false; - - public MappedFile() { - } - - public MappedFile(final String fileName, final int fileSize) throws IOException { - init(fileName, fileSize); - } - - public MappedFile(final String fileName, final int fileSize, - final TransientStorePool transientStorePool) throws IOException { - init(fileName, fileSize, transientStorePool); - } - - public static void ensureDirOK(final String dirName) { - if (dirName != null) { - File f = new File(dirName); - if (!f.exists()) { - boolean result = f.mkdirs(); - log.info(dirName + " mkdir " + (result ? "OK" : "Failed")); - } - } - } - - public static void clean(final ByteBuffer buffer) { - if (buffer == null || !buffer.isDirect() || buffer.capacity() == 0) - return; - invoke(invoke(viewed(buffer), "cleaner"), "clean"); - } - - private static Object invoke(final Object target, final String methodName, final Class... args) { - return AccessController.doPrivileged(new PrivilegedAction() { - public Object run() { - try { - Method method = method(target, methodName, args); - method.setAccessible(true); - return method.invoke(target); - } catch (Exception e) { - throw new IllegalStateException(e); - } - } - }); - } - - private static Method method(Object target, String methodName, Class[] args) - throws NoSuchMethodException { - try { - return target.getClass().getMethod(methodName, args); - } catch (NoSuchMethodException e) { - return target.getClass().getDeclaredMethod(methodName, args); - } - } - - private static ByteBuffer viewed(ByteBuffer buffer) { - String methodName = "viewedBuffer"; - - Method[] methods = buffer.getClass().getMethods(); - for (int i = 0; i < methods.length; i++) { - if (methods[i].getName().equals("attachment")) { - methodName = "attachment"; - break; - } - } - - ByteBuffer viewedBuffer = (ByteBuffer) invoke(buffer, methodName); - if (viewedBuffer == null) - return buffer; - else - return viewed(viewedBuffer); - } - - public static int getTotalMappedFiles() { - return TOTAL_MAPPED_FILES.get(); - } - - public static long getTotalMappedVirtualMemory() { - return TOTAL_MAPPED_VIRTUAL_MEMORY.get(); - } - - public void init(final String fileName, final int fileSize, - final TransientStorePool transientStorePool) throws IOException { - init(fileName, fileSize); - this.writeBuffer = transientStorePool.borrowBuffer(); - this.transientStorePool = transientStorePool; - } - - private void init(final String fileName, final int fileSize) throws IOException { - this.fileName = fileName; - this.fileSize = fileSize; - this.file = new File(fileName); - this.fileFromOffset = Long.parseLong(this.file.getName()); - boolean ok = false; - - ensureDirOK(this.file.getParent()); - - try { - this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel(); - this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize); - TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(fileSize); - TOTAL_MAPPED_FILES.incrementAndGet(); - ok = true; - } catch (FileNotFoundException e) { - log.error("create file channel " + this.fileName + " Failed. ", e); - throw e; - } catch (IOException e) { - log.error("map file " + this.fileName + " Failed. ", e); - throw e; - } finally { - if (!ok && this.fileChannel != null) { - this.fileChannel.close(); - } - } - } - - public long getLastModifiedTimestamp() { - return this.file.lastModified(); - } - - public int getFileSize() { - return fileSize; - } - - public FileChannel getFileChannel() { - return fileChannel; - } - - public AppendMessageResult appendMessage(final MessageExtBrokerInner msg, final AppendMessageCallback cb) { - return appendMessagesInner(msg, cb); - } - - public AppendMessageResult appendMessages(final MessageExtBatch messageExtBatch, final AppendMessageCallback cb) { - return appendMessagesInner(messageExtBatch, cb); - } - - public AppendMessageResult appendMessagesInner(final MessageExt messageExt, final AppendMessageCallback cb) { - assert messageExt != null; - assert cb != null; - - int currentPos = this.wrotePosition.get(); - - if (currentPos < this.fileSize) { - ByteBuffer byteBuffer = writeBuffer != null ? writeBuffer.slice() : this.mappedByteBuffer.slice(); - byteBuffer.position(currentPos); - AppendMessageResult result = null; - if (messageExt instanceof MessageExtBrokerInner) { - result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBrokerInner) messageExt); - } else if (messageExt instanceof MessageExtBatch) { - result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBatch) messageExt); - } else { - return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR); - } - this.wrotePosition.addAndGet(result.getWroteBytes()); - this.storeTimestamp = result.getStoreTimestamp(); - return result; - } - log.error("MappedFile.appendMessage return null, wrotePosition: {} fileSize: {}", currentPos, this.fileSize); - return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR); - } - - public long getFileFromOffset() { - return this.fileFromOffset; - } - - public boolean appendMessage(final byte[] data) { - int currentPos = this.wrotePosition.get(); - - if ((currentPos + data.length) <= this.fileSize) { - try { - this.fileChannel.position(currentPos); - this.fileChannel.write(ByteBuffer.wrap(data)); - } catch (Throwable e) { - log.error("Error occurred when append message to mappedFile.", e); - } - this.wrotePosition.addAndGet(data.length); - return true; - } - - return false; - } - - /** - * Content of data from offset to offset + length will be wrote to file. - * - * @param offset The offset of the subarray to be used. - * @param length The length of the subarray to be used. - */ - public boolean appendMessage(final byte[] data, final int offset, final int length) { - int currentPos = this.wrotePosition.get(); - - if ((currentPos + length) <= this.fileSize) { - try { - this.fileChannel.position(currentPos); - this.fileChannel.write(ByteBuffer.wrap(data, offset, length)); - } catch (Throwable e) { - log.error("Error occurred when append message to mappedFile.", e); - } - this.wrotePosition.addAndGet(length); - return true; - } - - return false; - } - - /** - * @return The current flushed position - */ - public int flush(final int flushLeastPages) { - if (this.isAbleToFlush(flushLeastPages)) { - if (this.hold()) { - int value = getReadPosition(); - - try { - //We only append data to fileChannel or mappedByteBuffer, never both. - if (writeBuffer != null || this.fileChannel.position() != 0) { - this.fileChannel.force(false); - } else { - this.mappedByteBuffer.force(); - } - } catch (Throwable e) { - log.error("Error occurred when force data to disk.", e); - } - - this.flushedPosition.set(value); - this.release(); - } else { - log.warn("in flush, hold failed, flush offset = " + this.flushedPosition.get()); - this.flushedPosition.set(getReadPosition()); - } - } - return this.getFlushedPosition(); - } - - public int commit(final int commitLeastPages) { - if (writeBuffer == null) { - //no need to commit data to file channel, so just regard wrotePosition as committedPosition. - return this.wrotePosition.get(); - } - if (this.isAbleToCommit(commitLeastPages)) { - if (this.hold()) { - commit0(commitLeastPages); - this.release(); - } else { - log.warn("in commit, hold failed, commit offset = " + this.committedPosition.get()); - } - } - - // All dirty data has been committed to FileChannel. - if (writeBuffer != null && this.transientStorePool != null && this.fileSize == this.committedPosition.get()) { - this.transientStorePool.returnBuffer(writeBuffer); - this.writeBuffer = null; - } - - return this.committedPosition.get(); - } - - protected void commit0(final int commitLeastPages) { - int writePos = this.wrotePosition.get(); - int lastCommittedPosition = this.committedPosition.get(); - - if (writePos - this.committedPosition.get() > 0) { - try { - ByteBuffer byteBuffer = writeBuffer.slice(); - byteBuffer.position(lastCommittedPosition); - byteBuffer.limit(writePos); - this.fileChannel.position(lastCommittedPosition); - this.fileChannel.write(byteBuffer); - this.committedPosition.set(writePos); - } catch (Throwable e) { - log.error("Error occurred when commit data to FileChannel.", e); - } - } - } - - private boolean isAbleToFlush(final int flushLeastPages) { - int flush = this.flushedPosition.get(); - int write = getReadPosition(); - - if (this.isFull()) { - return true; - } - - if (flushLeastPages > 0) { - return ((write / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE)) >= flushLeastPages; - } - - return write > flush; - } - - protected boolean isAbleToCommit(final int commitLeastPages) { - int flush = this.committedPosition.get(); - int write = this.wrotePosition.get(); - - if (this.isFull()) { - return true; - } - - if (commitLeastPages > 0) { - return ((write / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE)) >= commitLeastPages; - } - - return write > flush; - } - - public int getFlushedPosition() { - return flushedPosition.get(); - } - - public void setFlushedPosition(int pos) { - this.flushedPosition.set(pos); - } - - public boolean isFull() { - return this.fileSize == this.wrotePosition.get(); - } - - public SelectMappedBufferResult selectMappedBuffer(int pos, int size) { - int readPosition = getReadPosition(); - if ((pos + size) <= readPosition) { - - if (this.hold()) { - ByteBuffer byteBuffer = this.mappedByteBuffer.slice(); - byteBuffer.position(pos); - ByteBuffer byteBufferNew = byteBuffer.slice(); - byteBufferNew.limit(size); - return new SelectMappedBufferResult(this.fileFromOffset + pos, byteBufferNew, size, this); - } else { - log.warn("matched, but hold failed, request pos: " + pos + ", fileFromOffset: " - + this.fileFromOffset); - } - } else { - log.warn("selectMappedBuffer request pos invalid, request pos: " + pos + ", size: " + size - + ", fileFromOffset: " + this.fileFromOffset); - } - - return null; - } - - public SelectMappedBufferResult selectMappedBuffer(int pos) { - int readPosition = getReadPosition(); - if (pos < readPosition && pos >= 0) { - if (this.hold()) { - ByteBuffer byteBuffer = this.mappedByteBuffer.slice(); - byteBuffer.position(pos); - int size = readPosition - pos; - ByteBuffer byteBufferNew = byteBuffer.slice(); - byteBufferNew.limit(size); - return new SelectMappedBufferResult(this.fileFromOffset + pos, byteBufferNew, size, this); - } - } - - return null; - } - - @Override - public boolean cleanup(final long currentRef) { - if (this.isAvailable()) { - log.error("this file[REF:" + currentRef + "] " + this.fileName - + " have not shutdown, stop unmapping."); - return false; - } - - if (this.isCleanupOver()) { - log.error("this file[REF:" + currentRef + "] " + this.fileName - + " have cleanup, do not do it again."); - return true; - } - - clean(this.mappedByteBuffer); - TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(this.fileSize * (-1)); - TOTAL_MAPPED_FILES.decrementAndGet(); - log.info("unmap file[REF:" + currentRef + "] " + this.fileName + " OK"); - return true; - } - - public boolean destroy(final long intervalForcibly) { - this.shutdown(intervalForcibly); - - if (this.isCleanupOver()) { - try { - this.fileChannel.close(); - log.info("close file channel " + this.fileName + " OK"); - - long beginTime = System.currentTimeMillis(); - boolean result = this.file.delete(); - log.info("delete file[REF:" + this.getRefCount() + "] " + this.fileName - + (result ? " OK, " : " Failed, ") + "W:" + this.getWrotePosition() + " M:" - + this.getFlushedPosition() + ", " - + UtilAll.computeEclipseTimeMilliseconds(beginTime)); - } catch (Exception e) { - log.warn("close file channel " + this.fileName + " Failed. ", e); - } - - return true; - } else { - log.warn("destroy mapped file[REF:" + this.getRefCount() + "] " + this.fileName - + " Failed. cleanupOver: " + this.cleanupOver); - } - - return false; - } - - public int getWrotePosition() { - return wrotePosition.get(); - } - - public void setWrotePosition(int pos) { - this.wrotePosition.set(pos); - } - - /** - * @return The max position which have valid data - */ - public int getReadPosition() { - return this.writeBuffer == null ? this.wrotePosition.get() : this.committedPosition.get(); - } - - public void setCommittedPosition(int pos) { - this.committedPosition.set(pos); - } - - public void warmMappedFile(FlushDiskType type, int pages) { - long beginTime = System.currentTimeMillis(); - ByteBuffer byteBuffer = this.mappedByteBuffer.slice(); - int flush = 0; - long time = System.currentTimeMillis(); - for (int i = 0, j = 0; i < this.fileSize; i += MappedFile.OS_PAGE_SIZE, j++) { - byteBuffer.put(i, (byte) 0); - // force flush when flush disk type is sync - if (type == FlushDiskType.SYNC_FLUSH) { - if ((i / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE) >= pages) { - flush = i; - mappedByteBuffer.force(); - } - } - - // prevent gc - if (j % 1000 == 0) { - log.info("j={}, costTime={}", j, System.currentTimeMillis() - time); - time = System.currentTimeMillis(); - try { - Thread.sleep(0); - } catch (InterruptedException e) { - log.error("Interrupted", e); - } - } - } - - // force flush when prepare load finished - if (type == FlushDiskType.SYNC_FLUSH) { - log.info("mapped file warm-up done, force to disk, mappedFile={}, costTime={}", - this.getFileName(), System.currentTimeMillis() - beginTime); - mappedByteBuffer.force(); - } - log.info("mapped file warm-up done. mappedFile={}, costTime={}", this.getFileName(), - System.currentTimeMillis() - beginTime); - - this.mlock(); - } - - public String getFileName() { - return fileName; - } - - public MappedByteBuffer getMappedByteBuffer() { - return mappedByteBuffer; - } - - public ByteBuffer sliceByteBuffer() { - return this.mappedByteBuffer.slice(); - } - - public long getStoreTimestamp() { - return storeTimestamp; - } - - public boolean isFirstCreateInQueue() { - return firstCreateInQueue; - } - - public void setFirstCreateInQueue(boolean firstCreateInQueue) { - this.firstCreateInQueue = firstCreateInQueue; - } - - public void mlock() { - final long beginTime = System.currentTimeMillis(); - final long address = ((DirectBuffer) (this.mappedByteBuffer)).address(); - Pointer pointer = new Pointer(address); - { - int ret = LibC.INSTANCE.mlock(pointer, new NativeLong(this.fileSize)); - log.info("mlock {} {} {} ret = {} time consuming = {}", address, this.fileName, this.fileSize, ret, System.currentTimeMillis() - beginTime); - } - - { - int ret = LibC.INSTANCE.madvise(pointer, new NativeLong(this.fileSize), LibC.MADV_WILLNEED); - log.info("madvise {} {} {} ret = {} time consuming = {}", address, this.fileName, this.fileSize, ret, System.currentTimeMillis() - beginTime); - } - } - - public void munlock() { - final long beginTime = System.currentTimeMillis(); - final long address = ((DirectBuffer) (this.mappedByteBuffer)).address(); - Pointer pointer = new Pointer(address); - int ret = LibC.INSTANCE.munlock(pointer, new NativeLong(this.fileSize)); - log.info("munlock {} {} {} ret = {} time consuming = {}", address, this.fileName, this.fileSize, ret, System.currentTimeMillis() - beginTime); - } - - //testable - File getFile() { - return this.file; - } - - @Override - public String toString() { - return this.fileName; - } -} diff --git a/store/src/main/java/org/apache/rocketmq/store/MappedFileQueue.java b/store/src/main/java/org/apache/rocketmq/store/MappedFileQueue.java index 9eb3b3ab063..e32c16a82a8 100644 --- a/store/src/main/java/org/apache/rocketmq/store/MappedFileQueue.java +++ b/store/src/main/java/org/apache/rocketmq/store/MappedFileQueue.java @@ -16,37 +16,43 @@ */ package org.apache.rocketmq.store; +import com.google.common.collect.Lists; import java.io.File; import java.io.IOException; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Stream; +import org.apache.rocketmq.common.BoundaryType; +import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.UtilAll; import org.apache.rocketmq.common.constant.LoggerName; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.logfile.DefaultMappedFile; +import org.apache.rocketmq.store.logfile.MappedFile; -public class MappedFileQueue { +public class MappedFileQueue implements Swappable { private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); private static final Logger LOG_ERROR = LoggerFactory.getLogger(LoggerName.STORE_ERROR_LOGGER_NAME); - private static final int DELETE_FILES_BATCH_MAX = 10; + protected final String storePath; - private final String storePath; + protected final int mappedFileSize; - private final int mappedFileSize; + protected final CopyOnWriteArrayList mappedFiles = new CopyOnWriteArrayList<>(); - private final CopyOnWriteArrayList mappedFiles = new CopyOnWriteArrayList(); + protected final AllocateMappedFileService allocateMappedFileService; - private final AllocateMappedFileService allocateMappedFileService; + protected long flushedWhere = 0; + protected long committedWhere = 0; - private long flushedWhere = 0; - private long committedWhere = 0; - - private volatile long storeTimestamp = 0; + protected volatile long storeTimestamp = 0; public MappedFileQueue(final String storePath, int mappedFileSize, AllocateMappedFileService allocateMappedFileService) { @@ -56,8 +62,8 @@ public MappedFileQueue(final String storePath, int mappedFileSize, } public void checkSelf() { - - if (!this.mappedFiles.isEmpty()) { + List mappedFiles = new ArrayList<>(this.mappedFiles); + if (!mappedFiles.isEmpty()) { Iterator iterator = mappedFiles.iterator(); MappedFile pre = null; while (iterator.hasNext()) { @@ -74,6 +80,89 @@ public void checkSelf() { } } + public MappedFile getConsumeQueueMappedFileByTime(final long timestamp, CommitLog commitLog, + BoundaryType boundaryType) { + Object[] mfs = copyMappedFiles(0); + if (null == mfs) { + return null; + } + + /* + * Make sure each mapped file in consume queue has accurate start and stop time in accordance with commit log + * mapped files. Note last modified time from file system is not reliable. + */ + for (int i = mfs.length - 1; i >= 0; i--) { + DefaultMappedFile mappedFile = (DefaultMappedFile) mfs[i]; + // Figure out the earliest message store time in the consume queue mapped file. + if (mappedFile.getStartTimestamp() < 0) { + SelectMappedBufferResult selectMappedBufferResult = mappedFile.selectMappedBuffer(0, ConsumeQueue.CQ_STORE_UNIT_SIZE); + if (null != selectMappedBufferResult) { + try { + ByteBuffer buffer = selectMappedBufferResult.getByteBuffer(); + long physicalOffset = buffer.getLong(); + int messageSize = buffer.getInt(); + long messageStoreTime = commitLog.pickupStoreTimestamp(physicalOffset, messageSize); + if (messageStoreTime > 0) { + mappedFile.setStartTimestamp(messageStoreTime); + } + } finally { + selectMappedBufferResult.release(); + } + } + } + // Figure out the latest message store time in the consume queue mapped file. + if (i < mfs.length - 1 && mappedFile.getStopTimestamp() < 0) { + SelectMappedBufferResult selectMappedBufferResult = mappedFile.selectMappedBuffer(mappedFileSize - ConsumeQueue.CQ_STORE_UNIT_SIZE, ConsumeQueue.CQ_STORE_UNIT_SIZE); + if (null != selectMappedBufferResult) { + try { + ByteBuffer buffer = selectMappedBufferResult.getByteBuffer(); + long physicalOffset = buffer.getLong(); + int messageSize = buffer.getInt(); + long messageStoreTime = commitLog.pickupStoreTimestamp(physicalOffset, messageSize); + if (messageStoreTime > 0) { + mappedFile.setStopTimestamp(messageStoreTime); + } + } finally { + selectMappedBufferResult.release(); + } + } + } + } + + switch (boundaryType) { + case LOWER: { + for (int i = 0; i < mfs.length; i++) { + DefaultMappedFile mappedFile = (DefaultMappedFile) mfs[i]; + if (i < mfs.length - 1) { + long stopTimestamp = mappedFile.getStopTimestamp(); + if (stopTimestamp >= timestamp) { + return mappedFile; + } + } + + // Just return the latest one. + if (i == mfs.length - 1) { + return mappedFile; + } + } + } + case UPPER: { + for (int i = mfs.length - 1; i >= 0; i--) { + DefaultMappedFile mappedFile = (DefaultMappedFile) mfs[i]; + if (mappedFile.getStartTimestamp() <= timestamp) { + return mappedFile; + } + } + } + + default: { + log.warn("Unknown boundary type"); + break; + } + } + return null; + } + public MappedFile getMappedFileByTime(final long timestamp) { Object[] mfs = this.copyMappedFiles(0); @@ -90,7 +179,7 @@ public MappedFile getMappedFileByTime(final long timestamp) { return (MappedFile) mfs[mfs.length - 1]; } - private Object[] copyMappedFiles(final int reservedMappedFiles) { + protected Object[] copyMappedFiles(final int reservedMappedFiles) { Object[] mfs; if (this.mappedFiles.size() <= reservedMappedFiles) { @@ -102,7 +191,7 @@ private Object[] copyMappedFiles(final int reservedMappedFiles) { } public void truncateDirtyFiles(long offset) { - List willRemoveFiles = new ArrayList(); + List willRemoveFiles = new ArrayList<>(); for (MappedFile file : this.mappedFiles) { long fileTailOffset = file.getFileFromOffset() + this.mappedFileSize; @@ -144,35 +233,51 @@ void deleteExpiredFile(List files) { } } + public boolean load() { File dir = new File(this.storePath); - File[] files = dir.listFiles(); - if (files != null) { - // ascending order - Arrays.sort(files); - for (File file : files) { - - if (file.length() != this.mappedFileSize) { - log.warn(file + "\t" + file.length() - + " length not matched message store config value, ignore it"); - return true; - } + File[] ls = dir.listFiles(); + if (ls != null) { + return doLoad(Arrays.asList(ls)); + } + return true; + } - try { - MappedFile mappedFile = new MappedFile(file.getPath(), mappedFileSize); - - mappedFile.setWrotePosition(this.mappedFileSize); - mappedFile.setFlushedPosition(this.mappedFileSize); - mappedFile.setCommittedPosition(this.mappedFileSize); - this.mappedFiles.add(mappedFile); - log.info("load " + file.getPath() + " OK"); - } catch (IOException e) { - log.error("load file " + file + " error", e); - return false; - } + public boolean doLoad(List files) { + // ascending order + files.sort(Comparator.comparing(File::getName)); + + for (int i = 0; i < files.size(); i++) { + File file = files.get(i); + if (file.isDirectory()) { + continue; } - } + if (file.length() == 0 && i == files.size() - 1) { + boolean ok = file.delete(); + log.warn("{} size is 0, auto delete. is_ok: {}", file, ok); + continue; + } + + if (file.length() != this.mappedFileSize) { + log.warn(file + "\t" + file.length() + + " length not matched message store config value, please check it manually"); + return false; + } + + try { + MappedFile mappedFile = new DefaultMappedFile(file.getPath(), mappedFileSize); + + mappedFile.setWrotePosition(this.mappedFileSize); + mappedFile.setFlushedPosition(this.mappedFileSize); + mappedFile.setCommittedPosition(this.mappedFileSize); + this.mappedFiles.add(mappedFile); + log.info("load " + file.getPath() + " OK"); + } catch (IOException e) { + log.error("load file " + file + " error", e); + return false; + } + } return true; } @@ -180,7 +285,7 @@ public long howMuchFallBehind() { if (this.mappedFiles.isEmpty()) return 0; - long committed = this.flushedWhere; + long committed = this.getFlushedWhere(); if (committed != 0) { MappedFile mappedFile = this.getLastMappedFile(0, false); if (mappedFile != null) { @@ -204,33 +309,67 @@ public MappedFile getLastMappedFile(final long startOffset, boolean needCreate) } if (createOffset != -1 && needCreate) { - String nextFilePath = this.storePath + File.separator + UtilAll.offset2FileName(createOffset); - String nextNextFilePath = this.storePath + File.separator - + UtilAll.offset2FileName(createOffset + this.mappedFileSize); - MappedFile mappedFile = null; + return tryCreateMappedFile(createOffset); + } + + return mappedFileLast; + } + + public boolean isMappedFilesEmpty() { + return this.mappedFiles.isEmpty(); + } + + public boolean isEmptyOrCurrentFileFull() { + MappedFile mappedFileLast = getLastMappedFile(); + if (mappedFileLast == null) { + return true; + } + if (mappedFileLast.isFull()) { + return true; + } + return false; + } + + public boolean shouldRoll(final int msgSize) { + if (isEmptyOrCurrentFileFull()) { + return true; + } + MappedFile mappedFileLast = getLastMappedFile(); + if (mappedFileLast.getWrotePosition() + msgSize > mappedFileLast.getFileSize()) { + return true; + } + return false; + } + + public MappedFile tryCreateMappedFile(long createOffset) { + String nextFilePath = this.storePath + File.separator + UtilAll.offset2FileName(createOffset); + String nextNextFilePath = this.storePath + File.separator + UtilAll.offset2FileName(createOffset + + this.mappedFileSize); + return doCreateMappedFile(nextFilePath, nextNextFilePath); + } - if (this.allocateMappedFileService != null) { - mappedFile = this.allocateMappedFileService.putRequestAndReturnMappedFile(nextFilePath, + protected MappedFile doCreateMappedFile(String nextFilePath, String nextNextFilePath) { + MappedFile mappedFile = null; + + if (this.allocateMappedFileService != null) { + mappedFile = this.allocateMappedFileService.putRequestAndReturnMappedFile(nextFilePath, nextNextFilePath, this.mappedFileSize); - } else { - try { - mappedFile = new MappedFile(nextFilePath, this.mappedFileSize); - } catch (IOException e) { - log.error("create mappedFile exception", e); - } + } else { + try { + mappedFile = new DefaultMappedFile(nextFilePath, this.mappedFileSize); + } catch (IOException e) { + log.error("create mappedFile exception", e); } + } - if (mappedFile != null) { - if (this.mappedFiles.isEmpty()) { - mappedFile.setFirstCreateInQueue(true); - } - this.mappedFiles.add(mappedFile); + if (mappedFile != null) { + if (this.mappedFiles.isEmpty()) { + mappedFile.setFirstCreateInQueue(true); } - - return mappedFile; + this.mappedFiles.add(mappedFile); } - return mappedFileLast; + return mappedFile; } public MappedFile getLastMappedFile(final long startOffset) { @@ -239,7 +378,6 @@ public MappedFile getLastMappedFile(final long startOffset) { public MappedFile getLastMappedFile() { MappedFile mappedFileLast = null; - while (!this.mappedFiles.isEmpty()) { try { mappedFileLast = this.mappedFiles.get(this.mappedFiles.size() - 1); @@ -251,7 +389,6 @@ public MappedFile getLastMappedFile() { break; } } - return mappedFileLast; } @@ -268,7 +405,8 @@ public boolean resetOffset(long offset) { return false; } - ListIterator iterator = this.mappedFiles.listIterator(); + ListIterator iterator = this.mappedFiles.listIterator(mappedFiles.size()); + List toRemoves = new ArrayList<>(); while (iterator.hasPrevious()) { mappedFileLast = iterator.previous(); @@ -279,9 +417,14 @@ public boolean resetOffset(long offset) { mappedFileLast.setCommittedPosition(where); break; } else { - iterator.remove(); + toRemoves.add(mappedFileLast); } } + + if (!toRemoves.isEmpty()) { + this.mappedFiles.removeAll(toRemoves); + } + return true; } @@ -316,11 +459,11 @@ public long getMaxWrotePosition() { } public long remainHowManyDataToCommit() { - return getMaxWrotePosition() - committedWhere; + return getMaxWrotePosition() - getCommittedWhere(); } public long remainHowManyDataToFlush() { - return getMaxOffset() - flushedWhere; + return getMaxOffset() - this.getFlushedWhere(); } public void deleteLastMappedFile() { @@ -336,7 +479,8 @@ public void deleteLastMappedFile() { public int deleteExpiredFileByTime(final long expiredTime, final int deleteFilesInterval, final long intervalForcibly, - final boolean cleanImmediately) { + final boolean cleanImmediately, + final int deleteFileBatchMax) { Object[] mfs = this.copyMappedFiles(0); if (null == mfs) @@ -344,17 +488,23 @@ public int deleteExpiredFileByTime(final long expiredTime, int mfsLength = mfs.length - 1; int deleteCount = 0; - List files = new ArrayList(); + List files = new ArrayList<>(); + int skipFileNum = 0; if (null != mfs) { + //do check before deleting + checkSelf(); for (int i = 0; i < mfsLength; i++) { MappedFile mappedFile = (MappedFile) mfs[i]; long liveMaxTimestamp = mappedFile.getLastModifiedTimestamp() + expiredTime; if (System.currentTimeMillis() >= liveMaxTimestamp || cleanImmediately) { + if (skipFileNum > 0) { + log.info("Delete CommitLog {} but skip {} files", mappedFile.getFileName(), skipFileNum); + } if (mappedFile.destroy(intervalForcibly)) { files.add(mappedFile); deleteCount++; - if (files.size() >= DELETE_FILES_BATCH_MAX) { + if (files.size() >= deleteFileBatchMax) { break; } @@ -368,6 +518,7 @@ public int deleteExpiredFileByTime(final long expiredTime, break; } } else { + skipFileNum++; //avoid deleting files in the middle break; } @@ -382,7 +533,7 @@ public int deleteExpiredFileByTime(final long expiredTime, public int deleteExpiredFileByOffset(long offset, int unitSize) { Object[] mfs = this.copyMappedFiles(0); - List files = new ArrayList(); + List files = new ArrayList<>(); int deleteCount = 0; if (null != mfs) { @@ -422,31 +573,89 @@ public int deleteExpiredFileByOffset(long offset, int unitSize) { return deleteCount; } + public int deleteExpiredFileByOffsetForTimerLog(long offset, int checkOffset, int unitSize) { + Object[] mfs = this.copyMappedFiles(0); + + List files = new ArrayList<>(); + int deleteCount = 0; + if (null != mfs) { + + int mfsLength = mfs.length - 1; + + for (int i = 0; i < mfsLength; i++) { + boolean destroy = false; + MappedFile mappedFile = (MappedFile) mfs[i]; + SelectMappedBufferResult result = mappedFile.selectMappedBuffer(checkOffset); + try { + if (result != null) { + int position = result.getByteBuffer().position(); + int size = result.getByteBuffer().getInt();//size + result.getByteBuffer().getLong(); //prev pos + int magic = result.getByteBuffer().getInt(); + if (size == unitSize && (magic | 0xF) == 0xF) { + result.getByteBuffer().position(position + MixAll.UNIT_PRE_SIZE_FOR_MSG); + long maxOffsetPy = result.getByteBuffer().getLong(); + destroy = maxOffsetPy < offset; + if (destroy) { + log.info("physic min commitlog offset " + offset + ", current mappedFile's max offset " + + maxOffsetPy + ", delete it"); + } + } else { + log.warn("Found error data in [{}] checkOffset:{} unitSize:{}", mappedFile.getFileName(), + checkOffset, unitSize); + } + } else if (!mappedFile.isAvailable()) { // Handle hanged file. + log.warn("Found a hanged consume queue file, attempting to delete it."); + destroy = true; + } else { + log.warn("this being not executed forever."); + break; + } + } finally { + if (null != result) { + result.release(); + } + } + + if (destroy && mappedFile.destroy(1000 * 60)) { + files.add(mappedFile); + deleteCount++; + } else { + break; + } + } + } + + deleteExpiredFile(files); + + return deleteCount; + } + public boolean flush(final int flushLeastPages) { boolean result = true; - MappedFile mappedFile = this.findMappedFileByOffset(this.flushedWhere, this.flushedWhere == 0); + MappedFile mappedFile = this.findMappedFileByOffset(this.getFlushedWhere(), this.getFlushedWhere() == 0); if (mappedFile != null) { long tmpTimeStamp = mappedFile.getStoreTimestamp(); int offset = mappedFile.flush(flushLeastPages); long where = mappedFile.getFileFromOffset() + offset; - result = where == this.flushedWhere; - this.flushedWhere = where; + result = where == this.getFlushedWhere(); + this.setFlushedWhere(where); if (0 == flushLeastPages) { - this.storeTimestamp = tmpTimeStamp; + this.setStoreTimestamp(tmpTimeStamp); } } return result; } - public boolean commit(final int commitLeastPages) { + public synchronized boolean commit(final int commitLeastPages) { boolean result = true; - MappedFile mappedFile = this.findMappedFileByOffset(this.committedWhere, this.committedWhere == 0); + MappedFile mappedFile = this.findMappedFileByOffset(this.getCommittedWhere(), this.getCommittedWhere() == 0); if (mappedFile != null) { int offset = mappedFile.commit(commitLeastPages); long where = mappedFile.getFileFromOffset() + offset; - result = where == this.committedWhere; - this.committedWhere = where; + result = where == this.getCommittedWhere(); + this.setCommittedWhere(where); } return result; @@ -461,26 +670,39 @@ public boolean commit(final int commitLeastPages) { */ public MappedFile findMappedFileByOffset(final long offset, final boolean returnFirstOnNotFound) { try { - MappedFile mappedFile = this.getFirstMappedFile(); - if (mappedFile != null) { - int index = (int) ((offset / this.mappedFileSize) - (mappedFile.getFileFromOffset() / this.mappedFileSize)); - if (index < 0 || index >= this.mappedFiles.size()) { - LOG_ERROR.warn("Offset for {} not matched. Request offset: {}, index: {}, " + - "mappedFileSize: {}, mappedFiles count: {}", - mappedFile, + MappedFile firstMappedFile = this.getFirstMappedFile(); + MappedFile lastMappedFile = this.getLastMappedFile(); + if (firstMappedFile != null && lastMappedFile != null) { + if (offset < firstMappedFile.getFileFromOffset() || offset >= lastMappedFile.getFileFromOffset() + this.mappedFileSize) { + LOG_ERROR.warn("Offset not matched. Request offset: {}, firstOffset: {}, lastOffset: {}, mappedFileSize: {}, mappedFiles count: {}", offset, - index, + firstMappedFile.getFileFromOffset(), + lastMappedFile.getFileFromOffset() + this.mappedFileSize, this.mappedFileSize, this.mappedFiles.size()); - } + } else { + int index = (int) ((offset / this.mappedFileSize) - (firstMappedFile.getFileFromOffset() / this.mappedFileSize)); + MappedFile targetFile = null; + try { + targetFile = this.mappedFiles.get(index); + } catch (Exception ignored) { + } - try { - return this.mappedFiles.get(index); - } catch (Exception e) { - if (returnFirstOnNotFound) { - return mappedFile; + if (targetFile != null && offset >= targetFile.getFileFromOffset() + && offset < targetFile.getFileFromOffset() + this.mappedFileSize) { + return targetFile; } - LOG_ERROR.warn("findMappedFileByOffset failure. ", e); + + for (MappedFile tmpMappedFile : this.mappedFiles) { + if (offset >= tmpMappedFile.getFileFromOffset() + && offset < tmpMappedFile.getFileFromOffset() + this.mappedFileSize) { + return tmpMappedFile; + } + } + } + + if (returnFirstOnNotFound) { + return firstMappedFile; } } } catch (Exception e) { @@ -533,7 +755,7 @@ public boolean retryDeleteFirstFile(final long intervalForcibly) { boolean result = mappedFile.destroy(intervalForcibly); if (result) { log.info("the mappedFile re delete OK, " + mappedFile.getFileName()); - List tmpFiles = new ArrayList(); + List tmpFiles = new ArrayList<>(); tmpFiles.add(mappedFile); this.deleteExpiredFile(tmpFiles); } else { @@ -558,7 +780,7 @@ public void destroy() { mf.destroy(1000 * 3); } this.mappedFiles.clear(); - this.flushedWhere = 0; + this.setFlushedWhere(0); // delete parent directory File file = new File(storePath); @@ -567,6 +789,70 @@ public void destroy() { } } + @Override + public void swapMap(int reserveNum, long forceSwapIntervalMs, long normalSwapIntervalMs) { + + if (mappedFiles.isEmpty()) { + return; + } + + if (reserveNum < 3) { + reserveNum = 3; + } + + Object[] mfs = this.copyMappedFiles(0); + if (null == mfs) { + return; + } + + for (int i = mfs.length - reserveNum - 1; i >= 0; i--) { + MappedFile mappedFile = (MappedFile) mfs[i]; + if (System.currentTimeMillis() - mappedFile.getRecentSwapMapTime() > forceSwapIntervalMs) { + mappedFile.swapMap(); + continue; + } + if (System.currentTimeMillis() - mappedFile.getRecentSwapMapTime() > normalSwapIntervalMs + && mappedFile.getMappedByteBufferAccessCountSinceLastSwap() > 0) { + mappedFile.swapMap(); + continue; + } + } + } + + @Override + public void cleanSwappedMap(long forceCleanSwapIntervalMs) { + + if (mappedFiles.isEmpty()) { + return; + } + + int reserveNum = 3; + Object[] mfs = this.copyMappedFiles(0); + if (null == mfs) { + return; + } + + for (int i = mfs.length - reserveNum - 1; i >= 0; i--) { + MappedFile mappedFile = (MappedFile) mfs[i]; + if (System.currentTimeMillis() - mappedFile.getRecentSwapMapTime() > forceCleanSwapIntervalMs) { + mappedFile.cleanSwapedMap(false); + } + } + } + + public Object[] snapshot() { + // return a safe copy + return this.mappedFiles.toArray(); + } + + public Stream stream() { + return this.mappedFiles.stream(); + } + + public Stream reversedStream() { + return Lists.reverse(this.mappedFiles).stream(); + } + public long getFlushedWhere() { return flushedWhere; } @@ -579,6 +865,10 @@ public long getStoreTimestamp() { return storeTimestamp; } + public void setStoreTimestamp(long storeTimestamp) { + this.storeTimestamp = storeTimestamp; + } + public List getMappedFiles() { return mappedFiles; } @@ -594,4 +884,34 @@ public long getCommittedWhere() { public void setCommittedWhere(final long committedWhere) { this.committedWhere = committedWhere; } + + public long getTotalFileSize() { + return (long) mappedFileSize * mappedFiles.size(); + } + + public String getStorePath() { + return storePath; + } + + public List range(final long from, final long to) { + Object[] mfs = copyMappedFiles(0); + if (null == mfs) { + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + for (Object mf : mfs) { + MappedFile mappedFile = (MappedFile) mf; + if (mappedFile.getFileFromOffset() + mappedFile.getFileSize() <= from) { + continue; + } + + if (to <= mappedFile.getFileFromOffset()) { + break; + } + result.add(mappedFile); + } + + return result; + } } diff --git a/store/src/main/java/org/apache/rocketmq/store/MessageArrivingListener.java b/store/src/main/java/org/apache/rocketmq/store/MessageArrivingListener.java index bae7a161689..ceca98f042e 100644 --- a/store/src/main/java/org/apache/rocketmq/store/MessageArrivingListener.java +++ b/store/src/main/java/org/apache/rocketmq/store/MessageArrivingListener.java @@ -20,6 +20,17 @@ import java.util.Map; public interface MessageArrivingListener { + + /** + * Notify that a new message arrives in a consume queue + * @param topic topic name + * @param queueId consume queue id + * @param logicOffset consume queue offset + * @param tagsCode message tags hash code + * @param msgStoreTime message store time + * @param filterBitMap message bloom filter + * @param properties message properties + */ void arriving(String topic, int queueId, long logicOffset, long tagsCode, long msgStoreTime, byte[] filterBitMap, Map properties); } diff --git a/store/src/main/java/org/apache/rocketmq/store/MessageExtBrokerInner.java b/store/src/main/java/org/apache/rocketmq/store/MessageExtBrokerInner.java deleted file mode 100644 index c7879af0645..00000000000 --- a/store/src/main/java/org/apache/rocketmq/store/MessageExtBrokerInner.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.store; - -import org.apache.rocketmq.common.TopicFilterType; -import org.apache.rocketmq.common.message.MessageExt; - -public class MessageExtBrokerInner extends MessageExt { - private static final long serialVersionUID = 7256001576878700634L; - private String propertiesString; - private long tagsCode; - - public static long tagsString2tagsCode(final TopicFilterType filter, final String tags) { - if (null == tags || tags.length() == 0) - return 0; - - return tags.hashCode(); - } - - public String getPropertiesString() { - return propertiesString; - } - - public void setPropertiesString(String propertiesString) { - this.propertiesString = propertiesString; - } - - public long getTagsCode() { - return tagsCode; - } - - public void setTagsCode(long tagsCode) { - this.tagsCode = tagsCode; - } -} diff --git a/store/src/main/java/org/apache/rocketmq/store/MessageExtEncoder.java b/store/src/main/java/org/apache/rocketmq/store/MessageExtEncoder.java new file mode 100644 index 00000000000..20e9a652b7e --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/MessageExtEncoder.java @@ -0,0 +1,437 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.UnpooledByteBufAllocator; +import java.nio.ByteBuffer; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExtBatch; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.message.MessageVersion; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.config.MessageStoreConfig; + +public class MessageExtEncoder { + protected static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + private ByteBuf byteBuf; + // The maximum length of the message body. + private int maxMessageBodySize; + // The maximum length of the full message. + private int maxMessageSize; + private final int crc32ReservedLength; + private MessageStoreConfig messageStoreConfig; + + public MessageExtEncoder(final int maxMessageBodySize, final MessageStoreConfig messageStoreConfig) { + this(messageStoreConfig); + } + + public MessageExtEncoder(final MessageStoreConfig messageStoreConfig) { + ByteBufAllocator alloc = UnpooledByteBufAllocator.DEFAULT; + this.messageStoreConfig = messageStoreConfig; + this.maxMessageBodySize = messageStoreConfig.getMaxMessageSize(); + //Reserve 64kb for encoding buffer outside body + int maxMessageSize = Integer.MAX_VALUE - maxMessageBodySize >= 64 * 1024 ? + maxMessageBodySize + 64 * 1024 : Integer.MAX_VALUE; + byteBuf = alloc.directBuffer(maxMessageSize); + this.maxMessageSize = maxMessageSize; + this.crc32ReservedLength = messageStoreConfig.isEnabledAppendPropCRC() ? CommitLog.CRC32_RESERVED_LEN : 0; + } + + public static int calMsgLength(MessageVersion messageVersion, + int sysFlag, int bodyLength, int topicLength, int propertiesLength) { + + int bornhostLength = (sysFlag & MessageSysFlag.BORNHOST_V6_FLAG) == 0 ? 8 : 20; + int storehostAddressLength = (sysFlag & MessageSysFlag.STOREHOSTADDRESS_V6_FLAG) == 0 ? 8 : 20; + + return 4 //TOTALSIZE + + 4 //MAGICCODE + + 4 //BODYCRC + + 4 //QUEUEID + + 4 //FLAG + + 8 //QUEUEOFFSET + + 8 //PHYSICALOFFSET + + 4 //SYSFLAG + + 8 //BORNTIMESTAMP + + bornhostLength //BORNHOST + + 8 //STORETIMESTAMP + + storehostAddressLength //STOREHOSTADDRESS + + 4 //RECONSUMETIMES + + 8 //Prepared Transaction Offset + + 4 + (Math.max(bodyLength, 0)) //BODY + + messageVersion.getTopicLengthSize() + topicLength //TOPIC + + 2 + (Math.max(propertiesLength, 0)); //propertiesLength + } + + public static int calMsgLengthNoProperties(MessageVersion messageVersion, + int sysFlag, int bodyLength, int topicLength) { + + int bornhostLength = (sysFlag & MessageSysFlag.BORNHOST_V6_FLAG) == 0 ? 8 : 20; + int storehostAddressLength = (sysFlag & MessageSysFlag.STOREHOSTADDRESS_V6_FLAG) == 0 ? 8 : 20; + + return 4 //TOTALSIZE + + 4 //MAGICCODE + + 4 //BODYCRC + + 4 //QUEUEID + + 4 //FLAG + + 8 //QUEUEOFFSET + + 8 //PHYSICALOFFSET + + 4 //SYSFLAG + + 8 //BORNTIMESTAMP + + bornhostLength //BORNHOST + + 8 //STORETIMESTAMP + + storehostAddressLength //STOREHOSTADDRESS + + 4 //RECONSUMETIMES + + 8 //Prepared Transaction Offset + + 4 + (Math.max(bodyLength, 0)) //BODY + + messageVersion.getTopicLengthSize() + topicLength; //TOPIC + } + + public PutMessageResult encodeWithoutProperties(MessageExtBrokerInner msgInner) { + + final byte[] topicData = msgInner.getTopic().getBytes(MessageDecoder.CHARSET_UTF8); + final int topicLength = topicData.length; + + final int bodyLength = msgInner.getBody() == null ? 0 : msgInner.getBody().length; + + // Exceeds the maximum message body + if (bodyLength > this.maxMessageBodySize) { + CommitLog.log.warn("message body size exceeded, msg body size: " + bodyLength + + ", maxMessageSize: " + this.maxMessageBodySize); + return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null); + } + + final int msgLenNoProperties = calMsgLengthNoProperties(msgInner.getVersion(), msgInner.getSysFlag(), bodyLength, topicLength); + + // 1 TOTALSIZE + this.byteBuf.writeInt(msgLenNoProperties); + // 2 MAGICCODE + this.byteBuf.writeInt(msgInner.getVersion().getMagicCode()); + // 3 BODYCRC + this.byteBuf.writeInt(msgInner.getBodyCRC()); + // 4 QUEUEID + this.byteBuf.writeInt(msgInner.getQueueId()); + // 5 FLAG + this.byteBuf.writeInt(msgInner.getFlag()); + // 6 QUEUEOFFSET, need update later + this.byteBuf.writeLong(0); + // 7 PHYSICALOFFSET, need update later + this.byteBuf.writeLong(0); + // 8 SYSFLAG + this.byteBuf.writeInt(msgInner.getSysFlag()); + // 9 BORNTIMESTAMP + this.byteBuf.writeLong(msgInner.getBornTimestamp()); + + // 10 BORNHOST + ByteBuffer bornHostBytes = msgInner.getBornHostBytes(); + this.byteBuf.writeBytes(bornHostBytes.array()); + + // 11 STORETIMESTAMP + this.byteBuf.writeLong(msgInner.getStoreTimestamp()); + + // 12 STOREHOSTADDRESS + ByteBuffer storeHostBytes = msgInner.getStoreHostBytes(); + this.byteBuf.writeBytes(storeHostBytes.array()); + + // 13 RECONSUMETIMES + this.byteBuf.writeInt(msgInner.getReconsumeTimes()); + // 14 Prepared Transaction Offset + this.byteBuf.writeLong(msgInner.getPreparedTransactionOffset()); + // 15 BODY + this.byteBuf.writeInt(bodyLength); + if (bodyLength > 0) + this.byteBuf.writeBytes(msgInner.getBody()); + + // 16 TOPIC + if (MessageVersion.MESSAGE_VERSION_V2.equals(msgInner.getVersion())) { + this.byteBuf.writeShort((short) topicLength); + } else { + this.byteBuf.writeByte((byte) topicLength); + } + this.byteBuf.writeBytes(topicData); + + return null; + } + + public PutMessageResult encode(MessageExtBrokerInner msgInner) { + this.byteBuf.clear(); + + if (messageStoreConfig.isEnableMultiDispatch() && CommitLog.isMultiDispatchMsg(msgInner)) { + return encodeWithoutProperties(msgInner); + } + + /** + * Serialize message + */ + final byte[] propertiesData = + msgInner.getPropertiesString() == null ? null : msgInner.getPropertiesString().getBytes(MessageDecoder.CHARSET_UTF8); + + boolean needAppendLastPropertySeparator = crc32ReservedLength > 0 && propertiesData != null && propertiesData.length > 0 + && propertiesData[propertiesData.length - 1] != MessageDecoder.PROPERTY_SEPARATOR; + + final int propertiesLength = (propertiesData == null ? 0 : propertiesData.length) + (needAppendLastPropertySeparator ? 1 : 0) + crc32ReservedLength; + + if (propertiesLength > Short.MAX_VALUE) { + log.warn("putMessage message properties length too long. length={}", propertiesLength); + return new PutMessageResult(PutMessageStatus.PROPERTIES_SIZE_EXCEEDED, null); + } + + final byte[] topicData = msgInner.getTopic().getBytes(MessageDecoder.CHARSET_UTF8); + final int topicLength = topicData.length; + + final int bodyLength = msgInner.getBody() == null ? 0 : msgInner.getBody().length; + final int msgLen = calMsgLength( + msgInner.getVersion(), msgInner.getSysFlag(), bodyLength, topicLength, propertiesLength); + + // Exceeds the maximum message body + if (bodyLength > this.maxMessageBodySize) { + CommitLog.log.warn("message body size exceeded, msg total size: " + msgLen + ", msg body size: " + bodyLength + + ", maxMessageSize: " + this.maxMessageBodySize); + return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null); + } + + final long queueOffset = msgInner.getQueueOffset(); + + // Exceeds the maximum message + if (msgLen > this.maxMessageSize) { + CommitLog.log.warn("message size exceeded, msg total size: " + msgLen + ", msg body size: " + bodyLength + + ", maxMessageSize: " + this.maxMessageSize); + return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null); + } + + // 1 TOTALSIZE + this.byteBuf.writeInt(msgLen); + // 2 MAGICCODE + this.byteBuf.writeInt(msgInner.getVersion().getMagicCode()); + // 3 BODYCRC + this.byteBuf.writeInt(msgInner.getBodyCRC()); + // 4 QUEUEID + this.byteBuf.writeInt(msgInner.getQueueId()); + // 5 FLAG + this.byteBuf.writeInt(msgInner.getFlag()); + // 6 QUEUEOFFSET + this.byteBuf.writeLong(queueOffset); + // 7 PHYSICALOFFSET, need update later + this.byteBuf.writeLong(0); + // 8 SYSFLAG + this.byteBuf.writeInt(msgInner.getSysFlag()); + // 9 BORNTIMESTAMP + this.byteBuf.writeLong(msgInner.getBornTimestamp()); + + // 10 BORNHOST + ByteBuffer bornHostBytes = msgInner.getBornHostBytes(); + this.byteBuf.writeBytes(bornHostBytes.array()); + + // 11 STORETIMESTAMP + this.byteBuf.writeLong(msgInner.getStoreTimestamp()); + + // 12 STOREHOSTADDRESS + ByteBuffer storeHostBytes = msgInner.getStoreHostBytes(); + this.byteBuf.writeBytes(storeHostBytes.array()); + + // 13 RECONSUMETIMES + this.byteBuf.writeInt(msgInner.getReconsumeTimes()); + // 14 Prepared Transaction Offset + this.byteBuf.writeLong(msgInner.getPreparedTransactionOffset()); + // 15 BODY + this.byteBuf.writeInt(bodyLength); + if (bodyLength > 0) + this.byteBuf.writeBytes(msgInner.getBody()); + + // 16 TOPIC + if (MessageVersion.MESSAGE_VERSION_V2.equals(msgInner.getVersion())) { + this.byteBuf.writeShort((short) topicLength); + } else { + this.byteBuf.writeByte((byte) topicLength); + } + this.byteBuf.writeBytes(topicData); + + // 17 PROPERTIES + this.byteBuf.writeShort((short) propertiesLength); + if (propertiesLength > crc32ReservedLength) { + this.byteBuf.writeBytes(propertiesData); + } + if (needAppendLastPropertySeparator) { + this.byteBuf.writeByte((byte) MessageDecoder.PROPERTY_SEPARATOR); + } + // 18 CRC32 + this.byteBuf.writerIndex(this.byteBuf.writerIndex() + crc32ReservedLength); + + return null; + } + + public ByteBuffer encode(final MessageExtBatch messageExtBatch, PutMessageContext putMessageContext) { + this.byteBuf.clear(); + + ByteBuffer messagesByteBuff = messageExtBatch.wrap(); + + int totalLength = messagesByteBuff.limit(); + if (totalLength > this.maxMessageBodySize) { + CommitLog.log.warn("message body size exceeded, msg body size: " + totalLength + ", maxMessageSize: " + this.maxMessageBodySize); + throw new RuntimeException("message body size exceeded"); + } + + // properties from MessageExtBatch + String batchPropStr = MessageDecoder.messageProperties2String(messageExtBatch.getProperties()); + final byte[] batchPropData = batchPropStr.getBytes(MessageDecoder.CHARSET_UTF8); + int batchPropDataLen = batchPropData.length; + if (batchPropDataLen > Short.MAX_VALUE) { + CommitLog.log.warn("Properties size of messageExtBatch exceeded, properties size: {}, maxSize: {}.", batchPropDataLen, Short.MAX_VALUE); + throw new RuntimeException("Properties size of messageExtBatch exceeded!"); + } + final short batchPropLen = (short) batchPropDataLen; + + int batchSize = 0; + while (messagesByteBuff.hasRemaining()) { + batchSize++; + // 1 TOTALSIZE + messagesByteBuff.getInt(); + // 2 MAGICCODE + messagesByteBuff.getInt(); + // 3 BODYCRC + messagesByteBuff.getInt(); + // 4 FLAG + int flag = messagesByteBuff.getInt(); + // 5 BODY + int bodyLen = messagesByteBuff.getInt(); + int bodyPos = messagesByteBuff.position(); + int bodyCrc = UtilAll.crc32(messagesByteBuff.array(), bodyPos, bodyLen); + messagesByteBuff.position(bodyPos + bodyLen); + // 6 properties + short propertiesLen = messagesByteBuff.getShort(); + int propertiesPos = messagesByteBuff.position(); + messagesByteBuff.position(propertiesPos + propertiesLen); + boolean needAppendLastPropertySeparator = propertiesLen > 0 && batchPropLen > 0 + && messagesByteBuff.get(messagesByteBuff.position() - 1) != MessageDecoder.PROPERTY_SEPARATOR; + + final byte[] topicData = messageExtBatch.getTopic().getBytes(MessageDecoder.CHARSET_UTF8); + + final int topicLength = topicData.length; + int totalPropLen = needAppendLastPropertySeparator ? + propertiesLen + batchPropLen + 1 : propertiesLen + batchPropLen; + + // properties need to add crc32 + totalPropLen += crc32ReservedLength; + final int msgLen = calMsgLength( + messageExtBatch.getVersion(), messageExtBatch.getSysFlag(), bodyLen, topicLength, totalPropLen); + + // 1 TOTALSIZE + this.byteBuf.writeInt(msgLen); + // 2 MAGICCODE + this.byteBuf.writeInt(messageExtBatch.getVersion().getMagicCode()); + // 3 BODYCRC + this.byteBuf.writeInt(bodyCrc); + // 4 QUEUEID + this.byteBuf.writeInt(messageExtBatch.getQueueId()); + // 5 FLAG + this.byteBuf.writeInt(flag); + // 6 QUEUEOFFSET + this.byteBuf.writeLong(0); + // 7 PHYSICALOFFSET + this.byteBuf.writeLong(0); + // 8 SYSFLAG + this.byteBuf.writeInt(messageExtBatch.getSysFlag()); + // 9 BORNTIMESTAMP + this.byteBuf.writeLong(messageExtBatch.getBornTimestamp()); + + // 10 BORNHOST + ByteBuffer bornHostBytes = messageExtBatch.getBornHostBytes(); + this.byteBuf.writeBytes(bornHostBytes.array()); + + // 11 STORETIMESTAMP + this.byteBuf.writeLong(messageExtBatch.getStoreTimestamp()); + + // 12 STOREHOSTADDRESS + ByteBuffer storeHostBytes = messageExtBatch.getStoreHostBytes(); + this.byteBuf.writeBytes(storeHostBytes.array()); + + // 13 RECONSUMETIMES + this.byteBuf.writeInt(messageExtBatch.getReconsumeTimes()); + // 14 Prepared Transaction Offset, batch does not support transaction + this.byteBuf.writeLong(0); + // 15 BODY + this.byteBuf.writeInt(bodyLen); + if (bodyLen > 0) + this.byteBuf.writeBytes(messagesByteBuff.array(), bodyPos, bodyLen); + + // 16 TOPIC + if (MessageVersion.MESSAGE_VERSION_V2.equals(messageExtBatch.getVersion())) { + this.byteBuf.writeShort((short) topicLength); + } else { + this.byteBuf.writeByte((byte) topicLength); + } + this.byteBuf.writeBytes(topicData); + + // 17 PROPERTIES + this.byteBuf.writeShort((short) totalPropLen); + if (propertiesLen > 0) { + this.byteBuf.writeBytes(messagesByteBuff.array(), propertiesPos, propertiesLen); + } + if (batchPropLen > 0) { + if (needAppendLastPropertySeparator) { + this.byteBuf.writeByte((byte) MessageDecoder.PROPERTY_SEPARATOR); + } + this.byteBuf.writeBytes(batchPropData, 0, batchPropLen); + } + this.byteBuf.writerIndex(this.byteBuf.writerIndex() + crc32ReservedLength); + } + putMessageContext.setBatchSize(batchSize); + putMessageContext.setPhyPos(new long[batchSize]); + + return this.byteBuf.nioBuffer(); + } + + public ByteBuffer getEncoderBuffer() { + return this.byteBuf.nioBuffer(0, this.byteBuf.capacity()); + } + + public int getMaxMessageBodySize() { + return this.maxMessageBodySize; + } + + public void updateEncoderBufferCapacity(int newMaxMessageBodySize) { + this.maxMessageBodySize = newMaxMessageBodySize; + //Reserve 64kb for encoding buffer outside body + this.maxMessageSize = Integer.MAX_VALUE - newMaxMessageBodySize >= 64 * 1024 ? + this.maxMessageBodySize + 64 * 1024 : Integer.MAX_VALUE; + this.byteBuf.capacity(this.maxMessageSize); + } + + static class PutMessageThreadLocal { + private final MessageExtEncoder encoder; + private final StringBuilder keyBuilder; + + PutMessageThreadLocal(MessageStoreConfig messageStoreConfig) { + encoder = new MessageExtEncoder(messageStoreConfig); + keyBuilder = new StringBuilder(); + } + + public MessageExtEncoder getEncoder() { + return encoder; + } + + public StringBuilder getKeyBuilder() { + return keyBuilder; + } + } + +} diff --git a/store/src/main/java/org/apache/rocketmq/store/MessageStore.java b/store/src/main/java/org/apache/rocketmq/store/MessageStore.java index 907dfe2093b..814c6d1bfef 100644 --- a/store/src/main/java/org/apache/rocketmq/store/MessageStore.java +++ b/store/src/main/java/org/apache/rocketmq/store/MessageStore.java @@ -16,11 +16,35 @@ */ package org.apache.rocketmq.store; +import java.nio.ByteBuffer; import java.util.HashMap; import java.util.LinkedList; +import java.util.List; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import org.apache.rocketmq.common.BoundaryType; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.SystemClock; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageExtBatch; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.remoting.protocol.body.HARuntimeInfo; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.ha.HAService; +import org.apache.rocketmq.store.hook.PutMessageHook; +import org.apache.rocketmq.store.hook.SendMessageBackHook; +import org.apache.rocketmq.store.logfile.MappedFile; +import org.apache.rocketmq.store.queue.ConsumeQueueInterface; +import org.apache.rocketmq.store.queue.ConsumeQueueStoreInterface; +import org.apache.rocketmq.store.stats.BrokerStatsManager; +import org.apache.rocketmq.store.timer.TimerMessageStore; +import org.apache.rocketmq.store.util.PerfCounter; +import org.rocksdb.RocksDBException; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.sdk.metrics.InstrumentSelector; +import io.opentelemetry.sdk.metrics.ViewBuilder; /** * This class defines contracting interfaces to implement, allowing third-party vendor to use customized message store. @@ -51,6 +75,27 @@ public interface MessageStore { */ void destroy(); + /** + * Store a message into store in async manner, the processor can process the next request rather than wait for + * result when result is completed, notify the client in async manner + * + * @param msg MessageInstance to store + * @return a CompletableFuture for the result of store operation + */ + default CompletableFuture asyncPutMessage(final MessageExtBrokerInner msg) { + return CompletableFuture.completedFuture(putMessage(msg)); + } + + /** + * Store a batch of messages in async manner + * + * @param messageExtBatch the message batch + * @return a CompletableFuture for the result of store operation + */ + default CompletableFuture asyncPutMessages(final MessageExtBatch messageExtBatch) { + return CompletableFuture.completedFuture(putMessages(messageExtBatch)); + } + /** * Store a message into store. * @@ -71,40 +116,101 @@ public interface MessageStore { * Query at most maxMsgNums messages belonging to topic at queueId starting * from given offset. Resulting messages will further be screened using provided message filter. * - * @param group Consumer group that launches this query. - * @param topic Topic to query. - * @param queueId Queue ID to query. - * @param offset Logical offset to start from. - * @param maxMsgNums Maximum count of messages to query. + * @param group Consumer group that launches this query. + * @param topic Topic to query. + * @param queueId Queue ID to query. + * @param offset Logical offset to start from. + * @param maxMsgNums Maximum count of messages to query. * @param messageFilter Message filter used to screen desired messages. * @return Matched messages. */ GetMessageResult getMessage(final String group, final String topic, final int queueId, final long offset, final int maxMsgNums, final MessageFilter messageFilter); + /** + * Asynchronous get message + * @see #getMessage(String, String, int, long, int, MessageFilter) getMessage + * + * @param group Consumer group that launches this query. + * @param topic Topic to query. + * @param queueId Queue ID to query. + * @param offset Logical offset to start from. + * @param maxMsgNums Maximum count of messages to query. + * @param messageFilter Message filter used to screen desired messages. + * @return Matched messages. + */ + CompletableFuture getMessageAsync(final String group, final String topic, final int queueId, + final long offset, final int maxMsgNums, final MessageFilter messageFilter); + + /** + * Query at most maxMsgNums messages belonging to topic at queueId starting + * from given offset. Resulting messages will further be screened using provided message filter. + * + * @param group Consumer group that launches this query. + * @param topic Topic to query. + * @param queueId Queue ID to query. + * @param offset Logical offset to start from. + * @param maxMsgNums Maximum count of messages to query. + * @param maxTotalMsgSize Maximum total msg size of the messages + * @param messageFilter Message filter used to screen desired messages. + * @return Matched messages. + */ + GetMessageResult getMessage(final String group, final String topic, final int queueId, + final long offset, final int maxMsgNums, final int maxTotalMsgSize, final MessageFilter messageFilter); + + /** + * Asynchronous get message + * @see #getMessage(String, String, int, long, int, int, MessageFilter) getMessage + * + * @param group Consumer group that launches this query. + * @param topic Topic to query. + * @param queueId Queue ID to query. + * @param offset Logical offset to start from. + * @param maxMsgNums Maximum count of messages to query. + * @param maxTotalMsgSize Maximum total msg size of the messages + * @param messageFilter Message filter used to screen desired messages. + * @return Matched messages. + */ + CompletableFuture getMessageAsync(final String group, final String topic, final int queueId, + final long offset, final int maxMsgNums, final int maxTotalMsgSize, final MessageFilter messageFilter); + /** * Get maximum offset of the topic queue. * - * @param topic Topic name. + * @param topic Topic name. * @param queueId Queue ID. * @return Maximum offset at present. */ long getMaxOffsetInQueue(final String topic, final int queueId); + /** + * Get maximum offset of the topic queue. + * + * @param topic Topic name. + * @param queueId Queue ID. + * @param committed return the max offset in ConsumeQueue if true, or the max offset in CommitLog if false + * @return Maximum offset at present. + */ + long getMaxOffsetInQueue(final String topic, final int queueId, final boolean committed); + /** * Get the minimum offset of the topic queue. * - * @param topic Topic name. + * @param topic Topic name. * @param queueId Queue ID. * @return Minimum offset at present. */ long getMinOffsetInQueue(final String topic, final int queueId); + TimerMessageStore getTimerMessageStore(); + + void setTimerMessageStore(TimerMessageStore timerMessageStore); + /** * Get the offset of the message in the commit log, which is also known as physical offset. * - * @param topic Topic of the message to lookup. - * @param queueId Queue ID. + * @param topic Topic of the message to lookup. + * @param queueId Queue ID. * @param consumeQueueOffset offset of consume queue. * @return physical offset. */ @@ -113,13 +219,24 @@ GetMessageResult getMessage(final String group, final String topic, final int qu /** * Look up the physical offset of the message whose store timestamp is as specified. * - * @param topic Topic of the message. - * @param queueId Queue ID. + * @param topic Topic of the message. + * @param queueId Queue ID. * @param timestamp Timestamp to look up. * @return physical offset which matches. */ long getOffsetInQueueByTime(final String topic, final int queueId, final long timestamp); + /** + * Look up the physical offset of the message whose store timestamp is as specified with specific boundaryType. + * + * @param topic Topic of the message. + * @param queueId Queue ID. + * @param timestamp Timestamp to look up. + * @param boundaryType Lower or Upper + * @return physical offset which matches. + */ + long getOffsetInQueueByTime(final String topic, final int queueId, final long timestamp, final BoundaryType boundaryType); + /** * Look up the message by given commit log offset. * @@ -128,6 +245,15 @@ GetMessageResult getMessage(final String group, final String topic, final int qu */ MessageExt lookMessageByOffset(final long commitLogOffset); + /** + * Look up the message by given commit log offset and size. + * + * @param commitLogOffset physical offset. + * @param size message size + * @return Message whose physical offset is as specified. + */ + MessageExt lookMessageByOffset(long commitLogOffset, int size); + /** * Get one message from the specified commit log offset. * @@ -140,7 +266,7 @@ GetMessageResult getMessage(final String group, final String topic, final int qu * Get one message from the specified commit log offset. * * @param commitLogOffset commit log offset. - * @param msgSize message size. + * @param msgSize message size. * @return wrapped result of the message. */ SelectMappedBufferResult selectOneMessageByOffset(final long commitLogOffset, final int msgSize); @@ -152,6 +278,8 @@ GetMessageResult getMessage(final String group, final String topic, final int qu */ String getRunningDataInfo(); + long getTimingMessageCount(String topic); + /** * Message store runtime information, which should generally contains various statistical information. * @@ -159,6 +287,12 @@ GetMessageResult getMessage(final String group, final String topic, final int qu */ HashMap getRuntimeInfo(); + /** + * HA runtime information + * @return runtime information of ha + */ + HARuntimeInfo getHARuntimeInfo(); + /** * Get the maximum commit log offset. * @@ -176,7 +310,7 @@ GetMessageResult getMessage(final String group, final String topic, final int qu /** * Get the store time of the earliest message in the given queue. * - * @param topic Topic of the messages to query. + * @param topic Topic of the messages to query. * @param queueId Queue ID to find. * @return store time of the earliest message. */ @@ -189,20 +323,40 @@ GetMessageResult getMessage(final String group, final String topic, final int qu */ long getEarliestMessageTime(); + /** + * Asynchronous get the store time of the earliest message in this store. + * @see #getEarliestMessageTime() getEarliestMessageTime + * + * @return timestamp of the earliest message in this store. + */ + CompletableFuture getEarliestMessageTimeAsync(final String topic, final int queueId); + /** * Get the store time of the message specified. * - * @param topic message topic. - * @param queueId queue ID. + * @param topic message topic. + * @param queueId queue ID. * @param consumeQueueOffset consume queue offset. * @return store timestamp of the message. */ long getMessageStoreTimeStamp(final String topic, final int queueId, final long consumeQueueOffset); + /** + * Asynchronous get the store time of the message specified. + * @see #getMessageStoreTimeStamp(String, int, long) getMessageStoreTimeStamp + * + * @param topic message topic. + * @param queueId queue ID. + * @param consumeQueueOffset consume queue offset. + * @return store timestamp of the message. + */ + CompletableFuture getMessageStoreTimeStampAsync(final String topic, final int queueId, + final long consumeQueueOffset); + /** * Get the total number of the messages in the specified queue. * - * @param topic Topic + * @param topic Topic * @param queueId Queue ID. * @return total number. */ @@ -216,14 +370,25 @@ GetMessageResult getMessage(final String group, final String topic, final int qu */ SelectMappedBufferResult getCommitLogData(final long offset); + /** + * Get the raw commit log data starting from the given offset, across multiple mapped files. + * + * @param offset starting offset. + * @param size size of data to get + * @return commit log data. + */ + List getBulkCommitLogData(final long offset, final int size); + /** * Append data to commit log. * * @param startOffset starting offset. - * @param data data to append. + * @param data data to append. + * @param dataStart the start index of data array + * @param dataLength the length of data array * @return true if success; false otherwise. */ - boolean appendToCommitLog(final long startOffset, final byte[] data); + boolean appendToCommitLog(final long startOffset, final byte[] data, int dataStart, int dataLength); /** * Execute file deletion manually. @@ -233,15 +398,28 @@ GetMessageResult getMessage(final String group, final String topic, final int qu /** * Query messages by given key. * - * @param topic topic of the message. - * @param key message key. + * @param topic topic of the message. + * @param key message key. * @param maxNum maximum number of the messages possible. - * @param begin begin timestamp. - * @param end end timestamp. + * @param begin begin timestamp. + * @param end end timestamp. */ QueryMessageResult queryMessage(final String topic, final String key, final int maxNum, final long begin, final long end); + /** + * Asynchronous query messages by given key. + * @see #queryMessage(String, String, int, long, long) queryMessage + * + * @param topic topic of the message. + * @param key message key. + * @param maxNum maximum number of the messages possible. + * @param begin begin timestamp. + * @param end end timestamp. + */ + CompletableFuture queryMessageAsync(final String topic, final String key, final int maxNum, + final long begin, final long end); + /** * Update HA master address. * @@ -249,6 +427,13 @@ QueryMessageResult queryMessage(final String topic, final String key, final int */ void updateHaMasterAddress(final String newAddr); + /** + * Update master address. + * + * @param newAddr new address. + */ + void updateMasterAddress(final String newAddr); + /** * Return how much the slave falls behind. * @@ -264,12 +449,21 @@ QueryMessageResult queryMessage(final String topic, final String key, final int long now(); /** - * Clean unused topics. + * Delete topic's consume queue file and unused stats. + * This interface allows user delete system topic. + * + * @param deleteTopics unused topic name set + * @return the number of the topics which has been deleted. + */ + int deleteTopics(final Set deleteTopics); + + /** + * Clean unused topics which not in retain topic name set. * - * @param topics all valid topics. + * @param retainTopics all valid topics. * @return number of the topics deleted. */ - int cleanUnusedTopic(final Set topics); + int cleanUnusedTopic(final Set retainTopics); /** * Clean expired consume queues. @@ -279,13 +473,35 @@ QueryMessageResult queryMessage(final String topic, final String key, final int /** * Check if the given message has been swapped out of the memory. * - * @param topic topic. - * @param queueId queue ID. + * @param topic topic. + * @param queueId queue ID. * @param consumeOffset consume queue offset. * @return true if the message is no longer in memory; false otherwise. + * @deprecated As of RIP-57, replaced by {@link #checkInMemByConsumeOffset(String, int, long, int)}, see this issue for more details */ + @Deprecated boolean checkInDiskByConsumeOffset(final String topic, final int queueId, long consumeOffset); + /** + * Check if the given message is in the page cache. + * + * @param topic topic. + * @param queueId queue ID. + * @param consumeOffset consume queue offset. + * @return true if the message is in page cache; false otherwise. + */ + boolean checkInMemByConsumeOffset(final String topic, final int queueId, long consumeOffset, int batchSize); + + /** + * Check if the given message is in store. + * + * @param topic topic. + * @param queueId queue ID. + * @param consumeOffset consume queue offset. + * @return true if the message is in store; false otherwise. + */ + boolean checkInStoreByConsumeOffset(final String topic, final int queueId, long consumeOffset); + /** * Get number of the bytes that have been stored in commit log and not yet dispatched to consume queue. * @@ -300,6 +516,13 @@ QueryMessageResult queryMessage(final String topic, final String key, final int */ long flush(); + /** + * Get the current flushed offset. + * + * @return flushed offset + */ + long getFlushedWhere(); + /** * Reset written offset. * @@ -323,7 +546,7 @@ QueryMessageResult queryMessage(final String topic, final String key, final int void setConfirmOffset(long phyOffset); /** - * Check if the operation system page cache is busy or not. + * Check if the operating system page cache is busy or not. * * @return true if the OS page cache is busy; false otherwise. */ @@ -351,11 +574,419 @@ QueryMessageResult queryMessage(final String topic, final String key, final int LinkedList getDispatcherList(); /** - * Get consume queue of the topic/queue. + * Add dispatcher. * - * @param topic Topic. + * @param dispatcher commit log dispatcher to add + */ + void addDispatcher(CommitLogDispatcher dispatcher); + + /** + * Get consume queue of the topic/queue. If consume queue not exist, will return null + * + * @param topic Topic. + * @param queueId Queue ID. + * @return Consume queue. + */ + ConsumeQueueInterface getConsumeQueue(String topic, int queueId); + + /** + * Get consume queue of the topic/queue. If consume queue not exist, will create one then return it. + * @param topic Topic. * @param queueId Queue ID. * @return Consume queue. */ - ConsumeQueue getConsumeQueue(String topic, int queueId); + ConsumeQueueInterface findConsumeQueue(String topic, int queueId); + + /** + * Get BrokerStatsManager of the messageStore. + * + * @return BrokerStatsManager. + */ + BrokerStatsManager getBrokerStatsManager(); + + /** + * Will be triggered when a new message is appended to commit log. + * + * @param msg the msg that is appended to commit log + * @param result append message result + * @param commitLogFile commit log file + */ + void onCommitLogAppend(MessageExtBrokerInner msg, AppendMessageResult result, MappedFile commitLogFile); + + /** + * Will be triggered when a new dispatch request is sent to message store. + * + * @param dispatchRequest dispatch request + * @param doDispatch do dispatch if true + * @param commitLogFile commit log file + * @param isRecover is from recover process + * @param isFileEnd if the dispatch request represents 'file end' + * @throws RocksDBException only in rocksdb mode + */ + void onCommitLogDispatch(DispatchRequest dispatchRequest, boolean doDispatch, MappedFile commitLogFile, + boolean isRecover, boolean isFileEnd) throws RocksDBException; + + /** + * Only used in rocksdb mode, because we build consumeQueue in batch(default 16 dispatchRequests) + * It will be triggered in two cases: + * @see org.apache.rocketmq.store.DefaultMessageStore.ReputMessageService#doReput + * @see CommitLog#recoverAbnormally + */ + void finishCommitLogDispatch(); + + /** + * Get the message store config + * + * @return the message store config + */ + MessageStoreConfig getMessageStoreConfig(); + + /** + * Get the statistics service + * + * @return the statistics service + */ + StoreStatsService getStoreStatsService(); + + /** + * Get the store checkpoint component + * + * @return the checkpoint component + */ + StoreCheckpoint getStoreCheckpoint(); + + /** + * Get the system clock + * + * @return the system clock + */ + SystemClock getSystemClock(); + + /** + * Get the commit log + * + * @return the commit log + */ + CommitLog getCommitLog(); + + /** + * Get running flags + * + * @return running flags + */ + RunningFlags getRunningFlags(); + + /** + * Get the transient store pool + * + * @return the transient store pool + */ + TransientStorePool getTransientStorePool(); + + /** + * Get the HA service + * + * @return the HA service + */ + HAService getHaService(); + + /** + * Get the allocate-mappedFile service + * + * @return the allocate-mappedFile service + */ + AllocateMappedFileService getAllocateMappedFileService(); + + /** + * Truncate dirty logic files + * + * @param phyOffset physical offset + * @throws RocksDBException only in rocksdb mode + */ + void truncateDirtyLogicFiles(long phyOffset) throws RocksDBException; + + /** + * Unlock mappedFile + * + * @param unlockMappedFile the file that needs to be unlocked + */ + void unlockMappedFile(MappedFile unlockMappedFile); + + /** + * Get the perf counter component + * + * @return the perf counter component + */ + PerfCounter.Ticks getPerfCounter(); + + /** + * Get the queue store + * + * @return the queue store + */ + ConsumeQueueStoreInterface getQueueStore(); + + /** + * If 'sync disk flush' is configured in this message store + * + * @return yes if true, no if false + */ + boolean isSyncDiskFlush(); + + /** + * If this message store is sync master role + * + * @return yes if true, no if false + */ + boolean isSyncMaster(); + + /** + * Assign a message to queue offset. If there is a race condition, you need to lock/unlock this method + * yourself. + * + * @param msg message + * @throws RocksDBException + */ + void assignOffset(MessageExtBrokerInner msg) throws RocksDBException; + + /** + * Increase queue offset in memory table. If there is a race condition, you need to lock/unlock this method + * + * @param msg message + * @param messageNum message num + */ + void increaseOffset(MessageExtBrokerInner msg, short messageNum); + + /** + * Get master broker message store in process in broker container + * + * @return + */ + MessageStore getMasterStoreInProcess(); + + /** + * Set master broker message store in process + * + * @param masterStoreInProcess + */ + void setMasterStoreInProcess(MessageStore masterStoreInProcess); + + /** + * Use FileChannel to get data + * + * @param offset + * @param size + * @param byteBuffer + * @return + */ + boolean getData(long offset, int size, ByteBuffer byteBuffer); + + /** + * Set the number of alive replicas in group. + * + * @param aliveReplicaNums number of alive replicas + */ + void setAliveReplicaNumInGroup(int aliveReplicaNums); + + /** + * Get the number of alive replicas in group. + * + * @return number of alive replicas + */ + int getAliveReplicaNumInGroup(); + + /** + * Wake up AutoRecoverHAClient to start HA connection. + */ + void wakeupHAClient(); + + /** + * Get master flushed offset. + * + * @return master flushed offset + */ + long getMasterFlushedOffset(); + + /** + * Get broker init max offset. + * + * @return broker max offset in startup + */ + long getBrokerInitMaxOffset(); + + /** + * Set master flushed offset. + * + * @param masterFlushedOffset master flushed offset + */ + void setMasterFlushedOffset(long masterFlushedOffset); + + /** + * Set broker init max offset. + * + * @param brokerInitMaxOffset broker init max offset + */ + void setBrokerInitMaxOffset(long brokerInitMaxOffset); + + /** + * Calculate the checksum of a certain range of data. + * + * @param from begin offset + * @param to end offset + * @return checksum + */ + byte[] calcDeltaChecksum(long from, long to); + + /** + * Truncate commitLog and consume queue to certain offset. + * + * @param offsetToTruncate offset to truncate + * @return true if truncate succeed, false otherwise + * @throws RocksDBException only in rocksdb mode + */ + boolean truncateFiles(long offsetToTruncate) throws RocksDBException; + + /** + * Check if the offset is aligned with one message. + * + * @param offset offset to check + * @return true if aligned, false otherwise + */ + boolean isOffsetAligned(long offset); + + /** + * Get put message hook list + * + * @return List of PutMessageHook + */ + List getPutMessageHookList(); + + /** + * Set send message back hook + * + * @param sendMessageBackHook + */ + void setSendMessageBackHook(SendMessageBackHook sendMessageBackHook); + + /** + * Get send message back hook + * + * @return SendMessageBackHook + */ + SendMessageBackHook getSendMessageBackHook(); + + //The following interfaces are used for duplication mode + + /** + * Get last mapped file and return lase file first Offset + * + * @return lastMappedFile first Offset + */ + long getLastFileFromOffset(); + + /** + * Get last mapped file + * + * @param startOffset + * @return true when get the last mapped file, false when get null + */ + boolean getLastMappedFile(long startOffset); + + /** + * Set physical offset + * + * @param phyOffset + */ + void setPhysicalOffset(long phyOffset); + + /** + * Return whether mapped file is empty + * + * @return whether mapped file is empty + */ + boolean isMappedFilesEmpty(); + + /** + * Get state machine version + * + * @return state machine version + */ + long getStateMachineVersion(); + + /** + * Check message and return size + * + * @param byteBuffer + * @param checkCRC + * @param checkDupInfo + * @param readBody + * @return DispatchRequest + */ + DispatchRequest checkMessageAndReturnSize(final ByteBuffer byteBuffer, final boolean checkCRC, + final boolean checkDupInfo, final boolean readBody); + + /** + * Get remain transientStoreBuffer numbers + * + * @return remain transientStoreBuffer numbers + */ + int remainTransientStoreBufferNumbs(); + + /** + * Get remain how many data to commit + * + * @return remain how many data to commit + */ + long remainHowManyDataToCommit(); + + /** + * Get remain how many data to flush + * + * @return remain how many data to flush + */ + long remainHowManyDataToFlush(); + + /** + * Get whether message store is shutdown + * + * @return whether shutdown + */ + boolean isShutdown(); + + /** + * Estimate number of messages, within [from, to], which match given filter + * + * @param topic Topic name + * @param queueId Queue ID + * @param from Lower boundary of the range, inclusive. + * @param to Upper boundary of the range, inclusive. + * @param filter The message filter. + * @return Estimate number of messages matching given filter. + */ + long estimateMessageCount(String topic, int queueId, long from, long to, MessageFilter filter); + + /** + * Get metrics view of store + * + * @return List of metrics selector and view pair + */ + List> getMetricsView(); + + /** + * Init store metrics + * + * @param meter opentelemetry meter + * @param attributesBuilderSupplier metrics attributes builder + */ + void initMetrics(Meter meter, Supplier attributesBuilderSupplier); + + /** + * Recover topic queue table + */ + void recoverTopicQueueTable(); + + /** + * notify message arrive if necessary + */ + void notifyMessageArriveIfNecessary(DispatchRequest dispatchRequest); } diff --git a/store/src/main/java/org/apache/rocketmq/store/MultiDispatch.java b/store/src/main/java/org/apache/rocketmq/store/MultiDispatch.java new file mode 100644 index 00000000000..5bc587a8e03 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/MultiDispatch.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store; + +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; + +/** + * MultiDispatch for lmq, not-thread-safe + */ +public class MultiDispatch { + private final StringBuilder keyBuilder = new StringBuilder(); + private final DefaultMessageStore messageStore; + private static final short VALUE_OF_EACH_INCREMENT = 1; + + public MultiDispatch(DefaultMessageStore messageStore) { + this.messageStore = messageStore; + } + + public String queueKey(String queueName, MessageExtBrokerInner msgInner) { + keyBuilder.delete(0, keyBuilder.length()); + keyBuilder.append(queueName); + keyBuilder.append('-'); + int queueId = msgInner.getQueueId(); + if (messageStore.getMessageStoreConfig().isEnableLmq() && MixAll.isLmq(queueName)) { + queueId = 0; + } + keyBuilder.append(queueId); + return keyBuilder.toString(); + } + + public void wrapMultiDispatch(final MessageExtBrokerInner msg) { + + String multiDispatchQueue = msg.getProperty(MessageConst.PROPERTY_INNER_MULTI_DISPATCH); + String[] queues = multiDispatchQueue.split(MixAll.MULTI_DISPATCH_QUEUE_SPLITTER); + Long[] queueOffsets = new Long[queues.length]; + if (messageStore.getMessageStoreConfig().isEnableLmq()) { + for (int i = 0; i < queues.length; i++) { + String key = queueKey(queues[i], msg); + if (MixAll.isLmq(key)) { + queueOffsets[i] = messageStore.getQueueStore().getLmqQueueOffset(key); + } + } + } + MessageAccessor.putProperty(msg, MessageConst.PROPERTY_INNER_MULTI_QUEUE_OFFSET, + StringUtils.join(queueOffsets, MixAll.MULTI_DISPATCH_QUEUE_SPLITTER)); + msg.removeWaitStorePropertyString(); + } + + public void updateMultiQueueOffset(final MessageExtBrokerInner msgInner) { + String multiDispatchQueue = msgInner.getProperty(MessageConst.PROPERTY_INNER_MULTI_DISPATCH); + String[] queues = multiDispatchQueue.split(MixAll.MULTI_DISPATCH_QUEUE_SPLITTER); + for (String queue : queues) { + String key = queueKey(queue, msgInner); + if (messageStore.getMessageStoreConfig().isEnableLmq() && MixAll.isLmq(key)) { + messageStore.getQueueStore().increaseLmqOffset(key, VALUE_OF_EACH_INCREMENT); + } + } + } +} \ No newline at end of file diff --git a/store/src/main/java/org/apache/rocketmq/store/MultiPathMappedFileQueue.java b/store/src/main/java/org/apache/rocketmq/store/MultiPathMappedFileQueue.java new file mode 100644 index 00000000000..8ff050dfe3b --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/MultiPathMappedFileQueue.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Supplier; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.logfile.MappedFile; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class MultiPathMappedFileQueue extends MappedFileQueue { + + private final MessageStoreConfig config; + private final Supplier> fullStorePathsSupplier; + + public MultiPathMappedFileQueue(MessageStoreConfig messageStoreConfig, int mappedFileSize, + AllocateMappedFileService allocateMappedFileService, + Supplier> fullStorePathsSupplier) { + super(messageStoreConfig.getStorePathCommitLog(), mappedFileSize, allocateMappedFileService); + this.config = messageStoreConfig; + this.fullStorePathsSupplier = fullStorePathsSupplier; + } + + private Set getPaths() { + String[] paths = config.getStorePathCommitLog().trim().split(MixAll.MULTI_PATH_SPLITTER); + return new HashSet<>(Arrays.asList(paths)); + } + + private Set getReadonlyPaths() { + String pathStr = config.getReadOnlyCommitLogStorePaths(); + if (StringUtils.isBlank(pathStr)) { + return Collections.emptySet(); + } + String[] paths = pathStr.trim().split(MixAll.MULTI_PATH_SPLITTER); + return new HashSet<>(Arrays.asList(paths)); + } + + @Override + public boolean load() { + Set storePathSet = getPaths(); + storePathSet.addAll(getReadonlyPaths()); + + List files = new ArrayList<>(); + for (String path : storePathSet) { + File dir = new File(path); + File[] ls = dir.listFiles(); + if (ls != null) { + Collections.addAll(files, ls); + } + } + + return doLoad(files); + } + + @Override + public MappedFile tryCreateMappedFile(long createOffset) { + long fileIdx = createOffset / this.mappedFileSize; + Set storePath = getPaths(); + Set readonlyPathSet = getReadonlyPaths(); + Set fullStorePaths = + fullStorePathsSupplier == null ? Collections.emptySet() : fullStorePathsSupplier.get(); + + + HashSet availableStorePath = new HashSet<>(storePath); + //do not create file in readonly store path. + availableStorePath.removeAll(readonlyPathSet); + + //do not create file is space is nearly full. + availableStorePath.removeAll(fullStorePaths); + + //if no store path left, fall back to writable store path. + if (availableStorePath.isEmpty()) { + availableStorePath = new HashSet<>(storePath); + availableStorePath.removeAll(readonlyPathSet); + } + + String[] paths = availableStorePath.toArray(new String[]{}); + Arrays.sort(paths); + String nextFilePath = paths[(int) (fileIdx % paths.length)] + File.separator + + UtilAll.offset2FileName(createOffset); + String nextNextFilePath = paths[(int) ((fileIdx + 1) % paths.length)] + File.separator + + UtilAll.offset2FileName(createOffset + this.mappedFileSize); + return doCreateMappedFile(nextFilePath, nextNextFilePath); + } + + @Override + public void destroy() { + for (MappedFile mf : this.mappedFiles) { + mf.destroy(1000 * 3); + } + this.mappedFiles.clear(); + this.setFlushedWhere(0); + + Set storePathSet = getPaths(); + storePathSet.addAll(getReadonlyPaths()); + + for (String path : storePathSet) { + File file = new File(path); + if (file.isDirectory()) { + file.delete(); + } + } + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/PutMessageContext.java b/store/src/main/java/org/apache/rocketmq/store/PutMessageContext.java new file mode 100644 index 00000000000..bf8832d9a22 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/PutMessageContext.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store; + +public class PutMessageContext { + private String topicQueueTableKey; + private long[] phyPos; + private int batchSize; + + public PutMessageContext(String topicQueueTableKey) { + this.topicQueueTableKey = topicQueueTableKey; + } + + public String getTopicQueueTableKey() { + return topicQueueTableKey; + } + + public long[] getPhyPos() { + return phyPos; + } + + public void setPhyPos(long[] phyPos) { + this.phyPos = phyPos; + } + + public int getBatchSize() { + return batchSize; + } + + public void setBatchSize(int batchSize) { + this.batchSize = batchSize; + } +} \ No newline at end of file diff --git a/store/src/main/java/org/apache/rocketmq/store/PutMessageResult.java b/store/src/main/java/org/apache/rocketmq/store/PutMessageResult.java index e12cc0ca484..bcca6aee38a 100644 --- a/store/src/main/java/org/apache/rocketmq/store/PutMessageResult.java +++ b/store/src/main/java/org/apache/rocketmq/store/PutMessageResult.java @@ -19,14 +19,28 @@ public class PutMessageResult { private PutMessageStatus putMessageStatus; private AppendMessageResult appendMessageResult; + private boolean remotePut = false; public PutMessageResult(PutMessageStatus putMessageStatus, AppendMessageResult appendMessageResult) { this.putMessageStatus = putMessageStatus; this.appendMessageResult = appendMessageResult; } + public PutMessageResult(PutMessageStatus putMessageStatus, AppendMessageResult appendMessageResult, + boolean remotePut) { + this.putMessageStatus = putMessageStatus; + this.appendMessageResult = appendMessageResult; + this.remotePut = remotePut; + } + public boolean isOk() { - return this.appendMessageResult != null && this.appendMessageResult.isOk(); + if (remotePut) { + return putMessageStatus == PutMessageStatus.PUT_OK || putMessageStatus == PutMessageStatus.FLUSH_DISK_TIMEOUT + || putMessageStatus == PutMessageStatus.FLUSH_SLAVE_TIMEOUT || putMessageStatus == PutMessageStatus.SLAVE_NOT_AVAILABLE; + } else { + return this.appendMessageResult != null && this.appendMessageResult.isOk(); + } + } public AppendMessageResult getAppendMessageResult() { @@ -45,10 +59,18 @@ public void setPutMessageStatus(PutMessageStatus putMessageStatus) { this.putMessageStatus = putMessageStatus; } + public boolean isRemotePut() { + return remotePut; + } + + public void setRemotePut(boolean remotePut) { + this.remotePut = remotePut; + } + @Override public String toString() { return "PutMessageResult [putMessageStatus=" + putMessageStatus + ", appendMessageResult=" - + appendMessageResult + "]"; + + appendMessageResult + ", remotePut=" + remotePut + "]"; } } diff --git a/store/src/main/java/org/apache/rocketmq/store/PutMessageStatus.java b/store/src/main/java/org/apache/rocketmq/store/PutMessageStatus.java index e1631d7d05c..55afd37320d 100644 --- a/store/src/main/java/org/apache/rocketmq/store/PutMessageStatus.java +++ b/store/src/main/java/org/apache/rocketmq/store/PutMessageStatus.java @@ -22,9 +22,15 @@ public enum PutMessageStatus { FLUSH_SLAVE_TIMEOUT, SLAVE_NOT_AVAILABLE, SERVICE_NOT_AVAILABLE, - CREATE_MAPEDFILE_FAILED, + CREATE_MAPPED_FILE_FAILED, MESSAGE_ILLEGAL, PROPERTIES_SIZE_EXCEEDED, - OS_PAGECACHE_BUSY, + OS_PAGE_CACHE_BUSY, UNKNOWN_ERROR, + IN_SYNC_REPLICAS_NOT_ENOUGH, + PUT_TO_REMOTE_BROKER_FAIL, + LMQ_CONSUME_QUEUE_NUM_EXCEEDED, + WHEEL_TIMER_FLOW_CONTROL, + WHEEL_TIMER_MSG_ILLEGAL, + WHEEL_TIMER_NOT_ENABLE } diff --git a/store/src/main/java/org/apache/rocketmq/store/QueryMessageResult.java b/store/src/main/java/org/apache/rocketmq/store/QueryMessageResult.java index a7a68508162..fbcbc05acf1 100644 --- a/store/src/main/java/org/apache/rocketmq/store/QueryMessageResult.java +++ b/store/src/main/java/org/apache/rocketmq/store/QueryMessageResult.java @@ -23,9 +23,9 @@ public class QueryMessageResult { private final List messageMapedList = - new ArrayList(100); + new ArrayList<>(100); - private final List messageBufferList = new ArrayList(100); + private final List messageBufferList = new ArrayList<>(100); private long indexLastUpdateTimestamp; private long indexLastUpdatePhyoffset; @@ -66,4 +66,8 @@ public List getMessageBufferList() { public int getBufferTotalSize() { return bufferTotalSize; } + + public List getMessageMapedList() { + return messageMapedList; + } } diff --git a/store/src/main/java/org/apache/rocketmq/store/RocksDBMessageStore.java b/store/src/main/java/org/apache/rocketmq/store/RocksDBMessageStore.java new file mode 100644 index 00000000000..6141b778bf7 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/RocksDBMessageStore.java @@ -0,0 +1,181 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store; + +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Supplier; + +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.metrics.Meter; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.config.StorePathConfigHelper; +import org.apache.rocketmq.store.metrics.DefaultStoreMetricsManager; +import org.apache.rocketmq.store.metrics.RocksDBStoreMetricsManager; +import org.apache.rocketmq.store.queue.ConsumeQueueInterface; +import org.apache.rocketmq.store.queue.ConsumeQueueStoreInterface; +import org.apache.rocketmq.store.queue.RocksDBConsumeQueue; +import org.apache.rocketmq.store.queue.RocksDBConsumeQueueStore; +import org.apache.rocketmq.store.stats.BrokerStatsManager; +import org.rocksdb.RocksDBException; + +public class RocksDBMessageStore extends DefaultMessageStore { + + public RocksDBMessageStore(final MessageStoreConfig messageStoreConfig, final BrokerStatsManager brokerStatsManager, + final MessageArrivingListener messageArrivingListener, final BrokerConfig brokerConfig, final ConcurrentMap topicConfigTable) throws + IOException { + super(messageStoreConfig, brokerStatsManager, messageArrivingListener, brokerConfig, topicConfigTable); + notifyMessageArriveInBatch = true; + } + + @Override + public ConsumeQueueStoreInterface createConsumeQueueStore() { + return new RocksDBConsumeQueueStore(this); + } + + @Override + public CleanConsumeQueueService createCleanConsumeQueueService() { + return new RocksDBCleanConsumeQueueService(); + } + + @Override + public FlushConsumeQueueService createFlushConsumeQueueService() { + return new RocksDBFlushConsumeQueueService(); + } + + @Override + public CorrectLogicOffsetService createCorrectLogicOffsetService() { + return new RocksDBCorrectLogicOffsetService(); + } + + /** + * Try to set topicQueueTable = new HashMap<>(), otherwise it will cause bug when broker role changes. + * And unlike method in DefaultMessageStore, we don't need to really recover topic queue table advance, + * because we can recover topic queue table from rocksdb when we need to use it. + * @see RocksDBConsumeQueue#assignQueueOffset + */ + @Override + public void recoverTopicQueueTable() { + this.consumeQueueStore.setTopicQueueTable(new ConcurrentHashMap<>()); + } + + @Override + public void finishCommitLogDispatch() { + try { + putMessagePositionInfo(null); + } catch (RocksDBException e) { + ERROR_LOG.info("try to finish commitlog dispatch error.", e); + } + } + + @Override + public ConsumeQueueInterface getConsumeQueue(String topic, int queueId) { + return findConsumeQueue(topic, queueId); + } + + class RocksDBCleanConsumeQueueService extends CleanConsumeQueueService { + private final double diskSpaceWarningLevelRatio = + Double.parseDouble(System.getProperty("rocketmq.broker.diskSpaceWarningLevelRatio", "0.90")); + + private final double diskSpaceCleanForciblyRatio = + Double.parseDouble(System.getProperty("rocketmq.broker.diskSpaceCleanForciblyRatio", "0.85")); + + @Override + protected void deleteExpiredFiles() { + + long minOffset = RocksDBMessageStore.this.commitLog.getMinOffset(); + if (minOffset > this.lastPhysicalMinOffset) { + this.lastPhysicalMinOffset = minOffset; + + boolean spaceFull = isSpaceToDelete(); + boolean timeUp = cleanCommitLogService.isTimeToDelete(); + if (spaceFull || timeUp) { + RocksDBMessageStore.this.consumeQueueStore.cleanExpired(minOffset); + } + + RocksDBMessageStore.this.indexService.deleteExpiredFile(minOffset); + } + } + + private boolean isSpaceToDelete() { + double ratio = RocksDBMessageStore.this.getMessageStoreConfig().getDiskMaxUsedSpaceRatio() / 100.0; + + String storePathLogics = StorePathConfigHelper + .getStorePathConsumeQueue(RocksDBMessageStore.this.getMessageStoreConfig().getStorePathRootDir()); + double logicsRatio = UtilAll.getDiskPartitionSpaceUsedPercent(storePathLogics); + if (logicsRatio > diskSpaceWarningLevelRatio) { + boolean diskOk = RocksDBMessageStore.this.runningFlags.getAndMakeLogicDiskFull(); + if (diskOk) { + RocksDBMessageStore.LOGGER.error("logics disk maybe full soon " + logicsRatio + ", so mark disk full"); + } + } else if (logicsRatio > diskSpaceCleanForciblyRatio) { + } else { + boolean diskOk = RocksDBMessageStore.this.runningFlags.getAndMakeLogicDiskOK(); + if (!diskOk) { + RocksDBMessageStore.LOGGER.info("logics disk space OK " + logicsRatio + ", so mark disk ok"); + } + } + + if (logicsRatio < 0 || logicsRatio > ratio) { + RocksDBMessageStore.LOGGER.info("logics disk maybe full soon, so reclaim space, " + logicsRatio); + return true; + } + + return false; + } + } + + class RocksDBFlushConsumeQueueService extends FlushConsumeQueueService { + /** + * There is no need to flush consume queue, + * we put all consume queues in RocksDBConsumeQueueStore, + * it depends on rocksdb to flush consume queue to disk(sorted string table), + * we even don't flush WAL of consume store, since we think it can recover consume queue from commitlog. + */ + @Override + public void run() { + + } + } + + class RocksDBCorrectLogicOffsetService extends CorrectLogicOffsetService { + /** + * There is no need to correct min offset of consume queue, we already fix this problem. + * @see org.apache.rocketmq.store.queue.RocksDBConsumeQueueOffsetTable#getMinCqOffset + */ + public void run() { + + } + } + + @Override + public long estimateMessageCount(String topic, int queueId, long from, long to, MessageFilter filter) { + // todo + return 0; + } + + @Override + public void initMetrics(Meter meter, Supplier attributesBuilderSupplier) { + DefaultStoreMetricsManager.init(meter, attributesBuilderSupplier, this); + // Also add some metrics for rocksdb's monitoring. + RocksDBStoreMetricsManager.init(meter, attributesBuilderSupplier, this); + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/RunningFlags.java b/store/src/main/java/org/apache/rocketmq/store/RunningFlags.java index 7ff11a282a3..88b398a77e6 100644 --- a/store/src/main/java/org/apache/rocketmq/store/RunningFlags.java +++ b/store/src/main/java/org/apache/rocketmq/store/RunningFlags.java @@ -28,6 +28,10 @@ public class RunningFlags { private static final int DISK_FULL_BIT = 1 << 4; + private static final int FENCED_BIT = 1 << 5; + + private static final int LOGIC_DISK_FULL_BIT = 1 << 6; + private volatile int flagBits = 0; public RunningFlags() { @@ -46,11 +50,11 @@ public boolean getAndMakeReadable() { } public boolean isReadable() { - if ((this.flagBits & NOT_READABLE_BIT) == 0) { - return true; - } + return (this.flagBits & NOT_READABLE_BIT) == 0; + } - return false; + public boolean isFenced() { + return (this.flagBits & FENCED_BIT) != 0; } public boolean getAndMakeNotReadable() { @@ -61,6 +65,10 @@ public boolean getAndMakeNotReadable() { return result; } + public void clearLogicsQueueError() { + this.flagBits &= ~WRITE_LOGICS_QUEUE_ERROR_BIT; + } + public boolean getAndMakeWriteable() { boolean result = this.isWriteable(); if (!result) { @@ -70,7 +78,7 @@ public boolean getAndMakeWriteable() { } public boolean isWriteable() { - if ((this.flagBits & (NOT_WRITEABLE_BIT | WRITE_LOGICS_QUEUE_ERROR_BIT | DISK_FULL_BIT | WRITE_INDEX_FILE_ERROR_BIT)) == 0) { + if ((this.flagBits & (NOT_WRITEABLE_BIT | WRITE_LOGICS_QUEUE_ERROR_BIT | DISK_FULL_BIT | WRITE_INDEX_FILE_ERROR_BIT | FENCED_BIT | LOGIC_DISK_FULL_BIT)) == 0) { return true; } @@ -79,7 +87,7 @@ public boolean isWriteable() { //for consume queue, just ignore the DISK_FULL_BIT public boolean isCQWriteable() { - if ((this.flagBits & (NOT_WRITEABLE_BIT | WRITE_LOGICS_QUEUE_ERROR_BIT | WRITE_INDEX_FILE_ERROR_BIT)) == 0) { + if ((this.flagBits & (NOT_WRITEABLE_BIT | WRITE_LOGICS_QUEUE_ERROR_BIT | WRITE_INDEX_FILE_ERROR_BIT | LOGIC_DISK_FULL_BIT)) == 0) { return true; } @@ -98,6 +106,14 @@ public void makeLogicsQueueError() { this.flagBits |= WRITE_LOGICS_QUEUE_ERROR_BIT; } + public void makeFenced(boolean fenced) { + if (fenced) { + this.flagBits |= FENCED_BIT; + } else { + this.flagBits &= ~FENCED_BIT; + } + } + public boolean isLogicsQueueError() { if ((this.flagBits & WRITE_LOGICS_QUEUE_ERROR_BIT) == WRITE_LOGICS_QUEUE_ERROR_BIT) { return true; @@ -129,4 +145,16 @@ public boolean getAndMakeDiskOK() { this.flagBits &= ~DISK_FULL_BIT; return result; } + + public boolean getAndMakeLogicDiskFull() { + boolean result = !((this.flagBits & LOGIC_DISK_FULL_BIT) == LOGIC_DISK_FULL_BIT); + this.flagBits |= LOGIC_DISK_FULL_BIT; + return result; + } + + public boolean getAndMakeLogicDiskOK() { + boolean result = !((this.flagBits & LOGIC_DISK_FULL_BIT) == LOGIC_DISK_FULL_BIT); + this.flagBits &= ~LOGIC_DISK_FULL_BIT; + return result; + } } diff --git a/store/src/main/java/org/apache/rocketmq/store/SelectMappedBufferResult.java b/store/src/main/java/org/apache/rocketmq/store/SelectMappedBufferResult.java index 7a17114c8f3..5c38cfe92a9 100644 --- a/store/src/main/java/org/apache/rocketmq/store/SelectMappedBufferResult.java +++ b/store/src/main/java/org/apache/rocketmq/store/SelectMappedBufferResult.java @@ -17,6 +17,7 @@ package org.apache.rocketmq.store; import java.nio.ByteBuffer; +import org.apache.rocketmq.store.logfile.MappedFile; public class SelectMappedBufferResult { @@ -26,7 +27,9 @@ public class SelectMappedBufferResult { private int size; - private MappedFile mappedFile; + protected MappedFile mappedFile; + + private boolean isInCache = true; public SelectMappedBufferResult(long startOffset, ByteBuffer byteBuffer, int size, MappedFile mappedFile) { this.startOffset = startOffset; @@ -52,21 +55,33 @@ public MappedFile getMappedFile() { return mappedFile; } -// @Override -// protected void finalize() { -// if (this.mappedFile != null) { -// this.release(); -// } -// } - public synchronized void release() { if (this.mappedFile != null) { this.mappedFile.release(); this.mappedFile = null; } } + public synchronized boolean hasReleased() { + return this.mappedFile == null; + } public long getStartOffset() { return startOffset; } + + public boolean isInMem() { + if (mappedFile == null) { + return true; + } + long pos = startOffset - mappedFile.getFileFromOffset(); + return mappedFile.isLoaded(pos, size); + } + + public boolean isInCache() { + return isInCache; + } + + public void setInCache(boolean inCache) { + isInCache = inCache; + } } diff --git a/store/src/main/java/org/apache/rocketmq/store/SelectMappedFileResult.java b/store/src/main/java/org/apache/rocketmq/store/SelectMappedFileResult.java new file mode 100644 index 00000000000..9655f281cc3 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/SelectMappedFileResult.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store; + +import org.apache.rocketmq.store.logfile.MappedFile; + +public class SelectMappedFileResult { + + protected int size; + + protected MappedFile mappedFile; + + public SelectMappedFileResult(int size, MappedFile mappedFile) { + this.size = size; + this.mappedFile = mappedFile; + } + + public int getSize() { + return size; + } + + public void setSize(int size) { + this.size = size; + } + + public MappedFile getMappedFile() { + return mappedFile; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/StoreCheckpoint.java b/store/src/main/java/org/apache/rocketmq/store/StoreCheckpoint.java index c5981c6d476..1e2504a2be0 100644 --- a/store/src/main/java/org/apache/rocketmq/store/StoreCheckpoint.java +++ b/store/src/main/java/org/apache/rocketmq/store/StoreCheckpoint.java @@ -24,8 +24,9 @@ import java.nio.channels.FileChannel.MapMode; import org.apache.rocketmq.common.UtilAll; import org.apache.rocketmq.common.constant.LoggerName; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.logfile.DefaultMappedFile; public class StoreCheckpoint { private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); @@ -35,21 +36,25 @@ public class StoreCheckpoint { private volatile long physicMsgTimestamp = 0; private volatile long logicsMsgTimestamp = 0; private volatile long indexMsgTimestamp = 0; + private volatile long masterFlushedOffset = 0; + private volatile long confirmPhyOffset = 0; public StoreCheckpoint(final String scpPath) throws IOException { File file = new File(scpPath); - MappedFile.ensureDirOK(file.getParent()); + UtilAll.ensureDirOK(file.getParent()); boolean fileExists = file.exists(); this.randomAccessFile = new RandomAccessFile(file, "rw"); this.fileChannel = this.randomAccessFile.getChannel(); - this.mappedByteBuffer = fileChannel.map(MapMode.READ_WRITE, 0, MappedFile.OS_PAGE_SIZE); + this.mappedByteBuffer = fileChannel.map(MapMode.READ_WRITE, 0, DefaultMappedFile.OS_PAGE_SIZE); if (fileExists) { log.info("store checkpoint file exists, " + scpPath); this.physicMsgTimestamp = this.mappedByteBuffer.getLong(0); this.logicsMsgTimestamp = this.mappedByteBuffer.getLong(8); this.indexMsgTimestamp = this.mappedByteBuffer.getLong(16); + this.masterFlushedOffset = this.mappedByteBuffer.getLong(24); + this.confirmPhyOffset = this.mappedByteBuffer.getLong(32); log.info("store checkpoint file physicMsgTimestamp " + this.physicMsgTimestamp + ", " + UtilAll.timeMillisToHumanString(this.physicMsgTimestamp)); @@ -57,6 +62,8 @@ public StoreCheckpoint(final String scpPath) throws IOException { + UtilAll.timeMillisToHumanString(this.logicsMsgTimestamp)); log.info("store checkpoint file indexMsgTimestamp " + this.indexMsgTimestamp + ", " + UtilAll.timeMillisToHumanString(this.indexMsgTimestamp)); + log.info("store checkpoint file masterFlushedOffset " + this.masterFlushedOffset); + log.info("store checkpoint file confirmPhyOffset " + this.confirmPhyOffset); } else { log.info("store checkpoint file not exists, " + scpPath); } @@ -66,7 +73,7 @@ public void shutdown() { this.flush(); // unmap mappedByteBuffer - MappedFile.clean(this.mappedByteBuffer); + UtilAll.cleanBuffer(this.mappedByteBuffer); try { this.fileChannel.close(); @@ -79,6 +86,8 @@ public void flush() { this.mappedByteBuffer.putLong(0, this.physicMsgTimestamp); this.mappedByteBuffer.putLong(8, this.logicsMsgTimestamp); this.mappedByteBuffer.putLong(16, this.indexMsgTimestamp); + this.mappedByteBuffer.putLong(24, this.masterFlushedOffset); + this.mappedByteBuffer.putLong(32, this.confirmPhyOffset); this.mappedByteBuffer.force(); } @@ -98,6 +107,14 @@ public void setLogicsMsgTimestamp(long logicsMsgTimestamp) { this.logicsMsgTimestamp = logicsMsgTimestamp; } + public long getConfirmPhyOffset() { + return confirmPhyOffset; + } + + public void setConfirmPhyOffset(long confirmPhyOffset) { + this.confirmPhyOffset = confirmPhyOffset; + } + public long getMinTimestampIndex() { return Math.min(this.getMinTimestamp(), this.indexMsgTimestamp); } @@ -106,8 +123,9 @@ public long getMinTimestamp() { long min = Math.min(this.physicMsgTimestamp, this.logicsMsgTimestamp); min -= 1000 * 3; - if (min < 0) + if (min < 0) { min = 0; + } return min; } @@ -120,4 +138,11 @@ public void setIndexMsgTimestamp(long indexMsgTimestamp) { this.indexMsgTimestamp = indexMsgTimestamp; } + public long getMasterFlushedOffset() { + return masterFlushedOffset; + } + + public void setMasterFlushedOffset(long masterFlushedOffset) { + this.masterFlushedOffset = masterFlushedOffset; + } } diff --git a/store/src/main/java/org/apache/rocketmq/store/StoreStatsService.java b/store/src/main/java/org/apache/rocketmq/store/StoreStatsService.java index 586947ce6fc..1969b146aa6 100644 --- a/store/src/main/java/org/apache/rocketmq/store/StoreStatsService.java +++ b/store/src/main/java/org/apache/rocketmq/store/StoreStatsService.java @@ -17,16 +17,22 @@ package org.apache.rocketmq.store; import java.text.MessageFormat; +import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; +import java.util.List; import java.util.Map; +import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.LongAdder; import java.util.concurrent.locks.ReentrantLock; +import org.apache.rocketmq.common.BrokerIdentity; import org.apache.rocketmq.common.ServiceThread; import org.apache.rocketmq.common.constant.LoggerName; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; public class StoreStatsService extends ServiceThread { private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); @@ -38,52 +44,123 @@ public class StoreStatsService extends ServiceThread { "[<=0ms]", "[0~10ms]", "[10~50ms]", "[50~100ms]", "[100~200ms]", "[200~500ms]", "[500ms~1s]", "[1~2s]", "[2~3s]", "[3~4s]", "[4~5s]", "[5~10s]", "[10s~]", }; + //The rule to define buckets + private static final Map PUT_MESSAGE_ENTIRE_TIME_BUCKETS = new TreeMap<>(); + //buckets + private TreeMap buckets = new TreeMap<>(); + private Map lastBuckets = new TreeMap<>(); + private static int printTPSInterval = 60 * 1; - private final AtomicLong putMessageFailedTimes = new AtomicLong(0); + private final LongAdder putMessageFailedTimes = new LongAdder(); - private final Map putMessageTopicTimesTotal = - new ConcurrentHashMap(128); - private final Map putMessageTopicSizeTotal = - new ConcurrentHashMap(128); + private final ConcurrentMap putMessageTopicTimesTotal = + new ConcurrentHashMap<>(128); + private final ConcurrentMap putMessageTopicSizeTotal = + new ConcurrentHashMap<>(128); - private final AtomicLong getMessageTimesTotalFound = new AtomicLong(0); - private final AtomicLong getMessageTransferedMsgCount = new AtomicLong(0); - private final AtomicLong getMessageTimesTotalMiss = new AtomicLong(0); - private final LinkedList putTimesList = new LinkedList(); + private final LongAdder getMessageTimesTotalFound = new LongAdder(); + private final LongAdder getMessageTransferredMsgCount = new LongAdder(); + private final LongAdder getMessageTimesTotalMiss = new LongAdder(); + private final LinkedList putTimesList = new LinkedList<>(); - private final LinkedList getTimesFoundList = new LinkedList(); - private final LinkedList getTimesMissList = new LinkedList(); - private final LinkedList transferedMsgCountList = new LinkedList(); - private volatile AtomicLong[] putMessageDistributeTime; + private final LinkedList getTimesFoundList = new LinkedList<>(); + private final LinkedList getTimesMissList = new LinkedList<>(); + private final LinkedList transferredMsgCountList = new LinkedList<>(); + private volatile LongAdder[] putMessageDistributeTime; + private volatile LongAdder[] lastPutMessageDistributeTime; private long messageStoreBootTimestamp = System.currentTimeMillis(); private volatile long putMessageEntireTimeMax = 0; private volatile long getMessageEntireTimeMax = 0; // for putMessageEntireTimeMax - private ReentrantLock lockPut = new ReentrantLock(); + private ReentrantLock putLock = new ReentrantLock(); // for getMessageEntireTimeMax - private ReentrantLock lockGet = new ReentrantLock(); + private ReentrantLock getLock = new ReentrantLock(); private volatile long dispatchMaxBuffer = 0; - private ReentrantLock lockSampling = new ReentrantLock(); + private ReentrantLock samplingLock = new ReentrantLock(); private long lastPrintTimestamp = System.currentTimeMillis(); + private BrokerIdentity brokerIdentity; + + public StoreStatsService(BrokerIdentity brokerIdentity) { + this(); + this.brokerIdentity = brokerIdentity; + } + public StoreStatsService() { - this.initPutMessageDistributeTime(); + PUT_MESSAGE_ENTIRE_TIME_BUCKETS.put(1,20); //0-20 + PUT_MESSAGE_ENTIRE_TIME_BUCKETS.put(2,15); //20-50 + PUT_MESSAGE_ENTIRE_TIME_BUCKETS.put(5,10); //50-100 + PUT_MESSAGE_ENTIRE_TIME_BUCKETS.put(10,10); //100-200 + PUT_MESSAGE_ENTIRE_TIME_BUCKETS.put(50,6); //200-500 + PUT_MESSAGE_ENTIRE_TIME_BUCKETS.put(100,5); //500-1000 + PUT_MESSAGE_ENTIRE_TIME_BUCKETS.put(1000,9); //1s-10s + + this.resetPutMessageTimeBuckets(); + this.resetPutMessageDistributeTime(); + } + + private void resetPutMessageTimeBuckets() { + TreeMap nextBuckets = new TreeMap<>(); + AtomicLong index = new AtomicLong(0); + PUT_MESSAGE_ENTIRE_TIME_BUCKETS.forEach((interval, times) -> { + for (int i = 0; i < times; i++) { + nextBuckets.put(index.addAndGet(interval), new LongAdder()); + } + }); + nextBuckets.put(Long.MAX_VALUE, new LongAdder()); + + this.lastBuckets = this.buckets; + this.buckets = nextBuckets; } - private AtomicLong[] initPutMessageDistributeTime() { - AtomicLong[] next = new AtomicLong[13]; + public void incPutMessageEntireTime(long value) { + Map.Entry targetBucket = buckets.ceilingEntry(value); + if (targetBucket != null) { + targetBucket.getValue().add(1); + } + } + + public double findPutMessageEntireTimePX(double px) { + Map lastBuckets = this.lastBuckets; + long start = System.currentTimeMillis(); + double result = 0.0; + long totalRequest = lastBuckets.values().stream().mapToLong(LongAdder::longValue).sum(); + long pxIndex = (long) (totalRequest * px); + long passCount = 0; + List bucketValue = new ArrayList<>(lastBuckets.keySet()); + for (int i = 0; i < bucketValue.size(); i++) { + long count = lastBuckets.get(bucketValue.get(i)).longValue(); + if (pxIndex <= passCount + count) { + long relativeIndex = pxIndex - passCount; + if (i == 0) { + result = count == 0 ? 0 : bucketValue.get(i) * relativeIndex / (double)count; + } else { + long lastBucket = bucketValue.get(i - 1); + result = lastBucket + (count == 0 ? 0 : (bucketValue.get(i) - lastBucket) * relativeIndex / (double)count); + } + break; + } else { + passCount += count; + } + } + log.info("findPutMessageEntireTimePX {}={}ms cost {}ms", px, String.format("%.2f", result), System.currentTimeMillis() - start); + return result; + } + + private LongAdder[] resetPutMessageDistributeTime() { + LongAdder[] next = new LongAdder[13]; for (int i = 0; i < next.length; i++) { - next[i] = new AtomicLong(0); + next[i] = new LongAdder(); } - AtomicLong[] old = this.putMessageDistributeTime; + this.lastPutMessageDistributeTime = this.putMessageDistributeTime; this.putMessageDistributeTime = next; - return old; + return lastPutMessageDistributeTime; } public long getPutMessageEntireTimeMax() { @@ -91,55 +168,56 @@ public long getPutMessageEntireTimeMax() { } public void setPutMessageEntireTimeMax(long value) { - final AtomicLong[] times = this.putMessageDistributeTime; + this.incPutMessageEntireTime(value); + final LongAdder[] times = this.putMessageDistributeTime; if (null == times) return; // us if (value <= 0) { - times[0].incrementAndGet(); + times[0].add(1); } else if (value < 10) { - times[1].incrementAndGet(); + times[1].add(1); } else if (value < 50) { - times[2].incrementAndGet(); + times[2].add(1); } else if (value < 100) { - times[3].incrementAndGet(); + times[3].add(1); } else if (value < 200) { - times[4].incrementAndGet(); + times[4].add(1); } else if (value < 500) { - times[5].incrementAndGet(); + times[5].add(1); } else if (value < 1000) { - times[6].incrementAndGet(); + times[6].add(1); } // 2s else if (value < 2000) { - times[7].incrementAndGet(); + times[7].add(1); } // 3s else if (value < 3000) { - times[8].incrementAndGet(); + times[8].add(1); } // 4s else if (value < 4000) { - times[9].incrementAndGet(); + times[9].add(1); } // 5s else if (value < 5000) { - times[10].incrementAndGet(); + times[10].add(1); } // 10s else if (value < 10000) { - times[11].incrementAndGet(); + times[11].add(1); } else { - times[12].incrementAndGet(); + times[12].add(1); } if (value > this.putMessageEntireTimeMax) { - this.lockPut.lock(); + this.putLock.lock(); this.putMessageEntireTimeMax = value > this.putMessageEntireTimeMax ? value : this.putMessageEntireTimeMax; - this.lockPut.unlock(); + this.putLock.unlock(); } } @@ -149,10 +227,10 @@ public long getGetMessageEntireTimeMax() { public void setGetMessageEntireTimeMax(long value) { if (value > this.getMessageEntireTimeMax) { - this.lockGet.lock(); + this.getLock.lock(); this.getMessageEntireTimeMax = value > this.getMessageEntireTimeMax ? value : this.getMessageEntireTimeMax; - this.lockGet.unlock(); + this.getLock.unlock(); } } @@ -175,6 +253,7 @@ public String toString() { sb.append("\truntime: " + this.getFormatRuntime() + "\r\n"); sb.append("\tputMessageEntireTimeMax: " + this.putMessageEntireTimeMax + "\r\n"); sb.append("\tputMessageTimesTotal: " + totalTimes + "\r\n"); + sb.append("\tgetPutMessageFailedTimes: " + this.getPutMessageFailedTimes() + "\r\n"); sb.append("\tputMessageSizeTotal: " + this.getPutMessageSizeTotal() + "\r\n"); sb.append("\tputMessageDistributeTime: " + this.getPutMessageDistributeTimeStringInfo(totalTimes) + "\r\n"); @@ -186,16 +265,16 @@ public String toString() { sb.append("\tgetFoundTps: " + this.getGetFoundTps() + "\r\n"); sb.append("\tgetMissTps: " + this.getGetMissTps() + "\r\n"); sb.append("\tgetTotalTps: " + this.getGetTotalTps() + "\r\n"); - sb.append("\tgetTransferedTps: " + this.getGetTransferedTps() + "\r\n"); + sb.append("\tgetTransferredTps: " + this.getGetTransferredTps() + "\r\n"); return sb.toString(); } public long getPutMessageTimesTotal() { - long rs = 0; - for (AtomicLong data : putMessageTopicTimesTotal.values()) { - rs += data.get(); - } - return rs; + Map map = putMessageTopicTimesTotal; + return map.values() + .parallelStream() + .mapToLong(LongAdder::longValue) + .sum(); } private String getFormatRuntime() { @@ -215,11 +294,11 @@ private String getFormatRuntime() { } public long getPutMessageSizeTotal() { - long rs = 0; - for (AtomicLong data : putMessageTopicSizeTotal.values()) { - rs += data.get(); - } - return rs; + Map map = putMessageTopicSizeTotal; + return map.values() + .parallelStream() + .mapToLong(LongAdder::longValue) + .sum(); } private String getPutMessageDistributeTimeStringInfo(Long total) { @@ -282,28 +361,28 @@ private String getGetTotalTps() { return sb.toString(); } - private String getGetTransferedTps() { + private String getGetTransferredTps() { StringBuilder sb = new StringBuilder(); - sb.append(this.getGetTransferedTps(10)); + sb.append(this.getGetTransferredTps(10)); sb.append(" "); - sb.append(this.getGetTransferedTps(60)); + sb.append(this.getGetTransferredTps(60)); sb.append(" "); - sb.append(this.getGetTransferedTps(600)); + sb.append(this.getGetTransferredTps(600)); return sb.toString(); } private String putMessageDistributeTimeToString() { - final AtomicLong[] times = this.putMessageDistributeTime; + final LongAdder[] times = this.lastPutMessageDistributeTime; if (null == times) return null; final StringBuilder sb = new StringBuilder(); for (int i = 0; i < times.length; i++) { - long value = times[i].get(); + long value = times[i].longValue(); sb.append(String.format("%s:%d", PUT_MESSAGE_ENTIRE_TIME_MAX_DESC[i], value)); sb.append(" "); } @@ -313,7 +392,7 @@ private String putMessageDistributeTimeToString() { private String getPutTps(int time) { String result = ""; - this.lockSampling.lock(); + this.samplingLock.lock(); try { CallSnapshot last = this.putTimesList.getLast(); @@ -323,14 +402,14 @@ private String getPutTps(int time) { } } finally { - this.lockSampling.unlock(); + this.samplingLock.unlock(); } return result; } private String getGetFoundTps(int time) { String result = ""; - this.lockSampling.lock(); + this.samplingLock.lock(); try { CallSnapshot last = this.getTimesFoundList.getLast(); @@ -340,7 +419,7 @@ private String getGetFoundTps(int time) { result += CallSnapshot.getTPS(lastBefore, last); } } finally { - this.lockSampling.unlock(); + this.samplingLock.unlock(); } return result; @@ -348,7 +427,7 @@ private String getGetFoundTps(int time) { private String getGetMissTps(int time) { String result = ""; - this.lockSampling.lock(); + this.samplingLock.lock(); try { CallSnapshot last = this.getTimesMissList.getLast(); @@ -359,14 +438,14 @@ private String getGetMissTps(int time) { } } finally { - this.lockSampling.unlock(); + this.samplingLock.unlock(); } return result; } private String getGetTotalTps(int time) { - this.lockSampling.lock(); + this.samplingLock.lock(); double found = 0; double miss = 0; try { @@ -390,33 +469,33 @@ private String getGetTotalTps(int time) { } } finally { - this.lockSampling.unlock(); + this.samplingLock.unlock(); } return Double.toString(found + miss); } - private String getGetTransferedTps(int time) { + private String getGetTransferredTps(int time) { String result = ""; - this.lockSampling.lock(); + this.samplingLock.lock(); try { - CallSnapshot last = this.transferedMsgCountList.getLast(); + CallSnapshot last = this.transferredMsgCountList.getLast(); - if (this.transferedMsgCountList.size() > time) { + if (this.transferredMsgCountList.size() > time) { CallSnapshot lastBefore = - this.transferedMsgCountList.get(this.transferedMsgCountList.size() - (time + 1)); + this.transferredMsgCountList.get(this.transferredMsgCountList.size() - (time + 1)); result += CallSnapshot.getTPS(lastBefore, last); } } finally { - this.lockSampling.unlock(); + this.samplingLock.unlock(); } return result; } public HashMap getRuntimeInfo() { - HashMap result = new HashMap(64); + HashMap result = new HashMap<>(64); Long totalTimes = getPutMessageTimesTotal(); if (0 == totalTimes) { @@ -427,6 +506,7 @@ public HashMap getRuntimeInfo() { result.put("runtime", this.getFormatRuntime()); result.put("putMessageEntireTimeMax", String.valueOf(this.putMessageEntireTimeMax)); result.put("putMessageTimesTotal", String.valueOf(totalTimes)); + result.put("putMessageFailedTimes", String.valueOf(this.putMessageFailedTimes)); result.put("putMessageSizeTotal", String.valueOf(this.getPutMessageSizeTotal())); result.put("putMessageDistributeTime", String.valueOf(this.getPutMessageDistributeTimeStringInfo(totalTimes))); @@ -434,11 +514,13 @@ public HashMap getRuntimeInfo() { String.valueOf(this.getPutMessageSizeTotal() / totalTimes.doubleValue())); result.put("dispatchMaxBuffer", String.valueOf(this.dispatchMaxBuffer)); result.put("getMessageEntireTimeMax", String.valueOf(this.getMessageEntireTimeMax)); - result.put("putTps", String.valueOf(this.getPutTps())); - result.put("getFoundTps", String.valueOf(this.getGetFoundTps())); - result.put("getMissTps", String.valueOf(this.getGetMissTps())); - result.put("getTotalTps", String.valueOf(this.getGetTotalTps())); - result.put("getTransferedTps", String.valueOf(this.getGetTransferedTps())); + result.put("putTps", this.getPutTps()); + result.put("getFoundTps", this.getGetFoundTps()); + result.put("getMissTps", this.getGetMissTps()); + result.put("getTotalTps", this.getGetTotalTps()); + result.put("getTransferredTps", this.getGetTransferredTps()); + result.put("putLatency99", String.format("%.2f", this.findPutMessageEntireTimePX(0.99))); + result.put("putLatency999", String.format("%.2f", this.findPutMessageEntireTimePX(0.999))); return result; } @@ -463,11 +545,14 @@ public void run() { @Override public String getServiceName() { + if (this.brokerIdentity != null && this.brokerIdentity.isInBrokerContainer()) { + return brokerIdentity.getIdentifier() + StoreStatsService.class.getSimpleName(); + } return StoreStatsService.class.getSimpleName(); } private void sampling() { - this.lockSampling.lock(); + this.samplingLock.lock(); try { this.putTimesList.add(new CallSnapshot(System.currentTimeMillis(), getPutMessageTimesTotal())); if (this.putTimesList.size() > (MAX_RECORDS_OF_SAMPLING + 1)) { @@ -475,25 +560,25 @@ private void sampling() { } this.getTimesFoundList.add(new CallSnapshot(System.currentTimeMillis(), - this.getMessageTimesTotalFound.get())); + this.getMessageTimesTotalFound.longValue())); if (this.getTimesFoundList.size() > (MAX_RECORDS_OF_SAMPLING + 1)) { this.getTimesFoundList.removeFirst(); } this.getTimesMissList.add(new CallSnapshot(System.currentTimeMillis(), - this.getMessageTimesTotalMiss.get())); + this.getMessageTimesTotalMiss.longValue())); if (this.getTimesMissList.size() > (MAX_RECORDS_OF_SAMPLING + 1)) { this.getTimesMissList.removeFirst(); } - this.transferedMsgCountList.add(new CallSnapshot(System.currentTimeMillis(), - this.getMessageTransferedMsgCount.get())); - if (this.transferedMsgCountList.size() > (MAX_RECORDS_OF_SAMPLING + 1)) { - this.transferedMsgCountList.removeFirst(); + this.transferredMsgCountList.add(new CallSnapshot(System.currentTimeMillis(), + this.getMessageTransferredMsgCount.longValue())); + if (this.transferredMsgCountList.size() > (MAX_RECORDS_OF_SAMPLING + 1)) { + this.transferredMsgCountList.removeFirst(); } } finally { - this.lockSampling.unlock(); + this.samplingLock.unlock(); } } @@ -501,69 +586,77 @@ private void printTps() { if (System.currentTimeMillis() > (this.lastPrintTimestamp + printTPSInterval * 1000)) { this.lastPrintTimestamp = System.currentTimeMillis(); - log.info("[STORETPS] put_tps {} get_found_tps {} get_miss_tps {} get_transfered_tps {}", + log.info("[STORETPS] put_tps {} get_found_tps {} get_miss_tps {} get_transferred_tps {}", this.getPutTps(printTPSInterval), this.getGetFoundTps(printTPSInterval), this.getGetMissTps(printTPSInterval), - this.getGetTransferedTps(printTPSInterval) + this.getGetTransferredTps(printTPSInterval) ); - final AtomicLong[] times = this.initPutMessageDistributeTime(); + final LongAdder[] times = this.resetPutMessageDistributeTime(); if (null == times) return; final StringBuilder sb = new StringBuilder(); long totalPut = 0; for (int i = 0; i < times.length; i++) { - long value = times[i].get(); + long value = times[i].longValue(); totalPut += value; sb.append(String.format("%s:%d", PUT_MESSAGE_ENTIRE_TIME_MAX_DESC[i], value)); sb.append(" "); } - + this.resetPutMessageTimeBuckets(); + this.findPutMessageEntireTimePX(0.99); + this.findPutMessageEntireTimePX(0.999); log.info("[PAGECACHERT] TotalPut {}, PutMessageDistributeTime {}", totalPut, sb.toString()); } } - public AtomicLong getGetMessageTimesTotalFound() { + public LongAdder getGetMessageTimesTotalFound() { return getMessageTimesTotalFound; } - public AtomicLong getGetMessageTimesTotalMiss() { + public LongAdder getGetMessageTimesTotalMiss() { return getMessageTimesTotalMiss; } - public AtomicLong getGetMessageTransferedMsgCount() { - return getMessageTransferedMsgCount; + public LongAdder getGetMessageTransferredMsgCount() { + return getMessageTransferredMsgCount; } - public AtomicLong getPutMessageFailedTimes() { + public LongAdder getPutMessageFailedTimes() { return putMessageFailedTimes; } - public AtomicLong getSinglePutMessageTopicSizeTotal(String topic) { - AtomicLong rs = putMessageTopicSizeTotal.get(topic); + public LongAdder getSinglePutMessageTopicSizeTotal(String topic) { + LongAdder rs = putMessageTopicSizeTotal.get(topic); if (null == rs) { - rs = new AtomicLong(0); - putMessageTopicSizeTotal.put(topic, rs); + rs = new LongAdder(); + LongAdder previous = putMessageTopicSizeTotal.putIfAbsent(topic, rs); + if (previous != null) { + rs = previous; + } } return rs; } - public AtomicLong getSinglePutMessageTopicTimesTotal(String topic) { - AtomicLong rs = putMessageTopicTimesTotal.get(topic); + public LongAdder getSinglePutMessageTopicTimesTotal(String topic) { + LongAdder rs = putMessageTopicTimesTotal.get(topic); if (null == rs) { - rs = new AtomicLong(0); - putMessageTopicTimesTotal.put(topic, rs); + rs = new LongAdder(); + LongAdder previous = putMessageTopicTimesTotal.putIfAbsent(topic, rs); + if (previous != null) { + rs = previous; + } } return rs; } - public Map getPutMessageTopicTimesTotal() { + public Map getPutMessageTopicTimesTotal() { return putMessageTopicTimesTotal; } - public Map getPutMessageTopicSizeTotal() { + public Map getPutMessageTopicSizeTotal() { return putMessageTopicSizeTotal; } diff --git a/store/src/main/java/org/apache/rocketmq/store/StoreType.java b/store/src/main/java/org/apache/rocketmq/store/StoreType.java new file mode 100644 index 00000000000..4f9c4d0e448 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/StoreType.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store; + +public enum StoreType { + DEFAULT("default"), + DEFAULT_ROCKSDB("defaultRocksDB"); + + private String storeType; + + StoreType(String storeType) { + this.storeType = storeType; + } + + public String getStoreType() { + return storeType; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/StoreUtil.java b/store/src/main/java/org/apache/rocketmq/store/StoreUtil.java index f63efd6e982..526ca9bf1b0 100644 --- a/store/src/main/java/org/apache/rocketmq/store/StoreUtil.java +++ b/store/src/main/java/org/apache/rocketmq/store/StoreUtil.java @@ -16,10 +16,21 @@ */ package org.apache.rocketmq.store; +import com.google.common.base.Preconditions; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.logfile.MappedFile; + import java.lang.management.ManagementFactory; import java.lang.management.OperatingSystemMXBean; +import java.nio.ByteBuffer; + +import static java.lang.String.format; public class StoreUtil { + private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + public static final long TOTAL_PHYSICAL_MEMORY_SIZE = getTotalPhysicalMemorySize(); @SuppressWarnings("restriction") @@ -32,4 +43,37 @@ public static long getTotalPhysicalMemorySize() { return physicalTotal; } + + public static void fileAppend(MappedFile file, ByteBuffer data) { + boolean success = file.appendMessage(data); + if (!success) { + throw new RuntimeException(format("fileAppend failed for file: %s and data remaining: %d", file, data.remaining())); + } + } + + public static FileQueueSnapshot getFileQueueSnapshot(MappedFileQueue mappedFileQueue) { + return getFileQueueSnapshot(mappedFileQueue, mappedFileQueue.getLastMappedFile().getFileFromOffset()); + } + + public static FileQueueSnapshot getFileQueueSnapshot(MappedFileQueue mappedFileQueue, final long currentFile) { + try { + Preconditions.checkNotNull(mappedFileQueue, "file queue shouldn't be null"); + MappedFile firstFile = mappedFileQueue.getFirstMappedFile(); + MappedFile lastFile = mappedFileQueue.getLastMappedFile(); + int mappedFileSize = mappedFileQueue.getMappedFileSize(); + if (firstFile == null || lastFile == null) { + return new FileQueueSnapshot(firstFile, -1, lastFile, -1, currentFile, -1, 0, false); + } + + long firstFileIndex = 0; + long lastFileIndex = (lastFile.getFileFromOffset() - firstFile.getFileFromOffset()) / mappedFileSize; + long currentFileIndex = (currentFile - firstFile.getFileFromOffset()) / mappedFileSize; + long behind = (lastFile.getFileFromOffset() - currentFile) / mappedFileSize; + boolean exist = firstFile.getFileFromOffset() <= currentFile && currentFile <= lastFile.getFileFromOffset(); + return new FileQueueSnapshot(firstFile, firstFileIndex, lastFile, lastFileIndex, currentFile, currentFileIndex, behind, exist); + } catch (Exception e) { + log.error("[BUG] get file queue snapshot failed. fileQueue: {}, currentFile: {}", mappedFileQueue, currentFile, e); + } + return new FileQueueSnapshot(); + } } diff --git a/store/src/main/java/org/apache/rocketmq/store/Swappable.java b/store/src/main/java/org/apache/rocketmq/store/Swappable.java new file mode 100644 index 00000000000..cb8dee5e6b4 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/Swappable.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store; + +/** + * Clean up page-table on super large disk + */ +public interface Swappable { + void swapMap(int reserveNum, long forceSwapIntervalMs, long normalSwapIntervalMs); + void cleanSwappedMap(long forceCleanSwapIntervalMs); +} diff --git a/store/src/main/java/org/apache/rocketmq/store/TopicQueueLock.java b/store/src/main/java/org/apache/rocketmq/store/TopicQueueLock.java new file mode 100644 index 00000000000..5a131b5c35c --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/TopicQueueLock.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +public class TopicQueueLock { + private final int size; + private final List lockList; + + public TopicQueueLock() { + this.size = 32; + this.lockList = new ArrayList<>(32); + for (int i = 0; i < this.size; i++) { + this.lockList.add(new ReentrantLock()); + } + } + + public TopicQueueLock(int size) { + this.size = size; + this.lockList = new ArrayList<>(size); + for (int i = 0; i < this.size; i++) { + this.lockList.add(new ReentrantLock()); + } + } + + public void lock(String topicQueueKey) { + Lock lock = this.lockList.get((topicQueueKey.hashCode() & 0x7fffffff) % this.size); + lock.lock(); + } + + public void unlock(String topicQueueKey) { + Lock lock = this.lockList.get((topicQueueKey.hashCode() & 0x7fffffff) % this.size); + lock.unlock(); + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/TransientStorePool.java b/store/src/main/java/org/apache/rocketmq/store/TransientStorePool.java index 21da03eeb42..0d42ee69e68 100644 --- a/store/src/main/java/org/apache/rocketmq/store/TransientStorePool.java +++ b/store/src/main/java/org/apache/rocketmq/store/TransientStorePool.java @@ -22,10 +22,9 @@ import java.util.Deque; import java.util.concurrent.ConcurrentLinkedDeque; import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.store.util.LibC; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import sun.nio.ch.DirectBuffer; public class TransientStorePool { @@ -34,12 +33,11 @@ public class TransientStorePool { private final int poolSize; private final int fileSize; private final Deque availableBuffers; - private final MessageStoreConfig storeConfig; + private volatile boolean isRealCommit = true; - public TransientStorePool(final MessageStoreConfig storeConfig) { - this.storeConfig = storeConfig; - this.poolSize = storeConfig.getTransientStorePoolSize(); - this.fileSize = storeConfig.getMapedFileSizeCommitLog(); + public TransientStorePool(final int poolSize, final int fileSize) { + this.poolSize = poolSize; + this.fileSize = fileSize; this.availableBuffers = new ConcurrentLinkedDeque<>(); } @@ -80,10 +78,15 @@ public ByteBuffer borrowBuffer() { return buffer; } - public int remainBufferNumbs() { - if (storeConfig.isTransientStorePoolEnable()) { - return availableBuffers.size(); - } - return Integer.MAX_VALUE; + public int availableBufferNums() { + return availableBuffers.size(); + } + + public boolean isRealCommit() { + return isRealCommit; + } + + public void setRealCommit(boolean realCommit) { + isRealCommit = realCommit; } } diff --git a/store/src/main/java/org/apache/rocketmq/store/config/MessageStoreConfig.java b/store/src/main/java/org/apache/rocketmq/store/config/MessageStoreConfig.java index 02aa84a3e6b..0ba02e4cb9d 100644 --- a/store/src/main/java/org/apache/rocketmq/store/config/MessageStoreConfig.java +++ b/store/src/main/java/org/apache/rocketmq/store/config/MessageStoreConfig.java @@ -17,27 +17,101 @@ package org.apache.rocketmq.store.config; import java.io.File; + import org.apache.rocketmq.common.annotation.ImportantField; import org.apache.rocketmq.store.ConsumeQueue; +import org.apache.rocketmq.store.StoreType; +import org.apache.rocketmq.store.queue.BatchConsumeQueue; public class MessageStoreConfig { + + public static final String MULTI_PATH_SPLITTER = System.getProperty("rocketmq.broker.multiPathSplitter", ","); + //The root directory in which the log data is kept @ImportantField private String storePathRootDir = System.getProperty("user.home") + File.separator + "store"; //The directory in which the commitlog is kept @ImportantField - private String storePathCommitLog = System.getProperty("user.home") + File.separator + "store" - + File.separator + "commitlog"; + private String storePathCommitLog = null; + + @ImportantField + private String storePathDLedgerCommitLog = null; + + //The directory in which the epochFile is kept + @ImportantField + private String storePathEpochFile = null; + + @ImportantField + private String storePathBrokerIdentity = null; + + private String readOnlyCommitLogStorePaths = null; // CommitLog file size,default is 1G - private int mapedFileSizeCommitLog = 1024 * 1024 * 1024; + private int mappedFileSizeCommitLog = 1024 * 1024 * 1024; + + // CompactinLog file size, default is 100M + private int compactionMappedFileSize = 100 * 1024 * 1024; + + // CompactionLog consumeQueue file size, default is 10M + private int compactionCqMappedFileSize = 10 * 1024 * 1024; + + private int compactionScheduleInternal = 15 * 60 * 1000; + + private int maxOffsetMapSize = 100 * 1024 * 1024; + + private int compactionThreadNum = 6; + + private boolean enableCompaction = true; + + // TimerLog file size, default is 100M + private int mappedFileSizeTimerLog = 100 * 1024 * 1024; + + private int timerPrecisionMs = 1000; + + private int timerRollWindowSlot = 3600 * 24 * 2; + private int timerFlushIntervalMs = 1000; + private int timerGetMessageThreadNum = 3; + private int timerPutMessageThreadNum = 3; + + private boolean timerEnableDisruptor = false; + + private boolean timerEnableCheckMetrics = true; + private boolean timerInterceptDelayLevel = false; + private int timerMaxDelaySec = 3600 * 24 * 3; + private boolean timerWheelEnable = true; + + /** + * 1. Register to broker after (startTime + disappearTimeAfterStart) + * 2. Internal msg exchange will start after (startTime + disappearTimeAfterStart) + * A. PopReviveService + * B. TimerDequeueGetService + */ + @ImportantField + private int disappearTimeAfterStart = -1; + + private boolean timerStopEnqueue = false; + + private String timerCheckMetricsWhen = "05"; + + private boolean timerSkipUnknownError = false; + private boolean timerWarmEnable = false; + private boolean timerStopDequeue = false; + private int timerCongestNumEachSlot = Integer.MAX_VALUE; + + private int timerMetricSmallThreshold = 1000000; + private int timerProgressLogIntervalMs = 10 * 1000; + + // default, defaultRocksDB + @ImportantField + private String storeType = StoreType.DEFAULT.getStoreType(); // ConsumeQueue file size,default is 30W - private int mapedFileSizeConsumeQueue = 300000 * ConsumeQueue.CQ_STORE_UNIT_SIZE; + private int mappedFileSizeConsumeQueue = 300000 * ConsumeQueue.CQ_STORE_UNIT_SIZE; // enable consume queue ext private boolean enableConsumeQueueExt = false; // ConsumeQueue extend file size, 48M private int mappedFileSizeConsumeQueueExt = 48 * 1024 * 1024; + private int mapperFileSizeBatchConsumeQueue = 300000 * BatchConsumeQueue.CQ_STORE_UNIT_SIZE; // Bit count of filter bit map. // this will be set by pipe of calculate filter bit map. private int bitMapLengthConsumeQueueExt = 64; @@ -52,15 +126,20 @@ public class MessageStoreConfig { @ImportantField private int commitIntervalCommitLog = 200; + private int maxRecoveryCommitlogFiles = 30; + + private int diskSpaceWarningLevelRatio = 90; + + private int diskSpaceCleanForciblyRatio = 85; + /** * introduced since 4.0.x. Determine whether to use mutex reentrantLock when putting message.
    - * By default it is set to false indicating using spin lock when putting message. */ - private boolean useReentrantLockWhenPutMessage = false; + private boolean useReentrantLockWhenPutMessage = true; - // Whether schedule flush,default is real-time + // Whether schedule flush @ImportantField - private boolean flushCommitLogTimed = false; + private boolean flushCommitLogTimed = true; // ConsumeQueue flush interval private int flushIntervalConsumeQueue = 1000; // Resource reclaim interval @@ -78,10 +157,15 @@ public class MessageStoreConfig { // The number of hours to keep a log file before deleting it (in hours) @ImportantField private int fileReservedTime = 72; + @ImportantField + private int deleteFileBatchMax = 10; // Flow control for ConsumeQueue private int putMsgIndexHightWater = 600000; - // The maximum size of a single log file,default is 512K + // The maximum size of message body,default is 4M,4M only for body length,not include others. private int maxMessageSize = 1024 * 1024 * 4; + + // The maximum size of message body can be set in config;count with maxMsgNums * CQ_STORE_UNIT_SIZE(20 || 46) + private int maxFilterMessageSize = 16000; // Whether check the CRC32 of the records consumed. // This ensures no on-the-wire or on-disk corruption to the messages occurred. // This check adds some overhead,so it may be disabled in cases seeking extreme performance. @@ -117,15 +201,23 @@ public class MessageStoreConfig { private int haListenPort = 10912; private int haSendHeartbeatInterval = 1000 * 5; private int haHousekeepingInterval = 1000 * 20; + /** + * Maximum size of data to transfer to slave. + * NOTE: cannot be larger than HAClient.READ_MAX_BUFFER_SIZE + */ private int haTransferBatchSize = 1024 * 32; @ImportantField private String haMasterAddress = null; - private int haSlaveFallbehindMax = 1024 * 1024 * 256; + private int haMaxGapNotInSync = 1024 * 1024 * 256; @ImportantField - private BrokerRole brokerRole = BrokerRole.ASYNC_MASTER; + private volatile BrokerRole brokerRole = BrokerRole.ASYNC_MASTER; @ImportantField private FlushDiskType flushDiskType = FlushDiskType.ASYNC_FLUSH; + // Used by GroupTransferService to sync messages from master to slave private int syncFlushTimeout = 1000 * 5; + // Used by PutMessage to wait messages be flushed to disk and synchronized in current broker member group. + private int putMessageTimeout = 1000 * 8; + private int slaveTimeout = 3000; private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"; private long flushDelayOffsetInterval = 1000 * 10; @ImportantField @@ -143,6 +235,192 @@ public class MessageStoreConfig { private int transientStorePoolSize = 5; private boolean fastFailIfNoBufferInStorePool = false; + // DLedger message store config + private boolean enableDLegerCommitLog = false; + private String dLegerGroup; + private String dLegerPeers; + private String dLegerSelfId; + private String preferredLeaderId; + private boolean enableBatchPush = false; + + private boolean enableScheduleMessageStats = true; + + private boolean enableLmq = false; + private boolean enableMultiDispatch = false; + private int maxLmqConsumeQueueNum = 20000; + + private boolean enableScheduleAsyncDeliver = false; + private int scheduleAsyncDeliverMaxPendingLimit = 2000; + private int scheduleAsyncDeliverMaxResendNum2Blocked = 3; + + private int maxBatchDeleteFilesNum = 50; + //Polish dispatch + private int dispatchCqThreads = 10; + private int dispatchCqCacheNum = 1024 * 4; + private boolean enableAsyncReput = true; + //For recheck the reput + private boolean recheckReputOffsetFromCq = false; + + // Maximum length of topic, it will be removed in the future release + @Deprecated + private int maxTopicLength = Byte.MAX_VALUE; + + /** + * Use MessageVersion.MESSAGE_VERSION_V2 automatically if topic length larger than Bytes.MAX_VALUE. + * Otherwise, store use MESSAGE_VERSION_V1. Note: Client couldn't decode MESSAGE_VERSION_V2 version message. + * Enable this config to resolve this issue. https://github.com/apache/rocketmq/issues/5568 + */ + private boolean autoMessageVersionOnTopicLen = true; + + /** + * It cannot be changed after the broker is started. + * Modifications need to be restarted to take effect. + */ + private boolean enabledAppendPropCRC = false; + private boolean forceVerifyPropCRC = false; + private int travelCqFileNumWhenGetMessage = 1; + // Sleep interval between to corrections + private int correctLogicMinOffsetSleepInterval = 1; + // Force correct min offset interval + private int correctLogicMinOffsetForceInterval = 5 * 60 * 1000; + // swap + private boolean mappedFileSwapEnable = true; + private long commitLogForceSwapMapInterval = 12L * 60 * 60 * 1000; + private long commitLogSwapMapInterval = 1L * 60 * 60 * 1000; + private int commitLogSwapMapReserveFileNum = 100; + private long logicQueueForceSwapMapInterval = 12L * 60 * 60 * 1000; + private long logicQueueSwapMapInterval = 1L * 60 * 60 * 1000; + private long cleanSwapedMapInterval = 5L * 60 * 1000; + private int logicQueueSwapMapReserveFileNum = 20; + + private boolean searchBcqByCacheEnable = true; + + @ImportantField + private boolean dispatchFromSenderThread = false; + + @ImportantField + private boolean wakeCommitWhenPutMessage = true; + @ImportantField + private boolean wakeFlushWhenPutMessage = false; + + @ImportantField + private boolean enableCleanExpiredOffset = false; + + private int maxAsyncPutMessageRequests = 5000; + + private int pullBatchMaxMessageCount = 160; + + @ImportantField + private int totalReplicas = 1; + + /** + * Each message must be written successfully to at least in-sync replicas. + * The master broker is considered one of the in-sync replicas, and it's included in the count of total. + * If a master broker is ASYNC_MASTER, inSyncReplicas will be ignored. + * If enableControllerMode is true and ackAckInSyncStateSet is true, inSyncReplicas will be ignored. + */ + @ImportantField + private int inSyncReplicas = 1; + + /** + * Will be worked in auto multiple replicas mode, to provide minimum in-sync replicas. + * It is still valid in controller mode. + */ + @ImportantField + private int minInSyncReplicas = 1; + + /** + * Each message must be written successfully to all replicas in SyncStateSet. + */ + @ImportantField + private boolean allAckInSyncStateSet = false; + + /** + * Dynamically adjust in-sync replicas to provide higher availability, the real time in-sync replicas + * will smaller than inSyncReplicas config. + */ + @ImportantField + private boolean enableAutoInSyncReplicas = false; + + /** + * Enable or not ha flow control + */ + @ImportantField + private boolean haFlowControlEnable = false; + + /** + * The max speed for one slave when transfer data in ha + */ + private long maxHaTransferByteInSecond = 100 * 1024 * 1024; + + /** + * The max gap time that slave doesn't catch up to master. + */ + private long haMaxTimeSlaveNotCatchup = 1000 * 15; + + /** + * Sync flush offset from master when broker startup, used in upgrading from old version broker. + */ + private boolean syncMasterFlushOffsetWhenStartup = false; + + /** + * Max checksum range. + */ + private long maxChecksumRange = 1024 * 1024 * 1024; + + private int replicasPerDiskPartition = 1; + + private double logicalDiskSpaceCleanForciblyThreshold = 0.8; + + private long maxSlaveResendLength = 256 * 1024 * 1024; + + /** + * Whether sync from lastFile when a new broker replicas(no data) join the master. + */ + private boolean syncFromLastFile = false; + + private boolean asyncLearner = false; + + /** + * Number of records to scan before starting to estimate. + */ + private int maxConsumeQueueScan = 20_000; + + /** + * Number of matched records before starting to estimate. + */ + private int sampleCountThreshold = 5000; + + private boolean coldDataFlowControlEnable = false; + private boolean coldDataScanEnable = false; + private boolean dataReadAheadEnable = true; + private int timerColdDataCheckIntervalMs = 60 * 1000; + private int sampleSteps = 32; + private int accessMessageInMemoryHotRatio = 26; + /** + * Build ConsumeQueue concurrently with multi-thread + */ + private boolean enableBuildConsumeQueueConcurrently = false; + + private int batchDispatchRequestThreadPoolNums = 16; + + // rocksdb mode + private long cleanRocksDBDirtyCQIntervalMin = 60; + private long statRocksDBCQIntervalSec = 10; + private long memTableFlushIntervalMs = 60 * 60 * 1000L; + private boolean realTimePersistRocksDBConfig = true; + private boolean enableRocksDBLog = false; + + private int topicQueueLockNum = 32; + + public boolean isEnabledAppendPropCRC() { + return enabledAppendPropCRC; + } + + public void setEnabledAppendPropCRC(boolean enabledAppendPropCRC) { + this.enabledAppendPropCRC = enabledAppendPropCRC; + } + public boolean isDebugLockEnable() { return debugLockEnable; } @@ -183,22 +461,81 @@ public void setWarmMapedFileEnable(boolean warmMapedFileEnable) { this.warmMapedFileEnable = warmMapedFileEnable; } - public int getMapedFileSizeCommitLog() { - return mapedFileSizeCommitLog; + public int getCompactionMappedFileSize() { + return compactionMappedFileSize; + } + + public int getCompactionCqMappedFileSize() { + return compactionCqMappedFileSize; + } + + public void setCompactionMappedFileSize(int compactionMappedFileSize) { + this.compactionMappedFileSize = compactionMappedFileSize; + } + + public void setCompactionCqMappedFileSize(int compactionCqMappedFileSize) { + this.compactionCqMappedFileSize = compactionCqMappedFileSize; + } + + public int getCompactionScheduleInternal() { + return compactionScheduleInternal; + } + + public void setCompactionScheduleInternal(int compactionScheduleInternal) { + this.compactionScheduleInternal = compactionScheduleInternal; + } + + public int getMaxOffsetMapSize() { + return maxOffsetMapSize; + } + + public void setMaxOffsetMapSize(int maxOffsetMapSize) { + this.maxOffsetMapSize = maxOffsetMapSize; + } + + public int getCompactionThreadNum() { + return compactionThreadNum; + } + + public void setCompactionThreadNum(int compactionThreadNum) { + this.compactionThreadNum = compactionThreadNum; + } + + public boolean isEnableCompaction() { + return enableCompaction; + } + + public void setEnableCompaction(boolean enableCompaction) { + this.enableCompaction = enableCompaction; + } + + public int getMappedFileSizeCommitLog() { + return mappedFileSizeCommitLog; + } + + public void setMappedFileSizeCommitLog(int mappedFileSizeCommitLog) { + this.mappedFileSizeCommitLog = mappedFileSizeCommitLog; + } + + public boolean isEnableRocksDBStore() { + return StoreType.DEFAULT_ROCKSDB.getStoreType().equalsIgnoreCase(this.storeType); } - public void setMapedFileSizeCommitLog(int mapedFileSizeCommitLog) { - this.mapedFileSizeCommitLog = mapedFileSizeCommitLog; + public String getStoreType() { + return storeType; } - public int getMapedFileSizeConsumeQueue() { + public void setStoreType(String storeType) { + this.storeType = storeType; + } - int factor = (int) Math.ceil(this.mapedFileSizeConsumeQueue / (ConsumeQueue.CQ_STORE_UNIT_SIZE * 1.0)); + public int getMappedFileSizeConsumeQueue() { + int factor = (int) Math.ceil(this.mappedFileSizeConsumeQueue / (ConsumeQueue.CQ_STORE_UNIT_SIZE * 1.0)); return (int) (factor * ConsumeQueue.CQ_STORE_UNIT_SIZE); } - public void setMapedFileSizeConsumeQueue(int mapedFileSizeConsumeQueue) { - this.mapedFileSizeConsumeQueue = mapedFileSizeConsumeQueue; + public void setMappedFileSizeConsumeQueue(int mappedFileSizeConsumeQueue) { + this.mappedFileSizeConsumeQueue = mappedFileSizeConsumeQueue; } public boolean isEnableConsumeQueueExt() { @@ -265,6 +602,56 @@ public void setMaxMessageSize(int maxMessageSize) { this.maxMessageSize = maxMessageSize; } + public int getMaxFilterMessageSize() { + return maxFilterMessageSize; + } + + public void setMaxFilterMessageSize(int maxFilterMessageSize) { + this.maxFilterMessageSize = maxFilterMessageSize; + } + + @Deprecated + public int getMaxTopicLength() { + return maxTopicLength; + } + + @Deprecated + public void setMaxTopicLength(int maxTopicLength) { + this.maxTopicLength = maxTopicLength; + } + + public boolean isAutoMessageVersionOnTopicLen() { + return autoMessageVersionOnTopicLen; + } + + public void setAutoMessageVersionOnTopicLen(boolean autoMessageVersionOnTopicLen) { + this.autoMessageVersionOnTopicLen = autoMessageVersionOnTopicLen; + } + + public int getTravelCqFileNumWhenGetMessage() { + return travelCqFileNumWhenGetMessage; + } + + public void setTravelCqFileNumWhenGetMessage(int travelCqFileNumWhenGetMessage) { + this.travelCqFileNumWhenGetMessage = travelCqFileNumWhenGetMessage; + } + + public int getCorrectLogicMinOffsetSleepInterval() { + return correctLogicMinOffsetSleepInterval; + } + + public void setCorrectLogicMinOffsetSleepInterval(int correctLogicMinOffsetSleepInterval) { + this.correctLogicMinOffsetSleepInterval = correctLogicMinOffsetSleepInterval; + } + + public int getCorrectLogicMinOffsetForceInterval() { + return correctLogicMinOffsetForceInterval; + } + + public void setCorrectLogicMinOffsetForceInterval(int correctLogicMinOffsetForceInterval) { + this.correctLogicMinOffsetForceInterval = correctLogicMinOffsetForceInterval; + } + public boolean isCheckCRCOnRecover() { return checkCRCOnRecover; } @@ -277,7 +664,19 @@ public void setCheckCRCOnRecover(boolean checkCRCOnRecover) { this.checkCRCOnRecover = checkCRCOnRecover; } + public boolean isForceVerifyPropCRC() { + return forceVerifyPropCRC; + } + + public void setForceVerifyPropCRC(boolean forceVerifyPropCRC) { + this.forceVerifyPropCRC = forceVerifyPropCRC; + } + + public String getStorePathCommitLog() { + if (storePathCommitLog == null) { + return storePathRootDir + File.separator + "commitlog"; + } return storePathCommitLog; } @@ -285,6 +684,36 @@ public void setStorePathCommitLog(String storePathCommitLog) { this.storePathCommitLog = storePathCommitLog; } + public String getStorePathDLedgerCommitLog() { + return storePathDLedgerCommitLog; + } + + public void setStorePathDLedgerCommitLog(String storePathDLedgerCommitLog) { + this.storePathDLedgerCommitLog = storePathDLedgerCommitLog; + } + + public String getStorePathEpochFile() { + if (storePathEpochFile == null) { + return storePathRootDir + File.separator + "epochFileCheckpoint"; + } + return storePathEpochFile; + } + + public void setStorePathEpochFile(String storePathEpochFile) { + this.storePathEpochFile = storePathEpochFile; + } + + public String getStorePathBrokerIdentity() { + if (storePathBrokerIdentity == null) { + return storePathRootDir + File.separator + "brokerIdentity"; + } + return storePathBrokerIdentity; + } + + public void setStorePathBrokerIdentity(String storePathBrokerIdentity) { + this.storePathBrokerIdentity = storePathBrokerIdentity; + } + public String getDeleteWhen() { return deleteWhen; } @@ -456,6 +885,10 @@ public int getHaListenPort() { } public void setHaListenPort(int haListenPort) { + if (haListenPort < 0) { + this.haListenPort = 0; + return; + } this.haListenPort = haListenPort; } @@ -495,12 +928,12 @@ public void setHaTransferBatchSize(int haTransferBatchSize) { this.haTransferBatchSize = haTransferBatchSize; } - public int getHaSlaveFallbehindMax() { - return haSlaveFallbehindMax; + public int getHaMaxGapNotInSync() { + return haMaxGapNotInSync; } - public void setHaSlaveFallbehindMax(int haSlaveFallbehindMax) { - this.haSlaveFallbehindMax = haSlaveFallbehindMax; + public void setHaMaxGapNotInSync(int haMaxGapNotInSync) { + this.haMaxGapNotInSync = haMaxGapNotInSync; } public FlushDiskType getFlushDiskType() { @@ -523,6 +956,22 @@ public void setSyncFlushTimeout(int syncFlushTimeout) { this.syncFlushTimeout = syncFlushTimeout; } + public int getPutMessageTimeout() { + return putMessageTimeout; + } + + public void setPutMessageTimeout(int putMessageTimeout) { + this.putMessageTimeout = putMessageTimeout; + } + + public int getSlaveTimeout() { + return slaveTimeout; + } + + public void setSlaveTimeout(int slaveTimeout) { + this.slaveTimeout = slaveTimeout; + } + public String getHaMasterAddress() { return haMasterAddress; } @@ -603,15 +1052,8 @@ public void setDefaultQueryMaxNum(int defaultQueryMaxNum) { this.defaultQueryMaxNum = defaultQueryMaxNum; } - /** - * Enable transient commitLog store poll only if transientStorePoolEnable is true and the FlushDiskType is - * ASYNC_FLUSH - * - * @return true or false - */ public boolean isTransientStorePoolEnable() { - return transientStorePoolEnable && FlushDiskType.ASYNC_FLUSH == getFlushDiskType() - && BrokerRole.SLAVE != getBrokerRole(); + return transientStorePoolEnable; } public void setTransientStorePoolEnable(final boolean transientStorePoolEnable) { @@ -666,4 +1108,715 @@ public void setCommitCommitLogThoroughInterval(final int commitCommitLogThorough this.commitCommitLogThoroughInterval = commitCommitLogThoroughInterval; } + public boolean isWakeCommitWhenPutMessage() { + return wakeCommitWhenPutMessage; + } + + public void setWakeCommitWhenPutMessage(boolean wakeCommitWhenPutMessage) { + this.wakeCommitWhenPutMessage = wakeCommitWhenPutMessage; + } + + public boolean isWakeFlushWhenPutMessage() { + return wakeFlushWhenPutMessage; + } + + public void setWakeFlushWhenPutMessage(boolean wakeFlushWhenPutMessage) { + this.wakeFlushWhenPutMessage = wakeFlushWhenPutMessage; + } + + public int getMapperFileSizeBatchConsumeQueue() { + return mapperFileSizeBatchConsumeQueue; + } + + public void setMapperFileSizeBatchConsumeQueue(int mapperFileSizeBatchConsumeQueue) { + this.mapperFileSizeBatchConsumeQueue = mapperFileSizeBatchConsumeQueue; + } + + public boolean isEnableCleanExpiredOffset() { + return enableCleanExpiredOffset; + } + + public void setEnableCleanExpiredOffset(boolean enableCleanExpiredOffset) { + this.enableCleanExpiredOffset = enableCleanExpiredOffset; + } + + public String getReadOnlyCommitLogStorePaths() { + return readOnlyCommitLogStorePaths; + } + + public void setReadOnlyCommitLogStorePaths(String readOnlyCommitLogStorePaths) { + this.readOnlyCommitLogStorePaths = readOnlyCommitLogStorePaths; + } + + public String getdLegerGroup() { + return dLegerGroup; + } + + public void setdLegerGroup(String dLegerGroup) { + this.dLegerGroup = dLegerGroup; + } + + public String getdLegerPeers() { + return dLegerPeers; + } + + public void setdLegerPeers(String dLegerPeers) { + this.dLegerPeers = dLegerPeers; + } + + public String getdLegerSelfId() { + return dLegerSelfId; + } + + public void setdLegerSelfId(String dLegerSelfId) { + this.dLegerSelfId = dLegerSelfId; + } + + public boolean isEnableDLegerCommitLog() { + return enableDLegerCommitLog; + } + + public void setEnableDLegerCommitLog(boolean enableDLegerCommitLog) { + this.enableDLegerCommitLog = enableDLegerCommitLog; + } + + public String getPreferredLeaderId() { + return preferredLeaderId; + } + + public void setPreferredLeaderId(String preferredLeaderId) { + this.preferredLeaderId = preferredLeaderId; + } + + public boolean isEnableBatchPush() { + return enableBatchPush; + } + + public void setEnableBatchPush(boolean enableBatchPush) { + this.enableBatchPush = enableBatchPush; + } + + public boolean isEnableScheduleMessageStats() { + return enableScheduleMessageStats; + } + + public void setEnableScheduleMessageStats(boolean enableScheduleMessageStats) { + this.enableScheduleMessageStats = enableScheduleMessageStats; + } + + public int getMaxAsyncPutMessageRequests() { + return maxAsyncPutMessageRequests; + } + + public void setMaxAsyncPutMessageRequests(int maxAsyncPutMessageRequests) { + this.maxAsyncPutMessageRequests = maxAsyncPutMessageRequests; + } + + public int getMaxRecoveryCommitlogFiles() { + return maxRecoveryCommitlogFiles; + } + + public void setMaxRecoveryCommitlogFiles(final int maxRecoveryCommitlogFiles) { + this.maxRecoveryCommitlogFiles = maxRecoveryCommitlogFiles; + } + + public boolean isDispatchFromSenderThread() { + return dispatchFromSenderThread; + } + + public void setDispatchFromSenderThread(boolean dispatchFromSenderThread) { + this.dispatchFromSenderThread = dispatchFromSenderThread; + } + + public int getDispatchCqThreads() { + return dispatchCqThreads; + } + + public void setDispatchCqThreads(final int dispatchCqThreads) { + this.dispatchCqThreads = dispatchCqThreads; + } + + public int getDispatchCqCacheNum() { + return dispatchCqCacheNum; + } + + public void setDispatchCqCacheNum(final int dispatchCqCacheNum) { + this.dispatchCqCacheNum = dispatchCqCacheNum; + } + + public boolean isEnableAsyncReput() { + return enableAsyncReput; + } + + public void setEnableAsyncReput(final boolean enableAsyncReput) { + this.enableAsyncReput = enableAsyncReput; + } + + public boolean isRecheckReputOffsetFromCq() { + return recheckReputOffsetFromCq; + } + + public void setRecheckReputOffsetFromCq(final boolean recheckReputOffsetFromCq) { + this.recheckReputOffsetFromCq = recheckReputOffsetFromCq; + } + + public long getCommitLogForceSwapMapInterval() { + return commitLogForceSwapMapInterval; + } + + public void setCommitLogForceSwapMapInterval(long commitLogForceSwapMapInterval) { + this.commitLogForceSwapMapInterval = commitLogForceSwapMapInterval; + } + + public int getCommitLogSwapMapReserveFileNum() { + return commitLogSwapMapReserveFileNum; + } + + public void setCommitLogSwapMapReserveFileNum(int commitLogSwapMapReserveFileNum) { + this.commitLogSwapMapReserveFileNum = commitLogSwapMapReserveFileNum; + } + + public long getLogicQueueForceSwapMapInterval() { + return logicQueueForceSwapMapInterval; + } + + public void setLogicQueueForceSwapMapInterval(long logicQueueForceSwapMapInterval) { + this.logicQueueForceSwapMapInterval = logicQueueForceSwapMapInterval; + } + + public int getLogicQueueSwapMapReserveFileNum() { + return logicQueueSwapMapReserveFileNum; + } + + public void setLogicQueueSwapMapReserveFileNum(int logicQueueSwapMapReserveFileNum) { + this.logicQueueSwapMapReserveFileNum = logicQueueSwapMapReserveFileNum; + } + + public long getCleanSwapedMapInterval() { + return cleanSwapedMapInterval; + } + + public void setCleanSwapedMapInterval(long cleanSwapedMapInterval) { + this.cleanSwapedMapInterval = cleanSwapedMapInterval; + } + + public long getCommitLogSwapMapInterval() { + return commitLogSwapMapInterval; + } + + public void setCommitLogSwapMapInterval(long commitLogSwapMapInterval) { + this.commitLogSwapMapInterval = commitLogSwapMapInterval; + } + + public long getLogicQueueSwapMapInterval() { + return logicQueueSwapMapInterval; + } + + public void setLogicQueueSwapMapInterval(long logicQueueSwapMapInterval) { + this.logicQueueSwapMapInterval = logicQueueSwapMapInterval; + } + + public int getMaxBatchDeleteFilesNum() { + return maxBatchDeleteFilesNum; + } + + public void setMaxBatchDeleteFilesNum(int maxBatchDeleteFilesNum) { + this.maxBatchDeleteFilesNum = maxBatchDeleteFilesNum; + } + + public boolean isSearchBcqByCacheEnable() { + return searchBcqByCacheEnable; + } + + public void setSearchBcqByCacheEnable(boolean searchBcqByCacheEnable) { + this.searchBcqByCacheEnable = searchBcqByCacheEnable; + } + + public int getDiskSpaceWarningLevelRatio() { + return diskSpaceWarningLevelRatio; + } + + public void setDiskSpaceWarningLevelRatio(int diskSpaceWarningLevelRatio) { + this.diskSpaceWarningLevelRatio = diskSpaceWarningLevelRatio; + } + + public int getDiskSpaceCleanForciblyRatio() { + return diskSpaceCleanForciblyRatio; + } + + public void setDiskSpaceCleanForciblyRatio(int diskSpaceCleanForciblyRatio) { + this.diskSpaceCleanForciblyRatio = diskSpaceCleanForciblyRatio; + } + + public boolean isMappedFileSwapEnable() { + return mappedFileSwapEnable; + } + + public void setMappedFileSwapEnable(boolean mappedFileSwapEnable) { + this.mappedFileSwapEnable = mappedFileSwapEnable; + } + + public int getPullBatchMaxMessageCount() { + return pullBatchMaxMessageCount; + } + + public void setPullBatchMaxMessageCount(int pullBatchMaxMessageCount) { + this.pullBatchMaxMessageCount = pullBatchMaxMessageCount; + } + + public int getDeleteFileBatchMax() { + return deleteFileBatchMax; + } + + public void setDeleteFileBatchMax(int deleteFileBatchMax) { + this.deleteFileBatchMax = deleteFileBatchMax; + } + + public int getTotalReplicas() { + return totalReplicas; + } + + public void setTotalReplicas(int totalReplicas) { + this.totalReplicas = totalReplicas; + } + + public int getInSyncReplicas() { + return inSyncReplicas; + } + + public void setInSyncReplicas(int inSyncReplicas) { + this.inSyncReplicas = inSyncReplicas; + } + + public int getMinInSyncReplicas() { + return minInSyncReplicas; + } + + public void setMinInSyncReplicas(int minInSyncReplicas) { + this.minInSyncReplicas = minInSyncReplicas; + } + + public boolean isAllAckInSyncStateSet() { + return allAckInSyncStateSet; + } + + public void setAllAckInSyncStateSet(boolean allAckInSyncStateSet) { + this.allAckInSyncStateSet = allAckInSyncStateSet; + } + + public boolean isEnableAutoInSyncReplicas() { + return enableAutoInSyncReplicas; + } + + public void setEnableAutoInSyncReplicas(boolean enableAutoInSyncReplicas) { + this.enableAutoInSyncReplicas = enableAutoInSyncReplicas; + } + + public boolean isHaFlowControlEnable() { + return haFlowControlEnable; + } + + public void setHaFlowControlEnable(boolean haFlowControlEnable) { + this.haFlowControlEnable = haFlowControlEnable; + } + + public long getMaxHaTransferByteInSecond() { + return maxHaTransferByteInSecond; + } + + public void setMaxHaTransferByteInSecond(long maxHaTransferByteInSecond) { + this.maxHaTransferByteInSecond = maxHaTransferByteInSecond; + } + + public long getHaMaxTimeSlaveNotCatchup() { + return haMaxTimeSlaveNotCatchup; + } + + public void setHaMaxTimeSlaveNotCatchup(long haMaxTimeSlaveNotCatchup) { + this.haMaxTimeSlaveNotCatchup = haMaxTimeSlaveNotCatchup; + } + + public boolean isSyncMasterFlushOffsetWhenStartup() { + return syncMasterFlushOffsetWhenStartup; + } + + public void setSyncMasterFlushOffsetWhenStartup(boolean syncMasterFlushOffsetWhenStartup) { + this.syncMasterFlushOffsetWhenStartup = syncMasterFlushOffsetWhenStartup; + } + + public long getMaxChecksumRange() { + return maxChecksumRange; + } + + public void setMaxChecksumRange(long maxChecksumRange) { + this.maxChecksumRange = maxChecksumRange; + } + + public int getReplicasPerDiskPartition() { + return replicasPerDiskPartition; + } + + public void setReplicasPerDiskPartition(int replicasPerDiskPartition) { + this.replicasPerDiskPartition = replicasPerDiskPartition; + } + + public double getLogicalDiskSpaceCleanForciblyThreshold() { + return logicalDiskSpaceCleanForciblyThreshold; + } + + public void setLogicalDiskSpaceCleanForciblyThreshold(double logicalDiskSpaceCleanForciblyThreshold) { + this.logicalDiskSpaceCleanForciblyThreshold = logicalDiskSpaceCleanForciblyThreshold; + } + + public int getDisappearTimeAfterStart() { + return disappearTimeAfterStart; + } + + public void setDisappearTimeAfterStart(int disappearTimeAfterStart) { + this.disappearTimeAfterStart = disappearTimeAfterStart; + } + + public long getMaxSlaveResendLength() { + return maxSlaveResendLength; + } + + public void setMaxSlaveResendLength(long maxSlaveResendLength) { + this.maxSlaveResendLength = maxSlaveResendLength; + } + + public boolean isSyncFromLastFile() { + return syncFromLastFile; + } + + public void setSyncFromLastFile(boolean syncFromLastFile) { + this.syncFromLastFile = syncFromLastFile; + } + + public boolean isEnableLmq() { + return enableLmq; + } + + public void setEnableLmq(boolean enableLmq) { + this.enableLmq = enableLmq; + } + + public boolean isEnableMultiDispatch() { + return enableMultiDispatch; + } + + public void setEnableMultiDispatch(boolean enableMultiDispatch) { + this.enableMultiDispatch = enableMultiDispatch; + } + + public int getMaxLmqConsumeQueueNum() { + return maxLmqConsumeQueueNum; + } + + public void setMaxLmqConsumeQueueNum(int maxLmqConsumeQueueNum) { + this.maxLmqConsumeQueueNum = maxLmqConsumeQueueNum; + } + + public boolean isEnableScheduleAsyncDeliver() { + return enableScheduleAsyncDeliver; + } + + public void setEnableScheduleAsyncDeliver(boolean enableScheduleAsyncDeliver) { + this.enableScheduleAsyncDeliver = enableScheduleAsyncDeliver; + } + + public int getScheduleAsyncDeliverMaxPendingLimit() { + return scheduleAsyncDeliverMaxPendingLimit; + } + + public void setScheduleAsyncDeliverMaxPendingLimit(int scheduleAsyncDeliverMaxPendingLimit) { + this.scheduleAsyncDeliverMaxPendingLimit = scheduleAsyncDeliverMaxPendingLimit; + } + + public int getScheduleAsyncDeliverMaxResendNum2Blocked() { + return scheduleAsyncDeliverMaxResendNum2Blocked; + } + + public void setScheduleAsyncDeliverMaxResendNum2Blocked(int scheduleAsyncDeliverMaxResendNum2Blocked) { + this.scheduleAsyncDeliverMaxResendNum2Blocked = scheduleAsyncDeliverMaxResendNum2Blocked; + } + + public boolean isAsyncLearner() { + return asyncLearner; + } + + public void setAsyncLearner(boolean asyncLearner) { + this.asyncLearner = asyncLearner; + } + + public int getMappedFileSizeTimerLog() { + return mappedFileSizeTimerLog; + } + + public void setMappedFileSizeTimerLog(final int mappedFileSizeTimerLog) { + this.mappedFileSizeTimerLog = mappedFileSizeTimerLog; + } + + public int getTimerPrecisionMs() { + return timerPrecisionMs; + } + + public void setTimerPrecisionMs(int timerPrecisionMs) { + int[] candidates = {100, 200, 500, 1000}; + for (int i = 1; i < candidates.length; i++) { + if (timerPrecisionMs < candidates[i]) { + this.timerPrecisionMs = candidates[i - 1]; + return; + } + } + this.timerPrecisionMs = candidates[candidates.length - 1]; + } + + public int getTimerRollWindowSlot() { + return timerRollWindowSlot; + } + + public int getTimerGetMessageThreadNum() { + return timerGetMessageThreadNum; + } + + public void setTimerGetMessageThreadNum(int timerGetMessageThreadNum) { + this.timerGetMessageThreadNum = timerGetMessageThreadNum; + } + + public int getTimerPutMessageThreadNum() { + return timerPutMessageThreadNum; + } + + public void setTimerPutMessageThreadNum(int timerPutMessageThreadNum) { + this.timerPutMessageThreadNum = timerPutMessageThreadNum; + } + + public boolean isTimerEnableDisruptor() { + return timerEnableDisruptor; + } + + public boolean isTimerEnableCheckMetrics() { + return timerEnableCheckMetrics; + } + + public void setTimerEnableCheckMetrics(boolean timerEnableCheckMetrics) { + this.timerEnableCheckMetrics = timerEnableCheckMetrics; + } + + public boolean isTimerStopEnqueue() { + return timerStopEnqueue; + } + + public void setTimerStopEnqueue(boolean timerStopEnqueue) { + this.timerStopEnqueue = timerStopEnqueue; + } + + public String getTimerCheckMetricsWhen() { + return timerCheckMetricsWhen; + } + + public boolean isTimerSkipUnknownError() { + return timerSkipUnknownError; + } + + public void setTimerSkipUnknownError(boolean timerSkipUnknownError) { + this.timerSkipUnknownError = timerSkipUnknownError; + } + + public boolean isTimerWarmEnable() { + return timerWarmEnable; + } + + public boolean isTimerWheelEnable() { + return timerWheelEnable; + } + + public void setTimerWheelEnable(boolean timerWheelEnable) { + this.timerWheelEnable = timerWheelEnable; + } + + public boolean isTimerStopDequeue() { + return timerStopDequeue; + } + + public int getTimerMetricSmallThreshold() { + return timerMetricSmallThreshold; + } + + public void setTimerMetricSmallThreshold(int timerMetricSmallThreshold) { + this.timerMetricSmallThreshold = timerMetricSmallThreshold; + } + + public int getTimerCongestNumEachSlot() { + return timerCongestNumEachSlot; + } + + public void setTimerCongestNumEachSlot(int timerCongestNumEachSlot) { + // In order to get this value from messageStoreConfig properties file created before v4.4.1. + this.timerCongestNumEachSlot = timerCongestNumEachSlot; + } + + public int getTimerFlushIntervalMs() { + return timerFlushIntervalMs; + } + + public void setTimerFlushIntervalMs(final int timerFlushIntervalMs) { + this.timerFlushIntervalMs = timerFlushIntervalMs; + } + + public void setTimerRollWindowSlot(final int timerRollWindowSlot) { + this.timerRollWindowSlot = timerRollWindowSlot; + } + + public int getTimerProgressLogIntervalMs() { + return timerProgressLogIntervalMs; + } + + public void setTimerProgressLogIntervalMs(final int timerProgressLogIntervalMs) { + this.timerProgressLogIntervalMs = timerProgressLogIntervalMs; + } + + public boolean isTimerInterceptDelayLevel() { + return timerInterceptDelayLevel; + } + + public void setTimerInterceptDelayLevel(boolean timerInterceptDelayLevel) { + this.timerInterceptDelayLevel = timerInterceptDelayLevel; + } + + public int getTimerMaxDelaySec() { + return timerMaxDelaySec; + } + + public void setTimerMaxDelaySec(final int timerMaxDelaySec) { + this.timerMaxDelaySec = timerMaxDelaySec; + } + + public int getMaxConsumeQueueScan() { + return maxConsumeQueueScan; + } + + public void setMaxConsumeQueueScan(int maxConsumeQueueScan) { + this.maxConsumeQueueScan = maxConsumeQueueScan; + } + + public int getSampleCountThreshold() { + return sampleCountThreshold; + } + + public void setSampleCountThreshold(int sampleCountThreshold) { + this.sampleCountThreshold = sampleCountThreshold; + } + + public boolean isColdDataFlowControlEnable() { + return coldDataFlowControlEnable; + } + + public void setColdDataFlowControlEnable(boolean coldDataFlowControlEnable) { + this.coldDataFlowControlEnable = coldDataFlowControlEnable; + } + + public boolean isColdDataScanEnable() { + return coldDataScanEnable; + } + + public void setColdDataScanEnable(boolean coldDataScanEnable) { + this.coldDataScanEnable = coldDataScanEnable; + } + + public int getTimerColdDataCheckIntervalMs() { + return timerColdDataCheckIntervalMs; + } + + public void setTimerColdDataCheckIntervalMs(int timerColdDataCheckIntervalMs) { + this.timerColdDataCheckIntervalMs = timerColdDataCheckIntervalMs; + } + + public int getSampleSteps() { + return sampleSteps; + } + + public void setSampleSteps(int sampleSteps) { + this.sampleSteps = sampleSteps; + } + + public int getAccessMessageInMemoryHotRatio() { + return accessMessageInMemoryHotRatio; + } + + public void setAccessMessageInMemoryHotRatio(int accessMessageInMemoryHotRatio) { + this.accessMessageInMemoryHotRatio = accessMessageInMemoryHotRatio; + } + + public boolean isDataReadAheadEnable() { + return dataReadAheadEnable; + } + + public void setDataReadAheadEnable(boolean dataReadAheadEnable) { + this.dataReadAheadEnable = dataReadAheadEnable; + } + + public boolean isEnableBuildConsumeQueueConcurrently() { + return enableBuildConsumeQueueConcurrently; + } + + public void setEnableBuildConsumeQueueConcurrently(boolean enableBuildConsumeQueueConcurrently) { + this.enableBuildConsumeQueueConcurrently = enableBuildConsumeQueueConcurrently; + } + + public int getBatchDispatchRequestThreadPoolNums() { + return batchDispatchRequestThreadPoolNums; + } + + public void setBatchDispatchRequestThreadPoolNums(int batchDispatchRequestThreadPoolNums) { + this.batchDispatchRequestThreadPoolNums = batchDispatchRequestThreadPoolNums; + } + + public boolean isRealTimePersistRocksDBConfig() { + return realTimePersistRocksDBConfig; + } + + public void setRealTimePersistRocksDBConfig(boolean realTimePersistRocksDBConfig) { + this.realTimePersistRocksDBConfig = realTimePersistRocksDBConfig; + } + + public long getStatRocksDBCQIntervalSec() { + return statRocksDBCQIntervalSec; + } + + public void setStatRocksDBCQIntervalSec(long statRocksDBCQIntervalSec) { + this.statRocksDBCQIntervalSec = statRocksDBCQIntervalSec; + } + + public long getCleanRocksDBDirtyCQIntervalMin() { + return cleanRocksDBDirtyCQIntervalMin; + } + + public void setCleanRocksDBDirtyCQIntervalMin(long cleanRocksDBDirtyCQIntervalMin) { + this.cleanRocksDBDirtyCQIntervalMin = cleanRocksDBDirtyCQIntervalMin; + } + + public long getMemTableFlushIntervalMs() { + return memTableFlushIntervalMs; + } + + public void setMemTableFlushIntervalMs(long memTableFlushIntervalMs) { + this.memTableFlushIntervalMs = memTableFlushIntervalMs; + } + + public boolean isEnableRocksDBLog() { + return enableRocksDBLog; + } + + public void setEnableRocksDBLog(boolean enableRocksDBLog) { + this.enableRocksDBLog = enableRocksDBLog; + } + + public int getTopicQueueLockNum() { + return topicQueueLockNum; + } + + public void setTopicQueueLockNum(int topicQueueLockNum) { + this.topicQueueLockNum = topicQueueLockNum; + } } diff --git a/store/src/main/java/org/apache/rocketmq/store/config/StorePathConfigHelper.java b/store/src/main/java/org/apache/rocketmq/store/config/StorePathConfigHelper.java index ccd76c4f092..2f34e7dff54 100644 --- a/store/src/main/java/org/apache/rocketmq/store/config/StorePathConfigHelper.java +++ b/store/src/main/java/org/apache/rocketmq/store/config/StorePathConfigHelper.java @@ -27,6 +27,9 @@ public static String getStorePathConsumeQueue(final String rootDir) { public static String getStorePathConsumeQueueExt(final String rootDir) { return rootDir + File.separator + "consumequeue_ext"; } + public static String getStorePathBatchConsumeQueue(final String rootDir) { + return rootDir + File.separator + "batchconsumequeue"; + } public static String getStorePathIndex(final String rootDir) { return rootDir + File.separator + "index"; diff --git a/store/src/main/java/org/apache/rocketmq/store/dledger/DLedgerCommitLog.java b/store/src/main/java/org/apache/rocketmq/store/dledger/DLedgerCommitLog.java new file mode 100644 index 00000000000..27a18abc9d9 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/dledger/DLedgerCommitLog.java @@ -0,0 +1,1143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.dledger; + +import java.net.Inet6Address; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExtBatch; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.message.MessageVersion; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.store.AppendMessageResult; +import org.apache.rocketmq.store.AppendMessageStatus; +import org.apache.rocketmq.store.CommitLog; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.DispatchRequest; +import org.apache.rocketmq.store.MessageExtEncoder; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.StoreStatsService; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.logfile.MappedFile; +import org.rocksdb.RocksDBException; + +import io.openmessaging.storage.dledger.AppendFuture; +import io.openmessaging.storage.dledger.BatchAppendFuture; +import io.openmessaging.storage.dledger.DLedgerConfig; +import io.openmessaging.storage.dledger.DLedgerServer; +import io.openmessaging.storage.dledger.entry.DLedgerEntry; +import io.openmessaging.storage.dledger.protocol.AppendEntryRequest; +import io.openmessaging.storage.dledger.protocol.AppendEntryResponse; +import io.openmessaging.storage.dledger.protocol.BatchAppendEntryRequest; +import io.openmessaging.storage.dledger.protocol.DLedgerResponseCode; +import io.openmessaging.storage.dledger.store.file.DLedgerMmapFileStore; +import io.openmessaging.storage.dledger.store.file.MmapFile; +import io.openmessaging.storage.dledger.store.file.MmapFileList; +import io.openmessaging.storage.dledger.store.file.SelectMmapBufferResult; +import io.openmessaging.storage.dledger.utils.DLedgerUtils; + +/** + * Store all metadata downtime for recovery, data protection reliability + */ +public class DLedgerCommitLog extends CommitLog { + + static { + System.setProperty("dLedger.multiPath.Splitter", MessageStoreConfig.MULTI_PATH_SPLITTER); + } + + private final DLedgerServer dLedgerServer; + private final DLedgerConfig dLedgerConfig; + private final DLedgerMmapFileStore dLedgerFileStore; + private final MmapFileList dLedgerFileList; + + //The id identifies the broker role, 0 means master, others means slave + private final int id; + + private final MessageSerializer messageSerializer; + private volatile long beginTimeInDledgerLock = 0; + + //This offset separate the old commitlog from dledger commitlog + private long dividedCommitlogOffset = -1; + + private boolean isInrecoveringOldCommitlog = false; + + private final StringBuilder msgIdBuilder = new StringBuilder(); + + public DLedgerCommitLog(final DefaultMessageStore defaultMessageStore) { + super(defaultMessageStore); + dLedgerConfig = new DLedgerConfig(); + dLedgerConfig.setEnableDiskForceClean(defaultMessageStore.getMessageStoreConfig().isCleanFileForciblyEnable()); + dLedgerConfig.setStoreType(DLedgerConfig.FILE); + dLedgerConfig.setSelfId(defaultMessageStore.getMessageStoreConfig().getdLegerSelfId()); + dLedgerConfig.setGroup(defaultMessageStore.getMessageStoreConfig().getdLegerGroup()); + dLedgerConfig.setPeers(defaultMessageStore.getMessageStoreConfig().getdLegerPeers()); + dLedgerConfig.setStoreBaseDir(defaultMessageStore.getMessageStoreConfig().getStorePathRootDir()); + dLedgerConfig.setDataStorePath(defaultMessageStore.getMessageStoreConfig().getStorePathDLedgerCommitLog()); + dLedgerConfig.setReadOnlyDataStoreDirs(defaultMessageStore.getMessageStoreConfig().getReadOnlyCommitLogStorePaths()); + dLedgerConfig.setMappedFileSizeForEntryData(defaultMessageStore.getMessageStoreConfig().getMappedFileSizeCommitLog()); + dLedgerConfig.setDeleteWhen(defaultMessageStore.getMessageStoreConfig().getDeleteWhen()); + dLedgerConfig.setFileReservedHours(defaultMessageStore.getMessageStoreConfig().getFileReservedTime() + 1); + dLedgerConfig.setPreferredLeaderId(defaultMessageStore.getMessageStoreConfig().getPreferredLeaderId()); + dLedgerConfig.setEnableBatchPush(defaultMessageStore.getMessageStoreConfig().isEnableBatchPush()); + dLedgerConfig.setDiskSpaceRatioToCheckExpired(defaultMessageStore.getMessageStoreConfig().getDiskMaxUsedSpaceRatio() / 100f); + + id = Integer.parseInt(dLedgerConfig.getSelfId().substring(1)) + 1; + dLedgerServer = new DLedgerServer(dLedgerConfig); + dLedgerFileStore = (DLedgerMmapFileStore) dLedgerServer.getdLedgerStore(); + DLedgerMmapFileStore.AppendHook appendHook = (entry, buffer, bodyOffset) -> { + assert bodyOffset == DLedgerEntry.BODY_OFFSET; + buffer.position(buffer.position() + bodyOffset + MessageDecoder.PHY_POS_POSITION); + buffer.putLong(entry.getPos() + bodyOffset); + }; + dLedgerFileStore.addAppendHook(appendHook); + dLedgerFileList = dLedgerFileStore.getDataFileList(); + this.messageSerializer = new MessageSerializer(defaultMessageStore.getMessageStoreConfig().getMaxMessageSize()); + + } + + @Override + public boolean load() { + return super.load(); + } + + private void refreshConfig() { + dLedgerConfig.setEnableDiskForceClean(defaultMessageStore.getMessageStoreConfig().isCleanFileForciblyEnable()); + dLedgerConfig.setDeleteWhen(defaultMessageStore.getMessageStoreConfig().getDeleteWhen()); + dLedgerConfig.setFileReservedHours(defaultMessageStore.getMessageStoreConfig().getFileReservedTime() + 1); + } + + private void disableDeleteDledger() { + dLedgerConfig.setEnableDiskForceClean(false); + dLedgerConfig.setFileReservedHours(24 * 365 * 10); + } + + @Override + public void start() { + dLedgerServer.startup(); + } + + @Override + public void shutdown() { + dLedgerServer.shutdown(); + } + + @Override + public long flush() { + dLedgerFileStore.flush(); + return dLedgerFileList.getFlushedWhere(); + } + + @Override + public long getMaxOffset() { + if (dLedgerFileStore.getCommittedPos() > 0) { + return dLedgerFileStore.getCommittedPos(); + } + if (dLedgerFileList.getMinOffset() > 0) { + return dLedgerFileList.getMinOffset(); + } + return 0; + } + + @Override + public long getMinOffset() { + if (!mappedFileQueue.getMappedFiles().isEmpty()) { + return mappedFileQueue.getMinOffset(); + } + for (MmapFile file : dLedgerFileList.getMappedFiles()) { + if (file.isAvailable()) { + return file.getFileFromOffset() + file.getStartPosition(); + } + } + return 0; + } + + @Override + public long getConfirmOffset() { + return this.getMaxOffset(); + } + + @Override + public void setConfirmOffset(long phyOffset) { + log.warn("Should not set confirm offset {} for dleger commitlog", phyOffset); + } + + @Override + public long remainHowManyDataToCommit() { + return dLedgerFileList.remainHowManyDataToCommit(); + } + + @Override + public long remainHowManyDataToFlush() { + return dLedgerFileList.remainHowManyDataToFlush(); + } + + @Override + public int deleteExpiredFile( + final long expiredTime, + final int deleteFilesInterval, + final long intervalForcibly, + final boolean cleanImmediately + ) { + if (mappedFileQueue.getMappedFiles().isEmpty()) { + refreshConfig(); + //To prevent too much log in defaultMessageStore + return Integer.MAX_VALUE; + } else { + disableDeleteDledger(); + } + int count = super.deleteExpiredFile(expiredTime, deleteFilesInterval, intervalForcibly, cleanImmediately); + if (count > 0 || mappedFileQueue.getMappedFiles().size() != 1) { + return count; + } + //the old logic will keep the last file, here to delete it + MappedFile mappedFile = mappedFileQueue.getLastMappedFile(); + log.info("Try to delete the last old commitlog file {}", mappedFile.getFileName()); + long liveMaxTimestamp = mappedFile.getLastModifiedTimestamp() + expiredTime; + if (System.currentTimeMillis() >= liveMaxTimestamp || cleanImmediately) { + while (!mappedFile.destroy(10 * 1000)) { + DLedgerUtils.sleep(1000); + } + mappedFileQueue.getMappedFiles().remove(mappedFile); + } + return 1; + } + + public SelectMappedBufferResult convertSbr(SelectMmapBufferResult sbr) { + if (sbr == null) { + return null; + } else { + return new DLedgerSelectMappedBufferResult(sbr); + } + + } + + public SelectMmapBufferResult truncate(SelectMmapBufferResult sbr) { + long committedPos = dLedgerFileStore.getCommittedPos(); + if (sbr == null || sbr.getStartOffset() == committedPos) { + return null; + } + if (sbr.getStartOffset() + sbr.getSize() <= committedPos) { + return sbr; + } else { + sbr.setSize((int) (committedPos - sbr.getStartOffset())); + return sbr; + } + } + + @Override + public SelectMappedBufferResult getData(final long offset) { + if (offset < dividedCommitlogOffset) { + return super.getData(offset); + } + return this.getData(offset, offset == 0); + } + + @Override + public SelectMappedBufferResult getData(final long offset, final boolean returnFirstOnNotFound) { + if (offset < dividedCommitlogOffset) { + return super.getData(offset, returnFirstOnNotFound); + } + if (offset >= dLedgerFileStore.getCommittedPos()) { + return null; + } + int mappedFileSize = this.dLedgerServer.getdLedgerConfig().getMappedFileSizeForEntryData(); + MmapFile mappedFile = this.dLedgerFileList.findMappedFileByOffset(offset, returnFirstOnNotFound); + if (mappedFile != null) { + int pos = (int) (offset % mappedFileSize); + SelectMmapBufferResult sbr = mappedFile.selectMappedBuffer(pos); + return convertSbr(truncate(sbr)); + } + + return null; + } + + @Override + public boolean getData(final long offset, final int size, final ByteBuffer byteBuffer) { + if (offset < dividedCommitlogOffset) { + return super.getData(offset, size, byteBuffer); + } + if (offset >= dLedgerFileStore.getCommittedPos()) { + return false; + } + int mappedFileSize = this.dLedgerServer.getdLedgerConfig().getMappedFileSizeForEntryData(); + MmapFile mappedFile = this.dLedgerFileList.findMappedFileByOffset(offset, offset == 0); + if (mappedFile != null) { + int pos = (int) (offset % mappedFileSize); + return mappedFile.getData(pos, size, byteBuffer); + } + return false; + } + + private void dledgerRecoverNormally(long maxPhyOffsetOfConsumeQueue) throws RocksDBException { + dLedgerFileStore.load(); + if (!dLedgerFileList.getMappedFiles().isEmpty()) { + dLedgerFileStore.recover(); + dividedCommitlogOffset = dLedgerFileList.getFirstMappedFile().getFileFromOffset(); + MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(); + if (mappedFile != null) { + disableDeleteDledger(); + } + long maxPhyOffset = dLedgerFileList.getMaxWrotePosition(); + // Clear ConsumeQueue redundant data + if (maxPhyOffsetOfConsumeQueue >= maxPhyOffset) { + log.warn("[TruncateCQ]maxPhyOffsetOfConsumeQueue({}) >= processOffset({}), truncate dirty logic files", maxPhyOffsetOfConsumeQueue, maxPhyOffset); + this.defaultMessageStore.truncateDirtyLogicFiles(maxPhyOffset); + } + return; + } + //Indicate that, it is the first time to load mixed commitlog, need to recover the old commitlog + isInrecoveringOldCommitlog = true; + super.recoverNormally(maxPhyOffsetOfConsumeQueue); + isInrecoveringOldCommitlog = false; + + setRecoverPosition(); + + } + + private void dledgerRecoverAbnormally(long maxPhyOffsetOfConsumeQueue) throws RocksDBException { + boolean checkCRCOnRecover = this.defaultMessageStore.getMessageStoreConfig().isCheckCRCOnRecover(); + dLedgerFileStore.load(); + if (!dLedgerFileList.getMappedFiles().isEmpty()) { + dLedgerFileStore.recover(); + dividedCommitlogOffset = dLedgerFileList.getFirstMappedFile().getFileFromOffset(); + MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(); + if (mappedFile != null) { + disableDeleteDledger(); + } + List mmapFiles = dLedgerFileList.getMappedFiles(); + int index = mmapFiles.size() - 1; + MmapFile mmapFile = null; + for (; index >= 0; index--) { + mmapFile = mmapFiles.get(index); + if (isMmapFileMatchedRecover(mmapFile)) { + log.info("dledger recover from this mappFile " + mmapFile.getFileName()); + break; + } + } + + if (index < 0) { + index = 0; + mmapFile = mmapFiles.get(index); + } + + ByteBuffer byteBuffer = mmapFile.sliceByteBuffer(); + long processOffset = mmapFile.getFileFromOffset(); + long mmapFileOffset = 0; + while (true) { + DispatchRequest dispatchRequest = this.checkMessageAndReturnSize(byteBuffer, checkCRCOnRecover, true); + int size = dispatchRequest.getMsgSize(); + + if (dispatchRequest.isSuccess()) { + if (size > 0) { + mmapFileOffset += size; + if (this.defaultMessageStore.getMessageStoreConfig().isDuplicationEnable()) { + if (dispatchRequest.getCommitLogOffset() < this.defaultMessageStore.getConfirmOffset()) { + this.defaultMessageStore.doDispatch(dispatchRequest); + } + } else { + this.defaultMessageStore.doDispatch(dispatchRequest); + } + } else if (size == 0) { + index++; + if (index >= mmapFiles.size()) { + log.info("dledger recover physics file over, last mapped file " + mmapFile.getFileName()); + break; + } else { + mmapFile = mmapFiles.get(index); + byteBuffer = mmapFile.sliceByteBuffer(); + processOffset = mmapFile.getFileFromOffset(); + mmapFileOffset = 0; + log.info("dledger recover next physics file, " + mmapFile.getFileName()); + } + } + } else { + log.info("dledger recover physics file end, " + mmapFile.getFileName() + " pos=" + byteBuffer.position()); + break; + } + } + + processOffset += mmapFileOffset; + + if (maxPhyOffsetOfConsumeQueue >= processOffset) { + log.warn("dledger maxPhyOffsetOfConsumeQueue({}) >= processOffset({}), truncate dirty logic files", maxPhyOffsetOfConsumeQueue, processOffset); + this.defaultMessageStore.truncateDirtyLogicFiles(processOffset); + } + return; + } + isInrecoveringOldCommitlog = true; + super.recoverAbnormally(maxPhyOffsetOfConsumeQueue); + + isInrecoveringOldCommitlog = false; + + setRecoverPosition(); + + } + + private void setRecoverPosition() { + MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(); + if (mappedFile == null) { + return; + } + ByteBuffer byteBuffer = mappedFile.sliceByteBuffer(); + byteBuffer.position(mappedFile.getWrotePosition()); + boolean needWriteMagicCode = true; + // 1 TOTAL SIZE + byteBuffer.getInt(); //size + int magicCode = byteBuffer.getInt(); + if (magicCode == CommitLog.BLANK_MAGIC_CODE) { + needWriteMagicCode = false; + } else { + log.info("Recover old commitlog found a illegal magic code={}", magicCode); + } + dLedgerConfig.setEnableDiskForceClean(false); + dividedCommitlogOffset = mappedFile.getFileFromOffset() + mappedFile.getFileSize(); + log.info("Recover old commitlog needWriteMagicCode={} pos={} file={} dividedCommitlogOffset={}", needWriteMagicCode, mappedFile.getFileFromOffset() + mappedFile.getWrotePosition(), mappedFile.getFileName(), dividedCommitlogOffset); + if (needWriteMagicCode) { + byteBuffer.position(mappedFile.getWrotePosition()); + byteBuffer.putInt(mappedFile.getFileSize() - mappedFile.getWrotePosition()); + byteBuffer.putInt(BLANK_MAGIC_CODE); + mappedFile.flush(0); + } + mappedFile.setWrotePosition(mappedFile.getFileSize()); + mappedFile.setCommittedPosition(mappedFile.getFileSize()); + mappedFile.setFlushedPosition(mappedFile.getFileSize()); + dLedgerFileList.getLastMappedFile(dividedCommitlogOffset); + log.info("Will set the initial commitlog offset={} for dledger", dividedCommitlogOffset); + } + + private boolean isMmapFileMatchedRecover(final MmapFile mmapFile) { + ByteBuffer byteBuffer = mmapFile.sliceByteBuffer(); + + int magicCode = byteBuffer.getInt(DLedgerEntry.BODY_OFFSET + MessageDecoder.MESSAGE_MAGIC_CODE_POSITION); + if (magicCode != MESSAGE_MAGIC_CODE) { + return false; + } + + int storeTimestampPosition; + int sysFlag = byteBuffer.getInt(DLedgerEntry.BODY_OFFSET + MessageDecoder.SYSFLAG_POSITION); + if ((sysFlag & MessageSysFlag.BORNHOST_V6_FLAG) == 0) { + storeTimestampPosition = MessageDecoder.MESSAGE_STORE_TIMESTAMP_POSITION; + } else { + // v6 address is 12 byte larger than v4 + storeTimestampPosition = MessageDecoder.MESSAGE_STORE_TIMESTAMP_POSITION + 12; + } + + long storeTimestamp = byteBuffer.getLong(DLedgerEntry.BODY_OFFSET + storeTimestampPosition); + if (storeTimestamp == 0) { + return false; + } + + if (this.defaultMessageStore.getMessageStoreConfig().isMessageIndexEnable() + && this.defaultMessageStore.getMessageStoreConfig().isMessageIndexSafe()) { + if (storeTimestamp <= this.defaultMessageStore.getStoreCheckpoint().getMinTimestampIndex()) { + log.info("dledger find check timestamp, {} {}", + storeTimestamp, + UtilAll.timeMillisToHumanString(storeTimestamp)); + return true; + } + } else { + if (storeTimestamp <= this.defaultMessageStore.getStoreCheckpoint().getMinTimestamp()) { + log.info("dledger find check timestamp, {} {}", + storeTimestamp, + UtilAll.timeMillisToHumanString(storeTimestamp)); + return true; + } + } + + return false; + + } + + @Override + public void recoverNormally(long maxPhyOffsetOfConsumeQueue) throws RocksDBException { + dledgerRecoverNormally(maxPhyOffsetOfConsumeQueue); + } + + @Override + public void recoverAbnormally(long maxPhyOffsetOfConsumeQueue) throws RocksDBException { + dledgerRecoverAbnormally(maxPhyOffsetOfConsumeQueue); + } + + @Override + public DispatchRequest checkMessageAndReturnSize(ByteBuffer byteBuffer, final boolean checkCRC, + final boolean checkDupInfo, final boolean readBody) { + if (isInrecoveringOldCommitlog) { + return super.checkMessageAndReturnSize(byteBuffer, checkCRC, checkDupInfo, readBody); + } + try { + int bodyOffset = DLedgerEntry.BODY_OFFSET; + int pos = byteBuffer.position(); + int magic = byteBuffer.getInt(); + //In dledger, this field is size, it must be gt 0, so it could prevent collision + int magicOld = byteBuffer.getInt(); + if (magicOld == CommitLog.BLANK_MAGIC_CODE + || magicOld == MessageDecoder.MESSAGE_MAGIC_CODE + || magicOld == MessageDecoder.MESSAGE_MAGIC_CODE_V2) { + byteBuffer.position(pos); + return super.checkMessageAndReturnSize(byteBuffer, checkCRC, checkDupInfo, readBody); + } + if (magic == MmapFileList.BLANK_MAGIC_CODE) { + return new DispatchRequest(0, true); + } + byteBuffer.position(pos + bodyOffset); + DispatchRequest dispatchRequest = super.checkMessageAndReturnSize(byteBuffer, checkCRC, checkDupInfo, readBody); + if (dispatchRequest.isSuccess()) { + dispatchRequest.setBufferSize(dispatchRequest.getMsgSize() + bodyOffset); + } else if (dispatchRequest.getMsgSize() > 0) { + dispatchRequest.setBufferSize(dispatchRequest.getMsgSize() + bodyOffset); + } + return dispatchRequest; + } catch (Throwable ignored) { + } + + return new DispatchRequest(-1, false /* success */); + } + + @Override + public boolean resetOffset(long offset) { + //currently, it seems resetOffset has no use + return false; + } + + @Override + public long getBeginTimeInLock() { + return beginTimeInDledgerLock; + } + + private void setMessageInfo(MessageExtBrokerInner msg, int tranType) { + // Set the storage time + msg.setStoreTimestamp(System.currentTimeMillis()); + // Set the message body BODY CRC (consider the most appropriate setting + // on the client) + msg.setBodyCRC(UtilAll.crc32(msg.getBody())); + + InetSocketAddress bornSocketAddress = (InetSocketAddress) msg.getBornHost(); + if (bornSocketAddress.getAddress() instanceof Inet6Address) { + msg.setBornHostV6Flag(); + } + + InetSocketAddress storeSocketAddress = (InetSocketAddress) msg.getStoreHost(); + if (storeSocketAddress.getAddress() instanceof Inet6Address) { + msg.setStoreHostAddressV6Flag(); + } + } + + @Override + public CompletableFuture asyncPutMessage(MessageExtBrokerInner msg) { + + StoreStatsService storeStatsService = this.defaultMessageStore.getStoreStatsService(); + + final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag()); + + setMessageInfo(msg, tranType); + + final String finalTopic = msg.getTopic(); + + msg.setVersion(MessageVersion.MESSAGE_VERSION_V1); + boolean autoMessageVersionOnTopicLen = + this.defaultMessageStore.getMessageStoreConfig().isAutoMessageVersionOnTopicLen(); + if (autoMessageVersionOnTopicLen && msg.getTopic().length() > Byte.MAX_VALUE) { + msg.setVersion(MessageVersion.MESSAGE_VERSION_V2); + } + + // Back to Results + AppendMessageResult appendResult; + AppendFuture dledgerFuture; + EncodeResult encodeResult; + + String topicQueueKey = msg.getTopic() + "-" + msg.getQueueId(); + topicQueueLock.lock(topicQueueKey); + try { + defaultMessageStore.assignOffset(msg); + + encodeResult = this.messageSerializer.serialize(msg); + if (encodeResult.status != AppendMessageStatus.PUT_OK) { + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, new AppendMessageResult(encodeResult.status))); + } + putMessageLock.lock(); //spin or ReentrantLock ,depending on store config + long elapsedTimeInLock; + long queueOffset; + try { + beginTimeInDledgerLock = this.defaultMessageStore.getSystemClock().now(); + queueOffset = getQueueOffsetByKey(msg, tranType); + encodeResult.setQueueOffsetKey(queueOffset, false); + AppendEntryRequest request = new AppendEntryRequest(); + request.setGroup(dLedgerConfig.getGroup()); + request.setRemoteId(dLedgerServer.getMemberState().getSelfId()); + request.setBody(encodeResult.getData()); + dledgerFuture = (AppendFuture) dLedgerServer.handleAppend(request); + if (dledgerFuture.getPos() == -1) { + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.OS_PAGE_CACHE_BUSY, new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR))); + } + long wroteOffset = dledgerFuture.getPos() + DLedgerEntry.BODY_OFFSET; + + int msgIdLength = (msg.getSysFlag() & MessageSysFlag.STOREHOSTADDRESS_V6_FLAG) == 0 ? 4 + 4 + 8 : 16 + 4 + 8; + ByteBuffer buffer = ByteBuffer.allocate(msgIdLength); + + String msgId = MessageDecoder.createMessageId(buffer, msg.getStoreHostBytes(), wroteOffset); + elapsedTimeInLock = this.defaultMessageStore.getSystemClock().now() - beginTimeInDledgerLock; + appendResult = new AppendMessageResult(AppendMessageStatus.PUT_OK, wroteOffset, encodeResult.getData().length, msgId, System.currentTimeMillis(), queueOffset, elapsedTimeInLock); + } finally { + beginTimeInDledgerLock = 0; + putMessageLock.unlock(); + } + + if (elapsedTimeInLock > 500) { + log.warn("[NOTIFYME]putMessage in lock cost time(ms)={}, bodyLength={} AppendMessageResult={}", elapsedTimeInLock, msg.getBody().length, appendResult); + } + + defaultMessageStore.increaseOffset(msg, getMessageNum(msg)); + } catch (Exception e) { + log.error("Put message error", e); + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR))); + } finally { + topicQueueLock.unlock(topicQueueKey); + } + + return dledgerFuture.thenApply(appendEntryResponse -> { + PutMessageStatus putMessageStatus = PutMessageStatus.UNKNOWN_ERROR; + switch (DLedgerResponseCode.valueOf(appendEntryResponse.getCode())) { + case SUCCESS: + putMessageStatus = PutMessageStatus.PUT_OK; + break; + case INCONSISTENT_LEADER: + case NOT_LEADER: + case LEADER_NOT_READY: + case DISK_FULL: + putMessageStatus = PutMessageStatus.SERVICE_NOT_AVAILABLE; + break; + case WAIT_QUORUM_ACK_TIMEOUT: + //Do not return flush_slave_timeout to the client, for the client will ignore it. + putMessageStatus = PutMessageStatus.IN_SYNC_REPLICAS_NOT_ENOUGH; + break; + case LEADER_PENDING_FULL: + putMessageStatus = PutMessageStatus.OS_PAGE_CACHE_BUSY; + break; + } + PutMessageResult putMessageResult = new PutMessageResult(putMessageStatus, appendResult); + if (putMessageStatus == PutMessageStatus.PUT_OK) { + // Statistics + storeStatsService.getSinglePutMessageTopicTimesTotal(finalTopic).add(1); + storeStatsService.getSinglePutMessageTopicSizeTotal(msg.getTopic()).add(appendResult.getWroteBytes()); + } + return putMessageResult; + }); + } + + @Override + public CompletableFuture asyncPutMessages(MessageExtBatch messageExtBatch) { + final int tranType = MessageSysFlag.getTransactionValue(messageExtBatch.getSysFlag()); + + if (tranType != MessageSysFlag.TRANSACTION_NOT_TYPE) { + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null)); + } + if (messageExtBatch.getDelayTimeLevel() > 0) { + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null)); + } + + // Set the storage time + messageExtBatch.setStoreTimestamp(System.currentTimeMillis()); + + StoreStatsService storeStatsService = this.defaultMessageStore.getStoreStatsService(); + + InetSocketAddress bornSocketAddress = (InetSocketAddress) messageExtBatch.getBornHost(); + if (bornSocketAddress.getAddress() instanceof Inet6Address) { + messageExtBatch.setBornHostV6Flag(); + } + + InetSocketAddress storeSocketAddress = (InetSocketAddress) messageExtBatch.getStoreHost(); + if (storeSocketAddress.getAddress() instanceof Inet6Address) { + messageExtBatch.setStoreHostAddressV6Flag(); + } + + messageExtBatch.setVersion(MessageVersion.MESSAGE_VERSION_V1); + boolean autoMessageVersionOnTopicLen = + this.defaultMessageStore.getMessageStoreConfig().isAutoMessageVersionOnTopicLen(); + if (autoMessageVersionOnTopicLen && messageExtBatch.getTopic().length() > Byte.MAX_VALUE) { + messageExtBatch.setVersion(MessageVersion.MESSAGE_VERSION_V2); + } + + // Back to Results + AppendMessageResult appendResult; + BatchAppendFuture dledgerFuture; + EncodeResult encodeResult; + + encodeResult = this.messageSerializer.serialize(messageExtBatch); + if (encodeResult.status != AppendMessageStatus.PUT_OK) { + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, new AppendMessageResult(encodeResult + .status))); + } + + int batchNum = encodeResult.batchData.size(); + topicQueueLock.lock(encodeResult.queueOffsetKey); + try { + defaultMessageStore.assignOffset(messageExtBatch); + + putMessageLock.lock(); //spin or ReentrantLock ,depending on store config + msgIdBuilder.setLength(0); + long elapsedTimeInLock; + long queueOffset; + int msgNum = 0; + try { + beginTimeInDledgerLock = this.defaultMessageStore.getSystemClock().now(); + queueOffset = getQueueOffsetByKey(messageExtBatch, tranType); + encodeResult.setQueueOffsetKey(queueOffset, true); + BatchAppendEntryRequest request = new BatchAppendEntryRequest(); + request.setGroup(dLedgerConfig.getGroup()); + request.setRemoteId(dLedgerServer.getMemberState().getSelfId()); + request.setBatchMsgs(encodeResult.batchData); + AppendFuture appendFuture = (AppendFuture) dLedgerServer.handleAppend(request); + if (appendFuture.getPos() == -1) { + log.warn("HandleAppend return false due to error code {}", appendFuture.get().getCode()); + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.OS_PAGE_CACHE_BUSY, new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR))); + } + dledgerFuture = (BatchAppendFuture) appendFuture; + + long wroteOffset = 0; + + int msgIdLength = (messageExtBatch.getSysFlag() & MessageSysFlag.STOREHOSTADDRESS_V6_FLAG) == 0 ? 4 + 4 + 8 : 16 + 4 + 8; + ByteBuffer buffer = ByteBuffer.allocate(msgIdLength); + + boolean isFirstOffset = true; + long firstWroteOffset = 0; + for (long pos : dledgerFuture.getPositions()) { + wroteOffset = pos + DLedgerEntry.BODY_OFFSET; + if (isFirstOffset) { + firstWroteOffset = wroteOffset; + isFirstOffset = false; + } + String msgId = MessageDecoder.createMessageId(buffer, messageExtBatch.getStoreHostBytes(), wroteOffset); + if (msgIdBuilder.length() > 0) { + msgIdBuilder.append(',').append(msgId); + } else { + msgIdBuilder.append(msgId); + } + msgNum++; + } + + elapsedTimeInLock = this.defaultMessageStore.getSystemClock().now() - beginTimeInDledgerLock; + appendResult = new AppendMessageResult(AppendMessageStatus.PUT_OK, firstWroteOffset, encodeResult.totalMsgLen, + msgIdBuilder.toString(), System.currentTimeMillis(), queueOffset, elapsedTimeInLock); + appendResult.setMsgNum(msgNum); + } finally { + beginTimeInDledgerLock = 0; + putMessageLock.unlock(); + } + + if (elapsedTimeInLock > 500) { + log.warn("[NOTIFYME]putMessage in lock cost time(ms)={}, bodyLength={} AppendMessageResult={}", + elapsedTimeInLock, messageExtBatch.getBody().length, appendResult); + } + + defaultMessageStore.increaseOffset(messageExtBatch, (short) batchNum); + + } catch (Exception e) { + log.error("Put message error", e); + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR))); + } finally { + topicQueueLock.unlock(encodeResult.queueOffsetKey); + } + + return dledgerFuture.thenApply(appendEntryResponse -> { + PutMessageStatus putMessageStatus = PutMessageStatus.UNKNOWN_ERROR; + switch (DLedgerResponseCode.valueOf(appendEntryResponse.getCode())) { + case SUCCESS: + putMessageStatus = PutMessageStatus.PUT_OK; + break; + case INCONSISTENT_LEADER: + case NOT_LEADER: + case LEADER_NOT_READY: + case DISK_FULL: + putMessageStatus = PutMessageStatus.SERVICE_NOT_AVAILABLE; + break; + case WAIT_QUORUM_ACK_TIMEOUT: + //Do not return flush_slave_timeout to the client, for the client will ignore it. + putMessageStatus = PutMessageStatus.IN_SYNC_REPLICAS_NOT_ENOUGH; + break; + case LEADER_PENDING_FULL: + putMessageStatus = PutMessageStatus.OS_PAGE_CACHE_BUSY; + break; + } + PutMessageResult putMessageResult = new PutMessageResult(putMessageStatus, appendResult); + if (putMessageStatus == PutMessageStatus.PUT_OK) { + // Statistics + storeStatsService.getSinglePutMessageTopicTimesTotal(messageExtBatch.getTopic()).add(appendResult.getMsgNum()); + storeStatsService.getSinglePutMessageTopicSizeTotal(messageExtBatch.getTopic()).add(appendResult.getWroteBytes()); + } + return putMessageResult; + }); + } + + @Override + public SelectMappedBufferResult getMessage(final long offset, final int size) { + if (offset < dividedCommitlogOffset) { + return super.getMessage(offset, size); + } + int mappedFileSize = this.dLedgerServer.getdLedgerConfig().getMappedFileSizeForEntryData(); + MmapFile mappedFile = this.dLedgerFileList.findMappedFileByOffset(offset, offset == 0); + if (mappedFile != null) { + int pos = (int) (offset % mappedFileSize); + return convertSbr(mappedFile.selectMappedBuffer(pos, size)); + } + return null; + } + + @Override + public long rollNextFile(final long offset) { + int mappedFileSize = this.defaultMessageStore.getMessageStoreConfig().getMappedFileSizeCommitLog(); + return offset + mappedFileSize - offset % mappedFileSize; + } + + @Override + public void destroy() { + super.destroy(); + dLedgerFileList.destroy(); + } + + @Override + public boolean appendData(long startOffset, byte[] data, int dataStart, int dataLength) { + //the old ha service will invoke method, here to prevent it + return false; + } + + @Override + public void checkSelf() { + dLedgerFileList.checkSelf(); + } + + @Override + public long lockTimeMills() { + long diff = 0; + long begin = this.beginTimeInDledgerLock; + if (begin > 0) { + diff = this.defaultMessageStore.now() - begin; + } + + if (diff < 0) { + diff = 0; + } + + return diff; + } + + private long getQueueOffsetByKey(MessageExtBrokerInner msg, int tranType) { + Long queueOffset = msg.getQueueOffset(); + + // Transaction messages that require special handling + switch (tranType) { + // Prepared and Rollback message is not consumed, will not enter the + // consumer queuec + case MessageSysFlag.TRANSACTION_PREPARED_TYPE: + case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE: + queueOffset = 0L; + break; + case MessageSysFlag.TRANSACTION_NOT_TYPE: + case MessageSysFlag.TRANSACTION_COMMIT_TYPE: + default: + break; + } + return queueOffset; + } + + class EncodeResult { + private String queueOffsetKey; + private ByteBuffer data; + private List batchData; + private AppendMessageStatus status; + private int totalMsgLen; + + public EncodeResult(AppendMessageStatus status, ByteBuffer data, String queueOffsetKey) { + this.data = data; + this.status = status; + this.queueOffsetKey = queueOffsetKey; + } + + public void setQueueOffsetKey(long offset, boolean isBatch) { + if (!isBatch) { + this.data.putLong(MessageDecoder.QUEUE_OFFSET_POSITION, offset); + return; + } + + for (byte[] data : batchData) { + ByteBuffer.wrap(data).putLong(MessageDecoder.QUEUE_OFFSET_POSITION, offset++); + } + } + + public byte[] getData() { + return data.array(); + } + + public EncodeResult(AppendMessageStatus status, String queueOffsetKey, List batchData, + int totalMsgLen) { + this.batchData = batchData; + this.status = status; + this.queueOffsetKey = queueOffsetKey; + this.totalMsgLen = totalMsgLen; + } + } + + class MessageSerializer { + + // The maximum length of the message body + private final int maxMessageBodySize; + + MessageSerializer(final int size) { + this.maxMessageBodySize = size; + } + + public EncodeResult serialize(final MessageExtBrokerInner msgInner) { + // STORETIMESTAMP + STOREHOSTADDRESS + OFFSET
    + + // PHY OFFSET + long wroteOffset = 0; + + long queueOffset = 0; + + int sysflag = msgInner.getSysFlag(); + + int bornHostLength = (sysflag & MessageSysFlag.BORNHOST_V6_FLAG) == 0 ? 4 + 4 : 16 + 4; + int storeHostLength = (sysflag & MessageSysFlag.STOREHOSTADDRESS_V6_FLAG) == 0 ? 4 + 4 : 16 + 4; + ByteBuffer bornHostHolder = ByteBuffer.allocate(bornHostLength); + ByteBuffer storeHostHolder = ByteBuffer.allocate(storeHostLength); + + String key = msgInner.getTopic() + "-" + msgInner.getQueueId(); + + /** + * Serialize message + */ + final byte[] propertiesData = + msgInner.getPropertiesString() == null ? null : msgInner.getPropertiesString().getBytes(MessageDecoder.CHARSET_UTF8); + + final int propertiesLength = propertiesData == null ? 0 : propertiesData.length; + + if (propertiesLength > Short.MAX_VALUE) { + log.warn("putMessage message properties length too long. length={}", propertiesData.length); + return new EncodeResult(AppendMessageStatus.PROPERTIES_SIZE_EXCEEDED, null, key); + } + + final byte[] topicData = msgInner.getTopic().getBytes(MessageDecoder.CHARSET_UTF8); + final int topicLength = topicData.length; + + final int bodyLength = msgInner.getBody() == null ? 0 : msgInner.getBody().length; + + final int msgLen = MessageExtEncoder.calMsgLength(msgInner.getVersion(), msgInner.getSysFlag(), bodyLength, topicLength, propertiesLength); + + ByteBuffer msgStoreItemMemory = ByteBuffer.allocate(msgLen); + + // Exceeds the maximum message + if (bodyLength > this.maxMessageBodySize) { + DLedgerCommitLog.log.warn("message body size exceeded, msg total size: " + msgLen + ", msg body size: " + bodyLength + + ", maxMessageBodySize: " + this.maxMessageBodySize); + return new EncodeResult(AppendMessageStatus.MESSAGE_SIZE_EXCEEDED, null, key); + } + // Initialization of storage space + this.resetByteBuffer(msgStoreItemMemory, msgLen); + // 1 TOTALSIZE + msgStoreItemMemory.putInt(msgLen); + // 2 MAGICCODE + msgStoreItemMemory.putInt(msgInner.getVersion().getMagicCode()); + // 3 BODYCRC + msgStoreItemMemory.putInt(msgInner.getBodyCRC()); + // 4 QUEUEID + msgStoreItemMemory.putInt(msgInner.getQueueId()); + // 5 FLAG + msgStoreItemMemory.putInt(msgInner.getFlag()); + // 6 QUEUEOFFSET + msgStoreItemMemory.putLong(queueOffset); + // 7 PHYSICALOFFSET + msgStoreItemMemory.putLong(wroteOffset); + // 8 SYSFLAG + msgStoreItemMemory.putInt(msgInner.getSysFlag()); + // 9 BORNTIMESTAMP + msgStoreItemMemory.putLong(msgInner.getBornTimestamp()); + // 10 BORNHOST + resetByteBuffer(bornHostHolder, bornHostLength); + msgStoreItemMemory.put(msgInner.getBornHostBytes(bornHostHolder)); + // 11 STORETIMESTAMP + msgStoreItemMemory.putLong(msgInner.getStoreTimestamp()); + // 12 STOREHOSTADDRESS + resetByteBuffer(storeHostHolder, storeHostLength); + msgStoreItemMemory.put(msgInner.getStoreHostBytes(storeHostHolder)); + //this.msgBatchMemory.put(msgInner.getStoreHostBytes()); + // 13 RECONSUMETIMES + msgStoreItemMemory.putInt(msgInner.getReconsumeTimes()); + // 14 Prepared Transaction Offset + msgStoreItemMemory.putLong(msgInner.getPreparedTransactionOffset()); + // 15 BODY + msgStoreItemMemory.putInt(bodyLength); + if (bodyLength > 0) { + msgStoreItemMemory.put(msgInner.getBody()); + } + // 16 TOPIC + msgInner.getVersion().putTopicLength(msgStoreItemMemory, topicLength); + msgStoreItemMemory.put(topicData); + // 17 PROPERTIES + msgStoreItemMemory.putShort((short) propertiesLength); + if (propertiesLength > 0) { + msgStoreItemMemory.put(propertiesData); + } + return new EncodeResult(AppendMessageStatus.PUT_OK, msgStoreItemMemory, key); + } + + public EncodeResult serialize(final MessageExtBatch messageExtBatch) { + String key = messageExtBatch.getTopic() + "-" + messageExtBatch.getQueueId(); + + int totalMsgLen = 0; + ByteBuffer messagesByteBuff = messageExtBatch.wrap(); + + int totalLength = messagesByteBuff.limit(); + if (totalLength > this.maxMessageBodySize) { + CommitLog.log.warn("message body size exceeded, msg body size: " + totalLength + + ", maxMessageBodySize: " + this.maxMessageBodySize); + throw new RuntimeException("message size exceeded"); + } + + List batchBody = new LinkedList<>(); + + int sysFlag = messageExtBatch.getSysFlag(); + int bornHostLength = (sysFlag & MessageSysFlag.BORNHOST_V6_FLAG) == 0 ? 4 + 4 : 16 + 4; + int storeHostLength = (sysFlag & MessageSysFlag.STOREHOSTADDRESS_V6_FLAG) == 0 ? 4 + 4 : 16 + 4; + ByteBuffer bornHostHolder = ByteBuffer.allocate(bornHostLength); + ByteBuffer storeHostHolder = ByteBuffer.allocate(storeHostLength); + + while (messagesByteBuff.hasRemaining()) { + // 1 TOTALSIZE + messagesByteBuff.getInt(); + // 2 MAGICCODE + messagesByteBuff.getInt(); + // 3 BODYCRC + messagesByteBuff.getInt(); + // 4 FLAG + int flag = messagesByteBuff.getInt(); + // 5 BODY + int bodyLen = messagesByteBuff.getInt(); + int bodyPos = messagesByteBuff.position(); + int bodyCrc = UtilAll.crc32(messagesByteBuff.array(), bodyPos, bodyLen); + messagesByteBuff.position(bodyPos + bodyLen); + // 6 properties + short propertiesLen = messagesByteBuff.getShort(); + int propertiesPos = messagesByteBuff.position(); + messagesByteBuff.position(propertiesPos + propertiesLen); + + final byte[] topicData = messageExtBatch.getTopic().getBytes(MessageDecoder.CHARSET_UTF8); + + final int topicLength = topicData.length; + + final int msgLen = MessageExtEncoder.calMsgLength(messageExtBatch.getVersion(), messageExtBatch.getSysFlag(), bodyLen, topicLength, propertiesLen); + ByteBuffer msgStoreItemMemory = ByteBuffer.allocate(msgLen); + + totalMsgLen += msgLen; + + // Initialization of storage space + this.resetByteBuffer(msgStoreItemMemory, msgLen); + // 1 TOTALSIZE + msgStoreItemMemory.putInt(msgLen); + // 2 MAGICCODE + msgStoreItemMemory.putInt(messageExtBatch.getVersion().getMagicCode()); + // 3 BODYCRC + msgStoreItemMemory.putInt(bodyCrc); + // 4 QUEUEID + msgStoreItemMemory.putInt(messageExtBatch.getQueueId()); + // 5 FLAG + msgStoreItemMemory.putInt(flag); + // 6 QUEUEOFFSET + msgStoreItemMemory.putLong(0L); + // 7 PHYSICALOFFSET + msgStoreItemMemory.putLong(0); + // 8 SYSFLAG + msgStoreItemMemory.putInt(messageExtBatch.getSysFlag()); + // 9 BORNTIMESTAMP + msgStoreItemMemory.putLong(messageExtBatch.getBornTimestamp()); + // 10 BORNHOST + resetByteBuffer(bornHostHolder, bornHostLength); + msgStoreItemMemory.put(messageExtBatch.getBornHostBytes(bornHostHolder)); + // 11 STORETIMESTAMP + msgStoreItemMemory.putLong(messageExtBatch.getStoreTimestamp()); + // 12 STOREHOSTADDRESS + resetByteBuffer(storeHostHolder, storeHostLength); + msgStoreItemMemory.put(messageExtBatch.getStoreHostBytes(storeHostHolder)); + // 13 RECONSUMETIMES + msgStoreItemMemory.putInt(messageExtBatch.getReconsumeTimes()); + // 14 Prepared Transaction Offset + msgStoreItemMemory.putLong(0); + // 15 BODY + msgStoreItemMemory.putInt(bodyLen); + if (bodyLen > 0) { + msgStoreItemMemory.put(messagesByteBuff.array(), bodyPos, bodyLen); + } + // 16 TOPIC + messageExtBatch.getVersion().putTopicLength(msgStoreItemMemory, topicLength); + msgStoreItemMemory.put(topicData); + // 17 PROPERTIES + msgStoreItemMemory.putShort(propertiesLen); + if (propertiesLen > 0) { + msgStoreItemMemory.put(messagesByteBuff.array(), propertiesPos, propertiesLen); + } + byte[] data = new byte[msgLen]; + msgStoreItemMemory.clear(); + msgStoreItemMemory.get(data); + batchBody.add(data); + } + + return new EncodeResult(AppendMessageStatus.PUT_OK, key, batchBody, totalMsgLen); + } + + private void resetByteBuffer(final ByteBuffer byteBuffer, final int limit) { + byteBuffer.flip(); + byteBuffer.limit(limit); + } + } + + public static class DLedgerSelectMappedBufferResult extends SelectMappedBufferResult { + + private SelectMmapBufferResult sbr; + + public DLedgerSelectMappedBufferResult(SelectMmapBufferResult sbr) { + super(sbr.getStartOffset(), sbr.getByteBuffer(), sbr.getSize(), null); + this.sbr = sbr; + } + + @Override + public synchronized void release() { + super.release(); + if (sbr != null) { + sbr.release(); + } + } + + } + + public DLedgerServer getdLedgerServer() { + return dLedgerServer; + } + + public int getId() { + return id; + } + + public long getDividedCommitlogOffset() { + return dividedCommitlogOffset; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/ha/DefaultHAClient.java b/store/src/main/java/org/apache/rocketmq/store/ha/DefaultHAClient.java new file mode 100644 index 00000000000..530d295ae96 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ha/DefaultHAClient.java @@ -0,0 +1,411 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.ha; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.store.DefaultMessageStore; + +public class DefaultHAClient extends ServiceThread implements HAClient { + + /** + * Report header buffer size. Schema: slaveMaxOffset. Format: + * + *
    +     * ┌───────────────────────────────────────────────┐
    +     * │                  slaveMaxOffset               │
    +     * │                    (8bytes)                   │
    +     * ├───────────────────────────────────────────────┤
    +     * │                                               │
    +     * │                  Report Header                │
    +     * 
    + *

    + */ + public static final int REPORT_HEADER_SIZE = 8; + + private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + private static final int READ_MAX_BUFFER_SIZE = 1024 * 1024 * 4; + private final AtomicReference masterHaAddress = new AtomicReference<>(); + private final AtomicReference masterAddress = new AtomicReference<>(); + private final ByteBuffer reportOffset = ByteBuffer.allocate(REPORT_HEADER_SIZE); + private SocketChannel socketChannel; + private Selector selector; + /** + * last time that slave reads date from master. + */ + private long lastReadTimestamp = System.currentTimeMillis(); + /** + * last time that slave reports offset to master. + */ + private long lastWriteTimestamp = System.currentTimeMillis(); + + private long currentReportedOffset = 0; + private int dispatchPosition = 0; + private ByteBuffer byteBufferRead = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE); + private ByteBuffer byteBufferBackup = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE); + private DefaultMessageStore defaultMessageStore; + private volatile HAConnectionState currentState = HAConnectionState.READY; + private FlowMonitor flowMonitor; + + public DefaultHAClient(DefaultMessageStore defaultMessageStore) throws IOException { + this.selector = NetworkUtil.openSelector(); + this.defaultMessageStore = defaultMessageStore; + this.flowMonitor = new FlowMonitor(defaultMessageStore.getMessageStoreConfig()); + } + + public void updateHaMasterAddress(final String newAddr) { + String currentAddr = this.masterHaAddress.get(); + if (masterHaAddress.compareAndSet(currentAddr, newAddr)) { + log.info("update master ha address, OLD: " + currentAddr + " NEW: " + newAddr); + } + } + + public void updateMasterAddress(final String newAddr) { + String currentAddr = this.masterAddress.get(); + if (masterAddress.compareAndSet(currentAddr, newAddr)) { + log.info("update master address, OLD: " + currentAddr + " NEW: " + newAddr); + } + } + + public String getHaMasterAddress() { + return this.masterHaAddress.get(); + } + + public String getMasterAddress() { + return this.masterAddress.get(); + } + + private boolean isTimeToReportOffset() { + long interval = defaultMessageStore.now() - this.lastWriteTimestamp; + return interval > defaultMessageStore.getMessageStoreConfig().getHaSendHeartbeatInterval(); + } + + private boolean reportSlaveMaxOffset(final long maxOffset) { + this.reportOffset.position(0); + this.reportOffset.limit(REPORT_HEADER_SIZE); + this.reportOffset.putLong(maxOffset); + this.reportOffset.position(0); + this.reportOffset.limit(REPORT_HEADER_SIZE); + + for (int i = 0; i < 3 && this.reportOffset.hasRemaining(); i++) { + try { + this.socketChannel.write(this.reportOffset); + } catch (IOException e) { + log.error(this.getServiceName() + + "reportSlaveMaxOffset this.socketChannel.write exception", e); + return false; + } + } + lastWriteTimestamp = this.defaultMessageStore.getSystemClock().now(); + return !this.reportOffset.hasRemaining(); + } + + private void reallocateByteBuffer() { + int remain = READ_MAX_BUFFER_SIZE - this.dispatchPosition; + if (remain > 0) { + this.byteBufferRead.position(this.dispatchPosition); + + this.byteBufferBackup.position(0); + this.byteBufferBackup.limit(READ_MAX_BUFFER_SIZE); + this.byteBufferBackup.put(this.byteBufferRead); + } + + this.swapByteBuffer(); + + this.byteBufferRead.position(remain); + this.byteBufferRead.limit(READ_MAX_BUFFER_SIZE); + this.dispatchPosition = 0; + } + + private void swapByteBuffer() { + ByteBuffer tmp = this.byteBufferRead; + this.byteBufferRead = this.byteBufferBackup; + this.byteBufferBackup = tmp; + } + + private boolean processReadEvent() { + int readSizeZeroTimes = 0; + while (this.byteBufferRead.hasRemaining()) { + try { + int readSize = this.socketChannel.read(this.byteBufferRead); + if (readSize > 0) { + flowMonitor.addByteCountTransferred(readSize); + readSizeZeroTimes = 0; + boolean result = this.dispatchReadRequest(); + if (!result) { + log.error("HAClient, dispatchReadRequest error"); + return false; + } + lastReadTimestamp = System.currentTimeMillis(); + } else if (readSize == 0) { + if (++readSizeZeroTimes >= 3) { + break; + } + } else { + log.info("HAClient, processReadEvent read socket < 0"); + return false; + } + } catch (IOException e) { + log.info("HAClient, processReadEvent read socket exception", e); + return false; + } + } + + return true; + } + + private boolean dispatchReadRequest() { + int readSocketPos = this.byteBufferRead.position(); + + while (true) { + int diff = this.byteBufferRead.position() - this.dispatchPosition; + if (diff >= DefaultHAConnection.TRANSFER_HEADER_SIZE) { + long masterPhyOffset = this.byteBufferRead.getLong(this.dispatchPosition); + int bodySize = this.byteBufferRead.getInt(this.dispatchPosition + 8); + + long slavePhyOffset = this.defaultMessageStore.getMaxPhyOffset(); + + if (slavePhyOffset != 0) { + if (slavePhyOffset != masterPhyOffset) { + log.error("master pushed offset not equal the max phy offset in slave, SLAVE: " + + slavePhyOffset + " MASTER: " + masterPhyOffset); + return false; + } + } + + if (diff >= (DefaultHAConnection.TRANSFER_HEADER_SIZE + bodySize)) { + byte[] bodyData = byteBufferRead.array(); + int dataStart = this.dispatchPosition + DefaultHAConnection.TRANSFER_HEADER_SIZE; + + this.defaultMessageStore.appendToCommitLog( + masterPhyOffset, bodyData, dataStart, bodySize); + + this.byteBufferRead.position(readSocketPos); + this.dispatchPosition += DefaultHAConnection.TRANSFER_HEADER_SIZE + bodySize; + + if (!reportSlaveMaxOffsetPlus()) { + return false; + } + + continue; + } + } + + if (!this.byteBufferRead.hasRemaining()) { + this.reallocateByteBuffer(); + } + + break; + } + + return true; + } + + private boolean reportSlaveMaxOffsetPlus() { + boolean result = true; + long currentPhyOffset = this.defaultMessageStore.getMaxPhyOffset(); + if (currentPhyOffset > this.currentReportedOffset) { + this.currentReportedOffset = currentPhyOffset; + result = this.reportSlaveMaxOffset(this.currentReportedOffset); + if (!result) { + this.closeMaster(); + log.error("HAClient, reportSlaveMaxOffset error, " + this.currentReportedOffset); + } + } + + return result; + } + + public void changeCurrentState(HAConnectionState currentState) { + log.info("change state to {}", currentState); + this.currentState = currentState; + } + + public boolean connectMaster() throws ClosedChannelException { + if (null == socketChannel) { + String addr = this.masterHaAddress.get(); + if (addr != null) { + SocketAddress socketAddress = NetworkUtil.string2SocketAddress(addr); + this.socketChannel = RemotingHelper.connect(socketAddress); + if (this.socketChannel != null) { + this.socketChannel.register(this.selector, SelectionKey.OP_READ); + log.info("HAClient connect to master {}", addr); + this.changeCurrentState(HAConnectionState.TRANSFER); + } + } + + this.currentReportedOffset = this.defaultMessageStore.getMaxPhyOffset(); + + this.lastReadTimestamp = System.currentTimeMillis(); + } + + return this.socketChannel != null; + } + + public void closeMaster() { + if (null != this.socketChannel) { + try { + + SelectionKey sk = this.socketChannel.keyFor(this.selector); + if (sk != null) { + sk.cancel(); + } + + this.socketChannel.close(); + + this.socketChannel = null; + + log.info("HAClient close connection with master {}", this.masterHaAddress.get()); + this.changeCurrentState(HAConnectionState.READY); + } catch (IOException e) { + log.warn("closeMaster exception. ", e); + } + + this.lastReadTimestamp = 0; + this.dispatchPosition = 0; + + this.byteBufferBackup.position(0); + this.byteBufferBackup.limit(READ_MAX_BUFFER_SIZE); + + this.byteBufferRead.position(0); + this.byteBufferRead.limit(READ_MAX_BUFFER_SIZE); + } + } + + @Override + public void run() { + log.info(this.getServiceName() + " service started"); + + this.flowMonitor.start(); + + while (!this.isStopped()) { + try { + switch (this.currentState) { + case SHUTDOWN: + this.flowMonitor.shutdown(true); + return; + case READY: + if (!this.connectMaster()) { + log.warn("HAClient connect to master {} failed", this.masterHaAddress.get()); + this.waitForRunning(1000 * 5); + } + continue; + case TRANSFER: + if (!transferFromMaster()) { + closeMasterAndWait(); + continue; + } + break; + default: + this.waitForRunning(1000 * 2); + continue; + } + long interval = this.defaultMessageStore.now() - this.lastReadTimestamp; + if (interval > this.defaultMessageStore.getMessageStoreConfig().getHaHousekeepingInterval()) { + log.warn("AutoRecoverHAClient, housekeeping, found this connection[" + this.masterHaAddress + + "] expired, " + interval); + this.closeMaster(); + log.warn("AutoRecoverHAClient, master not response some time, so close connection"); + } + } catch (Exception e) { + log.warn(this.getServiceName() + " service has exception. ", e); + this.closeMasterAndWait(); + } + } + + this.flowMonitor.shutdown(true); + log.info(this.getServiceName() + " service end"); + } + + private boolean transferFromMaster() throws IOException { + boolean result; + if (this.isTimeToReportOffset()) { + log.info("Slave report current offset {}", this.currentReportedOffset); + result = this.reportSlaveMaxOffset(this.currentReportedOffset); + if (!result) { + return false; + } + } + + this.selector.select(1000); + + result = this.processReadEvent(); + if (!result) { + return false; + } + + return reportSlaveMaxOffsetPlus(); + } + + public void closeMasterAndWait() { + this.closeMaster(); + this.waitForRunning(1000 * 5); + } + + public long getLastWriteTimestamp() { + return this.lastWriteTimestamp; + } + + public long getLastReadTimestamp() { + return lastReadTimestamp; + } + + @Override + public HAConnectionState getCurrentState() { + return currentState; + } + + @Override + public long getTransferredByteInSecond() { + return flowMonitor.getTransferredByteInSecond(); + } + + @Override + public void shutdown() { + this.changeCurrentState(HAConnectionState.SHUTDOWN); + this.flowMonitor.shutdown(); + super.shutdown(); + + closeMaster(); + try { + this.selector.close(); + } catch (IOException e) { + log.warn("Close the selector of AutoRecoverHAClient error, ", e); + } + } + + @Override + public String getServiceName() { + if (this.defaultMessageStore != null && this.defaultMessageStore.getBrokerConfig().isInBrokerContainer()) { + return this.defaultMessageStore.getBrokerIdentity().getIdentifier() + DefaultHAClient.class.getSimpleName(); + } + return DefaultHAClient.class.getSimpleName(); + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/ha/DefaultHAConnection.java b/store/src/main/java/org/apache/rocketmq/store/ha/DefaultHAConnection.java new file mode 100644 index 00000000000..5dd24410ed6 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ha/DefaultHAConnection.java @@ -0,0 +1,476 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.ha; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.netty.NettySystemConfig; +import org.apache.rocketmq.store.SelectMappedBufferResult; + +public class DefaultHAConnection implements HAConnection { + + /** + * Transfer Header buffer size. Schema: physic offset and body size. Format: + * + *

    +     * ┌───────────────────────────────────────────────┬───────────────────────┐
    +     * │                  physicOffset                 │         bodySize      │
    +     * │                    (8bytes)                   │         (4bytes)      │
    +     * ├───────────────────────────────────────────────┴───────────────────────┤
    +     * │                                                                       │
    +     * │                           Transfer Header                             │
    +     * 
    + *

    + */ + public static final int TRANSFER_HEADER_SIZE = 8 + 4; + + private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + private final DefaultHAService haService; + private final SocketChannel socketChannel; + private final String clientAddress; + private WriteSocketService writeSocketService; + private ReadSocketService readSocketService; + private volatile HAConnectionState currentState = HAConnectionState.TRANSFER; + private volatile long slaveRequestOffset = -1; + private volatile long slaveAckOffset = -1; + private FlowMonitor flowMonitor; + + public DefaultHAConnection(final DefaultHAService haService, final SocketChannel socketChannel) throws IOException { + this.haService = haService; + this.socketChannel = socketChannel; + this.clientAddress = this.socketChannel.socket().getRemoteSocketAddress().toString(); + this.socketChannel.configureBlocking(false); + this.socketChannel.socket().setSoLinger(false, -1); + this.socketChannel.socket().setTcpNoDelay(true); + if (NettySystemConfig.socketSndbufSize > 0) { + this.socketChannel.socket().setReceiveBufferSize(NettySystemConfig.socketSndbufSize); + } + if (NettySystemConfig.socketRcvbufSize > 0) { + this.socketChannel.socket().setSendBufferSize(NettySystemConfig.socketRcvbufSize); + } + this.writeSocketService = new WriteSocketService(this.socketChannel); + this.readSocketService = new ReadSocketService(this.socketChannel); + this.haService.getConnectionCount().incrementAndGet(); + this.flowMonitor = new FlowMonitor(haService.getDefaultMessageStore().getMessageStoreConfig()); + } + + public void start() { + changeCurrentState(HAConnectionState.TRANSFER); + this.flowMonitor.start(); + this.readSocketService.start(); + this.writeSocketService.start(); + } + + public void shutdown() { + changeCurrentState(HAConnectionState.SHUTDOWN); + this.writeSocketService.shutdown(true); + this.readSocketService.shutdown(true); + this.flowMonitor.shutdown(true); + this.close(); + } + + public void close() { + if (this.socketChannel != null) { + try { + this.socketChannel.close(); + } catch (IOException e) { + log.error("", e); + } + } + } + + public SocketChannel getSocketChannel() { + return socketChannel; + } + + public void changeCurrentState(HAConnectionState currentState) { + log.info("change state to {}", currentState); + this.currentState = currentState; + } + + @Override + public HAConnectionState getCurrentState() { + return currentState; + } + + @Override + public String getClientAddress() { + return this.clientAddress; + } + + @Override + public long getSlaveAckOffset() { + return slaveAckOffset; + } + + public long getTransferredByteInSecond() { + return this.flowMonitor.getTransferredByteInSecond(); + } + + public long getTransferFromWhere() { + return writeSocketService.getNextTransferFromWhere(); + } + + class ReadSocketService extends ServiceThread { + private static final int READ_MAX_BUFFER_SIZE = 1024 * 1024; + private final Selector selector; + private final SocketChannel socketChannel; + private final ByteBuffer byteBufferRead = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE); + private int processPosition = 0; + private volatile long lastReadTimestamp = System.currentTimeMillis(); + + public ReadSocketService(final SocketChannel socketChannel) throws IOException { + this.selector = NetworkUtil.openSelector(); + this.socketChannel = socketChannel; + this.socketChannel.register(this.selector, SelectionKey.OP_READ); + this.setDaemon(true); + } + + @Override + public void run() { + log.info(this.getServiceName() + " service started"); + + while (!this.isStopped()) { + try { + this.selector.select(1000); + boolean ok = this.processReadEvent(); + if (!ok) { + log.error("processReadEvent error"); + break; + } + + long interval = DefaultHAConnection.this.haService.getDefaultMessageStore().getSystemClock().now() - this.lastReadTimestamp; + if (interval > DefaultHAConnection.this.haService.getDefaultMessageStore().getMessageStoreConfig().getHaHousekeepingInterval()) { + log.warn("ha housekeeping, found this connection[" + DefaultHAConnection.this.clientAddress + "] expired, " + interval); + break; + } + } catch (Exception e) { + log.error(this.getServiceName() + " service has exception.", e); + break; + } + } + + changeCurrentState(HAConnectionState.SHUTDOWN); + + this.makeStop(); + + writeSocketService.makeStop(); + + haService.removeConnection(DefaultHAConnection.this); + + DefaultHAConnection.this.haService.getConnectionCount().decrementAndGet(); + + SelectionKey sk = this.socketChannel.keyFor(this.selector); + if (sk != null) { + sk.cancel(); + } + + try { + this.selector.close(); + this.socketChannel.close(); + } catch (IOException e) { + log.error("", e); + } + + flowMonitor.shutdown(true); + + log.info(this.getServiceName() + " service end"); + } + + @Override + public String getServiceName() { + if (haService.getDefaultMessageStore().getBrokerConfig().isInBrokerContainer()) { + return haService.getDefaultMessageStore().getBrokerIdentity().getIdentifier() + ReadSocketService.class.getSimpleName(); + } + return ReadSocketService.class.getSimpleName(); + } + + private boolean processReadEvent() { + int readSizeZeroTimes = 0; + + if (!this.byteBufferRead.hasRemaining()) { + this.byteBufferRead.flip(); + this.processPosition = 0; + } + + while (this.byteBufferRead.hasRemaining()) { + try { + int readSize = this.socketChannel.read(this.byteBufferRead); + if (readSize > 0) { + readSizeZeroTimes = 0; + this.lastReadTimestamp = DefaultHAConnection.this.haService.getDefaultMessageStore().getSystemClock().now(); + if ((this.byteBufferRead.position() - this.processPosition) >= DefaultHAClient.REPORT_HEADER_SIZE) { + int pos = this.byteBufferRead.position() - (this.byteBufferRead.position() % DefaultHAClient.REPORT_HEADER_SIZE); + long readOffset = this.byteBufferRead.getLong(pos - 8); + this.processPosition = pos; + + DefaultHAConnection.this.slaveAckOffset = readOffset; + if (DefaultHAConnection.this.slaveRequestOffset < 0) { + DefaultHAConnection.this.slaveRequestOffset = readOffset; + log.info("slave[" + DefaultHAConnection.this.clientAddress + "] request offset " + readOffset); + } + + DefaultHAConnection.this.haService.notifyTransferSome(DefaultHAConnection.this.slaveAckOffset); + } + } else if (readSize == 0) { + if (++readSizeZeroTimes >= 3) { + break; + } + } else { + log.error("read socket[" + DefaultHAConnection.this.clientAddress + "] < 0"); + return false; + } + } catch (IOException e) { + log.error("processReadEvent exception", e); + return false; + } + } + + return true; + } + } + + class WriteSocketService extends ServiceThread { + private final Selector selector; + private final SocketChannel socketChannel; + + private final ByteBuffer byteBufferHeader = ByteBuffer.allocate(TRANSFER_HEADER_SIZE); + private long nextTransferFromWhere = -1; + private SelectMappedBufferResult selectMappedBufferResult; + private boolean lastWriteOver = true; + private long lastPrintTimestamp = System.currentTimeMillis(); + private long lastWriteTimestamp = System.currentTimeMillis(); + + public WriteSocketService(final SocketChannel socketChannel) throws IOException { + this.selector = NetworkUtil.openSelector(); + this.socketChannel = socketChannel; + this.socketChannel.register(this.selector, SelectionKey.OP_WRITE); + this.setDaemon(true); + } + + @Override + public void run() { + log.info(this.getServiceName() + " service started"); + + while (!this.isStopped()) { + try { + this.selector.select(1000); + + if (-1 == DefaultHAConnection.this.slaveRequestOffset) { + Thread.sleep(10); + continue; + } + + if (-1 == this.nextTransferFromWhere) { + if (0 == DefaultHAConnection.this.slaveRequestOffset) { + long masterOffset = DefaultHAConnection.this.haService.getDefaultMessageStore().getCommitLog().getMaxOffset(); + masterOffset = + masterOffset + - (masterOffset % DefaultHAConnection.this.haService.getDefaultMessageStore().getMessageStoreConfig() + .getMappedFileSizeCommitLog()); + + if (masterOffset < 0) { + masterOffset = 0; + } + + this.nextTransferFromWhere = masterOffset; + } else { + this.nextTransferFromWhere = DefaultHAConnection.this.slaveRequestOffset; + } + + log.info("master transfer data from " + this.nextTransferFromWhere + " to slave[" + DefaultHAConnection.this.clientAddress + + "], and slave request " + DefaultHAConnection.this.slaveRequestOffset); + } + + if (this.lastWriteOver) { + + long interval = + DefaultHAConnection.this.haService.getDefaultMessageStore().getSystemClock().now() - this.lastWriteTimestamp; + + if (interval > DefaultHAConnection.this.haService.getDefaultMessageStore().getMessageStoreConfig() + .getHaSendHeartbeatInterval()) { + + // Build Header + this.byteBufferHeader.position(0); + this.byteBufferHeader.limit(TRANSFER_HEADER_SIZE); + this.byteBufferHeader.putLong(this.nextTransferFromWhere); + this.byteBufferHeader.putInt(0); + this.byteBufferHeader.flip(); + + this.lastWriteOver = this.transferData(); + if (!this.lastWriteOver) + continue; + } + } else { + this.lastWriteOver = this.transferData(); + if (!this.lastWriteOver) + continue; + } + + SelectMappedBufferResult selectResult = + DefaultHAConnection.this.haService.getDefaultMessageStore().getCommitLogData(this.nextTransferFromWhere); + if (selectResult != null) { + int size = selectResult.getSize(); + if (size > DefaultHAConnection.this.haService.getDefaultMessageStore().getMessageStoreConfig().getHaTransferBatchSize()) { + size = DefaultHAConnection.this.haService.getDefaultMessageStore().getMessageStoreConfig().getHaTransferBatchSize(); + } + + int canTransferMaxBytes = flowMonitor.canTransferMaxByteNum(); + if (size > canTransferMaxBytes) { + if (System.currentTimeMillis() - lastPrintTimestamp > 1000) { + log.warn("Trigger HA flow control, max transfer speed {}KB/s, current speed: {}KB/s", + String.format("%.2f", flowMonitor.maxTransferByteInSecond() / 1024.0), + String.format("%.2f", flowMonitor.getTransferredByteInSecond() / 1024.0)); + lastPrintTimestamp = System.currentTimeMillis(); + } + size = canTransferMaxBytes; + } + + long thisOffset = this.nextTransferFromWhere; + this.nextTransferFromWhere += size; + + selectResult.getByteBuffer().limit(size); + this.selectMappedBufferResult = selectResult; + + // Build Header + this.byteBufferHeader.position(0); + this.byteBufferHeader.limit(TRANSFER_HEADER_SIZE); + this.byteBufferHeader.putLong(thisOffset); + this.byteBufferHeader.putInt(size); + this.byteBufferHeader.flip(); + + this.lastWriteOver = this.transferData(); + } else { + + DefaultHAConnection.this.haService.getWaitNotifyObject().allWaitForRunning(100); + } + } catch (Exception e) { + + DefaultHAConnection.log.error(this.getServiceName() + " service has exception.", e); + break; + } + } + + DefaultHAConnection.this.haService.getWaitNotifyObject().removeFromWaitingThreadTable(); + + if (this.selectMappedBufferResult != null) { + this.selectMappedBufferResult.release(); + } + + changeCurrentState(HAConnectionState.SHUTDOWN); + + this.makeStop(); + + readSocketService.makeStop(); + + haService.removeConnection(DefaultHAConnection.this); + + SelectionKey sk = this.socketChannel.keyFor(this.selector); + if (sk != null) { + sk.cancel(); + } + + try { + this.selector.close(); + this.socketChannel.close(); + } catch (IOException e) { + DefaultHAConnection.log.error("", e); + } + + flowMonitor.shutdown(true); + + DefaultHAConnection.log.info(this.getServiceName() + " service end"); + } + + private boolean transferData() throws Exception { + int writeSizeZeroTimes = 0; + // Write Header + while (this.byteBufferHeader.hasRemaining()) { + int writeSize = this.socketChannel.write(this.byteBufferHeader); + if (writeSize > 0) { + flowMonitor.addByteCountTransferred(writeSize); + writeSizeZeroTimes = 0; + this.lastWriteTimestamp = DefaultHAConnection.this.haService.getDefaultMessageStore().getSystemClock().now(); + } else if (writeSize == 0) { + if (++writeSizeZeroTimes >= 3) { + break; + } + } else { + throw new Exception("ha master write header error < 0"); + } + } + + if (null == this.selectMappedBufferResult) { + return !this.byteBufferHeader.hasRemaining(); + } + + writeSizeZeroTimes = 0; + + // Write Body + if (!this.byteBufferHeader.hasRemaining()) { + while (this.selectMappedBufferResult.getByteBuffer().hasRemaining()) { + int writeSize = this.socketChannel.write(this.selectMappedBufferResult.getByteBuffer()); + if (writeSize > 0) { + writeSizeZeroTimes = 0; + this.lastWriteTimestamp = DefaultHAConnection.this.haService.getDefaultMessageStore().getSystemClock().now(); + } else if (writeSize == 0) { + if (++writeSizeZeroTimes >= 3) { + break; + } + } else { + throw new Exception("ha master write body error < 0"); + } + } + } + + boolean result = !this.byteBufferHeader.hasRemaining() && !this.selectMappedBufferResult.getByteBuffer().hasRemaining(); + + if (!this.selectMappedBufferResult.getByteBuffer().hasRemaining()) { + this.selectMappedBufferResult.release(); + this.selectMappedBufferResult = null; + } + + return result; + } + + @Override + public String getServiceName() { + if (haService.getDefaultMessageStore().getBrokerConfig().isInBrokerContainer()) { + return haService.getDefaultMessageStore().getBrokerIdentity().getIdentifier() + WriteSocketService.class.getSimpleName(); + } + return WriteSocketService.class.getSimpleName(); + } + + @Override + public void shutdown() { + super.shutdown(); + } + + public long getNextTransferFromWhere() { + return nextTransferFromWhere; + } + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/ha/DefaultHAService.java b/store/src/main/java/org/apache/rocketmq/store/ha/DefaultHAService.java new file mode 100644 index 00000000000..75e0afa4e89 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ha/DefaultHAService.java @@ -0,0 +1,380 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.ha; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.body.HARuntimeInfo; +import org.apache.rocketmq.store.CommitLog; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.config.BrokerRole; +import org.apache.rocketmq.store.config.MessageStoreConfig; + +public class DefaultHAService implements HAService { + + private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + + protected final AtomicInteger connectionCount = new AtomicInteger(0); + + protected final List connectionList = new LinkedList<>(); + + protected AcceptSocketService acceptSocketService; + + protected DefaultMessageStore defaultMessageStore; + + protected WaitNotifyObject waitNotifyObject = new WaitNotifyObject(); + protected AtomicLong push2SlaveMaxOffset = new AtomicLong(0); + + protected GroupTransferService groupTransferService; + + protected HAClient haClient; + + protected HAConnectionStateNotificationService haConnectionStateNotificationService; + + public DefaultHAService() { + } + + @Override + public void init(final DefaultMessageStore defaultMessageStore) throws IOException { + this.defaultMessageStore = defaultMessageStore; + this.acceptSocketService = new DefaultAcceptSocketService(defaultMessageStore.getMessageStoreConfig()); + this.groupTransferService = new GroupTransferService(this, defaultMessageStore); + if (this.defaultMessageStore.getMessageStoreConfig().getBrokerRole() == BrokerRole.SLAVE) { + this.haClient = new DefaultHAClient(this.defaultMessageStore); + } + this.haConnectionStateNotificationService = new HAConnectionStateNotificationService(this, defaultMessageStore); + } + + @Override + public void updateMasterAddress(final String newAddr) { + if (this.haClient != null) { + this.haClient.updateMasterAddress(newAddr); + } + } + + @Override + public void updateHaMasterAddress(String newAddr) { + if (this.haClient != null) { + this.haClient.updateHaMasterAddress(newAddr); + } + } + + @Override + public void putRequest(final CommitLog.GroupCommitRequest request) { + this.groupTransferService.putRequest(request); + } + + @Override + public boolean isSlaveOK(final long masterPutWhere) { + boolean result = this.connectionCount.get() > 0; + result = + result + && masterPutWhere - this.push2SlaveMaxOffset.get() < this.defaultMessageStore + .getMessageStoreConfig().getHaMaxGapNotInSync(); + return result; + } + + public void notifyTransferSome(final long offset) { + for (long value = this.push2SlaveMaxOffset.get(); offset > value; ) { + boolean ok = this.push2SlaveMaxOffset.compareAndSet(value, offset); + if (ok) { + this.groupTransferService.notifyTransferSome(); + break; + } else { + value = this.push2SlaveMaxOffset.get(); + } + } + } + + @Override + public AtomicInteger getConnectionCount() { + return connectionCount; + } + + @Override + public void start() throws Exception { + this.acceptSocketService.beginAccept(); + this.acceptSocketService.start(); + this.groupTransferService.start(); + this.haConnectionStateNotificationService.start(); + if (haClient != null) { + this.haClient.start(); + } + } + + public void addConnection(final HAConnection conn) { + synchronized (this.connectionList) { + this.connectionList.add(conn); + } + } + + public void removeConnection(final HAConnection conn) { + this.haConnectionStateNotificationService.checkConnectionStateAndNotify(conn); + synchronized (this.connectionList) { + this.connectionList.remove(conn); + } + } + + @Override + public void shutdown() { + if (this.haClient != null) { + this.haClient.shutdown(); + } + this.acceptSocketService.shutdown(true); + this.destroyConnections(); + this.groupTransferService.shutdown(); + this.haConnectionStateNotificationService.shutdown(); + } + + public void destroyConnections() { + synchronized (this.connectionList) { + for (HAConnection c : this.connectionList) { + c.shutdown(); + } + + this.connectionList.clear(); + } + } + + public DefaultMessageStore getDefaultMessageStore() { + return defaultMessageStore; + } + + @Override + public WaitNotifyObject getWaitNotifyObject() { + return waitNotifyObject; + } + + @Override + public AtomicLong getPush2SlaveMaxOffset() { + return push2SlaveMaxOffset; + } + + @Override + public int inSyncReplicasNums(final long masterPutWhere) { + int inSyncNums = 1; + for (HAConnection conn : this.connectionList) { + if (this.isInSyncSlave(masterPutWhere, conn)) { + inSyncNums++; + } + } + return inSyncNums; + } + + protected boolean isInSyncSlave(final long masterPutWhere, HAConnection conn) { + if (masterPutWhere - conn.getSlaveAckOffset() < this.defaultMessageStore.getMessageStoreConfig() + .getHaMaxGapNotInSync()) { + return true; + } + return false; + } + + @Override + public void putGroupConnectionStateRequest(HAConnectionStateNotificationRequest request) { + this.haConnectionStateNotificationService.setRequest(request); + } + + @Override + public List getConnectionList() { + return connectionList; + } + + @Override + public HAClient getHAClient() { + return this.haClient; + } + + @Override + public HARuntimeInfo getRuntimeInfo(long masterPutWhere) { + HARuntimeInfo info = new HARuntimeInfo(); + + if (BrokerRole.SLAVE.equals(this.getDefaultMessageStore().getMessageStoreConfig().getBrokerRole())) { + info.setMaster(false); + + info.getHaClientRuntimeInfo().setMasterAddr(this.haClient.getHaMasterAddress()); + info.getHaClientRuntimeInfo().setMaxOffset(this.getDefaultMessageStore().getMaxPhyOffset()); + info.getHaClientRuntimeInfo().setLastReadTimestamp(this.haClient.getLastReadTimestamp()); + info.getHaClientRuntimeInfo().setLastWriteTimestamp(this.haClient.getLastWriteTimestamp()); + info.getHaClientRuntimeInfo().setTransferredByteInSecond(this.haClient.getTransferredByteInSecond()); + info.getHaClientRuntimeInfo().setMasterFlushOffset(this.defaultMessageStore.getMasterFlushedOffset()); + } else { + info.setMaster(true); + int inSyncNums = 0; + + info.setMasterCommitLogMaxOffset(masterPutWhere); + + for (HAConnection conn : this.connectionList) { + HARuntimeInfo.HAConnectionRuntimeInfo cInfo = new HARuntimeInfo.HAConnectionRuntimeInfo(); + + long slaveAckOffset = conn.getSlaveAckOffset(); + cInfo.setSlaveAckOffset(slaveAckOffset); + cInfo.setDiff(masterPutWhere - slaveAckOffset); + cInfo.setAddr(conn.getClientAddress().substring(1)); + cInfo.setTransferredByteInSecond(conn.getTransferredByteInSecond()); + cInfo.setTransferFromWhere(conn.getTransferFromWhere()); + + boolean isInSync = this.isInSyncSlave(masterPutWhere, conn); + if (isInSync) { + inSyncNums++; + } + cInfo.setInSync(isInSync); + + info.getHaConnectionInfo().add(cInfo); + } + info.setInSyncSlaveNums(inSyncNums); + } + return info; + } + + class DefaultAcceptSocketService extends AcceptSocketService { + + public DefaultAcceptSocketService(final MessageStoreConfig messageStoreConfig) { + super(messageStoreConfig); + } + + @Override + protected HAConnection createConnection(SocketChannel sc) throws IOException { + return new DefaultHAConnection(DefaultHAService.this, sc); + } + + @Override + public String getServiceName() { + if (defaultMessageStore.getBrokerConfig().isInBrokerContainer()) { + return defaultMessageStore.getBrokerConfig().getIdentifier() + AcceptSocketService.class.getSimpleName(); + } + return DefaultAcceptSocketService.class.getSimpleName(); + } + } + + /** + * Listens to slave connections to create {@link HAConnection}. + */ + protected abstract class AcceptSocketService extends ServiceThread { + private final SocketAddress socketAddressListen; + private ServerSocketChannel serverSocketChannel; + private Selector selector; + + private final MessageStoreConfig messageStoreConfig; + + public AcceptSocketService(final MessageStoreConfig messageStoreConfig) { + this.messageStoreConfig = messageStoreConfig; + this.socketAddressListen = new InetSocketAddress(messageStoreConfig.getHaListenPort()); + } + + /** + * Starts listening to slave connections. + * + * @throws Exception If fails. + */ + public void beginAccept() throws Exception { + this.serverSocketChannel = ServerSocketChannel.open(); + this.selector = NetworkUtil.openSelector(); + this.serverSocketChannel.socket().setReuseAddress(true); + this.serverSocketChannel.socket().bind(this.socketAddressListen); + if (0 == messageStoreConfig.getHaListenPort()) { + messageStoreConfig.setHaListenPort(this.serverSocketChannel.socket().getLocalPort()); + log.info("OS picked up {} to listen for HA", messageStoreConfig.getHaListenPort()); + } + this.serverSocketChannel.configureBlocking(false); + this.serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT); + } + + /** + * {@inheritDoc} + */ + @Override + public void shutdown(final boolean interrupt) { + super.shutdown(interrupt); + try { + if (null != this.serverSocketChannel) { + this.serverSocketChannel.close(); + } + + if (null != this.selector) { + this.selector.close(); + } + } catch (IOException e) { + log.error("AcceptSocketService shutdown exception", e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void run() { + log.info(this.getServiceName() + " service started"); + + while (!this.isStopped()) { + try { + this.selector.select(1000); + Set selected = this.selector.selectedKeys(); + + if (selected != null) { + for (SelectionKey k : selected) { + if (k.isAcceptable()) { + SocketChannel sc = ((ServerSocketChannel) k.channel()).accept(); + + if (sc != null) { + DefaultHAService.log.info("HAService receive new connection, " + + sc.socket().getRemoteSocketAddress()); + try { + HAConnection conn = createConnection(sc); + conn.start(); + DefaultHAService.this.addConnection(conn); + } catch (Exception e) { + log.error("new HAConnection exception", e); + sc.close(); + } + } + } else { + log.warn("Unexpected ops in select " + k.readyOps()); + } + } + + selected.clear(); + } + } catch (Exception e) { + log.error(this.getServiceName() + " service has exception.", e); + } + } + + log.info(this.getServiceName() + " service end"); + } + + /** + * Create ha connection + */ + protected abstract HAConnection createConnection(final SocketChannel sc) throws IOException; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/ha/FlowMonitor.java b/store/src/main/java/org/apache/rocketmq/store/ha/FlowMonitor.java new file mode 100644 index 00000000000..810f2865caf --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ha/FlowMonitor.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.ha; + +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.store.config.MessageStoreConfig; + +public class FlowMonitor extends ServiceThread { + private final AtomicLong transferredByte = new AtomicLong(0L); + private volatile long transferredByteInSecond; + protected MessageStoreConfig messageStoreConfig; + + public FlowMonitor(MessageStoreConfig messageStoreConfig) { + this.messageStoreConfig = messageStoreConfig; + } + + @Override + public void run() { + while (!this.isStopped()) { + this.waitForRunning(1 * 1000); + this.calculateSpeed(); + } + } + + public void calculateSpeed() { + this.transferredByteInSecond = this.transferredByte.get(); + this.transferredByte.set(0); + } + + public int canTransferMaxByteNum() { + // Flow control is not started at present + if (this.isFlowControlEnable()) { + long res = Math.max(this.maxTransferByteInSecond() - this.transferredByte.get(), 0); + return res > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) res; + } + return Integer.MAX_VALUE; + } + + public void addByteCountTransferred(long count) { + this.transferredByte.addAndGet(count); + } + + public long getTransferredByteInSecond() { + return this.transferredByteInSecond; + } + + @Override + public String getServiceName() { + return FlowMonitor.class.getSimpleName(); + } + + protected boolean isFlowControlEnable() { + return this.messageStoreConfig.isHaFlowControlEnable(); + } + + public long maxTransferByteInSecond() { + return this.messageStoreConfig.getMaxHaTransferByteInSecond(); + } +} \ No newline at end of file diff --git a/store/src/main/java/org/apache/rocketmq/store/ha/GroupTransferService.java b/store/src/main/java/org/apache/rocketmq/store/ha/GroupTransferService.java new file mode 100644 index 00000000000..a75cae8ef0c --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ha/GroupTransferService.java @@ -0,0 +1,176 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.ha; + +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.CommitLog; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.PutMessageSpinLock; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.ha.autoswitch.AutoSwitchHAConnection; +import org.apache.rocketmq.store.ha.autoswitch.AutoSwitchHAService; + +/** + * GroupTransferService Service + */ +public class GroupTransferService extends ServiceThread { + + private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + + private final WaitNotifyObject notifyTransferObject = new WaitNotifyObject(); + private final PutMessageSpinLock lock = new PutMessageSpinLock(); + private final DefaultMessageStore defaultMessageStore; + private final HAService haService; + private volatile List requestsWrite = new LinkedList<>(); + private volatile List requestsRead = new LinkedList<>(); + + public GroupTransferService(final HAService haService, final DefaultMessageStore defaultMessageStore) { + this.haService = haService; + this.defaultMessageStore = defaultMessageStore; + } + + public void putRequest(final CommitLog.GroupCommitRequest request) { + lock.lock(); + try { + this.requestsWrite.add(request); + } finally { + lock.unlock(); + } + wakeup(); + } + + public void notifyTransferSome() { + this.notifyTransferObject.wakeup(); + } + + private void swapRequests() { + lock.lock(); + try { + List tmp = this.requestsWrite; + this.requestsWrite = this.requestsRead; + this.requestsRead = tmp; + } finally { + lock.unlock(); + } + } + + private void doWaitTransfer() { + if (!this.requestsRead.isEmpty()) { + for (CommitLog.GroupCommitRequest req : this.requestsRead) { + boolean transferOK = false; + + long deadLine = req.getDeadLine(); + final boolean allAckInSyncStateSet = req.getAckNums() == MixAll.ALL_ACK_IN_SYNC_STATE_SET; + + for (int i = 0; !transferOK && deadLine - System.nanoTime() > 0; i++) { + if (i > 0) { + this.notifyTransferObject.waitForRunning(1); + } + + if (!allAckInSyncStateSet && req.getAckNums() <= 1) { + transferOK = haService.getPush2SlaveMaxOffset().get() >= req.getNextOffset(); + continue; + } + + if (allAckInSyncStateSet && this.haService instanceof AutoSwitchHAService) { + // In this mode, we must wait for all replicas that in SyncStateSet. + final AutoSwitchHAService autoSwitchHAService = (AutoSwitchHAService) this.haService; + final Set syncStateSet = autoSwitchHAService.getSyncStateSet(); + if (syncStateSet.size() <= 1) { + // Only master + transferOK = true; + break; + } + + // Include master + int ackNums = 1; + for (HAConnection conn : haService.getConnectionList()) { + final AutoSwitchHAConnection autoSwitchHAConnection = (AutoSwitchHAConnection) conn; + if (syncStateSet.contains(autoSwitchHAConnection.getSlaveId()) && autoSwitchHAConnection.getSlaveAckOffset() >= req.getNextOffset()) { + ackNums++; + } + if (ackNums >= syncStateSet.size()) { + transferOK = true; + break; + } + } + } else { + // Include master + int ackNums = 1; + for (HAConnection conn : haService.getConnectionList()) { + // TODO: We must ensure every HAConnection represents a different slave + // Solution: Consider assign a unique and fixed IP:ADDR for each different slave + if (conn.getSlaveAckOffset() >= req.getNextOffset()) { + ackNums++; + } + if (ackNums >= req.getAckNums()) { + transferOK = true; + break; + } + } + } + } + + if (!transferOK) { + log.warn("transfer message to slave timeout, offset : {}, request acks: {}", + req.getNextOffset(), req.getAckNums()); + } + + req.wakeupCustomer(transferOK ? PutMessageStatus.PUT_OK : PutMessageStatus.FLUSH_SLAVE_TIMEOUT); + } + + this.requestsRead = new LinkedList<>(); + } + } + + @Override + public void run() { + log.info(this.getServiceName() + " service started"); + + while (!this.isStopped()) { + try { + this.waitForRunning(10); + this.doWaitTransfer(); + } catch (Exception e) { + log.warn(this.getServiceName() + " service has exception. ", e); + } + } + + log.info(this.getServiceName() + " service end"); + } + + @Override + protected void onWaitEnd() { + this.swapRequests(); + } + + @Override + public String getServiceName() { + if (defaultMessageStore != null && defaultMessageStore.getBrokerConfig().isInBrokerContainer()) { + return defaultMessageStore.getBrokerIdentity().getIdentifier() + GroupTransferService.class.getSimpleName(); + } + return GroupTransferService.class.getSimpleName(); + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/ha/HAClient.java b/store/src/main/java/org/apache/rocketmq/store/ha/HAClient.java new file mode 100644 index 00000000000..0449e01aa69 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ha/HAClient.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.ha; + +public interface HAClient { + + /** + * Start HAClient + */ + void start(); + + /** + * Shutdown HAClient + */ + void shutdown(); + + /** + * Wakeup HAClient + */ + void wakeup(); + + /** + * Update master address + * + * @param newAddress + */ + void updateMasterAddress(String newAddress); + + /** + * Update master ha address + * + * @param newAddress + */ + void updateHaMasterAddress(String newAddress); + + /** + * Get master address + * + * @return master address + */ + String getMasterAddress(); + + /** + * Get master ha address + * + * @return master ha address + */ + String getHaMasterAddress(); + + /** + * Get HAClient last read timestamp + * + * @return last read timestamp + */ + long getLastReadTimestamp(); + + /** + * Get HAClient last write timestamp + * + * @return last write timestamp + */ + long getLastWriteTimestamp(); + + /** + * Get current state for ha connection + * + * @return HAConnectionState + */ + HAConnectionState getCurrentState(); + + /** + * Change the current state for ha connection for testing + * + * @param haConnectionState + */ + void changeCurrentState(HAConnectionState haConnectionState); + + /** + * Disconnecting from the master for testing + */ + void closeMaster(); + + /** + * Get the transfer rate per second + * + * @return transfer bytes in second + */ + long getTransferredByteInSecond(); +} diff --git a/store/src/main/java/org/apache/rocketmq/store/ha/HAConnection.java b/store/src/main/java/org/apache/rocketmq/store/ha/HAConnection.java index 8b9750464f1..8e1e922979f 100644 --- a/store/src/main/java/org/apache/rocketmq/store/ha/HAConnection.java +++ b/store/src/main/java/org/apache/rocketmq/store/ha/HAConnection.java @@ -14,375 +14,64 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.apache.rocketmq.store.ha; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; import java.nio.channels.SocketChannel; -import org.apache.rocketmq.common.ServiceThread; -import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.remoting.common.RemotingUtil; -import org.apache.rocketmq.store.SelectMappedBufferResult; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class HAConnection { - private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); - private final HAService haService; - private final SocketChannel socketChannel; - private final String clientAddr; - private WriteSocketService writeSocketService; - private ReadSocketService readSocketService; - - private volatile long slaveRequestOffset = -1; - private volatile long slaveAckOffset = -1; - - public HAConnection(final HAService haService, final SocketChannel socketChannel) throws IOException { - this.haService = haService; - this.socketChannel = socketChannel; - this.clientAddr = this.socketChannel.socket().getRemoteSocketAddress().toString(); - this.socketChannel.configureBlocking(false); - this.socketChannel.socket().setSoLinger(false, -1); - this.socketChannel.socket().setTcpNoDelay(true); - this.socketChannel.socket().setReceiveBufferSize(1024 * 64); - this.socketChannel.socket().setSendBufferSize(1024 * 64); - this.writeSocketService = new WriteSocketService(this.socketChannel); - this.readSocketService = new ReadSocketService(this.socketChannel); - this.haService.getConnectionCount().incrementAndGet(); - } - - public void start() { - this.readSocketService.start(); - this.writeSocketService.start(); - } - - public void shutdown() { - this.writeSocketService.shutdown(true); - this.readSocketService.shutdown(true); - this.close(); - } - - public void close() { - if (this.socketChannel != null) { - try { - this.socketChannel.close(); - } catch (IOException e) { - HAConnection.log.error("", e); - } - } - } - - public SocketChannel getSocketChannel() { - return socketChannel; - } - - class ReadSocketService extends ServiceThread { - private static final int READ_MAX_BUFFER_SIZE = 1024 * 1024; - private final Selector selector; - private final SocketChannel socketChannel; - private final ByteBuffer byteBufferRead = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE); - private int processPostion = 0; - private volatile long lastReadTimestamp = System.currentTimeMillis(); - - public ReadSocketService(final SocketChannel socketChannel) throws IOException { - this.selector = RemotingUtil.openSelector(); - this.socketChannel = socketChannel; - this.socketChannel.register(this.selector, SelectionKey.OP_READ); - this.thread.setDaemon(true); - } - - @Override - public void run() { - HAConnection.log.info(this.getServiceName() + " service started"); - - while (!this.isStopped()) { - try { - this.selector.select(1000); - boolean ok = this.processReadEvent(); - if (!ok) { - HAConnection.log.error("processReadEvent error"); - break; - } - - long interval = HAConnection.this.haService.getDefaultMessageStore().getSystemClock().now() - this.lastReadTimestamp; - if (interval > HAConnection.this.haService.getDefaultMessageStore().getMessageStoreConfig().getHaHousekeepingInterval()) { - log.warn("ha housekeeping, found this connection[" + HAConnection.this.clientAddr + "] expired, " + interval); - break; - } - } catch (Exception e) { - HAConnection.log.error(this.getServiceName() + " service has exception.", e); - break; - } - } - - this.makeStop(); - - writeSocketService.makeStop(); - - haService.removeConnection(HAConnection.this); - - HAConnection.this.haService.getConnectionCount().decrementAndGet(); - - SelectionKey sk = this.socketChannel.keyFor(this.selector); - if (sk != null) { - sk.cancel(); - } - - try { - this.selector.close(); - this.socketChannel.close(); - } catch (IOException e) { - HAConnection.log.error("", e); - } - - HAConnection.log.info(this.getServiceName() + " service end"); - } - - @Override - public String getServiceName() { - return ReadSocketService.class.getSimpleName(); - } - - private boolean processReadEvent() { - int readSizeZeroTimes = 0; - - if (!this.byteBufferRead.hasRemaining()) { - this.byteBufferRead.flip(); - this.processPostion = 0; - } - - while (this.byteBufferRead.hasRemaining()) { - try { - int readSize = this.socketChannel.read(this.byteBufferRead); - if (readSize > 0) { - readSizeZeroTimes = 0; - this.lastReadTimestamp = HAConnection.this.haService.getDefaultMessageStore().getSystemClock().now(); - if ((this.byteBufferRead.position() - this.processPostion) >= 8) { - int pos = this.byteBufferRead.position() - (this.byteBufferRead.position() % 8); - long readOffset = this.byteBufferRead.getLong(pos - 8); - this.processPostion = pos; - - HAConnection.this.slaveAckOffset = readOffset; - if (HAConnection.this.slaveRequestOffset < 0) { - HAConnection.this.slaveRequestOffset = readOffset; - log.info("slave[" + HAConnection.this.clientAddr + "] request offset " + readOffset); - } - - HAConnection.this.haService.notifyTransferSome(HAConnection.this.slaveAckOffset); - } - } else if (readSize == 0) { - if (++readSizeZeroTimes >= 3) { - break; - } - } else { - log.error("read socket[" + HAConnection.this.clientAddr + "] < 0"); - return false; - } - } catch (IOException e) { - log.error("processReadEvent exception", e); - return false; - } - } - - return true; - } - } - - class WriteSocketService extends ServiceThread { - private final Selector selector; - private final SocketChannel socketChannel; - - private final int headerSize = 8 + 4; - private final ByteBuffer byteBufferHeader = ByteBuffer.allocate(headerSize); - private long nextTransferFromWhere = -1; - private SelectMappedBufferResult selectMappedBufferResult; - private boolean lastWriteOver = true; - private long lastWriteTimestamp = System.currentTimeMillis(); - - public WriteSocketService(final SocketChannel socketChannel) throws IOException { - this.selector = RemotingUtil.openSelector(); - this.socketChannel = socketChannel; - this.socketChannel.register(this.selector, SelectionKey.OP_WRITE); - this.thread.setDaemon(true); - } - - @Override - public void run() { - HAConnection.log.info(this.getServiceName() + " service started"); - - while (!this.isStopped()) { - try { - this.selector.select(1000); - - if (-1 == HAConnection.this.slaveRequestOffset) { - Thread.sleep(10); - continue; - } - - if (-1 == this.nextTransferFromWhere) { - if (0 == HAConnection.this.slaveRequestOffset) { - long masterOffset = HAConnection.this.haService.getDefaultMessageStore().getCommitLog().getMaxOffset(); - masterOffset = - masterOffset - - (masterOffset % HAConnection.this.haService.getDefaultMessageStore().getMessageStoreConfig() - .getMapedFileSizeCommitLog()); - - if (masterOffset < 0) { - masterOffset = 0; - } - - this.nextTransferFromWhere = masterOffset; - } else { - this.nextTransferFromWhere = HAConnection.this.slaveRequestOffset; - } - - log.info("master transfer data from " + this.nextTransferFromWhere + " to slave[" + HAConnection.this.clientAddr - + "], and slave request " + HAConnection.this.slaveRequestOffset); - } - - if (this.lastWriteOver) { - - long interval = - HAConnection.this.haService.getDefaultMessageStore().getSystemClock().now() - this.lastWriteTimestamp; - - if (interval > HAConnection.this.haService.getDefaultMessageStore().getMessageStoreConfig() - .getHaSendHeartbeatInterval()) { - - // Build Header - this.byteBufferHeader.position(0); - this.byteBufferHeader.limit(headerSize); - this.byteBufferHeader.putLong(this.nextTransferFromWhere); - this.byteBufferHeader.putInt(0); - this.byteBufferHeader.flip(); - - this.lastWriteOver = this.transferData(); - if (!this.lastWriteOver) - continue; - } - } else { - this.lastWriteOver = this.transferData(); - if (!this.lastWriteOver) - continue; - } - - SelectMappedBufferResult selectResult = - HAConnection.this.haService.getDefaultMessageStore().getCommitLogData(this.nextTransferFromWhere); - if (selectResult != null) { - int size = selectResult.getSize(); - if (size > HAConnection.this.haService.getDefaultMessageStore().getMessageStoreConfig().getHaTransferBatchSize()) { - size = HAConnection.this.haService.getDefaultMessageStore().getMessageStoreConfig().getHaTransferBatchSize(); - } - - long thisOffset = this.nextTransferFromWhere; - this.nextTransferFromWhere += size; - - selectResult.getByteBuffer().limit(size); - this.selectMappedBufferResult = selectResult; - - // Build Header - this.byteBufferHeader.position(0); - this.byteBufferHeader.limit(headerSize); - this.byteBufferHeader.putLong(thisOffset); - this.byteBufferHeader.putInt(size); - this.byteBufferHeader.flip(); - - this.lastWriteOver = this.transferData(); - } else { - - HAConnection.this.haService.getWaitNotifyObject().allWaitForRunning(100); - } - } catch (Exception e) { - - HAConnection.log.error(this.getServiceName() + " service has exception.", e); - break; - } - } - - if (this.selectMappedBufferResult != null) { - this.selectMappedBufferResult.release(); - } - - this.makeStop(); - - readSocketService.makeStop(); - - haService.removeConnection(HAConnection.this); - - SelectionKey sk = this.socketChannel.keyFor(this.selector); - if (sk != null) { - sk.cancel(); - } - - try { - this.selector.close(); - this.socketChannel.close(); - } catch (IOException e) { - HAConnection.log.error("", e); - } - - HAConnection.log.info(this.getServiceName() + " service end"); - } - - private boolean transferData() throws Exception { - int writeSizeZeroTimes = 0; - // Write Header - while (this.byteBufferHeader.hasRemaining()) { - int writeSize = this.socketChannel.write(this.byteBufferHeader); - if (writeSize > 0) { - writeSizeZeroTimes = 0; - this.lastWriteTimestamp = HAConnection.this.haService.getDefaultMessageStore().getSystemClock().now(); - } else if (writeSize == 0) { - if (++writeSizeZeroTimes >= 3) { - break; - } - } else { - throw new Exception("ha master write header error < 0"); - } - } - - if (null == this.selectMappedBufferResult) { - return !this.byteBufferHeader.hasRemaining(); - } - - writeSizeZeroTimes = 0; - - // Write Body - if (!this.byteBufferHeader.hasRemaining()) { - while (this.selectMappedBufferResult.getByteBuffer().hasRemaining()) { - int writeSize = this.socketChannel.write(this.selectMappedBufferResult.getByteBuffer()); - if (writeSize > 0) { - writeSizeZeroTimes = 0; - this.lastWriteTimestamp = HAConnection.this.haService.getDefaultMessageStore().getSystemClock().now(); - } else if (writeSize == 0) { - if (++writeSizeZeroTimes >= 3) { - break; - } - } else { - throw new Exception("ha master write body error < 0"); - } - } - } - - boolean result = !this.byteBufferHeader.hasRemaining() && !this.selectMappedBufferResult.getByteBuffer().hasRemaining(); - - if (!this.selectMappedBufferResult.getByteBuffer().hasRemaining()) { - this.selectMappedBufferResult.release(); - this.selectMappedBufferResult = null; - } - - return result; - } - - @Override - public String getServiceName() { - return WriteSocketService.class.getSimpleName(); - } - @Override - public void shutdown() { - super.shutdown(); - } - } +public interface HAConnection { + /** + * Start HA Connection + */ + void start(); + + /** + * Shutdown HA Connection + */ + void shutdown(); + + /** + * Close HA Connection + */ + void close(); + + /** + * Get socket channel + */ + SocketChannel getSocketChannel(); + + /** + * Get current state for ha connection + * + * @return HAConnectionState + */ + HAConnectionState getCurrentState(); + + /** + * Get client address for ha connection + * + * @return client ip address + */ + String getClientAddress(); + + /** + * Get the transfer rate per second + * + * @return transfer bytes in second + */ + long getTransferredByteInSecond(); + + /** + * Get the current transfer offset to the slave + * + * @return the current transfer offset to the slave + */ + long getTransferFromWhere(); + + /** + * Get slave ack offset + * + * @return slave ack offset + */ + long getSlaveAckOffset(); } diff --git a/store/src/main/java/org/apache/rocketmq/store/ha/HAConnectionState.java b/store/src/main/java/org/apache/rocketmq/store/ha/HAConnectionState.java new file mode 100644 index 00000000000..4f0c5ca9095 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ha/HAConnectionState.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.ha; + +public enum HAConnectionState { + /** + * Ready to start connection. + */ + READY, + /** + * CommitLog consistency checking. + */ + HANDSHAKE, + /** + * Synchronizing data. + */ + TRANSFER, + /** + * Temporarily stop transferring. + */ + SUSPEND, + /** + * Connection shutdown. + */ + SHUTDOWN, +} diff --git a/store/src/main/java/org/apache/rocketmq/store/ha/HAConnectionStateNotificationRequest.java b/store/src/main/java/org/apache/rocketmq/store/ha/HAConnectionStateNotificationRequest.java new file mode 100644 index 00000000000..8a3f6aabfc1 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ha/HAConnectionStateNotificationRequest.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.ha; + +import java.util.concurrent.CompletableFuture; + +public class HAConnectionStateNotificationRequest { + private final CompletableFuture requestFuture = new CompletableFuture<>(); + private final HAConnectionState expectState; + private final String remoteAddr; + private final boolean notifyWhenShutdown; + + public HAConnectionStateNotificationRequest(HAConnectionState expectState, String remoteAddr, boolean notifyWhenShutdown) { + this.expectState = expectState; + this.remoteAddr = remoteAddr; + this.notifyWhenShutdown = notifyWhenShutdown; + } + + public CompletableFuture getRequestFuture() { + return requestFuture; + } + + public String getRemoteAddr() { + return remoteAddr; + } + + public boolean isNotifyWhenShutdown() { + return notifyWhenShutdown; + } + + public HAConnectionState getExpectState() { + return expectState; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/ha/HAConnectionStateNotificationService.java b/store/src/main/java/org/apache/rocketmq/store/ha/HAConnectionStateNotificationService.java new file mode 100644 index 00000000000..197d9f6ba4f --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ha/HAConnectionStateNotificationService.java @@ -0,0 +1,150 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.ha; + +import java.net.InetSocketAddress; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.config.BrokerRole; + +/** + * Service to periodically check and notify for certain connection state. + */ +public class HAConnectionStateNotificationService extends ServiceThread { + + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + + private static final long CONNECTION_ESTABLISH_TIMEOUT = 10 * 1000; + + private volatile HAConnectionStateNotificationRequest request; + private volatile long lastCheckTimeStamp = -1; + private HAService haService; + private DefaultMessageStore defaultMessageStore; + + public HAConnectionStateNotificationService(HAService haService, DefaultMessageStore defaultMessageStore) { + this.haService = haService; + this.defaultMessageStore = defaultMessageStore; + } + + @Override + public String getServiceName() { + if (defaultMessageStore != null && defaultMessageStore.getBrokerConfig().isInBrokerContainer()) { + return defaultMessageStore.getBrokerIdentity().getIdentifier() + HAConnectionStateNotificationService.class.getSimpleName(); + } + return HAConnectionStateNotificationService.class.getSimpleName(); + } + + public synchronized void setRequest(HAConnectionStateNotificationRequest request) { + if (this.request != null) { + this.request.getRequestFuture().cancel(true); + } + this.request = request; + lastCheckTimeStamp = System.currentTimeMillis(); + } + + private synchronized void doWaitConnectionState() { + if (this.request == null || this.request.getRequestFuture().isDone()) { + return; + } + + if (this.defaultMessageStore.getMessageStoreConfig().getBrokerRole() == BrokerRole.SLAVE) { + if (haService.getHAClient().getCurrentState() == this.request.getExpectState()) { + this.request.getRequestFuture().complete(true); + this.request = null; + } else if (haService.getHAClient().getCurrentState() == HAConnectionState.READY) { + if ((System.currentTimeMillis() - lastCheckTimeStamp) > CONNECTION_ESTABLISH_TIMEOUT) { + LOGGER.error("Wait HA connection establish with {} timeout", this.request.getRemoteAddr()); + this.request.getRequestFuture().complete(false); + this.request = null; + } + } else { + lastCheckTimeStamp = System.currentTimeMillis(); + } + } else { + boolean connectionFound = false; + for (HAConnection connection : haService.getConnectionList()) { + if (checkConnectionStateAndNotify(connection)) { + connectionFound = true; + } + } + + if (connectionFound) { + lastCheckTimeStamp = System.currentTimeMillis(); + } + + if (!connectionFound && (System.currentTimeMillis() - lastCheckTimeStamp) > CONNECTION_ESTABLISH_TIMEOUT) { + LOGGER.error("Wait HA connection establish with {} timeout", this.request.getRemoteAddr()); + this.request.getRequestFuture().complete(false); + this.request = null; + } + } + } + + /** + * Check if connection matched and notify request. + * + * @param connection connection to check. + * @return if connection remote address match request. + */ + public synchronized boolean checkConnectionStateAndNotify(HAConnection connection) { + if (this.request == null || connection == null) { + return false; + } + + String remoteAddress; + try { + remoteAddress = ((InetSocketAddress) connection.getSocketChannel().getRemoteAddress()) + .getAddress().getHostAddress(); + if (remoteAddress.equals(request.getRemoteAddr())) { + HAConnectionState connState = connection.getCurrentState(); + + if (connState == this.request.getExpectState()) { + this.request.getRequestFuture().complete(true); + this.request = null; + } else if (this.request.isNotifyWhenShutdown() && connState == HAConnectionState.SHUTDOWN) { + this.request.getRequestFuture().complete(false); + this.request = null; + } + return true; + } + } catch (Exception e) { + LOGGER.error("Check connection address exception: {}", e); + } + + return false; + } + + @Override + public void run() { + LOGGER.info(this.getServiceName() + " service started"); + + while (!this.isStopped()) { + try { + this.waitForRunning(1000); + this.doWaitConnectionState(); + } catch (Exception e) { + LOGGER.warn(this.getServiceName() + " service has exception. ", e); + } + } + + LOGGER.info(this.getServiceName() + " service end"); + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/ha/HAService.java b/store/src/main/java/org/apache/rocketmq/store/ha/HAService.java index 51a8a27035a..aaea7d6900a 100644 --- a/store/src/main/java/org/apache/rocketmq/store/ha/HAService.java +++ b/store/src/main/java/org/apache/rocketmq/store/ha/HAService.java @@ -14,605 +14,155 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.apache.rocketmq.store.ha; import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.nio.ByteBuffer; -import java.nio.channels.ClosedChannelException; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; -import java.nio.channels.ServerSocketChannel; -import java.nio.channels.SocketChannel; -import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; -import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import org.apache.rocketmq.common.ServiceThread; -import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.remoting.common.RemotingUtil; +import org.apache.rocketmq.remoting.protocol.body.HARuntimeInfo; import org.apache.rocketmq.store.CommitLog; import org.apache.rocketmq.store.DefaultMessageStore; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class HAService { - private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); - - private final AtomicInteger connectionCount = new AtomicInteger(0); - - private final List connectionList = new LinkedList<>(); - - private final AcceptSocketService acceptSocketService; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.rocksdb.RocksDBException; - private final DefaultMessageStore defaultMessageStore; +public interface HAService { - private final WaitNotifyObject waitNotifyObject = new WaitNotifyObject(); - private final AtomicLong push2SlaveMaxOffset = new AtomicLong(0); - - private final GroupTransferService groupTransferService; - - private final HAClient haClient; - - public HAService(final DefaultMessageStore defaultMessageStore) throws IOException { - this.defaultMessageStore = defaultMessageStore; - this.acceptSocketService = - new AcceptSocketService(defaultMessageStore.getMessageStoreConfig().getHaListenPort()); - this.groupTransferService = new GroupTransferService(); - this.haClient = new HAClient(); - } - - public void updateMasterAddress(final String newAddr) { - if (this.haClient != null) { - this.haClient.updateMasterAddress(newAddr); - } - } - - public void putRequest(final CommitLog.GroupCommitRequest request) { - this.groupTransferService.putRequest(request); - } - - public boolean isSlaveOK(final long masterPutWhere) { - boolean result = this.connectionCount.get() > 0; - result = - result - && ((masterPutWhere - this.push2SlaveMaxOffset.get()) < this.defaultMessageStore - .getMessageStoreConfig().getHaSlaveFallbehindMax()); - return result; - } - - public void notifyTransferSome(final long offset) { - for (long value = this.push2SlaveMaxOffset.get(); offset > value; ) { - boolean ok = this.push2SlaveMaxOffset.compareAndSet(value, offset); - if (ok) { - this.groupTransferService.notifyTransferSome(); - break; - } else { - value = this.push2SlaveMaxOffset.get(); - } - } - } - - public AtomicInteger getConnectionCount() { - return connectionCount; - } - - // public void notifyTransferSome() { - // this.groupTransferService.notifyTransferSome(); - // } - - public void start() throws Exception { - this.acceptSocketService.beginAccept(); - this.acceptSocketService.start(); - this.groupTransferService.start(); - this.haClient.start(); - } - - public void addConnection(final HAConnection conn) { - synchronized (this.connectionList) { - this.connectionList.add(conn); - } - } - - public void removeConnection(final HAConnection conn) { - synchronized (this.connectionList) { - this.connectionList.remove(conn); - } - } - - public void shutdown() { - this.haClient.shutdown(); - this.acceptSocketService.shutdown(true); - this.destroyConnections(); - this.groupTransferService.shutdown(); - } - - public void destroyConnections() { - synchronized (this.connectionList) { - for (HAConnection c : this.connectionList) { - c.shutdown(); - } + /** + * Init HAService, must be called before other methods. + * + * @param defaultMessageStore + * @throws IOException + */ + void init(DefaultMessageStore defaultMessageStore) throws IOException; - this.connectionList.clear(); - } - } + /** + * Start HA Service + * + * @throws Exception + */ + void start() throws Exception; - public DefaultMessageStore getDefaultMessageStore() { - return defaultMessageStore; - } + /** + * Shutdown HA Service + */ + void shutdown(); - public WaitNotifyObject getWaitNotifyObject() { - return waitNotifyObject; + /** + * Change to master state + * + * @param masterEpoch the new masterEpoch + */ + default boolean changeToMaster(int masterEpoch) throws RocksDBException { + return false; } - public AtomicLong getPush2SlaveMaxOffset() { - return push2SlaveMaxOffset; + /** + * Change to master state + * + * @param masterEpoch the new masterEpoch + */ + default boolean changeToMasterWhenLastRoleIsMaster(int masterEpoch) { + return false; } /** - * Listens to slave connections to create {@link HAConnection}. + * Change to slave state + * + * @param newMasterAddr new master addr + * @param newMasterEpoch new masterEpoch */ - class AcceptSocketService extends ServiceThread { - private final SocketAddress socketAddressListen; - private ServerSocketChannel serverSocketChannel; - private Selector selector; - - public AcceptSocketService(final int port) { - this.socketAddressListen = new InetSocketAddress(port); - } - - /** - * Starts listening to slave connections. - * - * @throws Exception If fails. - */ - public void beginAccept() throws Exception { - this.serverSocketChannel = ServerSocketChannel.open(); - this.selector = RemotingUtil.openSelector(); - this.serverSocketChannel.socket().setReuseAddress(true); - this.serverSocketChannel.socket().bind(this.socketAddressListen); - this.serverSocketChannel.configureBlocking(false); - this.serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT); - } - - /** - * {@inheritDoc} - */ - @Override - public void shutdown(final boolean interrupt) { - super.shutdown(interrupt); - try { - this.serverSocketChannel.close(); - this.selector.close(); - } catch (IOException e) { - log.error("AcceptSocketService shutdown exception", e); - } - } - - /** - * {@inheritDoc} - */ - @Override - public void run() { - log.info(this.getServiceName() + " service started"); - - while (!this.isStopped()) { - try { - this.selector.select(1000); - Set selected = this.selector.selectedKeys(); - - if (selected != null) { - for (SelectionKey k : selected) { - if ((k.readyOps() & SelectionKey.OP_ACCEPT) != 0) { - SocketChannel sc = ((ServerSocketChannel) k.channel()).accept(); - - if (sc != null) { - HAService.log.info("HAService receive new connection, " - + sc.socket().getRemoteSocketAddress()); - - try { - HAConnection conn = new HAConnection(HAService.this, sc); - conn.start(); - HAService.this.addConnection(conn); - } catch (Exception e) { - log.error("new HAConnection exception", e); - sc.close(); - } - } - } else { - log.warn("Unexpected ops in select " + k.readyOps()); - } - } - - selected.clear(); - } - } catch (Exception e) { - log.error(this.getServiceName() + " service has exception.", e); - } - } - - log.info(this.getServiceName() + " service end"); - } - - /** - * {@inheritDoc} - */ - @Override - public String getServiceName() { - return AcceptSocketService.class.getSimpleName(); - } + default boolean changeToSlave(String newMasterAddr, int newMasterEpoch, Long slaveId) { + return false; } /** - * GroupTransferService Service + * Change to slave state + * + * @param newMasterAddr new master addr + * @param newMasterEpoch new masterEpoch */ - class GroupTransferService extends ServiceThread { - - private final WaitNotifyObject notifyTransferObject = new WaitNotifyObject(); - private volatile List requestsWrite = new ArrayList<>(); - private volatile List requestsRead = new ArrayList<>(); - - public synchronized void putRequest(final CommitLog.GroupCommitRequest request) { - synchronized (this.requestsWrite) { - this.requestsWrite.add(request); - } - if (hasNotified.compareAndSet(false, true)) { - waitPoint.countDown(); // notify - } - } - - public void notifyTransferSome() { - this.notifyTransferObject.wakeup(); - } - - private void swapRequests() { - List tmp = this.requestsWrite; - this.requestsWrite = this.requestsRead; - this.requestsRead = tmp; - } - - private void doWaitTransfer() { - synchronized (this.requestsRead) { - if (!this.requestsRead.isEmpty()) { - for (CommitLog.GroupCommitRequest req : this.requestsRead) { - boolean transferOK = HAService.this.push2SlaveMaxOffset.get() >= req.getNextOffset(); - for (int i = 0; !transferOK && i < 5; i++) { - this.notifyTransferObject.waitForRunning(1000); - transferOK = HAService.this.push2SlaveMaxOffset.get() >= req.getNextOffset(); - } - - if (!transferOK) { - log.warn("transfer messsage to slave timeout, " + req.getNextOffset()); - } - - req.wakeupCustomer(transferOK); - } - - this.requestsRead.clear(); - } - } - } - - public void run() { - log.info(this.getServiceName() + " service started"); - - while (!this.isStopped()) { - try { - this.waitForRunning(10); - this.doWaitTransfer(); - } catch (Exception e) { - log.warn(this.getServiceName() + " service has exception. ", e); - } - } - - log.info(this.getServiceName() + " service end"); - } - - @Override - protected void onWaitEnd() { - this.swapRequests(); - } - - @Override - public String getServiceName() { - return GroupTransferService.class.getSimpleName(); - } + default boolean changeToSlaveWhenMasterNotChange(String newMasterAddr, int newMasterEpoch) { + return false; } - class HAClient extends ServiceThread { - private static final int READ_MAX_BUFFER_SIZE = 1024 * 1024 * 4; - private final AtomicReference masterAddress = new AtomicReference<>(); - private final ByteBuffer reportOffset = ByteBuffer.allocate(8); - private SocketChannel socketChannel; - private Selector selector; - private long lastWriteTimestamp = System.currentTimeMillis(); - - private long currentReportedOffset = 0; - private int dispatchPostion = 0; - private ByteBuffer byteBufferRead = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE); - private ByteBuffer byteBufferBackup = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE); - - public HAClient() throws IOException { - this.selector = RemotingUtil.openSelector(); - } - - public void updateMasterAddress(final String newAddr) { - String currentAddr = this.masterAddress.get(); - if (currentAddr == null || !currentAddr.equals(newAddr)) { - this.masterAddress.set(newAddr); - log.info("update master address, OLD: " + currentAddr + " NEW: " + newAddr); - } - } - - private boolean isTimeToReportOffset() { - long interval = - HAService.this.defaultMessageStore.getSystemClock().now() - this.lastWriteTimestamp; - boolean needHeart = interval > HAService.this.defaultMessageStore.getMessageStoreConfig() - .getHaSendHeartbeatInterval(); - - return needHeart; - } - - private boolean reportSlaveMaxOffset(final long maxOffset) { - this.reportOffset.position(0); - this.reportOffset.limit(8); - this.reportOffset.putLong(maxOffset); - this.reportOffset.position(0); - this.reportOffset.limit(8); - - for (int i = 0; i < 3 && this.reportOffset.hasRemaining(); i++) { - try { - this.socketChannel.write(this.reportOffset); - } catch (IOException e) { - log.error(this.getServiceName() - + "reportSlaveMaxOffset this.socketChannel.write exception", e); - return false; - } - } - - return !this.reportOffset.hasRemaining(); - } - - private void reallocateByteBuffer() { - int remain = READ_MAX_BUFFER_SIZE - this.dispatchPostion; - if (remain > 0) { - this.byteBufferRead.position(this.dispatchPostion); - - this.byteBufferBackup.position(0); - this.byteBufferBackup.limit(READ_MAX_BUFFER_SIZE); - this.byteBufferBackup.put(this.byteBufferRead); - } - - this.swapByteBuffer(); - - this.byteBufferRead.position(remain); - this.byteBufferRead.limit(READ_MAX_BUFFER_SIZE); - this.dispatchPostion = 0; - } - - private void swapByteBuffer() { - ByteBuffer tmp = this.byteBufferRead; - this.byteBufferRead = this.byteBufferBackup; - this.byteBufferBackup = tmp; - } - - private boolean processReadEvent() { - int readSizeZeroTimes = 0; - while (this.byteBufferRead.hasRemaining()) { - try { - int readSize = this.socketChannel.read(this.byteBufferRead); - if (readSize > 0) { - lastWriteTimestamp = HAService.this.defaultMessageStore.getSystemClock().now(); - readSizeZeroTimes = 0; - boolean result = this.dispatchReadRequest(); - if (!result) { - log.error("HAClient, dispatchReadRequest error"); - return false; - } - } else if (readSize == 0) { - if (++readSizeZeroTimes >= 3) { - break; - } - } else { - log.info("HAClient, processReadEvent read socket < 0"); - return false; - } - } catch (IOException e) { - log.info("HAClient, processReadEvent read socket exception", e); - return false; - } - } - - return true; - } - - private boolean dispatchReadRequest() { - final int msgHeaderSize = 8 + 4; // phyoffset + size - int readSocketPos = this.byteBufferRead.position(); - - while (true) { - int diff = this.byteBufferRead.position() - this.dispatchPostion; - if (diff >= msgHeaderSize) { - long masterPhyOffset = this.byteBufferRead.getLong(this.dispatchPostion); - int bodySize = this.byteBufferRead.getInt(this.dispatchPostion + 8); - - long slavePhyOffset = HAService.this.defaultMessageStore.getMaxPhyOffset(); - - if (slavePhyOffset != 0) { - if (slavePhyOffset != masterPhyOffset) { - log.error("master pushed offset not equal the max phy offset in slave, SLAVE: " - + slavePhyOffset + " MASTER: " + masterPhyOffset); - return false; - } - } - - if (diff >= (msgHeaderSize + bodySize)) { - byte[] bodyData = new byte[bodySize]; - this.byteBufferRead.position(this.dispatchPostion + msgHeaderSize); - this.byteBufferRead.get(bodyData); - - HAService.this.defaultMessageStore.appendToCommitLog(masterPhyOffset, bodyData); - - this.byteBufferRead.position(readSocketPos); - this.dispatchPostion += msgHeaderSize + bodySize; - - if (!reportSlaveMaxOffsetPlus()) { - return false; - } - - continue; - } - } - - if (!this.byteBufferRead.hasRemaining()) { - this.reallocateByteBuffer(); - } - - break; - } - - return true; - } - - private boolean reportSlaveMaxOffsetPlus() { - boolean result = true; - long currentPhyOffset = HAService.this.defaultMessageStore.getMaxPhyOffset(); - if (currentPhyOffset > this.currentReportedOffset) { - this.currentReportedOffset = currentPhyOffset; - result = this.reportSlaveMaxOffset(this.currentReportedOffset); - if (!result) { - this.closeMaster(); - log.error("HAClient, reportSlaveMaxOffset error, " + this.currentReportedOffset); - } - } - - return result; - } - - private boolean connectMaster() throws ClosedChannelException { - if (null == socketChannel) { - String addr = this.masterAddress.get(); - if (addr != null) { - - SocketAddress socketAddress = RemotingUtil.string2SocketAddress(addr); - if (socketAddress != null) { - this.socketChannel = RemotingUtil.connect(socketAddress); - if (this.socketChannel != null) { - this.socketChannel.register(this.selector, SelectionKey.OP_READ); - } - } - } - - this.currentReportedOffset = HAService.this.defaultMessageStore.getMaxPhyOffset(); - - this.lastWriteTimestamp = System.currentTimeMillis(); - } - - return this.socketChannel != null; - } - - private void closeMaster() { - if (null != this.socketChannel) { - try { - - SelectionKey sk = this.socketChannel.keyFor(this.selector); - if (sk != null) { - sk.cancel(); - } - - this.socketChannel.close(); - - this.socketChannel = null; - } catch (IOException e) { - log.warn("closeMaster exception. ", e); - } - - this.lastWriteTimestamp = 0; - this.dispatchPostion = 0; + /** + * Update master address + * + * @param newAddr + */ + void updateMasterAddress(String newAddr); - this.byteBufferBackup.position(0); - this.byteBufferBackup.limit(READ_MAX_BUFFER_SIZE); + /** + * Update ha master address + * + * @param newAddr + */ + void updateHaMasterAddress(String newAddr); - this.byteBufferRead.position(0); - this.byteBufferRead.limit(READ_MAX_BUFFER_SIZE); - } - } + /** + * Returns the number of replicas those commit log are not far behind the master. It includes master itself. Returns + * syncStateSet size if HAService instanceof AutoSwitchService + * + * @return the number of slaves + * @see MessageStoreConfig#getHaMaxGapNotInSync() + */ + int inSyncReplicasNums(long masterPutWhere); - @Override - public void run() { - log.info(this.getServiceName() + " service started"); + /** + * Get connection count + * + * @return the number of connection + */ + AtomicInteger getConnectionCount(); - while (!this.isStopped()) { - try { - if (this.connectMaster()) { + /** + * Put request to handle HA + * + * @param request + */ + void putRequest(final CommitLog.GroupCommitRequest request); - if (this.isTimeToReportOffset()) { - boolean result = this.reportSlaveMaxOffset(this.currentReportedOffset); - if (!result) { - this.closeMaster(); - } - } + /** + * Put GroupConnectionStateRequest for preOnline + * + * @param request + */ + void putGroupConnectionStateRequest(HAConnectionStateNotificationRequest request); - this.selector.select(1000); + /** + * Get ha connection list + * + * @return List + */ + List getConnectionList(); - boolean ok = this.processReadEvent(); - if (!ok) { - this.closeMaster(); - } + /** + * Get HAClient + * + * @return HAClient + */ + HAClient getHAClient(); - if (!reportSlaveMaxOffsetPlus()) { - continue; - } + /** + * Get the max offset in all slaves + */ + AtomicLong getPush2SlaveMaxOffset(); - long interval = - HAService.this.getDefaultMessageStore().getSystemClock().now() - - this.lastWriteTimestamp; - if (interval > HAService.this.getDefaultMessageStore().getMessageStoreConfig() - .getHaHousekeepingInterval()) { - log.warn("HAClient, housekeeping, found this connection[" + this.masterAddress - + "] expired, " + interval); - this.closeMaster(); - log.warn("HAClient, master not response some time, so close connection"); - } - } else { - this.waitForRunning(1000 * 5); - } - } catch (Exception e) { - log.warn(this.getServiceName() + " service has exception. ", e); - this.waitForRunning(1000 * 5); - } - } + /** + * Get HA runtime info + */ + HARuntimeInfo getRuntimeInfo(final long masterPutWhere); - log.info(this.getServiceName() + " service end"); - } - // private void disableWriteFlag() { - // if (this.socketChannel != null) { - // SelectionKey sk = this.socketChannel.keyFor(this.selector); - // if (sk != null) { - // int ops = sk.interestOps(); - // ops &= ~SelectionKey.OP_WRITE; - // sk.interestOps(ops); - // } - // } - // } - // private void enableWriteFlag() { - // if (this.socketChannel != null) { - // SelectionKey sk = this.socketChannel.keyFor(this.selector); - // if (sk != null) { - // int ops = sk.interestOps(); - // ops |= SelectionKey.OP_WRITE; - // sk.interestOps(ops); - // } - // } - // } + /** + * Get WaitNotifyObject + */ + WaitNotifyObject getWaitNotifyObject(); - @Override - public String getServiceName() { - return HAClient.class.getSimpleName(); - } - } + /** + * Judge whether the slave keeps up according to the masterPutWhere, If the offset gap exceeds haSlaveFallBehindMax, + * then slave is not OK + */ + boolean isSlaveOK(long masterPutWhere); } diff --git a/store/src/main/java/org/apache/rocketmq/store/ha/WaitNotifyObject.java b/store/src/main/java/org/apache/rocketmq/store/ha/WaitNotifyObject.java index 6aba37529a4..c040bf98b3a 100644 --- a/store/src/main/java/org/apache/rocketmq/store/ha/WaitNotifyObject.java +++ b/store/src/main/java/org/apache/rocketmq/store/ha/WaitNotifyObject.java @@ -17,42 +17,47 @@ package org.apache.rocketmq.store.ha; import org.apache.rocketmq.common.constant.LoggerName; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.common.utils.ConcurrentHashMapUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; -import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; public class WaitNotifyObject { private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); - protected final HashMap waitingThreadTable = - new HashMap(16); + protected final ConcurrentHashMap waitingThreadTable = + new ConcurrentHashMap<>(16); - protected volatile boolean hasNotified = false; + protected AtomicBoolean hasNotified = new AtomicBoolean(false); public void wakeup() { - synchronized (this) { - if (!this.hasNotified) { - this.hasNotified = true; + boolean needNotify = hasNotified.compareAndSet(false, true); + if (needNotify) { + synchronized (this) { this.notify(); } } } protected void waitForRunning(long interval) { + if (this.hasNotified.compareAndSet(true, false)) { + this.onWaitEnd(); + return; + } synchronized (this) { - if (this.hasNotified) { - this.hasNotified = false; - this.onWaitEnd(); - return; - } - try { + if (this.hasNotified.compareAndSet(true, false)) { + this.onWaitEnd(); + return; + } this.wait(interval); } catch (InterruptedException e) { log.error("Interrupted", e); } finally { - this.hasNotified = false; + this.hasNotified.set(false); this.onWaitEnd(); } } @@ -62,15 +67,14 @@ protected void onWaitEnd() { } public void wakeupAll() { - synchronized (this) { - boolean needNotify = false; - - for (Boolean value : this.waitingThreadTable.values()) { - needNotify = needNotify || !value; - value = true; + boolean needNotify = false; + for (Map.Entry entry : this.waitingThreadTable.entrySet()) { + if (entry.getValue().compareAndSet(false, true)) { + needNotify = true; } - - if (needNotify) { + } + if (needNotify) { + synchronized (this) { this.notifyAll(); } } @@ -78,22 +82,31 @@ public void wakeupAll() { public void allWaitForRunning(long interval) { long currentThreadId = Thread.currentThread().getId(); + AtomicBoolean notified = ConcurrentHashMapUtils.computeIfAbsent(this.waitingThreadTable, currentThreadId, k -> new AtomicBoolean(false)); + if (notified.compareAndSet(true, false)) { + this.onWaitEnd(); + return; + } synchronized (this) { - Boolean notified = this.waitingThreadTable.get(currentThreadId); - if (notified != null && notified) { - this.waitingThreadTable.put(currentThreadId, false); - this.onWaitEnd(); - return; - } - try { + if (notified.compareAndSet(true, false)) { + this.onWaitEnd(); + return; + } this.wait(interval); } catch (InterruptedException e) { log.error("Interrupted", e); } finally { - this.waitingThreadTable.put(currentThreadId, false); + notified.set(false); this.onWaitEnd(); } } } + + public void removeFromWaitingThreadTable() { + long currentThreadId = Thread.currentThread().getId(); + synchronized (this) { + this.waitingThreadTable.remove(currentThreadId); + } + } } diff --git a/store/src/main/java/org/apache/rocketmq/store/ha/autoswitch/AutoSwitchHAClient.java b/store/src/main/java/org/apache/rocketmq/store/ha/autoswitch/AutoSwitchHAClient.java new file mode 100644 index 00000000000..176c25a96f6 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ha/autoswitch/AutoSwitchHAClient.java @@ -0,0 +1,598 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.ha.autoswitch; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.apache.rocketmq.remoting.protocol.EpochEntry; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.ha.FlowMonitor; +import org.apache.rocketmq.store.ha.HAClient; +import org.apache.rocketmq.store.ha.HAConnectionState; +import org.apache.rocketmq.store.ha.io.AbstractHAReader; +import org.apache.rocketmq.store.ha.io.HAWriter; + +public class AutoSwitchHAClient extends ServiceThread implements HAClient { + + /** + * Handshake header buffer size. Schema: state ordinal + Two flags + slaveBrokerId. Format: + * + *

    +     *                   ┌──────────────────┬───────────────┐
    +     *                   │isSyncFromLastFile│ isAsyncLearner│
    +     *                   │     (2bytes)     │   (2bytes)    │
    +     *                   └──────────────────┴───────────────┘
    +     *                     \                              /
    +     *                      \                            /
    +     *                       ╲                          /
    +     *                        ╲                        /
    +     * ┌───────────────────────┬───────────────────────┬───────────────────────┐
    +     * │      current state    │          Flags        │      slaveBrokerId    │
    +     * │         (4bytes)      │         (4bytes)      │         (8bytes)      │
    +     * ├───────────────────────┴───────────────────────┴───────────────────────┤
    +     * │                                                                       │
    +     * │                          HANDSHAKE  Header                            │
    +     * 
    + *

    + * Flag: isSyncFromLastFile(short), isAsyncLearner(short)... we can add more flags in the future if needed + */ + public static final int HANDSHAKE_HEADER_SIZE = 4 + 4 + 8; + + /** + * Header + slaveAddress, Format: + *

    +     *                   ┌──────────────────┬───────────────┐
    +     *                   │isSyncFromLastFile│ isAsyncLearner│
    +     *                   │     (2bytes)     │   (2bytes)    │
    +     *                   └──────────────────┴───────────────┘
    +     *                     \                              /
    +     *                      \                            /
    +     *                       ╲                          /
    +     *                        ╲                        /
    +     * ┌───────────────────────┬───────────────────────┬───────────────────────┬───────────────────────────────┐
    +     * │      current state    │          Flags        │  slaveAddressLength   │          slaveAddress         │
    +     * │         (4bytes)      │         (4bytes)      │         (4bytes)      │             (50bytes)         │
    +     * ├───────────────────────┴───────────────────────┴───────────────────────┼───────────────────────────────┤
    +     * │                                                                       │                               │
    +     * │                        HANDSHAKE  Header                              │               body            │
    +     * 
    + */ + @Deprecated + public static final int HANDSHAKE_SIZE = HANDSHAKE_HEADER_SIZE + 50; + + /** + * Transfer header buffer size. Schema: state ordinal + maxOffset. Format: + *
    +     * ┌───────────────────────┬───────────────────────┐
    +     * │      current state    │        maxOffset      │
    +     * │         (4bytes)      │         (8bytes)      │
    +     * ├───────────────────────┴───────────────────────┤
    +     * │                                               │
    +     * │                TRANSFER  Header               │
    +     * 
    + */ + public static final int TRANSFER_HEADER_SIZE = 4 + 8; + public static final int MIN_HEADER_SIZE = Math.min(HANDSHAKE_HEADER_SIZE, TRANSFER_HEADER_SIZE); + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + private static final int READ_MAX_BUFFER_SIZE = 1024 * 1024 * 4; + private final AtomicReference masterHaAddress = new AtomicReference<>(); + private final AtomicReference masterAddress = new AtomicReference<>(); + private final ByteBuffer handshakeHeaderBuffer = ByteBuffer.allocate(HANDSHAKE_HEADER_SIZE); + private final ByteBuffer transferHeaderBuffer = ByteBuffer.allocate(TRANSFER_HEADER_SIZE); + private final AutoSwitchHAService haService; + private final ByteBuffer byteBufferRead = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE); + private final DefaultMessageStore messageStore; + private final EpochFileCache epochCache; + + private final Long brokerId; + + private SocketChannel socketChannel; + private Selector selector; + private AbstractHAReader haReader; + private HAWriter haWriter; + private FlowMonitor flowMonitor; + /** + * last time that slave reads date from master. + */ + private long lastReadTimestamp; + /** + * last time that slave reports offset to master. + */ + private long lastWriteTimestamp; + + private long currentReportedOffset; + private int processPosition; + private volatile HAConnectionState currentState; + /** + * Current epoch + */ + private volatile int currentReceivedEpoch; + + public AutoSwitchHAClient(AutoSwitchHAService haService, DefaultMessageStore defaultMessageStore, + EpochFileCache epochCache, Long brokerId) throws IOException { + this.haService = haService; + this.messageStore = defaultMessageStore; + this.epochCache = epochCache; + this.brokerId = brokerId; + init(); + } + + public void init() throws IOException { + this.selector = NetworkUtil.openSelector(); + this.flowMonitor = new FlowMonitor(this.messageStore.getMessageStoreConfig()); + this.haReader = new HAClientReader(); + haReader.registerHook(readSize -> { + if (readSize > 0) { + AutoSwitchHAClient.this.flowMonitor.addByteCountTransferred(readSize); + lastReadTimestamp = System.currentTimeMillis(); + } + }); + this.haWriter = new HAWriter(); + haWriter.registerHook(writeSize -> { + if (writeSize > 0) { + lastWriteTimestamp = System.currentTimeMillis(); + } + }); + changeCurrentState(HAConnectionState.READY); + this.currentReceivedEpoch = -1; + this.currentReportedOffset = 0; + this.processPosition = 0; + this.lastReadTimestamp = System.currentTimeMillis(); + this.lastWriteTimestamp = System.currentTimeMillis(); + } + + public void reOpen() throws IOException { + shutdown(); + init(); + } + + @Override + public String getServiceName() { + if (haService.getDefaultMessageStore().getBrokerConfig().isInBrokerContainer()) { + return haService.getDefaultMessageStore().getBrokerIdentity().getIdentifier() + AutoSwitchHAClient.class.getSimpleName(); + } + return AutoSwitchHAClient.class.getSimpleName(); + } + + @Override + public void updateMasterAddress(String newAddress) { + String currentAddr = this.masterAddress.get(); + if (!StringUtils.equals(newAddress, currentAddr) && masterAddress.compareAndSet(currentAddr, newAddress)) { + LOGGER.info("update master address, OLD: " + currentAddr + " NEW: " + newAddress); + } + } + + @Override + public void updateHaMasterAddress(String newAddress) { + String currentAddr = this.masterHaAddress.get(); + if (!StringUtils.equals(newAddress, currentAddr) && masterHaAddress.compareAndSet(currentAddr, newAddress)) { + LOGGER.info("update master ha address, OLD: " + currentAddr + " NEW: " + newAddress); + wakeup(); + } + } + + @Override + public String getMasterAddress() { + return this.masterAddress.get(); + } + + @Override + public String getHaMasterAddress() { + return this.masterHaAddress.get(); + } + + @Override + public long getLastReadTimestamp() { + return this.lastReadTimestamp; + } + + @Override + public long getLastWriteTimestamp() { + return this.lastWriteTimestamp; + } + + @Override + public HAConnectionState getCurrentState() { + return this.currentState; + } + + @Override + public void changeCurrentState(HAConnectionState haConnectionState) { + LOGGER.info("change state to {}", haConnectionState); + this.currentState = haConnectionState; + } + + public void closeMasterAndWait() { + this.closeMaster(); + this.waitForRunning(1000 * 5); + } + + @Override + public void closeMaster() { + if (null != this.socketChannel) { + try { + SelectionKey sk = this.socketChannel.keyFor(this.selector); + if (sk != null) { + sk.cancel(); + } + + this.socketChannel.close(); + this.socketChannel = null; + + LOGGER.info("AutoSwitchHAClient close connection with master {}", this.masterHaAddress.get()); + this.changeCurrentState(HAConnectionState.READY); + } catch (IOException e) { + LOGGER.warn("CloseMaster exception. ", e); + } + + this.lastReadTimestamp = 0; + this.processPosition = 0; + + this.byteBufferRead.position(0); + this.byteBufferRead.limit(READ_MAX_BUFFER_SIZE); + } + } + + @Override + public long getTransferredByteInSecond() { + return this.flowMonitor.getTransferredByteInSecond(); + } + + @Override + public void shutdown() { + changeCurrentState(HAConnectionState.SHUTDOWN); + // Shutdown thread firstly + this.flowMonitor.shutdown(); + super.shutdown(); + + closeMaster(); + try { + this.selector.close(); + } catch (IOException e) { + LOGGER.warn("Close the selector of AutoSwitchHAClient error, ", e); + } + } + + private boolean isTimeToReportOffset() { + long interval = this.messageStore.now() - this.lastWriteTimestamp; + return interval > this.messageStore.getMessageStoreConfig().getHaSendHeartbeatInterval(); + } + + private boolean sendHandshakeHeader() throws IOException { + this.handshakeHeaderBuffer.position(0); + this.handshakeHeaderBuffer.limit(HANDSHAKE_HEADER_SIZE); + // Original state + this.handshakeHeaderBuffer.putInt(HAConnectionState.HANDSHAKE.ordinal()); + // IsSyncFromLastFile + short isSyncFromLastFile = this.haService.getDefaultMessageStore().getMessageStoreConfig().isSyncFromLastFile() ? (short) 1 : (short) 0; + this.handshakeHeaderBuffer.putShort(isSyncFromLastFile); + // IsAsyncLearner role + short isAsyncLearner = this.haService.getDefaultMessageStore().getMessageStoreConfig().isAsyncLearner() ? (short) 1 : (short) 0; + this.handshakeHeaderBuffer.putShort(isAsyncLearner); + // Slave brokerId + this.handshakeHeaderBuffer.putLong(this.brokerId); + + this.handshakeHeaderBuffer.flip(); + return this.haWriter.write(this.socketChannel, this.handshakeHeaderBuffer); + } + + private void handshakeWithMaster() throws IOException { + boolean result = this.sendHandshakeHeader(); + if (!result) { + closeMasterAndWait(); + } + + this.selector.select(5000); + + result = this.haReader.read(this.socketChannel, this.byteBufferRead); + if (!result) { + closeMasterAndWait(); + } + } + + private boolean reportSlaveOffset(HAConnectionState currentState, final long offsetToReport) throws IOException { + this.transferHeaderBuffer.position(0); + this.transferHeaderBuffer.limit(TRANSFER_HEADER_SIZE); + this.transferHeaderBuffer.putInt(currentState.ordinal()); + this.transferHeaderBuffer.putLong(offsetToReport); + this.transferHeaderBuffer.flip(); + return this.haWriter.write(this.socketChannel, this.transferHeaderBuffer); + } + + private boolean reportSlaveMaxOffset(HAConnectionState currentState) throws IOException { + boolean result = true; + final long maxPhyOffset = this.messageStore.getMaxPhyOffset(); + if (maxPhyOffset > this.currentReportedOffset) { + this.currentReportedOffset = maxPhyOffset; + result = reportSlaveOffset(currentState, this.currentReportedOffset); + } + return result; + } + + public boolean connectMaster() throws IOException { + if (null == this.socketChannel) { + String addr = this.masterHaAddress.get(); + if (StringUtils.isNotEmpty(addr)) { + SocketAddress socketAddress = NetworkUtil.string2SocketAddress(addr); + this.socketChannel = RemotingHelper.connect(socketAddress); + if (this.socketChannel != null) { + this.socketChannel.register(this.selector, SelectionKey.OP_READ); + LOGGER.info("AutoSwitchHAClient connect to master {}", addr); + changeCurrentState(HAConnectionState.HANDSHAKE); + } + } + this.currentReportedOffset = this.messageStore.getMaxPhyOffset(); + this.lastReadTimestamp = System.currentTimeMillis(); + } + return this.socketChannel != null; + } + + private boolean transferFromMaster() throws IOException { + boolean result; + if (isTimeToReportOffset()) { + LOGGER.info("Slave report current offset {}", this.currentReportedOffset); + result = reportSlaveOffset(HAConnectionState.TRANSFER, this.currentReportedOffset); + if (!result) { + return false; + } + } + + this.selector.select(1000); + + result = this.haReader.read(this.socketChannel, this.byteBufferRead); + if (!result) { + return false; + } + + return this.reportSlaveMaxOffset(HAConnectionState.TRANSFER); + } + + @Override + public void run() { + LOGGER.info(this.getServiceName() + " service started"); + + this.flowMonitor.start(); + while (!this.isStopped()) { + try { + switch (this.currentState) { + case SHUTDOWN: + this.flowMonitor.shutdown(true); + return; + case READY: + // Truncate invalid msg first + final long truncateOffset = AutoSwitchHAClient.this.haService.truncateInvalidMsg(); + if (truncateOffset >= 0) { + AutoSwitchHAClient.this.epochCache.truncateSuffixByOffset(truncateOffset); + } + if (!connectMaster()) { + LOGGER.warn("AutoSwitchHAClient connect to master {} failed", this.masterHaAddress.get()); + waitForRunning(1000 * 5); + } + continue; + case HANDSHAKE: + handshakeWithMaster(); + continue; + case TRANSFER: + if (!transferFromMaster()) { + closeMasterAndWait(); + continue; + } + break; + case SUSPEND: + default: + waitForRunning(1000 * 5); + continue; + } + long interval = this.messageStore.now() - this.lastReadTimestamp; + if (interval > this.messageStore.getMessageStoreConfig().getHaHousekeepingInterval()) { + LOGGER.warn("AutoSwitchHAClient, housekeeping, found this connection[" + this.masterHaAddress + + "] expired, " + interval); + closeMaster(); + LOGGER.warn("AutoSwitchHAClient, master not response some time, so close connection"); + } + } catch (Exception e) { + LOGGER.warn(this.getServiceName() + " service has exception. ", e); + closeMasterAndWait(); + } + } + + this.flowMonitor.shutdown(true); + LOGGER.info(this.getServiceName() + " service end"); + } + + /** + * Compare the master and slave's epoch file, find consistent point, do truncate. + */ + private boolean doTruncate(List masterEpochEntries, long masterEndOffset) throws Exception { + if (this.epochCache.getEntrySize() == 0) { + // If epochMap is empty, means the broker is a new replicas + LOGGER.info("Slave local epochCache is empty, skip truncate log"); + changeCurrentState(HAConnectionState.TRANSFER); + this.currentReportedOffset = 0; + } else { + final EpochFileCache masterEpochCache = new EpochFileCache(); + masterEpochCache.initCacheFromEntries(masterEpochEntries); + masterEpochCache.setLastEpochEntryEndOffset(masterEndOffset); + final List localEpochEntries = this.epochCache.getAllEntries(); + final EpochFileCache localEpochCache = new EpochFileCache(); + localEpochCache.initCacheFromEntries(localEpochEntries); + localEpochCache.setLastEpochEntryEndOffset(this.messageStore.getMaxPhyOffset()); + + LOGGER.info("master epoch entries is {}", masterEpochCache.getAllEntries()); + LOGGER.info("local epoch entries is {}", localEpochEntries); + + final long truncateOffset = localEpochCache.findConsistentPoint(masterEpochCache); + + LOGGER.info("truncateOffset is {}", truncateOffset); + + if (truncateOffset < 0) { + // If truncateOffset < 0, means we can't find a consistent point + LOGGER.error("Failed to find a consistent point between masterEpoch:{} and slaveEpoch:{}", masterEpochEntries, localEpochEntries); + return false; + } + if (!this.messageStore.truncateFiles(truncateOffset)) { + LOGGER.error("Failed to truncate slave log to {}", truncateOffset); + return false; + } + this.epochCache.truncateSuffixByOffset(truncateOffset); + LOGGER.info("Truncate slave log to {} success, change to transfer state", truncateOffset); + changeCurrentState(HAConnectionState.TRANSFER); + this.currentReportedOffset = truncateOffset; + } + if (!reportSlaveMaxOffset(HAConnectionState.TRANSFER)) { + LOGGER.error("AutoSwitchHAClient report max offset to master failed"); + return false; + } + return true; + } + + class HAClientReader extends AbstractHAReader { + + @Override + protected boolean processReadResult(ByteBuffer byteBufferRead) { + int readSocketPos = byteBufferRead.position(); + try { + while (true) { + int diff = byteBufferRead.position() - AutoSwitchHAClient.this.processPosition; + if (diff >= AutoSwitchHAConnection.HANDSHAKE_HEADER_SIZE) { + final int processPosition = AutoSwitchHAClient.this.processPosition; + int masterState = byteBufferRead.getInt(processPosition + AutoSwitchHAConnection.HANDSHAKE_HEADER_SIZE - 20); + int bodySize = byteBufferRead.getInt(processPosition + AutoSwitchHAConnection.HANDSHAKE_HEADER_SIZE - 16); + long masterOffset = byteBufferRead.getLong(processPosition + AutoSwitchHAConnection.HANDSHAKE_HEADER_SIZE - 12); + int masterEpoch = byteBufferRead.getInt(processPosition + AutoSwitchHAConnection.HANDSHAKE_HEADER_SIZE - 4); + long masterEpochStartOffset = 0; + long confirmOffset = 0; + // If master send transfer header data, set masterEpochStartOffset and confirmOffset value. + if (masterState == HAConnectionState.TRANSFER.ordinal() && diff >= AutoSwitchHAConnection.TRANSFER_HEADER_SIZE) { + masterEpochStartOffset = byteBufferRead.getLong(processPosition + AutoSwitchHAConnection.TRANSFER_HEADER_SIZE - 16); + confirmOffset = byteBufferRead.getLong(processPosition + AutoSwitchHAConnection.TRANSFER_HEADER_SIZE - 8); + } + if (masterState != AutoSwitchHAClient.this.currentState.ordinal()) { + int headerSize = masterState == HAConnectionState.TRANSFER.ordinal() ? AutoSwitchHAConnection.TRANSFER_HEADER_SIZE : AutoSwitchHAConnection.HANDSHAKE_HEADER_SIZE; + AutoSwitchHAClient.this.processPosition += headerSize + bodySize; + AutoSwitchHAClient.this.waitForRunning(1); + LOGGER.error("State not matched, masterState:{}, slaveState:{}, bodySize:{}, offset:{}, masterEpoch:{}, masterEpochStartOffset:{}, confirmOffset:{}", + HAConnectionState.values()[masterState], AutoSwitchHAClient.this.currentState, bodySize, masterOffset, masterEpoch, masterEpochStartOffset, confirmOffset); + return false; + } + + // Flag whether the received data is complete + boolean isComplete = true; + switch (AutoSwitchHAClient.this.currentState) { + case HANDSHAKE: { + if (diff < AutoSwitchHAConnection.HANDSHAKE_HEADER_SIZE + bodySize) { + // The received HANDSHAKE data is not complete + isComplete = false; + break; + } + AutoSwitchHAClient.this.processPosition += AutoSwitchHAConnection.HANDSHAKE_HEADER_SIZE; + // Truncate log + int entrySize = AutoSwitchHAConnection.EPOCH_ENTRY_SIZE; + final int entryNums = bodySize / entrySize; + final ArrayList epochEntries = new ArrayList<>(entryNums); + for (int i = 0; i < entryNums; i++) { + int epoch = byteBufferRead.getInt(AutoSwitchHAClient.this.processPosition + i * entrySize); + long startOffset = byteBufferRead.getLong(AutoSwitchHAClient.this.processPosition + i * entrySize + 4); + epochEntries.add(new EpochEntry(epoch, startOffset)); + } + byteBufferRead.position(readSocketPos); + AutoSwitchHAClient.this.processPosition += bodySize; + LOGGER.info("Receive handshake, masterMaxPosition {}, masterEpochEntries:{}, try truncate log", masterOffset, epochEntries); + if (!doTruncate(epochEntries, masterOffset)) { + waitForRunning(1000 * 2); + LOGGER.error("AutoSwitchHAClient truncate log failed in handshake state"); + return false; + } + } + break; + case TRANSFER: { + if (diff < AutoSwitchHAConnection.TRANSFER_HEADER_SIZE + bodySize) { + // The received TRANSFER data is not complete + isComplete = false; + break; + } + byte[] bodyData = new byte[bodySize]; + byteBufferRead.position(AutoSwitchHAClient.this.processPosition + AutoSwitchHAConnection.TRANSFER_HEADER_SIZE); + byteBufferRead.get(bodyData); + byteBufferRead.position(readSocketPos); + AutoSwitchHAClient.this.processPosition += AutoSwitchHAConnection.TRANSFER_HEADER_SIZE + bodySize; + long slavePhyOffset = AutoSwitchHAClient.this.messageStore.getMaxPhyOffset(); + if (slavePhyOffset != 0) { + if (slavePhyOffset != masterOffset) { + LOGGER.error("master pushed offset not equal the max phy offset in slave, SLAVE: " + + slavePhyOffset + " MASTER: " + masterOffset); + return false; + } + } + + // If epoch changed + if (masterEpoch != AutoSwitchHAClient.this.currentReceivedEpoch) { + AutoSwitchHAClient.this.currentReceivedEpoch = masterEpoch; + AutoSwitchHAClient.this.epochCache.appendEntry(new EpochEntry(masterEpoch, masterEpochStartOffset)); + } + + if (bodySize > 0) { + AutoSwitchHAClient.this.messageStore.appendToCommitLog(masterOffset, bodyData, 0, bodyData.length); + } + + haService.getDefaultMessageStore().setConfirmOffset(Math.min(confirmOffset, messageStore.getMaxPhyOffset())); + + if (!reportSlaveMaxOffset(HAConnectionState.TRANSFER)) { + LOGGER.error("AutoSwitchHAClient report max offset to master failed"); + return false; + } + break; + } + default: + break; + } + if (isComplete) { + continue; + } + + } + + if (!byteBufferRead.hasRemaining()) { + byteBufferRead.position(AutoSwitchHAClient.this.processPosition); + byteBufferRead.compact(); + AutoSwitchHAClient.this.processPosition = 0; + } + + break; + } + } catch (final Exception e) { + LOGGER.error("Error when ha client process read request", e); + } + return true; + } + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/ha/autoswitch/AutoSwitchHAConnection.java b/store/src/main/java/org/apache/rocketmq/store/ha/autoswitch/AutoSwitchHAConnection.java new file mode 100644 index 00000000000..440cd3c7a50 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ha/autoswitch/AutoSwitchHAConnection.java @@ -0,0 +1,744 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.ha.autoswitch; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.util.List; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.netty.NettySystemConfig; +import org.apache.rocketmq.remoting.protocol.EpochEntry; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.ha.FlowMonitor; +import org.apache.rocketmq.store.ha.HAConnection; +import org.apache.rocketmq.store.ha.HAConnectionState; +import org.apache.rocketmq.store.ha.io.AbstractHAReader; +import org.apache.rocketmq.store.ha.io.HAWriter; + +public class AutoSwitchHAConnection implements HAConnection { + + /** + * Handshake data protocol in syncing msg from master. Format: + *
    +     * ┌─────────────────┬───────────────┬───────────┬───────────┬────────────────────────────────────┐
    +     * │  current state  │   body size   │   offset  │   epoch   │   EpochEntrySize * EpochEntryNums  │
    +     * │     (4bytes)    │   (4bytes)    │  (8bytes) │  (4bytes) │      (12bytes * EpochEntryNums)    │
    +     * ├─────────────────┴───────────────┴───────────┴───────────┼────────────────────────────────────┤
    +     * │                       Header                            │             Body                   │
    +     * │                                                         │                                    │
    +     * 
    + * Handshake Header protocol Format: + * current state + body size + offset + epoch + */ + public static final int HANDSHAKE_HEADER_SIZE = 4 + 4 + 8 + 4; + + /** + * Transfer data protocol in syncing msg from master. Format: + *
    +     * ┌─────────────────┬───────────────┬───────────┬───────────┬─────────────────────┬──────────────────┬──────────────────┐
    +     * │  current state  │   body size   │   offset  │   epoch   │   epochStartOffset  │   confirmOffset  │    log data      │
    +     * │     (4bytes)    │   (4bytes)    │  (8bytes) │  (4bytes) │      (8bytes)       │      (8bytes)    │   (data size)    │
    +     * ├─────────────────┴───────────────┴───────────┴───────────┴─────────────────────┴──────────────────┼──────────────────┤
    +     * │                                               Header                                             │       Body       │
    +     * │                                                                                                  │                  │
    +     * 
    + * Transfer Header protocol Format: + * current state + body size + offset + epoch + epochStartOffset + additionalInfo(confirmOffset) + */ + public static final int TRANSFER_HEADER_SIZE = HANDSHAKE_HEADER_SIZE + 8 + 8; + public static final int EPOCH_ENTRY_SIZE = 12; + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + private final AutoSwitchHAService haService; + private final SocketChannel socketChannel; + private final String clientAddress; + private final EpochFileCache epochCache; + private final AbstractWriteSocketService writeSocketService; + private final ReadSocketService readSocketService; + private final FlowMonitor flowMonitor; + + private volatile HAConnectionState currentState = HAConnectionState.HANDSHAKE; + private volatile long slaveRequestOffset = -1; + private volatile long slaveAckOffset = -1; + /** + * Whether the slave have already sent a handshake message + */ + private volatile boolean isSlaveSendHandshake = false; + private volatile int currentTransferEpoch = -1; + private volatile long currentTransferEpochEndOffset = 0; + private volatile boolean isSyncFromLastFile = false; + private volatile boolean isAsyncLearner = false; + private volatile long slaveId = -1; + + /** + * Last endOffset when master transfer data to slave + */ + private volatile long lastMasterMaxOffset = -1; + /** + * Last time ms when transfer data to slave. + */ + private volatile long lastTransferTimeMs = 0; + + public AutoSwitchHAConnection(AutoSwitchHAService haService, SocketChannel socketChannel, + EpochFileCache epochCache) throws IOException { + this.haService = haService; + this.socketChannel = socketChannel; + this.epochCache = epochCache; + this.clientAddress = this.socketChannel.socket().getRemoteSocketAddress().toString(); + this.socketChannel.configureBlocking(false); + this.socketChannel.socket().setSoLinger(false, -1); + this.socketChannel.socket().setTcpNoDelay(true); + if (NettySystemConfig.socketSndbufSize > 0) { + this.socketChannel.socket().setReceiveBufferSize(NettySystemConfig.socketSndbufSize); + } + if (NettySystemConfig.socketRcvbufSize > 0) { + this.socketChannel.socket().setSendBufferSize(NettySystemConfig.socketRcvbufSize); + } + this.writeSocketService = new WriteSocketService(this.socketChannel); + this.readSocketService = new ReadSocketService(this.socketChannel); + this.haService.getConnectionCount().incrementAndGet(); + this.flowMonitor = new FlowMonitor(haService.getDefaultMessageStore().getMessageStoreConfig()); + } + + @Override + public void start() { + changeCurrentState(HAConnectionState.HANDSHAKE); + this.flowMonitor.start(); + this.readSocketService.start(); + this.writeSocketService.start(); + } + + @Override + public void shutdown() { + changeCurrentState(HAConnectionState.SHUTDOWN); + this.flowMonitor.shutdown(true); + this.writeSocketService.shutdown(true); + this.readSocketService.shutdown(true); + this.close(); + } + + @Override + public void close() { + if (this.socketChannel != null) { + try { + this.socketChannel.close(); + } catch (final IOException e) { + LOGGER.error("", e); + } + } + } + + public void changeCurrentState(HAConnectionState connectionState) { + LOGGER.info("change state to {}", connectionState); + this.currentState = connectionState; + } + + public long getSlaveId() { + return slaveId; + } + + @Override + public HAConnectionState getCurrentState() { + return currentState; + } + + @Override + public SocketChannel getSocketChannel() { + return socketChannel; + } + + @Override + public String getClientAddress() { + return clientAddress; + } + + @Override + public long getSlaveAckOffset() { + return slaveAckOffset; + } + + @Override + public long getTransferredByteInSecond() { + return flowMonitor.getTransferredByteInSecond(); + } + + @Override + public long getTransferFromWhere() { + return this.writeSocketService.getNextTransferFromWhere(); + } + + private void changeTransferEpochToNext(final EpochEntry entry) { + this.currentTransferEpoch = entry.getEpoch(); + this.currentTransferEpochEndOffset = entry.getEndOffset(); + if (entry.getEpoch() == this.epochCache.lastEpoch()) { + // Use -1 to stand for Long.max + this.currentTransferEpochEndOffset = -1; + } + } + + public boolean isAsyncLearner() { + return isAsyncLearner; + } + + public boolean isSyncFromLastFile() { + return isSyncFromLastFile; + } + + private synchronized void updateLastTransferInfo() { + this.lastMasterMaxOffset = this.haService.getDefaultMessageStore().getMaxPhyOffset(); + this.lastTransferTimeMs = System.currentTimeMillis(); + } + + private synchronized void maybeExpandInSyncStateSet(long slaveMaxOffset) { + if (!this.isAsyncLearner && slaveMaxOffset >= this.lastMasterMaxOffset) { + long caughtUpTimeMs = this.haService.getDefaultMessageStore().getMaxPhyOffset() == slaveMaxOffset ? System.currentTimeMillis() : this.lastTransferTimeMs; + this.haService.updateConnectionLastCaughtUpTime(this.slaveId, caughtUpTimeMs); + this.haService.maybeExpandInSyncStateSet(this.slaveId, slaveMaxOffset); + } + } + + class ReadSocketService extends ServiceThread { + private static final int READ_MAX_BUFFER_SIZE = 1024 * 1024; + private final Selector selector; + private final SocketChannel socketChannel; + private final ByteBuffer byteBufferRead = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE); + private final AbstractHAReader haReader; + private int processPosition = 0; + private volatile long lastReadTimestamp = System.currentTimeMillis(); + + public ReadSocketService(final SocketChannel socketChannel) throws IOException { + this.selector = NetworkUtil.openSelector(); + this.socketChannel = socketChannel; + this.socketChannel.register(this.selector, SelectionKey.OP_READ); + this.setDaemon(true); + haReader = new HAServerReader(); + haReader.registerHook(readSize -> { + if (readSize > 0) { + ReadSocketService.this.lastReadTimestamp = + haService.getDefaultMessageStore().getSystemClock().now(); + } + }); + } + + @Override + public void run() { + LOGGER.info(this.getServiceName() + " service started"); + + while (!this.isStopped()) { + try { + this.selector.select(1000); + boolean ok = this.haReader.read(this.socketChannel, this.byteBufferRead); + if (!ok) { + AutoSwitchHAConnection.LOGGER.error("processReadEvent error"); + break; + } + + long interval = haService.getDefaultMessageStore().getSystemClock().now() - this.lastReadTimestamp; + if (interval > haService.getDefaultMessageStore().getMessageStoreConfig().getHaHousekeepingInterval()) { + LOGGER.warn("ha housekeeping, found this connection[" + clientAddress + "] expired, " + interval); + break; + } + } catch (Exception e) { + AutoSwitchHAConnection.LOGGER.error(this.getServiceName() + " service has exception.", e); + break; + } + } + + this.makeStop(); + + changeCurrentState(HAConnectionState.SHUTDOWN); + + writeSocketService.makeStop(); + + haService.removeConnection(AutoSwitchHAConnection.this); + + haService.getConnectionCount().decrementAndGet(); + + SelectionKey sk = this.socketChannel.keyFor(this.selector); + if (sk != null) { + sk.cancel(); + } + + try { + this.selector.close(); + this.socketChannel.close(); + } catch (IOException e) { + AutoSwitchHAConnection.LOGGER.error("", e); + } + + flowMonitor.shutdown(true); + + AutoSwitchHAConnection.LOGGER.info(this.getServiceName() + " service end"); + } + + @Override + public String getServiceName() { + if (haService.getDefaultMessageStore().getBrokerConfig().isInBrokerContainer()) { + return haService.getDefaultMessageStore().getBrokerIdentity().getIdentifier() + ReadSocketService.class.getSimpleName(); + } + return ReadSocketService.class.getSimpleName(); + } + + class HAServerReader extends AbstractHAReader { + @Override + protected boolean processReadResult(ByteBuffer byteBufferRead) { + while (true) { + boolean processSuccess = true; + int readSocketPos = byteBufferRead.position(); + int diff = byteBufferRead.position() - ReadSocketService.this.processPosition; + if (diff >= AutoSwitchHAClient.MIN_HEADER_SIZE) { + int readPosition = ReadSocketService.this.processPosition; + HAConnectionState slaveState = HAConnectionState.values()[byteBufferRead.getInt(readPosition)]; + + switch (slaveState) { + case HANDSHAKE: + // SlaveBrokerId + Long slaveBrokerId = byteBufferRead.getLong(readPosition + AutoSwitchHAClient.HANDSHAKE_HEADER_SIZE - 8); + AutoSwitchHAConnection.this.slaveId = slaveBrokerId; + // Flag(isSyncFromLastFile) + short syncFromLastFileFlag = byteBufferRead.getShort(readPosition + AutoSwitchHAClient.HANDSHAKE_HEADER_SIZE - 12); + if (syncFromLastFileFlag == 1) { + AutoSwitchHAConnection.this.isSyncFromLastFile = true; + } + // Flag(isAsyncLearner role) + short isAsyncLearner = byteBufferRead.getShort(readPosition + AutoSwitchHAClient.HANDSHAKE_HEADER_SIZE - 10); + if (isAsyncLearner == 1) { + AutoSwitchHAConnection.this.isAsyncLearner = true; + } + + isSlaveSendHandshake = true; + byteBufferRead.position(readSocketPos); + ReadSocketService.this.processPosition += AutoSwitchHAClient.HANDSHAKE_HEADER_SIZE; + LOGGER.info("Receive slave handshake, slaveBrokerId:{}, isSyncFromLastFile:{}, isAsyncLearner:{}", + AutoSwitchHAConnection.this.slaveId, AutoSwitchHAConnection.this.isSyncFromLastFile, AutoSwitchHAConnection.this.isAsyncLearner); + break; + case TRANSFER: + long slaveMaxOffset = byteBufferRead.getLong(readPosition + 4); + ReadSocketService.this.processPosition += AutoSwitchHAClient.TRANSFER_HEADER_SIZE; + + AutoSwitchHAConnection.this.slaveAckOffset = slaveMaxOffset; + if (slaveRequestOffset < 0) { + slaveRequestOffset = slaveMaxOffset; + } + byteBufferRead.position(readSocketPos); + maybeExpandInSyncStateSet(slaveMaxOffset); + AutoSwitchHAConnection.this.haService.updateConfirmOffsetWhenSlaveAck(AutoSwitchHAConnection.this.slaveId); + AutoSwitchHAConnection.this.haService.notifyTransferSome(AutoSwitchHAConnection.this.slaveAckOffset); + break; + default: + LOGGER.error("Current state illegal {}", currentState); + return false; + } + + if (!slaveState.equals(currentState)) { + LOGGER.warn("Master change state from {} to {}", currentState, slaveState); + changeCurrentState(slaveState); + } + if (processSuccess) { + continue; + } + } + + if (!byteBufferRead.hasRemaining()) { + byteBufferRead.position(ReadSocketService.this.processPosition); + byteBufferRead.compact(); + ReadSocketService.this.processPosition = 0; + } + break; + } + + return true; + } + } + } + + class WriteSocketService extends AbstractWriteSocketService { + private SelectMappedBufferResult selectMappedBufferResult; + + public WriteSocketService(final SocketChannel socketChannel) throws IOException { + super(socketChannel); + } + + @Override + protected int getNextTransferDataSize() { + SelectMappedBufferResult selectResult = haService.getDefaultMessageStore().getCommitLogData(this.nextTransferFromWhere); + if (selectResult == null || selectResult.getSize() <= 0) { + return 0; + } + this.selectMappedBufferResult = selectResult; + return selectResult.getSize(); + } + + @Override + protected void releaseData() { + this.selectMappedBufferResult.release(); + this.selectMappedBufferResult = null; + } + + @Override + protected boolean transferData(int maxTransferSize) throws Exception { + + if (null != this.selectMappedBufferResult && maxTransferSize >= 0) { + this.selectMappedBufferResult.getByteBuffer().limit(maxTransferSize); + } + + // Write Header + boolean result = haWriter.write(this.socketChannel, this.byteBufferHeader); + + if (!result) { + return false; + } + + if (null == this.selectMappedBufferResult) { + return true; + } + + // Write Body + result = haWriter.write(this.socketChannel, this.selectMappedBufferResult.getByteBuffer()); + + if (result) { + releaseData(); + } + return result; + } + + @Override + protected void onStop() { + if (this.selectMappedBufferResult != null) { + this.selectMappedBufferResult.release(); + } + } + + @Override + public String getServiceName() { + if (haService.getDefaultMessageStore().getBrokerConfig().isInBrokerContainer()) { + return haService.getDefaultMessageStore().getBrokerIdentity().getIdentifier() + WriteSocketService.class.getSimpleName(); + } + return WriteSocketService.class.getSimpleName(); + } + } + + abstract class AbstractWriteSocketService extends ServiceThread { + protected final Selector selector; + protected final SocketChannel socketChannel; + protected final HAWriter haWriter; + + protected final ByteBuffer byteBufferHeader = ByteBuffer.allocate(TRANSFER_HEADER_SIZE); + // Store master epochFileCache: (Epoch + startOffset) * 1000 + private final ByteBuffer handShakeBuffer = ByteBuffer.allocate(EPOCH_ENTRY_SIZE * 1000); + protected long nextTransferFromWhere = -1; + protected boolean lastWriteOver = true; + protected long lastWriteTimestamp = System.currentTimeMillis(); + protected long lastPrintTimestamp = System.currentTimeMillis(); + protected long transferOffset = 0; + + public AbstractWriteSocketService(final SocketChannel socketChannel) throws IOException { + this.selector = NetworkUtil.openSelector(); + this.socketChannel = socketChannel; + this.socketChannel.register(this.selector, SelectionKey.OP_WRITE); + this.setDaemon(true); + haWriter = new HAWriter(); + haWriter.registerHook(writeSize -> { + flowMonitor.addByteCountTransferred(writeSize); + if (writeSize > 0) { + AbstractWriteSocketService.this.lastWriteTimestamp = + haService.getDefaultMessageStore().getSystemClock().now(); + } + }); + } + + public long getNextTransferFromWhere() { + return this.nextTransferFromWhere; + } + + private boolean buildHandshakeBuffer() { + final List epochEntries = AutoSwitchHAConnection.this.epochCache.getAllEntries(); + final int lastEpoch = AutoSwitchHAConnection.this.epochCache.lastEpoch(); + final long maxPhyOffset = AutoSwitchHAConnection.this.haService.getDefaultMessageStore().getMaxPhyOffset(); + this.byteBufferHeader.position(0); + this.byteBufferHeader.limit(HANDSHAKE_HEADER_SIZE); + // State + this.byteBufferHeader.putInt(currentState.ordinal()); + // Body size + this.byteBufferHeader.putInt(epochEntries.size() * EPOCH_ENTRY_SIZE); + // Offset + this.byteBufferHeader.putLong(maxPhyOffset); + // Epoch + this.byteBufferHeader.putInt(lastEpoch); + this.byteBufferHeader.flip(); + + // EpochEntries + this.handShakeBuffer.position(0); + this.handShakeBuffer.limit(EPOCH_ENTRY_SIZE * epochEntries.size()); + for (final EpochEntry entry : epochEntries) { + if (entry != null) { + this.handShakeBuffer.putInt(entry.getEpoch()); + this.handShakeBuffer.putLong(entry.getStartOffset()); + } + } + this.handShakeBuffer.flip(); + LOGGER.info("Master build handshake header: maxEpoch:{}, maxOffset:{}, epochEntries:{}", lastEpoch, maxPhyOffset, epochEntries); + return true; + } + + private boolean handshakeWithSlave() throws IOException { + // Write Header + boolean result = this.haWriter.write(this.socketChannel, this.byteBufferHeader); + + if (!result) { + return false; + } + + // Write Body + return this.haWriter.write(this.socketChannel, this.handShakeBuffer); + } + + // Normal transfer method + private void buildTransferHeaderBuffer(long nextOffset, int bodySize) { + + EpochEntry entry = AutoSwitchHAConnection.this.epochCache.getEntry(AutoSwitchHAConnection.this.currentTransferEpoch); + + if (entry == null) { + + // If broker is started on empty disk and no message entered (nextOffset = -1 and currentTransferEpoch = -1), do not output error log when sending heartbeat + if (nextOffset != -1 || currentTransferEpoch != -1 || bodySize > 0) { + LOGGER.error("Failed to find epochEntry with epoch {} when build msg header", AutoSwitchHAConnection.this.currentTransferEpoch); + } + + if (bodySize > 0) { + return; + } + // Maybe it's used for heartbeat + entry = AutoSwitchHAConnection.this.epochCache.firstEntry(); + } + // Build Header + this.byteBufferHeader.position(0); + this.byteBufferHeader.limit(TRANSFER_HEADER_SIZE); + // State + this.byteBufferHeader.putInt(currentState.ordinal()); + // Body size + this.byteBufferHeader.putInt(bodySize); + // Offset + this.byteBufferHeader.putLong(nextOffset); + // Epoch + this.byteBufferHeader.putInt(entry.getEpoch()); + // EpochStartOffset + this.byteBufferHeader.putLong(entry.getStartOffset()); + // Additional info(confirm offset) + final long confirmOffset = AutoSwitchHAConnection.this.haService.getDefaultMessageStore().getConfirmOffset(); + this.byteBufferHeader.putLong(confirmOffset); + this.byteBufferHeader.flip(); + } + + private boolean sendHeartbeatIfNeeded() throws Exception { + long interval = haService.getDefaultMessageStore().getSystemClock().now() - this.lastWriteTimestamp; + if (interval > haService.getDefaultMessageStore().getMessageStoreConfig().getHaSendHeartbeatInterval()) { + buildTransferHeaderBuffer(this.nextTransferFromWhere, 0); + return this.transferData(0); + } + return true; + } + + private void transferToSlave() throws Exception { + if (this.lastWriteOver) { + this.lastWriteOver = sendHeartbeatIfNeeded(); + } else { + // maxTransferSize == -1 means to continue transfer remaining data. + this.lastWriteOver = this.transferData(-1); + } + if (!this.lastWriteOver) { + return; + } + + int size = this.getNextTransferDataSize(); + if (size > 0) { + if (size > haService.getDefaultMessageStore().getMessageStoreConfig().getHaTransferBatchSize()) { + size = haService.getDefaultMessageStore().getMessageStoreConfig().getHaTransferBatchSize(); + } + int canTransferMaxBytes = flowMonitor.canTransferMaxByteNum(); + if (size > canTransferMaxBytes) { + if (System.currentTimeMillis() - lastPrintTimestamp > 1000) { + LOGGER.warn("Trigger HA flow control, max transfer speed {}KB/s, current speed: {}KB/s", + String.format("%.2f", flowMonitor.maxTransferByteInSecond() / 1024.0), + String.format("%.2f", flowMonitor.getTransferredByteInSecond() / 1024.0)); + lastPrintTimestamp = System.currentTimeMillis(); + } + size = canTransferMaxBytes; + } + if (size <= 0) { + this.releaseData(); + this.waitForRunning(100); + return; + } + + // We must ensure that the transmitted logs are within the same epoch + // If currentEpochEndOffset == -1, means that currentTransferEpoch = last epoch, so the endOffset = Long.max + final long currentEpochEndOffset = AutoSwitchHAConnection.this.currentTransferEpochEndOffset; + if (currentEpochEndOffset != -1 && this.nextTransferFromWhere + size > currentEpochEndOffset) { + final EpochEntry epochEntry = AutoSwitchHAConnection.this.epochCache.nextEntry(AutoSwitchHAConnection.this.currentTransferEpoch); + if (epochEntry == null) { + LOGGER.error("Can't find a bigger epochEntry than epoch {}", AutoSwitchHAConnection.this.currentTransferEpoch); + waitForRunning(100); + return; + } + size = (int) (currentEpochEndOffset - this.nextTransferFromWhere); + changeTransferEpochToNext(epochEntry); + } + + this.transferOffset = this.nextTransferFromWhere; + this.nextTransferFromWhere += size; + updateLastTransferInfo(); + + // Build Header + buildTransferHeaderBuffer(this.transferOffset, size); + + this.lastWriteOver = this.transferData(size); + } else { + // If size == 0, we should update the lastCatchupTimeMs + AutoSwitchHAConnection.this.haService.updateConnectionLastCaughtUpTime(AutoSwitchHAConnection.this.slaveId, System.currentTimeMillis()); + haService.getWaitNotifyObject().allWaitForRunning(100); + } + } + + @Override + public void run() { + AutoSwitchHAConnection.LOGGER.info(this.getServiceName() + " service started"); + + while (!this.isStopped()) { + try { + this.selector.select(1000); + + switch (currentState) { + case HANDSHAKE: + // Wait until the slave send it handshake msg to master. + if (!isSlaveSendHandshake) { + this.waitForRunning(10); + continue; + } + + if (this.lastWriteOver) { + if (!buildHandshakeBuffer()) { + LOGGER.error("AutoSwitchHAConnection build handshake buffer failed"); + this.waitForRunning(5000); + continue; + } + } + + this.lastWriteOver = handshakeWithSlave(); + if (this.lastWriteOver) { + // change flag to {false} to wait for slave notification + isSlaveSendHandshake = false; + } + break; + case TRANSFER: + if (-1 == slaveRequestOffset) { + this.waitForRunning(10); + continue; + } + + if (-1 == this.nextTransferFromWhere) { + if (0 == slaveRequestOffset) { + // We must ensure that the starting point of syncing log + // must be the startOffset of a file (maybe the last file, or the minOffset) + final MessageStoreConfig config = haService.getDefaultMessageStore().getMessageStoreConfig(); + if (AutoSwitchHAConnection.this.isSyncFromLastFile) { + long masterOffset = haService.getDefaultMessageStore().getCommitLog().getMaxOffset(); + masterOffset = masterOffset - (masterOffset % config.getMappedFileSizeCommitLog()); + if (masterOffset < 0) { + masterOffset = 0; + } + this.nextTransferFromWhere = masterOffset; + } else { + this.nextTransferFromWhere = haService.getDefaultMessageStore().getCommitLog().getMinOffset(); + } + } else { + this.nextTransferFromWhere = slaveRequestOffset; + } + + // nextTransferFromWhere is not found. It may be empty disk and no message is entered + if (this.nextTransferFromWhere == -1) { + sendHeartbeatIfNeeded(); + waitForRunning(500); + break; + } + // Setup initial transferEpoch + EpochEntry epochEntry = AutoSwitchHAConnection.this.epochCache.findEpochEntryByOffset(this.nextTransferFromWhere); + if (epochEntry == null) { + LOGGER.error("Failed to find an epochEntry to match nextTransferFromWhere {}", this.nextTransferFromWhere); + sendHeartbeatIfNeeded(); + waitForRunning(500); + break; + } + changeTransferEpochToNext(epochEntry); + LOGGER.info("Master transfer data to slave {}, from offset:{}, currentEpoch:{}", + AutoSwitchHAConnection.this.clientAddress, this.nextTransferFromWhere, epochEntry); + } + transferToSlave(); + break; + default: + throw new Exception("unexpected state " + currentState); + } + } catch (Exception e) { + AutoSwitchHAConnection.LOGGER.error(this.getServiceName() + " service has exception.", e); + break; + } + } + + this.onStop(); + + changeCurrentState(HAConnectionState.SHUTDOWN); + + this.makeStop(); + + readSocketService.makeStop(); + + haService.removeConnection(AutoSwitchHAConnection.this); + + SelectionKey sk = this.socketChannel.keyFor(this.selector); + if (sk != null) { + sk.cancel(); + } + + try { + this.selector.close(); + this.socketChannel.close(); + } catch (IOException e) { + AutoSwitchHAConnection.LOGGER.error("", e); + } + + flowMonitor.shutdown(true); + + AutoSwitchHAConnection.LOGGER.info(this.getServiceName() + " service end"); + } + + abstract protected int getNextTransferDataSize(); + + abstract protected void releaseData(); + + abstract protected boolean transferData(int maxTransferSize) throws Exception; + + abstract protected void onStop(); + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/ha/autoswitch/AutoSwitchHAService.java b/store/src/main/java/org/apache/rocketmq/store/ha/autoswitch/AutoSwitchHAService.java new file mode 100644 index 00000000000..64dad9aef21 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ha/autoswitch/AutoSwitchHAService.java @@ -0,0 +1,577 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.ha.autoswitch; + +import java.io.IOException; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.ConcurrentHashMapUtils; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.EpochEntry; +import org.apache.rocketmq.remoting.protocol.body.HARuntimeInfo; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.DispatchRequest; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.config.BrokerRole; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.ha.DefaultHAService; +import org.apache.rocketmq.store.ha.GroupTransferService; +import org.apache.rocketmq.store.ha.HAClient; +import org.apache.rocketmq.store.ha.HAConnection; +import org.apache.rocketmq.store.ha.HAConnectionStateNotificationService; +import org.rocksdb.RocksDBException; + +/** + * SwitchAble ha service, support switch role to master or slave. + */ +public class AutoSwitchHAService extends DefaultHAService { + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + private final ExecutorService executorService = ThreadUtils.newSingleThreadExecutor(new ThreadFactoryImpl("AutoSwitchHAService_Executor_")); + private final ConcurrentHashMap connectionCaughtUpTimeTable = new ConcurrentHashMap<>(); + private final List>> syncStateSetChangedListeners = new ArrayList<>(); + private final Set syncStateSet = new HashSet<>(); + private final Set remoteSyncStateSet = new HashSet<>(); + private final ReadWriteLock syncStateSetReadWriteLock = new ReentrantReadWriteLock(); + private final Lock readLock = syncStateSetReadWriteLock.readLock(); + private final Lock writeLock = syncStateSetReadWriteLock.writeLock(); + + // Indicate whether the syncStateSet is currently in the process of being synchronized to controller. + private volatile boolean isSynchronizingSyncStateSet = false; + + private EpochFileCache epochCache; + private AutoSwitchHAClient haClient; + + private Long localBrokerId = null; + + public AutoSwitchHAService() { + } + + @Override + public void init(final DefaultMessageStore defaultMessageStore) throws IOException { + this.epochCache = new EpochFileCache(defaultMessageStore.getMessageStoreConfig().getStorePathEpochFile()); + this.epochCache.initCacheFromFile(); + this.defaultMessageStore = defaultMessageStore; + this.acceptSocketService = new AutoSwitchAcceptSocketService(defaultMessageStore.getMessageStoreConfig()); + this.groupTransferService = new GroupTransferService(this, defaultMessageStore); + this.haConnectionStateNotificationService = new HAConnectionStateNotificationService(this, defaultMessageStore); + } + + @Override + public void shutdown() { + super.shutdown(); + if (this.haClient != null) { + this.haClient.shutdown(); + } + this.executorService.shutdown(); + } + + @Override + public void removeConnection(HAConnection conn) { + if (!defaultMessageStore.isShutdown()) { + final Set syncStateSet = getLocalSyncStateSet(); + Long slave = ((AutoSwitchHAConnection) conn).getSlaveId(); + if (syncStateSet.contains(slave)) { + syncStateSet.remove(slave); + markSynchronizingSyncStateSet(syncStateSet); + notifySyncStateSetChanged(syncStateSet); + } + } + super.removeConnection(conn); + } + + @Override + public boolean changeToMaster(int masterEpoch) throws RocksDBException { + final int lastEpoch = this.epochCache.lastEpoch(); + if (masterEpoch < lastEpoch) { + LOGGER.warn("newMasterEpoch {} < lastEpoch {}, fail to change to master", masterEpoch, lastEpoch); + return false; + } + destroyConnections(); + // Stop ha client if needed + if (this.haClient != null) { + this.haClient.shutdown(); + } + + // Truncate dirty file + final long truncateOffset = truncateInvalidMsg(); + + this.defaultMessageStore.setConfirmOffset(computeConfirmOffset()); + + if (truncateOffset >= 0) { + this.epochCache.truncateSuffixByOffset(truncateOffset); + } + + // Append new epoch to epochFile + final EpochEntry newEpochEntry = new EpochEntry(masterEpoch, this.defaultMessageStore.getMaxPhyOffset()); + if (this.epochCache.lastEpoch() >= masterEpoch) { + this.epochCache.truncateSuffixByEpoch(masterEpoch); + } + this.epochCache.appendEntry(newEpochEntry); + + // Waiting consume queue dispatch + while (defaultMessageStore.dispatchBehindBytes() > 0) { + try { + Thread.sleep(100); + } catch (Exception ignored) { + + } + } + + if (defaultMessageStore.isTransientStorePoolEnable()) { + waitingForAllCommit(); + defaultMessageStore.getTransientStorePool().setRealCommit(true); + } + + LOGGER.info("TruncateOffset is {}, confirmOffset is {}, maxPhyOffset is {}", truncateOffset, this.defaultMessageStore.getConfirmOffset(), this.defaultMessageStore.getMaxPhyOffset()); + this.defaultMessageStore.recoverTopicQueueTable(); + this.defaultMessageStore.setStateMachineVersion(masterEpoch); + LOGGER.info("Change ha to master success, newMasterEpoch:{}, startOffset:{}", masterEpoch, newEpochEntry.getStartOffset()); + return true; + } + + @Override + public boolean changeToSlave(String newMasterAddr, int newMasterEpoch, Long slaveId) { + final int lastEpoch = this.epochCache.lastEpoch(); + if (newMasterEpoch < lastEpoch) { + LOGGER.warn("newMasterEpoch {} < lastEpoch {}, fail to change to slave", newMasterEpoch, lastEpoch); + return false; + } + try { + destroyConnections(); + if (this.haClient == null) { + this.haClient = new AutoSwitchHAClient(this, defaultMessageStore, this.epochCache, slaveId); + } else { + this.haClient.reOpen(); + } + this.haClient.updateMasterAddress(newMasterAddr); + this.haClient.updateHaMasterAddress(null); + this.haClient.start(); + + if (defaultMessageStore.isTransientStorePoolEnable()) { + waitingForAllCommit(); + defaultMessageStore.getTransientStorePool().setRealCommit(false); + } + + this.defaultMessageStore.setStateMachineVersion(newMasterEpoch); + + LOGGER.info("Change ha to slave success, newMasterAddress:{}, newMasterEpoch:{}", newMasterAddr, newMasterEpoch); + return true; + } catch (final Exception e) { + LOGGER.error("Error happen when change ha to slave", e); + return false; + } + } + + @Override + public boolean changeToMasterWhenLastRoleIsMaster(int masterEpoch) { + final int lastEpoch = this.epochCache.lastEpoch(); + if (masterEpoch < lastEpoch) { + LOGGER.warn("newMasterEpoch {} < lastEpoch {}, fail to change to master", masterEpoch, lastEpoch); + return false; + } + // Append new epoch to epochFile + final EpochEntry newEpochEntry = new EpochEntry(masterEpoch, this.defaultMessageStore.getMaxPhyOffset()); + if (this.epochCache.lastEpoch() >= masterEpoch) { + this.epochCache.truncateSuffixByEpoch(masterEpoch); + } + this.epochCache.appendEntry(newEpochEntry); + + this.defaultMessageStore.setStateMachineVersion(masterEpoch); + LOGGER.info("Change ha to master success, last role is master, newMasterEpoch:{}, startOffset:{}", + masterEpoch, newEpochEntry.getStartOffset()); + return true; + } + + @Override + public boolean changeToSlaveWhenMasterNotChange(String newMasterAddr, int newMasterEpoch) { + final int lastEpoch = this.epochCache.lastEpoch(); + if (newMasterEpoch < lastEpoch) { + LOGGER.warn("newMasterEpoch {} < lastEpoch {}, fail to change to slave", newMasterEpoch, lastEpoch); + return false; + } + + this.defaultMessageStore.setStateMachineVersion(newMasterEpoch); + LOGGER.info("Change ha to slave success, master doesn't change, newMasterAddress:{}, newMasterEpoch:{}", + newMasterAddr, newMasterEpoch); + return true; + } + + public void waitingForAllCommit() { + while (getDefaultMessageStore().remainHowManyDataToCommit() > 0) { + getDefaultMessageStore().getCommitLog().getFlushManager().wakeUpCommit(); + try { + Thread.sleep(100); + } catch (Exception e) { + + } + } + } + + @Override + public HAClient getHAClient() { + return this.haClient; + } + + @Override + public void updateHaMasterAddress(String newAddr) { + if (this.haClient != null) { + this.haClient.updateHaMasterAddress(newAddr); + } + } + + @Override + public void updateMasterAddress(String newAddr) { + } + + public void registerSyncStateSetChangedListener(final Consumer> listener) { + this.syncStateSetChangedListeners.add(listener); + } + + public void notifySyncStateSetChanged(final Set newSyncStateSet) { + this.executorService.submit(() -> { + syncStateSetChangedListeners.forEach(listener -> listener.accept(newSyncStateSet)); + }); + LOGGER.info("Notify the syncStateSet has been changed into {}.", newSyncStateSet); + } + + /** + * Check and maybe shrink the SyncStateSet. + * A slave will be removed from SyncStateSet if (curTime - HaConnection.lastCaughtUpTime) > option(haMaxTimeSlaveNotCatchup) + */ + public Set maybeShrinkSyncStateSet() { + final Set newSyncStateSet = getLocalSyncStateSet(); + boolean isSyncStateSetChanged = false; + final long haMaxTimeSlaveNotCatchup = this.defaultMessageStore.getMessageStoreConfig().getHaMaxTimeSlaveNotCatchup(); + for (Map.Entry next : this.connectionCaughtUpTimeTable.entrySet()) { + final Long slaveBrokerId = next.getKey(); + if (newSyncStateSet.contains(slaveBrokerId)) { + final Long lastCaughtUpTimeMs = next.getValue(); + if ((System.currentTimeMillis() - lastCaughtUpTimeMs) > haMaxTimeSlaveNotCatchup) { + newSyncStateSet.remove(slaveBrokerId); + isSyncStateSetChanged = true; + } + } + } + + // If the slaveBrokerId is in syncStateSet but not in connectionCaughtUpTimeTable, + // it means that the broker has not connected. + Iterator iterator = newSyncStateSet.iterator(); + while (iterator.hasNext()) { + Long slaveBrokerId = iterator.next(); + if (!Objects.equals(slaveBrokerId, this.localBrokerId) && !this.connectionCaughtUpTimeTable.containsKey(slaveBrokerId)) { + iterator.remove(); + isSyncStateSetChanged = true; + } + } + + if (isSyncStateSetChanged) { + markSynchronizingSyncStateSet(newSyncStateSet); + } + return newSyncStateSet; + } + + /** + * Check and maybe add the slave to SyncStateSet. A slave will be added to SyncStateSet if its slaveMaxOffset >= + * current confirmOffset, and it is caught up to an offset within the current leader epoch. + */ + public void maybeExpandInSyncStateSet(final Long slaveBrokerId, final long slaveMaxOffset) { + final Set currentSyncStateSet = getLocalSyncStateSet(); + if (currentSyncStateSet.contains(slaveBrokerId)) { + return; + } + final long confirmOffset = this.defaultMessageStore.getConfirmOffset(); + if (slaveMaxOffset >= confirmOffset) { + final EpochEntry currentLeaderEpoch = this.epochCache.lastEntry(); + if (slaveMaxOffset >= currentLeaderEpoch.getStartOffset()) { + LOGGER.info("The slave {} has caught up, slaveMaxOffset: {}, confirmOffset: {}, epoch: {}, leader epoch startOffset: {}.", + slaveBrokerId, slaveMaxOffset, confirmOffset, currentLeaderEpoch.getEpoch(), currentLeaderEpoch.getStartOffset()); + currentSyncStateSet.add(slaveBrokerId); + markSynchronizingSyncStateSet(currentSyncStateSet); + // Notify the upper layer that syncStateSet changed. + notifySyncStateSetChanged(currentSyncStateSet); + } + } + } + + private void markSynchronizingSyncStateSet(final Set newSyncStateSet) { + this.writeLock.lock(); + try { + this.isSynchronizingSyncStateSet = true; + this.remoteSyncStateSet.clear(); + this.remoteSyncStateSet.addAll(newSyncStateSet); + } finally { + this.writeLock.unlock(); + } + } + + private void markSynchronizingSyncStateSetDone() { + // No need to lock, because the upper-level calling method has already locked write lock + this.isSynchronizingSyncStateSet = false; + } + + public boolean isSynchronizingSyncStateSet() { + return isSynchronizingSyncStateSet; + } + + public void updateConnectionLastCaughtUpTime(final Long slaveBrokerId, final long lastCaughtUpTimeMs) { + Long prevTime = ConcurrentHashMapUtils.computeIfAbsent(this.connectionCaughtUpTimeTable, slaveBrokerId, k -> 0L); + this.connectionCaughtUpTimeTable.put(slaveBrokerId, Math.max(prevTime, lastCaughtUpTimeMs)); + } + + public void updateConfirmOffsetWhenSlaveAck(final Long slaveBrokerId) { + this.readLock.lock(); + try { + if (this.syncStateSet.contains(slaveBrokerId)) { + this.defaultMessageStore.setConfirmOffset(computeConfirmOffset()); + } + } finally { + this.readLock.unlock(); + } + } + + @Override + public int inSyncReplicasNums(final long masterPutWhere) { + this.readLock.lock(); + try { + if (this.isSynchronizingSyncStateSet) { + return Math.max(this.syncStateSet.size(), this.remoteSyncStateSet.size()); + } else { + return this.syncStateSet.size(); + } + } finally { + this.readLock.unlock(); + } + } + + @Override + public HARuntimeInfo getRuntimeInfo(long masterPutWhere) { + HARuntimeInfo info = new HARuntimeInfo(); + + if (BrokerRole.SLAVE.equals(this.getDefaultMessageStore().getMessageStoreConfig().getBrokerRole())) { + info.setMaster(false); + + info.getHaClientRuntimeInfo().setMasterAddr(this.haClient.getHaMasterAddress()); + info.getHaClientRuntimeInfo().setMaxOffset(this.getDefaultMessageStore().getMaxPhyOffset()); + info.getHaClientRuntimeInfo().setLastReadTimestamp(this.haClient.getLastReadTimestamp()); + info.getHaClientRuntimeInfo().setLastWriteTimestamp(this.haClient.getLastWriteTimestamp()); + info.getHaClientRuntimeInfo().setTransferredByteInSecond(this.haClient.getTransferredByteInSecond()); + info.getHaClientRuntimeInfo().setMasterFlushOffset(this.defaultMessageStore.getMasterFlushedOffset()); + } else { + info.setMaster(true); + + info.setMasterCommitLogMaxOffset(masterPutWhere); + + Set localSyncStateSet = getLocalSyncStateSet(); + for (HAConnection conn : this.connectionList) { + HARuntimeInfo.HAConnectionRuntimeInfo cInfo = new HARuntimeInfo.HAConnectionRuntimeInfo(); + + long slaveAckOffset = conn.getSlaveAckOffset(); + cInfo.setSlaveAckOffset(slaveAckOffset); + cInfo.setDiff(masterPutWhere - slaveAckOffset); + cInfo.setAddr(conn.getClientAddress().substring(1)); + cInfo.setTransferredByteInSecond(conn.getTransferredByteInSecond()); + cInfo.setTransferFromWhere(conn.getTransferFromWhere()); + + cInfo.setInSync(localSyncStateSet.contains(((AutoSwitchHAConnection) conn).getSlaveId())); + + info.getHaConnectionInfo().add(cInfo); + } + info.setInSyncSlaveNums(localSyncStateSet.size() - 1); + } + return info; + } + + public long computeConfirmOffset() { + final Set currentSyncStateSet = getSyncStateSet(); + long newConfirmOffset = this.defaultMessageStore.getMaxPhyOffset(); + List idList = this.connectionList.stream().map(connection -> ((AutoSwitchHAConnection)connection).getSlaveId()).collect(Collectors.toList()); + + // To avoid the syncStateSet is not consistent with connectionList. + // Fix issue: https://github.com/apache/rocketmq/issues/6662 + for (Long syncId : currentSyncStateSet) { + if (!idList.contains(syncId) && this.localBrokerId != null && !Objects.equals(syncId, this.localBrokerId)) { + LOGGER.warn("Slave {} is still in syncStateSet, but has lost its connection. So new offset can't be compute.", syncId); + // Without check and re-compute, return the confirmOffset's value directly. + return this.defaultMessageStore.getConfirmOffsetDirectly(); + } + } + + for (HAConnection connection : this.connectionList) { + final Long slaveId = ((AutoSwitchHAConnection) connection).getSlaveId(); + if (currentSyncStateSet.contains(slaveId) && connection.getSlaveAckOffset() > 0) { + newConfirmOffset = Math.min(newConfirmOffset, connection.getSlaveAckOffset()); + } + } + return newConfirmOffset; + } + + public void setSyncStateSet(final Set syncStateSet) { + this.writeLock.lock(); + try { + markSynchronizingSyncStateSetDone(); + this.syncStateSet.clear(); + this.syncStateSet.addAll(syncStateSet); + this.defaultMessageStore.setConfirmOffset(computeConfirmOffset()); + } finally { + this.writeLock.unlock(); + } + } + + /** + * Return the union of the local and remote syncStateSets + */ + public Set getSyncStateSet() { + this.readLock.lock(); + try { + if (this.isSynchronizingSyncStateSet) { + Set unionSyncStateSet = new HashSet<>(this.syncStateSet.size() + this.remoteSyncStateSet.size()); + unionSyncStateSet.addAll(this.syncStateSet); + unionSyncStateSet.addAll(this.remoteSyncStateSet); + return unionSyncStateSet; + } else { + HashSet syncStateSet = new HashSet<>(this.syncStateSet.size()); + syncStateSet.addAll(this.syncStateSet); + return syncStateSet; + } + } finally { + this.readLock.unlock(); + } + } + + public Set getLocalSyncStateSet() { + this.readLock.lock(); + try { + HashSet localSyncStateSet = new HashSet<>(this.syncStateSet.size()); + localSyncStateSet.addAll(this.syncStateSet); + return localSyncStateSet; + } finally { + this.readLock.unlock(); + } + } + + public void truncateEpochFilePrefix(final long offset) { + this.epochCache.truncatePrefixByOffset(offset); + } + + public void truncateEpochFileSuffix(final long offset) { + this.epochCache.truncateSuffixByOffset(offset); + } + + /** + * Try to truncate incomplete msg transferred from master. + */ + public long truncateInvalidMsg() throws RocksDBException { + long dispatchBehind = this.defaultMessageStore.dispatchBehindBytes(); + if (dispatchBehind <= 0) { + LOGGER.info("Dispatch complete, skip truncate"); + return -1; + } + + boolean doNext = true; + + // Here we could use reputFromOffset in DefaultMessageStore directly. + long reputFromOffset = this.defaultMessageStore.getReputFromOffset(); + do { + SelectMappedBufferResult result = this.defaultMessageStore.getCommitLog().getData(reputFromOffset); + if (result == null) { + break; + } + + try { + reputFromOffset = result.getStartOffset(); + + int readSize = 0; + while (readSize < result.getSize()) { + DispatchRequest dispatchRequest = this.defaultMessageStore.getCommitLog().checkMessageAndReturnSize(result.getByteBuffer(), false, false); + if (dispatchRequest.isSuccess()) { + int size = dispatchRequest.getMsgSize(); + if (size > 0) { + reputFromOffset += size; + readSize += size; + } else { + reputFromOffset = this.defaultMessageStore.getCommitLog().rollNextFile(reputFromOffset); + break; + } + } else { + doNext = false; + break; + } + } + } finally { + result.release(); + } + } while (reputFromOffset < this.defaultMessageStore.getMaxPhyOffset() && doNext); + + LOGGER.info("Truncate commitLog to {}", reputFromOffset); + this.defaultMessageStore.truncateDirtyFiles(reputFromOffset); + return reputFromOffset; + } + + public int getLastEpoch() { + return this.epochCache.lastEpoch(); + } + + public List getEpochEntries() { + return this.epochCache.getAllEntries(); + } + + public Long getLocalBrokerId() { + return localBrokerId; + } + + public void setLocalBrokerId(Long localBrokerId) { + this.localBrokerId = localBrokerId; + } + + class AutoSwitchAcceptSocketService extends AcceptSocketService { + + public AutoSwitchAcceptSocketService(final MessageStoreConfig messageStoreConfig) { + super(messageStoreConfig); + } + + @Override + public String getServiceName() { + if (defaultMessageStore.getBrokerConfig().isInBrokerContainer()) { + return defaultMessageStore.getBrokerConfig().getIdentifier() + AcceptSocketService.class.getSimpleName(); + } + return AutoSwitchAcceptSocketService.class.getSimpleName(); + } + + @Override + protected HAConnection createConnection(SocketChannel sc) throws IOException { + return new AutoSwitchHAConnection(AutoSwitchHAService.this, sc, AutoSwitchHAService.this.epochCache); + } + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/ha/autoswitch/BrokerMetadata.java b/store/src/main/java/org/apache/rocketmq/store/ha/autoswitch/BrokerMetadata.java new file mode 100644 index 00000000000..eb6ab639f23 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ha/autoswitch/BrokerMetadata.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.ha.autoswitch; + +import org.apache.commons.lang3.StringUtils; + +import java.util.Objects; + +public class BrokerMetadata extends MetadataFile { + + protected String clusterName; + + protected String brokerName; + + protected Long brokerId; + + public BrokerMetadata(String filePath) { + this.filePath = filePath; + } + + public void updateAndPersist(String clusterName, String brokerName, Long brokerId) throws Exception { + this.clusterName = clusterName; + this.brokerName = brokerName; + this.brokerId = brokerId; + writeToFile(); + } + + @Override + public String encodeToStr() { + StringBuilder sb = new StringBuilder(); + sb.append(clusterName).append("#"); + sb.append(brokerName).append("#"); + sb.append(brokerId); + return sb.toString(); + } + + @Override + public void decodeFromStr(String dataStr) { + if (dataStr == null) return; + String[] dataArr = dataStr.split("#"); + this.clusterName = dataArr[0]; + this.brokerName = dataArr[1]; + this.brokerId = Long.valueOf(dataArr[2]); + } + + @Override + public boolean isLoaded() { + return StringUtils.isNotEmpty(this.clusterName) && StringUtils.isNotEmpty(this.brokerName) && brokerId != null; + } + + @Override + public void clearInMem() { + this.clusterName = null; + this.brokerName = null; + this.brokerId = null; + } + + public String getBrokerName() { + return brokerName; + } + + public Long getBrokerId() { + return brokerId; + } + + public String getClusterName() { + return clusterName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BrokerMetadata that = (BrokerMetadata) o; + return Objects.equals(clusterName, that.clusterName) && Objects.equals(brokerName, that.brokerName) && Objects.equals(brokerId, that.brokerId); + } + + @Override + public int hashCode() { + return Objects.hash(clusterName, brokerName, brokerId); + } + + @Override + public String toString() { + return "BrokerMetadata{" + + "clusterName='" + clusterName + '\'' + + ", brokerName='" + brokerName + '\'' + + ", brokerId=" + brokerId + + ", filePath='" + filePath + '\'' + + '}'; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/ha/autoswitch/EpochFileCache.java b/store/src/main/java/org/apache/rocketmq/store/ha/autoswitch/EpochFileCache.java new file mode 100644 index 00000000000..f23e4aa06bf --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ha/autoswitch/EpochFileCache.java @@ -0,0 +1,327 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.ha.autoswitch; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Predicate; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.CheckpointFile; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.EpochEntry; + +/** + * Cache for epochFile. Mapping (Epoch -> StartOffset) + */ +public class EpochFileCache { + private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + private final Lock readLock = this.readWriteLock.readLock(); + private final Lock writeLock = this.readWriteLock.writeLock(); + private final TreeMap epochMap; + private CheckpointFile checkpoint; + + public EpochFileCache() { + this.epochMap = new TreeMap<>(); + } + + public EpochFileCache(final String path) { + this.epochMap = new TreeMap<>(); + this.checkpoint = new CheckpointFile<>(path, new EpochEntrySerializer()); + } + + public boolean initCacheFromFile() { + this.writeLock.lock(); + try { + final List entries = this.checkpoint.read(); + initEntries(entries); + return true; + } catch (final IOException e) { + log.error("Error happen when init epoch entries from epochFile", e); + return false; + } finally { + this.writeLock.unlock(); + } + } + + public void initCacheFromEntries(final List entries) { + this.writeLock.lock(); + try { + initEntries(entries); + flush(); + } finally { + this.writeLock.unlock(); + } + } + + private void initEntries(final List entries) { + this.epochMap.clear(); + EpochEntry preEntry = null; + for (final EpochEntry entry : entries) { + this.epochMap.put(entry.getEpoch(), entry); + if (preEntry != null) { + preEntry.setEndOffset(entry.getStartOffset()); + } + preEntry = entry; + } + } + + public int getEntrySize() { + this.readLock.lock(); + try { + return this.epochMap.size(); + } finally { + this.readLock.unlock(); + } + } + + public boolean appendEntry(final EpochEntry entry) { + this.writeLock.lock(); + try { + if (!this.epochMap.isEmpty()) { + final EpochEntry lastEntry = this.epochMap.lastEntry().getValue(); + if (lastEntry.getEpoch() >= entry.getEpoch() || lastEntry.getStartOffset() >= entry.getStartOffset()) { + log.error("The appending entry's lastEpoch or endOffset {} is not bigger than lastEntry {}, append failed", entry, lastEntry); + return false; + } + lastEntry.setEndOffset(entry.getStartOffset()); + } + this.epochMap.put(entry.getEpoch(), new EpochEntry(entry)); + flush(); + return true; + } finally { + this.writeLock.unlock(); + } + } + + /** + * Set endOffset for lastEpochEntry. + */ + public void setLastEpochEntryEndOffset(final long endOffset) { + this.writeLock.lock(); + try { + if (!this.epochMap.isEmpty()) { + final EpochEntry lastEntry = this.epochMap.lastEntry().getValue(); + if (lastEntry.getStartOffset() <= endOffset) { + lastEntry.setEndOffset(endOffset); + } + } + } finally { + this.writeLock.unlock(); + } + } + + public EpochEntry firstEntry() { + this.readLock.lock(); + try { + if (this.epochMap.isEmpty()) { + return null; + } + return new EpochEntry(this.epochMap.firstEntry().getValue()); + } finally { + this.readLock.unlock(); + } + } + + public EpochEntry lastEntry() { + this.readLock.lock(); + try { + if (this.epochMap.isEmpty()) { + return null; + } + return new EpochEntry(this.epochMap.lastEntry().getValue()); + } finally { + this.readLock.unlock(); + } + } + + public int lastEpoch() { + final EpochEntry entry = lastEntry(); + if (entry != null) { + return entry.getEpoch(); + } + return -1; + } + + public EpochEntry getEntry(final int epoch) { + this.readLock.lock(); + try { + if (this.epochMap.containsKey(epoch)) { + final EpochEntry entry = this.epochMap.get(epoch); + return new EpochEntry(entry); + } + return null; + } finally { + this.readLock.unlock(); + } + } + + public EpochEntry findEpochEntryByOffset(final long offset) { + this.readLock.lock(); + try { + if (!this.epochMap.isEmpty()) { + for (Map.Entry entry : this.epochMap.entrySet()) { + if (entry.getValue().getStartOffset() <= offset && entry.getValue().getEndOffset() > offset) { + return new EpochEntry(entry.getValue()); + } + } + } + return null; + } finally { + this.readLock.unlock(); + } + } + + public EpochEntry nextEntry(final int epoch) { + this.readLock.lock(); + try { + final Map.Entry entry = this.epochMap.ceilingEntry(epoch + 1); + if (entry != null) { + return new EpochEntry(entry.getValue()); + } + return null; + } finally { + this.readLock.unlock(); + } + } + + public List getAllEntries() { + this.readLock.lock(); + try { + final ArrayList result = new ArrayList<>(this.epochMap.size()); + this.epochMap.forEach((key, value) -> result.add(new EpochEntry(value))); + return result; + } finally { + this.readLock.unlock(); + } + } + + /** + * Find the consistentPoint between compareCache and local. + * + * @return the consistent offset + */ + public long findConsistentPoint(final EpochFileCache compareCache) { + this.readLock.lock(); + try { + long consistentOffset = -1; + final Map descendingMap = new TreeMap<>(this.epochMap).descendingMap(); + final Iterator> iter = descendingMap.entrySet().iterator(); + while (iter.hasNext()) { + final Map.Entry curLocalEntry = iter.next(); + final EpochEntry compareEntry = compareCache.getEntry(curLocalEntry.getKey()); + if (compareEntry != null && compareEntry.getStartOffset() == curLocalEntry.getValue().getStartOffset()) { + consistentOffset = Math.min(curLocalEntry.getValue().getEndOffset(), compareEntry.getEndOffset()); + break; + } + } + return consistentOffset; + } finally { + this.readLock.unlock(); + } + } + + /** + * Remove epochEntries with epoch >= truncateEpoch. + */ + public void truncateSuffixByEpoch(final int truncateEpoch) { + Predicate predict = entry -> entry.getEpoch() >= truncateEpoch; + doTruncateSuffix(predict); + } + + /** + * Remove epochEntries with startOffset >= truncateOffset. + */ + public void truncateSuffixByOffset(final long truncateOffset) { + Predicate predict = entry -> entry.getStartOffset() >= truncateOffset; + doTruncateSuffix(predict); + } + + private void doTruncateSuffix(Predicate predict) { + this.writeLock.lock(); + try { + this.epochMap.entrySet().removeIf(entry -> predict.test(entry.getValue())); + final EpochEntry entry = lastEntry(); + if (entry != null) { + entry.setEndOffset(Long.MAX_VALUE); + } + flush(); + } finally { + this.writeLock.unlock(); + } + } + + /** + * Remove epochEntries with endOffset <= truncateOffset. + */ + public void truncatePrefixByOffset(final long truncateOffset) { + Predicate predict = entry -> entry.getEndOffset() <= truncateOffset; + this.writeLock.lock(); + try { + this.epochMap.entrySet().removeIf(entry -> predict.test(entry.getValue())); + flush(); + } finally { + this.writeLock.unlock(); + } + } + + private void flush() { + this.writeLock.lock(); + try { + if (this.checkpoint != null) { + final ArrayList entries = new ArrayList<>(this.epochMap.values()); + this.checkpoint.write(entries); + } + } catch (final IOException e) { + log.error("Error happen when flush epochEntries to epochCheckpointFile", e); + } finally { + this.writeLock.unlock(); + } + } + + static class EpochEntrySerializer implements CheckpointFile.CheckpointSerializer { + + @Override + public String toLine(EpochEntry entry) { + if (entry != null) { + return String.format("%d-%d", entry.getEpoch(), entry.getStartOffset()); + } else { + return null; + } + } + + @Override + public EpochEntry fromLine(String line) { + final String[] arr = line.split("-"); + if (arr.length == 2) { + final int epoch = Integer.parseInt(arr[0]); + final long startOffset = Long.parseLong(arr[1]); + return new EpochEntry(epoch, startOffset); + } + return null; + } + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/ha/autoswitch/MetadataFile.java b/store/src/main/java/org/apache/rocketmq/store/ha/autoswitch/MetadataFile.java new file mode 100644 index 00000000000..e89aedbea14 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ha/autoswitch/MetadataFile.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.ha.autoswitch; + +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.UtilAll; + +import java.io.File; + +public abstract class MetadataFile { + + protected String filePath; + + public abstract String encodeToStr(); + + public abstract void decodeFromStr(String dataStr); + + public abstract boolean isLoaded(); + + public abstract void clearInMem(); + + public void writeToFile() throws Exception { + UtilAll.deleteFile(new File(filePath)); + MixAll.string2File(encodeToStr(), this.filePath); + } + + public void readFromFile() throws Exception { + String dataStr = MixAll.file2String(filePath); + decodeFromStr(dataStr); + } + public boolean fileExists() { + File file = new File(filePath); + return file.exists(); + } + + public void clear() { + clearInMem(); + UtilAll.deleteFile(new File(filePath)); + } + + public String getFilePath() { + return filePath; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/ha/autoswitch/TempBrokerMetadata.java b/store/src/main/java/org/apache/rocketmq/store/ha/autoswitch/TempBrokerMetadata.java new file mode 100644 index 00000000000..7a4126b02ab --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ha/autoswitch/TempBrokerMetadata.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.ha.autoswitch; + +import org.apache.commons.lang3.StringUtils; + +public class TempBrokerMetadata extends BrokerMetadata { + + private String registerCheckCode; + + public TempBrokerMetadata(String filePath) { + this(filePath, null, null, null, null); + } + + public TempBrokerMetadata(String filePath, String clusterName, String brokerName, Long brokerId, String registerCheckCode) { + super(filePath); + super.clusterName = clusterName; + super.brokerId = brokerId; + super.brokerName = brokerName; + this.registerCheckCode = registerCheckCode; + } + + public void updateAndPersist(String clusterName, String brokerName, Long brokerId, String registerCheckCode) throws Exception { + super.clusterName = clusterName; + super.brokerName = brokerName; + super.brokerId = brokerId; + this.registerCheckCode = registerCheckCode; + writeToFile(); + } + + @Override + public String encodeToStr() { + StringBuilder sb = new StringBuilder(); + sb.append(clusterName).append("#"); + sb.append(brokerName).append("#"); + sb.append(brokerId).append("#"); + sb.append(registerCheckCode); + return sb.toString(); + } + + @Override + public void decodeFromStr(String dataStr) { + if (dataStr == null) return; + String[] dataArr = dataStr.split("#"); + this.clusterName = dataArr[0]; + this.brokerName = dataArr[1]; + this.brokerId = Long.valueOf(dataArr[2]); + this.registerCheckCode = dataArr[3]; + } + + @Override + public boolean isLoaded() { + return super.isLoaded() && StringUtils.isNotEmpty(this.registerCheckCode); + } + + @Override + public void clearInMem() { + super.clearInMem(); + this.registerCheckCode = null; + } + + public Long getBrokerId() { + return brokerId; + } + + public String getRegisterCheckCode() { + return registerCheckCode; + } + + @Override + public String toString() { + return "TempBrokerMetadata{" + + "registerCheckCode='" + registerCheckCode + '\'' + + ", clusterName='" + clusterName + '\'' + + ", brokerName='" + brokerName + '\'' + + ", brokerId=" + brokerId + + ", filePath='" + filePath + '\'' + + '}'; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/ha/io/AbstractHAReader.java b/store/src/main/java/org/apache/rocketmq/store/ha/io/AbstractHAReader.java new file mode 100644 index 00000000000..b71e2160b33 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ha/io/AbstractHAReader.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.ha.io; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.List; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public abstract class AbstractHAReader { + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + protected final List readHookList = new ArrayList<>(); + + public boolean read(SocketChannel socketChannel, ByteBuffer byteBufferRead) { + int readSizeZeroTimes = 0; + while (byteBufferRead.hasRemaining()) { + try { + int readSize = socketChannel.read(byteBufferRead); + for (HAReadHook readHook : readHookList) { + readHook.afterRead(readSize); + } + if (readSize > 0) { + readSizeZeroTimes = 0; + boolean result = processReadResult(byteBufferRead); + if (!result) { + LOGGER.error("Process read result failed"); + return false; + } + } else if (readSize == 0) { + if (++readSizeZeroTimes >= 3) { + break; + } + } else { + LOGGER.info("Read socket < 0"); + return false; + } + } catch (IOException e) { + LOGGER.info("Read socket exception", e); + return false; + } + } + + return true; + } + + public void registerHook(HAReadHook readHook) { + readHookList.add(readHook); + } + + public void clearHook() { + readHookList.clear(); + } + + /** + * Process read result. + * + * @param byteBufferRead read result + * @return true if process succeed, false otherwise + */ + protected abstract boolean processReadResult(ByteBuffer byteBufferRead); +} diff --git a/store/src/main/java/org/apache/rocketmq/store/ha/io/HAReadHook.java b/store/src/main/java/org/apache/rocketmq/store/ha/io/HAReadHook.java new file mode 100644 index 00000000000..4efcadd5580 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ha/io/HAReadHook.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.ha.io; + +public interface HAReadHook { + void afterRead(int readSize); +} diff --git a/store/src/main/java/org/apache/rocketmq/store/ha/io/HAWriteHook.java b/store/src/main/java/org/apache/rocketmq/store/ha/io/HAWriteHook.java new file mode 100644 index 00000000000..9594328880a --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ha/io/HAWriteHook.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.ha.io; + +public interface HAWriteHook { + void afterWrite(int writeSize); +} diff --git a/store/src/main/java/org/apache/rocketmq/store/ha/io/HAWriter.java b/store/src/main/java/org/apache/rocketmq/store/ha/io/HAWriter.java new file mode 100644 index 00000000000..0f5699bac13 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/ha/io/HAWriter.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.ha.io; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.List; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +public class HAWriter { + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + protected final List writeHookList = new ArrayList<>(); + + public boolean write(SocketChannel socketChannel, ByteBuffer byteBufferWrite) throws IOException { + int writeSizeZeroTimes = 0; + while (byteBufferWrite.hasRemaining()) { + int writeSize = socketChannel.write(byteBufferWrite); + for (HAWriteHook writeHook : writeHookList) { + writeHook.afterWrite(writeSize); + } + if (writeSize > 0) { + writeSizeZeroTimes = 0; + } else if (writeSize == 0) { + if (++writeSizeZeroTimes >= 3) { + break; + } + } else { + LOGGER.info("Write socket < 0"); + } + } + + return !byteBufferWrite.hasRemaining(); + } + + public void registerHook(HAWriteHook writeHook) { + writeHookList.add(writeHook); + } + + public void clearHook() { + writeHookList.clear(); + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/hook/PutMessageHook.java b/store/src/main/java/org/apache/rocketmq/store/hook/PutMessageHook.java new file mode 100644 index 00000000000..dc47d32934c --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/hook/PutMessageHook.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.hook; + +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.store.PutMessageResult; + +public interface PutMessageHook { + + /** + * Name of the hook. + * + * @return name of the hook + */ + String hookName(); + + /** + * Execute before put message. For example, Message verification or special message transform + * @param msg + * @return + */ + PutMessageResult executeBeforePutMessage(MessageExt msg); +} diff --git a/store/src/main/java/org/apache/rocketmq/store/hook/SendMessageBackHook.java b/store/src/main/java/org/apache/rocketmq/store/hook/SendMessageBackHook.java new file mode 100644 index 00000000000..02254503217 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/hook/SendMessageBackHook.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.hook; + +import java.util.List; +import org.apache.rocketmq.common.message.MessageExt; + +public interface SendMessageBackHook { + + /** + * Slave send message back to master at certain offset when HA handshake + * + * @param msgList + * @param brokerName + * @param brokerAddr + * @return + */ + boolean executeSendMessageBack(List msgList, String brokerName, String brokerAddr); +} diff --git a/store/src/main/java/org/apache/rocketmq/store/index/IndexFile.java b/store/src/main/java/org/apache/rocketmq/store/index/IndexFile.java index edc24764e75..9e0669fa035 100644 --- a/store/src/main/java/org/apache/rocketmq/store/index/IndexFile.java +++ b/store/src/main/java/org/apache/rocketmq/store/index/IndexFile.java @@ -19,32 +19,44 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.channels.FileLock; import java.util.List; +import org.apache.rocketmq.common.UtilAll; import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.store.MappedFile; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.logfile.DefaultMappedFile; +import org.apache.rocketmq.store.logfile.MappedFile; public class IndexFile { private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); private static int hashSlotSize = 4; + /** + * Each index's store unit. Format: + *
    +     * ┌───────────────┬───────────────────────────────┬───────────────┬───────────────┐
    +     * │ Key HashCode  │        Physical Offset        │   Time Diff   │ Next Index Pos│
    +     * │   (4 Bytes)   │          (8 Bytes)            │   (4 Bytes)   │   (4 Bytes)   │
    +     * ├───────────────┴───────────────────────────────┴───────────────┴───────────────┤
    +     * │                                 Index Store Unit                              │
    +     * │                                                                               │
    +     * 
    + * Each index's store unit. Size: + * Key HashCode(4) + Physical Offset(8) + Time Diff(4) + Next Index Pos(4) = 20 Bytes + */ private static int indexSize = 20; private static int invalidIndex = 0; private final int hashSlotNum; private final int indexNum; + private final int fileTotalSize; private final MappedFile mappedFile; - private final FileChannel fileChannel; private final MappedByteBuffer mappedByteBuffer; private final IndexHeader indexHeader; public IndexFile(final String fileName, final int hashSlotNum, final int indexNum, final long endPhyOffset, final long endTimestamp) throws IOException { - int fileTotalSize = + this.fileTotalSize = IndexHeader.INDEX_HEADER_SIZE + (hashSlotNum * hashSlotSize) + (indexNum * indexSize); - this.mappedFile = new MappedFile(fileName, fileTotalSize); - this.fileChannel = this.mappedFile.getFileChannel(); + this.mappedFile = new DefaultMappedFile(fileName, fileTotalSize); this.mappedByteBuffer = this.mappedFile.getMappedByteBuffer(); this.hashSlotNum = hashSlotNum; this.indexNum = indexNum; @@ -67,17 +79,26 @@ public String getFileName() { return this.mappedFile.getFileName(); } + public int getFileSize() { + return this.fileTotalSize; + } + public void load() { this.indexHeader.load(); } + public void shutdown() { + this.flush(); + UtilAll.cleanBuffer(this.mappedByteBuffer); + } + public void flush() { long beginTime = System.currentTimeMillis(); if (this.mappedFile.hold()) { this.indexHeader.updateByteBuffer(); this.mappedByteBuffer.force(); this.mappedFile.release(); - log.info("flush index file eclipse time(ms) " + (System.currentTimeMillis() - beginTime)); + log.info("flush index file elapsed time(ms) " + (System.currentTimeMillis() - beginTime)); } } @@ -95,12 +116,8 @@ public boolean putKey(final String key, final long phyOffset, final long storeTi int slotPos = keyHash % this.hashSlotNum; int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize; - FileLock fileLock = null; - try { - // fileLock = this.fileChannel.lock(absSlotPos, hashSlotSize, - // false); int slotValue = this.mappedByteBuffer.getInt(absSlotPos); if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount()) { slotValue = invalidIndex; @@ -134,7 +151,9 @@ public boolean putKey(final String key, final long phyOffset, final long storeTi this.indexHeader.setBeginTimestamp(storeTimestamp); } - this.indexHeader.incHashSlotCount(); + if (invalidIndex == slotValue) { + this.indexHeader.incHashSlotCount(); + } this.indexHeader.incIndexCount(); this.indexHeader.setEndPhyOffset(phyOffset); this.indexHeader.setEndTimestamp(storeTimestamp); @@ -142,14 +161,6 @@ public boolean putKey(final String key, final long phyOffset, final long storeTi return true; } catch (Exception e) { log.error("putKey exception, Key: " + key + " KeyHashCode: " + key.hashCode(), e); - } finally { - if (fileLock != null) { - try { - fileLock.release(); - } catch (IOException e) { - log.error("Failed to release the lock", e); - } - } } } else { log.warn("Over index file capacity: index count = " + this.indexHeader.getIndexCount() @@ -162,8 +173,9 @@ public boolean putKey(final String key, final long phyOffset, final long storeTi public int indexKeyHashMethod(final String key) { int keyHash = key.hashCode(); int keyHashPositive = Math.abs(keyHash); - if (keyHashPositive < 0) + if (keyHashPositive < 0) { keyHashPositive = 0; + } return keyHashPositive; } @@ -181,31 +193,20 @@ public long getEndPhyOffset() { public boolean isTimeMatched(final long begin, final long end) { boolean result = begin < this.indexHeader.getBeginTimestamp() && end > this.indexHeader.getEndTimestamp(); - result = result || (begin >= this.indexHeader.getBeginTimestamp() && begin <= this.indexHeader.getEndTimestamp()); - result = result || (end >= this.indexHeader.getBeginTimestamp() && end <= this.indexHeader.getEndTimestamp()); + result = result || begin >= this.indexHeader.getBeginTimestamp() && begin <= this.indexHeader.getEndTimestamp(); + result = result || end >= this.indexHeader.getBeginTimestamp() && end <= this.indexHeader.getEndTimestamp(); return result; } public void selectPhyOffset(final List phyOffsets, final String key, final int maxNum, - final long begin, final long end, boolean lock) { + final long begin, final long end) { if (this.mappedFile.hold()) { int keyHash = indexKeyHashMethod(key); int slotPos = keyHash % this.hashSlotNum; int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize; - FileLock fileLock = null; try { - if (lock) { - // fileLock = this.fileChannel.lock(absSlotPos, - // hashSlotSize, true); - } - int slotValue = this.mappedByteBuffer.getInt(absSlotPos); - // if (fileLock != null) { - // fileLock.release(); - // fileLock = null; - // } - if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount() || this.indexHeader.getIndexCount() <= 1) { } else { @@ -221,7 +222,7 @@ public void selectPhyOffset(final List phyOffsets, final String key, final int keyHashRead = this.mappedByteBuffer.getInt(absIndexPos); long phyOffsetRead = this.mappedByteBuffer.getLong(absIndexPos + 4); - long timeDiff = (long) this.mappedByteBuffer.getInt(absIndexPos + 4 + 8); + long timeDiff = this.mappedByteBuffer.getInt(absIndexPos + 4 + 8); int prevIndexRead = this.mappedByteBuffer.getInt(absIndexPos + 4 + 8 + 4); if (timeDiff < 0) { @@ -231,7 +232,7 @@ public void selectPhyOffset(final List phyOffsets, final String key, final timeDiff *= 1000L; long timeRead = this.indexHeader.getBeginTimestamp() + timeDiff; - boolean timeMatched = (timeRead >= begin) && (timeRead <= end); + boolean timeMatched = timeRead >= begin && timeRead <= end; if (keyHash == keyHashRead && timeMatched) { phyOffsets.add(phyOffsetRead); @@ -249,14 +250,6 @@ public void selectPhyOffset(final List phyOffsets, final String key, final } catch (Exception e) { log.error("selectPhyOffset exception ", e); } finally { - if (fileLock != null) { - try { - fileLock.release(); - } catch (IOException e) { - log.error("Failed to release the lock", e); - } - } - this.mappedFile.release(); } } diff --git a/store/src/main/java/org/apache/rocketmq/store/index/IndexHeader.java b/store/src/main/java/org/apache/rocketmq/store/index/IndexHeader.java index 44021cd5895..fe319caada9 100644 --- a/store/src/main/java/org/apache/rocketmq/store/index/IndexHeader.java +++ b/store/src/main/java/org/apache/rocketmq/store/index/IndexHeader.java @@ -20,6 +20,19 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; +/** + * Index File Header. Format: + *
    + * ┌───────────────────────────────┬───────────────────────────────┬───────────────────────────────┬───────────────────────────────┬───────────────────┬───────────────────┐
    + * │        Begin Timestamp        │          End Timestamp        │     Begin Physical Offset     │       End Physical Offset     │  Hash Slot Count  │    Index Count    │
    + * │           (8 Bytes)           │            (8 Bytes)          │           (8 Bytes)           │           (8 Bytes)           │      (4 Bytes)    │      (4 Bytes)    │
    + * ├───────────────────────────────┴───────────────────────────────┴───────────────────────────────┴───────────────────────────────┴───────────────────┴───────────────────┤
    + * │                                                                      Index File Header                                                                                │
    + * │
    + * 
    + * Index File Header. Size: + * Begin Timestamp(8) + End Timestamp(8) + Begin Physical Offset(8) + End Physical Offset(8) + Hash Slot Count(4) + Index Count(4) = 40 Bytes + */ public class IndexHeader { public static final int INDEX_HEADER_SIZE = 40; private static int beginTimestampIndex = 0; @@ -29,13 +42,12 @@ public class IndexHeader { private static int hashSlotcountIndex = 32; private static int indexCountIndex = 36; private final ByteBuffer byteBuffer; - private AtomicLong beginTimestamp = new AtomicLong(0); - private AtomicLong endTimestamp = new AtomicLong(0); - private AtomicLong beginPhyOffset = new AtomicLong(0); - private AtomicLong endPhyOffset = new AtomicLong(0); - private AtomicInteger hashSlotCount = new AtomicInteger(0); - - private AtomicInteger indexCount = new AtomicInteger(1); + private final AtomicLong beginTimestamp = new AtomicLong(0); + private final AtomicLong endTimestamp = new AtomicLong(0); + private final AtomicLong beginPhyOffset = new AtomicLong(0); + private final AtomicLong endPhyOffset = new AtomicLong(0); + private final AtomicInteger hashSlotCount = new AtomicInteger(0); + private final AtomicInteger indexCount = new AtomicInteger(1); public IndexHeader(final ByteBuffer byteBuffer) { this.byteBuffer = byteBuffer; diff --git a/store/src/main/java/org/apache/rocketmq/store/index/IndexService.java b/store/src/main/java/org/apache/rocketmq/store/index/IndexService.java index b565349e12f..ef5d21ac7ce 100644 --- a/store/src/main/java/org/apache/rocketmq/store/index/IndexService.java +++ b/store/src/main/java/org/apache/rocketmq/store/index/IndexService.java @@ -23,18 +23,19 @@ import java.util.List; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import org.apache.rocketmq.common.AbstractBrokerRunnable; import org.apache.rocketmq.common.UtilAll; import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; import org.apache.rocketmq.common.message.MessageConst; import org.apache.rocketmq.common.sysflag.MessageSysFlag; import org.apache.rocketmq.store.DefaultMessageStore; import org.apache.rocketmq.store.DispatchRequest; import org.apache.rocketmq.store.config.StorePathConfigHelper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class IndexService { - private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); /** * Maximum times to attempt index file creation. */ @@ -43,7 +44,7 @@ public class IndexService { private final int hashSlotNum; private final int indexNum; private final String storePath; - private final ArrayList indexFileList = new ArrayList(); + private final ArrayList indexFileList = new ArrayList<>(); private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); public IndexService(final DefaultMessageStore store) { @@ -51,7 +52,7 @@ public IndexService(final DefaultMessageStore store) { this.hashSlotNum = store.getMessageStoreConfig().getMaxHashSlotNum(); this.indexNum = store.getMessageStoreConfig().getMaxIndexNum(); this.storePath = - StorePathConfigHelper.getStorePathIndex(store.getMessageStoreConfig().getStorePathRootDir()); + StorePathConfigHelper.getStorePathIndex(defaultMessageStore.getMessageStoreConfig().getStorePathRootDir()); } public boolean load(final boolean lastExitOK) { @@ -73,13 +74,13 @@ public boolean load(final boolean lastExitOK) { } } - log.info("load index file OK, " + f.getFileName()); + LOGGER.info("load index file OK, " + f.getFileName()); this.indexFileList.add(f); } catch (IOException e) { - log.error("load file {} error", file, e); + LOGGER.error("load file {} error", file, e); return false; } catch (NumberFormatException e) { - log.error("load file {} error", file, e); + LOGGER.error("load file {} error", file, e); } } } @@ -87,6 +88,14 @@ public boolean load(final boolean lastExitOK) { return true; } + public long getTotalSize() { + if (indexFileList.isEmpty()) { + return 0; + } + + return (long) indexFileList.get(0).getFileSize() * indexFileList.size(); + } + public void deleteExpiredFile(long offset) { Object[] files = null; try { @@ -100,13 +109,13 @@ public void deleteExpiredFile(long offset) { files = this.indexFileList.toArray(); } } catch (Exception e) { - log.error("destroy exception", e); + LOGGER.error("destroy exception", e); } finally { this.readWriteLock.readLock().unlock(); } if (files != null) { - List fileList = new ArrayList(); + List fileList = new ArrayList<>(); for (int i = 0; i < (files.length - 1); i++) { IndexFile f = (IndexFile) files[i]; if (f.getEndPhyOffset() < offset) { @@ -128,12 +137,12 @@ private void deleteExpiredFile(List files) { boolean destroyed = file.destroy(3000); destroyed = destroyed && this.indexFileList.remove(file); if (!destroyed) { - log.error("deleteExpiredFile remove failed."); + LOGGER.error("deleteExpiredFile remove failed."); break; } } } catch (Exception e) { - log.error("deleteExpiredFile has exception.", e); + LOGGER.error("deleteExpiredFile has exception.", e); } finally { this.readWriteLock.writeLock().unlock(); } @@ -148,14 +157,14 @@ public void destroy() { } this.indexFileList.clear(); } catch (Exception e) { - log.error("destroy exception", e); + LOGGER.error("destroy exception", e); } finally { this.readWriteLock.writeLock().unlock(); } } public QueryOffsetResult queryOffset(String topic, String key, int maxNum, long begin, long end) { - List phyOffsets = new ArrayList(maxNum); + List phyOffsets = new ArrayList<>(maxNum); long indexLastUpdateTimestamp = 0; long indexLastUpdatePhyoffset = 0; @@ -173,7 +182,7 @@ public QueryOffsetResult queryOffset(String topic, String key, int maxNum, long if (f.isTimeMatched(begin, end)) { - f.selectPhyOffset(phyOffsets, buildKey(topic, key), maxNum, begin, end, lastFile); + f.selectPhyOffset(phyOffsets, buildKey(topic, key), maxNum, begin, end); } if (f.getBeginTimestamp() < begin) { @@ -186,7 +195,7 @@ public QueryOffsetResult queryOffset(String topic, String key, int maxNum, long } } } catch (Exception e) { - log.error("queryMsg exception", e); + LOGGER.error("queryMsg exception", e); } finally { this.readWriteLock.readLock().unlock(); } @@ -222,7 +231,7 @@ public void buildIndex(DispatchRequest req) { if (req.getUniqKey() != null) { indexFile = putKey(indexFile, msg, buildKey(topic, req.getUniqKey())); if (indexFile == null) { - log.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey()); + LOGGER.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey()); return; } } @@ -234,20 +243,20 @@ public void buildIndex(DispatchRequest req) { if (key.length() > 0) { indexFile = putKey(indexFile, msg, buildKey(topic, key)); if (indexFile == null) { - log.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey()); + LOGGER.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey()); return; } } } } } else { - log.error("build index error, stop building index"); + LOGGER.error("build index error, stop building index"); } } private IndexFile putKey(IndexFile indexFile, DispatchRequest msg, String idxKey) { for (boolean ok = indexFile.putKey(idxKey, msg.getCommitLogOffset(), msg.getStoreTimestamp()); !ok; ) { - log.warn("Index file [" + indexFile.getFileName() + "] is full, trying to create another one"); + LOGGER.warn("Index file [" + indexFile.getFileName() + "] is full, trying to create another one"); indexFile = retryGetAndCreateIndexFile(); if (null == indexFile) { @@ -270,20 +279,21 @@ public IndexFile retryGetAndCreateIndexFile() { for (int times = 0; null == indexFile && times < MAX_TRY_IDX_CREATE; times++) { indexFile = this.getAndCreateLastIndexFile(); - if (null != indexFile) + if (null != indexFile) { break; + } try { - log.info("Tried to create index file " + times + " times"); + LOGGER.info("Tried to create index file " + times + " times"); Thread.sleep(1000); } catch (InterruptedException e) { - log.error("Interrupted", e); + LOGGER.error("Interrupted", e); } } if (null == indexFile) { - this.defaultMessageStore.getAccessRights().makeIndexFileError(); - log.error("Mark index file cannot build flag"); + this.defaultMessageStore.getRunningFlags().makeIndexFileError(); + LOGGER.error("Mark index file cannot build flag"); } return indexFile; @@ -322,16 +332,17 @@ public IndexFile getAndCreateLastIndexFile() { this.readWriteLock.writeLock().lock(); this.indexFileList.add(indexFile); } catch (Exception e) { - log.error("getLastIndexFile exception ", e); + LOGGER.error("getLastIndexFile exception ", e); } finally { this.readWriteLock.writeLock().unlock(); } if (indexFile != null) { final IndexFile flushThisFile = prevIndexFile; - Thread flushThread = new Thread(new Runnable() { + + Thread flushThread = new Thread(new AbstractBrokerRunnable(defaultMessageStore.getBrokerConfig()) { @Override - public void run() { + public void run0() { IndexService.this.flush(flushThisFile); } }, "FlushIndexFileThread"); @@ -345,8 +356,9 @@ public void run() { } public void flush(final IndexFile f) { - if (null == f) + if (null == f) { return; + } long indexMsgTimestamp = 0; @@ -367,6 +379,20 @@ public void start() { } public void shutdown() { - + try { + this.readWriteLock.writeLock().lock(); + for (IndexFile f : this.indexFileList) { + try { + f.shutdown(); + } catch (Exception e) { + LOGGER.error("shutdown " + f.getFileName() + " exception", e); + } + } + this.indexFileList.clear(); + } catch (Exception e) { + LOGGER.error("shutdown exception", e); + } finally { + this.readWriteLock.writeLock().unlock(); + } } } diff --git a/store/src/main/java/org/apache/rocketmq/store/kv/CommitLogDispatcherCompaction.java b/store/src/main/java/org/apache/rocketmq/store/kv/CommitLogDispatcherCompaction.java new file mode 100644 index 00000000000..5c285b144a9 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/kv/CommitLogDispatcherCompaction.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.kv; + +import org.apache.rocketmq.store.CommitLogDispatcher; +import org.apache.rocketmq.store.DispatchRequest; + +public class CommitLogDispatcherCompaction implements CommitLogDispatcher { + private final CompactionService cptService; + + public CommitLogDispatcherCompaction(CompactionService srv) { + this.cptService = srv; + } + + @Override + public void dispatch(DispatchRequest request) { + if (cptService != null) { + cptService.putRequest(request); + } + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/kv/CompactionLog.java b/store/src/main/java/org/apache/rocketmq/store/kv/CompactionLog.java new file mode 100644 index 00000000000..be2bb551ad7 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/kv/CompactionLog.java @@ -0,0 +1,1149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.kv; + +import com.google.common.collect.Lists; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.AppendMessageResult; +import org.apache.rocketmq.store.AppendMessageStatus; +import org.apache.rocketmq.store.CompactionAppendMsgCallback; +import org.apache.rocketmq.store.DispatchRequest; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.GetMessageStatus; +import org.apache.rocketmq.store.MappedFileQueue; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.PutMessageLock; +import org.apache.rocketmq.store.PutMessageReentrantLock; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageSpinLock; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.StoreUtil; +import org.apache.rocketmq.store.config.BrokerRole; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.logfile.MappedFile; +import org.apache.rocketmq.store.queue.BatchConsumeQueue; +import org.apache.rocketmq.store.queue.CqUnit; +import org.apache.rocketmq.store.queue.ReferredIterator; +import org.apache.rocketmq.store.queue.SparseConsumeQueue; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.security.DigestException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import static org.apache.rocketmq.common.message.MessageDecoder.BLANK_MAGIC_CODE; + +public class CompactionLog { + private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + + private static final int END_FILE_MIN_BLANK_LENGTH = 4 + 4; + private static final int MAX_PULL_MSG_SIZE = 128 * 1024 * 1024; + public static final String COMPACTING_SUB_FOLDER = "compacting"; + public static final String REPLICATING_SUB_FOLDER = "replicating"; + + private final int compactionLogMappedFileSize; + private final int compactionCqMappedFileSize; + private final String compactionLogFilePath; + private final String compactionCqFilePath; + private final MessageStore defaultMessageStore; + private final CompactionStore compactionStore; + private final MessageStoreConfig messageStoreConfig; + private final CompactionAppendMsgCallback endMsgCallback; + private final String topic; + private final int queueId; + private final int offsetMapMemorySize; + private final PutMessageLock putMessageLock; + private final PutMessageLock readMessageLock; + private TopicPartitionLog current; + private TopicPartitionLog compacting; + private TopicPartitionLog replicating; + private final CompactionPositionMgr positionMgr; + private final AtomicReference state; + + public CompactionLog(final MessageStore messageStore, final CompactionStore compactionStore, final String topic, final int queueId) + throws IOException { + this.topic = topic; + this.queueId = queueId; + this.defaultMessageStore = messageStore; + this.compactionStore = compactionStore; + this.messageStoreConfig = messageStore.getMessageStoreConfig(); + this.offsetMapMemorySize = compactionStore.getOffsetMapSize(); + this.compactionCqMappedFileSize = + messageStoreConfig.getCompactionCqMappedFileSize() / BatchConsumeQueue.CQ_STORE_UNIT_SIZE + * BatchConsumeQueue.CQ_STORE_UNIT_SIZE; + this.compactionLogMappedFileSize = getCompactionLogSize(compactionCqMappedFileSize, + messageStoreConfig.getCompactionMappedFileSize()); + this.compactionLogFilePath = Paths.get(compactionStore.getCompactionLogPath(), + topic, String.valueOf(queueId)).toString(); + this.compactionCqFilePath = compactionStore.getCompactionCqPath(); // batch consume queue already separated + this.positionMgr = compactionStore.getPositionMgr(); + + this.putMessageLock = + messageStore.getMessageStoreConfig().isUseReentrantLockWhenPutMessage() ? new PutMessageReentrantLock() : + new PutMessageSpinLock(); + this.readMessageLock = + messageStore.getMessageStoreConfig().isUseReentrantLockWhenPutMessage() ? new PutMessageReentrantLock() : + new PutMessageSpinLock(); + this.endMsgCallback = new CompactionAppendEndMsgCallback(); + this.state = new AtomicReference<>(State.INITIALIZING); + log.info("CompactionLog {}:{} init completed.", topic, queueId); + } + + private int getCompactionLogSize(int cqSize, int origLogSize) { + int n = origLogSize / cqSize; + if (n < 5) { + return cqSize * 5; + } + int m = origLogSize % cqSize; + if (m > 0 && m < (cqSize >> 1)) { + return n * cqSize; + } else { + return (n + 1) * cqSize; + } + } + + public void load(boolean exitOk) throws IOException, RuntimeException { + initLogAndCq(exitOk); + if (defaultMessageStore.getMessageStoreConfig().getBrokerRole() == BrokerRole.SLAVE + && getLog().isMappedFilesEmpty()) { + log.info("{}:{} load compactionLog from remote master", topic, queueId); + loadFromRemoteAsync(); + } else { + state.compareAndSet(State.INITIALIZING, State.NORMAL); + } + } + + private void initLogAndCq(boolean exitOk) throws IOException, RuntimeException { + current = new TopicPartitionLog(this); + current.init(exitOk); + } + + + private boolean putMessageFromRemote(byte[] bytes) { + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + // split bytebuffer to avoid encode message again + while (byteBuffer.hasRemaining()) { + int mark = byteBuffer.position(); + ByteBuffer bb = byteBuffer.slice(); + int size = bb.getInt(); + if (size < 0 || size > byteBuffer.capacity()) { + break; + } else { + bb.limit(size); + bb.rewind(); + } + + MessageExt messageExt = MessageDecoder.decode(bb, false, false); + long messageOffset = messageExt.getQueueOffset(); + long minOffsetInQueue = getCQ().getMinOffsetInQueue(); + if (getLog().isMappedFilesEmpty() || messageOffset < minOffsetInQueue) { + asyncPutMessage(bb, messageExt, replicating); + } else { + log.info("{}:{} message offset {} >= minOffsetInQueue {}, stop pull...", + topic, queueId, messageOffset, minOffsetInQueue); + return false; + } + + byteBuffer.position(mark + size); + } + + return true; + + } + + private void pullMessageFromMaster() throws Exception { + + if (StringUtils.isBlank(compactionStore.getMasterAddr())) { + compactionStore.getCompactionSchedule().schedule(() -> { + try { + pullMessageFromMaster(); + } catch (Exception e) { + log.error("pullMessageFromMaster exception: ", e); + } + }, 5, TimeUnit.SECONDS); + return; + } + + replicating = new TopicPartitionLog(this, REPLICATING_SUB_FOLDER); + try (MessageFetcher messageFetcher = new MessageFetcher()) { + messageFetcher.pullMessageFromMaster(topic, queueId, getCQ().getMinOffsetInQueue(), + compactionStore.getMasterAddr(), (currOffset, response) -> { + if (currOffset < 0) { + log.info("{}:{} current offset {}, stop pull...", topic, queueId, currOffset); + return false; + } + return putMessageFromRemote(response.getBody()); +// positionMgr.setOffset(topic, queueId, currOffset); + }); + } + + // merge files + if (getLog().isMappedFilesEmpty()) { + replaceFiles(getLog().getMappedFiles(), current, replicating); + } else if (replicating.getLog().isMappedFilesEmpty()) { + log.info("replicating message is empty"); //break + } else { + List newFiles = Lists.newArrayList(); + List toCompactFiles = Lists.newArrayList(replicating.getLog().getMappedFiles()); + putMessageLock.lock(); + try { + // combine current and replicating to mappedFileList + newFiles = Lists.newArrayList(getLog().getMappedFiles()); + toCompactFiles.addAll(newFiles); //all from current + current.roll(toCompactFiles.size() * compactionLogMappedFileSize); + } catch (Throwable e) { + log.error("roll log and cq exception: ", e); + } finally { + putMessageLock.unlock(); + } + + try { + // doCompaction with current and replicating + compactAndReplace(new ProcessFileList(toCompactFiles, toCompactFiles)); + } catch (Throwable e) { + log.error("do merge replicating and current exception: ", e); + } + } + + // cleanReplicatingResource, force clean cq + replicating.clean(false, true); + +// positionMgr.setOffset(topic, queueId, currentPullOffset); + state.compareAndSet(State.INITIALIZING, State.NORMAL); + } + private void loadFromRemoteAsync() { + compactionStore.getCompactionSchedule().submit(() -> { + try { + pullMessageFromMaster(); + } catch (Exception e) { + log.error("fetch message from master exception: ", e); + } + }); + + // update (currentStatus) = LOADING + + // request => get (start, end) + // pull message => current message offset > end + // done + // positionMgr.persist(); + + // update (currentStatus) = RUNNING + } + + private long nextOffsetCorrection(long oldOffset, long newOffset) { + long nextOffset = oldOffset; + if (messageStoreConfig.getBrokerRole() != BrokerRole.SLAVE || messageStoreConfig.isOffsetCheckInSlave()) { + nextOffset = newOffset; + } + return nextOffset; + } + + private boolean checkInDiskByCommitOffset(long offsetPy, long maxOffsetPy) { + long memory = (long) (StoreUtil.TOTAL_PHYSICAL_MEMORY_SIZE * + (this.messageStoreConfig.getAccessMessageInMemoryMaxRatio() / 100.0)); + return (maxOffsetPy - offsetPy) > memory; + } + + private boolean isTheBatchFull(int sizePy, int unitBatchNum, int maxMsgNums, long maxMsgSize, + int bufferTotal, int messageTotal, boolean isInDisk) { + + if (0 == bufferTotal || 0 == messageTotal) { + return false; + } + + if (messageTotal + unitBatchNum > maxMsgNums) { + return true; + } + + if (bufferTotal + sizePy > maxMsgSize) { + return true; + } + + if (isInDisk) { + if ((bufferTotal + sizePy) > this.messageStoreConfig.getMaxTransferBytesOnMessageInDisk()) { + return true; + } + + if (messageTotal > this.messageStoreConfig.getMaxTransferCountOnMessageInDisk() - 1) { + return true; + } + } else { + if ((bufferTotal + sizePy) > this.messageStoreConfig.getMaxTransferBytesOnMessageInMemory()) { + return true; + } + + if (messageTotal > this.messageStoreConfig.getMaxTransferCountOnMessageInMemory() - 1) { + return true; + } + } + + return false; + } + + public long rollNextFile(final long offset) { + return offset + compactionLogMappedFileSize - offset % compactionLogMappedFileSize; + } + + boolean shouldRetainMsg(final MessageExt msgExt, final OffsetMap map) throws DigestException { + if (msgExt.getQueueOffset() > map.getLastOffset()) { + return true; + } + + String key = msgExt.getKeys(); + if (StringUtils.isNotBlank(key)) { + boolean keyNotExistOrOffsetBigger = msgExt.getQueueOffset() >= map.get(key); + boolean hasBody = ArrayUtils.isNotEmpty(msgExt.getBody()); + return keyNotExistOrOffsetBigger && hasBody; + } else { + log.error("message has no keys"); + return false; + } + } + + public void checkAndPutMessage(final SelectMappedBufferResult selectMappedBufferResult, final MessageExt msgExt, + final OffsetMap offsetMap, final TopicPartitionLog tpLog) + throws DigestException { + if (shouldRetainMsg(msgExt, offsetMap)) { + asyncPutMessage(selectMappedBufferResult.getByteBuffer(), msgExt, tpLog); + } + } + + public CompletableFuture asyncPutMessage(final SelectMappedBufferResult selectMappedBufferResult) { + return asyncPutMessage(selectMappedBufferResult, current); + } + + public CompletableFuture asyncPutMessage(final SelectMappedBufferResult selectMappedBufferResult, + final TopicPartitionLog tpLog) { + MessageExt msgExt = MessageDecoder.decode(selectMappedBufferResult.getByteBuffer(), false, false); + return asyncPutMessage(selectMappedBufferResult.getByteBuffer(), msgExt, tpLog); + } + + public CompletableFuture asyncPutMessage(final ByteBuffer msgBuffer, + final MessageExt msgExt, final TopicPartitionLog tpLog) { + return asyncPutMessage(msgBuffer, msgExt.getTopic(), msgExt.getQueueId(), + msgExt.getQueueOffset(), msgExt.getMsgId(), msgExt.getKeys(), + MessageExtBrokerInner.tagsString2tagsCode(msgExt.getTags()), msgExt.getStoreTimestamp(), tpLog); + } + + public CompletableFuture asyncPutMessage(final ByteBuffer msgBuffer, + final DispatchRequest dispatchRequest) { + return asyncPutMessage(msgBuffer, dispatchRequest.getTopic(), dispatchRequest.getQueueId(), + dispatchRequest.getConsumeQueueOffset(), dispatchRequest.getUniqKey(), dispatchRequest.getKeys(), + dispatchRequest.getTagsCode(), dispatchRequest.getStoreTimestamp(), current); + } + + public CompletableFuture asyncPutMessage(final ByteBuffer msgBuffer, + final DispatchRequest dispatchRequest, final TopicPartitionLog tpLog) { + return asyncPutMessage(msgBuffer, dispatchRequest.getTopic(), dispatchRequest.getQueueId(), + dispatchRequest.getConsumeQueueOffset(), dispatchRequest.getUniqKey(), dispatchRequest.getKeys(), + dispatchRequest.getTagsCode(), dispatchRequest.getStoreTimestamp(), tpLog); + } + + public CompletableFuture asyncPutMessage(final ByteBuffer msgBuffer, + String topic, int queueId, long queueOffset, String msgId, String keys, long tagsCode, long storeTimestamp, final TopicPartitionLog tpLog) { + + // fix duplicate + if (tpLog.getCQ().getMaxOffsetInQueue() - 1 >= queueOffset) { + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null)); + } + + if (StringUtils.isBlank(keys)) { + log.warn("message {}-{}:{} have no key, will not put in compaction log", topic, queueId, msgId); + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null)); + } + + putMessageLock.lock(); + try { + long beginTime = System.nanoTime(); + + if (tpLog.isEmptyOrCurrentFileFull()) { + try { + tpLog.roll(); + } catch (IOException e) { + log.error("create mapped file or consumerQueue exception: ", e); + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.CREATE_MAPPED_FILE_FAILED, null)); + } + } + + MappedFile mappedFile = tpLog.getLog().getLastMappedFile(); + + CompactionAppendMsgCallback callback = new CompactionAppendMessageCallback(topic, queueId, tagsCode, storeTimestamp, tpLog.getCQ()); + AppendMessageResult result = mappedFile.appendMessage(msgBuffer, callback); + + switch (result.getStatus()) { + case PUT_OK: + break; + case END_OF_FILE: + try { + tpLog.roll(); + } catch (IOException e) { + log.error("create mapped file2 error, topic: {}, msgId: {}", topic, msgId); + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.CREATE_MAPPED_FILE_FAILED, result)); + } + mappedFile = tpLog.getLog().getLastMappedFile(); + result = mappedFile.appendMessage(msgBuffer, callback); + break; + default: + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result)); + } + + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.PUT_OK, result)); + } finally { + putMessageLock.unlock(); + } + } + + private SelectMappedBufferResult getMessage(final long offset, final int size) { + + MappedFile mappedFile = this.getLog().findMappedFileByOffset(offset, offset == 0); + if (mappedFile != null) { + int pos = (int) (offset % compactionLogMappedFileSize); + return mappedFile.selectMappedBuffer(pos, size); + } + return null; + } + + private boolean validateCqUnit(CqUnit cqUnit) { + return cqUnit.getPos() >= 0 + && cqUnit.getSize() > 0 + && cqUnit.getQueueOffset() >= 0 + && cqUnit.getBatchNum() > 0; + } + + public GetMessageResult getMessage(final String group, final String topic, final int queueId, final long offset, + final int maxMsgNums, final int maxTotalMsgSize) { + readMessageLock.lock(); + try { + long beginTime = System.nanoTime(); + + GetMessageStatus status; + long nextBeginOffset = offset; + long minOffset = 0; + long maxOffset = 0; + + GetMessageResult getResult = new GetMessageResult(); + + final long maxOffsetPy = getLog().getMaxOffset(); + + SparseConsumeQueue consumeQueue = getCQ(); + if (consumeQueue != null) { + minOffset = consumeQueue.getMinOffsetInQueue(); + maxOffset = consumeQueue.getMaxOffsetInQueue(); + + if (maxOffset == 0) { + status = GetMessageStatus.NO_MESSAGE_IN_QUEUE; + nextBeginOffset = nextOffsetCorrection(offset, 0); + } else if (offset == maxOffset) { + status = GetMessageStatus.OFFSET_OVERFLOW_ONE; + nextBeginOffset = nextOffsetCorrection(offset, offset); + } else if (offset > maxOffset) { + status = GetMessageStatus.OFFSET_OVERFLOW_BADLY; + if (0 == minOffset) { + nextBeginOffset = nextOffsetCorrection(offset, minOffset); + } else { + nextBeginOffset = nextOffsetCorrection(offset, maxOffset); + } + } else { + + long maxPullSize = Math.max(maxTotalMsgSize, 100); + if (maxPullSize > MAX_PULL_MSG_SIZE) { + log.warn("The max pull size is too large maxPullSize={} topic={} queueId={}", + maxPullSize, topic, queueId); + maxPullSize = MAX_PULL_MSG_SIZE; + } + status = GetMessageStatus.NO_MATCHED_MESSAGE; + long maxPhyOffsetPulling = 0; + int cqFileNum = 0; + + while (getResult.getBufferTotalSize() <= 0 && nextBeginOffset < maxOffset + && cqFileNum++ < this.messageStoreConfig.getTravelCqFileNumWhenGetMessage()) { + ReferredIterator bufferConsumeQueue = consumeQueue.iterateFromOrNext(nextBeginOffset); + + if (bufferConsumeQueue == null) { + status = GetMessageStatus.OFFSET_FOUND_NULL; + nextBeginOffset = nextOffsetCorrection(nextBeginOffset, consumeQueue.rollNextFile(nextBeginOffset)); + log.warn("consumer request topic:{}, offset:{}, minOffset:{}, maxOffset:{}, " + + "but access logic queue failed. correct nextBeginOffset to {}", + topic, offset, minOffset, maxOffset, nextBeginOffset); + break; + } + + try { + long nextPhyFileStartOffset = Long.MIN_VALUE; + while (bufferConsumeQueue.hasNext() && nextBeginOffset < maxOffset) { + CqUnit cqUnit = bufferConsumeQueue.next(); + if (!validateCqUnit(cqUnit)) { + break; + } + long offsetPy = cqUnit.getPos(); + int sizePy = cqUnit.getSize(); + + boolean isInDisk = checkInDiskByCommitOffset(offsetPy, maxOffsetPy); + + if (isTheBatchFull(sizePy, cqUnit.getBatchNum(), maxMsgNums, maxPullSize, + getResult.getBufferTotalSize(), getResult.getMessageCount(), isInDisk)) { + break; + } + + if (getResult.getBufferTotalSize() >= maxPullSize) { + break; + } + + maxPhyOffsetPulling = offsetPy; + + //Be careful, here should before the isTheBatchFull + nextBeginOffset = cqUnit.getQueueOffset() + cqUnit.getBatchNum(); + + if (nextPhyFileStartOffset != Long.MIN_VALUE) { + if (offsetPy < nextPhyFileStartOffset) { + continue; + } + } + + SelectMappedBufferResult selectResult = getMessage(offsetPy, sizePy); + if (null == selectResult) { + if (getResult.getBufferTotalSize() == 0) { + status = GetMessageStatus.MESSAGE_WAS_REMOVING; + } + + // nextPhyFileStartOffset = this.commitLog.rollNextFile(offsetPy); + nextPhyFileStartOffset = rollNextFile(offsetPy); + continue; + } + this.defaultMessageStore.getStoreStatsService().getGetMessageTransferredMsgCount().add(cqUnit.getBatchNum()); + getResult.addMessage(selectResult, cqUnit.getQueueOffset(), cqUnit.getBatchNum()); + status = GetMessageStatus.FOUND; + nextPhyFileStartOffset = Long.MIN_VALUE; + } + } finally { + bufferConsumeQueue.release(); + } + } + + long diff = maxOffsetPy - maxPhyOffsetPulling; + long memory = (long)(StoreUtil.TOTAL_PHYSICAL_MEMORY_SIZE * (this.messageStoreConfig.getAccessMessageInMemoryMaxRatio() / 100.0)); + getResult.setSuggestPullingFromSlave(diff > memory); + } + } else { + status = GetMessageStatus.NO_MATCHED_LOGIC_QUEUE; + nextBeginOffset = nextOffsetCorrection(offset, 0); + } + + if (GetMessageStatus.FOUND == status) { + this.defaultMessageStore.getStoreStatsService().getGetMessageTimesTotalFound().add(getResult.getMessageCount()); + } else { + this.defaultMessageStore.getStoreStatsService().getGetMessageTimesTotalMiss().add(getResult.getMessageCount()); + } + long elapsedTime = this.defaultMessageStore.getSystemClock().now() - beginTime; + this.defaultMessageStore.getStoreStatsService().setGetMessageEntireTimeMax(elapsedTime); + + getResult.setStatus(status); + getResult.setNextBeginOffset(nextBeginOffset); + getResult.setMaxOffset(maxOffset); + getResult.setMinOffset(minOffset); + return getResult; + } finally { + readMessageLock.unlock(); + } + } + + ProcessFileList getCompactionFile() { + List mappedFileList = Lists.newArrayList(getLog().getMappedFiles()); + if (mappedFileList.size() < 2) { + return null; + } + + List toCompactFiles = mappedFileList.subList(0, mappedFileList.size() - 1); + + //exclude the last writing file + List newFiles = Lists.newArrayList(); + for (int i = 0; i < mappedFileList.size() - 1; i++) { + MappedFile mf = mappedFileList.get(i); + long maxQueueOffsetInFile = getCQ().getMaxMsgOffsetFromFile(mf.getFile().getName()); + if (maxQueueOffsetInFile > positionMgr.getOffset(topic, queueId)) { + newFiles.add(mf); + } + } + + if (newFiles.isEmpty()) { + return null; + } + + return new ProcessFileList(toCompactFiles, newFiles); + } + + void compactAndReplace(ProcessFileList compactFiles) throws Throwable { + if (compactFiles == null || compactFiles.isEmpty()) { + return; + } + + long startTime = System.nanoTime(); + OffsetMap offsetMap = getOffsetMap(compactFiles.newFiles); + compaction(compactFiles.toCompactFiles, offsetMap); + replaceFiles(compactFiles.toCompactFiles, current, compacting); + positionMgr.setOffset(topic, queueId, offsetMap.lastOffset); + positionMgr.persist(); + compacting.clean(false, false); + log.info("this compaction elapsed {} milliseconds", + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)); + + } + + void doCompaction() { + if (!state.compareAndSet(State.NORMAL, State.COMPACTING)) { + log.warn("compactionLog state is {}, skip this time", state.get()); + return; + } + + try { + compactAndReplace(getCompactionFile()); + } catch (Throwable e) { + log.error("do compaction exception: ", e); + } + state.compareAndSet(State.COMPACTING, State.NORMAL); + } + + protected OffsetMap getOffsetMap(List mappedFileList) throws NoSuchAlgorithmException, DigestException { + OffsetMap offsetMap = new OffsetMap(offsetMapMemorySize); + + for (MappedFile mappedFile : mappedFileList) { + Iterator iterator = mappedFile.iterator(0); + while (iterator.hasNext()) { + SelectMappedBufferResult smb = null; + try { + smb = iterator.next(); + //decode bytebuffer + MessageExt msg = MessageDecoder.decode(smb.getByteBuffer(), true, false); + if (msg != null) { + ////get key & offset and put to offsetMap + if (msg.getQueueOffset() > positionMgr.getOffset(topic, queueId)) { + offsetMap.put(msg.getKeys(), msg.getQueueOffset()); + } + } else { + // msg is null indicate that file is end + break; + } + } catch (DigestException e) { + log.error("offsetMap put exception: ", e); + throw e; + } finally { + if (smb != null) { + smb.release(); + } + } + } + } + return offsetMap; + } + + protected void putEndMessage(MappedFileQueue mappedFileQueue) { + MappedFile lastFile = mappedFileQueue.getLastMappedFile(); + if (!lastFile.isFull()) { + lastFile.appendMessage(ByteBuffer.allocate(0), endMsgCallback); + } + } + + protected void compaction(List mappedFileList, OffsetMap offsetMap) throws DigestException { + compacting = new TopicPartitionLog(this, COMPACTING_SUB_FOLDER); + + for (MappedFile mappedFile : mappedFileList) { + Iterator iterator = mappedFile.iterator(0); + while (iterator.hasNext()) { + SelectMappedBufferResult smb = null; + try { + smb = iterator.next(); + MessageExt msgExt = MessageDecoder.decode(smb.getByteBuffer(), true, true); + if (msgExt == null) { + // file end + break; + } else { + checkAndPutMessage(smb, msgExt, offsetMap, compacting); + } + } finally { + if (smb != null) { + smb.release(); + } + } + } + } + putEndMessage(compacting.getLog()); + } + + protected void replaceFiles(List mappedFileList, TopicPartitionLog current, + TopicPartitionLog newLog) { + + MappedFileQueue dest = current.getLog(); + MappedFileQueue src = newLog.getLog(); + + long beginTime = System.nanoTime(); +// List fileNameToReplace = mappedFileList.stream() +// .map(m -> m.getFile().getName()) +// .collect(Collectors.toList()); + + List fileNameToReplace = dest.getMappedFiles().stream() + .filter(mappedFileList::contains) + .map(mf -> mf.getFile().getName()) + .collect(Collectors.toList()); + + mappedFileList.forEach(MappedFile::renameToDelete); + + src.getMappedFiles().forEach(mappedFile -> { + try { + mappedFile.flush(0); + mappedFile.moveToParent(); + } catch (IOException e) { + log.error("move file {} to parent directory exception: ", mappedFile.getFileName()); + } + }); + + dest.getMappedFiles().stream() + .filter(m -> !mappedFileList.contains(m)) + .forEach(m -> src.getMappedFiles().add(m)); + + readMessageLock.lock(); + try { + mappedFileList.forEach(mappedFile -> mappedFile.destroy(1000)); + + dest.getMappedFiles().clear(); + dest.getMappedFiles().addAll(src.getMappedFiles()); + src.getMappedFiles().clear(); + + replaceCqFiles(getCQ(), newLog.getCQ(), fileNameToReplace); + + log.info("replace file elapsed {} milliseconds", + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - beginTime)); + } finally { + readMessageLock.unlock(); + } + } + + protected void replaceCqFiles(SparseConsumeQueue currentBcq, SparseConsumeQueue compactionBcq, + List fileNameToReplace) { + long beginTime = System.nanoTime(); + + MappedFileQueue currentMq = currentBcq.getMappedFileQueue(); + MappedFileQueue compactMq = compactionBcq.getMappedFileQueue(); + List fileListToDelete = currentMq.getMappedFiles().stream().filter(m -> + fileNameToReplace.contains(m.getFile().getName())).collect(Collectors.toList()); + + fileListToDelete.forEach(MappedFile::renameToDelete); + compactMq.getMappedFiles().forEach(mappedFile -> { + try { + mappedFile.flush(0); + mappedFile.moveToParent(); + } catch (IOException e) { + log.error("move consume queue file {} to parent directory exception: ", mappedFile.getFileName(), e); + } + }); + + currentMq.getMappedFiles().stream() + .filter(m -> !fileListToDelete.contains(m)) + .forEach(m -> compactMq.getMappedFiles().add(m)); + + fileListToDelete.forEach(mappedFile -> mappedFile.destroy(1000)); + + currentMq.getMappedFiles().clear(); + currentMq.getMappedFiles().addAll(compactMq.getMappedFiles()); + compactMq.getMappedFiles().clear(); + + currentBcq.refresh(); + log.info("replace consume queue file elapsed {} millsecs.", + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - beginTime)); + } + + public MappedFileQueue getLog() { + return current.mappedFileQueue; + } + + public SparseConsumeQueue getCQ() { + return current.consumeQueue; + } + +// public SparseConsumeQueue getCompactionScq() { +// return compactionScq; +// } + + public void flush(int flushLeastPages) { + this.flushLog(flushLeastPages); + this.flushCQ(flushLeastPages); + } + + public void flushLog(int flushLeastPages) { + getLog().flush(flushLeastPages); + } + + public void flushCQ(int flushLeastPages) { + getCQ().flush(flushLeastPages); + } + + static class CompactionAppendEndMsgCallback implements CompactionAppendMsgCallback { + @Override + public AppendMessageResult doAppend(ByteBuffer bbDest, long fileFromOffset, int maxBlank, ByteBuffer bbSrc) { + ByteBuffer endInfo = ByteBuffer.allocate(END_FILE_MIN_BLANK_LENGTH); + endInfo.putInt(maxBlank); + endInfo.putInt(BLANK_MAGIC_CODE); + return new AppendMessageResult(AppendMessageStatus.END_OF_FILE, + fileFromOffset + bbDest.position(), maxBlank, System.currentTimeMillis()); + } + } + + static class CompactionAppendMessageCallback implements CompactionAppendMsgCallback { + private final String topic; + private final int queueId; + private final long tagsCode; + private final long storeTimestamp; + + private final SparseConsumeQueue bcq; + public CompactionAppendMessageCallback(MessageExt msgExt, SparseConsumeQueue bcq) { + this.topic = msgExt.getTopic(); + this.queueId = msgExt.getQueueId(); + this.tagsCode = MessageExtBrokerInner.tagsString2tagsCode(msgExt.getTags()); + this.storeTimestamp = msgExt.getStoreTimestamp(); + + this.bcq = bcq; + } + public CompactionAppendMessageCallback(String topic, int queueId, long tagsCode, long storeTimestamp, SparseConsumeQueue bcq) { + this.topic = topic; + this.queueId = queueId; + this.tagsCode = tagsCode; + this.storeTimestamp = storeTimestamp; + + this.bcq = bcq; + } + + @Override + public AppendMessageResult doAppend(ByteBuffer bbDest, long fileFromOffset, int maxBlank, ByteBuffer bbSrc) { + + final int msgLen = bbSrc.getInt(0); + MappedFile bcqMappedFile = bcq.getMappedFileQueue().getLastMappedFile(); + if (bcqMappedFile.getWrotePosition() + BatchConsumeQueue.CQ_STORE_UNIT_SIZE >= bcqMappedFile.getFileSize() + || (msgLen + END_FILE_MIN_BLANK_LENGTH) > maxBlank) { //bcq will full or log will full + + bcq.putEndPositionInfo(bcqMappedFile); + + bbDest.putInt(maxBlank); + bbDest.putInt(BLANK_MAGIC_CODE); + return new AppendMessageResult(AppendMessageStatus.END_OF_FILE, + fileFromOffset + bbDest.position(), maxBlank, storeTimestamp); + } + + //get logic offset and physical offset + int logicOffsetPos = 4 + 4 + 4 + 4 + 4; + long logicOffset = bbSrc.getLong(logicOffsetPos); + int destPos = bbDest.position(); + long physicalOffset = fileFromOffset + bbDest.position(); + bbSrc.rewind(); + bbSrc.limit(msgLen); + bbDest.put(bbSrc); + bbDest.putLong(destPos + logicOffsetPos + 8, physicalOffset); //replace physical offset + + boolean result = bcq.putBatchMessagePositionInfo(physicalOffset, msgLen, + tagsCode, storeTimestamp, logicOffset, (short)1); + if (!result) { + log.error("put message {}-{} position info failed", topic, queueId); + } + return new AppendMessageResult(AppendMessageStatus.PUT_OK, physicalOffset, msgLen, storeTimestamp); + } + } + + static class OffsetMap { + private final ByteBuffer dataBytes; + private final int capacity; + private final int entrySize; + private int entryNum; + private final MessageDigest digest; + private final int hashSize; + private long lastOffset; + private final byte[] hash1; + private final byte[] hash2; + + public OffsetMap(int memorySize) throws NoSuchAlgorithmException { + this(memorySize, MessageDigest.getInstance("MD5")); + } + + public OffsetMap(int memorySize, MessageDigest digest) { + this.hashSize = digest.getDigestLength(); + this.entrySize = hashSize + (Long.SIZE / Byte.SIZE); + this.capacity = Math.max(memorySize / entrySize, 100); + this.dataBytes = ByteBuffer.allocate(capacity * entrySize); + this.hash1 = new byte[hashSize]; + this.hash2 = new byte[hashSize]; + this.entryNum = 0; + this.digest = digest; + } + + public void put(String key, final long offset) throws DigestException { + if (entryNum >= capacity) { + throw new IllegalArgumentException("offset map is full"); + } + hashInto(key, hash1); + int tryNum = 0; + int index = indexOf(hash1, tryNum); + while (!isEmpty(index)) { + dataBytes.position(index); + dataBytes.get(hash2); + if (Arrays.equals(hash1, hash2)) { + dataBytes.putLong(offset); + lastOffset = offset; + return; + } + tryNum++; + index = indexOf(hash1, tryNum); + } + + dataBytes.position(index); + dataBytes.put(hash1); + dataBytes.putLong(offset); + lastOffset = offset; + entryNum += 1; + } + + public long get(String key) throws DigestException { + hashInto(key, hash1); + int tryNum = 0; + int maxTryNum = entryNum + hashSize - 4; + int index = 0; + do { + if (tryNum >= maxTryNum) { + return -1L; + } + index = indexOf(hash1, tryNum); + dataBytes.position(index); + if (isEmpty(index)) { + return -1L; + } + dataBytes.get(hash2); + tryNum++; + } while (!Arrays.equals(hash1, hash2)); + return dataBytes.getLong(); + } + + public long getLastOffset() { + return lastOffset; + } + + private boolean isEmpty(int pos) { + return dataBytes.getLong(pos) == 0 + && dataBytes.getLong(pos + 8) == 0 + && dataBytes.getLong(pos + 16) == 0; + } + + private int indexOf(byte[] hash, int tryNum) { + int index = readInt(hash, Math.min(tryNum, hashSize - 4)) + Math.max(0, tryNum - hashSize + 4); + int entry = Math.abs(index) % capacity; + return entry * entrySize; + } + + private void hashInto(String key, byte[] buf) throws DigestException { + digest.update(key.getBytes(StandardCharsets.UTF_8)); + digest.digest(buf, 0, hashSize); + } + + private int readInt(byte[] buf, int offset) { + return ((buf[offset] & 0xFF) << 24) | + ((buf[offset + 1] & 0xFF) << 16) | + ((buf[offset + 2] & 0xFF) << 8) | + ((buf[offset + 3] & 0xFF)); + } + } + + static class TopicPartitionLog { + MappedFileQueue mappedFileQueue; + SparseConsumeQueue consumeQueue; + + public TopicPartitionLog(CompactionLog compactionLog) { + this(compactionLog, null); + } + public TopicPartitionLog(CompactionLog compactionLog, String subFolder) { + if (StringUtils.isBlank(subFolder)) { + mappedFileQueue = new MappedFileQueue(compactionLog.compactionLogFilePath, + compactionLog.compactionLogMappedFileSize, null); + consumeQueue = new SparseConsumeQueue(compactionLog.topic, compactionLog.queueId, + compactionLog.compactionCqFilePath, compactionLog.compactionCqMappedFileSize, + compactionLog.defaultMessageStore); + } else { + mappedFileQueue = new MappedFileQueue(compactionLog.compactionLogFilePath + File.separator + subFolder, + compactionLog.compactionLogMappedFileSize, null); + consumeQueue = new SparseConsumeQueue(compactionLog.topic, compactionLog.queueId, + compactionLog.compactionCqFilePath, compactionLog.compactionCqMappedFileSize, + compactionLog.defaultMessageStore, subFolder); + } + } + + public void shutdown() { + mappedFileQueue.shutdown(1000 * 30); + consumeQueue.getMappedFileQueue().shutdown(1000 * 30); + } + + public void init(boolean exitOk) throws IOException, RuntimeException { + if (!mappedFileQueue.load()) { + shutdown(); + throw new IOException("load log exception"); + } + + if (!consumeQueue.load()) { + shutdown(); + throw new IOException("load consume queue exception"); + } + + try { + consumeQueue.recover(); + recover(); + sanityCheck(); + } catch (Exception e) { + shutdown(); + throw e; + } + } + + private void recover() { + long maxCqPhysicOffset = consumeQueue.getMaxPhyOffsetInLog(); + log.info("{}:{} max physical offset in compaction log is {}", + consumeQueue.getTopic(), consumeQueue.getQueueId(), maxCqPhysicOffset); + if (maxCqPhysicOffset > 0) { + this.mappedFileQueue.setFlushedWhere(maxCqPhysicOffset); + this.mappedFileQueue.setCommittedWhere(maxCqPhysicOffset); + this.mappedFileQueue.truncateDirtyFiles(maxCqPhysicOffset); + } + } + + void sanityCheck() throws RuntimeException { + List mappedFileList = mappedFileQueue.getMappedFiles(); + for (MappedFile file : mappedFileList) { + if (!consumeQueue.containsOffsetFile(Long.parseLong(file.getFile().getName()))) { + throw new RuntimeException("log file mismatch with consumeQueue file " + file.getFileName()); + } + } + + List cqMappedFileList = consumeQueue.getMappedFileQueue().getMappedFiles(); + for (MappedFile file: cqMappedFileList) { + if (mappedFileList.stream().noneMatch(m -> Objects.equals(m.getFile().getName(), file.getFile().getName()))) { + throw new RuntimeException("consumeQueue file mismatch with log file " + file.getFileName()); + } + } + } + + public synchronized void roll() throws IOException { + MappedFile mappedFile = mappedFileQueue.getLastMappedFile(0); + if (mappedFile == null) { + throw new IOException("create new file error"); + } + long baseOffset = mappedFile.getFileFromOffset(); + MappedFile cqFile = consumeQueue.createFile(baseOffset); + if (cqFile == null) { + mappedFile.destroy(1000); + mappedFileQueue.getMappedFiles().remove(mappedFile); + throw new IOException("create new consumeQueue file error"); + } + } + + public synchronized void roll(int baseOffset) throws IOException { + + MappedFile mappedFile = mappedFileQueue.tryCreateMappedFile(baseOffset); + if (mappedFile == null) { + throw new IOException("create new file error"); + } + + MappedFile cqFile = consumeQueue.createFile(baseOffset); + if (cqFile == null) { + mappedFile.destroy(1000); + mappedFileQueue.getMappedFiles().remove(mappedFile); + throw new IOException("create new consumeQueue file error"); + } + } + + public boolean isEmptyOrCurrentFileFull() { + return mappedFileQueue.isEmptyOrCurrentFileFull() || + consumeQueue.getMappedFileQueue().isEmptyOrCurrentFileFull(); + } + + public void clean(MappedFileQueue mappedFileQueue) throws IOException { + for (MappedFile mf : mappedFileQueue.getMappedFiles()) { + if (mf.getFile().exists()) { + log.error("directory {} with {} not empty.", mappedFileQueue.getStorePath(), mf.getFileName()); + throw new IOException("directory " + mappedFileQueue.getStorePath() + " not empty."); + } + } + + mappedFileQueue.destroy(); + } + + public void clean(boolean forceCleanLog, boolean forceCleanCq) throws IOException { + //clean and delete sub_folder + if (forceCleanLog) { + mappedFileQueue.destroy(); + } else { + clean(mappedFileQueue); + } + + if (forceCleanCq) { + consumeQueue.getMappedFileQueue().destroy(); + } else { + clean(consumeQueue.getMappedFileQueue()); + } + } + + public MappedFileQueue getLog() { + return mappedFileQueue; + } + + public SparseConsumeQueue getCQ() { + return consumeQueue; + } + } + + enum State { + NORMAL, + INITIALIZING, + COMPACTING, + } + + static class ProcessFileList { + List newFiles; + List toCompactFiles; + public ProcessFileList(List toCompactFiles, List newFiles) { + this.toCompactFiles = toCompactFiles; + this.newFiles = newFiles; + } + + boolean isEmpty() { + return CollectionUtils.isEmpty(newFiles) || CollectionUtils.isEmpty(toCompactFiles); + } + } + +} diff --git a/store/src/main/java/org/apache/rocketmq/store/kv/CompactionPositionMgr.java b/store/src/main/java/org/apache/rocketmq/store/kv/CompactionPositionMgr.java new file mode 100644 index 00000000000..4181b34b8b4 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/kv/CompactionPositionMgr.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.kv; + +import org.apache.rocketmq.common.ConfigManager; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +import java.io.File; +import java.util.concurrent.ConcurrentHashMap; + +public class CompactionPositionMgr extends ConfigManager { + + public static final String CHECKPOINT_FILE = "position-checkpoint"; + + private transient String compactionPath; + private transient String checkpointFileName; + + private ConcurrentHashMap queueOffsetMap = new ConcurrentHashMap<>(); + + private CompactionPositionMgr() { + + } + + public CompactionPositionMgr(final String compactionPath) { + this.compactionPath = compactionPath; + this.checkpointFileName = compactionPath + File.separator + CHECKPOINT_FILE; + this.load(); + } + + public void setOffset(String topic, int queueId, final long offset) { + queueOffsetMap.put(topic + "_" + queueId, offset); + } + + public long getOffset(String topic, int queueId) { + return queueOffsetMap.getOrDefault(topic + "_" + queueId, -1L); + } + + public boolean isEmpty() { + return queueOffsetMap.isEmpty(); + } + + public boolean isCompaction(String topic, int queueId, long offset) { + return getOffset(topic, queueId) > offset; + } + + @Override + public String configFilePath() { + return checkpointFileName; + } + + @Override + public String encode() { + return this.encode(false); + } + + @Override + public String encode(boolean prettyFormat) { + return RemotingSerializable.toJson(this, prettyFormat); + } + + @Override + public void decode(String jsonString) { + if (jsonString != null) { + CompactionPositionMgr obj = RemotingSerializable.fromJson(jsonString, CompactionPositionMgr.class); + if (obj != null) { + this.queueOffsetMap = obj.queueOffsetMap; + } + } + } + + public ConcurrentHashMap getQueueOffsetMap() { + return queueOffsetMap; + } + + public void setQueueOffsetMap(ConcurrentHashMap queueOffsetMap) { + this.queueOffsetMap = queueOffsetMap; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/kv/CompactionService.java b/store/src/main/java/org/apache/rocketmq/store/kv/CompactionService.java new file mode 100644 index 00000000000..5e07a50082b --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/kv/CompactionService.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.kv; + +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.attribute.CleanupPolicy; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.CleanupPolicyUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.CommitLog; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.DispatchRequest; +import org.apache.rocketmq.store.SelectMappedBufferResult; + +import java.util.Objects; +import java.util.Optional; + +public class CompactionService { + private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + + private final CompactionStore compactionStore; + private final DefaultMessageStore defaultMessageStore; + private final CommitLog commitLog; + + public CompactionService(CommitLog commitLog, DefaultMessageStore messageStore, CompactionStore compactionStore) { + this.commitLog = commitLog; + this.defaultMessageStore = messageStore; + this.compactionStore = compactionStore; + } + + public void putRequest(DispatchRequest request) { + if (request == null) { + return; + } + + String topic = request.getTopic(); + Optional topicConfig = defaultMessageStore.getTopicConfig(topic); + CleanupPolicy policy = CleanupPolicyUtils.getDeletePolicy(topicConfig); + //check request topic flag + if (Objects.equals(policy, CleanupPolicy.COMPACTION)) { + SelectMappedBufferResult smr = null; + try { + smr = commitLog.getData(request.getCommitLogOffset()); + if (smr != null) { + compactionStore.doDispatch(request, smr); + } + } catch (Exception e) { + log.error("putMessage into {}:{} compactionLog exception: ", request.getTopic(), request.getQueueId(), e); + } finally { + if (smr != null) { + smr.release(); + } + } + } // else skip if message isn't compaction + } + + public boolean load(boolean exitOK) { + try { + compactionStore.load(exitOK); + return true; + } catch (Exception e) { + log.error("load compaction store error ", e); + return false; + } + } + +// @Override +// public void start() { +// compactionStore.load(); +// super.start(); +// } + + public void shutdown() { + compactionStore.shutdown(); + } + + public void updateMasterAddress(String addr) { + compactionStore.updateMasterAddress(addr); + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/kv/CompactionStore.java b/store/src/main/java/org/apache/rocketmq/store/kv/CompactionStore.java new file mode 100644 index 00000000000..639084fa2d8 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/kv/CompactionStore.java @@ -0,0 +1,246 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.kv; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.attribute.CleanupPolicy; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.CleanupPolicyUtils; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.DispatchRequest; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.config.MessageStoreConfig; + +public class CompactionStore { + + public static final String COMPACTION_DIR = "compaction"; + public static final String COMPACTION_LOG_DIR = "compactionLog"; + public static final String COMPACTION_CQ_DIR = "compactionCq"; + + private final String compactionPath; + private final String compactionLogPath; + private final String compactionCqPath; + private final DefaultMessageStore defaultMessageStore; + private final CompactionPositionMgr positionMgr; + private final ConcurrentHashMap compactionLogTable; + private final ScheduledExecutorService compactionSchedule; + private final int scanInterval = 30000; + private final int compactionInterval; + private final int compactionThreadNum; + private final int offsetMapSize; + private String masterAddr; + + private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + + public CompactionStore(DefaultMessageStore defaultMessageStore) { + this.defaultMessageStore = defaultMessageStore; + this.compactionLogTable = new ConcurrentHashMap<>(); + MessageStoreConfig config = defaultMessageStore.getMessageStoreConfig(); + String storeRootPath = config.getStorePathRootDir(); + this.compactionPath = Paths.get(storeRootPath, COMPACTION_DIR).toString(); + this.compactionLogPath = Paths.get(compactionPath, COMPACTION_LOG_DIR).toString(); + this.compactionCqPath = Paths.get(compactionPath, COMPACTION_CQ_DIR).toString(); + this.positionMgr = new CompactionPositionMgr(compactionPath); + this.compactionThreadNum = Math.min(Runtime.getRuntime().availableProcessors(), Math.max(1, config.getCompactionThreadNum())); + + this.compactionSchedule = ThreadUtils.newScheduledThreadPool(this.compactionThreadNum, + new ThreadFactoryImpl("compactionSchedule_")); + this.offsetMapSize = config.getMaxOffsetMapSize() / compactionThreadNum; + + this.compactionInterval = defaultMessageStore.getMessageStoreConfig().getCompactionScheduleInternal(); + } + + public void load(boolean exitOk) throws Exception { + File logRoot = new File(compactionLogPath); + File[] fileTopicList = logRoot.listFiles(); + if (fileTopicList != null) { + for (File fileTopic : fileTopicList) { + if (!fileTopic.isDirectory()) { + continue; + } + + File[] fileQueueIdList = fileTopic.listFiles(); + if (fileQueueIdList != null) { + for (File fileQueueId : fileQueueIdList) { + if (!fileQueueId.isDirectory()) { + continue; + } + try { + String topic = fileTopic.getName(); + int queueId = Integer.parseInt(fileQueueId.getName()); + + if (Files.isDirectory(Paths.get(compactionCqPath, topic, String.valueOf(queueId)))) { + loadAndGetClog(topic, queueId); + } else { + log.error("{}:{} compactionLog mismatch with compactionCq", topic, queueId); + } + } catch (Exception e) { + log.error("load compactionLog {}:{} exception: ", + fileTopic.getName(), fileQueueId.getName(), e); + throw new Exception("load compactionLog " + fileTopic.getName() + + ":" + fileQueueId.getName() + " exception: " + e.getMessage()); + } + } + } + } + } + log.info("compactionStore {}:{} load completed.", compactionLogPath, compactionCqPath); + + compactionSchedule.scheduleWithFixedDelay(this::scanAllTopicConfig, scanInterval, scanInterval, TimeUnit.MILLISECONDS); + log.info("loop to scan all topicConfig with fixed delay {}ms", scanInterval); + } + + private void scanAllTopicConfig() { + log.info("start to scan all topicConfig"); + try { + Iterator> iterator = defaultMessageStore.getTopicConfigs().entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry it = iterator.next(); + TopicConfig topicConfig = it.getValue(); + CleanupPolicy policy = CleanupPolicyUtils.getDeletePolicy(Optional.ofNullable(topicConfig)); + //check topic flag + if (Objects.equals(policy, CleanupPolicy.COMPACTION)) { + for (int queueId = 0; queueId < topicConfig.getWriteQueueNums(); queueId++) { + loadAndGetClog(it.getKey(), queueId); + } + } + } + } catch (Throwable ignore) { + // ignore + } + log.info("scan all topicConfig over"); + } + + private CompactionLog loadAndGetClog(String topic, int queueId) { + CompactionLog clog = compactionLogTable.compute(topic + "_" + queueId, (k, v) -> { + if (v == null) { + try { + v = new CompactionLog(defaultMessageStore, this, topic, queueId); + v.load(true); + int randomDelay = 1000 + new Random(System.currentTimeMillis()).nextInt(compactionInterval); + compactionSchedule.scheduleWithFixedDelay(v::doCompaction, compactionInterval + randomDelay, compactionInterval + randomDelay, TimeUnit.MILLISECONDS); + } catch (IOException e) { + log.error("create compactionLog exception: ", e); + return null; + } + } + return v; + }); + return clog; + } + + public void putMessage(String topic, int queueId, SelectMappedBufferResult smr) throws Exception { + CompactionLog clog = loadAndGetClog(topic, queueId); + + if (clog != null) { + clog.asyncPutMessage(smr); + } + } + + public void doDispatch(DispatchRequest dispatchRequest, SelectMappedBufferResult smr) throws Exception { + CompactionLog clog = loadAndGetClog(dispatchRequest.getTopic(), dispatchRequest.getQueueId()); + + if (clog != null) { + clog.asyncPutMessage(smr.getByteBuffer(), dispatchRequest); + } + } + + public GetMessageResult getMessage(final String group, final String topic, final int queueId, final long offset, + final int maxMsgNums, final int maxTotalMsgSize) { + CompactionLog log = compactionLogTable.get(topic + "_" + queueId); + if (log == null) { + return GetMessageResult.NO_MATCH_LOGIC_QUEUE; + } else { + return log.getMessage(group, topic, queueId, offset, maxMsgNums, maxTotalMsgSize); + } + + } + + public void flush(int flushLeastPages) { + compactionLogTable.values().forEach(log -> log.flush(flushLeastPages)); + } + + public void flushLog(int flushLeastPages) { + compactionLogTable.values().forEach(log -> log.flushLog(flushLeastPages)); + } + + public void flushCQ(int flushLeastPages) { + compactionLogTable.values().forEach(log -> log.flushCQ(flushLeastPages)); + } + + public void updateMasterAddress(String addr) { + this.masterAddr = addr; + } + + public void shutdown() { + // close the thread pool first + compactionSchedule.shutdown(); + try { + if (!compactionSchedule.awaitTermination(1000, TimeUnit.MILLISECONDS)) { + List droppedTasks = compactionSchedule.shutdownNow(); + log.warn("compactionSchedule was abruptly shutdown. {} tasks will not be executed.", droppedTasks.size()); + } + } catch (InterruptedException e) { + log.warn("wait compaction schedule shutdown interrupted. "); + } + this.flush(0); + positionMgr.persist(); + } + + public ScheduledExecutorService getCompactionSchedule() { + return compactionSchedule; + } + + public String getCompactionLogPath() { + return compactionLogPath; + } + + public String getCompactionCqPath() { + return compactionCqPath; + } + + public CompactionPositionMgr getPositionMgr() { + return positionMgr; + } + + public int getOffsetMapSize() { + return offsetMapSize; + } + + public String getMasterAddr() { + return masterAddr; + } + +} diff --git a/store/src/main/java/org/apache/rocketmq/store/kv/MessageFetcher.java b/store/src/main/java/org/apache/rocketmq/store/kv/MessageFetcher.java new file mode 100644 index 00000000000..183f0667370 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/kv/MessageFetcher.java @@ -0,0 +1,212 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.kv; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; + +import java.io.IOException; +import java.util.function.BiFunction; + +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.sysflag.PullSysFlag; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.RemotingClient; +import org.apache.rocketmq.remoting.exception.RemotingCommandException; +import org.apache.rocketmq.remoting.exception.RemotingConnectException; +import org.apache.rocketmq.remoting.exception.RemotingSendRequestException; +import org.apache.rocketmq.remoting.exception.RemotingTimeoutException; +import org.apache.rocketmq.remoting.netty.NettyClientConfig; +import org.apache.rocketmq.remoting.netty.NettyRemotingClient; +import org.apache.rocketmq.remoting.protocol.LanguageCode; +import org.apache.rocketmq.remoting.protocol.RemotingCommand; +import org.apache.rocketmq.remoting.protocol.RequestCode; +import org.apache.rocketmq.remoting.protocol.ResponseCode; +import org.apache.rocketmq.remoting.protocol.header.HeartbeatRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PullMessageRequestHeader; +import org.apache.rocketmq.remoting.protocol.header.PullMessageResponseHeader; +import org.apache.rocketmq.remoting.protocol.header.UnregisterClientRequestHeader; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumeType; +import org.apache.rocketmq.remoting.protocol.heartbeat.ConsumerData; +import org.apache.rocketmq.remoting.protocol.heartbeat.HeartbeatData; +import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; +import org.apache.rocketmq.remoting.protocol.heartbeat.SubscriptionData; + +public class MessageFetcher implements AutoCloseable { + + private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + private final RemotingClient client; + + public MessageFetcher() { + NettyClientConfig nettyClientConfig = new NettyClientConfig(); + nettyClientConfig.setUseTLS(false); + this.client = new NettyRemotingClient(nettyClientConfig); + this.client.start(); + } + + @Override + public void close() throws IOException { + this.client.shutdown(); + } + + private PullMessageRequestHeader createPullMessageRequest(String topic, int queueId, long queueOffset, long subVersion) { + int sysFlag = PullSysFlag.buildSysFlag(false, false, false, false, true); + + PullMessageRequestHeader requestHeader = new PullMessageRequestHeader(); + requestHeader.setConsumerGroup(getConsumerGroup(topic, queueId)); + requestHeader.setTopic(topic); + requestHeader.setQueueId(queueId); + requestHeader.setQueueOffset(queueOffset); + requestHeader.setMaxMsgNums(10); + requestHeader.setSysFlag(sysFlag); + requestHeader.setCommitOffset(0L); + requestHeader.setSuspendTimeoutMillis(20_000L); +// requestHeader.setSubscription(subExpression); + requestHeader.setSubVersion(subVersion); + requestHeader.setMaxMsgBytes(Integer.MAX_VALUE); +// requestHeader.setExpressionType(expressionType); + return requestHeader; + } + + private String getConsumerGroup(String topic, int queueId) { + return String.join("-", topic, String.valueOf(queueId), "pull", "group"); + } + + private String getClientId() { + return String.join("@", NetworkUtil.getLocalAddress(), "compactionIns", "compactionUnit"); + } + + private boolean prepare(String masterAddr, String topic, String groupName, long subVersion) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException { + HeartbeatData heartbeatData = new HeartbeatData(); + + heartbeatData.setClientID(getClientId()); + + ConsumerData consumerData = new ConsumerData(); + consumerData.setGroupName(groupName); + consumerData.setConsumeType(ConsumeType.CONSUME_ACTIVELY); + consumerData.setMessageModel(MessageModel.CLUSTERING); + consumerData.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); +// consumerData.setSubscriptionDataSet(); + SubscriptionData subscriptionData = new SubscriptionData(); + subscriptionData.setTopic(topic); + subscriptionData.setSubString(SubscriptionData.SUB_ALL); + subscriptionData.setSubVersion(subVersion); + consumerData.setSubscriptionDataSet(Sets.newHashSet(subscriptionData)); + + heartbeatData.getConsumerDataSet().add(consumerData); + + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.HEART_BEAT, new HeartbeatRequestHeader()); + request.setLanguage(LanguageCode.JAVA); + request.setBody(heartbeatData.encode()); + + RemotingCommand response = client.invokeSync(masterAddr, request, 1000 * 30L); + if (response != null && response.getCode() == ResponseCode.SUCCESS) { + return true; + } + return false; + } + + private boolean pullDone(String masterAddr, String groupName) + throws RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException, InterruptedException { + UnregisterClientRequestHeader requestHeader = new UnregisterClientRequestHeader(); + requestHeader.setClientID(getClientId()); + requestHeader.setProducerGroup(""); + requestHeader.setConsumerGroup(groupName); + RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UNREGISTER_CLIENT, requestHeader); + + RemotingCommand response = client.invokeSync(masterAddr, request, 1000 * 30L); + if (response != null && response.getCode() == ResponseCode.SUCCESS) { + return true; + } + return false; + } + + private boolean stopPull(long currPullOffset, long endOffset) { + return currPullOffset >= endOffset && endOffset != -1; + } + + public void pullMessageFromMaster(String topic, int queueId, long endOffset, String masterAddr, + BiFunction responseHandler) throws Exception { + long currentPullOffset = 0; + + try { + long subVersion = System.currentTimeMillis(); + String groupName = getConsumerGroup(topic, queueId); + if (!prepare(masterAddr, topic, groupName, subVersion)) { + log.error("{}:{} prepare to {} pull message failed", topic, queueId, masterAddr); + throw new RemotingCommandException(topic + ":" + queueId + " prepare to " + masterAddr + " pull message failed"); + } + + boolean noNewMsg = false; + boolean keepPull = true; +// PullMessageRequestHeader requestHeader = createPullMessageRequest(topic, queueId, subVersion, currentPullOffset); + while (!stopPull(currentPullOffset, endOffset)) { +// requestHeader.setQueueOffset(currentPullOffset); + PullMessageRequestHeader requestHeader = createPullMessageRequest(topic, queueId, currentPullOffset, subVersion); + + RemotingCommand + request = RemotingCommand.createRequestCommand(RequestCode.LITE_PULL_MESSAGE, requestHeader); + RemotingCommand response = client.invokeSync(masterAddr, request, 1000 * 30L); + + PullMessageResponseHeader responseHeader = + (PullMessageResponseHeader) response.decodeCommandCustomHeader(PullMessageResponseHeader.class); + if (responseHeader == null) { + log.error("{}:{} pull message responseHeader is null", topic, queueId); + throw new RemotingCommandException(topic + ":" + queueId + " pull message responseHeader is null"); + } + + switch (response.getCode()) { + case ResponseCode.SUCCESS: + long curOffset = responseHeader.getNextBeginOffset() - 1; + keepPull = responseHandler.apply(curOffset, response); + currentPullOffset = responseHeader.getNextBeginOffset(); + break; + case ResponseCode.PULL_NOT_FOUND: // NO_NEW_MSG, need break loop + log.info("PULL_NOT_FOUND, topic:{}, queueId:{}, pullOffset:{},", + topic, queueId, currentPullOffset); + noNewMsg = true; + break; + case ResponseCode.PULL_RETRY_IMMEDIATELY: + log.info("PULL_RETRY_IMMEDIATE, topic:{}, queueId:{}, pullOffset:{},", + topic, queueId, currentPullOffset); + break; + case ResponseCode.PULL_OFFSET_MOVED: + log.info("PULL_OFFSET_MOVED, topic:{}, queueId:{}, pullOffset:{},", + topic, queueId, currentPullOffset); + break; + default: + log.warn("Pull Message error, response code: {}, remark: {}", + response.getCode(), response.getRemark()); + } + + if (noNewMsg || !keepPull) { + break; + } + } + pullDone(masterAddr, groupName); + } finally { + if (client != null) { + client.closeChannels(Lists.newArrayList(masterAddr)); + } + } + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/logfile/AbstractMappedFile.java b/store/src/main/java/org/apache/rocketmq/store/logfile/AbstractMappedFile.java new file mode 100644 index 00000000000..28d443cddec --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/logfile/AbstractMappedFile.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.logfile; + +import org.apache.rocketmq.store.ReferenceResource; + +public abstract class AbstractMappedFile extends ReferenceResource implements MappedFile { +} diff --git a/store/src/main/java/org/apache/rocketmq/store/logfile/DefaultMappedFile.java b/store/src/main/java/org/apache/rocketmq/store/logfile/DefaultMappedFile.java new file mode 100644 index 00000000000..03477c33249 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/logfile/DefaultMappedFile.java @@ -0,0 +1,929 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.logfile; + +import com.sun.jna.NativeLong; +import com.sun.jna.Pointer; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileChannel.MapMode; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Iterator; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; +import org.apache.commons.lang3.SystemUtils; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBatch; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.utils.NetworkUtil; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.AppendMessageCallback; +import org.apache.rocketmq.store.AppendMessageResult; +import org.apache.rocketmq.store.AppendMessageStatus; +import org.apache.rocketmq.store.CompactionAppendMsgCallback; +import org.apache.rocketmq.store.PutMessageContext; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.TransientStorePool; +import org.apache.rocketmq.store.config.FlushDiskType; +import org.apache.rocketmq.store.util.LibC; +import sun.misc.Unsafe; +import sun.nio.ch.DirectBuffer; + +public class DefaultMappedFile extends AbstractMappedFile { + public static final int OS_PAGE_SIZE = 1024 * 4; + public static final Unsafe UNSAFE = getUnsafe(); + private static final Method IS_LOADED_METHOD; + public static final int UNSAFE_PAGE_SIZE = UNSAFE == null ? OS_PAGE_SIZE : UNSAFE.pageSize(); + + protected static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + + protected static final AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY = new AtomicLong(0); + + protected static final AtomicInteger TOTAL_MAPPED_FILES = new AtomicInteger(0); + + protected static final AtomicIntegerFieldUpdater WROTE_POSITION_UPDATER; + protected static final AtomicIntegerFieldUpdater COMMITTED_POSITION_UPDATER; + protected static final AtomicIntegerFieldUpdater FLUSHED_POSITION_UPDATER; + + protected volatile int wrotePosition; + protected volatile int committedPosition; + protected volatile int flushedPosition; + protected int fileSize; + protected FileChannel fileChannel; + /** + * Message will put to here first, and then reput to FileChannel if writeBuffer is not null. + */ + protected ByteBuffer writeBuffer = null; + protected TransientStorePool transientStorePool = null; + protected String fileName; + protected long fileFromOffset; + protected File file; + protected MappedByteBuffer mappedByteBuffer; + protected volatile long storeTimestamp = 0; + protected boolean firstCreateInQueue = false; + private long lastFlushTime = -1L; + + protected MappedByteBuffer mappedByteBufferWaitToClean = null; + protected long swapMapTime = 0L; + protected long mappedByteBufferAccessCountSinceLastSwap = 0L; + + /** + * If this mapped file belongs to consume queue, this field stores store-timestamp of first message referenced + * by this logical queue. + */ + private long startTimestamp = -1; + + /** + * If this mapped file belongs to consume queue, this field stores store-timestamp of last message referenced + * by this logical queue. + */ + private long stopTimestamp = -1; + + static { + WROTE_POSITION_UPDATER = AtomicIntegerFieldUpdater.newUpdater(DefaultMappedFile.class, "wrotePosition"); + COMMITTED_POSITION_UPDATER = AtomicIntegerFieldUpdater.newUpdater(DefaultMappedFile.class, "committedPosition"); + FLUSHED_POSITION_UPDATER = AtomicIntegerFieldUpdater.newUpdater(DefaultMappedFile.class, "flushedPosition"); + + Method isLoaded0method = null; + // On the windows platform and openjdk 11 method isLoaded0 always returns false. + // see https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/19fb8f93c59dfd791f62d41f332db9e306bc1422/src/java.base/windows/native/libnio/MappedByteBuffer.c#L34 + if (!SystemUtils.IS_OS_WINDOWS) { + try { + isLoaded0method = MappedByteBuffer.class.getDeclaredMethod("isLoaded0", long.class, long.class, int.class); + isLoaded0method.setAccessible(true); + } catch (NoSuchMethodException ignore) { + } + } + IS_LOADED_METHOD = isLoaded0method; + } + + public DefaultMappedFile() { + } + + public DefaultMappedFile(final String fileName, final int fileSize) throws IOException { + init(fileName, fileSize); + } + + public DefaultMappedFile(final String fileName, final int fileSize, + final TransientStorePool transientStorePool) throws IOException { + init(fileName, fileSize, transientStorePool); + } + + public static int getTotalMappedFiles() { + return TOTAL_MAPPED_FILES.get(); + } + + public static long getTotalMappedVirtualMemory() { + return TOTAL_MAPPED_VIRTUAL_MEMORY.get(); + } + + @Override + public void init(final String fileName, final int fileSize, + final TransientStorePool transientStorePool) throws IOException { + init(fileName, fileSize); + this.writeBuffer = transientStorePool.borrowBuffer(); + this.transientStorePool = transientStorePool; + } + + private void init(final String fileName, final int fileSize) throws IOException { + this.fileName = fileName; + this.fileSize = fileSize; + this.file = new File(fileName); + this.fileFromOffset = Long.parseLong(this.file.getName()); + boolean ok = false; + + UtilAll.ensureDirOK(this.file.getParent()); + + try { + this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel(); + this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize); + TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(fileSize); + TOTAL_MAPPED_FILES.incrementAndGet(); + ok = true; + } catch (FileNotFoundException e) { + log.error("Failed to create file " + this.fileName, e); + throw e; + } catch (IOException e) { + log.error("Failed to map file " + this.fileName, e); + throw e; + } finally { + if (!ok && this.fileChannel != null) { + this.fileChannel.close(); + } + } + } + + @Override + public boolean renameTo(String fileName) { + File newFile = new File(fileName); + boolean rename = file.renameTo(newFile); + if (rename) { + this.fileName = fileName; + this.file = newFile; + } + return rename; + } + + @Override + public long getLastModifiedTimestamp() { + return this.file.lastModified(); + } + + public boolean getData(int pos, int size, ByteBuffer byteBuffer) { + if (byteBuffer.remaining() < size) { + return false; + } + + int readPosition = getReadPosition(); + if ((pos + size) <= readPosition) { + + if (this.hold()) { + try { + int readNum = fileChannel.read(byteBuffer, pos); + return size == readNum; + } catch (Throwable t) { + log.warn("Get data failed pos:{} size:{} fileFromOffset:{}", pos, size, this.fileFromOffset); + return false; + } finally { + this.release(); + } + } else { + log.debug("matched, but hold failed, request pos: " + pos + ", fileFromOffset: " + + this.fileFromOffset); + } + } else { + log.warn("selectMappedBuffer request pos invalid, request pos: " + pos + ", size: " + size + + ", fileFromOffset: " + this.fileFromOffset); + } + + return false; + } + + @Override + public int getFileSize() { + return fileSize; + } + + @Override + public FileChannel getFileChannel() { + return fileChannel; + } + + public AppendMessageResult appendMessage(final ByteBuffer byteBufferMsg, final CompactionAppendMsgCallback cb) { + assert byteBufferMsg != null; + assert cb != null; + + int currentPos = WROTE_POSITION_UPDATER.get(this); + if (currentPos < this.fileSize) { + ByteBuffer byteBuffer = appendMessageBuffer().slice(); + byteBuffer.position(currentPos); + AppendMessageResult result = cb.doAppend(byteBuffer, this.fileFromOffset, this.fileSize - currentPos, byteBufferMsg); + WROTE_POSITION_UPDATER.addAndGet(this, result.getWroteBytes()); + this.storeTimestamp = result.getStoreTimestamp(); + return result; + } + log.error("MappedFile.appendMessage return null, wrotePosition: {} fileSize: {}", currentPos, this.fileSize); + return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR); + } + + @Override + public AppendMessageResult appendMessage(final MessageExtBrokerInner msg, final AppendMessageCallback cb, + PutMessageContext putMessageContext) { + return appendMessagesInner(msg, cb, putMessageContext); + } + + @Override + public AppendMessageResult appendMessages(final MessageExtBatch messageExtBatch, final AppendMessageCallback cb, + PutMessageContext putMessageContext) { + return appendMessagesInner(messageExtBatch, cb, putMessageContext); + } + + public AppendMessageResult appendMessagesInner(final MessageExt messageExt, final AppendMessageCallback cb, + PutMessageContext putMessageContext) { + assert messageExt != null; + assert cb != null; + + int currentPos = WROTE_POSITION_UPDATER.get(this); + + if (currentPos < this.fileSize) { + ByteBuffer byteBuffer = appendMessageBuffer().slice(); + byteBuffer.position(currentPos); + AppendMessageResult result; + if (messageExt instanceof MessageExtBatch && !((MessageExtBatch) messageExt).isInnerBatch()) { + // traditional batch message + result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, + (MessageExtBatch) messageExt, putMessageContext); + } else if (messageExt instanceof MessageExtBrokerInner) { + // traditional single message or newly introduced inner-batch message + result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, + (MessageExtBrokerInner) messageExt, putMessageContext); + } else { + return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR); + } + WROTE_POSITION_UPDATER.addAndGet(this, result.getWroteBytes()); + this.storeTimestamp = result.getStoreTimestamp(); + return result; + } + log.error("MappedFile.appendMessage return null, wrotePosition: {} fileSize: {}", currentPos, this.fileSize); + return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR); + } + + protected ByteBuffer appendMessageBuffer() { + this.mappedByteBufferAccessCountSinceLastSwap++; + return writeBuffer != null ? writeBuffer : this.mappedByteBuffer; + } + + @Override + public long getFileFromOffset() { + return this.fileFromOffset; + } + + @Override + public boolean appendMessage(final byte[] data) { + return appendMessage(data, 0, data.length); + } + + @Override + public boolean appendMessage(ByteBuffer data) { + int currentPos = WROTE_POSITION_UPDATER.get(this); + int remaining = data.remaining(); + + if ((currentPos + remaining) <= this.fileSize) { + try { + this.fileChannel.position(currentPos); + while (data.hasRemaining()) { + this.fileChannel.write(data); + } + } catch (Throwable e) { + log.error("Error occurred when append message to mappedFile.", e); + } + WROTE_POSITION_UPDATER.addAndGet(this, remaining); + return true; + } + return false; + } + + /** + * Content of data from offset to offset + length will be written to file. + * + * @param offset The offset of the subarray to be used. + * @param length The length of the subarray to be used. + */ + @Override + public boolean appendMessage(final byte[] data, final int offset, final int length) { + int currentPos = WROTE_POSITION_UPDATER.get(this); + + if ((currentPos + length) <= this.fileSize) { + try { + ByteBuffer buf = this.mappedByteBuffer.slice(); + buf.position(currentPos); + buf.put(data, offset, length); + } catch (Throwable e) { + log.error("Error occurred when append message to mappedFile.", e); + } + WROTE_POSITION_UPDATER.addAndGet(this, length); + return true; + } + + return false; + } + + /** + * @return The current flushed position + */ + @Override + public int flush(final int flushLeastPages) { + if (this.isAbleToFlush(flushLeastPages)) { + if (this.hold()) { + int value = getReadPosition(); + + try { + this.mappedByteBufferAccessCountSinceLastSwap++; + + //We only append data to fileChannel or mappedByteBuffer, never both. + if (writeBuffer != null || this.fileChannel.position() != 0) { + this.fileChannel.force(false); + } else { + this.mappedByteBuffer.force(); + } + this.lastFlushTime = System.currentTimeMillis(); + } catch (Throwable e) { + log.error("Error occurred when force data to disk.", e); + } + + FLUSHED_POSITION_UPDATER.set(this, value); + this.release(); + } else { + log.warn("in flush, hold failed, flush offset = " + FLUSHED_POSITION_UPDATER.get(this)); + FLUSHED_POSITION_UPDATER.set(this, getReadPosition()); + } + } + return this.getFlushedPosition(); + } + + @Override + public int commit(final int commitLeastPages) { + if (writeBuffer == null) { + //no need to commit data to file channel, so just regard wrotePosition as committedPosition. + return WROTE_POSITION_UPDATER.get(this); + } + + //no need to commit data to file channel, so just set committedPosition to wrotePosition. + if (transientStorePool != null && !transientStorePool.isRealCommit()) { + COMMITTED_POSITION_UPDATER.set(this, WROTE_POSITION_UPDATER.get(this)); + } else if (this.isAbleToCommit(commitLeastPages)) { + if (this.hold()) { + commit0(); + this.release(); + } else { + log.warn("in commit, hold failed, commit offset = " + COMMITTED_POSITION_UPDATER.get(this)); + } + } + + // All dirty data has been committed to FileChannel. + if (writeBuffer != null && this.transientStorePool != null && this.fileSize == COMMITTED_POSITION_UPDATER.get(this)) { + this.transientStorePool.returnBuffer(writeBuffer); + this.writeBuffer = null; + } + + return COMMITTED_POSITION_UPDATER.get(this); + } + + protected void commit0() { + int writePos = WROTE_POSITION_UPDATER.get(this); + int lastCommittedPosition = COMMITTED_POSITION_UPDATER.get(this); + + if (writePos - lastCommittedPosition > 0) { + try { + ByteBuffer byteBuffer = writeBuffer.slice(); + byteBuffer.position(lastCommittedPosition); + byteBuffer.limit(writePos); + this.fileChannel.position(lastCommittedPosition); + this.fileChannel.write(byteBuffer); + COMMITTED_POSITION_UPDATER.set(this, writePos); + } catch (Throwable e) { + log.error("Error occurred when commit data to FileChannel.", e); + } + } + } + + private boolean isAbleToFlush(final int flushLeastPages) { + int flush = FLUSHED_POSITION_UPDATER.get(this); + int write = getReadPosition(); + + if (this.isFull()) { + return true; + } + + if (flushLeastPages > 0) { + return ((write / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE)) >= flushLeastPages; + } + + return write > flush; + } + + protected boolean isAbleToCommit(final int commitLeastPages) { + int commit = COMMITTED_POSITION_UPDATER.get(this); + int write = WROTE_POSITION_UPDATER.get(this); + + if (this.isFull()) { + return true; + } + + if (commitLeastPages > 0) { + return ((write / OS_PAGE_SIZE) - (commit / OS_PAGE_SIZE)) >= commitLeastPages; + } + + return write > commit; + } + + @Override + public int getFlushedPosition() { + return FLUSHED_POSITION_UPDATER.get(this); + } + + @Override + public void setFlushedPosition(int pos) { + FLUSHED_POSITION_UPDATER.set(this, pos); + } + + @Override + public boolean isFull() { + return this.fileSize == WROTE_POSITION_UPDATER.get(this); + } + + @Override + public SelectMappedBufferResult selectMappedBuffer(int pos, int size) { + int readPosition = getReadPosition(); + if ((pos + size) <= readPosition) { + if (this.hold()) { + this.mappedByteBufferAccessCountSinceLastSwap++; + + ByteBuffer byteBuffer = this.mappedByteBuffer.slice(); + byteBuffer.position(pos); + ByteBuffer byteBufferNew = byteBuffer.slice(); + byteBufferNew.limit(size); + return new SelectMappedBufferResult(this.fileFromOffset + pos, byteBufferNew, size, this); + } else { + log.warn("matched, but hold failed, request pos: " + pos + ", fileFromOffset: " + + this.fileFromOffset); + } + } else { + log.warn("selectMappedBuffer request pos invalid, request pos: " + pos + ", size: " + size + + ", fileFromOffset: " + this.fileFromOffset); + } + + return null; + } + + @Override + public SelectMappedBufferResult selectMappedBuffer(int pos) { + int readPosition = getReadPosition(); + if (pos < readPosition && pos >= 0) { + if (this.hold()) { + this.mappedByteBufferAccessCountSinceLastSwap++; + ByteBuffer byteBuffer = this.mappedByteBuffer.slice(); + byteBuffer.position(pos); + int size = readPosition - pos; + ByteBuffer byteBufferNew = byteBuffer.slice(); + byteBufferNew.limit(size); + return new SelectMappedBufferResult(this.fileFromOffset + pos, byteBufferNew, size, this); + } + } + + return null; + } + + @Override + public boolean cleanup(final long currentRef) { + if (this.isAvailable()) { + log.error("this file[REF:" + currentRef + "] " + this.fileName + + " have not shutdown, stop unmapping."); + return false; + } + + if (this.isCleanupOver()) { + log.error("this file[REF:" + currentRef + "] " + this.fileName + + " have cleanup, do not do it again."); + return true; + } + + UtilAll.cleanBuffer(this.mappedByteBuffer); + UtilAll.cleanBuffer(this.mappedByteBufferWaitToClean); + this.mappedByteBufferWaitToClean = null; + TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(this.fileSize * (-1)); + TOTAL_MAPPED_FILES.decrementAndGet(); + log.info("unmap file[REF:" + currentRef + "] " + this.fileName + " OK"); + return true; + } + + @Override + public boolean destroy(final long intervalForcibly) { + this.shutdown(intervalForcibly); + + if (this.isCleanupOver()) { + try { + long lastModified = getLastModifiedTimestamp(); + this.fileChannel.close(); + log.info("close file channel " + this.fileName + " OK"); + + long beginTime = System.currentTimeMillis(); + boolean result = this.file.delete(); + log.info("delete file[REF:" + this.getRefCount() + "] " + this.fileName + + (result ? " OK, " : " Failed, ") + "W:" + this.getWrotePosition() + " M:" + + this.getFlushedPosition() + ", " + + UtilAll.computeElapsedTimeMilliseconds(beginTime) + + "," + (System.currentTimeMillis() - lastModified)); + } catch (Exception e) { + log.warn("close file channel " + this.fileName + " Failed. ", e); + } + + return true; + } else { + log.warn("destroy mapped file[REF:" + this.getRefCount() + "] " + this.fileName + + " Failed. cleanupOver: " + this.cleanupOver); + } + + return false; + } + + @Override + public int getWrotePosition() { + return WROTE_POSITION_UPDATER.get(this); + } + + @Override + public void setWrotePosition(int pos) { + WROTE_POSITION_UPDATER.set(this, pos); + } + + /** + * @return The max position which have valid data + */ + @Override + public int getReadPosition() { + return transientStorePool == null || !transientStorePool.isRealCommit() ? WROTE_POSITION_UPDATER.get(this) : COMMITTED_POSITION_UPDATER.get(this); + } + + @Override + public void setCommittedPosition(int pos) { + COMMITTED_POSITION_UPDATER.set(this, pos); + } + + @Override + public void warmMappedFile(FlushDiskType type, int pages) { + this.mappedByteBufferAccessCountSinceLastSwap++; + + long beginTime = System.currentTimeMillis(); + ByteBuffer byteBuffer = this.mappedByteBuffer.slice(); + long flush = 0; + // long time = System.currentTimeMillis(); + for (long i = 0, j = 0; i < this.fileSize; i += DefaultMappedFile.OS_PAGE_SIZE, j++) { + byteBuffer.put((int) i, (byte) 0); + // force flush when flush disk type is sync + if (type == FlushDiskType.SYNC_FLUSH) { + if ((i / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE) >= pages) { + flush = i; + mappedByteBuffer.force(); + } + } + + // prevent gc + // if (j % 1000 == 0) { + // log.info("j={}, costTime={}", j, System.currentTimeMillis() - time); + // time = System.currentTimeMillis(); + // try { + // Thread.sleep(0); + // } catch (InterruptedException e) { + // log.error("Interrupted", e); + // } + // } + } + + // force flush when prepare load finished + if (type == FlushDiskType.SYNC_FLUSH) { + log.info("mapped file warm-up done, force to disk, mappedFile={}, costTime={}", + this.getFileName(), System.currentTimeMillis() - beginTime); + mappedByteBuffer.force(); + } + log.info("mapped file warm-up done. mappedFile={}, costTime={}", this.getFileName(), + System.currentTimeMillis() - beginTime); + + this.mlock(); + } + + @Override + public boolean swapMap() { + if (getRefCount() == 1 && this.mappedByteBufferWaitToClean == null) { + + if (!hold()) { + log.warn("in swapMap, hold failed, fileName: " + this.fileName); + return false; + } + try { + this.mappedByteBufferWaitToClean = this.mappedByteBuffer; + this.mappedByteBuffer = this.fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileSize); + this.mappedByteBufferAccessCountSinceLastSwap = 0L; + this.swapMapTime = System.currentTimeMillis(); + log.info("swap file " + this.fileName + " success."); + return true; + } catch (Exception e) { + log.error("swapMap file " + this.fileName + " Failed. ", e); + } finally { + this.release(); + } + } else { + log.info("Will not swap file: " + this.fileName + ", ref=" + getRefCount()); + } + return false; + } + + @Override + public void cleanSwapedMap(boolean force) { + try { + if (this.mappedByteBufferWaitToClean == null) { + return; + } + long minGapTime = 120 * 1000L; + long gapTime = System.currentTimeMillis() - this.swapMapTime; + if (!force && gapTime < minGapTime) { + Thread.sleep(minGapTime - gapTime); + } + UtilAll.cleanBuffer(this.mappedByteBufferWaitToClean); + mappedByteBufferWaitToClean = null; + log.info("cleanSwapedMap file " + this.fileName + " success."); + } catch (Exception e) { + log.error("cleanSwapedMap file " + this.fileName + " Failed. ", e); + } + } + + @Override + public long getRecentSwapMapTime() { + return 0; + } + + @Override + public long getMappedByteBufferAccessCountSinceLastSwap() { + return this.mappedByteBufferAccessCountSinceLastSwap; + } + + @Override + public long getLastFlushTime() { + return this.lastFlushTime; + } + + @Override + public String getFileName() { + return fileName; + } + + @Override + public MappedByteBuffer getMappedByteBuffer() { + this.mappedByteBufferAccessCountSinceLastSwap++; + return mappedByteBuffer; + } + + @Override + public ByteBuffer sliceByteBuffer() { + this.mappedByteBufferAccessCountSinceLastSwap++; + return this.mappedByteBuffer.slice(); + } + + @Override + public long getStoreTimestamp() { + return storeTimestamp; + } + + @Override + public boolean isFirstCreateInQueue() { + return firstCreateInQueue; + } + + @Override + public void setFirstCreateInQueue(boolean firstCreateInQueue) { + this.firstCreateInQueue = firstCreateInQueue; + } + + @Override + public void mlock() { + final long beginTime = System.currentTimeMillis(); + final long address = ((DirectBuffer) (this.mappedByteBuffer)).address(); + Pointer pointer = new Pointer(address); + { + int ret = LibC.INSTANCE.mlock(pointer, new NativeLong(this.fileSize)); + log.info("mlock {} {} {} ret = {} time consuming = {}", address, this.fileName, this.fileSize, ret, System.currentTimeMillis() - beginTime); + } + + { + int ret = LibC.INSTANCE.madvise(pointer, new NativeLong(this.fileSize), LibC.MADV_WILLNEED); + log.info("madvise {} {} {} ret = {} time consuming = {}", address, this.fileName, this.fileSize, ret, System.currentTimeMillis() - beginTime); + } + } + + @Override + public void munlock() { + final long beginTime = System.currentTimeMillis(); + final long address = ((DirectBuffer) (this.mappedByteBuffer)).address(); + Pointer pointer = new Pointer(address); + int ret = LibC.INSTANCE.munlock(pointer, new NativeLong(this.fileSize)); + log.info("munlock {} {} {} ret = {} time consuming = {}", address, this.fileName, this.fileSize, ret, System.currentTimeMillis() - beginTime); + } + + @Override + public File getFile() { + return this.file; + } + + @Override + public void renameToDelete() { + //use Files.move + if (!fileName.endsWith(".delete")) { + String newFileName = this.fileName + ".delete"; + try { + Path newFilePath = Paths.get(newFileName); + // https://bugs.openjdk.org/browse/JDK-4724038 + // https://bugs.java.com/bugdatabase/view_bug.do?bug_id=4715154 + // Windows can't move the file when mmapped. + if (NetworkUtil.isWindowsPlatform() && mappedByteBuffer != null) { + long position = this.fileChannel.position(); + UtilAll.cleanBuffer(this.mappedByteBuffer); + this.fileChannel.close(); + Files.move(Paths.get(fileName), newFilePath, StandardCopyOption.ATOMIC_MOVE); + try (RandomAccessFile file = new RandomAccessFile(newFileName, "rw")) { + this.fileChannel = file.getChannel(); + this.fileChannel.position(position); + this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize); + } + } else { + Files.move(Paths.get(fileName), newFilePath, StandardCopyOption.ATOMIC_MOVE); + } + this.fileName = newFileName; + this.file = new File(newFileName); + } catch (IOException e) { + log.error("move file {} failed", fileName, e); + } + } + } + + @Override + public void moveToParent() throws IOException { + Path currentPath = Paths.get(fileName); + String baseName = currentPath.getFileName().toString(); + Path parentPath = currentPath.getParent().getParent().resolve(baseName); + // https://bugs.openjdk.org/browse/JDK-4724038 + // https://bugs.java.com/bugdatabase/view_bug.do?bug_id=4715154 + // Windows can't move the file when mmapped. + if (NetworkUtil.isWindowsPlatform() && mappedByteBuffer != null) { + long position = this.fileChannel.position(); + UtilAll.cleanBuffer(this.mappedByteBuffer); + this.fileChannel.close(); + Files.move(Paths.get(fileName), parentPath, StandardCopyOption.ATOMIC_MOVE); + try (RandomAccessFile file = new RandomAccessFile(parentPath.toFile(), "rw")) { + this.fileChannel = file.getChannel(); + this.fileChannel.position(position); + this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize); + } + } else { + Files.move(Paths.get(fileName), parentPath, StandardCopyOption.ATOMIC_MOVE); + } + this.file = parentPath.toFile(); + this.fileName = parentPath.toString(); + } + + @Override + public String toString() { + return this.fileName; + } + + public long getStartTimestamp() { + return startTimestamp; + } + + public void setStartTimestamp(long startTimestamp) { + this.startTimestamp = startTimestamp; + } + + public long getStopTimestamp() { + return stopTimestamp; + } + + public void setStopTimestamp(long stopTimestamp) { + this.stopTimestamp = stopTimestamp; + } + + + public Iterator iterator(int startPos) { + return new Itr(startPos); + } + + public static Unsafe getUnsafe() { + try { + Field f = Unsafe.class.getDeclaredField("theUnsafe"); + f.setAccessible(true); + return (Unsafe) f.get(null); + } catch (Exception ignore) { + + } + return null; + } + + public static long mappingAddr(long addr) { + long offset = addr % UNSAFE_PAGE_SIZE; + offset = (offset >= 0) ? offset : (UNSAFE_PAGE_SIZE + offset); + return addr - offset; + } + + public static int pageCount(long size) { + return (int) (size + (long) UNSAFE_PAGE_SIZE - 1L) / UNSAFE_PAGE_SIZE; + } + + @Override + public boolean isLoaded(long position, int size) { + if (IS_LOADED_METHOD == null) { + return true; + } + try { + long addr = ((DirectBuffer) mappedByteBuffer).address() + position; + return (boolean) IS_LOADED_METHOD.invoke(mappedByteBuffer, mappingAddr(addr), size, pageCount(size)); + } catch (Exception e) { + log.info("invoke isLoaded0 of file {} error:", file.getAbsolutePath(), e); + } + return true; + } + + private class Itr implements Iterator { + private int start; + private int current; + private ByteBuffer buf; + + public Itr(int pos) { + this.start = pos; + this.current = pos; + this.buf = mappedByteBuffer.slice(); + this.buf.position(start); + } + + @Override + public boolean hasNext() { + return current < getReadPosition(); + } + + @Override + public SelectMappedBufferResult next() { + int readPosition = getReadPosition(); + if (current < readPosition && current >= 0) { + if (hold()) { + ByteBuffer byteBuffer = buf.slice(); + byteBuffer.position(current); + int size = byteBuffer.getInt(current); + ByteBuffer bufferResult = byteBuffer.slice(); + bufferResult.limit(size); + current += size; + return new SelectMappedBufferResult(fileFromOffset + current, bufferResult, size, + DefaultMappedFile.this); + } + } + return null; + } + + @Override + public void forEachRemaining(Consumer action) { + Iterator.super.forEachRemaining(action); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + +} diff --git a/store/src/main/java/org/apache/rocketmq/store/logfile/MappedFile.java b/store/src/main/java/org/apache/rocketmq/store/logfile/MappedFile.java new file mode 100644 index 00000000000..dfcf66f0882 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/logfile/MappedFile.java @@ -0,0 +1,371 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.logfile; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.util.Iterator; +import org.apache.rocketmq.common.message.MessageExtBatch; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.store.AppendMessageCallback; +import org.apache.rocketmq.store.AppendMessageResult; +import org.apache.rocketmq.store.CompactionAppendMsgCallback; +import org.apache.rocketmq.store.PutMessageContext; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.TransientStorePool; +import org.apache.rocketmq.store.config.FlushDiskType; + +public interface MappedFile { + /** + * Returns the file name of the {@code MappedFile}. + * + * @return the file name + */ + String getFileName(); + + /** + * Change the file name of the {@code MappedFile}. + * + * @param fileName the new file name + */ + boolean renameTo(String fileName); + + /** + * Returns the file size of the {@code MappedFile}. + * + * @return the file size + */ + int getFileSize(); + + /** + * Returns the {@code FileChannel} behind the {@code MappedFile}. + * + * @return the file channel + */ + FileChannel getFileChannel(); + + /** + * Returns true if this {@code MappedFile} is full and no new messages can be added. + * + * @return true if the file is full + */ + boolean isFull(); + + /** + * Returns true if this {@code MappedFile} is available. + *

    + * The mapped file will be not available if it's shutdown or destroyed. + * + * @return true if the file is available + */ + boolean isAvailable(); + + /** + * Appends a message object to the current {@code MappedFile} with a specific call back. + * + * @param message a message to append + * @param messageCallback the specific call back to execute the real append action + * @param putMessageContext + * @return the append result + */ + AppendMessageResult appendMessage(MessageExtBrokerInner message, AppendMessageCallback messageCallback, PutMessageContext putMessageContext); + + /** + * Appends a batch message object to the current {@code MappedFile} with a specific call back. + * + * @param message a message to append + * @param messageCallback the specific call back to execute the real append action + * @param putMessageContext + * @return the append result + */ + AppendMessageResult appendMessages(MessageExtBatch message, AppendMessageCallback messageCallback, PutMessageContext putMessageContext); + + AppendMessageResult appendMessage(final ByteBuffer byteBufferMsg, final CompactionAppendMsgCallback cb); + + /** + * Appends a raw message data represents by a byte array to the current {@code MappedFile}. + * + * @param data the byte array to append + * @return true if success; false otherwise. + */ + boolean appendMessage(byte[] data); + + /** + * Appends a raw message data represents by a byte array to the current {@code MappedFile}. + * + * @param data the byte buffer to append + * @return true if success; false otherwise. + */ + boolean appendMessage(ByteBuffer data); + + /** + * Appends a raw message data represents by a byte array to the current {@code MappedFile}, + * starting at the given offset in the array. + * + * @param data the byte array to append + * @param offset the offset within the array of the first byte to be read + * @param length the number of bytes to be read from the given array + * @return true if success; false otherwise. + */ + boolean appendMessage(byte[] data, int offset, int length); + + /** + * Returns the global offset of the current {code MappedFile}, it's a long value of the file name. + * + * @return the offset of this file + */ + long getFileFromOffset(); + + /** + * Flushes the data in cache to disk immediately. + * + * @param flushLeastPages the least pages to flush + * @return the flushed position after the method call + */ + int flush(int flushLeastPages); + + /** + * Flushes the data in the secondary cache to page cache or disk immediately. + * + * @param commitLeastPages the least pages to commit + * @return the committed position after the method call + */ + int commit(int commitLeastPages); + + /** + * Selects a slice of the mapped byte buffer's sub-region behind the mapped file, + * starting at the given position. + * + * @param pos the given position + * @param size the size of the returned sub-region + * @return a {@code SelectMappedBufferResult} instance contains the selected slice + */ + SelectMappedBufferResult selectMappedBuffer(int pos, int size); + + /** + * Selects a slice of the mapped byte buffer's sub-region behind the mapped file, + * starting at the given position. + * + * @param pos the given position + * @return a {@code SelectMappedBufferResult} instance contains the selected slice + */ + SelectMappedBufferResult selectMappedBuffer(int pos); + + /** + * Returns the mapped byte buffer behind the mapped file. + * + * @return the mapped byte buffer + */ + MappedByteBuffer getMappedByteBuffer(); + + /** + * Returns a slice of the mapped byte buffer behind the mapped file. + * + * @return the slice of the mapped byte buffer + */ + ByteBuffer sliceByteBuffer(); + + /** + * Returns the store timestamp of the last message. + * + * @return the store timestamp + */ + long getStoreTimestamp(); + + /** + * Returns the last modified timestamp of the file. + * + * @return the last modified timestamp + */ + long getLastModifiedTimestamp(); + + /** + * Get data from a certain pos offset with size byte + * + * @param pos a certain pos offset to get data + * @param size the size of data + * @param byteBuffer the data + * @return true if with data; false if no data; + */ + boolean getData(int pos, int size, ByteBuffer byteBuffer); + + /** + * Destroys the file and delete it from the file system. + * + * @param intervalForcibly If {@code true} then this method will destroy the file forcibly and ignore the reference + * @return true if success; false otherwise. + */ + boolean destroy(long intervalForcibly); + + /** + * Shutdowns the file and mark it unavailable. + * + * @param intervalForcibly If {@code true} then this method will shutdown the file forcibly and ignore the reference + */ + void shutdown(long intervalForcibly); + + /** + * Decreases the reference count by {@code 1} and clean up the mapped file if the reference count reaches at + * {@code 0}. + */ + void release(); + + /** + * Increases the reference count by {@code 1}. + * + * @return true if success; false otherwise. + */ + boolean hold(); + + /** + * Returns true if the current file is first mapped file of some consume queue. + * + * @return true or false + */ + boolean isFirstCreateInQueue(); + + /** + * Sets the flag whether the current file is first mapped file of some consume queue. + * + * @param firstCreateInQueue true or false + */ + void setFirstCreateInQueue(boolean firstCreateInQueue); + + /** + * Returns the flushed position of this mapped file. + * + * @return the flushed posotion + */ + int getFlushedPosition(); + + /** + * Sets the flushed position of this mapped file. + * + * @param flushedPosition the specific flushed position + */ + void setFlushedPosition(int flushedPosition); + + /** + * Returns the wrote position of this mapped file. + * + * @return the wrote position + */ + int getWrotePosition(); + + /** + * Sets the wrote position of this mapped file. + * + * @param wrotePosition the specific wrote position + */ + void setWrotePosition(int wrotePosition); + + /** + * Returns the current max readable position of this mapped file. + * + * @return the max readable position + */ + int getReadPosition(); + + /** + * Sets the committed position of this mapped file. + * + * @param committedPosition the specific committed position + */ + void setCommittedPosition(int committedPosition); + + /** + * Lock the mapped bytebuffer + */ + void mlock(); + + /** + * Unlock the mapped bytebuffer + */ + void munlock(); + + /** + * Warm up the mapped bytebuffer + * @param type + * @param pages + */ + void warmMappedFile(FlushDiskType type, int pages); + + /** + * Swap map + */ + boolean swapMap(); + + /** + * Clean pageTable + */ + void cleanSwapedMap(boolean force); + + /** + * Get recent swap map time + */ + long getRecentSwapMapTime(); + + /** + * Get recent MappedByteBuffer access count since last swap + */ + long getMappedByteBufferAccessCountSinceLastSwap(); + + /** + * Get the underlying file + * @return + */ + File getFile(); + + /** + * rename file to add ".delete" suffix + */ + void renameToDelete(); + + /** + * move the file to the parent directory + * @throws IOException + */ + void moveToParent() throws IOException; + + /** + * Get the last flush time + * @return + */ + long getLastFlushTime(); + + /** + * Init mapped file + * @param fileName file name + * @param fileSize file size + * @param transientStorePool transient store pool + * @throws IOException + */ + void init(String fileName, int fileSize, TransientStorePool transientStorePool) throws IOException; + + Iterator iterator(int pos); + + /** + * Check mapped file is loaded to memory with given position and size + * @param position start offset of data + * @param size data size + * @return data is resided in memory or not + */ + boolean isLoaded(long position, int size); +} diff --git a/store/src/main/java/org/apache/rocketmq/store/metrics/DefaultStoreMetricsConstant.java b/store/src/main/java/org/apache/rocketmq/store/metrics/DefaultStoreMetricsConstant.java new file mode 100644 index 00000000000..956501c64f3 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/metrics/DefaultStoreMetricsConstant.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.metrics; + +public class DefaultStoreMetricsConstant { + public static final String GAUGE_STORAGE_SIZE = "rocketmq_storage_size"; + public static final String GAUGE_STORAGE_FLUSH_BEHIND = "rocketmq_storage_flush_behind_bytes"; + public static final String GAUGE_STORAGE_DISPATCH_BEHIND = "rocketmq_storage_dispatch_behind_bytes"; + public static final String GAUGE_STORAGE_MESSAGE_RESERVE_TIME = "rocketmq_storage_message_reserve_time"; + + public static final String GAUGE_TIMER_ENQUEUE_LAG = "rocketmq_timer_enqueue_lag"; + public static final String GAUGE_TIMER_ENQUEUE_LATENCY = "rocketmq_timer_enqueue_latency"; + public static final String GAUGE_TIMER_DEQUEUE_LAG = "rocketmq_timer_dequeue_lag"; + public static final String GAUGE_TIMER_DEQUEUE_LATENCY = "rocketmq_timer_dequeue_latency"; + public static final String GAUGE_TIMING_MESSAGES = "rocketmq_timing_messages"; + + public static final String COUNTER_TIMER_ENQUEUE_TOTAL = "rocketmq_timer_enqueue_total"; + public static final String COUNTER_TIMER_DEQUEUE_TOTAL = "rocketmq_timer_dequeue_total"; + public static final String GAUGE_TIMER_MESSAGE_SNAPSHOT = "rocketmq_timer_message_snapshot"; + public static final String HISTOGRAM_DELAY_MSG_LATENCY = "rocketmq_delay_message_latency"; + + public static final String LABEL_STORAGE_TYPE = "storage_type"; + public static final String DEFAULT_STORAGE_TYPE = "local"; + public static final String LABEL_STORAGE_MEDIUM = "storage_medium"; + public static final String DEFAULT_STORAGE_MEDIUM = "disk"; + public static final String LABEL_TOPIC = "topic"; + public static final String LABEL_TIMING_BOUND = "timer_bound_s"; + public static final String GAUGE_BYTES_ROCKSDB_WRITTEN = "rocketmq_rocksdb_bytes_written"; + public static final String GAUGE_BYTES_ROCKSDB_READ = "rocketmq_rocksdb_bytes_read"; + + public static final String GAUGE_TIMES_ROCKSDB_WRITTEN_SELF = "rocketmq_rocksdb_times_written_self"; + public static final String GAUGE_TIMES_ROCKSDB_WRITTEN_OTHER = "rocketmq_rocksdb_times_written_other"; + public static final String GAUGE_RATE_ROCKSDB_CACHE_HIT = "rocketmq_rocksdb_rate_cache_hit"; + public static final String GAUGE_TIMES_ROCKSDB_COMPRESSED = "rocketmq_rocksdb_times_compressed"; + public static final String GAUGE_BYTES_READ_AMPLIFICATION = "rocketmq_rocksdb_read_amplification_bytes"; + public static final String GAUGE_TIMES_ROCKSDB_READ = "rocketmq_rocksdb_times_read"; +} diff --git a/store/src/main/java/org/apache/rocketmq/store/metrics/DefaultStoreMetricsManager.java b/store/src/main/java/org/apache/rocketmq/store/metrics/DefaultStoreMetricsManager.java new file mode 100644 index 00000000000..db4c7bb7662 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/metrics/DefaultStoreMetricsManager.java @@ -0,0 +1,251 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.metrics; + +import com.google.common.collect.Lists; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.ObservableLongGauge; +import io.opentelemetry.sdk.metrics.Aggregation; +import io.opentelemetry.sdk.metrics.InstrumentSelector; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.View; +import io.opentelemetry.sdk.metrics.ViewBuilder; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.metrics.NopLongCounter; +import org.apache.rocketmq.common.metrics.NopLongHistogram; +import org.apache.rocketmq.common.metrics.NopObservableLongGauge; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.timer.Slot; +import org.apache.rocketmq.store.timer.TimerMessageStore; +import org.apache.rocketmq.store.timer.TimerMetrics; +import org.apache.rocketmq.store.timer.TimerWheel; + +import java.io.File; +import java.util.Arrays; +import java.util.List; +import java.util.function.Supplier; + +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.COUNTER_TIMER_DEQUEUE_TOTAL; +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.COUNTER_TIMER_ENQUEUE_TOTAL; +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.DEFAULT_STORAGE_MEDIUM; +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.DEFAULT_STORAGE_TYPE; +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.GAUGE_STORAGE_DISPATCH_BEHIND; +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.GAUGE_STORAGE_FLUSH_BEHIND; +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.GAUGE_STORAGE_MESSAGE_RESERVE_TIME; +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.GAUGE_STORAGE_SIZE; +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.GAUGE_TIMER_DEQUEUE_LAG; +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.GAUGE_TIMER_DEQUEUE_LATENCY; +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.GAUGE_TIMER_ENQUEUE_LAG; +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.GAUGE_TIMER_ENQUEUE_LATENCY; +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.GAUGE_TIMER_MESSAGE_SNAPSHOT; +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.GAUGE_TIMING_MESSAGES; +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.HISTOGRAM_DELAY_MSG_LATENCY; +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.LABEL_STORAGE_MEDIUM; +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.LABEL_STORAGE_TYPE; +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.LABEL_TIMING_BOUND; +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.LABEL_TOPIC; + +public class DefaultStoreMetricsManager { + public static Supplier attributesBuilderSupplier; + public static MessageStoreConfig messageStoreConfig; + + public static ObservableLongGauge storageSize = new NopObservableLongGauge(); + public static ObservableLongGauge flushBehind = new NopObservableLongGauge(); + public static ObservableLongGauge dispatchBehind = new NopObservableLongGauge(); + public static ObservableLongGauge messageReserveTime = new NopObservableLongGauge(); + + public static ObservableLongGauge timerEnqueueLag = new NopObservableLongGauge(); + public static ObservableLongGauge timerEnqueueLatency = new NopObservableLongGauge(); + public static ObservableLongGauge timerDequeueLag = new NopObservableLongGauge(); + public static ObservableLongGauge timerDequeueLatency = new NopObservableLongGauge(); + public static ObservableLongGauge timingMessages = new NopObservableLongGauge(); + + public static LongCounter timerDequeueTotal = new NopLongCounter(); + public static LongCounter timerEnqueueTotal = new NopLongCounter(); + public static ObservableLongGauge timerMessageSnapshot = new NopObservableLongGauge(); + public static LongHistogram timerMessageSetLatency = new NopLongHistogram(); + + public static List> getMetricsView() { + List rpcCostTimeBuckets = Arrays.asList( + // day * hour * min * second + 1d * 1 * 1 * 60, // 60 second + 1d * 1 * 10 * 60, // 10 min + 1d * 1 * 60 * 60, // 1 hour + 1d * 12 * 60 * 60, // 12 hour + 1d * 24 * 60 * 60, // 1 day + 3d * 24 * 60 * 60 // 3 day + ); + InstrumentSelector selector = InstrumentSelector.builder() + .setType(InstrumentType.HISTOGRAM) + .setName(HISTOGRAM_DELAY_MSG_LATENCY) + .build(); + ViewBuilder viewBuilder = View.builder() + .setAggregation(Aggregation.explicitBucketHistogram(rpcCostTimeBuckets)); + return Lists.newArrayList(new Pair<>(selector, viewBuilder)); + } + + public static void init(Meter meter, Supplier attributesBuilderSupplier, + DefaultMessageStore messageStore) { + DefaultStoreMetricsManager.attributesBuilderSupplier = attributesBuilderSupplier; + DefaultStoreMetricsManager.messageStoreConfig = messageStore.getMessageStoreConfig(); + + storageSize = meter.gaugeBuilder(GAUGE_STORAGE_SIZE) + .setDescription("Broker storage size") + .setUnit("bytes") + .ofLongs() + .buildWithCallback(measurement -> { + File storeDir = new File(messageStoreConfig.getStorePathRootDir()); + if (storeDir.exists() && storeDir.isDirectory()) { + long totalSpace = storeDir.getTotalSpace(); + if (totalSpace > 0) { + measurement.record(totalSpace - storeDir.getFreeSpace(), newAttributesBuilder().build()); + } + } + }); + + flushBehind = meter.gaugeBuilder(GAUGE_STORAGE_FLUSH_BEHIND) + .setDescription("Broker flush behind bytes") + .setUnit("bytes") + .ofLongs() + .buildWithCallback(measurement -> measurement.record(messageStore.flushBehindBytes(), newAttributesBuilder().build())); + + dispatchBehind = meter.gaugeBuilder(GAUGE_STORAGE_DISPATCH_BEHIND) + .setDescription("Broker dispatch behind bytes") + .setUnit("bytes") + .ofLongs() + .buildWithCallback(measurement -> measurement.record(messageStore.dispatchBehindBytes(), newAttributesBuilder().build())); + + messageReserveTime = meter.gaugeBuilder(GAUGE_STORAGE_MESSAGE_RESERVE_TIME) + .setDescription("Broker message reserve time") + .setUnit("milliseconds") + .ofLongs() + .buildWithCallback(measurement -> { + long earliestMessageTime = messageStore.getEarliestMessageTime(); + if (earliestMessageTime <= 0) { + return; + } + measurement.record(System.currentTimeMillis() - earliestMessageTime, newAttributesBuilder().build()); + }); + + if (messageStore.getMessageStoreConfig().isTimerWheelEnable()) { + timerEnqueueLag = meter.gaugeBuilder(GAUGE_TIMER_ENQUEUE_LAG) + .setDescription("Timer enqueue messages lag") + .ofLongs() + .buildWithCallback(measurement -> { + TimerMessageStore timerMessageStore = messageStore.getTimerMessageStore(); + measurement.record(timerMessageStore.getEnqueueBehindMessages(), newAttributesBuilder().build()); + }); + + timerEnqueueLatency = meter.gaugeBuilder(GAUGE_TIMER_ENQUEUE_LATENCY) + .setDescription("Timer enqueue latency") + .setUnit("milliseconds") + .ofLongs() + .buildWithCallback(measurement -> { + TimerMessageStore timerMessageStore = messageStore.getTimerMessageStore(); + measurement.record(timerMessageStore.getEnqueueBehindMillis(), newAttributesBuilder().build()); + }); + timerDequeueLag = meter.gaugeBuilder(GAUGE_TIMER_DEQUEUE_LAG) + .setDescription("Timer dequeue messages lag") + .ofLongs() + .buildWithCallback(measurement -> { + TimerMessageStore timerMessageStore = messageStore.getTimerMessageStore(); + measurement.record(timerMessageStore.getDequeueBehindMessages(), newAttributesBuilder().build()); + }); + timerDequeueLatency = meter.gaugeBuilder(GAUGE_TIMER_DEQUEUE_LATENCY) + .setDescription("Timer dequeue latency") + .setUnit("milliseconds") + .ofLongs() + .buildWithCallback(measurement -> { + TimerMessageStore timerMessageStore = messageStore.getTimerMessageStore(); + measurement.record(timerMessageStore.getDequeueBehind(), newAttributesBuilder().build()); + }); + timingMessages = meter.gaugeBuilder(GAUGE_TIMING_MESSAGES) + .setDescription("Current message number in timing") + .ofLongs() + .buildWithCallback(measurement -> { + TimerMessageStore timerMessageStore = messageStore.getTimerMessageStore(); + timerMessageStore.getTimerMetrics() + .getTimingCount() + .forEach((topic, metric) -> { + measurement.record( + metric.getCount().get(), + newAttributesBuilder().put(LABEL_TOPIC, topic).build() + ); + }); + }); + timerDequeueTotal = meter.counterBuilder(COUNTER_TIMER_DEQUEUE_TOTAL) + .setDescription("Total number of timer dequeue") + .build(); + timerEnqueueTotal = meter.counterBuilder(COUNTER_TIMER_ENQUEUE_TOTAL) + .setDescription("Total number of timer enqueue") + .build(); + timerMessageSnapshot = meter.gaugeBuilder(GAUGE_TIMER_MESSAGE_SNAPSHOT) + .setDescription("Timer message distribution snapshot, only count timing messages in 24h.") + .ofLongs() + .buildWithCallback(measurement -> { + TimerMetrics timerMetrics = messageStore.getTimerMessageStore().getTimerMetrics(); + TimerWheel timerWheel = messageStore.getTimerMessageStore().getTimerWheel(); + int precisionMs = messageStoreConfig.getTimerPrecisionMs(); + List timerDist = timerMetrics.getTimerDistList(); + long currTime = System.currentTimeMillis() / precisionMs * precisionMs; + for (int i = 0; i < timerDist.size(); i++) { + int slotBeforeNum = i == 0 ? 0 : timerDist.get(i - 1) * 1000 / precisionMs; + int slotTotalNum = timerDist.get(i) * 1000 / precisionMs; + int periodTotal = 0; + for (int j = slotBeforeNum; j < slotTotalNum; j++) { + Slot slotEach = timerWheel.getSlot(currTime + (long) j * precisionMs); + periodTotal += slotEach.num; + } + measurement.record(periodTotal, newAttributesBuilder().put(LABEL_TIMING_BOUND, timerDist.get(i).toString()).build()); + } + }); + timerMessageSetLatency = meter.histogramBuilder(HISTOGRAM_DELAY_MSG_LATENCY) + .setDescription("Timer message set latency distribution") + .setUnit("seconds") + .ofLongs() + .build(); + } + } + + public static void incTimerDequeueCount(String topic) { + timerDequeueTotal.add(1, newAttributesBuilder() + .put(LABEL_TOPIC, topic) + .build()); + } + + public static void incTimerEnqueueCount(String topic) { + AttributesBuilder attributesBuilder = newAttributesBuilder(); + if (topic != null) { + attributesBuilder.put(LABEL_TOPIC, topic); + } + timerEnqueueTotal.add(1, attributesBuilder.build()); + } + + public static AttributesBuilder newAttributesBuilder() { + if (attributesBuilderSupplier == null) { + return Attributes.builder(); + } + return attributesBuilderSupplier.get() + .put(LABEL_STORAGE_TYPE, DEFAULT_STORAGE_TYPE) + .put(LABEL_STORAGE_MEDIUM, DEFAULT_STORAGE_MEDIUM); + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/metrics/RocksDBStoreMetricsManager.java b/store/src/main/java/org/apache/rocketmq/store/metrics/RocksDBStoreMetricsManager.java new file mode 100644 index 00000000000..6029488056c --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/metrics/RocksDBStoreMetricsManager.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.metrics; + +import com.google.common.collect.Lists; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.ObservableDoubleGauge; +import io.opentelemetry.api.metrics.ObservableLongGauge; +import io.opentelemetry.sdk.metrics.InstrumentSelector; +import io.opentelemetry.sdk.metrics.ViewBuilder; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.metrics.NopObservableDoubleGauge; +import org.apache.rocketmq.common.metrics.NopObservableLongGauge; +import org.apache.rocketmq.store.RocksDBMessageStore; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.queue.RocksDBConsumeQueueStore; +import org.rocksdb.TickerType; + +import java.util.List; +import java.util.function.Supplier; + +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.DEFAULT_STORAGE_MEDIUM; +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.DEFAULT_STORAGE_TYPE; +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.GAUGE_BYTES_ROCKSDB_READ; +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.GAUGE_BYTES_ROCKSDB_WRITTEN; +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.LABEL_STORAGE_MEDIUM; +import static org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant.LABEL_STORAGE_TYPE; + +public class RocksDBStoreMetricsManager { + public static Supplier attributesBuilderSupplier; + public static MessageStoreConfig messageStoreConfig; + + // The cumulative number of bytes read from the database. + public static ObservableLongGauge bytesRocksdbRead = new NopObservableLongGauge(); + + // The cumulative number of bytes written to the database. + public static ObservableLongGauge bytesRocksdbWritten = new NopObservableLongGauge(); + + // The cumulative number of read operations performed. + public static ObservableLongGauge timesRocksdbRead = new NopObservableLongGauge(); + + // The cumulative number of write operations performed. + public static ObservableLongGauge timesRocksdbWrittenSelf = new NopObservableLongGauge(); + public static ObservableLongGauge timesRocksdbWrittenOther = new NopObservableLongGauge(); + + // The cumulative number of compressions that have occurred. + public static ObservableLongGauge timesRocksdbCompressed = new NopObservableLongGauge(); + + // The ratio of the amount of data actually written to the storage medium to the amount of data written by the application. + public static ObservableDoubleGauge bytesRocksdbAmplificationRead = new NopObservableDoubleGauge(); + + // The rate at which cache lookups were served from the cache rather than needing to be fetched from disk. + public static ObservableDoubleGauge rocksdbCacheHitRate = new NopObservableDoubleGauge(); + + public static volatile long blockCacheHitTimes = 0; + public static volatile long blockCacheMissTimes = 0; + + + + public static List> getMetricsView() { + return Lists.newArrayList(); + } + + public static void init(Meter meter, Supplier attributesBuilderSupplier, + RocksDBMessageStore messageStore) { + RocksDBStoreMetricsManager.attributesBuilderSupplier = attributesBuilderSupplier; + bytesRocksdbWritten = meter.gaugeBuilder(GAUGE_BYTES_ROCKSDB_WRITTEN) + .setDescription("The cumulative number of bytes written to the database.") + .ofLongs() + .buildWithCallback(measurement -> { + measurement.record(((RocksDBConsumeQueueStore)messageStore.getQueueStore()) + .getStatistics().getTickerCount(TickerType.BYTES_WRITTEN), newAttributesBuilder().put("type", "consume_queue").build()); + }); + bytesRocksdbRead = meter.gaugeBuilder(GAUGE_BYTES_ROCKSDB_READ) + .setDescription("The cumulative number of bytes read from the database.") + .ofLongs() + .buildWithCallback(measurement -> { + measurement.record(((RocksDBConsumeQueueStore)messageStore.getQueueStore()) + .getStatistics().getTickerCount(TickerType.BYTES_READ), newAttributesBuilder().put("type", "consume_queue").build()); + }); + timesRocksdbWrittenSelf = meter.gaugeBuilder(DefaultStoreMetricsConstant.GAUGE_TIMES_ROCKSDB_WRITTEN_SELF) + .setDescription("The cumulative number of write operations performed by self.") + .ofLongs() + .buildWithCallback(measurement -> { + measurement.record(((RocksDBConsumeQueueStore)messageStore.getQueueStore()) + .getStatistics().getTickerCount(TickerType.WRITE_DONE_BY_SELF), newAttributesBuilder().put("type", "consume_queue").build()); + }); + timesRocksdbWrittenOther = meter.gaugeBuilder(DefaultStoreMetricsConstant.GAUGE_TIMES_ROCKSDB_WRITTEN_OTHER) + .setDescription("The cumulative number of write operations performed by other.") + .ofLongs() + .buildWithCallback(measurement -> { + measurement.record(((RocksDBConsumeQueueStore)messageStore.getQueueStore()) + .getStatistics().getTickerCount(TickerType.WRITE_DONE_BY_OTHER), newAttributesBuilder().put("type", "consume_queue").build()); + }); + timesRocksdbRead = meter.gaugeBuilder(DefaultStoreMetricsConstant.GAUGE_TIMES_ROCKSDB_READ) + .setDescription("The cumulative number of write operations performed by other.") + .ofLongs() + .buildWithCallback(measurement -> { + measurement.record(((RocksDBConsumeQueueStore)messageStore.getQueueStore()) + .getStatistics().getTickerCount(TickerType.NUMBER_KEYS_READ), newAttributesBuilder().put("type", "consume_queue").build()); + }); + rocksdbCacheHitRate = meter.gaugeBuilder(DefaultStoreMetricsConstant.GAUGE_RATE_ROCKSDB_CACHE_HIT) + .setDescription("The rate at which cache lookups were served from the cache rather than needing to be fetched from disk.") + .buildWithCallback(measurement -> { + long newHitTimes = ((RocksDBConsumeQueueStore)messageStore.getQueueStore()) + .getStatistics().getTickerCount(TickerType.BLOCK_CACHE_HIT); + long newMissTimes = ((RocksDBConsumeQueueStore)messageStore.getQueueStore()) + .getStatistics().getTickerCount(TickerType.BLOCK_CACHE_MISS); + long totalPeriod = newHitTimes - blockCacheHitTimes + newMissTimes - blockCacheMissTimes; + double hitRate = totalPeriod == 0 ? 0 : (double)(newHitTimes - blockCacheHitTimes) / totalPeriod; + blockCacheHitTimes = newHitTimes; + blockCacheMissTimes = newMissTimes; + measurement.record(hitRate, newAttributesBuilder().put("type", "consume_queue").build()); + }); + timesRocksdbCompressed = meter.gaugeBuilder(DefaultStoreMetricsConstant.GAUGE_TIMES_ROCKSDB_COMPRESSED) + .setDescription("The cumulative number of compressions that have occurred.") + .ofLongs() + .buildWithCallback(measurement -> { + measurement.record(((RocksDBConsumeQueueStore)messageStore.getQueueStore()) + .getStatistics().getTickerCount(TickerType.NUMBER_BLOCK_COMPRESSED), newAttributesBuilder().put("type", "consume_queue").build()); + }); + bytesRocksdbAmplificationRead = meter.gaugeBuilder(DefaultStoreMetricsConstant.GAUGE_BYTES_READ_AMPLIFICATION) + .setDescription("The rate at which cache lookups were served from the cache rather than needing to be fetched from disk.") + .buildWithCallback(measurement -> { + measurement.record(((RocksDBConsumeQueueStore)messageStore.getQueueStore()) + .getStatistics().getTickerCount(TickerType.READ_AMP_TOTAL_READ_BYTES), newAttributesBuilder().put("type", "consume_queue").build()); + }); + } + + public static AttributesBuilder newAttributesBuilder() { + if (attributesBuilderSupplier == null) { + return Attributes.builder(); + } + return attributesBuilderSupplier.get() + .put(LABEL_STORAGE_TYPE, DEFAULT_STORAGE_TYPE) + .put(LABEL_STORAGE_MEDIUM, DEFAULT_STORAGE_MEDIUM); + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/plugin/AbstractPluginMessageStore.java b/store/src/main/java/org/apache/rocketmq/store/plugin/AbstractPluginMessageStore.java new file mode 100644 index 00000000000..2f2ce981257 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/plugin/AbstractPluginMessageStore.java @@ -0,0 +1,664 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.plugin; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.SystemClock; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBatch; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.remoting.protocol.body.HARuntimeInfo; +import org.apache.rocketmq.store.AllocateMappedFileService; +import org.apache.rocketmq.store.AppendMessageResult; +import org.apache.rocketmq.store.CommitLog; +import org.apache.rocketmq.store.CommitLogDispatcher; +import org.apache.rocketmq.store.DispatchRequest; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.MessageFilter; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.QueryMessageResult; +import org.apache.rocketmq.store.RunningFlags; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.StoreCheckpoint; +import org.apache.rocketmq.store.StoreStatsService; +import org.apache.rocketmq.store.TransientStorePool; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.ha.HAService; +import org.apache.rocketmq.store.hook.PutMessageHook; +import org.apache.rocketmq.store.hook.SendMessageBackHook; +import org.apache.rocketmq.store.logfile.MappedFile; +import org.apache.rocketmq.store.queue.ConsumeQueueInterface; +import org.apache.rocketmq.store.queue.ConsumeQueueStoreInterface; +import org.apache.rocketmq.store.stats.BrokerStatsManager; +import org.apache.rocketmq.store.timer.TimerMessageStore; +import org.apache.rocketmq.store.util.PerfCounter; +import org.rocksdb.RocksDBException; + +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.sdk.metrics.InstrumentSelector; +import io.opentelemetry.sdk.metrics.ViewBuilder; + +public abstract class AbstractPluginMessageStore implements MessageStore { + protected MessageStore next = null; + protected MessageStorePluginContext context; + + public AbstractPluginMessageStore(MessageStorePluginContext context, MessageStore next) { + this.next = next; + this.context = context; + } + + @Override + public long getEarliestMessageTime() { + return next.getEarliestMessageTime(); + } + + @Override + public long lockTimeMills() { + return next.lockTimeMills(); + } + + @Override + public boolean isOSPageCacheBusy() { + return next.isOSPageCacheBusy(); + } + + @Override + public boolean isTransientStorePoolDeficient() { + return next.isTransientStorePoolDeficient(); + } + + @Override + public boolean load() { + return next.load(); + } + + @Override + public void start() throws Exception { + next.start(); + } + + @Override + public void shutdown() { + next.shutdown(); + } + + @Override + public void destroy() { + next.destroy(); + } + + @Override + public PutMessageResult putMessage(MessageExtBrokerInner msg) { + return next.putMessage(msg); + } + + @Override + public CompletableFuture asyncPutMessage(MessageExtBrokerInner msg) { + return next.asyncPutMessage(msg); + } + + @Override + public CompletableFuture asyncPutMessages(MessageExtBatch messageExtBatch) { + return next.asyncPutMessages(messageExtBatch); + } + + @Override + public GetMessageResult getMessage(String group, String topic, int queueId, long offset, + int maxMsgNums, final MessageFilter messageFilter) { + return next.getMessage(group, topic, queueId, offset, maxMsgNums, messageFilter); + } + + @Override + public CompletableFuture getMessageAsync(String group, String topic, + int queueId, long offset, int maxMsgNums, MessageFilter messageFilter) { + return next.getMessageAsync(group, topic, queueId, offset, maxMsgNums, messageFilter); + } + + @Override + public long getMaxOffsetInQueue(String topic, int queueId) { + return next.getMaxOffsetInQueue(topic, queueId); + } + + @Override + public long getMaxOffsetInQueue(String topic, int queueId, boolean committed) { + return next.getMaxOffsetInQueue(topic, queueId, committed); + } + + @Override + public long getMinOffsetInQueue(String topic, int queueId) { + return next.getMinOffsetInQueue(topic, queueId); + } + + @Override + public long getCommitLogOffsetInQueue(String topic, int queueId, long consumeQueueOffset) { + return next.getCommitLogOffsetInQueue(topic, queueId, consumeQueueOffset); + } + + @Override + public long getOffsetInQueueByTime(String topic, int queueId, long timestamp) { + return next.getOffsetInQueueByTime(topic, queueId, timestamp); + } + + @Override + public MessageExt lookMessageByOffset(long commitLogOffset) { + return next.lookMessageByOffset(commitLogOffset); + } + + @Override + public SelectMappedBufferResult selectOneMessageByOffset(long commitLogOffset) { + return next.selectOneMessageByOffset(commitLogOffset); + } + + @Override + public SelectMappedBufferResult selectOneMessageByOffset(long commitLogOffset, int msgSize) { + return next.selectOneMessageByOffset(commitLogOffset, msgSize); + } + + @Override + public String getRunningDataInfo() { + return next.getRunningDataInfo(); + } + + @Override + public HashMap getRuntimeInfo() { + return next.getRuntimeInfo(); + } + + @Override + public long getMaxPhyOffset() { + return next.getMaxPhyOffset(); + } + + @Override + public long getMinPhyOffset() { + return next.getMinPhyOffset(); + } + + @Override + public long getEarliestMessageTime(String topic, int queueId) { + return next.getEarliestMessageTime(topic, queueId); + } + + @Override + public CompletableFuture getEarliestMessageTimeAsync(String topic, int queueId) { + return next.getEarliestMessageTimeAsync(topic, queueId); + } + + @Override + public long getMessageStoreTimeStamp(String topic, int queueId, long consumeQueueOffset) { + return next.getMessageStoreTimeStamp(topic, queueId, consumeQueueOffset); + } + + @Override + public CompletableFuture getMessageStoreTimeStampAsync(String topic, int queueId, + long consumeQueueOffset) { + return next.getMessageStoreTimeStampAsync(topic, queueId, consumeQueueOffset); + } + + @Override + public long getMessageTotalInQueue(String topic, int queueId) { + return next.getMessageTotalInQueue(topic, queueId); + } + + @Override + public SelectMappedBufferResult getCommitLogData(long offset) { + return next.getCommitLogData(offset); + } + + @Override + public boolean appendToCommitLog(long startOffset, byte[] data, int dataStart, int dataLength) { + return next.appendToCommitLog(startOffset, data, dataStart, dataLength); + } + + @Override + public void executeDeleteFilesManually() { + next.executeDeleteFilesManually(); + } + + @Override + public QueryMessageResult queryMessage(String topic, String key, int maxNum, long begin, + long end) { + return next.queryMessage(topic, key, maxNum, begin, end); + } + + @Override + public CompletableFuture queryMessageAsync(String topic, String key, + int maxNum, long begin, long end) { + return next.queryMessageAsync(topic, key, maxNum, begin, end); + } + + @Override + public long now() { + return next.now(); + } + + @Override + public int deleteTopics(final Set deleteTopics) { + return next.deleteTopics(deleteTopics); + } + + @Override + public int cleanUnusedTopic(final Set retainTopics) { + return next.cleanUnusedTopic(retainTopics); + } + + @Override + public void cleanExpiredConsumerQueue() { + next.cleanExpiredConsumerQueue(); + } + + @Override + @Deprecated + public boolean checkInDiskByConsumeOffset(String topic, int queueId, long consumeOffset) { + return next.checkInDiskByConsumeOffset(topic, queueId, consumeOffset); + } + + @Override + public boolean checkInMemByConsumeOffset(String topic, int queueId, long consumeOffset, int batchSize) { + return next.checkInMemByConsumeOffset(topic, queueId, consumeOffset, batchSize); + } + + @Override + public boolean checkInStoreByConsumeOffset(String topic, int queueId, long consumeOffset) { + return next.checkInStoreByConsumeOffset(topic, queueId, consumeOffset); + } + + @Override + public long dispatchBehindBytes() { + return next.dispatchBehindBytes(); + } + + @Override + public long flush() { + return next.flush(); + } + + @Override + public boolean resetWriteOffset(long phyOffset) { + return next.resetWriteOffset(phyOffset); + } + + @Override + public long getConfirmOffset() { + return next.getConfirmOffset(); + } + + @Override + public void setConfirmOffset(long phyOffset) { + next.setConfirmOffset(phyOffset); + } + + @Override + public LinkedList getDispatcherList() { + return next.getDispatcherList(); + } + + @Override + public void addDispatcher(CommitLogDispatcher dispatcher) { + next.addDispatcher(dispatcher); + } + + @Override + public ConsumeQueueInterface getConsumeQueue(String topic, int queueId) { + return next.getConsumeQueue(topic, queueId); + } + + @Override + public ConsumeQueueInterface findConsumeQueue(String topic, int queueId) { + return next.findConsumeQueue(topic, queueId); + } + + @Override + public BrokerStatsManager getBrokerStatsManager() { + return next.getBrokerStatsManager(); + } + + @Override + public int remainTransientStoreBufferNumbs() { + return next.remainTransientStoreBufferNumbs(); + } + + @Override + public long remainHowManyDataToCommit() { + return next.remainHowManyDataToCommit(); + } + + @Override + public long remainHowManyDataToFlush() { + return next.remainHowManyDataToFlush(); + } + + @Override + public DispatchRequest checkMessageAndReturnSize(final ByteBuffer byteBuffer, final boolean checkCRC, + final boolean checkDupInfo, final boolean readBody) { + return next.checkMessageAndReturnSize(byteBuffer, checkCRC, checkDupInfo, readBody); + } + + @Override + public long getStateMachineVersion() { + return next.getStateMachineVersion(); + } + + @Override + public PutMessageResult putMessages(MessageExtBatch messageExtBatch) { + return next.putMessages(messageExtBatch); + } + + @Override + public HARuntimeInfo getHARuntimeInfo() { + return next.getHARuntimeInfo(); + } + + @Override + public boolean getLastMappedFile(long startOffset) { + return next.getLastMappedFile(startOffset); + } + + @Override + public void updateHaMasterAddress(String newAddr) { + next.updateHaMasterAddress(newAddr); + } + + @Override + public void updateMasterAddress(String newAddr) { + next.updateMasterAddress(newAddr); + } + + @Override + public long slaveFallBehindMuch() { + return next.slaveFallBehindMuch(); + } + + @Override + public long getFlushedWhere() { + return next.getFlushedWhere(); + } + + @Override + public MessageStore getMasterStoreInProcess() { + return next.getMasterStoreInProcess(); + } + + @Override + public void setMasterStoreInProcess(MessageStore masterStoreInProcess) { + next.setMasterStoreInProcess(masterStoreInProcess); + } + + @Override + public boolean getData(long offset, int size, ByteBuffer byteBuffer) { + return next.getData(offset, size, byteBuffer); + } + + @Override + public void setAliveReplicaNumInGroup(int aliveReplicaNums) { + next.setAliveReplicaNumInGroup(aliveReplicaNums); + } + + @Override + public int getAliveReplicaNumInGroup() { + return next.getAliveReplicaNumInGroup(); + } + + @Override + public void wakeupHAClient() { + next.wakeupHAClient(); + } + + @Override + public long getMasterFlushedOffset() { + return next.getMasterFlushedOffset(); + } + + @Override + public long getBrokerInitMaxOffset() { + return next.getBrokerInitMaxOffset(); + } + + @Override + public void setMasterFlushedOffset(long masterFlushedOffset) { + next.setMasterFlushedOffset(masterFlushedOffset); + } + + @Override + public void setBrokerInitMaxOffset(long brokerInitMaxOffset) { + next.setBrokerInitMaxOffset(brokerInitMaxOffset); + } + + @Override + public byte[] calcDeltaChecksum(long from, long to) { + return next.calcDeltaChecksum(from, to); + } + + @Override + public HAService getHaService() { + return next.getHaService(); + } + + @Override + public boolean truncateFiles(long offsetToTruncate) throws RocksDBException { + return next.truncateFiles(offsetToTruncate); + } + + @Override + public boolean isOffsetAligned(long offset) { + return next.isOffsetAligned(offset); + } + + @Override + public RunningFlags getRunningFlags() { + return next.getRunningFlags(); + } + + @Override + public void setSendMessageBackHook(SendMessageBackHook sendMessageBackHook) { + next.setSendMessageBackHook(sendMessageBackHook); + } + + @Override + public SendMessageBackHook getSendMessageBackHook() { + return next.getSendMessageBackHook(); + } + + @Override + public GetMessageResult getMessage(String group, String topic, int queueId, long offset, + int maxMsgNums, int maxTotalMsgSize, MessageFilter messageFilter) { + return next.getMessage(group, topic, queueId, offset, maxMsgNums, maxTotalMsgSize, messageFilter); + } + + @Override + public CompletableFuture getMessageAsync(String group, String topic, + int queueId, long offset, int maxMsgNums, int maxTotalMsgSize, + MessageFilter messageFilter) { + return next.getMessageAsync(group, topic, queueId, offset, maxMsgNums, maxTotalMsgSize, messageFilter); + } + + @Override + public MessageExt lookMessageByOffset(long commitLogOffset, int size) { + return next.lookMessageByOffset(commitLogOffset, size); + } + + @Override + public List getBulkCommitLogData(long offset, int size) { + return next.getBulkCommitLogData(offset, size); + } + + @Override + public void onCommitLogAppend(MessageExtBrokerInner msg, AppendMessageResult result, MappedFile commitLogFile) { + next.onCommitLogAppend(msg, result, commitLogFile); + } + + @Override + public void onCommitLogDispatch(DispatchRequest dispatchRequest, boolean doDispatch, MappedFile commitLogFile, + boolean isRecover, boolean isFileEnd) throws RocksDBException { + next.onCommitLogDispatch(dispatchRequest, doDispatch, commitLogFile, isRecover, isFileEnd); + } + + @Override + public MessageStoreConfig getMessageStoreConfig() { + return next.getMessageStoreConfig(); + } + + @Override + public StoreStatsService getStoreStatsService() { + return next.getStoreStatsService(); + } + + @Override + public StoreCheckpoint getStoreCheckpoint() { + return next.getStoreCheckpoint(); + } + + @Override + public SystemClock getSystemClock() { + return next.getSystemClock(); + } + + @Override + public CommitLog getCommitLog() { + return next.getCommitLog(); + } + + @Override + public TransientStorePool getTransientStorePool() { + return next.getTransientStorePool(); + } + + @Override + public AllocateMappedFileService getAllocateMappedFileService() { + return next.getAllocateMappedFileService(); + } + + @Override + public void truncateDirtyLogicFiles(long phyOffset) throws RocksDBException { + next.truncateDirtyLogicFiles(phyOffset); + } + + @Override + public void unlockMappedFile(MappedFile unlockMappedFile) { + next.unlockMappedFile(unlockMappedFile); + } + + @Override + public PerfCounter.Ticks getPerfCounter() { + return next.getPerfCounter(); + } + + @Override + public ConsumeQueueStoreInterface getQueueStore() { + return next.getQueueStore(); + } + + @Override + public boolean isSyncDiskFlush() { + return next.isSyncDiskFlush(); + } + + @Override + public boolean isSyncMaster() { + return next.isSyncMaster(); + } + + @Override + public void assignOffset(MessageExtBrokerInner msg) throws RocksDBException { + next.assignOffset(msg); + } + + @Override + public void increaseOffset(MessageExtBrokerInner msg, short messageNum) { + next.increaseOffset(msg, messageNum); + } + + @Override + public List getPutMessageHookList() { + return next.getPutMessageHookList(); + } + + @Override + public long getLastFileFromOffset() { + return next.getLastFileFromOffset(); + } + + @Override + public void setPhysicalOffset(long phyOffset) { + next.setPhysicalOffset(phyOffset); + } + + @Override + public boolean isMappedFilesEmpty() { + return next.isMappedFilesEmpty(); + } + + @Override + public TimerMessageStore getTimerMessageStore() { + return next.getTimerMessageStore(); + } + + @Override + public void setTimerMessageStore(TimerMessageStore timerMessageStore) { + next.setTimerMessageStore(timerMessageStore); + } + + @Override + public long getTimingMessageCount(String topic) { + return next.getTimingMessageCount(topic); + } + + @Override + public boolean isShutdown() { + return next.isShutdown(); + } + + @Override + public long estimateMessageCount(String topic, int queueId, long from, long to, MessageFilter filter) { + return next.estimateMessageCount(topic, queueId, from, to, filter); + } + + @Override + public List> getMetricsView() { + return next.getMetricsView(); + } + + @Override + public void initMetrics(Meter meter, Supplier attributesBuilderSupplier) { + next.initMetrics(meter, attributesBuilderSupplier); + } + + @Override + public void finishCommitLogDispatch() { + next.finishCommitLogDispatch(); + } + + @Override + public void recoverTopicQueueTable() { + next.recoverTopicQueueTable(); + } + + @Override + public void notifyMessageArriveIfNecessary(DispatchRequest dispatchRequest) { + next.notifyMessageArriveIfNecessary(dispatchRequest); + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/plugin/MessageStoreFactory.java b/store/src/main/java/org/apache/rocketmq/store/plugin/MessageStoreFactory.java similarity index 79% rename from broker/src/main/java/org/apache/rocketmq/broker/plugin/MessageStoreFactory.java rename to store/src/main/java/org/apache/rocketmq/store/plugin/MessageStoreFactory.java index 8db538b84df..8d929ea5651 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/plugin/MessageStoreFactory.java +++ b/store/src/main/java/org/apache/rocketmq/store/plugin/MessageStoreFactory.java @@ -1,45 +1,45 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.broker.plugin; - -import java.io.IOException; -import java.lang.reflect.Constructor; -import org.apache.rocketmq.store.MessageStore; - -public final class MessageStoreFactory { - public final static MessageStore build(MessageStorePluginContext context, MessageStore messageStore) - throws IOException { - String plugin = context.getBrokerConfig().getMessageStorePlugIn(); - if (plugin != null && plugin.trim().length() != 0) { - String[] pluginClasses = plugin.split(","); - for (int i = pluginClasses.length - 1; i >= 0; --i) { - String pluginClass = pluginClasses[i]; - try { - @SuppressWarnings("unchecked") - Class clazz = (Class) Class.forName(pluginClass); - Constructor construct = clazz.getConstructor(MessageStorePluginContext.class, MessageStore.class); - messageStore = construct.newInstance(context, messageStore); - } catch (Throwable e) { - throw new RuntimeException(String.format( - "Initialize plugin's class %s not found!", pluginClass), e); - } - } - } - return messageStore; - } -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.plugin; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import org.apache.rocketmq.store.MessageStore; + +public final class MessageStoreFactory { + public static MessageStore build(MessageStorePluginContext context, + MessageStore messageStore) throws IOException { + String plugin = context.getBrokerConfig().getMessageStorePlugIn(); + if (plugin != null && plugin.trim().length() != 0) { + String[] pluginClasses = plugin.split(","); + for (int i = pluginClasses.length - 1; i >= 0; --i) { + String pluginClass = pluginClasses[i]; + try { + @SuppressWarnings("unchecked") + Class clazz = (Class) Class.forName(pluginClass); + Constructor construct = clazz.getConstructor(MessageStorePluginContext.class, MessageStore.class); + AbstractPluginMessageStore pluginMessageStore = construct.newInstance(context, messageStore); + messageStore = pluginMessageStore; + } catch (Throwable e) { + throw new RuntimeException("Initialize plugin's class: " + pluginClass + " not found!", e); + } + } + } + return messageStore; + } +} diff --git a/broker/src/main/java/org/apache/rocketmq/broker/plugin/MessageStorePluginContext.java b/store/src/main/java/org/apache/rocketmq/store/plugin/MessageStorePluginContext.java similarity index 81% rename from broker/src/main/java/org/apache/rocketmq/broker/plugin/MessageStorePluginContext.java rename to store/src/main/java/org/apache/rocketmq/store/plugin/MessageStorePluginContext.java index b822a2f7585..d39ccddf8aa 100644 --- a/broker/src/main/java/org/apache/rocketmq/broker/plugin/MessageStorePluginContext.java +++ b/store/src/main/java/org/apache/rocketmq/store/plugin/MessageStorePluginContext.java @@ -1,57 +1,65 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.broker.plugin; - -import org.apache.rocketmq.common.BrokerConfig; -import org.apache.rocketmq.store.MessageArrivingListener; -import org.apache.rocketmq.store.config.MessageStoreConfig; -import org.apache.rocketmq.store.stats.BrokerStatsManager; - -public class MessageStorePluginContext { - private MessageStoreConfig messageStoreConfig; - private BrokerStatsManager brokerStatsManager; - private MessageArrivingListener messageArrivingListener; - private BrokerConfig brokerConfig; - - public MessageStorePluginContext(MessageStoreConfig messageStoreConfig, - BrokerStatsManager brokerStatsManager, MessageArrivingListener messageArrivingListener, - BrokerConfig brokerConfig) { - super(); - this.messageStoreConfig = messageStoreConfig; - this.brokerStatsManager = brokerStatsManager; - this.messageArrivingListener = messageArrivingListener; - this.brokerConfig = brokerConfig; - } - - public MessageStoreConfig getMessageStoreConfig() { - return messageStoreConfig; - } - - public BrokerStatsManager getBrokerStatsManager() { - return brokerStatsManager; - } - - public MessageArrivingListener getMessageArrivingListener() { - return messageArrivingListener; - } - - public BrokerConfig getBrokerConfig() { - return brokerConfig; - } - -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.plugin; + +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.remoting.Configuration; +import org.apache.rocketmq.store.MessageArrivingListener; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.stats.BrokerStatsManager; + +public class MessageStorePluginContext { + private MessageStoreConfig messageStoreConfig; + private BrokerStatsManager brokerStatsManager; + private MessageArrivingListener messageArrivingListener; + private BrokerConfig brokerConfig; + private final Configuration configuration; + + public MessageStorePluginContext(MessageStoreConfig messageStoreConfig, + BrokerStatsManager brokerStatsManager, MessageArrivingListener messageArrivingListener, + BrokerConfig brokerConfig, Configuration configuration) { + super(); + this.messageStoreConfig = messageStoreConfig; + this.brokerStatsManager = brokerStatsManager; + this.messageArrivingListener = messageArrivingListener; + this.brokerConfig = brokerConfig; + this.configuration = configuration; + } + + public MessageStoreConfig getMessageStoreConfig() { + return messageStoreConfig; + } + + public BrokerStatsManager getBrokerStatsManager() { + return brokerStatsManager; + } + + public MessageArrivingListener getMessageArrivingListener() { + return messageArrivingListener; + } + + public BrokerConfig getBrokerConfig() { + return brokerConfig; + } + + public void registerConfiguration(Object config) { + MixAll.properties2Object(configuration.getAllConfigs(), config); + configuration.registerConfig(config); + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/pop/AckMsg.java b/store/src/main/java/org/apache/rocketmq/store/pop/AckMsg.java new file mode 100644 index 00000000000..3e65c104b12 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/pop/AckMsg.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.pop; + +import com.alibaba.fastjson.annotation.JSONField; + +public class AckMsg { + + @JSONField(name = "ao", alternateNames = {"ackOffset"}) + private long ackOffset; + + @JSONField(name = "so", alternateNames = {"startOffset"}) + private long startOffset; + + @JSONField(name = "c", alternateNames = {"consumerGroup"}) + private String consumerGroup; + + @JSONField(name = "t", alternateNames = {"topic"}) + private String topic; + + @JSONField(name = "q", alternateNames = {"queueId"}) + private int queueId; + + @JSONField(name = "pt", alternateNames = {"popTime"}) + private long popTime; + + @JSONField(name = "bn", alternateNames = {"brokerName"}) + private String brokerName; + + public long getPopTime() { + return popTime; + } + + public void setPopTime(long popTime) { + this.popTime = popTime; + } + + public void setQueueId(int queueId) { + this.queueId = queueId; + } + + public int getQueueId() { + return queueId; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getTopic() { + return topic; + } + + public long getAckOffset() { + return ackOffset; + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public void setAckOffset(long ackOffset) { + this.ackOffset = ackOffset; + } + + public long getStartOffset() { + return startOffset; + } + + public void setStartOffset(long startOffset) { + this.startOffset = startOffset; + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("AckMsg{"); + sb.append("ackOffset=").append(ackOffset); + sb.append(", startOffset=").append(startOffset); + sb.append(", consumerGroup='").append(consumerGroup).append('\''); + sb.append(", topic='").append(topic).append('\''); + sb.append(", queueId=").append(queueId); + sb.append(", popTime=").append(popTime); + sb.append(", brokerName=").append(brokerName); + sb.append('}'); + return sb.toString(); + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/pop/BatchAckMsg.java b/store/src/main/java/org/apache/rocketmq/store/pop/BatchAckMsg.java new file mode 100644 index 00000000000..991a1f085de --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/pop/BatchAckMsg.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.pop; + +import com.alibaba.fastjson.annotation.JSONField; +import java.util.ArrayList; +import java.util.List; + + +public class BatchAckMsg extends AckMsg { + @JSONField(name = "aol", alternateNames = {"ackOffsetList"}) + private List ackOffsetList = new ArrayList(32); + + + public List getAckOffsetList() { + return ackOffsetList; + } + + public void setAckOffsetList(List ackOffsetList) { + this.ackOffsetList = ackOffsetList; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("BatchAckMsg{"); + sb.append("ackOffsetList=").append(ackOffsetList); + sb.append(", startOffset=").append(getStartOffset()); + sb.append(", consumerGroup='").append(getConsumerGroup()).append('\''); + sb.append(", topic='").append(getTopic()).append('\''); + sb.append(", queueId=").append(getQueueId()); + sb.append(", popTime=").append(getPopTime()); + sb.append(", brokerName=").append(getBrokerName()); + sb.append('}'); + return sb.toString(); + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/pop/PopCheckPoint.java b/store/src/main/java/org/apache/rocketmq/store/pop/PopCheckPoint.java new file mode 100644 index 00000000000..e041b66d9c5 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/pop/PopCheckPoint.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.pop; + +import com.alibaba.fastjson.annotation.JSONField; +import java.util.ArrayList; +import java.util.List; + +public class PopCheckPoint implements Comparable { + @JSONField(name = "so") + private long startOffset; + @JSONField(name = "pt") + private long popTime; + @JSONField(name = "it") + private long invisibleTime; + @JSONField(name = "bm") + private int bitMap; + @JSONField(name = "n") + private byte num; + @JSONField(name = "q") + private int queueId; + @JSONField(name = "t") + private String topic; + @JSONField(name = "c") + private String cid; + @JSONField(name = "ro") + private long reviveOffset; + @JSONField(name = "d") + private List queueOffsetDiff; + @JSONField(name = "bn") + String brokerName; + + public long getReviveOffset() { + return reviveOffset; + } + + public void setReviveOffset(long reviveOffset) { + this.reviveOffset = reviveOffset; + } + + public long getStartOffset() { + return startOffset; + } + + public void setStartOffset(long startOffset) { + this.startOffset = startOffset; + } + + public void setPopTime(long popTime) { + this.popTime = popTime; + } + + public void setInvisibleTime(long invisibleTime) { + this.invisibleTime = invisibleTime; + } + + public long getPopTime() { + return popTime; + } + + public long getInvisibleTime() { + return invisibleTime; + } + + public long getReviveTime() { + return popTime + invisibleTime; + } + + public int getBitMap() { + return bitMap; + } + + public void setBitMap(int bitMap) { + this.bitMap = bitMap; + } + + public byte getNum() { + return num; + } + + public void setNum(byte num) { + this.num = num; + } + + public int getQueueId() { + return queueId; + } + + public void setQueueId(int queueId) { + this.queueId = queueId; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getCId() { + return cid; + } + + public void setCId(String cid) { + this.cid = cid; + } + + public List getQueueOffsetDiff() { + return queueOffsetDiff; + } + + public void setQueueOffsetDiff(List queueOffsetDiff) { + this.queueOffsetDiff = queueOffsetDiff; + } + + public String getBrokerName() { + return brokerName; + } + + public void setBrokerName(String brokerName) { + this.brokerName = brokerName; + } + + public void addDiff(int diff) { + if (this.queueOffsetDiff == null) { + this.queueOffsetDiff = new ArrayList<>(8); + } + this.queueOffsetDiff.add(diff); + } + + public int indexOfAck(long ackOffset) { + if (ackOffset < startOffset) { + return -1; + } + + // old version of checkpoint + if (queueOffsetDiff == null || queueOffsetDiff.isEmpty()) { + + if (ackOffset - startOffset < num) { + return (int) (ackOffset - startOffset); + } + + return -1; + } + + // new version of checkpoint + return queueOffsetDiff.indexOf((int) (ackOffset - startOffset)); + } + + public long ackOffsetByIndex(byte index) { + // old version of checkpoint + if (queueOffsetDiff == null || queueOffsetDiff.isEmpty()) { + return startOffset + index; + } + + return startOffset + queueOffsetDiff.get(index); + } + + @Override + public String toString() { + return "PopCheckPoint [topic=" + topic + ", cid=" + cid + ", queueId=" + queueId + ", startOffset=" + startOffset + ", bitMap=" + bitMap + ", num=" + num + ", reviveTime=" + getReviveTime() + + ", reviveOffset=" + reviveOffset + ", diff=" + queueOffsetDiff + ", brokerName=" + brokerName + "]"; + } + + @Override + public int compareTo(PopCheckPoint o) { + return (int) (this.getStartOffset() - o.getStartOffset()); + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/queue/AbstractConsumeQueueStore.java b/store/src/main/java/org/apache/rocketmq/store/queue/AbstractConsumeQueueStore.java new file mode 100644 index 00000000000..d76b0557737 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/AbstractConsumeQueueStore.java @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.queue; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.DispatchRequest; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.rocksdb.RocksDBException; + +public abstract class AbstractConsumeQueueStore implements ConsumeQueueStoreInterface { + protected static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + + protected final DefaultMessageStore messageStore; + protected final MessageStoreConfig messageStoreConfig; + protected final QueueOffsetOperator queueOffsetOperator = new QueueOffsetOperator(); + protected final ConcurrentMap> consumeQueueTable; + + public AbstractConsumeQueueStore(DefaultMessageStore messageStore) { + this.messageStore = messageStore; + this.messageStoreConfig = messageStore.getMessageStoreConfig(); + this.consumeQueueTable = new ConcurrentHashMap<>(32); + } + + @Override + public void putMessagePositionInfoWrapper(ConsumeQueueInterface consumeQueue, DispatchRequest request) { + consumeQueue.putMessagePositionInfoWrapper(request); + } + + @Override + public Long getMaxOffset(String topic, int queueId) { + return this.queueOffsetOperator.currentQueueOffset(topic + "-" + queueId); + } + + @Override + public void setTopicQueueTable(ConcurrentMap topicQueueTable) { + this.queueOffsetOperator.setTopicQueueTable(topicQueueTable); + this.queueOffsetOperator.setLmqTopicQueueTable(topicQueueTable); + } + + @Override + public ConcurrentMap getTopicQueueTable() { + return this.queueOffsetOperator.getTopicQueueTable(); + } + + @Override + public void assignQueueOffset(MessageExtBrokerInner msg) throws RocksDBException { + ConsumeQueueInterface consumeQueue = findOrCreateConsumeQueue(msg.getTopic(), msg.getQueueId()); + consumeQueue.assignQueueOffset(this.queueOffsetOperator, msg); + } + + @Override + public void increaseQueueOffset(MessageExtBrokerInner msg, short messageNum) { + ConsumeQueueInterface consumeQueue = findOrCreateConsumeQueue(msg.getTopic(), msg.getQueueId()); + consumeQueue.increaseQueueOffset(this.queueOffsetOperator, msg, messageNum); + } + + @Override + public void increaseLmqOffset(String queueKey, short messageNum) { + queueOffsetOperator.increaseLmqOffset(queueKey, messageNum); + } + + @Override + public long getLmqQueueOffset(String queueKey) { + return queueOffsetOperator.getLmqOffset(queueKey); + } + + @Override + public void removeTopicQueueTable(String topic, Integer queueId) { + this.queueOffsetOperator.remove(topic, queueId); + } + + @Override + public ConcurrentMap> getConsumeQueueTable() { + return this.consumeQueueTable; + } + + @Override + public ConcurrentMap findConsumeQueueMap(String topic) { + return this.consumeQueueTable.get(topic); + } + + @Override + public long getStoreTime(CqUnit cqUnit) { + if (cqUnit != null) { + try { + final long phyOffset = cqUnit.getPos(); + final int size = cqUnit.getSize(); + long storeTime = this.messageStore.getCommitLog().pickupStoreTimestamp(phyOffset, size); + return storeTime; + } catch (Exception e) { + } + } + return -1; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/queue/BatchConsumeQueue.java b/store/src/main/java/org/apache/rocketmq/store/queue/BatchConsumeQueue.java new file mode 100644 index 00000000000..7108c835c8e --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/BatchConsumeQueue.java @@ -0,0 +1,1173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.queue; + +import java.io.File; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.function.Function; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.BoundaryType; +import org.apache.rocketmq.common.attribute.CQType; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.DispatchRequest; +import org.apache.rocketmq.store.MappedFileQueue; +import org.apache.rocketmq.store.MessageFilter; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.config.BrokerRole; +import org.apache.rocketmq.store.logfile.MappedFile; + +public class BatchConsumeQueue implements ConsumeQueueInterface { + protected static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + + /** + * BatchConsumeQueue's store unit. Format: + *

    +     * ┌─────────────────────────┬───────────┬────────────┬──────────┬─────────────┬─────────┬───────────────┬─────────┐
    +     * │CommitLog Physical Offset│ Body Size │Tag HashCode│Store time│msgBaseOffset│batchSize│compactedOffset│reserved │
    +     * │        (8 Bytes)        │ (4 Bytes) │ (8 Bytes)  │(8 Bytes) │(8 Bytes)    │(2 Bytes)│   (4 Bytes)   │(4 Bytes)│
    +     * ├─────────────────────────┴───────────┴────────────┴──────────┴─────────────┴─────────┴───────────────┴─────────┤
    +     * │                                                  Store Unit                                                   │
    +     * │                                                                                                               │
    +     * 
    + * BatchConsumeQueue's store unit. Size: + * CommitLog Physical Offset(8) + Body Size(4) + Tag HashCode(8) + Store time(8) + + * msgBaseOffset(8) + batchSize(2) + compactedOffset(4) + reserved(4)= 46 Bytes + */ + public static final int CQ_STORE_UNIT_SIZE = 46; + public static final int MSG_TAG_OFFSET_INDEX = 12; + public static final int MSG_STORE_TIME_OFFSET_INDEX = 20; + public static final int MSG_BASE_OFFSET_INDEX = 28; + public static final int MSG_BATCH_SIZE_INDEX = 36; + public static final int MSG_COMPACT_OFFSET_INDEX = 38; + private static final int MSG_COMPACT_OFFSET_LENGTH = 4; + public static final int INVALID_POS = -1; + protected final MappedFileQueue mappedFileQueue; + protected MessageStore messageStore; + protected final String topic; + protected final int queueId; + protected final ByteBuffer byteBufferItem; + + protected final String storePath; + protected final int mappedFileSize; + protected volatile long maxMsgPhyOffsetInCommitLog = -1; + + protected volatile long minLogicOffset = 0; + + protected volatile long maxOffsetInQueue = 0; + protected volatile long minOffsetInQueue = -1; + protected final int commitLogSize; + + protected ConcurrentSkipListMap offsetCache = new ConcurrentSkipListMap<>(); + protected ConcurrentSkipListMap timeCache = new ConcurrentSkipListMap<>(); + + public BatchConsumeQueue( + final String topic, + final int queueId, + final String storePath, + final int mappedFileSize, + final MessageStore messageStore, + final String subfolder) { + this.storePath = storePath; + this.mappedFileSize = mappedFileSize; + this.messageStore = messageStore; + this.commitLogSize = messageStore.getCommitLog().getCommitLogSize(); + + this.topic = topic; + this.queueId = queueId; + + if (StringUtils.isBlank(subfolder)) { + String queueDir = this.storePath + File.separator + topic + File.separator + queueId; + this.mappedFileQueue = new MappedFileQueue(queueDir, mappedFileSize, null); + } else { + String queueDir = this.storePath + File.separator + topic + File.separator + queueId + File.separator + subfolder; + this.mappedFileQueue = new MappedFileQueue(queueDir, mappedFileSize, null); + } + + this.byteBufferItem = ByteBuffer.allocate(CQ_STORE_UNIT_SIZE); + } + + public BatchConsumeQueue( + final String topic, + final int queueId, + final String storePath, + final int mappedFileSize, + final MessageStore defaultMessageStore) { + this(topic, queueId, storePath, mappedFileSize, defaultMessageStore, StringUtils.EMPTY); + } + + @Override + public boolean load() { + boolean result = this.mappedFileQueue.load(); + log.info("Load batch consume queue {}-{} {} {}", topic, queueId, result ? "OK" : "Failed", mappedFileQueue.getMappedFiles().size()); + return result; + } + + protected void doRefreshCache(Function offsetFunction) { + if (!this.messageStore.getMessageStoreConfig().isSearchBcqByCacheEnable()) { + return; + } + ConcurrentSkipListMap newOffsetCache = new ConcurrentSkipListMap<>(); + ConcurrentSkipListMap newTimeCache = new ConcurrentSkipListMap<>(); + + List mappedFiles = mappedFileQueue.getMappedFiles(); + // iterate all BCQ files + for (int i = 0; i < mappedFiles.size(); i++) { + MappedFile bcq = mappedFiles.get(i); + if (isNewFile(bcq)) { + continue; + } + + BatchOffsetIndex offset = offsetFunction.apply(bcq); + if (offset == null) { + continue; + } + newOffsetCache.put(offset.getMsgOffset(), offset.getMappedFile()); + newTimeCache.put(offset.getStoreTimestamp(), offset.getMappedFile()); + } + + this.offsetCache = newOffsetCache; + this.timeCache = newTimeCache; + + log.info("refreshCache for BCQ [Topic: {}, QueueId: {}]." + + "offsetCacheSize: {}, minCachedMsgOffset: {}, maxCachedMsgOffset: {}, " + + "timeCacheSize: {}, minCachedTime: {}, maxCachedTime: {}", this.topic, this.queueId, + this.offsetCache.size(), this.offsetCache.firstEntry(), this.offsetCache.lastEntry(), + this.timeCache.size(), this.timeCache.firstEntry(), this.timeCache.lastEntry()); + } + + protected void refreshCache() { + doRefreshCache(m -> getMinMsgOffset(m, false, true)); + } + + private void destroyCache() { + this.offsetCache.clear(); + this.timeCache.clear(); + + log.info("BCQ [Topic: {}, QueueId: {}]. Cache destroyed", this.topic, this.queueId); + } + + protected void cacheBcq(MappedFile bcq) { + try { + BatchOffsetIndex min = getMinMsgOffset(bcq, false, true); + this.offsetCache.put(min.getMsgOffset(), min.getMappedFile()); + this.timeCache.put(min.getStoreTimestamp(), min.getMappedFile()); + } catch (Exception e) { + log.error("Failed caching offset and time on BCQ [Topic: {}, QueueId: {}, File: {}]", this.topic, this.queueId, bcq); + } + } + + protected boolean isNewFile(MappedFile mappedFile) { + return mappedFile.getReadPosition() < CQ_STORE_UNIT_SIZE; + } + + protected MappedFile searchOffsetFromCache(long msgOffset) { + Map.Entry floorEntry = this.offsetCache.floorEntry(msgOffset); + if (floorEntry == null) { + // the offset is too small. + return null; + } else { + return floorEntry.getValue(); + } + } + + private MappedFile searchTimeFromCache(long time) { + Map.Entry floorEntry = this.timeCache.floorEntry(time); + if (floorEntry == null) { + // the timestamp is too small. so we decide to result first BCQ file. + return this.mappedFileQueue.getFirstMappedFile(); + } else { + return floorEntry.getValue(); + } + } + + @Override + public void recover() { + final List mappedFiles = this.mappedFileQueue.getMappedFiles(); + if (!mappedFiles.isEmpty()) { + int index = mappedFiles.size() - 3; + if (index < 0) + index = 0; + + int mappedFileSizeLogics = this.mappedFileSize; + MappedFile mappedFile = mappedFiles.get(index); + ByteBuffer byteBuffer = mappedFile.sliceByteBuffer(); + long processOffset = mappedFile.getFileFromOffset(); + long mappedFileOffset = 0; + while (true) { + for (int i = 0; i < mappedFileSizeLogics; i += CQ_STORE_UNIT_SIZE) { + byteBuffer.position(i); + long offset = byteBuffer.getLong(); + int size = byteBuffer.getInt(); + byteBuffer.getLong();//tagscode + byteBuffer.getLong();//timestamp + long msgBaseOffset = byteBuffer.getLong(); + short batchSize = byteBuffer.getShort(); + if (offset >= 0 && size > 0 && msgBaseOffset >= 0 && batchSize > 0) { + mappedFileOffset = i + CQ_STORE_UNIT_SIZE; + this.maxMsgPhyOffsetInCommitLog = offset; + } else { + log.info("Recover current batch consume queue file over, file:{} offset:{} size:{} msgBaseOffset:{} batchSize:{} mappedFileOffset:{}", + mappedFile.getFileName(), offset, size, msgBaseOffset, batchSize, mappedFileOffset); + break; + } + } + + if (mappedFileOffset == mappedFileSizeLogics) { + index++; + if (index >= mappedFiles.size()) { + log.info("Recover last batch consume queue file over, last mapped file:{} ", mappedFile.getFileName()); + break; + } else { + mappedFile = mappedFiles.get(index); + byteBuffer = mappedFile.sliceByteBuffer(); + processOffset = mappedFile.getFileFromOffset(); + mappedFileOffset = 0; + log.info("Recover next batch consume queue file: " + mappedFile.getFileName()); + } + } else { + log.info("Recover current batch consume queue file over:{} processOffset:{}", mappedFile.getFileName(), processOffset + mappedFileOffset); + break; + } + } + processOffset += mappedFileOffset; + this.mappedFileQueue.setFlushedWhere(processOffset); + this.mappedFileQueue.setCommittedWhere(processOffset); + this.mappedFileQueue.truncateDirtyFiles(processOffset); + reviseMaxAndMinOffsetInQueue(); + } + } + + void reviseMinOffsetInQueue() { + MappedFile firstMappedFile = this.mappedFileQueue.getFirstMappedFile(); + if (null == firstMappedFile) { + maxOffsetInQueue = 0; + minOffsetInQueue = -1; + minLogicOffset = -1; + log.info("reviseMinOffsetInQueue found firstMappedFile null, topic:{} queue:{}", topic, queueId); + return; + } + minLogicOffset = firstMappedFile.getFileFromOffset(); + BatchOffsetIndex min = getMinMsgOffset(firstMappedFile, false, false); + minOffsetInQueue = null == min ? -1 : min.getMsgOffset(); + } + + void reviseMaxOffsetInQueue() { + MappedFile lastMappedFile = this.mappedFileQueue.getLastMappedFile(); + BatchOffsetIndex max = getMaxMsgOffset(lastMappedFile, true, false); + if (null == max && this.mappedFileQueue.getMappedFiles().size() >= 2) { + MappedFile lastTwoMappedFile = this.mappedFileQueue.getMappedFiles().get(this.mappedFileQueue.getMappedFiles().size() - 2); + max = getMaxMsgOffset(lastTwoMappedFile, true, false); + } + maxOffsetInQueue = (null == max) ? 0 : max.getMsgOffset() + max.getBatchSize(); + } + + void reviseMaxAndMinOffsetInQueue() { + reviseMinOffsetInQueue(); + reviseMaxOffsetInQueue(); + } + + @Override + public long getMaxPhysicOffset() { + return maxMsgPhyOffsetInCommitLog; + } + + @Override + public long getMinLogicOffset() { + return minLogicOffset; + } + + @Override + public ReferredIterator iterateFrom(long startOffset) { + SelectMappedBufferResult sbr = getBatchMsgIndexBuffer(startOffset); + if (sbr == null) { + return null; + } + return new BatchConsumeQueueIterator(sbr); + } + + @Override + public ReferredIterator iterateFrom(long startIndex, int count) { + return iterateFrom(startIndex); + } + + @Override + public CqUnit get(long offset) { + ReferredIterator it = iterateFrom(offset); + if (it == null) { + return null; + } + return it.nextAndRelease(); + } + + @Override + public Pair getCqUnitAndStoreTime(long index) { + CqUnit cqUnit = get(index); + Long messageStoreTime = this.messageStore.getQueueStore().getStoreTime(cqUnit); + return new Pair<>(cqUnit, messageStoreTime); + } + + @Override + public Pair getEarliestUnitAndStoreTime() { + CqUnit cqUnit = getEarliestUnit(); + Long messageStoreTime = this.messageStore.getQueueStore().getStoreTime(cqUnit); + return new Pair<>(cqUnit, messageStoreTime); + } + + @Override + public CqUnit getEarliestUnit() { + return get(minOffsetInQueue); + } + + @Override + public CqUnit getLatestUnit() { + return get(maxOffsetInQueue - 1); + } + + @Override + public long getLastOffset() { + CqUnit latestUnit = getLatestUnit(); + return latestUnit.getPos() + latestUnit.getSize(); + } + + @Override + public boolean isFirstFileAvailable() { + MappedFile mappedFile = this.mappedFileQueue.getFirstMappedFile(); + if (mappedFile != null) { + return mappedFile.isAvailable(); + } + return false; + } + + @Override + public boolean isFirstFileExist() { + MappedFile mappedFile = this.mappedFileQueue.getFirstMappedFile(); + return mappedFile != null; + } + + @Override + public void truncateDirtyLogicFiles(long phyOffset) { + + long oldMinOffset = minOffsetInQueue; + long oldMaxOffset = maxOffsetInQueue; + + int logicFileSize = this.mappedFileSize; + + this.maxMsgPhyOffsetInCommitLog = phyOffset - 1; + boolean stop = false; + while (!stop) { + MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(); + if (mappedFile != null) { + ByteBuffer byteBuffer = mappedFile.sliceByteBuffer(); + + mappedFile.setWrotePosition(0); + mappedFile.setCommittedPosition(0); + mappedFile.setFlushedPosition(0); + + for (int i = 0; i < logicFileSize; i += CQ_STORE_UNIT_SIZE) { + byteBuffer.position(i); + long offset = byteBuffer.getLong(); + int size = byteBuffer.getInt(); + byteBuffer.getLong();//tagscode + byteBuffer.getLong();//timestamp + long msgBaseOffset = byteBuffer.getLong(); + short batchSize = byteBuffer.getShort(); + + if (0 == i) { + if (offset >= phyOffset) { + this.mappedFileQueue.deleteLastMappedFile(); + break; + } else { + int pos = i + CQ_STORE_UNIT_SIZE; + mappedFile.setWrotePosition(pos); + mappedFile.setCommittedPosition(pos); + mappedFile.setFlushedPosition(pos); + this.maxMsgPhyOffsetInCommitLog = offset; + } + } else { + if (offset >= 0 && size > 0 && msgBaseOffset >= 0 && batchSize > 0) { + if (offset >= phyOffset) { + stop = true; + break; + } + + int pos = i + CQ_STORE_UNIT_SIZE; + mappedFile.setWrotePosition(pos); + mappedFile.setCommittedPosition(pos); + mappedFile.setFlushedPosition(pos); + this.maxMsgPhyOffsetInCommitLog = offset; + if (pos == logicFileSize) { + stop = true; + break; + } + } else { + stop = true; + break; + } + } + } + } else { + break; + } + } + reviseMaxAndMinOffsetInQueue(); + log.info("Truncate batch logic file topic={} queue={} oldMinOffset={} oldMaxOffset={} minOffset={} maxOffset={} maxPhyOffsetHere={} maxPhyOffsetThere={}", + topic, queueId, oldMinOffset, oldMaxOffset, minOffsetInQueue, maxOffsetInQueue, maxMsgPhyOffsetInCommitLog, phyOffset); + } + + @Override + public boolean flush(final int flushLeastPages) { + boolean result = this.mappedFileQueue.flush(flushLeastPages); + return result; + } + + @Override + public int deleteExpiredFile(long minCommitLogPos) { + int cnt = this.mappedFileQueue.deleteExpiredFileByOffset(minCommitLogPos, CQ_STORE_UNIT_SIZE); + this.correctMinOffset(minCommitLogPos); + return cnt; + } + + @Override + public void correctMinOffset(long phyMinOffset) { + reviseMinOffsetInQueue(); + refreshCache(); + long oldMinOffset = minOffsetInQueue; + MappedFile mappedFile = this.mappedFileQueue.getFirstMappedFile(); + if (mappedFile != null) { + SelectMappedBufferResult result = mappedFile.selectMappedBuffer(0); + if (result != null) { + try { + int startPos = result.getByteBuffer().position(); + for (int i = 0; i < result.getSize(); i += BatchConsumeQueue.CQ_STORE_UNIT_SIZE) { + result.getByteBuffer().position(startPos + i); + long offsetPy = result.getByteBuffer().getLong(); + result.getByteBuffer().getInt(); //size + result.getByteBuffer().getLong();//tagscode + result.getByteBuffer().getLong();//timestamp + long msgBaseOffset = result.getByteBuffer().getLong(); + short batchSize = result.getByteBuffer().getShort(); + + if (offsetPy < phyMinOffset) { + this.minOffsetInQueue = msgBaseOffset + batchSize; + } else { + break; + } + } + } catch (Exception e) { + log.error("Exception thrown when correctMinOffset", e); + } finally { + result.release(); + } + } else { + /** + * It will go to here under two conditions: + 1. the files number is 1, and it has no data + 2. the pull process hold the cq reference, and release it just the moment + */ + log.warn("Correct min offset found null cq file topic:{} queue:{} files:{} minOffset:{} maxOffset:{}", + topic, queueId, this.mappedFileQueue.getMappedFiles().size(), minOffsetInQueue, maxOffsetInQueue); + } + } + if (oldMinOffset != this.minOffsetInQueue) { + log.info("BatchCQ Compute new minOffset:{} oldMinOffset{} topic:{} queue:{}", minOffsetInQueue, oldMinOffset, topic, queueId); + } + } + + @Override + public void putMessagePositionInfoWrapper(DispatchRequest request) { + final int maxRetries = 30; + boolean canWrite = this.messageStore.getRunningFlags().isCQWriteable(); + if (request.getMsgBaseOffset() < 0 || request.getBatchSize() < 0) { + log.warn("[NOTIFYME]unexpected dispatch request in batch consume queue topic:{} queue:{} offset:{}", topic, queueId, request.getCommitLogOffset()); + return; + } + for (int i = 0; i < maxRetries && canWrite; i++) { + boolean result = this.putBatchMessagePositionInfo(request.getCommitLogOffset(), + request.getMsgSize(), request.getTagsCode(), + request.getStoreTimestamp(), request.getMsgBaseOffset(), request.getBatchSize()); + if (result) { + if (BrokerRole.SLAVE == this.messageStore.getMessageStoreConfig().getBrokerRole()) { + this.messageStore.getStoreCheckpoint().setPhysicMsgTimestamp(request.getStoreTimestamp()); + } + this.messageStore.getStoreCheckpoint().setLogicsMsgTimestamp(request.getStoreTimestamp()); + return; + } else { + // XXX: warn and notify me + log.warn("[NOTIFYME]put commit log position info to batch consume queue " + topic + ":" + queueId + " " + request.getCommitLogOffset() + + " failed, retry " + i + " times"); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + log.warn("", e); + } + } + } + // XXX: warn and notify me + log.error("[NOTIFYME]batch consume queue can not write, {} {}", this.topic, this.queueId); + this.messageStore.getRunningFlags().makeLogicsQueueError(); + } + + @Override + public void assignQueueOffset(QueueOffsetOperator queueOffsetOperator, MessageExtBrokerInner msg) { + String topicQueueKey = getTopic() + "-" + getQueueId(); + + long queueOffset = queueOffsetOperator.getBatchQueueOffset(topicQueueKey); + + if (MessageSysFlag.check(msg.getSysFlag(), MessageSysFlag.INNER_BATCH_FLAG)) { + MessageAccessor.putProperty(msg, MessageConst.PROPERTY_INNER_BASE, String.valueOf(queueOffset)); + msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); + } + msg.setQueueOffset(queueOffset); + } + + @Override + public void increaseQueueOffset(QueueOffsetOperator queueOffsetOperator, MessageExtBrokerInner msg, + short messageNum) { + String topicQueueKey = getTopic() + "-" + getQueueId(); + queueOffsetOperator.increaseBatchQueueOffset(topicQueueKey, messageNum); + } + + public boolean putBatchMessagePositionInfo(final long offset, final int size, final long tagsCode, + final long storeTime, + final long msgBaseOffset, final short batchSize) { + + if (offset <= this.maxMsgPhyOffsetInCommitLog) { + if (System.currentTimeMillis() % 1000 == 0) { + log.warn("Build batch consume queue repeatedly, maxMsgPhyOffsetInCommitLog:{} offset:{} Topic: {} QID: {}", + maxMsgPhyOffsetInCommitLog, offset, this.topic, this.queueId); + } + return true; + } + + long behind = System.currentTimeMillis() - storeTime; + if (behind > 10000 && System.currentTimeMillis() % 10000 == 0) { + String flag = "LEVEL" + (behind / 10000); + log.warn("Reput behind {} topic:{} queue:{} offset:{} behind:{}", flag, topic, queueId, offset, behind); + } + + this.byteBufferItem.flip(); + this.byteBufferItem.limit(CQ_STORE_UNIT_SIZE); + this.byteBufferItem.putLong(offset); + this.byteBufferItem.putInt(size); + this.byteBufferItem.putLong(tagsCode); + this.byteBufferItem.putLong(storeTime); + this.byteBufferItem.putLong(msgBaseOffset); + this.byteBufferItem.putShort(batchSize); + this.byteBufferItem.putInt(INVALID_POS); + this.byteBufferItem.putInt(0); // 4 bytes reserved + + MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(this.mappedFileQueue.getMaxOffset()); + if (mappedFile != null) { + boolean isNewFile = isNewFile(mappedFile); + boolean appendRes = mappedFile.appendMessage(this.byteBufferItem.array()); + if (appendRes) { + maxMsgPhyOffsetInCommitLog = offset; + maxOffsetInQueue = msgBaseOffset + batchSize; + //only the first time need to correct the minOffsetInQueue + //the other correctness is done in correctLogicMinoffsetService + if (mappedFile.isFirstCreateInQueue() && minOffsetInQueue == -1) { + reviseMinOffsetInQueue(); + } + if (isNewFile) { + // cache new file + this.cacheBcq(mappedFile); + } + } + return appendRes; + } + return false; + } + + protected BatchOffsetIndex getMinMsgOffset(MappedFile mappedFile, boolean getBatchSize, boolean getStoreTime) { + if (mappedFile.getReadPosition() < CQ_STORE_UNIT_SIZE) { + return null; + } + return getBatchOffsetIndexByPos(mappedFile, 0, getBatchSize, getStoreTime); + } + + protected BatchOffsetIndex getBatchOffsetIndexByPos(MappedFile mappedFile, int pos, boolean getBatchSize, + boolean getStoreTime) { + SelectMappedBufferResult sbr = mappedFile.selectMappedBuffer(pos); + try { + return new BatchOffsetIndex(mappedFile, pos, sbr.getByteBuffer().getLong(MSG_BASE_OFFSET_INDEX), + getBatchSize ? sbr.getByteBuffer().getShort(MSG_BATCH_SIZE_INDEX) : 0, + getStoreTime ? sbr.getByteBuffer().getLong(MSG_STORE_TIME_OFFSET_INDEX) : 0); + } finally { + if (sbr != null) { + sbr.release(); + } + } + } + + protected BatchOffsetIndex getMaxMsgOffset(MappedFile mappedFile, boolean getBatchSize, boolean getStoreTime) { + if (mappedFile == null || mappedFile.getReadPosition() < CQ_STORE_UNIT_SIZE) { + return null; + } + int pos = mappedFile.getReadPosition() - CQ_STORE_UNIT_SIZE; + return getBatchOffsetIndexByPos(mappedFile, pos, getBatchSize, getStoreTime); + } + + private static int ceil(int pos) { + return (pos / CQ_STORE_UNIT_SIZE) * CQ_STORE_UNIT_SIZE; + } + + /** + * Gets SelectMappedBufferResult by batch-message offset + * Node: the caller is responsible for the release of SelectMappedBufferResult + * @param msgOffset + * @return SelectMappedBufferResult + */ + public SelectMappedBufferResult getBatchMsgIndexBuffer(final long msgOffset) { + if (msgOffset >= maxOffsetInQueue) { + return null; + } + MappedFile targetBcq; + BatchOffsetIndex targetMinOffset; + + // first check the last bcq file + MappedFile lastBcq = mappedFileQueue.getLastMappedFile(); + BatchOffsetIndex minForLastBcq = getMinMsgOffset(lastBcq, false, false); + if (null != minForLastBcq && minForLastBcq.getMsgOffset() <= msgOffset) { + // found, it's the last bcq. + targetBcq = lastBcq; + targetMinOffset = minForLastBcq; + } else { + boolean searchBcqByCacheEnable = this.messageStore.getMessageStoreConfig().isSearchBcqByCacheEnable(); + if (searchBcqByCacheEnable) { + // it's not the last BCQ file, so search it through cache. + targetBcq = this.searchOffsetFromCache(msgOffset); + // not found in cache + if (targetBcq == null) { + MappedFile firstBcq = mappedFileQueue.getFirstMappedFile(); + BatchOffsetIndex minForFirstBcq = getMinMsgOffset(firstBcq, false, false); + if (minForFirstBcq != null && minForFirstBcq.getMsgOffset() <= msgOffset && msgOffset < minForLastBcq.getMsgOffset()) { + // old search logic + targetBcq = this.searchOffsetFromFiles(msgOffset); + } + log.warn("cache is not working on BCQ [Topic: {}, QueueId: {}] for msgOffset: {}, targetBcq: {}", this.topic, this.queueId, msgOffset, targetBcq); + } + } else { + // old search logic + targetBcq = this.searchOffsetFromFiles(msgOffset); + } + + if (targetBcq == null) { + return null; + } + + targetMinOffset = getMinMsgOffset(targetBcq, false, false); + } + + BatchOffsetIndex targetMaxOffset = getMaxMsgOffset(targetBcq, false, false); + if (null == targetMinOffset || null == targetMaxOffset) { + return null; + } + + // then use binary search to find the indexed position + SelectMappedBufferResult sbr = targetMinOffset.getMappedFile().selectMappedBuffer(0); + try { + ByteBuffer byteBuffer = sbr.getByteBuffer(); + int left = targetMinOffset.getIndexPos(), right = targetMaxOffset.getIndexPos(); + int mid = binarySearch(byteBuffer, left, right, CQ_STORE_UNIT_SIZE, MSG_BASE_OFFSET_INDEX, msgOffset); + if (mid != -1) { + // return a buffer that needs to be released manually. + return targetMinOffset.getMappedFile().selectMappedBuffer(mid); + } + } finally { + sbr.release(); + } + return null; + } + + public MappedFile searchOffsetFromFiles(long msgOffset) { + MappedFile targetBcq = null; + // find the mapped file one by one reversely + int mappedFileNum = this.mappedFileQueue.getMappedFiles().size(); + for (int i = mappedFileNum - 1; i >= 0; i--) { + MappedFile mappedFile = mappedFileQueue.getMappedFiles().get(i); + BatchOffsetIndex tmpMinMsgOffset = getMinMsgOffset(mappedFile, false, false); + if (null != tmpMinMsgOffset && tmpMinMsgOffset.getMsgOffset() <= msgOffset) { + targetBcq = mappedFile; + break; + } + } + + return targetBcq; + } + + /** + * Find the message whose timestamp is the smallest, greater than or equal to the given time. + * + * @param timestamp + * @return + */ + @Deprecated + @Override + public long getOffsetInQueueByTime(final long timestamp) { + return getOffsetInQueueByTime(timestamp, BoundaryType.LOWER); + } + + @Override + public long getOffsetInQueueByTime(long timestamp, BoundaryType boundaryType) { + MappedFile targetBcq; + BatchOffsetIndex targetMinOffset; + + // first check the last bcq + MappedFile lastBcq = mappedFileQueue.getLastMappedFile(); + BatchOffsetIndex minForLastBcq = getMinMsgOffset(lastBcq, false, true); + if (null != minForLastBcq && minForLastBcq.getStoreTimestamp() <= timestamp) { + // found, it's the last bcq. + targetBcq = lastBcq; + targetMinOffset = minForLastBcq; + } else { + boolean searchBcqByCacheEnable = this.messageStore.getMessageStoreConfig().isSearchBcqByCacheEnable(); + if (searchBcqByCacheEnable) { + // it's not the last BCQ file, so search it through cache. + targetBcq = this.searchTimeFromCache(timestamp); + if (targetBcq == null) { + // not found in cache + MappedFile firstBcq = mappedFileQueue.getFirstMappedFile(); + BatchOffsetIndex minForFirstBcq = getMinMsgOffset(firstBcq, false, true); + if (minForFirstBcq != null && minForFirstBcq.getStoreTimestamp() <= timestamp && timestamp < minForLastBcq.getStoreTimestamp()) { + // old search logic + targetBcq = this.searchTimeFromFiles(timestamp); + } + log.warn("cache is not working on BCQ [Topic: {}, QueueId: {}] for timestamp: {}, targetBcq: {}", this.topic, this.queueId, timestamp, targetBcq); + } + } else { + // old search logic + targetBcq = this.searchTimeFromFiles(timestamp); + } + + if (targetBcq == null) { + return -1; + } + targetMinOffset = getMinMsgOffset(targetBcq, false, true); + } + + BatchOffsetIndex targetMaxOffset = getMaxMsgOffset(targetBcq, false, true); + if (null == targetMinOffset || null == targetMaxOffset) { + return -1; + } + + //then use binary search to find the indexed position + SelectMappedBufferResult sbr = targetMinOffset.getMappedFile().selectMappedBuffer(0); + try { + ByteBuffer byteBuffer = sbr.getByteBuffer(); + int left = targetMinOffset.getIndexPos(), right = targetMaxOffset.getIndexPos(); + long maxQueueTimestamp = byteBuffer.getLong(right + MSG_STORE_TIME_OFFSET_INDEX); + if (timestamp >= maxQueueTimestamp) { + return byteBuffer.getLong(right + MSG_BASE_OFFSET_INDEX); + } + int mid = binarySearchRight(byteBuffer, left, right, CQ_STORE_UNIT_SIZE, MSG_STORE_TIME_OFFSET_INDEX, timestamp, boundaryType); + if (mid != -1) { + return byteBuffer.getLong(mid + MSG_BASE_OFFSET_INDEX); + } + } finally { + sbr.release(); + } + + return -1; + } + + private MappedFile searchTimeFromFiles(long timestamp) { + MappedFile targetBcq = null; + + int mappedFileNum = this.mappedFileQueue.getMappedFiles().size(); + for (int i = mappedFileNum - 1; i >= 0; i--) { + MappedFile mappedFile = mappedFileQueue.getMappedFiles().get(i); + BatchOffsetIndex tmpMinMsgOffset = getMinMsgOffset(mappedFile, false, true); + if (tmpMinMsgOffset == null) { + //Maybe the new file + continue; + } + BatchOffsetIndex tmpMaxMsgOffset = getMaxMsgOffset(mappedFile, false, true); + //Here should not be null + if (tmpMaxMsgOffset == null) { + break; + } + if (tmpMaxMsgOffset.getStoreTimestamp() >= timestamp) { + if (tmpMinMsgOffset.getStoreTimestamp() <= timestamp) { + targetBcq = mappedFile; + break; + } else { + if (i - 1 < 0) { + //This is the first file + targetBcq = mappedFile; + break; + } else { + //The min timestamp of this file is larger than the given timestamp, so check the next file + continue; + } + } + } else { + //The max timestamp of this file is smaller than the given timestamp, so double check the previous file + if (i + 1 <= mappedFileNum - 1) { + mappedFile = mappedFileQueue.getMappedFiles().get(i + 1); + targetBcq = mappedFile; + break; + } else { + //There is no timestamp larger than the given timestamp + break; + } + } + } + + return targetBcq; + } + + /** + * Find the offset of which the value is equal or larger than the given targetValue. + * If there are many values equal to the target, then return the lowest offset if boundaryType is LOWER while + * return the highest offset if boundaryType is UPPER. + */ + public static int binarySearchRight(ByteBuffer byteBuffer, int left, int right, final int unitSize, + final int unitShift, long targetValue, BoundaryType boundaryType) { + int mid = -1; + while (left <= right) { + mid = ceil((left + right) / 2); + long tmpValue = byteBuffer.getLong(mid + unitShift); + if (mid == right) { + //Means left and the right are the same + if (tmpValue >= targetValue) { + return mid; + } else { + return -1; + } + } else if (mid == left) { + //Means the left + unitSize = right + if (tmpValue >= targetValue) { + return mid; + } else { + left = mid + unitSize; + } + } else { + //mid is actually in the mid + switch (boundaryType) { + case LOWER: + if (tmpValue < targetValue) { + left = mid + unitSize; + } else { + right = mid; + } + break; + case UPPER: + if (tmpValue <= targetValue) { + left = mid; + } else { + right = mid - unitSize; + } + break; + default: + log.warn("Unknown boundary type"); + return -1; + } + } + } + return -1; + } + + /** + * Here is vulnerable, the min value of the bytebuffer must be smaller or equal then the given value. + * Otherwise, it may get -1 + */ + protected int binarySearch(ByteBuffer byteBuffer, int left, int right, final int unitSize, final int unitShift, + long targetValue) { + int maxRight = right; + int mid = -1; + while (left <= right) { + mid = ceil((left + right) / 2); + long tmpValue = byteBuffer.getLong(mid + unitShift); + if (tmpValue == targetValue) { + return mid; + } + if (tmpValue > targetValue) { + right = mid - unitSize; + } else { + if (mid == left) { + //the binary search is converging to the left, so maybe the one on the right of mid is the exactly correct one + if (mid + unitSize <= maxRight + && byteBuffer.getLong(mid + unitSize + unitShift) <= targetValue) { + return mid + unitSize; + } else { + return mid; + } + } else { + left = mid; + } + } + } + return -1; + } + + static class BatchConsumeQueueIterator implements ReferredIterator { + private SelectMappedBufferResult sbr; + private int relativePos = 0; + + public BatchConsumeQueueIterator(SelectMappedBufferResult sbr) { + this.sbr = sbr; + if (sbr != null && sbr.getByteBuffer() != null) { + relativePos = sbr.getByteBuffer().position(); + } + } + + @Override + public boolean hasNext() { + if (sbr == null || sbr.getByteBuffer() == null) { + return false; + } + + return sbr.getByteBuffer().hasRemaining(); + } + + @Override + public CqUnit next() { + if (!hasNext()) { + return null; + } + ByteBuffer tmpBuffer = sbr.getByteBuffer().slice(); + tmpBuffer.position(MSG_COMPACT_OFFSET_INDEX); + ByteBuffer compactOffsetStoreBuffer = tmpBuffer.slice(); + compactOffsetStoreBuffer.limit(MSG_COMPACT_OFFSET_LENGTH); + + int relativePos = sbr.getByteBuffer().position(); + long offsetPy = sbr.getByteBuffer().getLong(); + int sizePy = sbr.getByteBuffer().getInt(); + long tagsCode = sbr.getByteBuffer().getLong(); //tagscode + sbr.getByteBuffer().getLong();//timestamp + long msgBaseOffset = sbr.getByteBuffer().getLong(); + short batchSize = sbr.getByteBuffer().getShort(); + int compactedOffset = sbr.getByteBuffer().getInt(); + sbr.getByteBuffer().position(relativePos + CQ_STORE_UNIT_SIZE); + + return new CqUnit(msgBaseOffset, offsetPy, sizePy, tagsCode, batchSize, compactedOffset, compactOffsetStoreBuffer); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove"); + } + + @Override + public void release() { + if (sbr != null) { + sbr.release(); + sbr = null; + } + } + + @Override + public CqUnit nextAndRelease() { + try { + return next(); + } finally { + release(); + } + } + } + + @Override + public String getTopic() { + return topic; + } + + @Override + public int getQueueId() { + return queueId; + } + + @Override + public CQType getCQType() { + return CQType.BatchCQ; + } + + @Override + public long getTotalSize() { + return this.mappedFileQueue.getTotalFileSize(); + } + + @Override + public int getUnitSize() { + return CQ_STORE_UNIT_SIZE; + } + + @Override + public void destroy() { + this.maxMsgPhyOffsetInCommitLog = -1; + this.minOffsetInQueue = -1; + this.maxOffsetInQueue = 0; + this.mappedFileQueue.destroy(); + this.destroyCache(); + } + + @Override + public long getMessageTotalInQueue() { + return this.getMaxOffsetInQueue() - this.getMinOffsetInQueue(); + } + + @Override + public long rollNextFile(long nextBeginOffset) { + return 0; + } + + /** + * Batch msg offset (deep logic offset) + * + * @return max deep offset + */ + @Override + public long getMaxOffsetInQueue() { + return maxOffsetInQueue; + } + + @Override + public long getMinOffsetInQueue() { + return minOffsetInQueue; + } + + @Override + public void checkSelf() { + mappedFileQueue.checkSelf(); + } + + @Override + public void swapMap(int reserveNum, long forceSwapIntervalMs, long normalSwapIntervalMs) { + mappedFileQueue.swapMap(reserveNum, forceSwapIntervalMs, normalSwapIntervalMs); + } + + @Override + public void cleanSwappedMap(long forceCleanSwapIntervalMs) { + mappedFileQueue.cleanSwappedMap(forceCleanSwapIntervalMs); + } + + public MappedFileQueue getMappedFileQueue() { + return mappedFileQueue; + } + + @Override + public long estimateMessageCount(long from, long to, MessageFilter filter) { + // transfer message offset to physical offset + SelectMappedBufferResult firstMappedFileBuffer = getBatchMsgIndexBuffer(from); + if (firstMappedFileBuffer == null) { + return -1; + } + long physicalOffsetFrom = firstMappedFileBuffer.getStartOffset(); + + SelectMappedBufferResult lastMappedFileBuffer = getBatchMsgIndexBuffer(to); + if (lastMappedFileBuffer == null) { + return -1; + } + long physicalOffsetTo = lastMappedFileBuffer.getStartOffset(); + + List mappedFiles = mappedFileQueue.range(physicalOffsetFrom, physicalOffsetTo); + if (mappedFiles.isEmpty()) { + return -1; + } + + boolean sample = false; + long match = 0; + long matchCqUnitCount = 0; + long raw = 0; + long scanCqUnitCount = 0; + + for (MappedFile mappedFile : mappedFiles) { + int start = 0; + int len = mappedFile.getFileSize(); + + // calculate start and len for first segment and last segment to reduce scanning + // first file segment + if (mappedFile.getFileFromOffset() <= physicalOffsetFrom) { + start = (int) (physicalOffsetFrom - mappedFile.getFileFromOffset()); + if (mappedFile.getFileFromOffset() + mappedFile.getFileSize() >= physicalOffsetTo) { + // current mapped file covers search range completely. + len = (int) (physicalOffsetTo - physicalOffsetFrom); + } else { + len = mappedFile.getFileSize() - start; + } + } + + // last file segment + if (0 == start && mappedFile.getFileFromOffset() + mappedFile.getFileSize() > physicalOffsetTo) { + len = (int) (physicalOffsetTo - mappedFile.getFileFromOffset()); + } + + // select partial data to scan + SelectMappedBufferResult slice = mappedFile.selectMappedBuffer(start, len); + if (null != slice) { + try { + ByteBuffer buffer = slice.getByteBuffer(); + int current = 0; + while (current < len) { + // skip physicalOffset and message length fields. + buffer.position(current + MSG_TAG_OFFSET_INDEX); + long tagCode = buffer.getLong(); + buffer.position(current + MSG_BATCH_SIZE_INDEX); + long batchSize = buffer.getShort(); + if (filter.isMatchedByConsumeQueue(tagCode, null)) { + match += batchSize; + matchCqUnitCount++; + } + raw += batchSize; + scanCqUnitCount++; + current += CQ_STORE_UNIT_SIZE; + + if (scanCqUnitCount >= messageStore.getMessageStoreConfig().getMaxConsumeQueueScan()) { + sample = true; + break; + } + + if (matchCqUnitCount > messageStore.getMessageStoreConfig().getSampleCountThreshold()) { + sample = true; + break; + } + } + } finally { + slice.release(); + } + } + // we have scanned enough entries, now is the time to return an educated guess. + if (sample) { + break; + } + } + + long result = match; + if (sample) { + if (0 == raw) { + log.error("[BUG]. Raw should NOT be 0"); + return 0; + } + result = (long) (match * (to - from) * 1.0 / raw); + } + log.debug("Result={}, raw={}, match={}, sample={}", result, raw, match, sample); + return result; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/queue/BatchOffsetIndex.java b/store/src/main/java/org/apache/rocketmq/store/queue/BatchOffsetIndex.java new file mode 100644 index 00000000000..8ca85c6b6f7 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/BatchOffsetIndex.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.queue; + +import org.apache.rocketmq.store.logfile.MappedFile; + +public class BatchOffsetIndex { + + private final MappedFile mappedFile; + private final int indexPos; + private final long msgOffset; + private final short batchSize; + private final long storeTimestamp; + + public BatchOffsetIndex(MappedFile file, int pos, long msgOffset, short size, long storeTimestamp) { + mappedFile = file; + indexPos = pos; + this.msgOffset = msgOffset; + batchSize = size; + this.storeTimestamp = storeTimestamp; + } + + public MappedFile getMappedFile() { + return mappedFile; + } + + public int getIndexPos() { + return indexPos; + } + + public long getMsgOffset() { + return msgOffset; + } + + public short getBatchSize() { + return batchSize; + } + + public long getStoreTimestamp() { + return storeTimestamp; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/queue/ConsumeQueueInterface.java b/store/src/main/java/org/apache/rocketmq/store/queue/ConsumeQueueInterface.java new file mode 100644 index 00000000000..768c782b1dd --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/ConsumeQueueInterface.java @@ -0,0 +1,201 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.queue; + +import org.apache.rocketmq.common.BoundaryType; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.attribute.CQType; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.store.DispatchRequest; +import org.apache.rocketmq.store.MessageFilter; +import org.rocksdb.RocksDBException; + +public interface ConsumeQueueInterface extends FileQueueLifeCycle { + /** + * Get the topic name + * @return the topic this cq belongs to. + */ + String getTopic(); + + /** + * Get queue id + * @return the queue id this cq belongs to. + */ + int getQueueId(); + + /** + * Get the units from the start offset. + * + * @param startIndex start index + * @return the unit iterateFrom + */ + ReferredIterator iterateFrom(long startIndex); + + /** + * Get the units from the start offset. + * + * @param startIndex start index + * @param count the unit counts will be iterated + * @return the unit iterateFrom + * @throws RocksDBException only in rocksdb mode + */ + ReferredIterator iterateFrom(long startIndex, int count) throws RocksDBException; + + /** + * Get cq unit at specified index + * @param index index + * @return the cq unit at index + */ + CqUnit get(long index); + + /** + * Get earliest cq unit + * @return the cq unit and message storeTime at index + */ + Pair getCqUnitAndStoreTime(long index); + + /** + * Get earliest cq unit + * @return earliest cq unit and message storeTime + */ + Pair getEarliestUnitAndStoreTime(); + + /** + * Get earliest cq unit + * @return earliest cq unit + */ + CqUnit getEarliestUnit(); + + /** + * Get last cq unit + * @return last cq unit + */ + CqUnit getLatestUnit(); + + /** + * Get last commit log offset + * @return last commit log offset + */ + long getLastOffset(); + + /** + * Get min offset(index) in queue + * @return the min offset(index) in queue + */ + long getMinOffsetInQueue(); + + /** + * Get max offset(index) in queue + * @return the max offset(index) in queue + */ + long getMaxOffsetInQueue(); + + /** + * Get total message count + * @return total message count + */ + long getMessageTotalInQueue(); + + /** + * Get the message whose timestamp is the smallest, greater than or equal to the given time. + * @param timestamp timestamp + * @return the offset(index) + */ + long getOffsetInQueueByTime(final long timestamp); + + /** + * Get the message whose timestamp is the smallest, greater than or equal to the given time and when there are more + * than one message satisfy the condition, decide which one to return based on boundaryType. + * @param timestamp timestamp + * @param boundaryType Lower or Upper + * @return the offset(index) + */ + long getOffsetInQueueByTime(final long timestamp, final BoundaryType boundaryType); + + /** + * The max physical offset of commitlog has been dispatched to this queue. + * It should be exclusive. + * + * @return the max physical offset point to commitlog + */ + long getMaxPhysicOffset(); + + /** + * Usually, the cq files are not exactly consistent with the commitlog, there maybe some redundant data in the first + * cq file. + * + * @return the minimal effective pos of the cq file. + */ + long getMinLogicOffset(); + + /** + * Get cq type + * @return cq type + */ + CQType getCQType(); + + /** + * Gets the occupied size of CQ file on disk + * @return total size + */ + long getTotalSize(); + + /** + * Get the unit size of this CQ which is different in different CQ impl + * @return cq unit size + */ + int getUnitSize(); + + /** + * Correct min offset by min commit log offset. + * @param minCommitLogOffset min commit log offset + */ + void correctMinOffset(long minCommitLogOffset); + + /** + * Do dispatch. + * @param request the request containing dispatch information. + */ + void putMessagePositionInfoWrapper(DispatchRequest request); + + /** + * Assign queue offset. + * @param queueOffsetAssigner the delegated queue offset assigner + * @param msg message itself + * @throws RocksDBException only in rocksdb mode + */ + void assignQueueOffset(QueueOffsetOperator queueOffsetAssigner, MessageExtBrokerInner msg) throws RocksDBException; + + /** + * Increase queue offset. + * @param queueOffsetAssigner the delegated queue offset assigner + * @param msg message itself + * @param messageNum message number + */ + void increaseQueueOffset(QueueOffsetOperator queueOffsetAssigner, MessageExtBrokerInner msg, short messageNum); + + /** + * Estimate number of records matching given filter. + * + * @param from Lower boundary, inclusive. + * @param to Upper boundary, inclusive. + * @param filter Specified filter criteria + * @return Number of matching records. + */ + long estimateMessageCount(long from, long to, MessageFilter filter); +} diff --git a/store/src/main/java/org/apache/rocketmq/store/queue/ConsumeQueueStore.java b/store/src/main/java/org/apache/rocketmq/store/queue/ConsumeQueueStore.java new file mode 100644 index 00000000000..cbe9b4f5acd --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/ConsumeQueueStore.java @@ -0,0 +1,577 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.queue; + +import java.io.File; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.FutureTask; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.common.BoundaryType; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.attribute.CQType; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.common.utils.QueueTypeUtils; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.store.CommitLog; +import org.apache.rocketmq.store.ConsumeQueue; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.DispatchRequest; +import org.apache.rocketmq.store.SelectMappedBufferResult; + +import static java.lang.String.format; +import static org.apache.rocketmq.store.config.StorePathConfigHelper.getStorePathBatchConsumeQueue; +import static org.apache.rocketmq.store.config.StorePathConfigHelper.getStorePathConsumeQueue; + +public class ConsumeQueueStore extends AbstractConsumeQueueStore { + + public ConsumeQueueStore(DefaultMessageStore messageStore) { + super(messageStore); + } + + @Override + public void start() { + log.info("Default ConsumeQueueStore start!"); + } + + @Override + public boolean load() { + boolean cqLoadResult = loadConsumeQueues(getStorePathConsumeQueue(this.messageStoreConfig.getStorePathRootDir()), CQType.SimpleCQ); + boolean bcqLoadResult = loadConsumeQueues(getStorePathBatchConsumeQueue(this.messageStoreConfig.getStorePathRootDir()), CQType.BatchCQ); + return cqLoadResult && bcqLoadResult; + } + + @Override + public boolean loadAfterDestroy() { + return true; + } + + @Override + public void recover() { + for (ConcurrentMap maps : this.consumeQueueTable.values()) { + for (ConsumeQueueInterface logic : maps.values()) { + this.recover(logic); + } + } + } + + @Override + public boolean recoverConcurrently() { + int count = 0; + for (ConcurrentMap maps : this.consumeQueueTable.values()) { + count += maps.values().size(); + } + final CountDownLatch countDownLatch = new CountDownLatch(count); + BlockingQueue recoverQueue = new LinkedBlockingQueue<>(); + final ExecutorService executor = buildExecutorService(recoverQueue, "RecoverConsumeQueueThread_"); + List> result = new ArrayList<>(count); + try { + for (ConcurrentMap maps : this.consumeQueueTable.values()) { + for (final ConsumeQueueInterface logic : maps.values()) { + FutureTask futureTask = new FutureTask<>(() -> { + boolean ret = true; + try { + logic.recover(); + } catch (Throwable e) { + ret = false; + log.error("Exception occurs while recover consume queue concurrently, " + + "topic={}, queueId={}", logic.getTopic(), logic.getQueueId(), e); + } finally { + countDownLatch.countDown(); + } + return ret; + }); + + result.add(futureTask); + executor.submit(futureTask); + } + } + countDownLatch.await(); + for (FutureTask task : result) { + if (task != null && task.isDone()) { + if (!task.get()) { + return false; + } + } + } + } catch (Exception e) { + log.error("Exception occurs while recover consume queue concurrently", e); + return false; + } finally { + executor.shutdown(); + } + return true; + } + + @Override + public boolean shutdown() { + return true; + } + + @Override + public long rollNextFile(ConsumeQueueInterface consumeQueue, final long offset) { + FileQueueLifeCycle fileQueueLifeCycle = getLifeCycle(consumeQueue.getTopic(), consumeQueue.getQueueId()); + return fileQueueLifeCycle.rollNextFile(offset); + } + + public void correctMinOffset(ConsumeQueueInterface consumeQueue, long minCommitLogOffset) { + consumeQueue.correctMinOffset(minCommitLogOffset); + } + + @Override + public void putMessagePositionInfoWrapper(DispatchRequest dispatchRequest) { + ConsumeQueueInterface cq = this.findOrCreateConsumeQueue(dispatchRequest.getTopic(), dispatchRequest.getQueueId()); + this.putMessagePositionInfoWrapper(cq, dispatchRequest); + } + + @Override + public List rangeQuery(String topic, int queueId, long startIndex, int num) { + return null; + } + + @Override + public ByteBuffer get(String topic, int queueId, long startIndex) { + return null; + } + + @Override + public long getMaxOffsetInQueue(String topic, int queueId) { + ConsumeQueueInterface logic = findOrCreateConsumeQueue(topic, queueId); + if (logic != null) { + return logic.getMaxOffsetInQueue(); + } + return 0; + } + + @Override + public long getOffsetInQueueByTime(String topic, int queueId, long timestamp, BoundaryType boundaryType) { + ConsumeQueueInterface logic = findOrCreateConsumeQueue(topic, queueId); + if (logic != null) { + long resultOffset = logic.getOffsetInQueueByTime(timestamp, boundaryType); + // Make sure the result offset is in valid range. + resultOffset = Math.max(resultOffset, logic.getMinOffsetInQueue()); + resultOffset = Math.min(resultOffset, logic.getMaxOffsetInQueue()); + return resultOffset; + } + return 0; + } + + private FileQueueLifeCycle getLifeCycle(String topic, int queueId) { + return findOrCreateConsumeQueue(topic, queueId); + } + + public boolean load(ConsumeQueueInterface consumeQueue) { + FileQueueLifeCycle fileQueueLifeCycle = getLifeCycle(consumeQueue.getTopic(), consumeQueue.getQueueId()); + return fileQueueLifeCycle.load(); + } + + private boolean loadConsumeQueues(String storePath, CQType cqType) { + File dirLogic = new File(storePath); + File[] fileTopicList = dirLogic.listFiles(); + if (fileTopicList != null) { + + for (File fileTopic : fileTopicList) { + String topic = fileTopic.getName(); + + File[] fileQueueIdList = fileTopic.listFiles(); + if (fileQueueIdList != null) { + for (File fileQueueId : fileQueueIdList) { + int queueId; + try { + queueId = Integer.parseInt(fileQueueId.getName()); + } catch (NumberFormatException e) { + continue; + } + + queueTypeShouldBe(topic, cqType); + + ConsumeQueueInterface logic = createConsumeQueueByType(cqType, topic, queueId, storePath); + this.putConsumeQueue(topic, queueId, logic); + if (!this.load(logic)) { + return false; + } + } + } + } + } + + log.info("load {} all over, OK", cqType); + + return true; + } + + private ConsumeQueueInterface createConsumeQueueByType(CQType cqType, String topic, int queueId, String storePath) { + if (Objects.equals(CQType.SimpleCQ, cqType)) { + return new ConsumeQueue( + topic, + queueId, + storePath, + this.messageStoreConfig.getMappedFileSizeConsumeQueue(), + this.messageStore); + } else if (Objects.equals(CQType.BatchCQ, cqType)) { + return new BatchConsumeQueue( + topic, + queueId, + storePath, + this.messageStoreConfig.getMapperFileSizeBatchConsumeQueue(), + this.messageStore); + } else { + throw new RuntimeException(format("queue type %s is not supported.", cqType.toString())); + } + } + + private void queueTypeShouldBe(String topic, CQType cqTypeExpected) { + Optional topicConfig = this.messageStore.getTopicConfig(topic); + + CQType cqTypeActual = QueueTypeUtils.getCQType(topicConfig); + + if (!Objects.equals(cqTypeExpected, cqTypeActual)) { + throw new RuntimeException(format("The queue type of topic: %s should be %s, but is %s", topic, cqTypeExpected, cqTypeActual)); + } + } + + private ExecutorService buildExecutorService(BlockingQueue blockingQueue, String threadNamePrefix) { + return ThreadUtils.newThreadPoolExecutor( + this.messageStore.getBrokerConfig().getRecoverThreadPoolNums(), + this.messageStore.getBrokerConfig().getRecoverThreadPoolNums(), + 1000 * 60, + TimeUnit.MILLISECONDS, + blockingQueue, + new ThreadFactoryImpl(threadNamePrefix)); + } + + public void recover(ConsumeQueueInterface consumeQueue) { + FileQueueLifeCycle fileQueueLifeCycle = getLifeCycle(consumeQueue.getTopic(), consumeQueue.getQueueId()); + fileQueueLifeCycle.recover(); + } + + @Override + public Long getMaxPhyOffsetInConsumeQueue(String topic, int queueId) { + ConsumeQueueInterface logic = findOrCreateConsumeQueue(topic, queueId); + if (logic != null) { + return logic.getMaxPhysicOffset(); + } + return null; + } + + @Override + public long getMaxPhyOffsetInConsumeQueue() { + long maxPhysicOffset = -1L; + for (ConcurrentMap maps : this.consumeQueueTable.values()) { + for (ConsumeQueueInterface logic : maps.values()) { + if (logic.getMaxPhysicOffset() > maxPhysicOffset) { + maxPhysicOffset = logic.getMaxPhysicOffset(); + } + } + } + return maxPhysicOffset; + } + + @Override + public long getMinOffsetInQueue(String topic, int queueId) { + ConsumeQueueInterface logic = findOrCreateConsumeQueue(topic, queueId); + if (logic != null) { + return logic.getMinOffsetInQueue(); + } + + return -1; + } + + public void checkSelf(ConsumeQueueInterface consumeQueue) { + FileQueueLifeCycle fileQueueLifeCycle = getLifeCycle(consumeQueue.getTopic(), consumeQueue.getQueueId()); + fileQueueLifeCycle.checkSelf(); + } + + @Override + public void checkSelf() { + for (Map.Entry> topicEntry : this.consumeQueueTable.entrySet()) { + for (Map.Entry cqEntry : topicEntry.getValue().entrySet()) { + this.checkSelf(cqEntry.getValue()); + } + } + } + + @Override + public boolean flush(ConsumeQueueInterface consumeQueue, int flushLeastPages) { + FileQueueLifeCycle fileQueueLifeCycle = getLifeCycle(consumeQueue.getTopic(), consumeQueue.getQueueId()); + return fileQueueLifeCycle.flush(flushLeastPages); + } + + @Override + public void destroy(ConsumeQueueInterface consumeQueue) { + FileQueueLifeCycle fileQueueLifeCycle = getLifeCycle(consumeQueue.getTopic(), consumeQueue.getQueueId()); + fileQueueLifeCycle.destroy(); + } + + @Override + public int deleteExpiredFile(ConsumeQueueInterface consumeQueue, long minCommitLogPos) { + FileQueueLifeCycle fileQueueLifeCycle = getLifeCycle(consumeQueue.getTopic(), consumeQueue.getQueueId()); + return fileQueueLifeCycle.deleteExpiredFile(minCommitLogPos); + } + + public void truncateDirtyLogicFiles(ConsumeQueueInterface consumeQueue, long phyOffset) { + FileQueueLifeCycle fileQueueLifeCycle = getLifeCycle(consumeQueue.getTopic(), consumeQueue.getQueueId()); + fileQueueLifeCycle.truncateDirtyLogicFiles(phyOffset); + } + + public void swapMap(ConsumeQueueInterface consumeQueue, int reserveNum, long forceSwapIntervalMs, + long normalSwapIntervalMs) { + FileQueueLifeCycle fileQueueLifeCycle = getLifeCycle(consumeQueue.getTopic(), consumeQueue.getQueueId()); + fileQueueLifeCycle.swapMap(reserveNum, forceSwapIntervalMs, normalSwapIntervalMs); + } + + public void cleanSwappedMap(ConsumeQueueInterface consumeQueue, long forceCleanSwapIntervalMs) { + FileQueueLifeCycle fileQueueLifeCycle = getLifeCycle(consumeQueue.getTopic(), consumeQueue.getQueueId()); + fileQueueLifeCycle.cleanSwappedMap(forceCleanSwapIntervalMs); + } + + @Override + public boolean isFirstFileAvailable(ConsumeQueueInterface consumeQueue) { + FileQueueLifeCycle fileQueueLifeCycle = getLifeCycle(consumeQueue.getTopic(), consumeQueue.getQueueId()); + return fileQueueLifeCycle.isFirstFileAvailable(); + } + + @Override + public boolean isFirstFileExist(ConsumeQueueInterface consumeQueue) { + FileQueueLifeCycle fileQueueLifeCycle = getLifeCycle(consumeQueue.getTopic(), consumeQueue.getQueueId()); + return fileQueueLifeCycle.isFirstFileExist(); + } + + @Override + public ConsumeQueueInterface findOrCreateConsumeQueue(String topic, int queueId) { + ConcurrentMap map = consumeQueueTable.get(topic); + if (null == map) { + ConcurrentMap newMap = new ConcurrentHashMap<>(128); + ConcurrentMap oldMap = consumeQueueTable.putIfAbsent(topic, newMap); + if (oldMap != null) { + map = oldMap; + } else { + map = newMap; + } + } + + ConsumeQueueInterface logic = map.get(queueId); + if (logic != null) { + return logic; + } + + ConsumeQueueInterface newLogic; + + Optional topicConfig = this.messageStore.getTopicConfig(topic); + // TODO maybe the topic has been deleted. + if (Objects.equals(CQType.BatchCQ, QueueTypeUtils.getCQType(topicConfig))) { + newLogic = new BatchConsumeQueue( + topic, + queueId, + getStorePathBatchConsumeQueue(this.messageStoreConfig.getStorePathRootDir()), + this.messageStoreConfig.getMapperFileSizeBatchConsumeQueue(), + this.messageStore); + } else { + newLogic = new ConsumeQueue( + topic, + queueId, + getStorePathConsumeQueue(this.messageStoreConfig.getStorePathRootDir()), + this.messageStoreConfig.getMappedFileSizeConsumeQueue(), + this.messageStore); + } + + ConsumeQueueInterface oldLogic = map.putIfAbsent(queueId, newLogic); + if (oldLogic != null) { + logic = oldLogic; + } else { + logic = newLogic; + } + + return logic; + } + + public void setBatchTopicQueueTable(ConcurrentMap batchTopicQueueTable) { + this.queueOffsetOperator.setBatchTopicQueueTable(batchTopicQueueTable); + } + + public void updateQueueOffset(String topic, int queueId, long offset) { + String topicQueueKey = topic + "-" + queueId; + this.queueOffsetOperator.updateQueueOffset(topicQueueKey, offset); + } + + private void putConsumeQueue(final String topic, final int queueId, final ConsumeQueueInterface consumeQueue) { + ConcurrentMap map = this.consumeQueueTable.get(topic); + if (null == map) { + map = new ConcurrentHashMap<>(); + map.put(queueId, consumeQueue); + this.consumeQueueTable.put(topic, map); + } else { + map.put(queueId, consumeQueue); + } + } + + @Override + public void recoverOffsetTable(long minPhyOffset) { + ConcurrentMap cqOffsetTable = new ConcurrentHashMap<>(1024); + ConcurrentMap bcqOffsetTable = new ConcurrentHashMap<>(1024); + + for (ConcurrentMap maps : this.consumeQueueTable.values()) { + for (ConsumeQueueInterface logic : maps.values()) { + String key = logic.getTopic() + "-" + logic.getQueueId(); + + long maxOffsetInQueue = logic.getMaxOffsetInQueue(); + if (Objects.equals(CQType.BatchCQ, logic.getCQType())) { + bcqOffsetTable.put(key, maxOffsetInQueue); + } else { + cqOffsetTable.put(key, maxOffsetInQueue); + } + + this.correctMinOffset(logic, minPhyOffset); + } + } + + // Correct unSubmit consumeOffset + if (messageStoreConfig.isDuplicationEnable() || messageStore.getBrokerConfig().isEnableControllerMode()) { + compensateForHA(cqOffsetTable); + } + + this.setTopicQueueTable(cqOffsetTable); + this.setBatchTopicQueueTable(bcqOffsetTable); + } + private void compensateForHA(ConcurrentMap cqOffsetTable) { + SelectMappedBufferResult lastBuffer = null; + long startReadOffset = messageStore.getCommitLog().getConfirmOffset() == -1 ? 0 : messageStore.getCommitLog().getConfirmOffset(); + log.info("Correct unsubmitted offset...StartReadOffset = {}", startReadOffset); + while ((lastBuffer = messageStore.selectOneMessageByOffset(startReadOffset)) != null) { + try { + if (lastBuffer.getStartOffset() > startReadOffset) { + startReadOffset = lastBuffer.getStartOffset(); + continue; + } + + ByteBuffer bb = lastBuffer.getByteBuffer(); + int magicCode = bb.getInt(bb.position() + 4); + if (magicCode == CommitLog.BLANK_MAGIC_CODE) { + startReadOffset += bb.getInt(bb.position()); + continue; + } else if (magicCode != MessageDecoder.MESSAGE_MAGIC_CODE) { + throw new RuntimeException("Unknown magicCode: " + magicCode); + } + + lastBuffer.getByteBuffer().mark(); + DispatchRequest dispatchRequest = messageStore.getCommitLog().checkMessageAndReturnSize(lastBuffer.getByteBuffer(), true, messageStoreConfig.isDuplicationEnable(), true); + if (!dispatchRequest.isSuccess()) + break; + lastBuffer.getByteBuffer().reset(); + + MessageExt msg = MessageDecoder.decode(lastBuffer.getByteBuffer(), true, false, false, false, true); + if (msg == null) + break; + + String key = msg.getTopic() + "-" + msg.getQueueId(); + cqOffsetTable.put(key, msg.getQueueOffset() + 1); + startReadOffset += msg.getStoreSize(); + log.info("Correcting. Key:{}, start read Offset: {}", key, startReadOffset); + } finally { + if (lastBuffer != null) + lastBuffer.release(); + } + } + } + + @Override + public void destroy() { + for (ConcurrentMap maps : this.consumeQueueTable.values()) { + for (ConsumeQueueInterface logic : maps.values()) { + this.destroy(logic); + } + } + } + + @Override + public void cleanExpired(long minCommitLogOffset) { + Iterator>> it = this.consumeQueueTable.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry> next = it.next(); + String topic = next.getKey(); + if (!TopicValidator.isSystemTopic(topic)) { + ConcurrentMap queueTable = next.getValue(); + Iterator> itQT = queueTable.entrySet().iterator(); + while (itQT.hasNext()) { + Map.Entry nextQT = itQT.next(); + long maxCLOffsetInConsumeQueue = nextQT.getValue().getLastOffset(); + + if (maxCLOffsetInConsumeQueue == -1) { + log.warn("maybe ConsumeQueue was created just now. topic={} queueId={} maxPhysicOffset={} minLogicOffset={}.", + nextQT.getValue().getTopic(), + nextQT.getValue().getQueueId(), + nextQT.getValue().getMaxPhysicOffset(), + nextQT.getValue().getMinLogicOffset()); + } else if (maxCLOffsetInConsumeQueue < minCommitLogOffset) { + log.info( + "cleanExpiredConsumerQueue: {} {} consumer queue destroyed, minCommitLogOffset: {} maxCLOffsetInConsumeQueue: {}", + topic, + nextQT.getKey(), + minCommitLogOffset, + maxCLOffsetInConsumeQueue); + + removeTopicQueueTable(nextQT.getValue().getTopic(), + nextQT.getValue().getQueueId()); + + this.destroy(nextQT.getValue()); + itQT.remove(); + } + } + + if (queueTable.isEmpty()) { + log.info("cleanExpiredConsumerQueue: {},topic destroyed", topic); + it.remove(); + } + } + } + } + + @Override + public void truncateDirty(long offsetToTruncate) { + for (ConcurrentMap maps : this.consumeQueueTable.values()) { + for (ConsumeQueueInterface logic : maps.values()) { + this.truncateDirtyLogicFiles(logic, offsetToTruncate); + } + } + } + + @Override + public long getTotalSize() { + long totalSize = 0; + for (ConcurrentMap maps : this.consumeQueueTable.values()) { + for (ConsumeQueueInterface logic : maps.values()) { + totalSize += logic.getTotalSize(); + } + } + return totalSize; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/queue/ConsumeQueueStoreInterface.java b/store/src/main/java/org/apache/rocketmq/store/queue/ConsumeQueueStoreInterface.java new file mode 100644 index 00000000000..e68880a828c --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/ConsumeQueueStoreInterface.java @@ -0,0 +1,303 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.queue; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.ConcurrentMap; +import org.apache.rocketmq.common.BoundaryType; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.store.DispatchRequest; +import org.rocksdb.RocksDBException; + +public interface ConsumeQueueStoreInterface { + + /** + * Start the consumeQueueStore + */ + void start(); + + /** + * Load from file. + * @return true if loaded successfully. + */ + boolean load(); + + /** + * load after destroy + */ + boolean loadAfterDestroy(); + + /** + * Recover from file. + */ + void recover(); + + /** + * Recover concurrently from file. + * @return true if recovered successfully. + */ + boolean recoverConcurrently(); + + /** + * Shutdown the consumeQueueStore + * @return true if shutdown successfully. + */ + boolean shutdown(); + + /** + * destroy all consumeQueues + */ + void destroy(); + + /** + * destroy the specific consumeQueue + * @throws RocksDBException only in rocksdb mode + */ + void destroy(ConsumeQueueInterface consumeQueue) throws RocksDBException; + + /** + * Flush cache to file. + * @param consumeQueue the consumeQueue will be flushed + * @param flushLeastPages the minimum number of pages to be flushed + * @return true if any data has been flushed. + */ + boolean flush(ConsumeQueueInterface consumeQueue, int flushLeastPages); + + /** + * clean expired data from minPhyOffset + * @param minPhyOffset + */ + void cleanExpired(long minPhyOffset); + + /** + * Check files. + */ + void checkSelf(); + + /** + * Delete expired files ending at min commit log position. + * @param consumeQueue + * @param minCommitLogPos min commit log position + * @return deleted file numbers. + */ + int deleteExpiredFile(ConsumeQueueInterface consumeQueue, long minCommitLogPos); + + /** + * Is the first file available? + * @param consumeQueue + * @return true if it's available + */ + boolean isFirstFileAvailable(ConsumeQueueInterface consumeQueue); + + /** + * Does the first file exist? + * @param consumeQueue + * @return true if it exists + */ + boolean isFirstFileExist(ConsumeQueueInterface consumeQueue); + + /** + * Roll to next file. + * @param consumeQueue + * @param offset next beginning offset + * @return the beginning offset of the next file + */ + long rollNextFile(ConsumeQueueInterface consumeQueue, final long offset); + + /** + * truncate dirty data + * @param offsetToTruncate + * @throws RocksDBException only in rocksdb mode + */ + void truncateDirty(long offsetToTruncate) throws RocksDBException; + + /** + * Apply the dispatched request and build the consume queue. This function should be idempotent. + * + * @param consumeQueue consume queue + * @param request dispatch request + */ + void putMessagePositionInfoWrapper(ConsumeQueueInterface consumeQueue, DispatchRequest request); + + /** + * Apply the dispatched request. This function should be idempotent. + * + * @param request dispatch request + * @throws RocksDBException only in rocksdb mode will throw exception + */ + void putMessagePositionInfoWrapper(DispatchRequest request) throws RocksDBException; + + /** + * range query cqUnit(ByteBuffer) in rocksdb + * @param topic + * @param queueId + * @param startIndex + * @param num + * @return the byteBuffer list of the topic-queueId in rocksdb + * @throws RocksDBException only in rocksdb mode + */ + List rangeQuery(final String topic, final int queueId, final long startIndex, final int num) throws RocksDBException; + + /** + * query cqUnit(ByteBuffer) in rocksdb + * @param topic + * @param queueId + * @param startIndex + * @return the byteBuffer of the topic-queueId in rocksdb + * @throws RocksDBException only in rocksdb mode + */ + ByteBuffer get(final String topic, final int queueId, final long startIndex) throws RocksDBException; + + /** + * get consumeQueue table + * @return the consumeQueue table + */ + ConcurrentMap> getConsumeQueueTable(); + + /** + * Assign queue offset. + * @param msg message itself + * @throws RocksDBException only in rocksdb mode + */ + void assignQueueOffset(MessageExtBrokerInner msg) throws RocksDBException; + + /** + * Increase queue offset. + * @param msg message itself + * @param messageNum message number + */ + void increaseQueueOffset(MessageExtBrokerInner msg, short messageNum); + + /** + * Increase lmq offset + * @param queueKey + * @param messageNum + */ + void increaseLmqOffset(String queueKey, short messageNum); + + /** + * get lmq queue offset + * @param queueKey + * @return + */ + long getLmqQueueOffset(String queueKey); + + /** + * recover topicQueue table by minPhyOffset + * @param minPhyOffset + */ + void recoverOffsetTable(long minPhyOffset); + + /** + * set topicQueue table + * @param topicQueueTable + */ + void setTopicQueueTable(ConcurrentMap topicQueueTable); + + /** + * remove topic-queueId from topicQueue table + * @param topic + * @param queueId + */ + void removeTopicQueueTable(String topic, Integer queueId); + + /** + * get topicQueue table + * @return the topicQueue table + */ + ConcurrentMap getTopicQueueTable(); + + /** + * get the max physical offset in consumeQueue + * @param topic + * @param queueId + * @return + */ + Long getMaxPhyOffsetInConsumeQueue(String topic, int queueId); + + /** + * get maxOffset of specific topic-queueId in topicQueue table + * @param topic + * @param queueId + * @return the max offset in QueueOffsetOperator + */ + Long getMaxOffset(String topic, int queueId); + + /** + * get max physic offset in consumeQueue + * @return the max physic offset in consumeQueue + * @throws RocksDBException only in rocksdb mode + */ + long getMaxPhyOffsetInConsumeQueue() throws RocksDBException; + + /** + * get min logic offset of specific topic-queueId in consumeQueue + * @param topic + * @param queueId + * @return the min logic offset of specific topic-queueId in consumeQueue + * @throws RocksDBException only in rocksdb mode + */ + long getMinOffsetInQueue(final String topic, final int queueId) throws RocksDBException; + + /** + * get max logic offset of specific topic-queueId in consumeQueue + * @param topic + * @param queueId + * @return the max logic offset of specific topic-queueId in consumeQueue + * @throws RocksDBException only in rocksdb mode + */ + long getMaxOffsetInQueue(final String topic, final int queueId) throws RocksDBException; + + /** + * Get the message whose timestamp is the smallest, greater than or equal to the given time and when there are more + * than one message satisfy the condition, decide which one to return based on boundaryType. + * @param timestamp timestamp + * @param boundaryType Lower or Upper + * @return the offset(index) + * @throws RocksDBException only in rocksdb mode + */ + long getOffsetInQueueByTime(String topic, int queueId, long timestamp, BoundaryType boundaryType) throws RocksDBException; + + /** + * find or create the consumeQueue + * @param topic + * @param queueId + * @return the consumeQueue + */ + ConsumeQueueInterface findOrCreateConsumeQueue(String topic, int queueId); + + /** + * find the consumeQueueMap of topic + * @param topic + * @return the consumeQueueMap of topic + */ + ConcurrentMap findConsumeQueueMap(String topic); + + /** + * get the total size of all consumeQueue + * @return the total size of all consumeQueue + */ + long getTotalSize(); + + /** + * Get store time from commitlog by cqUnit + * @param cqUnit + * @return + */ + long getStoreTime(CqUnit cqUnit); +} diff --git a/store/src/main/java/org/apache/rocketmq/store/queue/CqUnit.java b/store/src/main/java/org/apache/rocketmq/store/queue/CqUnit.java new file mode 100644 index 00000000000..b8865fd9195 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/CqUnit.java @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.queue; + +import org.apache.rocketmq.store.ConsumeQueueExt; + +import java.nio.ByteBuffer; + +public class CqUnit { + private final long queueOffset; + private final int size; + private final long pos; + private final short batchNum; + /** + * Be careful, the tagsCode is reused as an address for extent file. To prevent accident mistake, we follow the + * rules: 1. If the cqExtUnit is not null, make tagsCode == cqExtUnit.getTagsCode() 2. If the cqExtUnit is null, and + * the tagsCode is smaller than 0, it is an invalid tagsCode, which means failed to get cqExtUnit by address + */ + private long tagsCode; + private ConsumeQueueExt.CqExtUnit cqExtUnit; + private final ByteBuffer nativeBuffer; + private final int compactedOffset; + + public CqUnit(long queueOffset, long pos, int size, long tagsCode) { + this(queueOffset, pos, size, tagsCode, (short) 1, 0, null); + } + + public CqUnit(long queueOffset, long pos, int size, long tagsCode, short batchNum, int compactedOffset, ByteBuffer buffer) { + this.queueOffset = queueOffset; + this.pos = pos; + this.size = size; + this.tagsCode = tagsCode; + this.batchNum = batchNum; + + this.nativeBuffer = buffer; + this.compactedOffset = compactedOffset; + } + + public int getSize() { + return size; + } + + public long getPos() { + return pos; + } + + public long getTagsCode() { + return tagsCode; + } + + public Long getValidTagsCodeAsLong() { + if (!isTagsCodeValid()) { + return null; + } + return tagsCode; + } + + public boolean isTagsCodeValid() { + return !ConsumeQueueExt.isExtAddr(tagsCode); + } + + public ConsumeQueueExt.CqExtUnit getCqExtUnit() { + return cqExtUnit; + } + + public void setCqExtUnit(ConsumeQueueExt.CqExtUnit cqExtUnit) { + this.cqExtUnit = cqExtUnit; + } + + public void setTagsCode(long tagsCode) { + this.tagsCode = tagsCode; + } + + public long getQueueOffset() { + return queueOffset; + } + + public short getBatchNum() { + return batchNum; + } + + public void correctCompactOffset(int correctedOffset) { + this.nativeBuffer.putInt(correctedOffset); + } + + public int getCompactedOffset() { + return compactedOffset; + } + + @Override + public String toString() { + return "CqUnit{" + + "queueOffset=" + queueOffset + + ", size=" + size + + ", pos=" + pos + + ", batchNum=" + batchNum + + ", compactedOffset=" + compactedOffset + + '}'; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/queue/FileQueueLifeCycle.java b/store/src/main/java/org/apache/rocketmq/store/queue/FileQueueLifeCycle.java new file mode 100644 index 00000000000..95cc0887f42 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/FileQueueLifeCycle.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.queue; + +import org.apache.rocketmq.store.Swappable; + +/** + * FileQueueLifeCycle contains life cycle methods of ConsumerQueue that is directly implemented by FILE. + */ +public interface FileQueueLifeCycle extends Swappable { + /** + * Load from file. + * @return true if loaded successfully. + */ + boolean load(); + + /** + * Recover from file. + */ + void recover(); + + /** + * Check files. + */ + void checkSelf(); + + /** + * Flush cache to file. + * @param flushLeastPages the minimum number of pages to be flushed + * @return true if any data has been flushed. + */ + boolean flush(int flushLeastPages); + + /** + * Destroy files. + */ + void destroy(); + + /** + * Truncate dirty logic files starting at max commit log position. + * @param maxCommitLogPos max commit log position + */ + void truncateDirtyLogicFiles(long maxCommitLogPos); + + /** + * Delete expired files ending at min commit log position. + * @param minCommitLogPos min commit log position + * @return deleted file numbers. + */ + int deleteExpiredFile(long minCommitLogPos); + + /** + * Roll to next file. + * @param nextBeginOffset next begin offset + * @return the beginning offset of the next file + */ + long rollNextFile(final long nextBeginOffset); + + /** + * Is the first file available? + * @return true if it's available + */ + boolean isFirstFileAvailable(); + + /** + * Does the first file exist? + * @return true if it exists + */ + boolean isFirstFileExist(); +} diff --git a/store/src/main/java/org/apache/rocketmq/store/queue/MultiDispatchUtils.java b/store/src/main/java/org/apache/rocketmq/store/queue/MultiDispatchUtils.java new file mode 100644 index 00000000000..44397a2fce1 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/MultiDispatchUtils.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.queue; + +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.store.DispatchRequest; +import org.apache.rocketmq.store.config.MessageStoreConfig; + +public class MultiDispatchUtils { + + public static String lmqQueueKey(String queueName) { + StringBuilder keyBuilder = new StringBuilder(); + keyBuilder.append(queueName); + keyBuilder.append('-'); + int queueId = 0; + keyBuilder.append(queueId); + return keyBuilder.toString(); + } + + public static boolean isNeedHandleMultiDispatch(MessageStoreConfig messageStoreConfig, String topic) { + return messageStoreConfig.isEnableMultiDispatch() + && !topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX) + && !topic.startsWith(TopicValidator.SYSTEM_TOPIC_PREFIX) + && !topic.equals(TopicValidator.RMQ_SYS_SCHEDULE_TOPIC); + } + + public static boolean checkMultiDispatchQueue(MessageStoreConfig messageStoreConfig, DispatchRequest dispatchRequest) { + if (!isNeedHandleMultiDispatch(messageStoreConfig, dispatchRequest.getTopic())) { + return false; + } + Map prop = dispatchRequest.getPropertiesMap(); + if (prop == null || prop.isEmpty()) { + return false; + } + String multiDispatchQueue = prop.get(MessageConst.PROPERTY_INNER_MULTI_DISPATCH); + String multiQueueOffset = prop.get(MessageConst.PROPERTY_INNER_MULTI_QUEUE_OFFSET); + if (StringUtils.isBlank(multiDispatchQueue) || StringUtils.isBlank(multiQueueOffset)) { + return false; + } + return true; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/queue/QueueOffsetOperator.java b/store/src/main/java/org/apache/rocketmq/store/queue/QueueOffsetOperator.java new file mode 100644 index 00000000000..5b4bf994e0e --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/QueueOffsetOperator.java @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.queue; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.utils.ConcurrentHashMapUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +/** + * QueueOffsetOperator is a component for operating offsets for queues. + */ +public class QueueOffsetOperator { + private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + + private ConcurrentMap topicQueueTable = new ConcurrentHashMap<>(1024); + private ConcurrentMap batchTopicQueueTable = new ConcurrentHashMap<>(1024); + private ConcurrentMap lmqTopicQueueTable = new ConcurrentHashMap<>(1024); + + public long getQueueOffset(String topicQueueKey) { + return ConcurrentHashMapUtils.computeIfAbsent(this.topicQueueTable, topicQueueKey, k -> 0L); + } + + public Long getTopicQueueNextOffset(String topicQueueKey) { + return this.topicQueueTable.get(topicQueueKey); + } + + public void increaseQueueOffset(String topicQueueKey, short messageNum) { + Long queueOffset = ConcurrentHashMapUtils.computeIfAbsent(this.topicQueueTable, topicQueueKey, k -> 0L); + topicQueueTable.put(topicQueueKey, queueOffset + messageNum); + } + + public void updateQueueOffset(String topicQueueKey, long offset) { + this.topicQueueTable.put(topicQueueKey, offset); + } + + public long getBatchQueueOffset(String topicQueueKey) { + return ConcurrentHashMapUtils.computeIfAbsent(this.batchTopicQueueTable, topicQueueKey, k -> 0L); + } + + public void increaseBatchQueueOffset(String topicQueueKey, short messageNum) { + Long batchQueueOffset = ConcurrentHashMapUtils.computeIfAbsent(this.batchTopicQueueTable, topicQueueKey, k -> 0L); + this.batchTopicQueueTable.put(topicQueueKey, batchQueueOffset + messageNum); + } + + public long getLmqOffset(String topicQueueKey) { + return ConcurrentHashMapUtils.computeIfAbsent(this.lmqTopicQueueTable, topicQueueKey, k -> 0L); + } + + public Long getLmqTopicQueueNextOffset(String topicQueueKey) { + return this.lmqTopicQueueTable.get(topicQueueKey); + } + + public void increaseLmqOffset(String queueKey, short messageNum) { + Long lmqOffset = ConcurrentHashMapUtils.computeIfAbsent(this.lmqTopicQueueTable, queueKey, k -> 0L); + this.lmqTopicQueueTable.put(queueKey, lmqOffset + messageNum); + } + + public long currentQueueOffset(String topicQueueKey) { + Long currentQueueOffset = this.topicQueueTable.get(topicQueueKey); + return currentQueueOffset == null ? 0L : currentQueueOffset; + } + + public synchronized void remove(String topic, Integer queueId) { + String topicQueueKey = topic + "-" + queueId; + // Beware of thread-safety + this.topicQueueTable.remove(topicQueueKey); + this.batchTopicQueueTable.remove(topicQueueKey); + this.lmqTopicQueueTable.remove(topicQueueKey); + + log.info("removeQueueFromTopicQueueTable OK Topic: {} QueueId: {}", topic, queueId); + } + + public void setTopicQueueTable(ConcurrentMap topicQueueTable) { + this.topicQueueTable = topicQueueTable; + } + + public void setLmqTopicQueueTable(ConcurrentMap lmqTopicQueueTable) { + ConcurrentMap table = new ConcurrentHashMap(1024); + for (Map.Entry entry : lmqTopicQueueTable.entrySet()) { + if (MixAll.isLmq(entry.getKey())) { + table.put(entry.getKey(), entry.getValue()); + } + } + this.lmqTopicQueueTable = table; + } + + public ConcurrentMap getTopicQueueTable() { + return topicQueueTable; + } + + public void setBatchTopicQueueTable(ConcurrentMap batchTopicQueueTable) { + this.batchTopicQueueTable = batchTopicQueueTable; + } +} \ No newline at end of file diff --git a/store/src/main/java/org/apache/rocketmq/store/queue/ReferredIterator.java b/store/src/main/java/org/apache/rocketmq/store/queue/ReferredIterator.java new file mode 100644 index 00000000000..eba8738af3b --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/ReferredIterator.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.queue; + +import java.util.Iterator; + +public interface ReferredIterator extends Iterator { + + /** + * Release the referred resources. + */ + void release(); + + T nextAndRelease(); +} diff --git a/store/src/main/java/org/apache/rocketmq/store/queue/RocksDBConsumeQueue.java b/store/src/main/java/org/apache/rocketmq/store/queue/RocksDBConsumeQueue.java new file mode 100644 index 00000000000..5a981bb4df1 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/RocksDBConsumeQueue.java @@ -0,0 +1,395 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.queue; + +import java.nio.ByteBuffer; +import java.util.List; + +import org.apache.rocketmq.common.BoundaryType; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.attribute.CQType; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.ConsumeQueue; +import org.apache.rocketmq.store.DispatchRequest; +import org.apache.rocketmq.store.MessageFilter; +import org.apache.rocketmq.store.MessageStore; +import org.rocksdb.RocksDBException; + +public class RocksDBConsumeQueue implements ConsumeQueueInterface { + private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + private static final Logger ERROR_LOG = LoggerFactory.getLogger(LoggerName.STORE_ERROR_LOGGER_NAME); + + private final MessageStore messageStore; + private final String topic; + private final int queueId; + + public RocksDBConsumeQueue(final MessageStore messageStore, final String topic, final int queueId) { + this.messageStore = messageStore; + this.topic = topic; + this.queueId = queueId; + } + + public RocksDBConsumeQueue(final String topic, final int queueId) { + this.messageStore = null; + this.topic = topic; + this.queueId = queueId; + } + + @Override + public boolean load() { + return true; + } + + @Override + public void recover() { + // ignore + } + + @Override + public void checkSelf() { + // ignore + } + + @Override + public boolean flush(final int flushLeastPages) { + return true; + } + + @Override + public void destroy() { + // ignore + } + + @Override + public void truncateDirtyLogicFiles(long maxCommitLogPos) { + // ignored + } + + @Override + public int deleteExpiredFile(long minCommitLogPos) { + return 0; + } + + @Override + public long rollNextFile(long nextBeginOffset) { + return 0; + } + + @Override + public boolean isFirstFileAvailable() { + return true; + } + + @Override + public boolean isFirstFileExist() { + return true; + } + + @Override + public void swapMap(int reserveNum, long forceSwapIntervalMs, long normalSwapIntervalMs) { + // ignore + } + + @Override + public void cleanSwappedMap(long forceCleanSwapIntervalMs) { + // ignore + } + + @Override + public long getMaxOffsetInQueue() { + try { + return this.messageStore.getQueueStore().getMaxOffsetInQueue(topic, queueId); + } catch (RocksDBException e) { + ERROR_LOG.error("getMaxOffsetInQueue Failed. topic: {}, queueId: {}", topic, queueId, e); + return 0; + } + } + + @Override + public long getMessageTotalInQueue() { + try { + long maxOffsetInQueue = this.messageStore.getQueueStore().getMaxOffsetInQueue(topic, queueId); + long minOffsetInQueue = this.messageStore.getQueueStore().getMinOffsetInQueue(topic, queueId); + return maxOffsetInQueue - minOffsetInQueue; + } catch (RocksDBException e) { + ERROR_LOG.error("getMessageTotalInQueue Failed. topic: {}, queueId: {}, {}", topic, queueId, e); + } + return -1; + } + + /** + * We already implement it in RocksDBConsumeQueueStore. + * @see RocksDBConsumeQueueStore#getOffsetInQueueByTime + * @param timestamp timestamp + * @return + */ + @Override + public long getOffsetInQueueByTime(long timestamp) { + return 0; + } + + /** + * We already implement it in RocksDBConsumeQueueStore. + * @see RocksDBConsumeQueueStore#getOffsetInQueueByTime + * @param timestamp timestamp + * @param boundaryType Lower or Upper + * @return + */ + @Override + public long getOffsetInQueueByTime(long timestamp, BoundaryType boundaryType) { + return 0; + } + + @Override + public long getMaxPhysicOffset() { + Long maxPhyOffset = this.messageStore.getQueueStore().getMaxPhyOffsetInConsumeQueue(topic, queueId); + return maxPhyOffset == null ? -1 : maxPhyOffset; + } + + @Override + public long getMinLogicOffset() { + return 0; + } + + @Override + public CQType getCQType() { + return CQType.RocksDBCQ; + } + + @Override + public long getTotalSize() { + // ignored + return 0; + } + + @Override + public int getUnitSize() { + // attention: unitSize should equal to 'ConsumeQueue.CQ_STORE_UNIT_SIZE' + return ConsumeQueue.CQ_STORE_UNIT_SIZE; + } + + /** + * Ignored, we already implement this method + * @see org.apache.rocketmq.store.queue.RocksDBConsumeQueueOffsetTable#getMinCqOffset(String, int) + */ + @Override + public void correctMinOffset(long minCommitLogOffset) { + + } + + /** + * Ignored, in rocksdb mode, we build cq in RocksDBConsumeQueueStore + * @see org.apache.rocketmq.store.queue.RocksDBConsumeQueueStore#putMessagePosition() + */ + @Override + public void putMessagePositionInfoWrapper(DispatchRequest request) { + + } + + @Override + public void assignQueueOffset(QueueOffsetOperator queueOffsetOperator, MessageExtBrokerInner msg) throws RocksDBException { + String topicQueueKey = getTopic() + "-" + getQueueId(); + Long queueOffset = queueOffsetOperator.getTopicQueueNextOffset(topicQueueKey); + if (queueOffset == null) { + // we will recover topic queue table from rocksdb when we use it. + queueOffset = this.messageStore.getQueueStore().getMaxOffsetInQueue(topic, queueId); + queueOffsetOperator.updateQueueOffset(topicQueueKey, queueOffset); + } + msg.setQueueOffset(queueOffset); + } + + @Override + public void increaseQueueOffset(QueueOffsetOperator queueOffsetOperator, MessageExtBrokerInner msg, short messageNum) { + String topicQueueKey = getTopic() + "-" + getQueueId(); + queueOffsetOperator.increaseQueueOffset(topicQueueKey, messageNum); + } + + @Override + public long estimateMessageCount(long from, long to, MessageFilter filter) { + // todo + return 0; + } + + @Override + public long getMinOffsetInQueue() { + return this.messageStore.getMinOffsetInQueue(this.topic, this.queueId); + } + + private int pullNum(long cqOffset, long maxCqOffset) { + long diffLong = maxCqOffset - cqOffset; + if (diffLong < Integer.MAX_VALUE) { + int diffInt = (int) diffLong; + return diffInt > 16 ? 16 : diffInt; + } + return 16; + } + + @Override + public ReferredIterator iterateFrom(final long startIndex) { + try { + long maxCqOffset = getMaxOffsetInQueue(); + if (startIndex < maxCqOffset) { + int num = pullNum(startIndex, maxCqOffset); + return iterateFrom0(startIndex, num); + } + } catch (RocksDBException e) { + log.error("[RocksDBConsumeQueue] iterateFrom error!", e); + } + return null; + } + + @Override + public ReferredIterator iterateFrom(long startIndex, int count) throws RocksDBException { + long maxCqOffset = getMaxOffsetInQueue(); + if (startIndex < maxCqOffset) { + int num = Math.min((int)(maxCqOffset - startIndex), count); + return iterateFrom0(startIndex, num); + } + return null; + } + + @Override + public CqUnit get(long index) { + Pair pair = getCqUnitAndStoreTime(index); + return pair == null ? null : pair.getObject1(); + } + + @Override + public Pair getCqUnitAndStoreTime(long index) { + ByteBuffer byteBuffer; + try { + byteBuffer = this.messageStore.getQueueStore().get(topic, queueId, index); + } catch (RocksDBException e) { + ERROR_LOG.error("getUnitAndStoreTime Failed. topic: {}, queueId: {}", topic, queueId, e); + return null; + } + if (byteBuffer == null || byteBuffer.remaining() < RocksDBConsumeQueueTable.CQ_UNIT_SIZE) { + return null; + } + long phyOffset = byteBuffer.getLong(); + int size = byteBuffer.getInt(); + long tagCode = byteBuffer.getLong(); + long messageStoreTime = byteBuffer.getLong(); + return new Pair<>(new CqUnit(index, phyOffset, size, tagCode), messageStoreTime); + } + + @Override + public Pair getEarliestUnitAndStoreTime() { + try { + long minOffset = this.messageStore.getQueueStore().getMinOffsetInQueue(topic, queueId); + return getCqUnitAndStoreTime(minOffset); + } catch (RocksDBException e) { + ERROR_LOG.error("getEarliestUnitAndStoreTime Failed. topic: {}, queueId: {}", topic, queueId, e); + } + return null; + } + + @Override + public CqUnit getEarliestUnit() { + Pair pair = getEarliestUnitAndStoreTime(); + return pair == null ? null : pair.getObject1(); + } + + @Override + public CqUnit getLatestUnit() { + try { + long maxOffset = this.messageStore.getQueueStore().getMaxOffsetInQueue(topic, queueId); + return get(maxOffset); + } catch (RocksDBException e) { + ERROR_LOG.error("getLatestUnit Failed. topic: {}, queueId: {}, {}", topic, queueId, e.getMessage()); + } + return null; + } + + @Override + public long getLastOffset() { + return getMaxPhysicOffset(); + } + + private ReferredIterator iterateFrom0(final long startIndex, final int count) throws RocksDBException { + List byteBufferList = this.messageStore.getQueueStore().rangeQuery(topic, queueId, startIndex, count); + if (byteBufferList == null || byteBufferList.isEmpty()) { + if (this.messageStore.getMessageStoreConfig().isEnableRocksDBLog()) { + log.warn("iterateFrom0 - find nothing, startIndex:{}, count:{}", startIndex, count); + } + return null; + } + return new RocksDBConsumeQueueIterator(byteBufferList, startIndex); + } + + @Override + public String getTopic() { + return topic; + } + + @Override + public int getQueueId() { + return queueId; + } + + private class RocksDBConsumeQueueIterator implements ReferredIterator { + private final List byteBufferList; + private final long startIndex; + private final int totalCount; + private int currentIndex; + + public RocksDBConsumeQueueIterator(final List byteBufferList, final long startIndex) { + this.byteBufferList = byteBufferList; + this.startIndex = startIndex; + this.totalCount = byteBufferList.size(); + this.currentIndex = 0; + } + + @Override + public boolean hasNext() { + return this.currentIndex < this.totalCount; + } + + @Override + public CqUnit next() { + if (!hasNext()) { + return null; + } + final int currentIndex = this.currentIndex; + final ByteBuffer byteBuffer = this.byteBufferList.get(currentIndex); + CqUnit cqUnit = new CqUnit(this.startIndex + currentIndex, byteBuffer.getLong(), byteBuffer.getInt(), byteBuffer.getLong()); + this.currentIndex++; + return cqUnit; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove"); + } + + @Override + public void release() { + } + + @Override + public CqUnit nextAndRelease() { + try { + return next(); + } finally { + release(); + } + } + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/queue/RocksDBConsumeQueueOffsetTable.java b/store/src/main/java/org/apache/rocketmq/store/queue/RocksDBConsumeQueueOffsetTable.java new file mode 100644 index 00000000000..6fa66282e9d --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/RocksDBConsumeQueueOffsetTable.java @@ -0,0 +1,641 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.queue; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.ConsumeQueue; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.DispatchRequest; +import org.apache.rocketmq.store.rocksdb.ConsumeQueueRocksDBStorage; +import org.rocksdb.ColumnFamilyHandle; +import org.rocksdb.RocksDBException; +import org.rocksdb.RocksIterator; +import org.rocksdb.WriteBatch; + +import static org.apache.rocketmq.common.utils.DataConverter.CHARSET_UTF8; +import static org.apache.rocketmq.store.queue.RocksDBConsumeQueueStore.CTRL_1; + +public class RocksDBConsumeQueueOffsetTable { + private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + private static final Logger ERROR_LOG = LoggerFactory.getLogger(LoggerName.STORE_ERROR_LOGGER_NAME); + private static final Logger ROCKSDB_LOG = LoggerFactory.getLogger(LoggerName.ROCKSDB_LOGGER_NAME); + + private static final byte[] MAX_BYTES = "max".getBytes(CHARSET_UTF8); + private static final byte[] MIN_BYTES = "min".getBytes(CHARSET_UTF8); + + /** + * Rocksdb ConsumeQueue's Offset unit. Format: + * + *
    +     * ┌─────────────────────────┬───────────┬───────────────────────┬───────────┬───────────┬───────────┬─────────────┐
    +     * │ Topic Bytes Array Size  │  CTRL_1   │   Topic Bytes Array   │  CTRL_1   │  Max(Min) │  CTRL_1   │   QueueId   │
    +     * │        (4 Bytes)        │ (1 Bytes) │       (n Bytes)       │ (1 Bytes) │ (3 Bytes) │ (1 Bytes) │  (4 Bytes)  │
    +     * ├─────────────────────────┴───────────┴───────────────────────┴───────────┴───────────┴───────────┴─────────────┤
    +     * │                                                    Key Unit                                                   │
    +     * │                                                                                                               │
    +     * 
    + * + *
    +     * ┌─────────────────────────────┬────────────────────────┐
    +     * │  CommitLog Physical Offset  │   ConsumeQueue Offset  │
    +     * │        (8 Bytes)            │    (8 Bytes)           │
    +     * ├─────────────────────────────┴────────────────────────┤
    +     * │                     Value Unit                       │
    +     * │                                                      │
    +     * 
    + * ConsumeQueue's Offset unit. Size: CommitLog Physical Offset(8) + ConsumeQueue Offset(8) = 16 Bytes + */ + private static final int OFFSET_PHY_OFFSET = 0; + private static final int OFFSET_CQ_OFFSET = 8; + /** + * + * ┌─────────────────────────┬───────────┬───────────┬───────────┬───────────┬─────────────┐ + * │ Topic Bytes Array Size │ CTRL_1 │ CTRL_1 │ Max(Min) │ CTRL_1 │ QueueId │ + * │ (4 Bytes) │ (1 Bytes) │ (1 Bytes) │ (3 Bytes) │ (1 Bytes) │ (4 Bytes) │ + * ├─────────────────────────┴───────────┴───────────┴───────────┴───────────┴─────────────┤ + */ + private static final int OFFSET_KEY_LENGTH_WITHOUT_TOPIC_BYTES = 4 + 1 + 1 + 3 + 1 + 4; + private static final int OFFSET_VALUE_LENGTH = 8 + 8; + + /** + * We use a new system topic='CHECKPOINT_TOPIC' to record the maxPhyOffset built by CQ dispatch thread. + * @see ConsumeQueueStore#getMaxPhyOffsetInConsumeQueue(), we use it to find the maxPhyOffset built by CQ dispatch thread. + * If we do not record the maxPhyOffset, it may take us a long time to start traversing from the head of + * RocksDBConsumeQueueOffsetTable to find it. + */ + private static final String MAX_PHYSICAL_OFFSET_CHECKPOINT = TopicValidator.RMQ_SYS_ROCKSDB_OFFSET_TOPIC; + private static final byte[] MAX_PHYSICAL_OFFSET_CHECKPOINT_BYTES = MAX_PHYSICAL_OFFSET_CHECKPOINT.getBytes(CHARSET_UTF8); + private static final int INNER_CHECKPOINT_TOPIC_LEN = OFFSET_KEY_LENGTH_WITHOUT_TOPIC_BYTES + MAX_PHYSICAL_OFFSET_CHECKPOINT_BYTES.length; + private static final ByteBuffer INNER_CHECKPOINT_TOPIC = ByteBuffer.allocateDirect(INNER_CHECKPOINT_TOPIC_LEN); + private static final byte[] MAX_PHYSICAL_OFFSET_CHECKPOINT_KEY = new byte[INNER_CHECKPOINT_TOPIC_LEN]; + private final ByteBuffer maxPhyOffsetBB; + static { + buildOffsetKeyByteBuffer0(INNER_CHECKPOINT_TOPIC, MAX_PHYSICAL_OFFSET_CHECKPOINT_BYTES, 0, true); + INNER_CHECKPOINT_TOPIC.position(0).limit(INNER_CHECKPOINT_TOPIC_LEN); + INNER_CHECKPOINT_TOPIC.get(MAX_PHYSICAL_OFFSET_CHECKPOINT_KEY); + } + + private final RocksDBConsumeQueueTable rocksDBConsumeQueueTable; + private final ConsumeQueueRocksDBStorage rocksDBStorage; + private final DefaultMessageStore messageStore; + + private ColumnFamilyHandle offsetCFH; + + /** + * Although we have already put max(min) consumeQueueOffset and physicalOffset in rocksdb, we still hope to get them + * from heap to avoid accessing rocksdb. + * @see ConsumeQueue#getMaxPhysicOffset(), maxPhysicOffset --> topicQueueMaxCqOffset + * @see ConsumeQueue#getMinLogicOffset(), minLogicOffset --> topicQueueMinOffset + */ + private final Map topicQueueMinOffset; + private final Map topicQueueMaxCqOffset; + + public RocksDBConsumeQueueOffsetTable(RocksDBConsumeQueueTable rocksDBConsumeQueueTable, + ConsumeQueueRocksDBStorage rocksDBStorage, DefaultMessageStore messageStore) { + this.rocksDBConsumeQueueTable = rocksDBConsumeQueueTable; + this.rocksDBStorage = rocksDBStorage; + this.messageStore = messageStore; + this.topicQueueMinOffset = new ConcurrentHashMap(1024); + this.topicQueueMaxCqOffset = new ConcurrentHashMap(1024); + + this.maxPhyOffsetBB = ByteBuffer.allocateDirect(8); + } + + public void load() { + this.offsetCFH = this.rocksDBStorage.getOffsetCFHandle(); + } + + public void updateTempTopicQueueMaxOffset(final Pair offsetBBPair, + final byte[] topicBytes, final DispatchRequest request, + final Map> tempTopicQueueMaxOffsetMap) { + buildOffsetKeyAndValueByteBuffer(offsetBBPair, topicBytes, request); + ByteBuffer topicQueueId = offsetBBPair.getObject1(); + ByteBuffer maxOffsetBB = offsetBBPair.getObject2(); + Pair old = tempTopicQueueMaxOffsetMap.get(topicQueueId); + if (old == null) { + tempTopicQueueMaxOffsetMap.put(topicQueueId, new Pair(maxOffsetBB, request)); + } else { + long oldMaxOffset = old.getObject1().getLong(OFFSET_CQ_OFFSET); + long maxOffset = maxOffsetBB.getLong(OFFSET_CQ_OFFSET); + if (maxOffset >= oldMaxOffset) { + ERROR_LOG.error("cqOffset invalid1. old: {}, now: {}", oldMaxOffset, maxOffset); + } + } + } + + public void putMaxPhyAndCqOffset(final Map> tempTopicQueueMaxOffsetMap, + final WriteBatch writeBatch, final long maxPhyOffset) throws RocksDBException { + for (Map.Entry> entry : tempTopicQueueMaxOffsetMap.entrySet()) { + writeBatch.put(this.offsetCFH, entry.getKey(), entry.getValue().getObject1()); + } + + appendMaxPhyOffset(writeBatch, maxPhyOffset); + } + + public void putHeapMaxCqOffset(final Map> tempTopicQueueMaxOffsetMap) { + for (Map.Entry> entry : tempTopicQueueMaxOffsetMap.entrySet()) { + DispatchRequest request = entry.getValue().getObject2(); + putHeapMaxCqOffset(request.getTopic(), request.getQueueId(), request.getConsumeQueueOffset()); + } + } + + /** + * When topic is deleted, we clean up its offset info in rocksdb. + * @param topic + * @param queueId + * @throws RocksDBException + */ + public void destroyOffset(String topic, int queueId, WriteBatch writeBatch) throws RocksDBException { + final byte[] topicBytes = topic.getBytes(CHARSET_UTF8); + final ByteBuffer minOffsetKey = buildOffsetKeyByteBuffer(topicBytes, queueId, false); + byte[] minOffsetBytes = this.rocksDBStorage.getOffset(minOffsetKey.array()); + Long startCQOffset = (minOffsetBytes != null) ? ByteBuffer.wrap(minOffsetBytes).getLong(OFFSET_CQ_OFFSET) : null; + + final ByteBuffer maxOffsetKey = buildOffsetKeyByteBuffer(topicBytes, queueId, true); + byte[] maxOffsetBytes = this.rocksDBStorage.getOffset(maxOffsetKey.array()); + Long endCQOffset = (maxOffsetBytes != null) ? ByteBuffer.wrap(maxOffsetBytes).getLong(OFFSET_CQ_OFFSET) : null; + + writeBatch.delete(this.offsetCFH, minOffsetKey.array()); + writeBatch.delete(this.offsetCFH, maxOffsetKey.array()); + + String topicQueueId = buildTopicQueueId(topic, queueId); + removeHeapMinCqOffset(topicQueueId); + removeHeapMaxCqOffset(topicQueueId); + + log.info("RocksDB offset table delete topic: {}, queueId: {}, minOffset: {}, maxOffset: {}", topic, queueId, + startCQOffset, endCQOffset); + } + + private void appendMaxPhyOffset(final WriteBatch writeBatch, final long maxPhyOffset) throws RocksDBException { + final ByteBuffer maxPhyOffsetBB = this.maxPhyOffsetBB; + maxPhyOffsetBB.position(0).limit(8); + maxPhyOffsetBB.putLong(maxPhyOffset); + maxPhyOffsetBB.flip(); + + INNER_CHECKPOINT_TOPIC.position(0).limit(INNER_CHECKPOINT_TOPIC_LEN); + writeBatch.put(this.offsetCFH, INNER_CHECKPOINT_TOPIC, maxPhyOffsetBB); + } + + public long getMaxPhyOffset() throws RocksDBException { + byte[] valueBytes = this.rocksDBStorage.getOffset(MAX_PHYSICAL_OFFSET_CHECKPOINT_KEY); + if (valueBytes == null) { + return 0; + } + ByteBuffer valueBB = ByteBuffer.wrap(valueBytes); + return valueBB.getLong(0); + } + + /** + * Traverse the offset table to find dirty topic + * @param existTopicSet + * @return + */ + public Map> iterateOffsetTable2FindDirty(final Set existTopicSet) { + Map> topicQueueIdToBeDeletedMap = new HashMap<>(); + + RocksIterator iterator = null; + try { + iterator = rocksDBStorage.seekOffsetCF(); + if (iterator == null) { + return topicQueueIdToBeDeletedMap; + } + for (iterator.seekToFirst(); iterator.isValid(); iterator.next()) { + byte[] key = iterator.key(); + byte[] value = iterator.value(); + if (key == null || key.length <= OFFSET_KEY_LENGTH_WITHOUT_TOPIC_BYTES + || value == null || value.length != OFFSET_VALUE_LENGTH) { + continue; + } + ByteBuffer keyBB = ByteBuffer.wrap(key); + int topicLen = keyBB.getInt(0); + byte[] topicBytes = new byte[topicLen]; + /** + * "Topic Bytes Array Size" + "CTRL_1" = 4 + 1 + */ + keyBB.position(4 + 1); + keyBB.get(topicBytes); + String topic = new String(topicBytes, CHARSET_UTF8); + if (TopicValidator.isSystemTopic(topic)) { + continue; + } + + /** + * "Topic Bytes Array Size" + "CTRL_1" + "Topic Bytes Array" + "CTRL_1" + "Max(min)" + "CTRL_1" + * = 4 + 1 + topicLen + 1 + 3 + 1 + */ + int queueId = keyBB.getInt(4 + 1 + topicLen + 1 + 3 + 1); + + if (!existTopicSet.contains(topic)) { + ByteBuffer valueBB = ByteBuffer.wrap(value); + long cqOffset = valueBB.getLong(OFFSET_CQ_OFFSET); + + Set topicQueueIdSet = topicQueueIdToBeDeletedMap.get(topic); + if (topicQueueIdSet == null) { + Set newSet = new HashSet<>(); + newSet.add(queueId); + topicQueueIdToBeDeletedMap.put(topic, newSet); + } else { + topicQueueIdSet.add(queueId); + } + ERROR_LOG.info("RocksDBConsumeQueueOffsetTable has dirty cqOffset. topic: {}, queueId: {}, cqOffset: {}", + topic, queueId, cqOffset); + } + } + } catch (Exception e) { + ERROR_LOG.error("iterateOffsetTable2MarkDirtyCQ Failed.", e); + } finally { + if (iterator != null) { + iterator.close(); + } + } + return topicQueueIdToBeDeletedMap; + } + + public Long getMaxCqOffset(String topic, int queueId) throws RocksDBException { + Long maxCqOffset = getHeapMaxCqOffset(topic, queueId); + + if (maxCqOffset == null) { + final ByteBuffer byteBuffer = getMaxPhyAndCqOffsetInKV(topic, queueId); + maxCqOffset = (byteBuffer != null) ? byteBuffer.getLong(OFFSET_CQ_OFFSET) : null; + String topicQueueId = buildTopicQueueId(topic, queueId); + this.topicQueueMaxCqOffset.putIfAbsent(topicQueueId, maxCqOffset != null ? maxCqOffset : -1L); + if (messageStore.getMessageStoreConfig().isEnableRocksDBLog()) { + ROCKSDB_LOG.warn("updateMaxOffsetInQueue. {}, {}", topicQueueId, maxCqOffset); + } + } + + return maxCqOffset; + } + + /** + * truncate dirty offset in rocksdb + * @param offsetToTruncate + * @throws RocksDBException + */ + public void truncateDirty(long offsetToTruncate) throws RocksDBException { + correctMaxPyhOffset(offsetToTruncate); + + ConcurrentMap allTopicConfigMap = this.messageStore.getTopicConfigs(); + if (allTopicConfigMap == null) { + return; + } + for (TopicConfig topicConfig : allTopicConfigMap.values()) { + for (int i = 0; i < topicConfig.getWriteQueueNums(); i++) { + truncateDirtyOffset(topicConfig.getTopicName(), i); + } + } + } + + private Pair isMinOffsetOk(final String topic, final int queueId, final long minPhyOffset) throws RocksDBException { + PhyAndCQOffset phyAndCQOffset = getHeapMinOffset(topic, queueId); + if (phyAndCQOffset != null) { + final long phyOffset = phyAndCQOffset.getPhyOffset(); + final long cqOffset = phyAndCQOffset.getCqOffset(); + + return (phyOffset >= minPhyOffset) ? new Pair(true, cqOffset) : new Pair(false, cqOffset); + } + ByteBuffer byteBuffer = getMinPhyAndCqOffsetInKV(topic, queueId); + if (byteBuffer == null) { + return new Pair(false, 0L); + } + final long phyOffset = byteBuffer.getLong(OFFSET_PHY_OFFSET); + final long cqOffset = byteBuffer.getLong(OFFSET_CQ_OFFSET); + if (phyOffset >= minPhyOffset) { + String topicQueueId = buildTopicQueueId(topic, queueId); + PhyAndCQOffset newPhyAndCQOffset = new PhyAndCQOffset(phyOffset, cqOffset); + this.topicQueueMinOffset.putIfAbsent(topicQueueId, newPhyAndCQOffset); + if (messageStore.getMessageStoreConfig().isEnableRocksDBLog()) { + ROCKSDB_LOG.warn("updateMinOffsetInQueue. {}, {}", topicQueueId, newPhyAndCQOffset); + } + return new Pair(true, cqOffset); + } + return new Pair(false, cqOffset); + } + + private void truncateDirtyOffset(String topic, int queueId) throws RocksDBException { + final ByteBuffer byteBuffer = getMaxPhyAndCqOffsetInKV(topic, queueId); + if (byteBuffer == null) { + return; + } + + long maxPhyOffset = byteBuffer.getLong(OFFSET_PHY_OFFSET); + long maxCqOffset = byteBuffer.getLong(OFFSET_CQ_OFFSET); + long maxPhyOffsetInCQ = getMaxPhyOffset(); + + if (maxPhyOffset >= maxPhyOffsetInCQ) { + correctMaxCqOffset(topic, queueId, maxCqOffset, maxPhyOffsetInCQ); + Long newMaxCqOffset = getHeapMaxCqOffset(topic, queueId); + ROCKSDB_LOG.warn("truncateDirtyLogicFile topic: {}, queueId: {} from {} to {}", topic, queueId, + maxPhyOffset, newMaxCqOffset); + } + } + + private void correctMaxPyhOffset(long maxPhyOffset) throws RocksDBException { + if (!this.rocksDBStorage.hold()) { + return; + } + try { + WriteBatch writeBatch = new WriteBatch(); + long oldMaxPhyOffset = getMaxPhyOffset(); + if (oldMaxPhyOffset <= maxPhyOffset) { + return; + } + log.info("correctMaxPyhOffset, oldMaxPhyOffset={}, newMaxPhyOffset={}", oldMaxPhyOffset, maxPhyOffset); + appendMaxPhyOffset(writeBatch, maxPhyOffset); + this.rocksDBStorage.batchPut(writeBatch); + } catch (RocksDBException e) { + ERROR_LOG.error("correctMaxPyhOffset Failed.", e); + throw e; + } finally { + this.rocksDBStorage.release(); + } + } + + public long getMinCqOffset(String topic, int queueId) throws RocksDBException { + final long minPhyOffset = this.messageStore.getMinPhyOffset(); + Pair pair = isMinOffsetOk(topic, queueId, minPhyOffset); + final long cqOffset = pair.getObject2(); + if (!pair.getObject1() && correctMinCqOffset(topic, queueId, cqOffset, minPhyOffset)) { + PhyAndCQOffset phyAndCQOffset = getHeapMinOffset(topic, queueId); + if (phyAndCQOffset != null) { + if (this.messageStore.getMessageStoreConfig().isEnableRocksDBLog()) { + ROCKSDB_LOG.warn("getMinOffsetInQueue miss heap. topic: {}, queueId: {}, old: {}, new: {}", + topic, queueId, cqOffset, phyAndCQOffset); + } + return phyAndCQOffset.getCqOffset(); + } + } + return cqOffset; + } + + public Long getMaxPhyOffset(String topic, int queueId) { + try { + ByteBuffer byteBuffer = getMaxPhyAndCqOffsetInKV(topic, queueId); + if (byteBuffer != null) { + return byteBuffer.getLong(OFFSET_PHY_OFFSET); + } + } catch (Exception e) { + ERROR_LOG.info("getMaxPhyOffset error. topic: {}, queueId: {}", topic, queueId); + } + return null; + } + + private ByteBuffer getMinPhyAndCqOffsetInKV(String topic, int queueId) throws RocksDBException { + return getPhyAndCqOffsetInKV(topic, queueId, false); + } + + private ByteBuffer getMaxPhyAndCqOffsetInKV(String topic, int queueId) throws RocksDBException { + return getPhyAndCqOffsetInKV(topic, queueId, true); + } + + private ByteBuffer getPhyAndCqOffsetInKV(String topic, int queueId, boolean max) throws RocksDBException { + final byte[] topicBytes = topic.getBytes(CHARSET_UTF8); + final ByteBuffer keyBB = buildOffsetKeyByteBuffer(topicBytes, queueId, max); + + byte[] value = this.rocksDBStorage.getOffset(keyBB.array()); + return (value != null) ? ByteBuffer.wrap(value) : null; + } + + private String buildTopicQueueId(final String topic, final int queueId) { + return topic + "-" + queueId; + } + + private void putHeapMinCqOffset(final String topic, final int queueId, final long minPhyOffset, final long minCQOffset) { + String topicQueueId = buildTopicQueueId(topic, queueId); + PhyAndCQOffset phyAndCQOffset = new PhyAndCQOffset(minPhyOffset, minCQOffset); + this.topicQueueMinOffset.put(topicQueueId, phyAndCQOffset); + } + + private void putHeapMaxCqOffset(final String topic, final int queueId, final long maxCQOffset) { + String topicQueueId = buildTopicQueueId(topic, queueId); + Long oldMaxCqOffset = this.topicQueueMaxCqOffset.put(topicQueueId, maxCQOffset); + if (oldMaxCqOffset != null && oldMaxCqOffset > maxCQOffset) { + ERROR_LOG.error("cqOffset invalid0. old: {}, now: {}", oldMaxCqOffset, maxCQOffset); + } + } + + private PhyAndCQOffset getHeapMinOffset(final String topic, final int queueId) { + return this.topicQueueMinOffset.get(buildTopicQueueId(topic, queueId)); + } + + private Long getHeapMaxCqOffset(final String topic, final int queueId) { + String topicQueueId = buildTopicQueueId(topic, queueId); + return this.topicQueueMaxCqOffset.get(topicQueueId); + } + + private PhyAndCQOffset removeHeapMinCqOffset(String topicQueueId) { + return this.topicQueueMinOffset.remove(topicQueueId); + } + + private Long removeHeapMaxCqOffset(String topicQueueId) { + return this.topicQueueMaxCqOffset.remove(topicQueueId); + } + + private void updateCqOffset(final String topic, final int queueId, final long phyOffset, + final long cqOffset, boolean max) throws RocksDBException { + if (!this.rocksDBStorage.hold()) { + return; + } + WriteBatch writeBatch = new WriteBatch(); + try { + final byte[] topicBytes = topic.getBytes(CHARSET_UTF8); + final ByteBuffer offsetKey = buildOffsetKeyByteBuffer(topicBytes, queueId, max); + + final ByteBuffer offsetValue = buildOffsetValueByteBuffer(phyOffset, cqOffset); + writeBatch.put(this.offsetCFH, offsetKey.array(), offsetValue.array()); + this.rocksDBStorage.batchPut(writeBatch); + + if (max) { + putHeapMaxCqOffset(topic, queueId, cqOffset); + } else { + putHeapMinCqOffset(topic, queueId, phyOffset, cqOffset); + } + } catch (RocksDBException e) { + ERROR_LOG.error("updateCqOffset({}) failed.", max ? "max" : "min", e); + throw e; + } finally { + writeBatch.close(); + this.rocksDBStorage.release(); + if (messageStore.getMessageStoreConfig().isEnableRocksDBLog()) { + ROCKSDB_LOG.warn("updateCqOffset({}). topic: {}, queueId: {}, phyOffset: {}, cqOffset: {}", + max ? "max" : "min", topic, queueId, phyOffset, cqOffset); + } + } + } + + private boolean correctMaxCqOffset(final String topic, final int queueId, final long maxCQOffset, + final long maxPhyOffsetInCQ) throws RocksDBException { + // 'getMinOffsetInQueue' may correct minCqOffset and put it into heap + long minCQOffset = getMinCqOffset(topic, queueId); + PhyAndCQOffset minPhyAndCQOffset = getHeapMinOffset(topic, queueId); + if (minPhyAndCQOffset == null + || minPhyAndCQOffset.getCqOffset() != minCQOffset + || minPhyAndCQOffset.getPhyOffset() > maxPhyOffsetInCQ) { + ROCKSDB_LOG.info("[BUG] correctMaxCqOffset error! topic: {}, queueId: {}, maxPhyOffsetInCQ: {}, " + + "minCqOffset: {}, phyAndCQOffset: {}", + topic, queueId, maxPhyOffsetInCQ, minCQOffset, minPhyAndCQOffset); + throw new RocksDBException("correctMaxCqOffset error"); + } + + long high = maxCQOffset; + long low = minCQOffset; + PhyAndCQOffset targetPhyAndCQOffset = this.rocksDBConsumeQueueTable.binarySearchInCQ(topic, queueId, high, + low, maxPhyOffsetInCQ, false); + + long targetCQOffset = targetPhyAndCQOffset.getCqOffset(); + long targetPhyOffset = targetPhyAndCQOffset.getPhyOffset(); + + if (targetCQOffset == -1) { + if (maxCQOffset != minCQOffset) { + updateCqOffset(topic, queueId, minPhyAndCQOffset.getPhyOffset(), minCQOffset, true); + } + if (messageStore.getMessageStoreConfig().isEnableRocksDBLog()) { + ROCKSDB_LOG.warn("correct error. {}, {}, {}, {}, {}", topic, queueId, minCQOffset, maxCQOffset, minPhyAndCQOffset.getPhyOffset()); + } + return false; + } else { + updateCqOffset(topic, queueId, targetPhyOffset, targetCQOffset, true); + return true; + } + } + + private boolean correctMinCqOffset(final String topic, final int queueId, + final long minCQOffset, final long minPhyOffset) throws RocksDBException { + final ByteBuffer maxBB = getMaxPhyAndCqOffsetInKV(topic, queueId); + if (maxBB == null) { + updateCqOffset(topic, queueId, minPhyOffset, 0L, false); + return true; + } + final long maxPhyOffset = maxBB.getLong(OFFSET_PHY_OFFSET); + final long maxCQOffset = maxBB.getLong(OFFSET_CQ_OFFSET); + + if (maxPhyOffset < minPhyOffset) { + updateCqOffset(topic, queueId, minPhyOffset, maxCQOffset + 1, false); + return true; + } + + long high = maxCQOffset; + long low = minCQOffset; + PhyAndCQOffset phyAndCQOffset = this.rocksDBConsumeQueueTable.binarySearchInCQ(topic, queueId, high, low, + minPhyOffset, true); + long targetCQOffset = phyAndCQOffset.getCqOffset(); + long targetPhyOffset = phyAndCQOffset.getPhyOffset(); + + if (targetCQOffset == -1) { + if (maxCQOffset != minCQOffset) { + updateCqOffset(topic, queueId, maxPhyOffset, maxCQOffset, false); + } + if (messageStore.getMessageStoreConfig().isEnableRocksDBLog()) { + ROCKSDB_LOG.warn("correct error. {}, {}, {}, {}, {}", topic, queueId, minCQOffset, maxCQOffset, minPhyOffset); + } + return false; + } else { + updateCqOffset(topic, queueId, targetPhyOffset, targetCQOffset, false); + return true; + } + } + + public static Pair getOffsetByteBufferPair() { + ByteBuffer offsetKey = ByteBuffer.allocateDirect(RocksDBConsumeQueueStore.MAX_KEY_LEN); + ByteBuffer offsetValue = ByteBuffer.allocateDirect(OFFSET_VALUE_LENGTH); + return new Pair<>(offsetKey, offsetValue); + } + + private void buildOffsetKeyAndValueByteBuffer(final Pair offsetBBPair, + final byte[] topicBytes, final DispatchRequest request) { + final ByteBuffer offsetKey = offsetBBPair.getObject1(); + buildOffsetKeyByteBuffer(offsetKey, topicBytes, request.getQueueId(), true); + + final ByteBuffer offsetValue = offsetBBPair.getObject2(); + buildOffsetValueByteBuffer(offsetValue, request.getCommitLogOffset(), request.getConsumeQueueOffset()); + } + + private ByteBuffer buildOffsetKeyByteBuffer(final byte[] topicBytes, final int queueId, final boolean max) { + ByteBuffer byteBuffer = ByteBuffer.allocate(OFFSET_KEY_LENGTH_WITHOUT_TOPIC_BYTES + topicBytes.length); + buildOffsetKeyByteBuffer0(byteBuffer, topicBytes, queueId, max); + return byteBuffer; + } + + private void buildOffsetKeyByteBuffer(final ByteBuffer byteBuffer, final byte[] topicBytes, final int queueId, final boolean max) { + byteBuffer.position(0).limit(OFFSET_KEY_LENGTH_WITHOUT_TOPIC_BYTES + topicBytes.length); + buildOffsetKeyByteBuffer0(byteBuffer, topicBytes, queueId, max); + } + + private static void buildOffsetKeyByteBuffer0(final ByteBuffer byteBuffer, final byte[] topicBytes, final int queueId, + final boolean max) { + byteBuffer.putInt(topicBytes.length).put(CTRL_1).put(topicBytes).put(CTRL_1); + if (max) { + byteBuffer.put(MAX_BYTES); + } else { + byteBuffer.put(MIN_BYTES); + } + byteBuffer.put(CTRL_1).putInt(queueId); + byteBuffer.flip(); + } + + private void buildOffsetValueByteBuffer(final ByteBuffer byteBuffer, final long phyOffset, final long cqOffset) { + byteBuffer.position(0).limit(OFFSET_VALUE_LENGTH); + buildOffsetValueByteBuffer0(byteBuffer, phyOffset, cqOffset); + } + + private ByteBuffer buildOffsetValueByteBuffer(final long phyOffset, final long cqOffset) { + final ByteBuffer byteBuffer = ByteBuffer.allocate(OFFSET_VALUE_LENGTH); + buildOffsetValueByteBuffer0(byteBuffer, phyOffset, cqOffset); + return byteBuffer; + } + + private void buildOffsetValueByteBuffer0(final ByteBuffer byteBuffer, final long phyOffset, final long cqOffset) { + byteBuffer.putLong(phyOffset).putLong(cqOffset); + byteBuffer.flip(); + } + + static class PhyAndCQOffset { + private final long phyOffset; + private final long cqOffset; + + public PhyAndCQOffset(final long phyOffset, final long cqOffset) { + this.phyOffset = phyOffset; + this.cqOffset = cqOffset; + } + + public long getPhyOffset() { + return this.phyOffset; + } + + public long getCqOffset() { + return this.cqOffset; + } + + @Override + public String toString() { + return "[cqOffset=" + cqOffset + ", phyOffset=" + phyOffset + "]"; + } + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/queue/RocksDBConsumeQueueStore.java b/store/src/main/java/org/apache/rocketmq/store/queue/RocksDBConsumeQueueStore.java new file mode 100644 index 00000000000..4c66696e3c8 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/RocksDBConsumeQueueStore.java @@ -0,0 +1,445 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.queue; + +import java.io.File; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.BoundaryType; +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.utils.DataConverter; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.DispatchRequest; +import org.apache.rocketmq.store.config.BrokerRole; +import org.apache.rocketmq.store.config.StorePathConfigHelper; +import org.apache.rocketmq.store.rocksdb.ConsumeQueueRocksDBStorage; +import org.rocksdb.RocksDBException; +import org.rocksdb.Statistics; +import org.rocksdb.WriteBatch; + +public class RocksDBConsumeQueueStore extends AbstractConsumeQueueStore { + private static final Logger ERROR_LOG = LoggerFactory.getLogger(LoggerName.STORE_ERROR_LOGGER_NAME); + private static final Logger ROCKSDB_LOG = LoggerFactory.getLogger(LoggerName.ROCKSDB_LOGGER_NAME); + + public static final byte CTRL_0 = '\u0000'; + public static final byte CTRL_1 = '\u0001'; + public static final byte CTRL_2 = '\u0002'; + + private static final int BATCH_SIZE = 16; + public static final int MAX_KEY_LEN = 300; + + private final ScheduledExecutorService scheduledExecutorService; + private final String storePath; + + /** + * we use two tables with different ColumnFamilyHandle, called RocksDBConsumeQueueTable and RocksDBConsumeQueueOffsetTable. + * 1.RocksDBConsumeQueueTable uses to store CqUnit[physicalOffset, msgSize, tagHashCode, msgStoreTime] + * 2.RocksDBConsumeQueueOffsetTable uses to store physicalOffset and consumeQueueOffset(@see PhyAndCQOffset) of topic-queueId + */ + private final ConsumeQueueRocksDBStorage rocksDBStorage; + private final RocksDBConsumeQueueTable rocksDBConsumeQueueTable; + private final RocksDBConsumeQueueOffsetTable rocksDBConsumeQueueOffsetTable; + + private final WriteBatch writeBatch; + private final List bufferDRList; + private final List> cqBBPairList; + private final List> offsetBBPairList; + private final Map> tempTopicQueueMaxOffsetMap; + private volatile boolean isCQError = false; + + public RocksDBConsumeQueueStore(DefaultMessageStore messageStore) { + super(messageStore); + + this.storePath = StorePathConfigHelper.getStorePathConsumeQueue(messageStoreConfig.getStorePathRootDir()); + this.rocksDBStorage = new ConsumeQueueRocksDBStorage(messageStore, storePath, 4); + this.rocksDBConsumeQueueTable = new RocksDBConsumeQueueTable(rocksDBStorage, messageStore); + this.rocksDBConsumeQueueOffsetTable = new RocksDBConsumeQueueOffsetTable(rocksDBConsumeQueueTable, rocksDBStorage, messageStore); + + this.writeBatch = new WriteBatch(); + this.bufferDRList = new ArrayList(BATCH_SIZE); + this.cqBBPairList = new ArrayList(BATCH_SIZE); + this.offsetBBPairList = new ArrayList(BATCH_SIZE); + for (int i = 0; i < BATCH_SIZE; i++) { + this.cqBBPairList.add(RocksDBConsumeQueueTable.getCQByteBufferPair()); + this.offsetBBPairList.add(RocksDBConsumeQueueOffsetTable.getOffsetByteBufferPair()); + } + + this.tempTopicQueueMaxOffsetMap = new HashMap<>(); + this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor( + new ThreadFactoryImpl("RocksDBConsumeQueueStoreScheduledThread", messageStore.getBrokerIdentity())); + } + + @Override + public void start() { + log.info("RocksDB ConsumeQueueStore start!"); + this.scheduledExecutorService.scheduleAtFixedRate(() -> { + this.rocksDBStorage.statRocksdb(ROCKSDB_LOG); + }, 10, this.messageStoreConfig.getStatRocksDBCQIntervalSec(), TimeUnit.SECONDS); + + this.scheduledExecutorService.scheduleWithFixedDelay(() -> { + cleanDirty(messageStore.getTopicConfigs().keySet()); + }, 10, this.messageStoreConfig.getCleanRocksDBDirtyCQIntervalMin(), TimeUnit.MINUTES); + } + + private void cleanDirty(final Set existTopicSet) { + try { + Map> topicQueueIdToBeDeletedMap = + this.rocksDBConsumeQueueOffsetTable.iterateOffsetTable2FindDirty(existTopicSet); + + for (Map.Entry> entry : topicQueueIdToBeDeletedMap.entrySet()) { + String topic = entry.getKey(); + for (int queueId : entry.getValue()) { + destroy(new RocksDBConsumeQueue(topic, queueId)); + } + } + } catch (Exception e) { + log.error("cleanUnusedTopic Failed.", e); + } + } + + @Override + public boolean load() { + boolean result = this.rocksDBStorage.start(); + this.rocksDBConsumeQueueTable.load(); + this.rocksDBConsumeQueueOffsetTable.load(); + log.info("load rocksdb consume queue {}.", result ? "OK" : "Failed"); + return result; + } + + @Override + public boolean loadAfterDestroy() { + return this.load(); + } + + @Override + public void recover() { + // ignored + } + + @Override + public boolean recoverConcurrently() { + return true; + } + + @Override + public boolean shutdown() { + this.scheduledExecutorService.shutdown(); + return shutdownInner(); + } + + private boolean shutdownInner() { + return this.rocksDBStorage.shutdown(); + } + + @Override + public void putMessagePositionInfoWrapper(DispatchRequest request) throws RocksDBException { + if (request == null || this.bufferDRList.size() >= BATCH_SIZE) { + putMessagePosition(); + } + if (request != null) { + this.bufferDRList.add(request); + } + } + + public void putMessagePosition() throws RocksDBException { + final int maxRetries = 30; + for (int i = 0; i < maxRetries; i++) { + if (putMessagePosition0()) { + if (this.isCQError) { + this.messageStore.getRunningFlags().clearLogicsQueueError(); + this.isCQError = false; + } + return; + } else { + ERROR_LOG.warn("{} put cq Failed. retryTime: {}", i); + try { + Thread.sleep(100); + } catch (InterruptedException ignored) { + } + } + } + if (!this.isCQError) { + ERROR_LOG.error("[BUG] put CQ Failed."); + this.messageStore.getRunningFlags().makeLogicsQueueError(); + this.isCQError = true; + } + throw new RocksDBException("put CQ Failed"); + } + + private boolean putMessagePosition0() { + if (!this.rocksDBStorage.hold()) { + return false; + } + + final Map> tempTopicQueueMaxOffsetMap = this.tempTopicQueueMaxOffsetMap; + try { + final List bufferDRList = this.bufferDRList; + final int size = bufferDRList.size(); + if (size == 0) { + return true; + } + final List> cqBBPairList = this.cqBBPairList; + final List> offsetBBPairList = this.offsetBBPairList; + final WriteBatch writeBatch = this.writeBatch; + + long maxPhyOffset = 0; + for (int i = size - 1; i >= 0; i--) { + final DispatchRequest request = bufferDRList.get(i); + final byte[] topicBytes = request.getTopic().getBytes(DataConverter.CHARSET_UTF8); + + this.rocksDBConsumeQueueTable.buildAndPutCQByteBuffer(cqBBPairList.get(i), topicBytes, request, writeBatch); + this.rocksDBConsumeQueueOffsetTable.updateTempTopicQueueMaxOffset(offsetBBPairList.get(i), + topicBytes, request, tempTopicQueueMaxOffsetMap); + + final int msgSize = request.getMsgSize(); + final long phyOffset = request.getCommitLogOffset(); + if (phyOffset + msgSize >= maxPhyOffset) { + maxPhyOffset = phyOffset + msgSize; + } + } + + this.rocksDBConsumeQueueOffsetTable.putMaxPhyAndCqOffset(tempTopicQueueMaxOffsetMap, writeBatch, maxPhyOffset); + + // clear writeBatch in batchPut + this.rocksDBStorage.batchPut(writeBatch); + + this.rocksDBConsumeQueueOffsetTable.putHeapMaxCqOffset(tempTopicQueueMaxOffsetMap); + + long storeTimeStamp = bufferDRList.get(size - 1).getStoreTimestamp(); + if (this.messageStore.getMessageStoreConfig().getBrokerRole() == BrokerRole.SLAVE + || this.messageStore.getMessageStoreConfig().isEnableDLegerCommitLog()) { + this.messageStore.getStoreCheckpoint().setPhysicMsgTimestamp(storeTimeStamp); + } + this.messageStore.getStoreCheckpoint().setLogicsMsgTimestamp(storeTimeStamp); + + notifyMessageArriveAndClear(); + return true; + } catch (Exception e) { + ERROR_LOG.error("putMessagePosition0 Failed.", e); + return false; + } finally { + tempTopicQueueMaxOffsetMap.clear(); + this.rocksDBStorage.release(); + } + } + + private void notifyMessageArriveAndClear() { + final List bufferDRList = this.bufferDRList; + try { + for (DispatchRequest dp : bufferDRList) { + this.messageStore.notifyMessageArriveIfNecessary(dp); + } + } catch (Exception e) { + ERROR_LOG.error("notifyMessageArriveAndClear Failed.", e); + } finally { + bufferDRList.clear(); + } + } + + public Statistics getStatistics() { + return rocksDBStorage.getStatistics(); + } + @Override + public List rangeQuery(final String topic, final int queueId, final long startIndex, final int num) throws RocksDBException { + return this.rocksDBConsumeQueueTable.rangeQuery(topic, queueId, startIndex, num); + } + + @Override + public ByteBuffer get(final String topic, final int queueId, final long cqOffset) throws RocksDBException { + return this.rocksDBConsumeQueueTable.getCQInKV(topic, queueId, cqOffset); + } + + /** + * Ignored, we do not need to recover topicQueueTable and correct minLogicOffset. Because we will correct them + * when we use them, we call it lazy correction. + * @see RocksDBConsumeQueue#increaseQueueOffset(QueueOffsetOperator, MessageExtBrokerInner, short) + * @see org.apache.rocketmq.store.queue.RocksDBConsumeQueueOffsetTable#getMinCqOffset(String, int) + */ + @Override + public void recoverOffsetTable(long minPhyOffset) { + + } + + @Override + public void destroy() { + try { + shutdownInner(); + FileUtils.deleteDirectory(new File(this.storePath)); + } catch (Exception e) { + ERROR_LOG.error("destroy cq Failed. {}", this.storePath, e); + } + } + + @Override + public void destroy(ConsumeQueueInterface consumeQueue) throws RocksDBException { + String topic = consumeQueue.getTopic(); + int queueId = consumeQueue.getQueueId(); + if (StringUtils.isEmpty(topic) || queueId < 0 || !this.rocksDBStorage.hold()) { + return; + } + + WriteBatch writeBatch = new WriteBatch(); + try { + this.rocksDBConsumeQueueTable.destroyCQ(topic, queueId, writeBatch); + this.rocksDBConsumeQueueOffsetTable.destroyOffset(topic, queueId, writeBatch); + + this.rocksDBStorage.batchPut(writeBatch); + } catch (RocksDBException e) { + ERROR_LOG.error("kv deleteTopic {} Failed.", topic, e); + throw e; + } finally { + writeBatch.close(); + this.rocksDBStorage.release(); + } + } + + @Override + public boolean flush(ConsumeQueueInterface consumeQueue, int flushLeastPages) { + try { + this.rocksDBStorage.flushWAL(); + } catch (Exception e) { + } + return true; + } + + @Override + public void checkSelf() { + // ignored + } + + @Override + public int deleteExpiredFile(ConsumeQueueInterface consumeQueue, long minCommitLogPos) { + // ignored + return 0; + } + + /** + * We do not need to truncate dirty CQ in RocksDBConsumeQueueTable, Because dirty CQ in RocksDBConsumeQueueTable + * will be rewritten by new KV when new messages are appended or will be cleaned up when topics are deleted. + * But dirty offset info in RocksDBConsumeQueueOffsetTable must be truncated, because we use offset info in + * RocksDBConsumeQueueOffsetTable to rebuild topicQueueTable(@see RocksDBConsumeQueue#increaseQueueOffset). + * @param offsetToTruncate + * @throws RocksDBException + */ + @Override + public void truncateDirty(long offsetToTruncate) throws RocksDBException { + long maxPhyOffsetInRocksdb = getMaxPhyOffsetInConsumeQueue(); + if (offsetToTruncate >= maxPhyOffsetInRocksdb) { + return; + } + + this.rocksDBConsumeQueueOffsetTable.truncateDirty(offsetToTruncate); + } + + @Override + public void cleanExpired(final long minPhyOffset) { + this.rocksDBStorage.manualCompaction(minPhyOffset); + } + + @Override + public long getOffsetInQueueByTime(String topic, int queueId, long timestamp, BoundaryType boundaryType) throws RocksDBException { + final long minPhysicOffset = this.messageStore.getMinPhyOffset(); + long low = this.rocksDBConsumeQueueOffsetTable.getMinCqOffset(topic, queueId); + Long high = this.rocksDBConsumeQueueOffsetTable.getMaxCqOffset(topic, queueId); + if (high == null || high == -1) { + return 0; + } + return this.rocksDBConsumeQueueTable.binarySearchInCQByTime(topic, queueId, high, low, timestamp, minPhysicOffset); + } + + @Override + public long getMaxOffsetInQueue(String topic, int queueId) throws RocksDBException { + Long maxOffset = this.rocksDBConsumeQueueOffsetTable.getMaxCqOffset(topic, queueId); + return (maxOffset != null) ? maxOffset + 1 : 0; + } + + @Override + public long getMinOffsetInQueue(String topic, int queueId) throws RocksDBException { + return this.rocksDBConsumeQueueOffsetTable.getMinCqOffset(topic, queueId); + } + + @Override + public Long getMaxPhyOffsetInConsumeQueue(String topic, int queueId) { + return this.rocksDBConsumeQueueOffsetTable.getMaxPhyOffset(topic, queueId); + } + + @Override + public long getMaxPhyOffsetInConsumeQueue() throws RocksDBException { + return this.rocksDBConsumeQueueOffsetTable.getMaxPhyOffset(); + } + + @Override + public ConsumeQueueInterface findOrCreateConsumeQueue(String topic, int queueId) { + ConcurrentMap map = this.consumeQueueTable.get(topic); + if (null == map) { + ConcurrentMap newMap = new ConcurrentHashMap<>(128); + ConcurrentMap oldMap = this.consumeQueueTable.putIfAbsent(topic, newMap); + if (oldMap != null) { + map = oldMap; + } else { + map = newMap; + } + } + + ConsumeQueueInterface logic = map.get(queueId); + if (logic != null) { + return logic; + } + + ConsumeQueueInterface newLogic = new RocksDBConsumeQueue(this.messageStore, topic, queueId); + ConsumeQueueInterface oldLogic = map.putIfAbsent(queueId, newLogic); + + return oldLogic != null ? oldLogic : newLogic; + } + + @Override + public long rollNextFile(ConsumeQueueInterface consumeQueue, long offset) { + return 0; + } + + @Override + public boolean isFirstFileExist(ConsumeQueueInterface consumeQueue) { + return true; + } + + @Override + public boolean isFirstFileAvailable(ConsumeQueueInterface consumeQueue) { + return true; + } + + @Override + public long getTotalSize() { + return 0; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/queue/RocksDBConsumeQueueTable.java b/store/src/main/java/org/apache/rocketmq/store/queue/RocksDBConsumeQueueTable.java new file mode 100644 index 00000000000..0a735ea27c1 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/RocksDBConsumeQueueTable.java @@ -0,0 +1,312 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.queue; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import org.apache.rocketmq.common.Pair; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.DispatchRequest; +import org.apache.rocketmq.store.queue.RocksDBConsumeQueueOffsetTable.PhyAndCQOffset; +import org.apache.rocketmq.store.rocksdb.ConsumeQueueRocksDBStorage; +import org.rocksdb.ColumnFamilyHandle; +import org.rocksdb.RocksDBException; +import org.rocksdb.WriteBatch; + +import static org.apache.rocketmq.common.utils.DataConverter.CHARSET_UTF8; +import static org.apache.rocketmq.store.queue.RocksDBConsumeQueueStore.CTRL_0; +import static org.apache.rocketmq.store.queue.RocksDBConsumeQueueStore.CTRL_1; +import static org.apache.rocketmq.store.queue.RocksDBConsumeQueueStore.CTRL_2; + +/** + * We use RocksDBConsumeQueueTable to store cqUnit. + */ +public class RocksDBConsumeQueueTable { + private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + private static final Logger ROCKSDB_LOG = LoggerFactory.getLogger(LoggerName.ROCKSDB_LOGGER_NAME); + private static final Logger ERROR_LOG = LoggerFactory.getLogger(LoggerName.STORE_ERROR_LOGGER_NAME); + + /** + * Rocksdb ConsumeQueue's store unit. Format: + * + *
    +     * ┌─────────────────────────┬───────────┬───────────────────────┬───────────┬───────────┬───────────┬───────────────────────┐
    +     * │ Topic Bytes Array Size  │  CTRL_1   │   Topic Bytes Array   │  CTRL_1   │  QueueId  │  CTRL_1   │  ConsumeQueue Offset  │
    +     * │        (4 Bytes)        │ (1 Bytes) │       (n Bytes)       │ (1 Bytes) │ (4 Bytes) │ (1 Bytes) │     (8 Bytes)         │
    +     * ├─────────────────────────┴───────────┴───────────────────────┴───────────┴───────────┴───────────┴───────────────────────┤
    +     * │                                                    Key Unit                                                             │
    +     * │                                                                                                                         │
    +     * 
    + * + *
    +     * ┌─────────────────────────────┬───────────────────┬──────────────────┬──────────────────┐
    +     * │  CommitLog Physical Offset  │      Body Size    │   Tag HashCode   │  Msg Store Time  │
    +     * │        (8 Bytes)            │      (4 Bytes)    │    (8 Bytes)     │    (8 Bytes)     │
    +     * ├─────────────────────────────┴───────────────────┴──────────────────┴──────────────────┤
    +     * │                                                    Value Unit                         │
    +     * │                                                                                       │
    +     * 
    + * ConsumeQueue's store unit. Size: + * CommitLog Physical Offset(8) + Body Size(4) + Tag HashCode(8) + Msg Store Time(8) = 28 Bytes + */ + private static final int PHY_OFFSET_OFFSET = 0; + private static final int PHY_MSG_LEN_OFFSET = 8; + private static final int MSG_TAG_HASHCODE_OFFSET = 12; + private static final int MSG_STORE_TIME_SIZE_OFFSET = 20; + public static final int CQ_UNIT_SIZE = 8 + 4 + 8 + 8; + + /** + * ┌─────────────────────────┬───────────┬───────────┬───────────┬───────────┬───────────────────────┐ + * │ Topic Bytes Array Size │ CTRL_1 │ CTRL_1 │ QueueId │ CTRL_1 │ ConsumeQueue Offset │ + * │ (4 Bytes) │ (1 Bytes) │ (1 Bytes) │ (4 Bytes) │ (1 Bytes) │ (8 Bytes) │ + * ├─────────────────────────┴───────────┴───────────┴───────────┴───────────┴───────────────────────┤ + */ + private static final int CQ_KEY_LENGTH_WITHOUT_TOPIC_BYTES = 4 + 1 + 1 + 4 + 1 + 8; + + /** + * ┌─────────────────────────┬───────────┬───────────┬───────────┬───────────────────┐ + * │ Topic Bytes Array Size │ CTRL_1 │ CTRL_1 │ QueueId │ CTRL_0(CTRL_2) │ + * │ (4 Bytes) │ (1 Bytes) │ (1 Bytes) │ (4 Bytes) │ (1 Bytes) │ + * ├─────────────────────────┴───────────┴───────────┴───────────┴───────────────────┤ + */ + private static final int DELETE_CQ_KEY_LENGTH_WITHOUT_TOPIC_BYTES = 4 + 1 + 1 + 4 + 1; + + private final ConsumeQueueRocksDBStorage rocksDBStorage; + private final DefaultMessageStore messageStore; + + private ColumnFamilyHandle defaultCFH; + + public RocksDBConsumeQueueTable(ConsumeQueueRocksDBStorage rocksDBStorage, DefaultMessageStore messageStore) { + this.rocksDBStorage = rocksDBStorage; + this.messageStore = messageStore; + } + + public void load() { + this.defaultCFH = this.rocksDBStorage.getDefaultCFHandle(); + } + + public void buildAndPutCQByteBuffer(final Pair cqBBPair, + final byte[] topicBytes, final DispatchRequest request, final WriteBatch writeBatch) throws RocksDBException { + final ByteBuffer cqKey = cqBBPair.getObject1(); + buildCQKeyByteBuffer(cqKey, topicBytes, request.getQueueId(), request.getConsumeQueueOffset()); + + final ByteBuffer cqValue = cqBBPair.getObject2(); + buildCQValueByteBuffer(cqValue, request.getCommitLogOffset(), request.getMsgSize(), request.getTagsCode(), request.getStoreTimestamp()); + + writeBatch.put(this.defaultCFH, cqKey, cqValue); + } + + public ByteBuffer getCQInKV(final String topic, final int queueId, final long cqOffset) throws RocksDBException { + final byte[] topicBytes = topic.getBytes(CHARSET_UTF8); + final ByteBuffer keyBB = buildCQKeyByteBuffer(topicBytes, queueId, cqOffset); + byte[] value = this.rocksDBStorage.getCQ(keyBB.array()); + return (value != null) ? ByteBuffer.wrap(value) : null; + } + + public List rangeQuery(final String topic, final int queueId, final long startIndex, final int num) throws RocksDBException { + final byte[] topicBytes = topic.getBytes(CHARSET_UTF8); + final List defaultCFHList = new ArrayList(num); + final ByteBuffer[] resultList = new ByteBuffer[num]; + final List kvIndexList = new ArrayList(num); + final List kvKeyList = new ArrayList(num); + for (int i = 0; i < num; i++) { + final ByteBuffer keyBB = buildCQKeyByteBuffer(topicBytes, queueId, startIndex + i); + kvIndexList.add(i); + kvKeyList.add(keyBB.array()); + defaultCFHList.add(this.defaultCFH); + } + int keyNum = kvIndexList.size(); + if (keyNum > 0) { + List kvValueList = this.rocksDBStorage.multiGet(defaultCFHList, kvKeyList); + final int valueNum = kvValueList.size(); + if (keyNum != valueNum) { + throw new RocksDBException("rocksdb bug, multiGet"); + } + for (int i = 0; i < valueNum; i++) { + byte[] value = kvValueList.get(i); + if (value == null) { + continue; + } + ByteBuffer byteBuffer = ByteBuffer.wrap(value); + resultList[kvIndexList.get(i)] = byteBuffer; + } + } + + final int resultSize = resultList.length; + List bbValueList = new ArrayList(resultSize); + for (int i = 0; i < resultSize; i++) { + ByteBuffer byteBuffer = resultList[i]; + if (byteBuffer == null) { + break; + } + bbValueList.add(byteBuffer); + } + return bbValueList; + } + + /** + * When topic is deleted, we clean up its CqUnit in rocksdb. + * @param topic + * @param queueId + * @throws RocksDBException + */ + public void destroyCQ(final String topic, final int queueId, WriteBatch writeBatch) throws RocksDBException { + final byte[] topicBytes = topic.getBytes(CHARSET_UTF8); + final ByteBuffer cqStartKey = buildDeleteCQKey(true, topicBytes, queueId); + final ByteBuffer cqEndKey = buildDeleteCQKey(false, topicBytes, queueId); + + writeBatch.deleteRange(this.defaultCFH, cqStartKey.array(), cqEndKey.array()); + + log.info("Rocksdb consumeQueue table delete topic. {}, {}", topic, queueId); + } + + public long binarySearchInCQByTime(String topic, int queueId, long high, long low, long timestamp, + long minPhysicOffset) throws RocksDBException { + long result = 0; + long targetOffset = -1L, leftOffset = -1L, rightOffset = -1L; + long leftValue = -1L, rightValue = -1L; + while (high >= low) { + long midOffset = low + ((high - low) >>> 1); + ByteBuffer byteBuffer = getCQInKV(topic, queueId, midOffset); + if (byteBuffer == null) { + ERROR_LOG.warn("binarySearchInCQByTimeStamp Failed. topic: {}, queueId: {}, timestamp: {}, result: null", + topic, queueId, timestamp); + low = midOffset + 1; + continue; + } + + long phyOffset = byteBuffer.getLong(PHY_OFFSET_OFFSET); + if (phyOffset < minPhysicOffset) { + low = midOffset + 1; + leftOffset = midOffset; + continue; + } + long storeTime = byteBuffer.getLong(MSG_STORE_TIME_SIZE_OFFSET); + if (storeTime < 0) { + return 0; + } else if (storeTime == timestamp) { + targetOffset = midOffset; + break; + } else if (storeTime > timestamp) { + high = midOffset - 1; + rightOffset = midOffset; + rightValue = storeTime; + } else { + low = midOffset + 1; + leftOffset = midOffset; + leftValue = storeTime; + } + } + if (targetOffset != -1) { + result = targetOffset; + } else { + if (leftValue == -1) { + result = rightOffset; + } else if (rightValue == -1) { + result = leftOffset; + } else { + result = Math.abs(timestamp - leftValue) > Math.abs(timestamp - rightValue) ? rightOffset : leftOffset; + } + } + return result; + } + + public PhyAndCQOffset binarySearchInCQ(String topic, int queueId, long high, long low, long targetPhyOffset, + boolean min) throws RocksDBException { + long resultCQOffset = -1L; + long resultPhyOffset = -1L; + while (high >= low) { + long midCQOffset = low + ((high - low) >>> 1); + ByteBuffer byteBuffer = getCQInKV(topic, queueId, midCQOffset); + if (this.messageStore.getMessageStoreConfig().isEnableRocksDBLog()) { + ROCKSDB_LOG.warn("binarySearchInCQ. {}, {}, {}, {}, {}", topic, queueId, midCQOffset, low, high); + } + if (byteBuffer == null) { + low = midCQOffset + 1; + continue; + } + + final long phyOffset = byteBuffer.getLong(PHY_OFFSET_OFFSET); + if (phyOffset == targetPhyOffset) { + if (min) { + resultCQOffset = midCQOffset; + resultPhyOffset = phyOffset; + } + break; + } else if (phyOffset > targetPhyOffset) { + high = midCQOffset - 1; + if (min) { + resultCQOffset = midCQOffset; + resultPhyOffset = phyOffset; + } + } else { + low = midCQOffset + 1; + if (!min) { + resultCQOffset = midCQOffset; + resultPhyOffset = phyOffset; + } + } + } + return new PhyAndCQOffset(resultPhyOffset, resultCQOffset); + } + + public static Pair getCQByteBufferPair() { + ByteBuffer cqKey = ByteBuffer.allocateDirect(RocksDBConsumeQueueStore.MAX_KEY_LEN); + ByteBuffer cqValue = ByteBuffer.allocateDirect(CQ_UNIT_SIZE); + return new Pair<>(cqKey, cqValue); + } + + private ByteBuffer buildCQKeyByteBuffer(final byte[] topicBytes, final int queueId, final long cqOffset) { + final ByteBuffer byteBuffer = ByteBuffer.allocate(CQ_KEY_LENGTH_WITHOUT_TOPIC_BYTES + topicBytes.length); + buildCQKeyByteBuffer0(byteBuffer, topicBytes, queueId, cqOffset); + return byteBuffer; + } + + private void buildCQKeyByteBuffer(final ByteBuffer byteBuffer, final byte[] topicBytes, final int queueId, final long cqOffset) { + byteBuffer.position(0).limit(CQ_KEY_LENGTH_WITHOUT_TOPIC_BYTES + topicBytes.length); + buildCQKeyByteBuffer0(byteBuffer, topicBytes, queueId, cqOffset); + } + + private void buildCQKeyByteBuffer0(final ByteBuffer byteBuffer, final byte[] topicBytes, final int queueId, final long cqOffset) { + byteBuffer.putInt(topicBytes.length).put(CTRL_1).put(topicBytes).put(CTRL_1).putInt(queueId).put(CTRL_1).putLong(cqOffset); + byteBuffer.flip(); + } + + private void buildCQValueByteBuffer(final ByteBuffer byteBuffer, final long phyOffset, final int msgSize, final long tagsCode, final long storeTimestamp) { + byteBuffer.position(0).limit(CQ_UNIT_SIZE); + buildCQValueByteBuffer0(byteBuffer, phyOffset, msgSize, tagsCode, storeTimestamp); + } + + private void buildCQValueByteBuffer0(final ByteBuffer byteBuffer, final long phyOffset, final int msgSize, + final long tagsCode, final long storeTimestamp) { + byteBuffer.putLong(phyOffset).putInt(msgSize).putLong(tagsCode).putLong(storeTimestamp); + byteBuffer.flip(); + } + + private ByteBuffer buildDeleteCQKey(final boolean start, final byte[] topicBytes, final int queueId) { + final ByteBuffer byteBuffer = ByteBuffer.allocate(DELETE_CQ_KEY_LENGTH_WITHOUT_TOPIC_BYTES + topicBytes.length); + + byteBuffer.putInt(topicBytes.length).put(CTRL_1).put(topicBytes).put(CTRL_1).putInt(queueId).put(start ? CTRL_0 : CTRL_2); + byteBuffer.flip(); + return byteBuffer; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/queue/SparseConsumeQueue.java b/store/src/main/java/org/apache/rocketmq/store/queue/SparseConsumeQueue.java new file mode 100644 index 00000000000..4a5f3a93b1d --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/queue/SparseConsumeQueue.java @@ -0,0 +1,397 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.queue; + +import org.apache.rocketmq.common.BoundaryType; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.logfile.MappedFile; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +public class SparseConsumeQueue extends BatchConsumeQueue { + + public SparseConsumeQueue( + final String topic, + final int queueId, + final String storePath, + final int mappedFileSize, + final MessageStore defaultMessageStore) { + super(topic, queueId, storePath, mappedFileSize, defaultMessageStore); + } + + public SparseConsumeQueue( + final String topic, + final int queueId, + final String storePath, + final int mappedFileSize, + final MessageStore defaultMessageStore, + final String subfolder) { + super(topic, queueId, storePath, mappedFileSize, defaultMessageStore, subfolder); + } + + @Override + public void recover() { + final List mappedFiles = this.mappedFileQueue.getMappedFiles(); + if (!mappedFiles.isEmpty()) { + int index = mappedFiles.size() - 3; + if (index < 0) { + index = 0; + } + + MappedFile mappedFile = mappedFiles.get(index); + ByteBuffer byteBuffer = mappedFile.sliceByteBuffer(); + int mappedFileOffset = 0; + long processOffset = mappedFile.getFileFromOffset(); + while (true) { + for (int i = 0; i < mappedFileSize; i += CQ_STORE_UNIT_SIZE) { + byteBuffer.position(i); + long offset = byteBuffer.getLong(); + int size = byteBuffer.getInt(); + byteBuffer.getLong(); //tagscode + byteBuffer.getLong(); //timestamp + long msgBaseOffset = byteBuffer.getLong(); + short batchSize = byteBuffer.getShort(); + if (offset >= 0 && size > 0 && msgBaseOffset >= 0 && batchSize > 0) { + mappedFileOffset += CQ_STORE_UNIT_SIZE; + this.maxMsgPhyOffsetInCommitLog = offset; + } else { + log.info("Recover current batch consume queue file over, " + "file:{} offset:{} size:{} msgBaseOffset:{} batchSize:{} mappedFileOffset:{}", + mappedFile.getFileName(), offset, size, msgBaseOffset, batchSize, mappedFileOffset); + + if (mappedFileOffset != mappedFileSize) { + mappedFile.setWrotePosition(mappedFileOffset); + mappedFile.setFlushedPosition(mappedFileOffset); + mappedFile.setCommittedPosition(mappedFileOffset); + } + + break; + } + } + + index++; + if (index >= mappedFiles.size()) { + log.info("Recover last batch consume queue file over, last mapped file:{} ", mappedFile.getFileName()); + break; + } else { + mappedFile = mappedFiles.get(index); + byteBuffer = mappedFile.sliceByteBuffer(); + processOffset = mappedFile.getFileFromOffset(); + mappedFileOffset = 0; + log.info("Recover next batch consume queue file: " + mappedFile.getFileName()); + } + } + + processOffset += mappedFileOffset; + mappedFileQueue.setFlushedWhere(processOffset); + mappedFileQueue.setCommittedWhere(processOffset); + mappedFileQueue.truncateDirtyFiles(processOffset); + reviseMaxAndMinOffsetInQueue(); + } + } + + public ReferredIterator iterateFromOrNext(long startOffset) { + SelectMappedBufferResult sbr = getBatchMsgIndexOrNextBuffer(startOffset); + if (sbr == null) { + return null; + } + return new BatchConsumeQueueIterator(sbr); + } + + /** + * Gets SelectMappedBufferResult by batch-message offset, if not found will return the next valid offset buffer + * Node: the caller is responsible for the release of SelectMappedBufferResult + * @param msgOffset + * @return SelectMappedBufferResult + */ + public SelectMappedBufferResult getBatchMsgIndexOrNextBuffer(final long msgOffset) { + + MappedFile targetBcq; + + if (msgOffset <= minOffsetInQueue) { + targetBcq = mappedFileQueue.getFirstMappedFile(); + } else { + targetBcq = searchFileByOffsetOrRight(msgOffset); + } + + if (targetBcq == null) { + return null; + } + + BatchOffsetIndex minOffset = getMinMsgOffset(targetBcq, false, false); + BatchOffsetIndex maxOffset = getMaxMsgOffset(targetBcq, false, false); + if (null == minOffset || null == maxOffset) { + return null; + } + + SelectMappedBufferResult sbr = minOffset.getMappedFile().selectMappedBuffer(0); + try { + ByteBuffer byteBuffer = sbr.getByteBuffer(); + int left = minOffset.getIndexPos(); + int right = maxOffset.getIndexPos(); + int mid = binarySearchRight(byteBuffer, left, right, CQ_STORE_UNIT_SIZE, MSG_BASE_OFFSET_INDEX, msgOffset, BoundaryType.LOWER); + if (mid != -1) { + return minOffset.getMappedFile().selectMappedBuffer(mid); + } + } finally { + sbr.release(); + } + + return null; + } + + protected MappedFile searchOffsetFromCacheOrRight(long msgOffset) { + Map.Entry ceilingEntry = this.offsetCache.ceilingEntry(msgOffset); + if (ceilingEntry == null) { + return null; + } else { + return ceilingEntry.getValue(); + } + } + + protected MappedFile searchFileByOffsetOrRight(long msgOffset) { + MappedFile targetBcq = null; + boolean searchBcqByCacheEnable = this.messageStore.getMessageStoreConfig().isSearchBcqByCacheEnable(); + if (searchBcqByCacheEnable) { + // it's not the last BCQ file, so search it through cache. + targetBcq = this.searchOffsetFromCacheOrRight(msgOffset); + // not found in cache + if (targetBcq == null) { + MappedFile firstBcq = mappedFileQueue.getFirstMappedFile(); + BatchOffsetIndex minForFirstBcq = getMinMsgOffset(firstBcq, false, false); + if (minForFirstBcq != null && minForFirstBcq.getMsgOffset() <= msgOffset && msgOffset < maxOffsetInQueue) { + // old search logic + targetBcq = this.searchOffsetFromFilesOrRight(msgOffset); + } + log.warn("cache is not working on BCQ [Topic: {}, QueueId: {}] for msgOffset: {}, targetBcq: {}", this.topic, this.queueId, msgOffset, targetBcq); + } + } else { + // old search logic + targetBcq = this.searchOffsetFromFilesOrRight(msgOffset); + } + + return targetBcq; + } + + public MappedFile searchOffsetFromFilesOrRight(long msgOffset) { + MappedFile targetBcq = null; + // find the mapped file one by one reversely + int mappedFileNum = this.mappedFileQueue.getMappedFiles().size(); + for (int i = mappedFileNum - 1; i >= 0; i--) { + MappedFile mappedFile = mappedFileQueue.getMappedFiles().get(i); + BatchOffsetIndex tmpMinMsgOffset = getMinMsgOffset(mappedFile, false, false); + BatchOffsetIndex tmpMaxMsgOffset = getMaxMsgOffset(mappedFile, false, false); + if (null != tmpMaxMsgOffset && tmpMaxMsgOffset.getMsgOffset() < msgOffset) { + if (i != mappedFileNum - 1) { //not the last mapped file max msg offset + targetBcq = mappedFileQueue.getMappedFiles().get(i + 1); + break; + } + } + + if (null != tmpMinMsgOffset && tmpMinMsgOffset.getMsgOffset() <= msgOffset + && null != tmpMaxMsgOffset && msgOffset <= tmpMaxMsgOffset.getMsgOffset()) { + targetBcq = mappedFile; + break; + } + } + + return targetBcq; + } + + private MappedFile getPreFile(MappedFile file) { + int index = mappedFileQueue.getMappedFiles().indexOf(file); + if (index < 1) { + // indicate that this is the first file or not found + return null; + } else { + return mappedFileQueue.getMappedFiles().get(index - 1); + } + } + + private void cacheOffset(MappedFile file, Function offsetGetFunc) { + try { + BatchOffsetIndex offset = offsetGetFunc.apply(file); + if (offset != null) { + this.offsetCache.put(offset.getMsgOffset(), offset.getMappedFile()); + this.timeCache.put(offset.getStoreTimestamp(), offset.getMappedFile()); + } + } catch (Exception e) { + log.error("Failed caching offset and time on BCQ [Topic: {}, QueueId: {}, File: {}]", + this.topic, this.queueId, file); + } + } + + @Override + protected void cacheBcq(MappedFile bcq) { + MappedFile file = getPreFile(bcq); + if (file != null) { + cacheOffset(file, m -> getMaxMsgOffset(m, false, true)); + } + } + + public void putEndPositionInfo(MappedFile mappedFile) { + // cache max offset + if (!mappedFile.isFull()) { + this.byteBufferItem.flip(); + this.byteBufferItem.limit(CQ_STORE_UNIT_SIZE); + this.byteBufferItem.putLong(-1); + this.byteBufferItem.putInt(0); + this.byteBufferItem.putLong(0); + this.byteBufferItem.putLong(0); + this.byteBufferItem.putLong(0); + this.byteBufferItem.putShort((short)0); + this.byteBufferItem.putInt(INVALID_POS); + this.byteBufferItem.putInt(0); // 4 bytes reserved + boolean appendRes = mappedFile.appendMessage(this.byteBufferItem.array()); + if (!appendRes) { + log.error("append end position info into {} failed", mappedFile.getFileName()); + } + } + + cacheOffset(mappedFile, m -> getMaxMsgOffset(m, false, true)); + } + + public MappedFile createFile(final long physicalOffset) throws IOException { + // cache max offset + return mappedFileQueue.tryCreateMappedFile(physicalOffset); + } + + public boolean isLastFileFull() { + if (mappedFileQueue.getLastMappedFile() != null) { + return mappedFileQueue.getLastMappedFile().isFull(); + } else { + return true; + } + } + + public boolean shouldRoll() { + if (mappedFileQueue.getLastMappedFile() == null) { + return true; + } + if (mappedFileQueue.getLastMappedFile().isFull()) { + return true; + } + if (mappedFileQueue.getLastMappedFile().getWrotePosition() + BatchConsumeQueue.CQ_STORE_UNIT_SIZE + > mappedFileQueue.getMappedFileSize()) { + return true; + } + + return false; + } + + public boolean containsOffsetFile(final long physicalOffset) { + String fileName = UtilAll.offset2FileName(physicalOffset); + return mappedFileQueue.getMappedFiles().stream() + .anyMatch(mf -> Objects.equals(mf.getFile().getName(), fileName)); + } + + public long getMaxPhyOffsetInLog() { + MappedFile lastMappedFile = mappedFileQueue.getLastMappedFile(); + Long maxOffsetInLog = getMax(lastMappedFile, b -> b.getLong(0) + b.getInt(8)); + if (maxOffsetInLog != null) { + return maxOffsetInLog; + } else { + return -1; + } + } + + private T getMax(MappedFile mappedFile, Function function) { + if (mappedFile == null || mappedFile.getReadPosition() < CQ_STORE_UNIT_SIZE) { + return null; + } + + ByteBuffer byteBuffer = mappedFile.sliceByteBuffer(); + for (int i = mappedFile.getReadPosition() - CQ_STORE_UNIT_SIZE; i >= 0; i -= CQ_STORE_UNIT_SIZE) { + byteBuffer.position(i); + long offset = byteBuffer.getLong(); + int size = byteBuffer.getInt(); + long tagsCode = byteBuffer.getLong(); //tagscode + long timestamp = byteBuffer.getLong(); //timestamp + long msgBaseOffset = byteBuffer.getLong(); + short batchSize = byteBuffer.getShort(); + if (offset >= 0 && size > 0 && msgBaseOffset >= 0 && batchSize > 0) { + byteBuffer.position(i); //reset position + return function.apply(byteBuffer.slice()); + } + } + + return null; + } + + @Override + protected BatchOffsetIndex getMaxMsgOffset(MappedFile mappedFile, boolean getBatchSize, boolean getStoreTime) { + if (mappedFile == null || mappedFile.getReadPosition() < CQ_STORE_UNIT_SIZE) { + return null; + } + + ByteBuffer byteBuffer = mappedFile.sliceByteBuffer(); + for (int i = mappedFile.getReadPosition() - CQ_STORE_UNIT_SIZE; i >= 0; i -= CQ_STORE_UNIT_SIZE) { + byteBuffer.position(i); + long offset = byteBuffer.getLong(); + int size = byteBuffer.getInt(); + byteBuffer.getLong(); //tagscode + long timestamp = byteBuffer.getLong();//timestamp + long msgBaseOffset = byteBuffer.getLong(); + short batchSize = byteBuffer.getShort(); + if (offset >= 0 && size > 0 && msgBaseOffset >= 0 && batchSize > 0) { +// mappedFile.setWrotePosition(i + CQ_STORE_UNIT_SIZE); +// mappedFile.setFlushedPosition(i + CQ_STORE_UNIT_SIZE); +// mappedFile.setCommittedPosition(i + CQ_STORE_UNIT_SIZE); + return new BatchOffsetIndex(mappedFile, i, msgBaseOffset, batchSize, timestamp); + } + } + + return null; + } + + public long getMaxMsgOffsetFromFile(String simpleFileName) { + MappedFile mappedFile = mappedFileQueue.getMappedFiles().stream() + .filter(m -> Objects.equals(m.getFile().getName(), simpleFileName)) + .findFirst() + .orElse(null); + + if (mappedFile == null) { + return -1; + } + + BatchOffsetIndex max = getMaxMsgOffset(mappedFile, false, false); + if (max == null) { + return -1; + } + return max.getMsgOffset(); + } + + private void refreshMaxCache() { + doRefreshCache(m -> getMaxMsgOffset(m, false, true)); + } + + @Override + protected void refreshCache() { + refreshMaxCache(); + } + + public void refresh() { + reviseMaxAndMinOffsetInQueue(); + refreshCache(); + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/rocksdb/ConsumeQueueCompactionFilterFactory.java b/store/src/main/java/org/apache/rocketmq/store/rocksdb/ConsumeQueueCompactionFilterFactory.java new file mode 100644 index 00000000000..aa796c4d39e --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/rocksdb/ConsumeQueueCompactionFilterFactory.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.rocksdb; + +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.MessageStore; +import org.rocksdb.AbstractCompactionFilter; +import org.rocksdb.AbstractCompactionFilterFactory; +import org.rocksdb.RemoveConsumeQueueCompactionFilter; + +public class ConsumeQueueCompactionFilterFactory extends AbstractCompactionFilterFactory { + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.ROCKSDB_LOGGER_NAME); + private final MessageStore messageStore; + + public ConsumeQueueCompactionFilterFactory(final MessageStore messageStore) { + this.messageStore = messageStore; + } + + @Override + public String name() { + return "ConsumeQueueCompactionFilterFactory"; + } + + @Override + public RemoveConsumeQueueCompactionFilter createCompactionFilter(final AbstractCompactionFilter.Context context) { + long minPhyOffset = this.messageStore.getMinPhyOffset(); + LOGGER.info("manualCompaction minPhyOffset: {}, isFull: {}, isManual: {}", + minPhyOffset, context.isFullCompaction(), context.isManualCompaction()); + return new RemoveConsumeQueueCompactionFilter(minPhyOffset); + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/rocksdb/ConsumeQueueRocksDBStorage.java b/store/src/main/java/org/apache/rocketmq/store/rocksdb/ConsumeQueueRocksDBStorage.java new file mode 100644 index 00000000000..362684560c8 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/rocksdb/ConsumeQueueRocksDBStorage.java @@ -0,0 +1,133 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.rocksdb; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.config.AbstractRocksDBStorage; +import org.apache.rocketmq.common.utils.DataConverter; +import org.apache.rocketmq.store.MessageStore; +import org.rocksdb.ColumnFamilyDescriptor; +import org.rocksdb.ColumnFamilyHandle; +import org.rocksdb.ColumnFamilyOptions; +import org.rocksdb.CompactRangeOptions; +import org.rocksdb.ReadOptions; +import org.rocksdb.RocksDB; +import org.rocksdb.RocksDBException; +import org.rocksdb.RocksIterator; +import org.rocksdb.WriteBatch; +import org.rocksdb.WriteOptions; + +public class ConsumeQueueRocksDBStorage extends AbstractRocksDBStorage { + private final MessageStore messageStore; + private volatile ColumnFamilyHandle offsetCFHandle; + + public ConsumeQueueRocksDBStorage(final MessageStore messageStore, final String dbPath, final int prefixLen) { + this.messageStore = messageStore; + this.dbPath = dbPath; + this.readOnly = false; + } + + private void initOptions() { + this.options = RocksDBOptionsFactory.createDBOptions(); + + this.writeOptions = new WriteOptions(); + this.writeOptions.setSync(false); + this.writeOptions.setDisableWAL(true); + this.writeOptions.setNoSlowdown(true); + + this.totalOrderReadOptions = new ReadOptions(); + this.totalOrderReadOptions.setPrefixSameAsStart(false); + this.totalOrderReadOptions.setTotalOrderSeek(false); + + this.compactRangeOptions = new CompactRangeOptions(); + this.compactRangeOptions.setBottommostLevelCompaction(CompactRangeOptions.BottommostLevelCompaction.kForce); + this.compactRangeOptions.setAllowWriteStall(true); + this.compactRangeOptions.setExclusiveManualCompaction(false); + this.compactRangeOptions.setChangeLevel(true); + this.compactRangeOptions.setTargetLevel(-1); + this.compactRangeOptions.setMaxSubcompactions(4); + } + + @Override + protected boolean postLoad() { + try { + UtilAll.ensureDirOK(this.dbPath); + + initOptions(); + + final List cfDescriptors = new ArrayList(); + + ColumnFamilyOptions cqCfOptions = RocksDBOptionsFactory.createCQCFOptions(this.messageStore); + this.cfOptions.add(cqCfOptions); + cfDescriptors.add(new ColumnFamilyDescriptor(RocksDB.DEFAULT_COLUMN_FAMILY, cqCfOptions)); + + ColumnFamilyOptions offsetCfOptions = RocksDBOptionsFactory.createOffsetCFOptions(); + this.cfOptions.add(offsetCfOptions); + cfDescriptors.add(new ColumnFamilyDescriptor("offset".getBytes(DataConverter.CHARSET_UTF8), offsetCfOptions)); + + final List cfHandles = new ArrayList(); + open(cfDescriptors, cfHandles); + + this.defaultCFHandle = cfHandles.get(0); + this.offsetCFHandle = cfHandles.get(1); + } catch (final Exception e) { + LOGGER.error("postLoad Failed. {}", this.dbPath, e); + return false; + } + return true; + } + + @Override + protected void preShutdown() { + this.offsetCFHandle.close(); + } + + public byte[] getCQ(final byte[] keyBytes) throws RocksDBException { + return get(this.defaultCFHandle, this.totalOrderReadOptions, keyBytes); + } + + public byte[] getOffset(final byte[] keyBytes) throws RocksDBException { + return get(this.offsetCFHandle, this.totalOrderReadOptions, keyBytes); + } + + public List multiGet(final List cfhList, final List keys) throws RocksDBException { + return multiGet(this.totalOrderReadOptions, cfhList, keys); + } + + public void batchPut(final WriteBatch batch) throws RocksDBException { + batchPut(this.writeOptions, batch); + } + + public void manualCompaction(final long minPhyOffset) { + try { + manualCompaction(minPhyOffset, this.compactRangeOptions); + } catch (Exception e) { + LOGGER.error("manualCompaction Failed. minPhyOffset: {}", minPhyOffset, e); + } + } + + public RocksIterator seekOffsetCF() { + return this.db.newIterator(this.offsetCFHandle, this.totalOrderReadOptions); + } + + public ColumnFamilyHandle getOffsetCFHandle() { + return this.offsetCFHandle; + } +} \ No newline at end of file diff --git a/store/src/main/java/org/apache/rocketmq/store/rocksdb/RocksDBOptionsFactory.java b/store/src/main/java/org/apache/rocketmq/store/rocksdb/RocksDBOptionsFactory.java new file mode 100644 index 00000000000..a3a99d3346c --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/rocksdb/RocksDBOptionsFactory.java @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.rocksdb; + +import org.apache.rocketmq.common.config.ConfigRocksDBStorage; +import org.apache.rocketmq.store.MessageStore; +import org.rocksdb.BlockBasedTableConfig; +import org.rocksdb.BloomFilter; +import org.rocksdb.ColumnFamilyOptions; +import org.rocksdb.CompactionOptionsUniversal; +import org.rocksdb.CompactionStopStyle; +import org.rocksdb.CompactionStyle; +import org.rocksdb.CompressionType; +import org.rocksdb.DBOptions; +import org.rocksdb.DataBlockIndexType; +import org.rocksdb.IndexType; +import org.rocksdb.InfoLogLevel; +import org.rocksdb.LRUCache; +import org.rocksdb.RateLimiter; +import org.rocksdb.SkipListMemTableConfig; +import org.rocksdb.Statistics; +import org.rocksdb.StatsLevel; +import org.rocksdb.StringAppendOperator; +import org.rocksdb.WALRecoveryMode; +import org.rocksdb.util.SizeUnit; + +public class RocksDBOptionsFactory { + + public static ColumnFamilyOptions createCQCFOptions(final MessageStore messageStore) { + BlockBasedTableConfig blockBasedTableConfig = new BlockBasedTableConfig(). + setFormatVersion(5). + setIndexType(IndexType.kBinarySearch). + setDataBlockIndexType(DataBlockIndexType.kDataBlockBinaryAndHash). + setDataBlockHashTableUtilRatio(0.75). + setBlockSize(32 * SizeUnit.KB). + setMetadataBlockSize(4 * SizeUnit.KB). + setFilterPolicy(new BloomFilter(16, false)). + setCacheIndexAndFilterBlocks(false). + setCacheIndexAndFilterBlocksWithHighPriority(true). + setPinL0FilterAndIndexBlocksInCache(false). + setPinTopLevelIndexAndFilter(true). + setBlockCache(new LRUCache(1024 * SizeUnit.MB, 8, false)). + setWholeKeyFiltering(true); + + ColumnFamilyOptions columnFamilyOptions = new ColumnFamilyOptions(); + CompactionOptionsUniversal compactionOption = new CompactionOptionsUniversal(); + compactionOption.setSizeRatio(100). + setMaxSizeAmplificationPercent(25). + setAllowTrivialMove(true). + setMinMergeWidth(2). + setMaxMergeWidth(Integer.MAX_VALUE). + setStopStyle(CompactionStopStyle.CompactionStopStyleTotalSize). + setCompressionSizePercent(-1); + return columnFamilyOptions.setMaxWriteBufferNumber(4). + setWriteBufferSize(128 * SizeUnit.MB). + setMinWriteBufferNumberToMerge(1). + setTableFormatConfig(blockBasedTableConfig). + setMemTableConfig(new SkipListMemTableConfig()). + setCompressionType(CompressionType.LZ4_COMPRESSION). + setBottommostCompressionType(CompressionType.ZSTD_COMPRESSION). + setNumLevels(7). + setCompactionStyle(CompactionStyle.UNIVERSAL). + setCompactionOptionsUniversal(compactionOption). + setMaxCompactionBytes(100 * SizeUnit.GB). + setSoftPendingCompactionBytesLimit(100 * SizeUnit.GB). + setHardPendingCompactionBytesLimit(256 * SizeUnit.GB). + setLevel0FileNumCompactionTrigger(2). + setLevel0SlowdownWritesTrigger(8). + setLevel0StopWritesTrigger(10). + setTargetFileSizeBase(256 * SizeUnit.MB). + setTargetFileSizeMultiplier(2). + setMergeOperator(new StringAppendOperator()). + setCompactionFilterFactory(new ConsumeQueueCompactionFilterFactory(messageStore)). + setReportBgIoStats(true). + setOptimizeFiltersForHits(true); + } + + public static ColumnFamilyOptions createOffsetCFOptions() { + BlockBasedTableConfig blockBasedTableConfig = new BlockBasedTableConfig(). + setFormatVersion(5). + setIndexType(IndexType.kBinarySearch). + setDataBlockIndexType(DataBlockIndexType.kDataBlockBinarySearch). + setBlockSize(32 * SizeUnit.KB). + setFilterPolicy(new BloomFilter(16, false)). + setCacheIndexAndFilterBlocks(false). + setCacheIndexAndFilterBlocksWithHighPriority(true). + setPinL0FilterAndIndexBlocksInCache(false). + setPinTopLevelIndexAndFilter(true). + setBlockCache(new LRUCache(128 * SizeUnit.MB, 8, false)). + setWholeKeyFiltering(true); + + ColumnFamilyOptions columnFamilyOptions = new ColumnFamilyOptions(); + return columnFamilyOptions.setMaxWriteBufferNumber(4). + setWriteBufferSize(64 * SizeUnit.MB). + setMinWriteBufferNumberToMerge(1). + setTableFormatConfig(blockBasedTableConfig). + setMemTableConfig(new SkipListMemTableConfig()). + setCompressionType(CompressionType.NO_COMPRESSION). + setNumLevels(7). + setCompactionStyle(CompactionStyle.LEVEL). + setLevel0FileNumCompactionTrigger(2). + setLevel0SlowdownWritesTrigger(8). + setLevel0StopWritesTrigger(10). + setTargetFileSizeBase(64 * SizeUnit.MB). + setTargetFileSizeMultiplier(2). + setMaxBytesForLevelBase(256 * SizeUnit.MB). + setMaxBytesForLevelMultiplier(2). + setMergeOperator(new StringAppendOperator()). + setInplaceUpdateSupport(true); + } + + /** + * Create a rocksdb db options, the user must take care to close it after closing db. + * @return + */ + public static DBOptions createDBOptions() { + //Turn based on https://github.com/facebook/rocksdb/wiki/RocksDB-Tuning-Guide + // and http://gitlab.alibaba-inc.com/aloha/aloha/blob/branch_2_5_0/jstorm-core/src/main/java/com/alibaba/jstorm/cache/rocksdb/RocksDbOptionsFactory.java + DBOptions options = new DBOptions(); + Statistics statistics = new Statistics(); + statistics.setStatsLevel(StatsLevel.EXCEPT_DETAILED_TIMERS); + return options. + setDbLogDir(ConfigRocksDBStorage.getDBLogDir()). + setInfoLogLevel(InfoLogLevel.INFO_LEVEL). + setWalRecoveryMode(WALRecoveryMode.PointInTimeRecovery). + setManualWalFlush(true). + setMaxTotalWalSize(0). + setWalSizeLimitMB(0). + setWalTtlSeconds(0). + setCreateIfMissing(true). + setCreateMissingColumnFamilies(true). + setMaxOpenFiles(-1). + setMaxLogFileSize(1 * SizeUnit.GB). + setKeepLogFileNum(5). + setMaxManifestFileSize(1 * SizeUnit.GB). + setAllowConcurrentMemtableWrite(false). + setStatistics(statistics). + setAtomicFlush(true). + setMaxBackgroundJobs(32). + setMaxSubcompactions(8). + setParanoidChecks(true). + setDelayedWriteRate(16 * SizeUnit.MB). + setRateLimiter(new RateLimiter(100 * SizeUnit.MB)). + setUseDirectIoForFlushAndCompaction(false). + setUseDirectReads(false); + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/schedule/ScheduleMessageService.java b/store/src/main/java/org/apache/rocketmq/store/schedule/ScheduleMessageService.java deleted file mode 100644 index 35b8e8565ea..00000000000 --- a/store/src/main/java/org/apache/rocketmq/store/schedule/ScheduleMessageService.java +++ /dev/null @@ -1,383 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.rocketmq.store.schedule; - -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Timer; -import java.util.TimerTask; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import org.apache.rocketmq.common.ConfigManager; -import org.apache.rocketmq.common.TopicFilterType; -import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.common.message.MessageAccessor; -import org.apache.rocketmq.common.message.MessageConst; -import org.apache.rocketmq.common.message.MessageDecoder; -import org.apache.rocketmq.common.message.MessageExt; -import org.apache.rocketmq.common.running.RunningStats; -import org.apache.rocketmq.store.ConsumeQueue; -import org.apache.rocketmq.store.ConsumeQueueExt; -import org.apache.rocketmq.store.DefaultMessageStore; -import org.apache.rocketmq.store.MessageExtBrokerInner; -import org.apache.rocketmq.store.PutMessageResult; -import org.apache.rocketmq.store.PutMessageStatus; -import org.apache.rocketmq.store.SelectMappedBufferResult; -import org.apache.rocketmq.store.config.StorePathConfigHelper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class ScheduleMessageService extends ConfigManager { - private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); - - public static final String SCHEDULE_TOPIC = "SCHEDULE_TOPIC_XXXX"; - private static final long FIRST_DELAY_TIME = 1000L; - private static final long DELAY_FOR_A_WHILE = 100L; - private static final long DELAY_FOR_A_PERIOD = 10000L; - - private final ConcurrentMap delayLevelTable = - new ConcurrentHashMap(32); - - private final ConcurrentMap offsetTable = - new ConcurrentHashMap(32); - - private final Timer timer = new Timer("ScheduleMessageTimerThread", true); - - private final DefaultMessageStore defaultMessageStore; - - private int maxDelayLevel; - - public ScheduleMessageService(final DefaultMessageStore defaultMessageStore) { - this.defaultMessageStore = defaultMessageStore; - } - - public static int queueId2DelayLevel(final int queueId) { - return queueId + 1; - } - - public static int delayLevel2QueueId(final int delayLevel) { - return delayLevel - 1; - } - - public void buildRunningStats(HashMap stats) { - Iterator> it = this.offsetTable.entrySet().iterator(); - while (it.hasNext()) { - Entry next = it.next(); - int queueId = delayLevel2QueueId(next.getKey()); - long delayOffset = next.getValue(); - long maxOffset = this.defaultMessageStore.getMaxOffsetInQueue(SCHEDULE_TOPIC, queueId); - String value = String.format("%d,%d", delayOffset, maxOffset); - String key = String.format("%s_%d", RunningStats.scheduleMessageOffset.name(), next.getKey()); - stats.put(key, value); - } - } - - private void updateOffset(int delayLevel, long offset) { - this.offsetTable.put(delayLevel, offset); - } - - public long computeDeliverTimestamp(final int delayLevel, final long storeTimestamp) { - Long time = this.delayLevelTable.get(delayLevel); - if (time != null) { - return time + storeTimestamp; - } - - return storeTimestamp + 1000; - } - - public void start() { - - for (Map.Entry entry : this.delayLevelTable.entrySet()) { - Integer level = entry.getKey(); - Long timeDelay = entry.getValue(); - Long offset = this.offsetTable.get(level); - if (null == offset) { - offset = 0L; - } - - if (timeDelay != null) { - this.timer.schedule(new DeliverDelayedMessageTimerTask(level, offset), FIRST_DELAY_TIME); - } - } - - this.timer.scheduleAtFixedRate(new TimerTask() { - - @Override - public void run() { - try { - ScheduleMessageService.this.persist(); - } catch (Throwable e) { - log.error("scheduleAtFixedRate flush exception", e); - } - } - }, 10000, this.defaultMessageStore.getMessageStoreConfig().getFlushDelayOffsetInterval()); - } - - public void shutdown() { - this.timer.cancel(); - } - - public int getMaxDelayLevel() { - return maxDelayLevel; - } - - public String encode() { - return this.encode(false); - } - - public boolean load() { - boolean result = super.load(); - result = result && this.parseDelayLevel(); - return result; - } - - @Override - public String configFilePath() { - return StorePathConfigHelper.getDelayOffsetStorePath(this.defaultMessageStore.getMessageStoreConfig() - .getStorePathRootDir()); - } - - @Override - public void decode(String jsonString) { - if (jsonString != null) { - DelayOffsetSerializeWrapper delayOffsetSerializeWrapper = - DelayOffsetSerializeWrapper.fromJson(jsonString, DelayOffsetSerializeWrapper.class); - if (delayOffsetSerializeWrapper != null) { - this.offsetTable.putAll(delayOffsetSerializeWrapper.getOffsetTable()); - } - } - } - - public String encode(final boolean prettyFormat) { - DelayOffsetSerializeWrapper delayOffsetSerializeWrapper = new DelayOffsetSerializeWrapper(); - delayOffsetSerializeWrapper.setOffsetTable(this.offsetTable); - return delayOffsetSerializeWrapper.toJson(prettyFormat); - } - - public boolean parseDelayLevel() { - HashMap timeUnitTable = new HashMap(); - timeUnitTable.put("s", 1000L); - timeUnitTable.put("m", 1000L * 60); - timeUnitTable.put("h", 1000L * 60 * 60); - timeUnitTable.put("d", 1000L * 60 * 60 * 24); - - String levelString = this.defaultMessageStore.getMessageStoreConfig().getMessageDelayLevel(); - try { - String[] levelArray = levelString.split(" "); - for (int i = 0; i < levelArray.length; i++) { - String value = levelArray[i]; - String ch = value.substring(value.length() - 1); - Long tu = timeUnitTable.get(ch); - - int level = i + 1; - if (level > this.maxDelayLevel) { - this.maxDelayLevel = level; - } - long num = Long.parseLong(value.substring(0, value.length() - 1)); - long delayTimeMillis = tu * num; - this.delayLevelTable.put(level, delayTimeMillis); - } - } catch (Exception e) { - log.error("parseDelayLevel exception", e); - log.info("levelString String = {}", levelString); - return false; - } - - return true; - } - - class DeliverDelayedMessageTimerTask extends TimerTask { - private final int delayLevel; - private final long offset; - - public DeliverDelayedMessageTimerTask(int delayLevel, long offset) { - this.delayLevel = delayLevel; - this.offset = offset; - } - - @Override - public void run() { - try { - this.executeOnTimeup(); - } catch (Exception e) { - // XXX: warn and notify me - log.error("ScheduleMessageService, executeOnTimeup exception", e); - ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask( - this.delayLevel, this.offset), DELAY_FOR_A_PERIOD); - } - } - - /** - * @return - */ - private long correctDeliverTimestamp(final long now, final long deliverTimestamp) { - - long result = deliverTimestamp; - - long maxTimestamp = now + ScheduleMessageService.this.delayLevelTable.get(this.delayLevel); - if (deliverTimestamp > maxTimestamp) { - result = now; - } - - return result; - } - - public void executeOnTimeup() { - ConsumeQueue cq = - ScheduleMessageService.this.defaultMessageStore.findConsumeQueue(SCHEDULE_TOPIC, - delayLevel2QueueId(delayLevel)); - - long failScheduleOffset = offset; - - if (cq != null) { - SelectMappedBufferResult bufferCQ = cq.getIndexBuffer(this.offset); - if (bufferCQ != null) { - try { - long nextOffset = offset; - int i = 0; - ConsumeQueueExt.CqExtUnit cqExtUnit = new ConsumeQueueExt.CqExtUnit(); - for (; i < bufferCQ.getSize(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE) { - long offsetPy = bufferCQ.getByteBuffer().getLong(); - int sizePy = bufferCQ.getByteBuffer().getInt(); - long tagsCode = bufferCQ.getByteBuffer().getLong(); - - if (cq.isExtAddr(tagsCode)) { - if (cq.getExt(tagsCode, cqExtUnit)) { - tagsCode = cqExtUnit.getTagsCode(); - } else { - //can't find ext content.So re compute tags code. - log.error("[BUG] can't find consume queue extend file content!addr={}, offsetPy={}, sizePy={}", - tagsCode, offsetPy, sizePy); - long msgStoreTime = defaultMessageStore.getCommitLog().pickupStoreTimestamp(offsetPy, sizePy); - tagsCode = computeDeliverTimestamp(delayLevel, msgStoreTime); - } - } - - long now = System.currentTimeMillis(); - long deliverTimestamp = this.correctDeliverTimestamp(now, tagsCode); - - nextOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE); - - long countdown = deliverTimestamp - now; - - if (countdown <= 0) { - MessageExt msgExt = - ScheduleMessageService.this.defaultMessageStore.lookMessageByOffset( - offsetPy, sizePy); - - if (msgExt != null) { - try { - MessageExtBrokerInner msgInner = this.messageTimeup(msgExt); - PutMessageResult putMessageResult = - ScheduleMessageService.this.defaultMessageStore - .putMessage(msgInner); - - if (putMessageResult != null - && putMessageResult.getPutMessageStatus() == PutMessageStatus.PUT_OK) { - continue; - } else { - // XXX: warn and notify me - log.error( - "ScheduleMessageService, a message time up, but reput it failed, topic: {} msgId {}", - msgExt.getTopic(), msgExt.getMsgId()); - ScheduleMessageService.this.timer.schedule( - new DeliverDelayedMessageTimerTask(this.delayLevel, - nextOffset), DELAY_FOR_A_PERIOD); - ScheduleMessageService.this.updateOffset(this.delayLevel, - nextOffset); - return; - } - } catch (Exception e) { - /* - * XXX: warn and notify me - - - - */ - log.error( - "ScheduleMessageService, messageTimeup execute error, drop it. msgExt=" - + msgExt + ", nextOffset=" + nextOffset + ",offsetPy=" - + offsetPy + ",sizePy=" + sizePy, e); - } - } - } else { - ScheduleMessageService.this.timer.schedule( - new DeliverDelayedMessageTimerTask(this.delayLevel, nextOffset), - countdown); - ScheduleMessageService.this.updateOffset(this.delayLevel, nextOffset); - return; - } - } // end of for - - nextOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE); - ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask( - this.delayLevel, nextOffset), DELAY_FOR_A_WHILE); - ScheduleMessageService.this.updateOffset(this.delayLevel, nextOffset); - return; - } finally { - - bufferCQ.release(); - } - } // end of if (bufferCQ != null) - else { - - long cqMinOffset = cq.getMinOffsetInQueue(); - if (offset < cqMinOffset) { - failScheduleOffset = cqMinOffset; - log.error("schedule CQ offset invalid. offset=" + offset + ", cqMinOffset=" - + cqMinOffset + ", queueId=" + cq.getQueueId()); - } - } - } // end of if (cq != null) - - ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(this.delayLevel, - failScheduleOffset), DELAY_FOR_A_WHILE); - } - - private MessageExtBrokerInner messageTimeup(MessageExt msgExt) { - MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); - msgInner.setBody(msgExt.getBody()); - msgInner.setFlag(msgExt.getFlag()); - MessageAccessor.setProperties(msgInner, msgExt.getProperties()); - - TopicFilterType topicFilterType = MessageExt.parseTopicFilterType(msgInner.getSysFlag()); - long tagsCodeValue = - MessageExtBrokerInner.tagsString2tagsCode(topicFilterType, msgInner.getTags()); - msgInner.setTagsCode(tagsCodeValue); - msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgExt.getProperties())); - - msgInner.setSysFlag(msgExt.getSysFlag()); - msgInner.setBornTimestamp(msgExt.getBornTimestamp()); - msgInner.setBornHost(msgExt.getBornHost()); - msgInner.setStoreHost(msgExt.getStoreHost()); - msgInner.setReconsumeTimes(msgExt.getReconsumeTimes()); - - msgInner.setWaitStoreMsgOK(false); - MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_DELAY_TIME_LEVEL); - - msgInner.setTopic(msgInner.getProperty(MessageConst.PROPERTY_REAL_TOPIC)); - - String queueIdStr = msgInner.getProperty(MessageConst.PROPERTY_REAL_QUEUE_ID); - int queueId = Integer.parseInt(queueIdStr); - msgInner.setQueueId(queueId); - - return msgInner; - } - } -} diff --git a/store/src/main/java/org/apache/rocketmq/store/stats/BrokerStats.java b/store/src/main/java/org/apache/rocketmq/store/stats/BrokerStats.java index a3240a4695a..fb717550f40 100644 --- a/store/src/main/java/org/apache/rocketmq/store/stats/BrokerStats.java +++ b/store/src/main/java/org/apache/rocketmq/store/stats/BrokerStats.java @@ -17,14 +17,14 @@ package org.apache.rocketmq.store.stats; import org.apache.rocketmq.common.constant.LoggerName; -import org.apache.rocketmq.store.DefaultMessageStore; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.MessageStore; public class BrokerStats { private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); - private final DefaultMessageStore defaultMessageStore; + private final MessageStore defaultMessageStore; private volatile long msgPutTotalYesterdayMorning; @@ -34,7 +34,7 @@ public class BrokerStats { private volatile long msgGetTotalTodayMorning; - public BrokerStats(DefaultMessageStore defaultMessageStore) { + public BrokerStats(MessageStore defaultMessageStore) { this.defaultMessageStore = defaultMessageStore; } @@ -43,9 +43,9 @@ public void record() { this.msgGetTotalYesterdayMorning = this.msgGetTotalTodayMorning; this.msgPutTotalTodayMorning = - this.defaultMessageStore.getStoreStatsService().getPutMessageTimesTotal(); + this.defaultMessageStore.getBrokerStatsManager().getBrokerPutNumsWithoutSystemTopic(); this.msgGetTotalTodayMorning = - this.defaultMessageStore.getStoreStatsService().getGetMessageTransferedMsgCount().get(); + this.defaultMessageStore.getBrokerStatsManager().getBrokerGetNumsWithoutSystemTopic(); log.info("yesterday put message total: {}", msgPutTotalTodayMorning - msgPutTotalYesterdayMorning); log.info("yesterday get message total: {}", msgGetTotalTodayMorning - msgGetTotalYesterdayMorning); @@ -84,10 +84,10 @@ public void setMsgGetTotalTodayMorning(long msgGetTotalTodayMorning) { } public long getMsgPutTotalTodayNow() { - return this.defaultMessageStore.getStoreStatsService().getPutMessageTimesTotal(); + return this.defaultMessageStore.getBrokerStatsManager().getBrokerPutNumsWithoutSystemTopic(); } public long getMsgGetTotalTodayNow() { - return this.defaultMessageStore.getStoreStatsService().getGetMessageTransferedMsgCount().get(); + return this.defaultMessageStore.getBrokerStatsManager().getBrokerGetNumsWithoutSystemTopic(); } } diff --git a/store/src/main/java/org/apache/rocketmq/store/stats/BrokerStatsManager.java b/store/src/main/java/org/apache/rocketmq/store/stats/BrokerStatsManager.java index 64f76cabacd..489d7b4fbce 100644 --- a/store/src/main/java/org/apache/rocketmq/store/stats/BrokerStatsManager.java +++ b/store/src/main/java/org/apache/rocketmq/store/stats/BrokerStatsManager.java @@ -17,83 +17,275 @@ package org.apache.rocketmq.store.stats; import java.util.HashMap; -import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.rocketmq.common.BrokerConfig; import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.UtilAll; import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.statistics.StatisticsItem; +import org.apache.rocketmq.common.statistics.StatisticsItemFormatter; +import org.apache.rocketmq.common.statistics.StatisticsItemPrinter; +import org.apache.rocketmq.common.statistics.StatisticsItemScheduledIncrementPrinter; +import org.apache.rocketmq.common.statistics.StatisticsItemScheduledPrinter; +import org.apache.rocketmq.common.statistics.StatisticsItemStateGetter; +import org.apache.rocketmq.common.statistics.StatisticsKindMeta; +import org.apache.rocketmq.common.statistics.StatisticsManager; import org.apache.rocketmq.common.stats.MomentStatsItemSet; +import org.apache.rocketmq.common.stats.Stats; import org.apache.rocketmq.common.stats.StatsItem; import org.apache.rocketmq.common.stats.StatsItemSet; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; public class BrokerStatsManager { - public static final String TOPIC_PUT_NUMS = "TOPIC_PUT_NUMS"; - public static final String TOPIC_PUT_SIZE = "TOPIC_PUT_SIZE"; - public static final String GROUP_GET_NUMS = "GROUP_GET_NUMS"; - public static final String GROUP_GET_SIZE = "GROUP_GET_SIZE"; - public static final String SNDBCK_PUT_NUMS = "SNDBCK_PUT_NUMS"; - public static final String BROKER_PUT_NUMS = "BROKER_PUT_NUMS"; - public static final String BROKER_GET_NUMS = "BROKER_GET_NUMS"; - public static final String GROUP_GET_FROM_DISK_NUMS = "GROUP_GET_FROM_DISK_NUMS"; - public static final String GROUP_GET_FROM_DISK_SIZE = "GROUP_GET_FROM_DISK_SIZE"; - public static final String BROKER_GET_FROM_DISK_NUMS = "BROKER_GET_FROM_DISK_NUMS"; - public static final String BROKER_GET_FROM_DISK_SIZE = "BROKER_GET_FROM_DISK_SIZE"; + @Deprecated public static final String QUEUE_PUT_NUMS = Stats.QUEUE_PUT_NUMS; + @Deprecated public static final String QUEUE_PUT_SIZE = Stats.QUEUE_PUT_SIZE; + @Deprecated public static final String QUEUE_GET_NUMS = Stats.QUEUE_GET_NUMS; + @Deprecated public static final String QUEUE_GET_SIZE = Stats.QUEUE_GET_SIZE; + @Deprecated public static final String TOPIC_PUT_NUMS = Stats.TOPIC_PUT_NUMS; + @Deprecated public static final String TOPIC_PUT_SIZE = Stats.TOPIC_PUT_SIZE; + + @Deprecated public static final String GROUP_GET_NUMS = Stats.GROUP_GET_NUMS; + @Deprecated public static final String GROUP_GET_SIZE = Stats.GROUP_GET_SIZE; + + @Deprecated public static final String SNDBCK_PUT_NUMS = Stats.SNDBCK_PUT_NUMS; + @Deprecated public static final String BROKER_PUT_NUMS = Stats.BROKER_PUT_NUMS; + @Deprecated public static final String BROKER_GET_NUMS = Stats.BROKER_GET_NUMS; + @Deprecated public static final String GROUP_GET_FROM_DISK_NUMS = Stats.GROUP_GET_FROM_DISK_NUMS; + @Deprecated public static final String GROUP_GET_FROM_DISK_SIZE = Stats.GROUP_GET_FROM_DISK_SIZE; + @Deprecated public static final String BROKER_GET_FROM_DISK_NUMS = Stats.BROKER_GET_FROM_DISK_NUMS; + @Deprecated public static final String BROKER_GET_FROM_DISK_SIZE = Stats.BROKER_GET_FROM_DISK_SIZE; // For commercial - public static final String COMMERCIAL_SEND_TIMES = "COMMERCIAL_SEND_TIMES"; - public static final String COMMERCIAL_SNDBCK_TIMES = "COMMERCIAL_SNDBCK_TIMES"; - public static final String COMMERCIAL_RCV_TIMES = "COMMERCIAL_RCV_TIMES"; - public static final String COMMERCIAL_RCV_EPOLLS = "COMMERCIAL_RCV_EPOLLS"; - public static final String COMMERCIAL_SEND_SIZE = "COMMERCIAL_SEND_SIZE"; - public static final String COMMERCIAL_RCV_SIZE = "COMMERCIAL_RCV_SIZE"; - public static final String COMMERCIAL_PERM_FAILURES = "COMMERCIAL_PERM_FAILURES"; + @Deprecated public static final String COMMERCIAL_SEND_TIMES = Stats.COMMERCIAL_SEND_TIMES; + @Deprecated public static final String COMMERCIAL_SNDBCK_TIMES = Stats.COMMERCIAL_SNDBCK_TIMES; + @Deprecated public static final String COMMERCIAL_RCV_TIMES = Stats.COMMERCIAL_RCV_TIMES; + @Deprecated public static final String COMMERCIAL_RCV_EPOLLS = Stats.COMMERCIAL_RCV_EPOLLS; + @Deprecated public static final String COMMERCIAL_SEND_SIZE = Stats.COMMERCIAL_SEND_SIZE; + @Deprecated public static final String COMMERCIAL_RCV_SIZE = Stats.COMMERCIAL_RCV_SIZE; + @Deprecated public static final String COMMERCIAL_PERM_FAILURES = Stats.COMMERCIAL_PERM_FAILURES; + + // Send message latency + public static final String TOPIC_PUT_LATENCY = "TOPIC_PUT_LATENCY"; + public static final String GROUP_ACK_NUMS = "GROUP_ACK_NUMS"; + public static final String GROUP_CK_NUMS = "GROUP_CK_NUMS"; + public static final String DLQ_PUT_NUMS = "DLQ_PUT_NUMS"; + public static final String BROKER_ACK_NUMS = "BROKER_ACK_NUMS"; + public static final String BROKER_CK_NUMS = "BROKER_CK_NUMS"; + public static final String BROKER_GET_NUMS_WITHOUT_SYSTEM_TOPIC = "BROKER_GET_NUMS_WITHOUT_SYSTEM_TOPIC"; + public static final String BROKER_PUT_NUMS_WITHOUT_SYSTEM_TOPIC = "BROKER_PUT_NUMS_WITHOUT_SYSTEM_TOPIC"; + public static final String SNDBCK2DLQ_TIMES = "SNDBCK2DLQ_TIMES"; + public static final String COMMERCIAL_OWNER = "Owner"; - // Message Size limit for one api-calling count. - public static final double SIZE_PER_COUNT = 64 * 1024; - public static final String GROUP_GET_FALL_SIZE = "GROUP_GET_FALL_SIZE"; - public static final String GROUP_GET_FALL_TIME = "GROUP_GET_FALL_TIME"; + public static final String ACCOUNT_OWNER_PARENT = "OWNER_PARENT"; + public static final String ACCOUNT_OWNER_SELF = "OWNER_SELF"; + + public static final long ACCOUNT_STAT_INVERTAL = 60 * 1000; + public static final String ACCOUNT_AUTH_TYPE = "AUTH_TYPE"; + + public static final String ACCOUNT_SEND = "SEND"; + public static final String ACCOUNT_RCV = "RCV"; + public static final String ACCOUNT_SEND_BACK = "SEND_BACK"; + public static final String ACCOUNT_SEND_BACK_TO_DLQ = "SEND_BACK_TO_DLQ"; + public static final String ACCOUNT_AUTH_FAILED = "AUTH_FAILED"; + public static final String ACCOUNT_SEND_REJ = "SEND_REJ"; + public static final String ACCOUNT_REV_REJ = "RCV_REJ"; + + public static final String MSG_NUM = "MSG_NUM"; + public static final String MSG_SIZE = "MSG_SIZE"; + public static final String SUCCESS_MSG_NUM = "SUCCESS_MSG_NUM"; + public static final String FAILURE_MSG_NUM = "FAILURE_MSG_NUM"; + public static final String COMMERCIAL_MSG_NUM = "COMMERCIAL_MSG_NUM"; + public static final String SUCCESS_REQ_NUM = "SUCCESS_REQ_NUM"; + public static final String FAILURE_REQ_NUM = "FAILURE_REQ_NUM"; + public static final String SUCCESS_MSG_SIZE = "SUCCESS_MSG_SIZE"; + public static final String FAILURE_MSG_SIZE = "FAILURE_MSG_SIZE"; + public static final String RT = "RT"; + public static final String INNER_RT = "INNER_RT"; + + @Deprecated public static final String GROUP_GET_FALL_SIZE = Stats.GROUP_GET_FALL_SIZE; + @Deprecated public static final String GROUP_GET_FALL_TIME = Stats.GROUP_GET_FALL_TIME; // Pull Message Latency - public static final String GROUP_GET_LATENCY = "GROUP_GET_LATENCY"; + @Deprecated public static final String GROUP_GET_LATENCY = Stats.GROUP_GET_LATENCY; + + // Consumer Register Time + public static final String CONSUMER_REGISTER_TIME = "CONSUMER_REGISTER_TIME"; + // Producer Register Time + public static final String PRODUCER_REGISTER_TIME = "PRODUCER_REGISTER_TIME"; + public static final String CHANNEL_ACTIVITY = "CHANNEL_ACTIVITY"; + public static final String CHANNEL_ACTIVITY_CONNECT = "CONNECT"; + public static final String CHANNEL_ACTIVITY_IDLE = "IDLE"; + public static final String CHANNEL_ACTIVITY_EXCEPTION = "EXCEPTION"; + public static final String CHANNEL_ACTIVITY_CLOSE = "CLOSE"; /** * read disk follow stats */ private static final Logger log = LoggerFactory.getLogger(LoggerName.ROCKETMQ_STATS_LOGGER_NAME); - private static final Logger COMMERCIAL_LOG = LoggerFactory.getLogger(LoggerName.COMMERCIAL_LOGGER_NAME); - private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl( - "BrokerStatsThread")); - private final ScheduledExecutorService commercialExecutor = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl( - "CommercialStatsThread")); - private final HashMap statsTable = new HashMap(); + private static final Logger COMMERCIAL_LOG = LoggerFactory.getLogger( + LoggerName.COMMERCIAL_LOGGER_NAME); + private static final Logger ACCOUNT_LOG = LoggerFactory.getLogger(LoggerName.ACCOUNT_LOGGER_NAME); + private static final Logger DLQ_STAT_LOG = LoggerFactory.getLogger( + LoggerName.DLQ_STATS_LOGGER_NAME); + private ScheduledExecutorService scheduledExecutorService; + private ScheduledExecutorService commercialExecutor; + private ScheduledExecutorService accountExecutor; + + private final HashMap statsTable = new HashMap<>(); private final String clusterName; - private final MomentStatsItemSet momentStatsItemSetFallSize = new MomentStatsItemSet(GROUP_GET_FALL_SIZE, scheduledExecutorService, log); - private final MomentStatsItemSet momentStatsItemSetFallTime = new MomentStatsItemSet(GROUP_GET_FALL_TIME, scheduledExecutorService, log); + private final boolean enableQueueStat; + private MomentStatsItemSet momentStatsItemSetFallSize; + private MomentStatsItemSet momentStatsItemSetFallTime; + + private final StatisticsManager accountStatManager = new StatisticsManager(); + private StateGetter produerStateGetter; + private StateGetter consumerStateGetter; - public BrokerStatsManager(String clusterName) { + private BrokerConfig brokerConfig; + + public BrokerStatsManager(BrokerConfig brokerConfig) { + this.brokerConfig = brokerConfig; + this.enableQueueStat = brokerConfig.isEnableDetailStat(); + initScheduleService(); + this.clusterName = brokerConfig.getBrokerClusterName(); + init(); + } + + public BrokerStatsManager(String clusterName, boolean enableQueueStat) { this.clusterName = clusterName; + this.enableQueueStat = enableQueueStat; + initScheduleService(); + init(); + } + + public void init() { + momentStatsItemSetFallSize = new MomentStatsItemSet(GROUP_GET_FALL_SIZE, + scheduledExecutorService, log); + + momentStatsItemSetFallTime = new MomentStatsItemSet(GROUP_GET_FALL_TIME, + scheduledExecutorService, log); + + if (enableQueueStat) { + this.statsTable.put(Stats.QUEUE_PUT_NUMS, new StatsItemSet(Stats.QUEUE_PUT_NUMS, this.scheduledExecutorService, log)); + this.statsTable.put(Stats.QUEUE_PUT_SIZE, new StatsItemSet(Stats.QUEUE_PUT_SIZE, this.scheduledExecutorService, log)); + this.statsTable.put(Stats.QUEUE_GET_NUMS, new StatsItemSet(Stats.QUEUE_GET_NUMS, this.scheduledExecutorService, log)); + this.statsTable.put(Stats.QUEUE_GET_SIZE, new StatsItemSet(Stats.QUEUE_GET_SIZE, this.scheduledExecutorService, log)); + } + this.statsTable.put(Stats.TOPIC_PUT_NUMS, new StatsItemSet(Stats.TOPIC_PUT_NUMS, this.scheduledExecutorService, log)); + this.statsTable.put(Stats.TOPIC_PUT_SIZE, new StatsItemSet(Stats.TOPIC_PUT_SIZE, this.scheduledExecutorService, log)); + this.statsTable.put(Stats.GROUP_GET_NUMS, new StatsItemSet(Stats.GROUP_GET_NUMS, this.scheduledExecutorService, log)); + this.statsTable.put(Stats.GROUP_GET_SIZE, new StatsItemSet(Stats.GROUP_GET_SIZE, this.scheduledExecutorService, log)); + this.statsTable.put(GROUP_ACK_NUMS, new StatsItemSet(GROUP_ACK_NUMS, this.scheduledExecutorService, log)); + this.statsTable.put(GROUP_CK_NUMS, new StatsItemSet(GROUP_CK_NUMS, this.scheduledExecutorService, log)); + this.statsTable.put(Stats.GROUP_GET_LATENCY, new StatsItemSet(Stats.GROUP_GET_LATENCY, this.scheduledExecutorService, log)); + this.statsTable.put(TOPIC_PUT_LATENCY, new StatsItemSet(TOPIC_PUT_LATENCY, this.scheduledExecutorService, log)); + this.statsTable.put(Stats.SNDBCK_PUT_NUMS, new StatsItemSet(Stats.SNDBCK_PUT_NUMS, this.scheduledExecutorService, log)); + this.statsTable.put(DLQ_PUT_NUMS, new StatsItemSet(DLQ_PUT_NUMS, this.scheduledExecutorService, log)); + this.statsTable.put(Stats.BROKER_PUT_NUMS, new StatsItemSet(Stats.BROKER_PUT_NUMS, this.scheduledExecutorService, log)); + this.statsTable.put(Stats.BROKER_GET_NUMS, new StatsItemSet(Stats.BROKER_GET_NUMS, this.scheduledExecutorService, log)); + this.statsTable.put(BROKER_ACK_NUMS, new StatsItemSet(BROKER_ACK_NUMS, this.scheduledExecutorService, log)); + this.statsTable.put(BROKER_CK_NUMS, new StatsItemSet(BROKER_CK_NUMS, this.scheduledExecutorService, log)); + this.statsTable.put(BROKER_GET_NUMS_WITHOUT_SYSTEM_TOPIC, + new StatsItemSet(BROKER_GET_NUMS_WITHOUT_SYSTEM_TOPIC, this.scheduledExecutorService, log)); + this.statsTable.put(BROKER_PUT_NUMS_WITHOUT_SYSTEM_TOPIC, + new StatsItemSet(BROKER_PUT_NUMS_WITHOUT_SYSTEM_TOPIC, this.scheduledExecutorService, log)); + this.statsTable.put(Stats.GROUP_GET_FROM_DISK_NUMS, + new StatsItemSet(Stats.GROUP_GET_FROM_DISK_NUMS, this.scheduledExecutorService, log)); + this.statsTable.put(Stats.GROUP_GET_FROM_DISK_SIZE, + new StatsItemSet(Stats.GROUP_GET_FROM_DISK_SIZE, this.scheduledExecutorService, log)); + this.statsTable.put(Stats.BROKER_GET_FROM_DISK_NUMS, + new StatsItemSet(Stats.BROKER_GET_FROM_DISK_NUMS, this.scheduledExecutorService, log)); + this.statsTable.put(Stats.BROKER_GET_FROM_DISK_SIZE, + new StatsItemSet(Stats.BROKER_GET_FROM_DISK_SIZE, this.scheduledExecutorService, log)); + + this.statsTable.put(SNDBCK2DLQ_TIMES, + new StatsItemSet(SNDBCK2DLQ_TIMES, this.scheduledExecutorService, DLQ_STAT_LOG)); + + this.statsTable.put(Stats.COMMERCIAL_SEND_TIMES, + new StatsItemSet(Stats.COMMERCIAL_SEND_TIMES, this.commercialExecutor, COMMERCIAL_LOG)); + this.statsTable.put(Stats.COMMERCIAL_RCV_TIMES, + new StatsItemSet(Stats.COMMERCIAL_RCV_TIMES, this.commercialExecutor, COMMERCIAL_LOG)); + this.statsTable.put(Stats.COMMERCIAL_SEND_SIZE, + new StatsItemSet(Stats.COMMERCIAL_SEND_SIZE, this.commercialExecutor, COMMERCIAL_LOG)); + this.statsTable.put(Stats.COMMERCIAL_RCV_SIZE, + new StatsItemSet(Stats.COMMERCIAL_RCV_SIZE, this.commercialExecutor, COMMERCIAL_LOG)); + this.statsTable.put(Stats.COMMERCIAL_RCV_EPOLLS, + new StatsItemSet(Stats.COMMERCIAL_RCV_EPOLLS, this.commercialExecutor, COMMERCIAL_LOG)); + this.statsTable.put(Stats.COMMERCIAL_SNDBCK_TIMES, + new StatsItemSet(Stats.COMMERCIAL_SNDBCK_TIMES, this.commercialExecutor, COMMERCIAL_LOG)); + this.statsTable.put(Stats.COMMERCIAL_PERM_FAILURES, + new StatsItemSet(Stats.COMMERCIAL_PERM_FAILURES, this.commercialExecutor, COMMERCIAL_LOG)); + + this.statsTable.put(CONSUMER_REGISTER_TIME, + new StatsItemSet(CONSUMER_REGISTER_TIME, this.scheduledExecutorService, log)); + this.statsTable.put(PRODUCER_REGISTER_TIME, + new StatsItemSet(PRODUCER_REGISTER_TIME, this.scheduledExecutorService, log)); - this.statsTable.put(TOPIC_PUT_NUMS, new StatsItemSet(TOPIC_PUT_NUMS, this.scheduledExecutorService, log)); - this.statsTable.put(TOPIC_PUT_SIZE, new StatsItemSet(TOPIC_PUT_SIZE, this.scheduledExecutorService, log)); - this.statsTable.put(GROUP_GET_NUMS, new StatsItemSet(GROUP_GET_NUMS, this.scheduledExecutorService, log)); - this.statsTable.put(GROUP_GET_SIZE, new StatsItemSet(GROUP_GET_SIZE, this.scheduledExecutorService, log)); - this.statsTable.put(GROUP_GET_LATENCY, new StatsItemSet(GROUP_GET_LATENCY, this.scheduledExecutorService, log)); - this.statsTable.put(SNDBCK_PUT_NUMS, new StatsItemSet(SNDBCK_PUT_NUMS, this.scheduledExecutorService, log)); - this.statsTable.put(BROKER_PUT_NUMS, new StatsItemSet(BROKER_PUT_NUMS, this.scheduledExecutorService, log)); - this.statsTable.put(BROKER_GET_NUMS, new StatsItemSet(BROKER_GET_NUMS, this.scheduledExecutorService, log)); - this.statsTable.put(GROUP_GET_FROM_DISK_NUMS, new StatsItemSet(GROUP_GET_FROM_DISK_NUMS, this.scheduledExecutorService, log)); - this.statsTable.put(GROUP_GET_FROM_DISK_SIZE, new StatsItemSet(GROUP_GET_FROM_DISK_SIZE, this.scheduledExecutorService, log)); - this.statsTable.put(BROKER_GET_FROM_DISK_NUMS, new StatsItemSet(BROKER_GET_FROM_DISK_NUMS, this.scheduledExecutorService, log)); - this.statsTable.put(BROKER_GET_FROM_DISK_SIZE, new StatsItemSet(BROKER_GET_FROM_DISK_SIZE, this.scheduledExecutorService, log)); - - this.statsTable.put(COMMERCIAL_SEND_TIMES, new StatsItemSet(COMMERCIAL_SEND_TIMES, this.commercialExecutor, COMMERCIAL_LOG)); - this.statsTable.put(COMMERCIAL_RCV_TIMES, new StatsItemSet(COMMERCIAL_RCV_TIMES, this.commercialExecutor, COMMERCIAL_LOG)); - this.statsTable.put(COMMERCIAL_SEND_SIZE, new StatsItemSet(COMMERCIAL_SEND_SIZE, this.commercialExecutor, COMMERCIAL_LOG)); - this.statsTable.put(COMMERCIAL_RCV_SIZE, new StatsItemSet(COMMERCIAL_RCV_SIZE, this.commercialExecutor, COMMERCIAL_LOG)); - this.statsTable.put(COMMERCIAL_RCV_EPOLLS, new StatsItemSet(COMMERCIAL_RCV_EPOLLS, this.commercialExecutor, COMMERCIAL_LOG)); - this.statsTable.put(COMMERCIAL_SNDBCK_TIMES, new StatsItemSet(COMMERCIAL_SNDBCK_TIMES, this.commercialExecutor, COMMERCIAL_LOG)); - this.statsTable.put(COMMERCIAL_PERM_FAILURES, new StatsItemSet(COMMERCIAL_PERM_FAILURES, this.commercialExecutor, COMMERCIAL_LOG)); + this.statsTable.put(CHANNEL_ACTIVITY, new StatsItemSet(CHANNEL_ACTIVITY, this.scheduledExecutorService, log)); + + StatisticsItemFormatter formatter = new StatisticsItemFormatter(); + accountStatManager.setBriefMeta(new Pair[] { + Pair.of(RT, new long[][] {{50, 50}, {100, 10}, {1000, 10}}), + Pair.of(INNER_RT, new long[][] {{10, 10}, {100, 10}, {1000, 10}})}); + String[] itemNames = new String[] { + MSG_NUM, SUCCESS_MSG_NUM, FAILURE_MSG_NUM, COMMERCIAL_MSG_NUM, + SUCCESS_REQ_NUM, FAILURE_REQ_NUM, + MSG_SIZE, SUCCESS_MSG_SIZE, FAILURE_MSG_SIZE, + RT, INNER_RT}; + this.accountStatManager.addStatisticsKindMeta(createStatisticsKindMeta( + ACCOUNT_SEND, itemNames, this.accountExecutor, formatter, ACCOUNT_LOG, ACCOUNT_STAT_INVERTAL)); + this.accountStatManager.addStatisticsKindMeta(createStatisticsKindMeta( + ACCOUNT_RCV, itemNames, this.accountExecutor, formatter, ACCOUNT_LOG, ACCOUNT_STAT_INVERTAL)); + this.accountStatManager.addStatisticsKindMeta(createStatisticsKindMeta( + ACCOUNT_SEND_BACK, itemNames, this.accountExecutor, formatter, ACCOUNT_LOG, ACCOUNT_STAT_INVERTAL)); + this.accountStatManager.addStatisticsKindMeta(createStatisticsKindMeta( + ACCOUNT_SEND_BACK_TO_DLQ, itemNames, this.accountExecutor, formatter, ACCOUNT_LOG, ACCOUNT_STAT_INVERTAL)); + this.accountStatManager.addStatisticsKindMeta(createStatisticsKindMeta( + ACCOUNT_SEND_REJ, itemNames, this.accountExecutor, formatter, ACCOUNT_LOG, ACCOUNT_STAT_INVERTAL)); + this.accountStatManager.addStatisticsKindMeta(createStatisticsKindMeta( + ACCOUNT_REV_REJ, itemNames, this.accountExecutor, formatter, ACCOUNT_LOG, ACCOUNT_STAT_INVERTAL)); + this.accountStatManager.setStatisticsItemStateGetter(new StatisticsItemStateGetter() { + @Override + public boolean online(StatisticsItem item) { + String[] strArr = null; + try { + strArr = splitAccountStatKey(item.getStatObject()); + } catch (Exception e) { + log.warn("parse account stat key failed, key: {}", item.getStatObject()); + return false; + } + + // TODO ugly + if (strArr == null || strArr.length < 4) { + return false; + } + + String instanceId = strArr[1]; + String topic = strArr[2]; + String group = strArr[3]; + + String kind = item.getStatKind(); + if (ACCOUNT_SEND.equals(kind) || ACCOUNT_SEND_REJ.equals(kind)) { + return produerStateGetter.online(instanceId, group, topic); + } else if (ACCOUNT_RCV.equals(kind) || ACCOUNT_SEND_BACK.equals(kind) || ACCOUNT_SEND_BACK_TO_DLQ.equals(kind) || ACCOUNT_REV_REJ.equals(kind)) { + return consumerStateGetter.online(instanceId, group, topic); + } + return false; + } + }); + } + + private void initScheduleService() { + this.scheduledExecutorService = + ThreadUtils.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("BrokerStatsThread", true, brokerConfig)); + this.commercialExecutor = + ThreadUtils.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("CommercialStatsThread", true, brokerConfig)); + this.accountExecutor = + ThreadUtils.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("AccountStatsThread", true, brokerConfig)); } public MomentStatsItemSet getMomentStatsItemSetFallSize() { @@ -104,11 +296,28 @@ public MomentStatsItemSet getMomentStatsItemSetFallTime() { return momentStatsItemSetFallTime; } + public StateGetter getProduerStateGetter() { + return produerStateGetter; + } + + public void setProduerStateGetter(StateGetter produerStateGetter) { + this.produerStateGetter = produerStateGetter; + } + + public StateGetter getConsumerStateGetter() { + return consumerStateGetter; + } + + public void setConsumerStateGetter(StateGetter consumerStateGetter) { + this.consumerStateGetter = consumerStateGetter; + } + public void start() { } public void shutdown() { this.scheduledExecutorService.shutdown(); + this.commercialExecutor.shutdown(); } public StatsItem getStatsItem(final String statsName, final String statsKey) { @@ -120,83 +329,315 @@ public StatsItem getStatsItem(final String statsName, final String statsKey) { return null; } + public void onTopicDeleted(final String topic) { + this.statsTable.get(Stats.TOPIC_PUT_NUMS).delValue(topic); + this.statsTable.get(Stats.TOPIC_PUT_SIZE).delValue(topic); + if (enableQueueStat) { + this.statsTable.get(Stats.QUEUE_PUT_NUMS).delValueByPrefixKey(topic, "@"); + this.statsTable.get(Stats.QUEUE_PUT_SIZE).delValueByPrefixKey(topic, "@"); + } + this.statsTable.get(Stats.GROUP_GET_NUMS).delValueByPrefixKey(topic, "@"); + this.statsTable.get(Stats.GROUP_GET_SIZE).delValueByPrefixKey(topic, "@"); + this.statsTable.get(Stats.QUEUE_GET_NUMS).delValueByPrefixKey(topic, "@"); + this.statsTable.get(Stats.QUEUE_GET_SIZE).delValueByPrefixKey(topic, "@"); + this.statsTable.get(Stats.SNDBCK_PUT_NUMS).delValueByPrefixKey(topic, "@"); + this.statsTable.get(Stats.GROUP_GET_LATENCY).delValueByInfixKey(topic, "@"); + this.momentStatsItemSetFallSize.delValueByInfixKey(topic, "@"); + this.momentStatsItemSetFallTime.delValueByInfixKey(topic, "@"); + } + + public void onGroupDeleted(final String group) { + this.statsTable.get(Stats.GROUP_GET_NUMS).delValueBySuffixKey(group, "@"); + this.statsTable.get(Stats.GROUP_GET_SIZE).delValueBySuffixKey(group, "@"); + if (enableQueueStat) { + this.statsTable.get(Stats.QUEUE_GET_NUMS).delValueBySuffixKey(group, "@"); + this.statsTable.get(Stats.QUEUE_GET_SIZE).delValueBySuffixKey(group, "@"); + } + this.statsTable.get(Stats.SNDBCK_PUT_NUMS).delValueBySuffixKey(group, "@"); + this.statsTable.get(Stats.GROUP_GET_LATENCY).delValueBySuffixKey(group, "@"); + this.momentStatsItemSetFallSize.delValueBySuffixKey(group, "@"); + this.momentStatsItemSetFallTime.delValueBySuffixKey(group, "@"); + } + + public void incQueuePutNums(final String topic, final Integer queueId) { + if (enableQueueStat) { + this.statsTable.get(Stats.QUEUE_PUT_NUMS).addValue(buildStatsKey(topic, queueId), 1, 1); + } + } + + public void incQueuePutNums(final String topic, final Integer queueId, int num, int times) { + if (enableQueueStat) { + this.statsTable.get(Stats.QUEUE_PUT_NUMS).addValue(buildStatsKey(topic, queueId), num, times); + } + } + + public void incQueuePutSize(final String topic, final Integer queueId, final int size) { + if (enableQueueStat) { + this.statsTable.get(Stats.QUEUE_PUT_SIZE).addValue(buildStatsKey(topic, queueId), size, 1); + } + } + + public void incQueueGetNums(final String group, final String topic, final Integer queueId, final int incValue) { + if (enableQueueStat) { + final String statsKey = buildStatsKey(topic, queueId, group); + this.statsTable.get(Stats.QUEUE_GET_NUMS).addValue(statsKey, incValue, 1); + } + } + + public void incQueueGetSize(final String group, final String topic, final Integer queueId, final int incValue) { + if (enableQueueStat) { + final String statsKey = buildStatsKey(topic, queueId, group); + this.statsTable.get(Stats.QUEUE_GET_SIZE).addValue(statsKey, incValue, 1); + } + } + + public void incConsumerRegisterTime(final int incValue) { + this.statsTable.get(CONSUMER_REGISTER_TIME).addValue(this.clusterName, incValue, 1); + } + + public void incProducerRegisterTime(final int incValue) { + this.statsTable.get(PRODUCER_REGISTER_TIME).addValue(this.clusterName, incValue, 1); + } + + public void incChannelConnectNum() { + this.statsTable.get(CHANNEL_ACTIVITY).addValue(CHANNEL_ACTIVITY_CONNECT, 1, 1); + } + + public void incChannelCloseNum() { + this.statsTable.get(CHANNEL_ACTIVITY).addValue(CHANNEL_ACTIVITY_CLOSE, 1, 1); + } + + public void incChannelExceptionNum() { + this.statsTable.get(CHANNEL_ACTIVITY).addValue(CHANNEL_ACTIVITY_EXCEPTION, 1, 1); + } + + public void incChannelIdleNum() { + this.statsTable.get(CHANNEL_ACTIVITY).addValue(CHANNEL_ACTIVITY_IDLE, 1, 1); + } + public void incTopicPutNums(final String topic) { - this.statsTable.get(TOPIC_PUT_NUMS).addValue(topic, 1, 1); + this.statsTable.get(Stats.TOPIC_PUT_NUMS).addValue(topic, 1, 1); } public void incTopicPutNums(final String topic, int num, int times) { - this.statsTable.get(TOPIC_PUT_NUMS).addValue(topic, num, times); + this.statsTable.get(Stats.TOPIC_PUT_NUMS).addValue(topic, num, times); } public void incTopicPutSize(final String topic, final int size) { - this.statsTable.get(TOPIC_PUT_SIZE).addValue(topic, size, 1); + this.statsTable.get(Stats.TOPIC_PUT_SIZE).addValue(topic, size, 1); } public void incGroupGetNums(final String group, final String topic, final int incValue) { final String statsKey = buildStatsKey(topic, group); - this.statsTable.get(GROUP_GET_NUMS).addValue(statsKey, incValue, 1); + this.statsTable.get(Stats.GROUP_GET_NUMS).addValue(statsKey, incValue, 1); + } + + public void incGroupCkNums(final String group, final String topic, final int incValue) { + final String statsKey = buildStatsKey(topic, group); + this.statsTable.get(GROUP_CK_NUMS).addValue(statsKey, incValue, 1); + } + + public void incGroupAckNums(final String group, final String topic, final int incValue) { + final String statsKey = buildStatsKey(topic, group); + this.statsTable.get(GROUP_ACK_NUMS).addValue(statsKey, incValue, 1); } public String buildStatsKey(String topic, String group) { - StringBuffer strBuilder = new StringBuffer(); - strBuilder.append(topic); - strBuilder.append("@"); - strBuilder.append(group); + StringBuilder strBuilder; + if (topic != null && group != null) { + strBuilder = new StringBuilder(topic.length() + group.length() + 1); + } else { + strBuilder = new StringBuilder(); + } + strBuilder.append(topic).append("@").append(group); + return strBuilder.toString(); + } + + public String buildStatsKey(String topic, int queueId) { + StringBuilder strBuilder; + if (topic != null) { + strBuilder = new StringBuilder(topic.length() + 5); + } else { + strBuilder = new StringBuilder(); + } + strBuilder.append(topic).append("@").append(queueId); + return strBuilder.toString(); + } + + public String buildStatsKey(String topic, int queueId, String group) { + StringBuilder strBuilder; + if (topic != null && group != null) { + strBuilder = new StringBuilder(topic.length() + group.length() + 6); + } else { + strBuilder = new StringBuilder(); + } + strBuilder.append(topic).append("@").append(queueId).append("@").append(group); + return strBuilder.toString(); + } + + public String buildStatsKey(int queueId, String topic, String group) { + StringBuilder strBuilder; + if (topic != null && group != null) { + strBuilder = new StringBuilder(topic.length() + group.length() + 6); + } else { + strBuilder = new StringBuilder(); + } + strBuilder.append(queueId).append("@").append(topic).append("@").append(group); return strBuilder.toString(); } public void incGroupGetSize(final String group, final String topic, final int incValue) { final String statsKey = buildStatsKey(topic, group); - this.statsTable.get(GROUP_GET_SIZE).addValue(statsKey, incValue, 1); + this.statsTable.get(Stats.GROUP_GET_SIZE).addValue(statsKey, incValue, 1); } public void incGroupGetLatency(final String group, final String topic, final int queueId, final int incValue) { - final String statsKey = String.format("%d@%s@%s", queueId, topic, group); - this.statsTable.get(GROUP_GET_LATENCY).addValue(statsKey, incValue, 1); + String statsKey; + if (enableQueueStat) { + statsKey = buildStatsKey(queueId, topic, group); + } else { + statsKey = buildStatsKey(topic, group); + } + this.statsTable.get(Stats.GROUP_GET_LATENCY).addRTValue(statsKey, incValue, 1); + } + + public void incTopicPutLatency(final String topic, final int queueId, final int incValue) { + StringBuilder statsKey; + if (topic != null) { + statsKey = new StringBuilder(topic.length() + 6); + } else { + statsKey = new StringBuilder(6); + } + statsKey.append(queueId).append("@").append(topic); + this.statsTable.get(TOPIC_PUT_LATENCY).addValue(statsKey.toString(), incValue, 1); } public void incBrokerPutNums() { - this.statsTable.get(BROKER_PUT_NUMS).getAndCreateStatsItem(this.clusterName).getValue().incrementAndGet(); + this.statsTable.get(Stats.BROKER_PUT_NUMS).getAndCreateStatsItem(this.clusterName).getValue().add(1); + } + + public void incBrokerPutNums(final String topic, final int incValue) { + this.statsTable.get(Stats.BROKER_PUT_NUMS).getAndCreateStatsItem(this.clusterName).getValue().add(incValue); + incBrokerPutNumsWithoutSystemTopic(topic, incValue); + } + + public void incBrokerGetNums(final String topic, final int incValue) { + this.statsTable.get(Stats.BROKER_GET_NUMS).getAndCreateStatsItem(this.clusterName).getValue().add(incValue); + this.incBrokerGetNumsWithoutSystemTopic(topic, incValue); + } + + public void incBrokerAckNums(final int incValue) { + this.statsTable.get(BROKER_ACK_NUMS).getAndCreateStatsItem(this.clusterName).getValue().add(incValue); + } + + public void incBrokerCkNums(final int incValue) { + this.statsTable.get(BROKER_CK_NUMS).getAndCreateStatsItem(this.clusterName).getValue().add(incValue); + } + + public void incBrokerGetNumsWithoutSystemTopic(final String topic, final int incValue) { + if (TopicValidator.isSystemTopic(topic)) { + return; + } + this.statsTable.get(BROKER_GET_NUMS_WITHOUT_SYSTEM_TOPIC).getAndCreateStatsItem(this.clusterName).getValue().add(incValue); + } + + public void incBrokerPutNumsWithoutSystemTopic(final String topic, final int incValue) { + if (TopicValidator.isSystemTopic(topic)) { + return; + } + this.statsTable.get(BROKER_PUT_NUMS_WITHOUT_SYSTEM_TOPIC).getAndCreateStatsItem(this.clusterName).getValue().add(incValue); } - public void incBrokerPutNums(final int incValue) { - this.statsTable.get(BROKER_PUT_NUMS).getAndCreateStatsItem(this.clusterName).getValue().addAndGet(incValue); + public long getBrokerGetNumsWithoutSystemTopic() { + final StatsItemSet statsItemSet = this.statsTable.get(BROKER_GET_NUMS_WITHOUT_SYSTEM_TOPIC); + if (statsItemSet == null) { + return 0; + } + final StatsItem statsItem = statsItemSet.getStatsItem(this.clusterName); + if (statsItem == null) { + return 0; + } + return statsItem.getValue().longValue(); } - public void incBrokerGetNums(final int incValue) { - this.statsTable.get(BROKER_GET_NUMS).getAndCreateStatsItem(this.clusterName).getValue().addAndGet(incValue); + public long getBrokerPutNumsWithoutSystemTopic() { + final StatsItemSet statsItemSet = this.statsTable.get(BROKER_PUT_NUMS_WITHOUT_SYSTEM_TOPIC); + if (statsItemSet == null) { + return 0; + } + final StatsItem statsItem = statsItemSet.getStatsItem(this.clusterName); + if (statsItem == null) { + return 0; + } + return statsItem.getValue().longValue(); } public void incSendBackNums(final String group, final String topic) { final String statsKey = buildStatsKey(topic, group); - this.statsTable.get(SNDBCK_PUT_NUMS).addValue(statsKey, 1, 1); + this.statsTable.get(Stats.SNDBCK_PUT_NUMS).addValue(statsKey, 1, 1); } public double tpsGroupGetNums(final String group, final String topic) { final String statsKey = buildStatsKey(topic, group); - return this.statsTable.get(GROUP_GET_NUMS).getStatsDataInMinute(statsKey).getTps(); + return this.statsTable.get(Stats.GROUP_GET_NUMS).getStatsDataInMinute(statsKey).getTps(); } public void recordDiskFallBehindTime(final String group, final String topic, final int queueId, final long fallBehind) { - final String statsKey = String.format("%d@%s@%s", queueId, topic, group); + final String statsKey = buildStatsKey(queueId, topic, group); this.momentStatsItemSetFallTime.getAndCreateStatsItem(statsKey).getValue().set(fallBehind); } public void recordDiskFallBehindSize(final String group, final String topic, final int queueId, final long fallBehind) { - final String statsKey = String.format("%d@%s@%s", queueId, topic, group); + final String statsKey = buildStatsKey(queueId, topic, group); this.momentStatsItemSetFallSize.getAndCreateStatsItem(statsKey).getValue().set(fallBehind); } + public void incDLQStatValue(final String key, final String owner, final String group, + final String topic, final String type, final int incValue) { + final String statsKey = buildCommercialStatsKey(owner, topic, group, type); + this.statsTable.get(key).addValue(statsKey, incValue, 1); + } + public void incCommercialValue(final String key, final String owner, final String group, final String topic, final String type, final int incValue) { final String statsKey = buildCommercialStatsKey(owner, topic, group, type); this.statsTable.get(key).addValue(statsKey, incValue, 1); } + public void incAccountValue(final String key, final String accountOwnerParent, final String accountOwnerSelf, + final String instanceId, final String group, final String topic, + final String msgType, final int incValue) { + final String statsKey = buildAccountStatsKey(accountOwnerParent, accountOwnerSelf, instanceId, topic, group, + msgType); + this.statsTable.get(key).addValue(statsKey, incValue, 1); + } + + public void incAccountValue(final String key, final String accountOwnerParent, final String accountOwnerSelf, + final String instanceId, final String group, final String topic, + final String msgType, final String flowlimitThreshold, final int incValue) { + final String statsKey = buildAccountStatsKey(accountOwnerParent, accountOwnerSelf, instanceId, topic, group, + msgType, flowlimitThreshold); + this.statsTable.get(key).addValue(statsKey, incValue, 1); + } + + public void incAccountValue(final String statType, final String owner, final String instanceId, final String topic, + final String group, final String msgType, + final long... incValues) { + final String key = buildAccountStatKey(owner, instanceId, topic, group, msgType); + this.accountStatManager.inc(statType, key, incValues); + } + + public void incAccountValue(final String statType, final String owner, final String instanceId, final String topic, + final String group, final String msgType, final String flowlimitThreshold, + final long... incValues) { + final String key = buildAccountStatKey(owner, instanceId, topic, group, msgType, flowlimitThreshold); + this.accountStatManager.inc(statType, key, incValues); + } + public String buildCommercialStatsKey(String owner, String topic, String group, String type) { - StringBuffer strBuilder = new StringBuffer(); + StringBuilder strBuilder = new StringBuilder(); strBuilder.append(owner); strBuilder.append("@"); strBuilder.append(topic); @@ -207,14 +648,131 @@ public String buildCommercialStatsKey(String owner, String topic, String group, return strBuilder.toString(); } + public String buildAccountStatsKey(String accountOwnerParent, String accountOwnerSelf, String instanceId, + String topic, String group, String msgType) { + StringBuffer strBuilder = new StringBuffer(); + strBuilder.append(accountOwnerParent); + strBuilder.append("@"); + strBuilder.append(accountOwnerSelf); + strBuilder.append("@"); + strBuilder.append(instanceId); + strBuilder.append("@"); + strBuilder.append(topic); + strBuilder.append("@"); + strBuilder.append(group); + strBuilder.append("@"); + strBuilder.append(msgType); + return strBuilder.toString(); + } + + public String buildAccountStatsKey(String accountOwnerParent, String accountOwnerSelf, String instanceId, + String topic, String group, String msgType, String flowlimitThreshold) { + StringBuffer strBuilder = new StringBuffer(); + strBuilder.append(accountOwnerParent); + strBuilder.append("@"); + strBuilder.append(accountOwnerSelf); + strBuilder.append("@"); + strBuilder.append(instanceId); + strBuilder.append("@"); + strBuilder.append(topic); + strBuilder.append("@"); + strBuilder.append(group); + strBuilder.append("@"); + strBuilder.append(msgType); + strBuilder.append("@"); + strBuilder.append(flowlimitThreshold); + return strBuilder.toString(); + } + + public String buildAccountStatKey(final String owner, final String instanceId, + final String topic, final String group, + final String msgType) { + final String sep = "|"; + StringBuffer strBuilder = new StringBuffer(); + strBuilder.append(owner).append(sep); + strBuilder.append(instanceId).append(sep); + strBuilder.append(topic).append(sep); + strBuilder.append(group).append(sep); + strBuilder.append(msgType); + return strBuilder.toString(); + } + + public String buildAccountStatKey(final String owner, final String instanceId, + final String topic, final String group, + final String msgType, String flowlimitThreshold) { + final String sep = "|"; + StringBuffer strBuilder = new StringBuffer(); + strBuilder.append(owner).append(sep); + strBuilder.append(instanceId).append(sep); + strBuilder.append(topic).append(sep); + strBuilder.append(group).append(sep); + strBuilder.append(msgType).append(sep); + strBuilder.append(flowlimitThreshold); + return strBuilder.toString(); + } + + public String[] splitAccountStatKey(final String accountStatKey) { + final String sep = "\\|"; + return accountStatKey.split(sep); + } + + private StatisticsKindMeta createStatisticsKindMeta(String name, + String[] itemNames, + ScheduledExecutorService executorService, + StatisticsItemFormatter formatter, + Logger log, + long interval) { + final BrokerConfig brokerConfig = this.brokerConfig; + StatisticsItemPrinter printer = new StatisticsItemPrinter(formatter, log); + StatisticsKindMeta kindMeta = new StatisticsKindMeta(); + kindMeta.setName(name); + kindMeta.setItemNames(itemNames); + kindMeta.setScheduledPrinter( + new StatisticsItemScheduledIncrementPrinter( + "Stat In One Minute: ", + printer, + executorService, + new StatisticsItemScheduledPrinter.InitialDelay() { + @Override + public long get() { + return Math.abs(UtilAll.computeNextMinutesTimeMillis() - System.currentTimeMillis()); + } + }, + interval, + new String[] {MSG_NUM}, + new StatisticsItemScheduledIncrementPrinter.Valve() { + @Override + public boolean enabled() { + return brokerConfig != null ? brokerConfig.isAccountStatsEnable() : true; + } + + @Override + public boolean printZeroLine() { + return brokerConfig != null ? brokerConfig.isAccountStatsPrintZeroValues() : true; + } + } + ) + ); + return kindMeta; + } + + public interface StateGetter { + boolean online(String instanceId, String group, String topic); + } + public enum StatsType { SEND_SUCCESS, SEND_FAILURE, + + RCV_SUCCESS, + RCV_EPOLLS, SEND_BACK, + SEND_BACK_TO_DLQ, + + SEND_ORDER, SEND_TIMER, SEND_TRANSACTION, - RCV_SUCCESS, - RCV_EPOLLS, + PERM_FAILURE } } diff --git a/store/src/main/java/org/apache/rocketmq/store/stats/LmqBrokerStatsManager.java b/store/src/main/java/org/apache/rocketmq/store/stats/LmqBrokerStatsManager.java new file mode 100644 index 00000000000..f0e23fe6388 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/stats/LmqBrokerStatsManager.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.stats; + +import org.apache.rocketmq.common.MixAll; + +public class LmqBrokerStatsManager extends BrokerStatsManager { + + public LmqBrokerStatsManager(String clusterName, boolean enableQueueStat) { + super(clusterName, enableQueueStat); + } + + @Override + public void incGroupGetNums(final String group, final String topic, final int incValue) { + String lmqGroup = group; + String lmqTopic = topic; + if (MixAll.isLmq(group)) { + lmqGroup = MixAll.LMQ_PREFIX; + } + if (MixAll.isLmq(topic)) { + lmqTopic = MixAll.LMQ_PREFIX; + } + super.incGroupGetNums(lmqGroup, lmqTopic, incValue); + } + + @Override + public void incGroupGetSize(final String group, final String topic, final int incValue) { + String lmqGroup = group; + String lmqTopic = topic; + if (MixAll.isLmq(group)) { + lmqGroup = MixAll.LMQ_PREFIX; + } + if (MixAll.isLmq(topic)) { + lmqTopic = MixAll.LMQ_PREFIX; + } + super.incGroupGetSize(lmqGroup, lmqTopic, incValue); + } + + @Override + public void incGroupGetLatency(final String group, final String topic, final int queueId, final int incValue) { + String lmqGroup = group; + String lmqTopic = topic; + if (MixAll.isLmq(group)) { + lmqGroup = MixAll.LMQ_PREFIX; + } + if (MixAll.isLmq(topic)) { + lmqTopic = MixAll.LMQ_PREFIX; + } + super.incGroupGetLatency(lmqGroup, lmqTopic, queueId, incValue); + } + + @Override + public void incSendBackNums(final String group, final String topic) { + String lmqGroup = group; + String lmqTopic = topic; + if (MixAll.isLmq(group)) { + lmqGroup = MixAll.LMQ_PREFIX; + } + if (MixAll.isLmq(topic)) { + lmqTopic = MixAll.LMQ_PREFIX; + } + super.incSendBackNums(lmqGroup, lmqTopic); + } + + @Override + public double tpsGroupGetNums(final String group, final String topic) { + String lmqGroup = group; + String lmqTopic = topic; + if (MixAll.isLmq(group)) { + lmqGroup = MixAll.LMQ_PREFIX; + } + if (MixAll.isLmq(topic)) { + lmqTopic = MixAll.LMQ_PREFIX; + } + return super.tpsGroupGetNums(lmqGroup, lmqTopic); + } + + @Override + public void recordDiskFallBehindTime(final String group, final String topic, final int queueId, + final long fallBehind) { + String lmqGroup = group; + String lmqTopic = topic; + if (MixAll.isLmq(group)) { + lmqGroup = MixAll.LMQ_PREFIX; + } + if (MixAll.isLmq(topic)) { + lmqTopic = MixAll.LMQ_PREFIX; + } + super.recordDiskFallBehindTime(lmqGroup, lmqTopic, queueId, fallBehind); + } + + @Override + public void recordDiskFallBehindSize(final String group, final String topic, final int queueId, + final long fallBehind) { + String lmqGroup = group; + String lmqTopic = topic; + if (MixAll.isLmq(group)) { + lmqGroup = MixAll.LMQ_PREFIX; + } + if (MixAll.isLmq(topic)) { + lmqTopic = MixAll.LMQ_PREFIX; + } + super.recordDiskFallBehindSize(lmqGroup, lmqTopic, queueId, fallBehind); + } + +} diff --git a/store/src/main/java/org/apache/rocketmq/store/timer/Slot.java b/store/src/main/java/org/apache/rocketmq/store/timer/Slot.java new file mode 100644 index 00000000000..2da846ceed1 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/timer/Slot.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.timer; + +/** + * Represents a slot of timing wheel. Format: + * ┌────────────┬───────────┬───────────┬───────────┬───────────┐ + * │delayed time│ first pos │ last pos │ num │ magic │ + * ├────────────┼───────────┼───────────┼───────────┼───────────┤ + * │ 8bytes │ 8bytes │ 8bytes │ 4bytes │ 4bytes │ + * └────────────┴───────────┴───────────┴───────────┴───────────┘ + */ +public class Slot { + public static final short SIZE = 32; + public final long timeMs; //delayed time + public final long firstPos; + public final long lastPos; + public final int num; + public final int magic; //no use now, just keep it + + public Slot(long timeMs, long firstPos, long lastPos) { + this.timeMs = timeMs; + this.firstPos = firstPos; + this.lastPos = lastPos; + this.num = 0; + this.magic = 0; + } + + public Slot(long timeMs, long firstPos, long lastPos, int num, int magic) { + this.timeMs = timeMs; + this.firstPos = firstPos; + this.lastPos = lastPos; + this.num = num; + this.magic = magic; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/timer/TimerCheckpoint.java b/store/src/main/java/org/apache/rocketmq/store/timer/TimerCheckpoint.java new file mode 100644 index 00000000000..2b17fa24886 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/timer/TimerCheckpoint.java @@ -0,0 +1,186 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.timer; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileChannel.MapMode; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.store.logfile.DefaultMappedFile; + +public class TimerCheckpoint { + private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + private final RandomAccessFile randomAccessFile; + private final FileChannel fileChannel; + private final MappedByteBuffer mappedByteBuffer; + private volatile long lastReadTimeMs = 0; //if it is slave, need to read from master + private volatile long lastTimerLogFlushPos = 0; + private volatile long lastTimerQueueOffset = 0; + private volatile long masterTimerQueueOffset = 0; // read from master + private final DataVersion dataVersion = new DataVersion(); + + public TimerCheckpoint() { + this.randomAccessFile = null; + this.fileChannel = null; + this.mappedByteBuffer = null; + } + + public TimerCheckpoint(final String scpPath) throws IOException { + File file = new File(scpPath); + UtilAll.ensureDirOK(file.getParent()); + boolean fileExists = file.exists(); + + this.randomAccessFile = new RandomAccessFile(file, "rw"); + this.fileChannel = this.randomAccessFile.getChannel(); + this.mappedByteBuffer = fileChannel.map(MapMode.READ_WRITE, 0, DefaultMappedFile.OS_PAGE_SIZE); + + if (fileExists) { + log.info("timer checkpoint file exists, " + scpPath); + this.lastReadTimeMs = this.mappedByteBuffer.getLong(0); + this.lastTimerLogFlushPos = this.mappedByteBuffer.getLong(8); + this.lastTimerQueueOffset = this.mappedByteBuffer.getLong(16); + this.masterTimerQueueOffset = this.mappedByteBuffer.getLong(24); + // new add to record dataVersion + if (this.mappedByteBuffer.hasRemaining()) { + dataVersion.setStateVersion(this.mappedByteBuffer.getLong(32)); + dataVersion.setTimestamp(this.mappedByteBuffer.getLong(40)); + dataVersion.setCounter(new AtomicLong(this.mappedByteBuffer.getLong(48))); + } + + log.info("timer checkpoint file lastReadTimeMs " + this.lastReadTimeMs + ", " + + UtilAll.timeMillisToHumanString(this.lastReadTimeMs)); + log.info("timer checkpoint file lastTimerLogFlushPos " + this.lastTimerLogFlushPos); + log.info("timer checkpoint file lastTimerQueueOffset " + this.lastTimerQueueOffset); + log.info("timer checkpoint file masterTimerQueueOffset " + this.masterTimerQueueOffset); + log.info("timer checkpoint file data version state version " + this.dataVersion.getStateVersion()); + log.info("timer checkpoint file data version timestamp " + this.dataVersion.getTimestamp()); + log.info("timer checkpoint file data version counter " + this.dataVersion.getCounter()); + } else { + log.info("timer checkpoint file not exists, " + scpPath); + } + } + + public void shutdown() { + if (null == this.mappedByteBuffer) { + return; + } + + this.flush(); + + // unmap mappedByteBuffer + UtilAll.cleanBuffer(this.mappedByteBuffer); + + try { + this.fileChannel.close(); + } catch (IOException e) { + log.error("Shutdown error in timer check point", e); + } + } + + public void flush() { + if (null == this.mappedByteBuffer) { + return; + } + this.mappedByteBuffer.putLong(0, this.lastReadTimeMs); + this.mappedByteBuffer.putLong(8, this.lastTimerLogFlushPos); + this.mappedByteBuffer.putLong(16, this.lastTimerQueueOffset); + this.mappedByteBuffer.putLong(24, this.masterTimerQueueOffset); + // new add to record dataVersion + this.mappedByteBuffer.putLong(32, this.dataVersion.getStateVersion()); + this.mappedByteBuffer.putLong(40, this.dataVersion.getTimestamp()); + this.mappedByteBuffer.putLong(48, this.dataVersion.getCounter().get()); + this.mappedByteBuffer.force(); + } + + public long getLastReadTimeMs() { + return lastReadTimeMs; + } + + public static ByteBuffer encode(TimerCheckpoint another) { + ByteBuffer byteBuffer = ByteBuffer.allocate(56); + byteBuffer.putLong(another.getLastReadTimeMs()); + byteBuffer.putLong(another.getLastTimerLogFlushPos()); + byteBuffer.putLong(another.getLastTimerQueueOffset()); + byteBuffer.putLong(another.getMasterTimerQueueOffset()); + // new add to record dataVersion + byteBuffer.putLong(another.getDataVersion().getStateVersion()); + byteBuffer.putLong(another.getDataVersion().getTimestamp()); + byteBuffer.putLong(another.getDataVersion().getCounter().get()); + byteBuffer.flip(); + return byteBuffer; + } + + public static TimerCheckpoint decode(ByteBuffer byteBuffer) { + TimerCheckpoint tmp = new TimerCheckpoint(); + tmp.setLastReadTimeMs(byteBuffer.getLong()); + tmp.setLastTimerLogFlushPos(byteBuffer.getLong()); + tmp.setLastTimerQueueOffset(byteBuffer.getLong()); + tmp.setMasterTimerQueueOffset(byteBuffer.getLong()); + // new add to record dataVersion + if (byteBuffer.hasRemaining()) { + tmp.getDataVersion().setStateVersion(byteBuffer.getLong()); + tmp.getDataVersion().setTimestamp(byteBuffer.getLong()); + tmp.getDataVersion().setCounter(new AtomicLong(byteBuffer.getLong())); + } + return tmp; + } + + public void setLastReadTimeMs(long lastReadTimeMs) { + this.lastReadTimeMs = lastReadTimeMs; + } + + public long getLastTimerLogFlushPos() { + return lastTimerLogFlushPos; + } + + public void setLastTimerLogFlushPos(long lastTimerLogFlushPos) { + this.lastTimerLogFlushPos = lastTimerLogFlushPos; + } + + public long getLastTimerQueueOffset() { + return lastTimerQueueOffset; + } + + public void setLastTimerQueueOffset(long lastTimerQueueOffset) { + this.lastTimerQueueOffset = lastTimerQueueOffset; + } + + public long getMasterTimerQueueOffset() { + return masterTimerQueueOffset; + } + + public void setMasterTimerQueueOffset(final long masterTimerQueueOffset) { + this.masterTimerQueueOffset = masterTimerQueueOffset; + } + + public void updateDateVersion(long stateVersion) { + dataVersion.nextVersion(stateVersion); + } + + public DataVersion getDataVersion() { + return dataVersion; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/timer/TimerLog.java b/store/src/main/java/org/apache/rocketmq/store/timer/TimerLog.java new file mode 100644 index 00000000000..e0836fef183 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/timer/TimerLog.java @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.timer; + +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.logfile.MappedFile; +import org.apache.rocketmq.store.MappedFileQueue; +import org.apache.rocketmq.store.SelectMappedBufferResult; + +import java.nio.ByteBuffer; + +public class TimerLog { + private static Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + public final static int BLANK_MAGIC_CODE = 0xBBCCDDEE ^ 1880681586 + 8; + private final static int MIN_BLANK_LEN = 4 + 8 + 4; + public final static int UNIT_SIZE = 4 //size + + 8 //prev pos + + 4 //magic value + + 8 //curr write time, for trace + + 4 //delayed time, for check + + 8 //offsetPy + + 4 //sizePy + + 4 //hash code of real topic + + 8; //reserved value, just in case of + public final static int UNIT_PRE_SIZE_FOR_MSG = 28; + public final static int UNIT_PRE_SIZE_FOR_METRIC = 40; + private final MappedFileQueue mappedFileQueue; + + private final int fileSize; + + public TimerLog(final String storePath, final int fileSize) { + this.fileSize = fileSize; + this.mappedFileQueue = new MappedFileQueue(storePath, fileSize, null); + } + + public boolean load() { + return this.mappedFileQueue.load(); + } + + public long append(byte[] data) { + return append(data, 0, data.length); + } + + public long append(byte[] data, int pos, int len) { + MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(); + if (null == mappedFile || mappedFile.isFull()) { + mappedFile = this.mappedFileQueue.getLastMappedFile(0); + } + if (null == mappedFile) { + log.error("Create mapped file1 error for timer log"); + return -1; + } + if (len + MIN_BLANK_LEN > mappedFile.getFileSize() - mappedFile.getWrotePosition()) { + ByteBuffer byteBuffer = ByteBuffer.allocate(MIN_BLANK_LEN); + byteBuffer.putInt(mappedFile.getFileSize() - mappedFile.getWrotePosition()); + byteBuffer.putLong(0); + byteBuffer.putInt(BLANK_MAGIC_CODE); + if (mappedFile.appendMessage(byteBuffer.array())) { + //need to set the wrote position + mappedFile.setWrotePosition(mappedFile.getFileSize()); + } else { + log.error("Append blank error for timer log"); + return -1; + } + mappedFile = this.mappedFileQueue.getLastMappedFile(0); + if (null == mappedFile) { + log.error("create mapped file2 error for timer log"); + return -1; + } + } + long currPosition = mappedFile.getFileFromOffset() + mappedFile.getWrotePosition(); + if (!mappedFile.appendMessage(data, pos, len)) { + log.error("Append error for timer log"); + return -1; + } + return currPosition; + } + + public SelectMappedBufferResult getTimerMessage(long offsetPy) { + MappedFile mappedFile = mappedFileQueue.findMappedFileByOffset(offsetPy); + if (null == mappedFile) + return null; + return mappedFile.selectMappedBuffer((int) (offsetPy % mappedFile.getFileSize())); + } + + public SelectMappedBufferResult getWholeBuffer(long offsetPy) { + MappedFile mappedFile = mappedFileQueue.findMappedFileByOffset(offsetPy); + if (null == mappedFile) + return null; + return mappedFile.selectMappedBuffer(0); + } + + public MappedFileQueue getMappedFileQueue() { + return mappedFileQueue; + } + + public void shutdown() { + this.mappedFileQueue.flush(0); + //it seems do not need to call shutdown + } + + // be careful. + // if the format of timerlog changed, this offset has to be changed too + // so dose the batch writing + public int getOffsetForLastUnit() { + + return fileSize - (fileSize - MIN_BLANK_LEN) % UNIT_SIZE - MIN_BLANK_LEN - UNIT_SIZE; + } + +} diff --git a/store/src/main/java/org/apache/rocketmq/store/timer/TimerMessageStore.java b/store/src/main/java/org/apache/rocketmq/store/timer/TimerMessageStore.java new file mode 100644 index 00000000000..819b3e96a43 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/timer/TimerMessageStore.java @@ -0,0 +1,1876 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.timer; + +import com.conversantmedia.util.concurrent.DisruptorBlockingQueue; +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import io.opentelemetry.api.common.Attributes; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.math.NumberUtils; +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.common.ThreadFactoryImpl; +import org.apache.rocketmq.common.TopicFilterType; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.common.utils.ThreadUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.config.BrokerRole; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.logfile.MappedFile; +import org.apache.rocketmq.store.metrics.DefaultStoreMetricsConstant; +import org.apache.rocketmq.store.metrics.DefaultStoreMetricsManager; +import org.apache.rocketmq.store.queue.ConsumeQueueInterface; +import org.apache.rocketmq.store.queue.CqUnit; +import org.apache.rocketmq.store.queue.ReferredIterator; +import org.apache.rocketmq.store.stats.BrokerStatsManager; +import org.apache.rocketmq.store.util.PerfCounter; + +public class TimerMessageStore { + + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + + public static final int INITIAL = 0, RUNNING = 1, HAULT = 2, SHUTDOWN = 3; + private volatile int state = INITIAL; + + public static final String TIMER_TOPIC = TopicValidator.SYSTEM_TOPIC_PREFIX + "wheel_timer"; + public static final String TIMER_OUT_MS = MessageConst.PROPERTY_TIMER_OUT_MS; + public static final String TIMER_ENQUEUE_MS = MessageConst.PROPERTY_TIMER_ENQUEUE_MS; + public static final String TIMER_DEQUEUE_MS = MessageConst.PROPERTY_TIMER_DEQUEUE_MS; + public static final String TIMER_ROLL_TIMES = MessageConst.PROPERTY_TIMER_ROLL_TIMES; + public static final String TIMER_DELETE_UNIQUE_KEY = MessageConst.PROPERTY_TIMER_DEL_UNIQKEY; + + public static final Random RANDOM = new Random(); + public static final int PUT_OK = 0, PUT_NEED_RETRY = 1, PUT_NO_RETRY = 2; + public static final int DAY_SECS = 24 * 3600; + public static final int DEFAULT_CAPACITY = 1024; + + // The total days in the timer wheel when precision is 1000ms. + // If the broker shutdown last more than the configured days, will cause message loss + public static final int TIMER_WHEEL_TTL_DAY = 7; + public static final int TIMER_BLANK_SLOTS = 60; + public static final int MAGIC_DEFAULT = 1; + public static final int MAGIC_ROLL = 1 << 1; + public static final int MAGIC_DELETE = 1 << 2; + public boolean debug = false; + + protected static final String ENQUEUE_PUT = "enqueue_put"; + protected static final String DEQUEUE_PUT = "dequeue_put"; + protected final PerfCounter.Ticks perfCounterTicks = new PerfCounter.Ticks(LOGGER); + + protected final BlockingQueue enqueuePutQueue; + protected final BlockingQueue> dequeueGetQueue; + protected final BlockingQueue dequeuePutQueue; + + private final ByteBuffer timerLogBuffer = ByteBuffer.allocate(4 * 1024); + private final ThreadLocal bufferLocal; + private final ScheduledExecutorService scheduler; + + private final MessageStore messageStore; + private final TimerWheel timerWheel; + private final TimerLog timerLog; + private final TimerCheckpoint timerCheckpoint; + + private TimerEnqueueGetService enqueueGetService; + private TimerEnqueuePutService enqueuePutService; + private TimerDequeueWarmService dequeueWarmService; + private TimerDequeueGetService dequeueGetService; + private TimerDequeuePutMessageService[] dequeuePutMessageServices; + private TimerDequeueGetMessageService[] dequeueGetMessageServices; + private TimerFlushService timerFlushService; + + protected volatile long currReadTimeMs; + protected volatile long currWriteTimeMs; + protected volatile long preReadTimeMs; + protected volatile long commitReadTimeMs; + protected volatile long currQueueOffset; //only one queue that is 0 + protected volatile long commitQueueOffset; + protected volatile long lastCommitReadTimeMs; + protected volatile long lastCommitQueueOffset; + + private long lastEnqueueButExpiredTime; + private long lastEnqueueButExpiredStoreTime; + + private final int commitLogFileSize; + private final int timerLogFileSize; + private final int timerRollWindowSlots; + private final int slotsTotal; + + protected final int precisionMs; + protected final MessageStoreConfig storeConfig; + protected TimerMetrics timerMetrics; + protected long lastTimeOfCheckMetrics = System.currentTimeMillis(); + protected AtomicInteger frequency = new AtomicInteger(0); + + private volatile BrokerRole lastBrokerRole = BrokerRole.SLAVE; + //the dequeue is an asynchronous process, use this flag to track if the status has changed + private boolean dequeueStatusChangeFlag = false; + private long shouldStartTime; + + // True if current store is master or current brokerId is equal to the minimum brokerId of the replica group in slaveActingMaster mode. + protected volatile boolean shouldRunningDequeue; + private final BrokerStatsManager brokerStatsManager; + private Function escapeBridgeHook; + + public TimerMessageStore(final MessageStore messageStore, final MessageStoreConfig storeConfig, + TimerCheckpoint timerCheckpoint, TimerMetrics timerMetrics, + final BrokerStatsManager brokerStatsManager) throws IOException { + + this.messageStore = messageStore; + this.storeConfig = storeConfig; + this.commitLogFileSize = storeConfig.getMappedFileSizeCommitLog(); + this.timerLogFileSize = storeConfig.getMappedFileSizeTimerLog(); + this.precisionMs = storeConfig.getTimerPrecisionMs(); + + // TimerWheel contains the fixed number of slots regardless of precision. + this.slotsTotal = TIMER_WHEEL_TTL_DAY * DAY_SECS; + this.timerWheel = new TimerWheel( + getTimerWheelPath(storeConfig.getStorePathRootDir()), this.slotsTotal, precisionMs); + this.timerLog = new TimerLog(getTimerLogPath(storeConfig.getStorePathRootDir()), timerLogFileSize); + this.timerMetrics = timerMetrics; + this.timerCheckpoint = timerCheckpoint; + this.lastBrokerRole = storeConfig.getBrokerRole(); + + if (messageStore instanceof DefaultMessageStore) { + scheduler = ThreadUtils.newSingleThreadScheduledExecutor( + new ThreadFactoryImpl("TimerScheduledThread", + ((DefaultMessageStore) messageStore).getBrokerIdentity())); + } else { + scheduler = ThreadUtils.newSingleThreadScheduledExecutor( + new ThreadFactoryImpl("TimerScheduledThread")); + } + + // timerRollWindow contains the fixed number of slots regardless of precision. + if (storeConfig.getTimerRollWindowSlot() > slotsTotal - TIMER_BLANK_SLOTS + || storeConfig.getTimerRollWindowSlot() < 2) { + this.timerRollWindowSlots = slotsTotal - TIMER_BLANK_SLOTS; + } else { + this.timerRollWindowSlots = storeConfig.getTimerRollWindowSlot(); + } + + bufferLocal = new ThreadLocal() { + @Override + protected ByteBuffer initialValue() { + return ByteBuffer.allocateDirect(storeConfig.getMaxMessageSize() + 100); + } + }; + + if (storeConfig.isTimerEnableDisruptor()) { + enqueuePutQueue = new DisruptorBlockingQueue<>(DEFAULT_CAPACITY); + dequeueGetQueue = new DisruptorBlockingQueue<>(DEFAULT_CAPACITY); + dequeuePutQueue = new DisruptorBlockingQueue<>(DEFAULT_CAPACITY); + } else { + enqueuePutQueue = new LinkedBlockingDeque<>(DEFAULT_CAPACITY); + dequeueGetQueue = new LinkedBlockingDeque<>(DEFAULT_CAPACITY); + dequeuePutQueue = new LinkedBlockingDeque<>(DEFAULT_CAPACITY); + } + this.brokerStatsManager = brokerStatsManager; + } + + public void initService() { + enqueueGetService = new TimerEnqueueGetService(); + enqueuePutService = new TimerEnqueuePutService(); + dequeueWarmService = new TimerDequeueWarmService(); + dequeueGetService = new TimerDequeueGetService(); + timerFlushService = new TimerFlushService(); + + int getThreadNum = Math.max(storeConfig.getTimerGetMessageThreadNum(), 1); + dequeueGetMessageServices = new TimerDequeueGetMessageService[getThreadNum]; + for (int i = 0; i < dequeueGetMessageServices.length; i++) { + dequeueGetMessageServices[i] = new TimerDequeueGetMessageService(); + } + + int putThreadNum = Math.max(storeConfig.getTimerPutMessageThreadNum(), 1); + dequeuePutMessageServices = new TimerDequeuePutMessageService[putThreadNum]; + for (int i = 0; i < dequeuePutMessageServices.length; i++) { + dequeuePutMessageServices[i] = new TimerDequeuePutMessageService(); + } + } + + public boolean load() { + this.initService(); + boolean load = timerLog.load(); + load = load && this.timerMetrics.load(); + recover(); + calcTimerDistribution(); + return load; + } + + public static String getTimerWheelPath(final String rootDir) { + return rootDir + File.separator + "timerwheel"; + } + + public static String getTimerLogPath(final String rootDir) { + return rootDir + File.separator + "timerlog"; + } + + private void calcTimerDistribution() { + long startTime = System.currentTimeMillis(); + List timerDist = this.timerMetrics.getTimerDistList(); + long currTime = System.currentTimeMillis() / precisionMs * precisionMs; + for (int i = 0; i < timerDist.size(); i++) { + int slotBeforeNum = i == 0 ? 0 : timerDist.get(i - 1) * 1000 / precisionMs; + int slotTotalNum = timerDist.get(i) * 1000 / precisionMs; + int periodTotal = 0; + for (int j = slotBeforeNum; j < slotTotalNum; j++) { + Slot slotEach = timerWheel.getSlot(currTime + (long) j * precisionMs); + periodTotal += slotEach.num; + } + LOGGER.debug("{} period's total num: {}", timerDist.get(i), periodTotal); + this.timerMetrics.updateDistPair(timerDist.get(i), periodTotal); + } + long endTime = System.currentTimeMillis(); + LOGGER.debug("Total cost Time: {}", endTime - startTime); + } + + @SuppressWarnings("NonAtomicOperationOnVolatileField") + public void recover() { + //recover timerLog + long lastFlushPos = timerCheckpoint.getLastTimerLogFlushPos(); + MappedFile lastFile = timerLog.getMappedFileQueue().getLastMappedFile(); + if (null != lastFile) { + lastFlushPos = lastFlushPos - lastFile.getFileSize(); + } + if (lastFlushPos < 0) { + lastFlushPos = 0; + } + long processOffset = recoverAndRevise(lastFlushPos, true); + + timerLog.getMappedFileQueue().setFlushedWhere(processOffset); + //revise queue offset + long queueOffset = reviseQueueOffset(processOffset); + if (-1 == queueOffset) { + currQueueOffset = timerCheckpoint.getLastTimerQueueOffset(); + } else { + currQueueOffset = queueOffset + 1; + } + currQueueOffset = Math.min(currQueueOffset, timerCheckpoint.getMasterTimerQueueOffset()); + + //check timer wheel + currReadTimeMs = timerCheckpoint.getLastReadTimeMs(); + long nextReadTimeMs = formatTimeMs( + System.currentTimeMillis()) - (long) slotsTotal * precisionMs + (long) TIMER_BLANK_SLOTS * precisionMs; + if (currReadTimeMs < nextReadTimeMs) { + currReadTimeMs = nextReadTimeMs; + } + //the timer wheel may contain physical offset bigger than timerLog + //This will only happen when the timerLog is damaged + //hard to test + long minFirst = timerWheel.checkPhyPos(currReadTimeMs, processOffset); + if (debug) { + minFirst = 0; + } + if (minFirst < processOffset) { + LOGGER.warn("Timer recheck because of minFirst:{} processOffset:{}", minFirst, processOffset); + recoverAndRevise(minFirst, false); + } + LOGGER.info("Timer recover ok currReadTimerMs:{} currQueueOffset:{} checkQueueOffset:{} processOffset:{}", + currReadTimeMs, currQueueOffset, timerCheckpoint.getLastTimerQueueOffset(), processOffset); + + commitReadTimeMs = currReadTimeMs; + commitQueueOffset = currQueueOffset; + + prepareTimerCheckPoint(); + } + + public long reviseQueueOffset(long processOffset) { + SelectMappedBufferResult selectRes = timerLog.getTimerMessage(processOffset - (TimerLog.UNIT_SIZE - TimerLog.UNIT_PRE_SIZE_FOR_MSG)); + if (null == selectRes) { + return -1; + } + try { + long offsetPy = selectRes.getByteBuffer().getLong(); + int sizePy = selectRes.getByteBuffer().getInt(); + MessageExt messageExt = getMessageByCommitOffset(offsetPy, sizePy); + if (null == messageExt) { + return -1; + } + + // check offset in msg is equal to offset of cq. + // if not, use cq offset. + long msgQueueOffset = messageExt.getQueueOffset(); + int queueId = messageExt.getQueueId(); + ConsumeQueueInterface cq = this.messageStore.getConsumeQueue(TIMER_TOPIC, queueId); + if (null == cq) { + return msgQueueOffset; + } + long cqOffset = msgQueueOffset; + long tmpOffset = msgQueueOffset; + int maxCount = 20000; + while (maxCount-- > 0) { + if (tmpOffset < 0) { + LOGGER.warn("reviseQueueOffset check cq offset fail, msg in cq is not found.{}, {}", + offsetPy, sizePy); + break; + } + ReferredIterator iterator = null; + try { + iterator = cq.iterateFrom(tmpOffset); + CqUnit cqUnit = null; + if (null == iterator || (cqUnit = iterator.next()) == null) { + // offset in msg may be greater than offset of cq. + tmpOffset -= 1; + continue; + } + + long offsetPyTemp = cqUnit.getPos(); + int sizePyTemp = cqUnit.getSize(); + if (offsetPyTemp == offsetPy && sizePyTemp == sizePy) { + LOGGER.info("reviseQueueOffset check cq offset ok. {}, {}, {}", + tmpOffset, offsetPyTemp, sizePyTemp); + cqOffset = tmpOffset; + break; + } + tmpOffset -= 1; + } catch (Throwable e) { + LOGGER.error("reviseQueueOffset check cq offset error.", e); + } finally { + if (iterator != null) { + iterator.release(); + } + } + } + + return cqOffset; + } finally { + selectRes.release(); + } + } + + //recover timerLog and revise timerWheel + //return process offset + private long recoverAndRevise(long beginOffset, boolean checkTimerLog) { + LOGGER.info("Begin to recover timerLog offset:{} check:{}", beginOffset, checkTimerLog); + MappedFile lastFile = timerLog.getMappedFileQueue().getLastMappedFile(); + if (null == lastFile) { + return 0; + } + + List mappedFiles = timerLog.getMappedFileQueue().getMappedFiles(); + int index = mappedFiles.size() - 1; + for (; index >= 0; index--) { + MappedFile mappedFile = mappedFiles.get(index); + if (beginOffset >= mappedFile.getFileFromOffset()) { + break; + } + } + if (index < 0) { + index = 0; + } + long checkOffset = mappedFiles.get(index).getFileFromOffset(); + for (; index < mappedFiles.size(); index++) { + MappedFile mappedFile = mappedFiles.get(index); + SelectMappedBufferResult sbr = mappedFile.selectMappedBuffer(0, checkTimerLog ? mappedFiles.get(index).getFileSize() : mappedFile.getReadPosition()); + ByteBuffer bf = sbr.getByteBuffer(); + int position = 0; + boolean stopCheck = false; + for (; position < sbr.getSize(); position += TimerLog.UNIT_SIZE) { + try { + bf.position(position); + int size = bf.getInt();//size + bf.getLong();//prev pos + int magic = bf.getInt(); + if (magic == TimerLog.BLANK_MAGIC_CODE) { + break; + } + if (checkTimerLog && (!isMagicOK(magic) || TimerLog.UNIT_SIZE != size)) { + stopCheck = true; + break; + } + long delayTime = bf.getLong() + bf.getInt(); + if (TimerLog.UNIT_SIZE == size && isMagicOK(magic)) { + timerWheel.reviseSlot(delayTime, TimerWheel.IGNORE, sbr.getStartOffset() + position, true); + } + } catch (Exception e) { + LOGGER.error("Recover timerLog error", e); + stopCheck = true; + break; + } + } + sbr.release(); + checkOffset = mappedFiles.get(index).getFileFromOffset() + position; + if (stopCheck) { + break; + } + } + if (checkTimerLog) { + timerLog.getMappedFileQueue().truncateDirtyFiles(checkOffset); + } + return checkOffset; + } + + public static boolean isMagicOK(int magic) { + return (magic | 0xF) == 0xF; + } + + public void start() { + this.shouldStartTime = storeConfig.getDisappearTimeAfterStart() + System.currentTimeMillis(); + maybeMoveWriteTime(); + enqueueGetService.start(); + enqueuePutService.start(); + dequeueWarmService.start(); + dequeueGetService.start(); + for (int i = 0; i < dequeueGetMessageServices.length; i++) { + dequeueGetMessageServices[i].start(); + } + for (int i = 0; i < dequeuePutMessageServices.length; i++) { + dequeuePutMessageServices[i].start(); + } + timerFlushService.start(); + + scheduler.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + long minPy = messageStore.getMinPhyOffset(); + int checkOffset = timerLog.getOffsetForLastUnit(); + timerLog.getMappedFileQueue() + .deleteExpiredFileByOffsetForTimerLog(minPy, checkOffset, TimerLog.UNIT_SIZE); + } catch (Exception e) { + LOGGER.error("Error in cleaning timerLog", e); + } + } + }, 30, 30, TimeUnit.SECONDS); + + scheduler.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + if (storeConfig.isTimerEnableCheckMetrics()) { + String when = storeConfig.getTimerCheckMetricsWhen(); + if (!UtilAll.isItTimeToDo(when)) { + return; + } + long curr = System.currentTimeMillis(); + if (curr - lastTimeOfCheckMetrics > 70 * 60 * 1000) { + lastTimeOfCheckMetrics = curr; + checkAndReviseMetrics(); + LOGGER.info("[CheckAndReviseMetrics]Timer do check timer metrics cost {} ms", + System.currentTimeMillis() - curr); + } + } + } catch (Exception e) { + LOGGER.error("Error in cleaning timerLog", e); + } + } + }, 45, 45, TimeUnit.MINUTES); + + state = RUNNING; + LOGGER.info("Timer start ok currReadTimerMs:[{}] queueOffset:[{}]", new Timestamp(currReadTimeMs), currQueueOffset); + } + + public void start(boolean shouldRunningDequeue) { + this.shouldRunningDequeue = shouldRunningDequeue; + this.start(); + } + + public void shutdown() { + if (SHUTDOWN == state) { + return; + } + state = SHUTDOWN; + //first save checkpoint + prepareTimerCheckPoint(); + timerFlushService.shutdown(); + timerLog.shutdown(); + timerCheckpoint.shutdown(); + + enqueuePutQueue.clear(); //avoid blocking + dequeueGetQueue.clear(); //avoid blocking + dequeuePutQueue.clear(); //avoid blocking + + enqueueGetService.shutdown(); + enqueuePutService.shutdown(); + dequeueWarmService.shutdown(); + dequeueGetService.shutdown(); + for (int i = 0; i < dequeueGetMessageServices.length; i++) { + dequeueGetMessageServices[i].shutdown(); + } + for (int i = 0; i < dequeuePutMessageServices.length; i++) { + dequeuePutMessageServices[i].shutdown(); + } + timerWheel.shutdown(false); + + this.scheduler.shutdown(); + UtilAll.cleanBuffer(this.bufferLocal.get()); + this.bufferLocal.remove(); + } + + protected void maybeMoveWriteTime() { + if (currWriteTimeMs < formatTimeMs(System.currentTimeMillis())) { + currWriteTimeMs = formatTimeMs(System.currentTimeMillis()); + } + } + + private void moveReadTime() { + currReadTimeMs = currReadTimeMs + precisionMs; + commitReadTimeMs = currReadTimeMs; + } + + private boolean isRunning() { + return RUNNING == state; + } + + private void checkBrokerRole() { + BrokerRole currRole = storeConfig.getBrokerRole(); + if (lastBrokerRole != currRole) { + synchronized (lastBrokerRole) { + LOGGER.info("Broker role change from {} to {}", lastBrokerRole, currRole); + //if change to master, do something + if (BrokerRole.SLAVE != currRole) { + currQueueOffset = Math.min(currQueueOffset, timerCheckpoint.getMasterTimerQueueOffset()); + commitQueueOffset = currQueueOffset; + prepareTimerCheckPoint(); + timerCheckpoint.flush(); + currReadTimeMs = timerCheckpoint.getLastReadTimeMs(); + commitReadTimeMs = currReadTimeMs; + } + //if change to slave, just let it go + lastBrokerRole = currRole; + } + } + } + + private boolean isRunningEnqueue() { + checkBrokerRole(); + if (!shouldRunningDequeue && !isMaster() && currQueueOffset >= timerCheckpoint.getMasterTimerQueueOffset()) { + return false; + } + + return isRunning(); + } + + private boolean isRunningDequeue() { + if (!this.shouldRunningDequeue) { + syncLastReadTimeMs(); + return false; + } + return isRunning(); + } + + public void syncLastReadTimeMs() { + currReadTimeMs = timerCheckpoint.getLastReadTimeMs(); + commitReadTimeMs = currReadTimeMs; + } + + public void setShouldRunningDequeue(final boolean shouldRunningDequeue) { + this.shouldRunningDequeue = shouldRunningDequeue; + } + + public boolean isShouldRunningDequeue() { + return shouldRunningDequeue; + } + + public void addMetric(MessageExt msg, int value) { + try { + if (null == msg || null == msg.getProperty(MessageConst.PROPERTY_REAL_TOPIC)) { + return; + } + if (msg.getProperty(TIMER_ENQUEUE_MS) != null + && NumberUtils.toLong(msg.getProperty(TIMER_ENQUEUE_MS)) == Long.MAX_VALUE) { + return; + } + // pass msg into addAndGet, for further more judgement extension. + timerMetrics.addAndGet(msg, value); + } catch (Throwable t) { + if (frequency.incrementAndGet() % 1000 == 0) { + LOGGER.error("error in adding metric", t); + } + } + + } + + public void holdMomentForUnknownError(long ms) { + try { + Thread.sleep(ms); + } catch (Exception ignored) { + + } + } + + public void holdMomentForUnknownError() { + holdMomentForUnknownError(50); + } + + public boolean enqueue(int queueId) { + if (storeConfig.isTimerStopEnqueue()) { + return false; + } + if (!isRunningEnqueue()) { + return false; + } + ConsumeQueueInterface cq = this.messageStore.getConsumeQueue(TIMER_TOPIC, queueId); + if (null == cq) { + return false; + } + if (currQueueOffset < cq.getMinOffsetInQueue()) { + LOGGER.warn("Timer currQueueOffset:{} is smaller than minOffsetInQueue:{}", + currQueueOffset, cq.getMinOffsetInQueue()); + currQueueOffset = cq.getMinOffsetInQueue(); + } + long offset = currQueueOffset; + ReferredIterator iterator = null; + try { + iterator = cq.iterateFrom(offset); + if (null == iterator) { + return false; + } + + int i = 0; + while (iterator.hasNext()) { + i++; + perfCounterTicks.startTick("enqueue_get"); + try { + CqUnit cqUnit = iterator.next(); + long offsetPy = cqUnit.getPos(); + int sizePy = cqUnit.getSize(); + cqUnit.getTagsCode(); //tags code + MessageExt msgExt = getMessageByCommitOffset(offsetPy, sizePy); + if (null == msgExt) { + perfCounterTicks.getCounter("enqueue_get_miss"); + } else { + lastEnqueueButExpiredTime = System.currentTimeMillis(); + lastEnqueueButExpiredStoreTime = msgExt.getStoreTimestamp(); + long delayedTime = Long.parseLong(msgExt.getProperty(TIMER_OUT_MS)); + // use CQ offset, not offset in Message + msgExt.setQueueOffset(offset + i); + TimerRequest timerRequest = new TimerRequest(offsetPy, sizePy, delayedTime, System.currentTimeMillis(), MAGIC_DEFAULT, msgExt); + // System.out.printf("build enqueue request, %s%n", timerRequest); + while (!enqueuePutQueue.offer(timerRequest, 3, TimeUnit.SECONDS)) { + if (!isRunningEnqueue()) { + return false; + } + } + Attributes attributes = DefaultStoreMetricsManager.newAttributesBuilder() + .put(DefaultStoreMetricsConstant.LABEL_TOPIC, msgExt.getProperty(MessageConst.PROPERTY_REAL_TOPIC)).build(); + DefaultStoreMetricsManager.timerMessageSetLatency.record((delayedTime - msgExt.getBornTimestamp()) / 1000, attributes); + } + } catch (Exception e) { + // here may cause the message loss + if (storeConfig.isTimerSkipUnknownError()) { + LOGGER.warn("Unknown error in skipped in enqueuing", e); + } else { + holdMomentForUnknownError(); + throw e; + } + } finally { + perfCounterTicks.endTick("enqueue_get"); + } + // if broker role changes, ignore last enqueue + if (!isRunningEnqueue()) { + return false; + } + currQueueOffset = offset + i; + } + currQueueOffset = offset + i; + return i > 0; + } catch (Exception e) { + LOGGER.error("Unknown exception in enqueuing", e); + } finally { + if (iterator != null) { + iterator.release(); + } + } + return false; + } + + public boolean doEnqueue(long offsetPy, int sizePy, long delayedTime, MessageExt messageExt) { + LOGGER.debug("Do enqueue [{}] [{}]", new Timestamp(delayedTime), messageExt); + //copy the value first, avoid concurrent problem + long tmpWriteTimeMs = currWriteTimeMs; + boolean needRoll = delayedTime - tmpWriteTimeMs >= (long) timerRollWindowSlots * precisionMs; + int magic = MAGIC_DEFAULT; + if (needRoll) { + magic = magic | MAGIC_ROLL; + if (delayedTime - tmpWriteTimeMs - (long) timerRollWindowSlots * precisionMs < (long) timerRollWindowSlots / 3 * precisionMs) { + //give enough time to next roll + delayedTime = tmpWriteTimeMs + (long) (timerRollWindowSlots / 2) * precisionMs; + } else { + delayedTime = tmpWriteTimeMs + (long) timerRollWindowSlots * precisionMs; + } + } + boolean isDelete = messageExt.getProperty(TIMER_DELETE_UNIQUE_KEY) != null; + if (isDelete) { + magic = magic | MAGIC_DELETE; + } + String realTopic = messageExt.getProperty(MessageConst.PROPERTY_REAL_TOPIC); + Slot slot = timerWheel.getSlot(delayedTime); + ByteBuffer tmpBuffer = timerLogBuffer; + tmpBuffer.clear(); + tmpBuffer.putInt(TimerLog.UNIT_SIZE); //size + tmpBuffer.putLong(slot.lastPos); //prev pos + tmpBuffer.putInt(magic); //magic + tmpBuffer.putLong(tmpWriteTimeMs); //currWriteTime + tmpBuffer.putInt((int) (delayedTime - tmpWriteTimeMs)); //delayTime + tmpBuffer.putLong(offsetPy); //offset + tmpBuffer.putInt(sizePy); //size + tmpBuffer.putInt(hashTopicForMetrics(realTopic)); //hashcode of real topic + tmpBuffer.putLong(0); //reserved value, just set to 0 now + long ret = timerLog.append(tmpBuffer.array(), 0, TimerLog.UNIT_SIZE); + if (-1 != ret) { + // If it's a delete message, then slot's total num -1 + // TODO: check if the delete msg is in the same slot with "the msg to be deleted". + timerWheel.putSlot(delayedTime, slot.firstPos == -1 ? ret : slot.firstPos, ret, + isDelete ? slot.num - 1 : slot.num + 1, slot.magic); + addMetric(messageExt, isDelete ? -1 : 1); + } + return -1 != ret; + } + + @SuppressWarnings("NonAtomicOperationOnVolatileField") + public int warmDequeue() { + if (!isRunningDequeue()) { + return -1; + } + if (!storeConfig.isTimerWarmEnable()) { + return -1; + } + if (preReadTimeMs <= currReadTimeMs) { + preReadTimeMs = currReadTimeMs + precisionMs; + } + if (preReadTimeMs >= currWriteTimeMs) { + return -1; + } + if (preReadTimeMs >= currReadTimeMs + 3L * precisionMs) { + return -1; + } + Slot slot = timerWheel.getSlot(preReadTimeMs); + if (-1 == slot.timeMs) { + preReadTimeMs = preReadTimeMs + precisionMs; + return 0; + } + long currOffsetPy = slot.lastPos; + LinkedList sbrs = new LinkedList<>(); + SelectMappedBufferResult timeSbr = null; + SelectMappedBufferResult msgSbr = null; + try { + //read the msg one by one + while (currOffsetPy != -1) { + if (!isRunning()) { + break; + } + perfCounterTicks.startTick("warm_dequeue"); + if (null == timeSbr || timeSbr.getStartOffset() > currOffsetPy) { + timeSbr = timerLog.getWholeBuffer(currOffsetPy); + if (null != timeSbr) { + sbrs.add(timeSbr); + } + } + if (null == timeSbr) { + break; + } + long prevPos = -1; + try { + int position = (int) (currOffsetPy % timerLogFileSize); + timeSbr.getByteBuffer().position(position); + timeSbr.getByteBuffer().getInt(); //size + prevPos = timeSbr.getByteBuffer().getLong(); + timeSbr.getByteBuffer().position(position + TimerLog.UNIT_PRE_SIZE_FOR_MSG); + long offsetPy = timeSbr.getByteBuffer().getLong(); + int sizePy = timeSbr.getByteBuffer().getInt(); + if (null == msgSbr || msgSbr.getStartOffset() > offsetPy) { + msgSbr = messageStore.getCommitLogData(offsetPy - offsetPy % commitLogFileSize); + if (null != msgSbr) { + sbrs.add(msgSbr); + } + } + if (null != msgSbr) { + ByteBuffer bf = msgSbr.getByteBuffer(); + int firstPos = (int) (offsetPy % commitLogFileSize); + for (int pos = firstPos; pos < firstPos + sizePy; pos += 4096) { + bf.position(pos); + bf.get(); + } + } + } catch (Exception e) { + LOGGER.error("Unexpected error in warm", e); + } finally { + currOffsetPy = prevPos; + perfCounterTicks.endTick("warm_dequeue"); + } + } + for (SelectMappedBufferResult sbr : sbrs) { + if (null != sbr) { + sbr.release(); + } + } + } finally { + preReadTimeMs = preReadTimeMs + precisionMs; + } + return 1; + } + + public boolean checkStateForPutMessages(int state) { + for (AbstractStateService service : dequeuePutMessageServices) { + if (!service.isState(state)) { + return false; + } + } + return true; + } + + public boolean checkStateForGetMessages(int state) { + for (AbstractStateService service : dequeueGetMessageServices) { + if (!service.isState(state)) { + return false; + } + } + return true; + } + + public void checkDequeueLatch(CountDownLatch latch, long delayedTime) throws Exception { + if (latch.await(1, TimeUnit.SECONDS)) { + return; + } + int checkNum = 0; + while (true) { + if (dequeuePutQueue.size() > 0 + || !checkStateForGetMessages(AbstractStateService.WAITING) + || !checkStateForPutMessages(AbstractStateService.WAITING)) { + //let it go + } else { + checkNum++; + if (checkNum >= 2) { + break; + } + } + if (latch.await(1, TimeUnit.SECONDS)) { + break; + } + } + if (!latch.await(1, TimeUnit.SECONDS)) { + LOGGER.warn("Check latch failed delayedTime:{}", delayedTime); + } + } + + public int dequeue() throws Exception { + if (storeConfig.isTimerStopDequeue()) { + return -1; + } + if (!isRunningDequeue()) { + return -1; + } + if (currReadTimeMs >= currWriteTimeMs) { + return -1; + } + + Slot slot = timerWheel.getSlot(currReadTimeMs); + if (-1 == slot.timeMs) { + moveReadTime(); + return 0; + } + try { + //clear the flag + dequeueStatusChangeFlag = false; + + long currOffsetPy = slot.lastPos; + Set deleteUniqKeys = new ConcurrentSkipListSet<>(); + LinkedList normalMsgStack = new LinkedList<>(); + LinkedList deleteMsgStack = new LinkedList<>(); + LinkedList sbrs = new LinkedList<>(); + SelectMappedBufferResult timeSbr = null; + //read the timer log one by one + while (currOffsetPy != -1) { + perfCounterTicks.startTick("dequeue_read_timerlog"); + if (null == timeSbr || timeSbr.getStartOffset() > currOffsetPy) { + timeSbr = timerLog.getWholeBuffer(currOffsetPy); + if (null != timeSbr) { + sbrs.add(timeSbr); + } + } + if (null == timeSbr) { + break; + } + long prevPos = -1; + try { + int position = (int) (currOffsetPy % timerLogFileSize); + timeSbr.getByteBuffer().position(position); + timeSbr.getByteBuffer().getInt(); //size + prevPos = timeSbr.getByteBuffer().getLong(); + int magic = timeSbr.getByteBuffer().getInt(); + long enqueueTime = timeSbr.getByteBuffer().getLong(); + long delayedTime = timeSbr.getByteBuffer().getInt() + enqueueTime; + long offsetPy = timeSbr.getByteBuffer().getLong(); + int sizePy = timeSbr.getByteBuffer().getInt(); + TimerRequest timerRequest = new TimerRequest(offsetPy, sizePy, delayedTime, enqueueTime, magic); + timerRequest.setDeleteList(deleteUniqKeys); + if (needDelete(magic) && !needRoll(magic)) { + deleteMsgStack.add(timerRequest); + } else { + normalMsgStack.addFirst(timerRequest); + } + } catch (Exception e) { + LOGGER.error("Error in dequeue_read_timerlog", e); + } finally { + currOffsetPy = prevPos; + perfCounterTicks.endTick("dequeue_read_timerlog"); + } + } + if (deleteMsgStack.size() == 0 && normalMsgStack.size() == 0) { + LOGGER.warn("dequeue time:{} but read nothing from timerLog", currReadTimeMs); + } + for (SelectMappedBufferResult sbr : sbrs) { + if (null != sbr) { + sbr.release(); + } + } + if (!isRunningDequeue()) { + return -1; + } + CountDownLatch deleteLatch = new CountDownLatch(deleteMsgStack.size()); + //read the delete msg: the msg used to mark another msg is deleted + for (List deleteList : splitIntoLists(deleteMsgStack)) { + for (TimerRequest tr : deleteList) { + tr.setLatch(deleteLatch); + } + dequeueGetQueue.put(deleteList); + } + //do we need to use loop with tryAcquire + checkDequeueLatch(deleteLatch, currReadTimeMs); + + CountDownLatch normalLatch = new CountDownLatch(normalMsgStack.size()); + //read the normal msg + for (List normalList : splitIntoLists(normalMsgStack)) { + for (TimerRequest tr : normalList) { + tr.setLatch(normalLatch); + } + dequeueGetQueue.put(normalList); + } + checkDequeueLatch(normalLatch, currReadTimeMs); + // if master -> slave -> master, then the read time move forward, and messages will be lossed + if (dequeueStatusChangeFlag) { + return -1; + } + if (!isRunningDequeue()) { + return -1; + } + moveReadTime(); + } catch (Throwable t) { + LOGGER.error("Unknown error in dequeue process", t); + if (storeConfig.isTimerSkipUnknownError()) { + moveReadTime(); + } + } + return 1; + } + + private List> splitIntoLists(List origin) { + //this method assume that the origin is not null; + List> lists = new LinkedList<>(); + if (origin.size() < 100) { + lists.add(origin); + return lists; + } + List currList = null; + int fileIndexPy = -1; + int msgIndex = 0; + for (TimerRequest tr : origin) { + if (fileIndexPy != tr.getOffsetPy() / commitLogFileSize) { + msgIndex = 0; + if (null != currList && currList.size() > 0) { + lists.add(currList); + } + currList = new LinkedList<>(); + currList.add(tr); + fileIndexPy = (int) (tr.getOffsetPy() / commitLogFileSize); + } else { + currList.add(tr); + if (++msgIndex % 2000 == 0) { + lists.add(currList); + currList = new ArrayList<>(); + } + } + } + if (null != currList && currList.size() > 0) { + lists.add(currList); + } + return lists; + } + + private MessageExt getMessageByCommitOffset(long offsetPy, int sizePy) { + for (int i = 0; i < 3; i++) { + MessageExt msgExt = null; + bufferLocal.get().position(0); + bufferLocal.get().limit(sizePy); + boolean res = messageStore.getData(offsetPy, sizePy, bufferLocal.get()); + if (res) { + bufferLocal.get().flip(); + msgExt = MessageDecoder.decode(bufferLocal.get(), true, false, false); + } + if (null == msgExt) { + LOGGER.warn("Fail to read msg from commitLog offsetPy:{} sizePy:{}", offsetPy, sizePy); + } else { + return msgExt; + } + } + return null; + } + + public MessageExtBrokerInner convert(MessageExt messageExt, long enqueueTime, boolean needRoll) { + if (enqueueTime != -1) { + MessageAccessor.putProperty(messageExt, TIMER_ENQUEUE_MS, enqueueTime + ""); + } + if (needRoll) { + if (messageExt.getProperty(TIMER_ROLL_TIMES) != null) { + MessageAccessor.putProperty(messageExt, TIMER_ROLL_TIMES, Integer.parseInt(messageExt.getProperty(TIMER_ROLL_TIMES)) + 1 + ""); + } else { + MessageAccessor.putProperty(messageExt, TIMER_ROLL_TIMES, 1 + ""); + } + } + MessageAccessor.putProperty(messageExt, TIMER_DEQUEUE_MS, System.currentTimeMillis() + ""); + MessageExtBrokerInner message = convertMessage(messageExt, needRoll); + return message; + } + + //0 succ; 1 fail, need retry; 2 fail, do not retry; + public int doPut(MessageExtBrokerInner message, boolean roll) throws Exception { + + if (!roll && null != message.getProperty(MessageConst.PROPERTY_TIMER_DEL_UNIQKEY)) { + LOGGER.warn("Trying do put delete timer msg:[{}] roll:[{}]", message, roll); + return PUT_NO_RETRY; + } + + PutMessageResult putMessageResult = null; + if (escapeBridgeHook != null) { + putMessageResult = escapeBridgeHook.apply(message); + } else { + putMessageResult = messageStore.putMessage(message); + } + + int retryNum = 0; + while (retryNum < 3) { + if (null == putMessageResult || null == putMessageResult.getPutMessageStatus()) { + retryNum++; + } else { + switch (putMessageResult.getPutMessageStatus()) { + case PUT_OK: + if (brokerStatsManager != null) { + this.brokerStatsManager.incTopicPutNums(message.getTopic(), 1, 1); + if (putMessageResult.getAppendMessageResult() != null) { + this.brokerStatsManager.incTopicPutSize(message.getTopic(), + putMessageResult.getAppendMessageResult().getWroteBytes()); + } + this.brokerStatsManager.incBrokerPutNums(message.getTopic(), 1); + } + return PUT_OK; + case SERVICE_NOT_AVAILABLE: + return PUT_NEED_RETRY; + case MESSAGE_ILLEGAL: + case PROPERTIES_SIZE_EXCEEDED: + return PUT_NO_RETRY; + case CREATE_MAPPED_FILE_FAILED: + case FLUSH_DISK_TIMEOUT: + case FLUSH_SLAVE_TIMEOUT: + case OS_PAGE_CACHE_BUSY: + case SLAVE_NOT_AVAILABLE: + case UNKNOWN_ERROR: + default: + retryNum++; + } + } + Thread.sleep(50); + if (escapeBridgeHook != null) { + putMessageResult = escapeBridgeHook.apply(message); + } else { + putMessageResult = messageStore.putMessage(message); + } + LOGGER.warn("Retrying to do put timer msg retryNum:{} putRes:{} msg:{}", retryNum, putMessageResult, message); + } + return PUT_NO_RETRY; + } + + public MessageExtBrokerInner convertMessage(MessageExt msgExt, boolean needRoll) { + MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); + msgInner.setBody(msgExt.getBody()); + msgInner.setFlag(msgExt.getFlag()); + MessageAccessor.setProperties(msgInner, MessageAccessor.deepCopyProperties(msgExt.getProperties())); + TopicFilterType topicFilterType = MessageExt.parseTopicFilterType(msgInner.getSysFlag()); + long tagsCodeValue = + MessageExtBrokerInner.tagsString2tagsCode(topicFilterType, msgInner.getTags()); + msgInner.setTagsCode(tagsCodeValue); + msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgExt.getProperties())); + + msgInner.setSysFlag(msgExt.getSysFlag()); + msgInner.setBornTimestamp(msgExt.getBornTimestamp()); + msgInner.setBornHost(msgExt.getBornHost()); + msgInner.setStoreHost(msgExt.getStoreHost()); + msgInner.setReconsumeTimes(msgExt.getReconsumeTimes()); + + msgInner.setWaitStoreMsgOK(false); + + if (needRoll) { + msgInner.setTopic(msgExt.getTopic()); + msgInner.setQueueId(msgExt.getQueueId()); + } else { + msgInner.setTopic(msgInner.getProperty(MessageConst.PROPERTY_REAL_TOPIC)); + msgInner.setQueueId(Integer.parseInt(msgInner.getProperty(MessageConst.PROPERTY_REAL_QUEUE_ID))); + MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_REAL_TOPIC); + MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_REAL_QUEUE_ID); + } + return msgInner; + } + + protected String getRealTopic(MessageExt msgExt) { + if (msgExt == null) { + return null; + } + return msgExt.getProperty(MessageConst.PROPERTY_REAL_TOPIC); + } + + private long formatTimeMs(long timeMs) { + return timeMs / precisionMs * precisionMs; + } + + public int hashTopicForMetrics(String topic) { + return null == topic ? 0 : topic.hashCode(); + } + + public void checkAndReviseMetrics() { + Map smallOnes = new HashMap<>(); + Map bigOnes = new HashMap<>(); + Map smallHashs = new HashMap<>(); + Set smallHashCollisions = new HashSet<>(); + for (Map.Entry entry : timerMetrics.getTimingCount().entrySet()) { + if (entry.getValue().getCount().get() < storeConfig.getTimerMetricSmallThreshold()) { + smallOnes.put(entry.getKey(), entry.getValue()); + int hash = hashTopicForMetrics(entry.getKey()); + if (smallHashs.containsKey(hash)) { + LOGGER.warn("[CheckAndReviseMetrics]Metric hash collision between small-small code:{} small topic:{}{} small topic:{}{}", hash, + entry.getKey(), entry.getValue(), + smallHashs.get(hash), smallOnes.get(smallHashs.get(hash))); + smallHashCollisions.add(hash); + } + smallHashs.put(hash, entry.getKey()); + } else { + bigOnes.put(entry.getKey(), entry.getValue()); + } + } + //check the hash collision between small ons and big ons + for (Map.Entry bjgEntry : bigOnes.entrySet()) { + if (smallHashs.containsKey(hashTopicForMetrics(bjgEntry.getKey()))) { + Iterator> smallIt = smallOnes.entrySet().iterator(); + while (smallIt.hasNext()) { + Map.Entry smallEntry = smallIt.next(); + if (hashTopicForMetrics(smallEntry.getKey()) == hashTopicForMetrics(bjgEntry.getKey())) { + LOGGER.warn("[CheckAndReviseMetrics]Metric hash collision between small-big code:{} small topic:{}{} big topic:{}{}", hashTopicForMetrics(smallEntry.getKey()), + smallEntry.getKey(), smallEntry.getValue(), + bjgEntry.getKey(), bjgEntry.getValue()); + smallIt.remove(); + } + } + } + } + //refresh + smallHashs.clear(); + Map newSmallOnes = new HashMap<>(); + for (String topic : smallOnes.keySet()) { + newSmallOnes.put(topic, new TimerMetrics.Metric()); + smallHashs.put(hashTopicForMetrics(topic), topic); + } + + //travel the timer log + long readTimeMs = currReadTimeMs; + long currOffsetPy = timerWheel.checkPhyPos(readTimeMs, 0); + LinkedList sbrs = new LinkedList<>(); + boolean hasError = false; + try { + while (true) { + SelectMappedBufferResult timeSbr = timerLog.getWholeBuffer(currOffsetPy); + if (timeSbr == null) { + break; + } else { + sbrs.add(timeSbr); + } + ByteBuffer bf = timeSbr.getByteBuffer(); + for (int position = 0; position < timeSbr.getSize(); position += TimerLog.UNIT_SIZE) { + bf.position(position); + bf.getInt();//size + bf.getLong();//prev pos + int magic = bf.getInt(); //magic + long enqueueTime = bf.getLong(); + long delayedTime = bf.getInt() + enqueueTime; + long offsetPy = bf.getLong(); + int sizePy = bf.getInt(); + int hashCode = bf.getInt(); + if (delayedTime < readTimeMs) { + continue; + } + if (!smallHashs.containsKey(hashCode)) { + continue; + } + String topic = null; + if (smallHashCollisions.contains(hashCode)) { + MessageExt messageExt = getMessageByCommitOffset(offsetPy, sizePy); + if (null != messageExt) { + topic = messageExt.getProperty(MessageConst.PROPERTY_REAL_TOPIC); + } + } else { + topic = smallHashs.get(hashCode); + } + if (null != topic && newSmallOnes.containsKey(topic)) { + newSmallOnes.get(topic).getCount().addAndGet(needDelete(magic) ? -1 : 1); + } else { + LOGGER.warn("[CheckAndReviseMetrics]Unexpected topic in checking timer metrics topic:{} code:{} offsetPy:{} size:{}", topic, hashCode, offsetPy, sizePy); + } + } + if (timeSbr.getSize() < timerLogFileSize) { + break; + } else { + currOffsetPy = currOffsetPy + timerLogFileSize; + } + } + + } catch (Exception e) { + hasError = true; + LOGGER.error("[CheckAndReviseMetrics]Unknown error in checkAndReviseMetrics and abort", e); + } finally { + for (SelectMappedBufferResult sbr : sbrs) { + if (null != sbr) { + sbr.release(); + } + } + } + + if (!hasError) { + //update + for (String topic : newSmallOnes.keySet()) { + LOGGER.info("[CheckAndReviseMetrics]Revise metric for topic {} from {} to {}", topic, smallOnes.get(topic), newSmallOnes.get(topic)); + } + timerMetrics.getTimingCount().putAll(newSmallOnes); + } + + } + + public class TimerEnqueueGetService extends ServiceThread { + + @Override + public String getServiceName() { + return getServiceThreadName() + this.getClass().getSimpleName(); + } + + @Override + public void run() { + TimerMessageStore.LOGGER.info(this.getServiceName() + " service start"); + while (!this.isStopped()) { + try { + if (!TimerMessageStore.this.enqueue(0)) { + waitForRunning(100L * precisionMs / 1000); + } + } catch (Throwable e) { + TimerMessageStore.LOGGER.error("Error occurred in " + getServiceName(), e); + } + } + TimerMessageStore.LOGGER.info(this.getServiceName() + " service end"); + } + } + + public String getServiceThreadName() { + String brokerIdentifier = ""; + if (TimerMessageStore.this.messageStore instanceof DefaultMessageStore) { + DefaultMessageStore messageStore = (DefaultMessageStore) TimerMessageStore.this.messageStore; + if (messageStore.getBrokerConfig().isInBrokerContainer()) { + brokerIdentifier = messageStore.getBrokerConfig().getIdentifier(); + } + } + return brokerIdentifier; + } + + public class TimerEnqueuePutService extends ServiceThread { + + @Override + public String getServiceName() { + return getServiceThreadName() + this.getClass().getSimpleName(); + } + + /** + * collect the requests + */ + protected List fetchTimerRequests() throws InterruptedException { + List trs = null; + TimerRequest firstReq = enqueuePutQueue.poll(10, TimeUnit.MILLISECONDS); + if (null != firstReq) { + trs = new ArrayList<>(16); + trs.add(firstReq); + while (true) { + TimerRequest tmpReq = enqueuePutQueue.poll(3, TimeUnit.MILLISECONDS); + if (null == tmpReq) { + break; + } + trs.add(tmpReq); + if (trs.size() > 10) { + break; + } + } + } + return trs; + } + + protected void putMessageToTimerWheel(TimerRequest req) { + try { + perfCounterTicks.startTick(ENQUEUE_PUT); + DefaultStoreMetricsManager.incTimerEnqueueCount(getRealTopic(req.getMsg())); + if (shouldRunningDequeue && req.getDelayTime() < currWriteTimeMs) { + req.setEnqueueTime(Long.MAX_VALUE); + dequeuePutQueue.put(req); + } else { + boolean doEnqueueRes = doEnqueue( + req.getOffsetPy(), req.getSizePy(), req.getDelayTime(), req.getMsg()); + req.idempotentRelease(doEnqueueRes || storeConfig.isTimerSkipUnknownError()); + } + perfCounterTicks.endTick(ENQUEUE_PUT); + } catch (Throwable t) { + LOGGER.error("Unknown error", t); + if (storeConfig.isTimerSkipUnknownError()) { + req.idempotentRelease(true); + } else { + holdMomentForUnknownError(); + } + } + } + + protected void fetchAndPutTimerRequest() throws Exception { + long tmpCommitQueueOffset = currQueueOffset; + List trs = this.fetchTimerRequests(); + if (CollectionUtils.isEmpty(trs)) { + commitQueueOffset = tmpCommitQueueOffset; + maybeMoveWriteTime(); + return; + } + + while (!isStopped()) { + CountDownLatch latch = new CountDownLatch(trs.size()); + for (TimerRequest req : trs) { + req.setLatch(latch); + this.putMessageToTimerWheel(req); + } + checkDequeueLatch(latch, -1); + boolean allSuccess = trs.stream().allMatch(TimerRequest::isSucc); + if (allSuccess) { + break; + } else { + holdMomentForUnknownError(); + } + } + commitQueueOffset = trs.get(trs.size() - 1).getMsg().getQueueOffset(); + maybeMoveWriteTime(); + } + + @Override + public void run() { + TimerMessageStore.LOGGER.info(this.getServiceName() + " service start"); + while (!this.isStopped() || enqueuePutQueue.size() != 0) { + try { + fetchAndPutTimerRequest(); + } catch (Throwable e) { + TimerMessageStore.LOGGER.error("Unknown error", e); + } + } + TimerMessageStore.LOGGER.info(this.getServiceName() + " service end"); + } + } + + public class TimerDequeueGetService extends ServiceThread { + + @Override + public String getServiceName() { + return getServiceThreadName() + this.getClass().getSimpleName(); + } + + @Override + public void run() { + TimerMessageStore.LOGGER.info(this.getServiceName() + " service start"); + while (!this.isStopped()) { + try { + if (System.currentTimeMillis() < shouldStartTime) { + TimerMessageStore.LOGGER.info("TimerDequeueGetService ready to run after {}.", shouldStartTime); + waitForRunning(1000); + continue; + } + if (-1 == TimerMessageStore.this.dequeue()) { + waitForRunning(100L * precisionMs / 1000); + } + } catch (Throwable e) { + TimerMessageStore.LOGGER.error("Error occurred in " + getServiceName(), e); + } + } + TimerMessageStore.LOGGER.info(this.getServiceName() + " service end"); + } + } + + abstract class AbstractStateService extends ServiceThread { + public static final int INITIAL = -1, START = 0, WAITING = 1, RUNNING = 2, END = 3; + protected int state = INITIAL; + + protected void setState(int state) { + this.state = state; + } + + protected boolean isState(int state) { + return this.state == state; + } + } + + public class TimerDequeuePutMessageService extends AbstractStateService { + + @Override + public String getServiceName() { + return getServiceThreadName() + this.getClass().getSimpleName(); + } + + @Override + public void run() { + setState(AbstractStateService.START); + TimerMessageStore.LOGGER.info(this.getServiceName() + " service start"); + while (!this.isStopped() || dequeuePutQueue.size() != 0) { + try { + setState(AbstractStateService.WAITING); + TimerRequest tr = dequeuePutQueue.poll(10, TimeUnit.MILLISECONDS); + if (null == tr) { + continue; + } + setState(AbstractStateService.RUNNING); + boolean doRes = false; + boolean tmpDequeueChangeFlag = false; + try { + while (!isStopped() && !doRes) { + if (!isRunningDequeue()) { + dequeueStatusChangeFlag = true; + tmpDequeueChangeFlag = true; + break; + } + try { + perfCounterTicks.startTick(DEQUEUE_PUT); + MessageExt msgExt = tr.getMsg(); + DefaultStoreMetricsManager.incTimerDequeueCount(getRealTopic(msgExt)); + if (tr.getEnqueueTime() == Long.MAX_VALUE) { + // never enqueue, mark it. + MessageAccessor.putProperty(msgExt, TIMER_ENQUEUE_MS, String.valueOf(Long.MAX_VALUE)); + } + addMetric(msgExt, -1); + MessageExtBrokerInner msg = convert(msgExt, tr.getEnqueueTime(), needRoll(tr.getMagic())); + doRes = PUT_NEED_RETRY != doPut(msg, needRoll(tr.getMagic())); + while (!doRes && !isStopped()) { + if (!isRunningDequeue()) { + dequeueStatusChangeFlag = true; + tmpDequeueChangeFlag = true; + break; + } + doRes = PUT_NEED_RETRY != doPut(msg, needRoll(tr.getMagic())); + Thread.sleep(500L * precisionMs / 1000); + } + perfCounterTicks.endTick(DEQUEUE_PUT); + } catch (Throwable t) { + LOGGER.info("Unknown error", t); + if (storeConfig.isTimerSkipUnknownError()) { + doRes = true; + } else { + holdMomentForUnknownError(); + } + } + } + } finally { + tr.idempotentRelease(!tmpDequeueChangeFlag); + } + + } catch (Throwable e) { + TimerMessageStore.LOGGER.error("Error occurred in " + getServiceName(), e); + } + } + TimerMessageStore.LOGGER.info(this.getServiceName() + " service end"); + setState(AbstractStateService.END); + } + } + + public class TimerDequeueGetMessageService extends AbstractStateService { + + @Override + public String getServiceName() { + return getServiceThreadName() + this.getClass().getSimpleName(); + } + + @Override + public void run() { + setState(AbstractStateService.START); + TimerMessageStore.LOGGER.info(this.getServiceName() + " service start"); + while (!this.isStopped()) { + try { + setState(AbstractStateService.WAITING); + List trs = dequeueGetQueue.poll(100L * precisionMs / 1000, TimeUnit.MILLISECONDS); + if (null == trs || trs.size() == 0) { + continue; + } + setState(AbstractStateService.RUNNING); + for (int i = 0; i < trs.size(); ) { + TimerRequest tr = trs.get(i); + boolean doRes = false; + try { + long start = System.currentTimeMillis(); + MessageExt msgExt = getMessageByCommitOffset(tr.getOffsetPy(), tr.getSizePy()); + if (null != msgExt) { + if (needDelete(tr.getMagic()) && !needRoll(tr.getMagic())) { + if (msgExt.getProperty(MessageConst.PROPERTY_TIMER_DEL_UNIQKEY) != null && tr.getDeleteList() != null) { + tr.getDeleteList().add(msgExt.getProperty(MessageConst.PROPERTY_TIMER_DEL_UNIQKEY)); + } + tr.idempotentRelease(); + doRes = true; + } else { + String uniqueKey = MessageClientIDSetter.getUniqID(msgExt); + if (null == uniqueKey) { + LOGGER.warn("No uniqueKey for msg:{}", msgExt); + } + if (null != uniqueKey && tr.getDeleteList() != null && tr.getDeleteList().size() > 0 && tr.getDeleteList().contains(uniqueKey)) { + doRes = true; + tr.idempotentRelease(); + perfCounterTicks.getCounter("dequeue_delete").flow(1); + } else { + tr.setMsg(msgExt); + while (!isStopped() && !doRes) { + doRes = dequeuePutQueue.offer(tr, 3, TimeUnit.SECONDS); + } + } + } + perfCounterTicks.getCounter("dequeue_get_msg").flow(System.currentTimeMillis() - start); + } else { + //the tr will never be processed afterwards, so idempotentRelease it + tr.idempotentRelease(); + doRes = true; + perfCounterTicks.getCounter("dequeue_get_msg_miss").flow(System.currentTimeMillis() - start); + } + } catch (Throwable e) { + LOGGER.error("Unknown exception", e); + if (storeConfig.isTimerSkipUnknownError()) { + tr.idempotentRelease(); + doRes = true; + } else { + holdMomentForUnknownError(); + } + } finally { + if (doRes) { + i++; + } + } + } + trs.clear(); + } catch (Throwable e) { + TimerMessageStore.LOGGER.error("Error occurred in " + getServiceName(), e); + } + } + TimerMessageStore.LOGGER.info(this.getServiceName() + " service end"); + setState(AbstractStateService.END); + } + } + + public class TimerDequeueWarmService extends ServiceThread { + + @Override + public String getServiceName() { + String brokerIdentifier = ""; + if (TimerMessageStore.this.messageStore instanceof DefaultMessageStore && ((DefaultMessageStore) TimerMessageStore.this.messageStore).getBrokerConfig().isInBrokerContainer()) { + brokerIdentifier = ((DefaultMessageStore) TimerMessageStore.this.messageStore).getBrokerConfig().getIdentifier(); + } + return brokerIdentifier + this.getClass().getSimpleName(); + } + + @Override + public void run() { + TimerMessageStore.LOGGER.info(this.getServiceName() + " service start"); + while (!this.isStopped()) { + try { + //if (!storeConfig.isTimerWarmEnable() || -1 == TimerMessageStore.this.warmDequeue()) { + waitForRunning(50); + //} + } catch (Throwable e) { + TimerMessageStore.LOGGER.error("Error occurred in " + getServiceName(), e); + } + } + TimerMessageStore.LOGGER.info(this.getServiceName() + " service end"); + } + } + + public boolean needRoll(int magic) { + return (magic & MAGIC_ROLL) != 0; + } + + public boolean needDelete(int magic) { + return (magic & MAGIC_DELETE) != 0; + } + + public class TimerFlushService extends ServiceThread { + private final SimpleDateFormat sdf = new SimpleDateFormat("MM-dd HH:mm:ss"); + + @Override public String getServiceName() { + String brokerIdentifier = ""; + if (TimerMessageStore.this.messageStore instanceof DefaultMessageStore && ((DefaultMessageStore) TimerMessageStore.this.messageStore).getBrokerConfig().isInBrokerContainer()) { + brokerIdentifier = ((DefaultMessageStore) TimerMessageStore.this.messageStore).getBrokerConfig().getIdentifier(); + } + return brokerIdentifier + this.getClass().getSimpleName(); + } + + private String format(long time) { + return sdf.format(new Date(time)); + } + + @Override + public void run() { + TimerMessageStore.LOGGER.info(this.getServiceName() + " service start"); + long start = System.currentTimeMillis(); + while (!this.isStopped()) { + try { + prepareTimerCheckPoint(); + timerLog.getMappedFileQueue().flush(0); + timerWheel.flush(); + timerCheckpoint.flush(); + if (System.currentTimeMillis() - start > storeConfig.getTimerProgressLogIntervalMs()) { + start = System.currentTimeMillis(); + long tmpQueueOffset = currQueueOffset; + ConsumeQueueInterface cq = messageStore.getConsumeQueue(TIMER_TOPIC, 0); + long maxOffsetInQueue = cq == null ? 0 : cq.getMaxOffsetInQueue(); + TimerMessageStore.LOGGER.info("[{}]Timer progress-check commitRead:[{}] currRead:[{}] currWrite:[{}] readBehind:{} currReadOffset:{} offsetBehind:{} behindMaster:{} " + + "enqPutQueue:{} deqGetQueue:{} deqPutQueue:{} allCongestNum:{} enqExpiredStoreTime:{}", + storeConfig.getBrokerRole(), + format(commitReadTimeMs), format(currReadTimeMs), format(currWriteTimeMs), getDequeueBehind(), + tmpQueueOffset, maxOffsetInQueue - tmpQueueOffset, timerCheckpoint.getMasterTimerQueueOffset() - tmpQueueOffset, + enqueuePutQueue.size(), dequeueGetQueue.size(), dequeuePutQueue.size(), getAllCongestNum(), format(lastEnqueueButExpiredStoreTime)); + } + timerMetrics.persist(); + waitForRunning(storeConfig.getTimerFlushIntervalMs()); + } catch (Throwable e) { + TimerMessageStore.LOGGER.error("Error occurred in " + getServiceName(), e); + } + } + TimerMessageStore.LOGGER.info(this.getServiceName() + " service end"); + } + } + + public long getAllCongestNum() { + return timerWheel.getAllNum(currReadTimeMs); + } + + public long getCongestNum(long deliverTimeMs) { + return timerWheel.getNum(deliverTimeMs); + } + + public boolean isReject(long deliverTimeMs) { + long congestNum = timerWheel.getNum(deliverTimeMs); + if (congestNum <= storeConfig.getTimerCongestNumEachSlot()) { + return false; + } + if (congestNum >= storeConfig.getTimerCongestNumEachSlot() * 2L) { + return true; + } + if (RANDOM.nextInt(1000) > 1000 * (congestNum - storeConfig.getTimerCongestNumEachSlot()) / (storeConfig.getTimerCongestNumEachSlot() + 0.1)) { + return true; + } + return false; + } + + public long getEnqueueBehindMessages() { + long tmpQueueOffset = currQueueOffset; + ConsumeQueueInterface cq = messageStore.getConsumeQueue(TIMER_TOPIC, 0); + long maxOffsetInQueue = cq == null ? 0 : cq.getMaxOffsetInQueue(); + return maxOffsetInQueue - tmpQueueOffset; + } + + public long getEnqueueBehindMillis() { + if (System.currentTimeMillis() - lastEnqueueButExpiredTime < 2000) { + return (System.currentTimeMillis() - lastEnqueueButExpiredStoreTime) / 1000; + } + return 0; + } + + public long getEnqueueBehind() { + return getEnqueueBehindMillis() / 1000; + } + + public long getDequeueBehindMessages() { + return timerWheel.getAllNum(currReadTimeMs); + } + + public long getDequeueBehindMillis() { + return System.currentTimeMillis() - currReadTimeMs; + } + + public long getDequeueBehind() { + return getDequeueBehindMillis() / 1000; + } + + public float getEnqueueTps() { + return perfCounterTicks.getCounter(ENQUEUE_PUT).getLastTps(); + } + + public float getDequeueTps() { + return perfCounterTicks.getCounter("dequeue_put").getLastTps(); + } + + public void prepareTimerCheckPoint() { + timerCheckpoint.setLastTimerLogFlushPos(timerLog.getMappedFileQueue().getFlushedWhere()); + timerCheckpoint.setLastReadTimeMs(commitReadTimeMs); + if (shouldRunningDequeue) { + timerCheckpoint.setMasterTimerQueueOffset(commitQueueOffset); + if (commitReadTimeMs != lastCommitReadTimeMs || commitQueueOffset != lastCommitQueueOffset) { + timerCheckpoint.updateDateVersion(messageStore.getStateMachineVersion()); + lastCommitReadTimeMs = commitReadTimeMs; + lastCommitQueueOffset = commitQueueOffset; + } + } + timerCheckpoint.setLastTimerQueueOffset(Math.min(commitQueueOffset, timerCheckpoint.getMasterTimerQueueOffset())); + } + + public void registerEscapeBridgeHook(Function escapeBridgeHook) { + this.escapeBridgeHook = escapeBridgeHook; + } + + public boolean isMaster() { + return BrokerRole.SLAVE != lastBrokerRole; + } + + public long getCurrReadTimeMs() { + return this.currReadTimeMs; + } + + public long getQueueOffset() { + return currQueueOffset; + } + + public long getCommitQueueOffset() { + return this.commitQueueOffset; + } + + public long getCommitReadTimeMs() { + return this.commitReadTimeMs; + } + + public MessageStore getMessageStore() { + return messageStore; + } + + public TimerWheel getTimerWheel() { + return timerWheel; + } + + public TimerLog getTimerLog() { + return timerLog; + } + + public TimerMetrics getTimerMetrics() { + return this.timerMetrics; + } + + public int getPrecisionMs() { + return precisionMs; + } + + public TimerEnqueueGetService getEnqueueGetService() { + return enqueueGetService; + } + + public void setEnqueueGetService(TimerEnqueueGetService enqueueGetService) { + this.enqueueGetService = enqueueGetService; + } + + public TimerEnqueuePutService getEnqueuePutService() { + return enqueuePutService; + } + + public void setEnqueuePutService(TimerEnqueuePutService enqueuePutService) { + this.enqueuePutService = enqueuePutService; + } + + public TimerDequeueWarmService getDequeueWarmService() { + return dequeueWarmService; + } + + public void setDequeueWarmService( + TimerDequeueWarmService dequeueWarmService) { + this.dequeueWarmService = dequeueWarmService; + } + + public TimerDequeueGetService getDequeueGetService() { + return dequeueGetService; + } + + public void setDequeueGetService(TimerDequeueGetService dequeueGetService) { + this.dequeueGetService = dequeueGetService; + } + + public TimerDequeuePutMessageService[] getDequeuePutMessageServices() { + return dequeuePutMessageServices; + } + + public void setDequeuePutMessageServices( + TimerDequeuePutMessageService[] dequeuePutMessageServices) { + this.dequeuePutMessageServices = dequeuePutMessageServices; + } + + public TimerDequeueGetMessageService[] getDequeueGetMessageServices() { + return dequeueGetMessageServices; + } + + public void setDequeueGetMessageServices( + TimerDequeueGetMessageService[] dequeueGetMessageServices) { + this.dequeueGetMessageServices = dequeueGetMessageServices; + } + + public void setTimerMetrics(TimerMetrics timerMetrics) { + this.timerMetrics = timerMetrics; + } + + public AtomicInteger getFrequency() { + return frequency; + } + + public void setFrequency(AtomicInteger frequency) { + this.frequency = frequency; + } + + public TimerCheckpoint getTimerCheckpoint() { + return timerCheckpoint; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/timer/TimerMetrics.java b/store/src/main/java/org/apache/rocketmq/store/timer/TimerMetrics.java new file mode 100644 index 00000000000..7f8fedd8a5b --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/timer/TimerMetrics.java @@ -0,0 +1,301 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.timer; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.serializer.SerializerFeature; +import com.google.common.io.Files; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import org.apache.rocketmq.common.ConfigManager; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.topic.TopicValidator; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.remoting.protocol.DataVersion; +import org.apache.rocketmq.remoting.protocol.RemotingSerializable; + +public class TimerMetrics extends ConfigManager { + private static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME); + private static final long LOCK_TIMEOUT_MILLIS = 3000; + private transient final Lock lock = new ReentrantLock(); + + private final ConcurrentMap timingCount = + new ConcurrentHashMap<>(1024); + + private final ConcurrentMap timingDistribution = + new ConcurrentHashMap<>(1024); + + public List timerDist = new ArrayList() {{ + add(5); + add(60); + add(300); // 5s, 1min, 5min + add(900); + add(3600); + add(14400); // 15min, 1h, 4h + add(28800); + add(86400); // 8h, 24h + }}; + private final DataVersion dataVersion = new DataVersion(); + + private final String configPath; + + public TimerMetrics(String configPath) { + this.configPath = configPath; + } + + public long updateDistPair(int period, int value) { + Metric distPair = getDistPair(period); + return distPair.getCount().addAndGet(value); + } + + public long addAndGet(MessageExt msg, int value) { + String topic = msg.getProperty(MessageConst.PROPERTY_REAL_TOPIC); + Metric pair = getTopicPair(topic); + getDataVersion().nextVersion(); + pair.setTimeStamp(System.currentTimeMillis()); + return pair.getCount().addAndGet(value); + } + + public Metric getDistPair(Integer period) { + Metric pair = timingDistribution.get(period); + if (null != pair) { + return pair; + } + pair = new Metric(); + final Metric previous = timingDistribution.putIfAbsent(period, pair); + if (null != previous) { + return previous; + } + return pair; + } + + public Metric getTopicPair(String topic) { + Metric pair = timingCount.get(topic); + if (null != pair) { + return pair; + } + pair = new Metric(); + final Metric previous = timingCount.putIfAbsent(topic, pair); + if (null != previous) { + return previous; + } + return pair; + } + + public List getTimerDistList() { + return this.timerDist; + } + + public void setTimerDistList(List timerDist) { + this.timerDist = timerDist; + } + + public long getTimingCount(String topic) { + Metric pair = timingCount.get(topic); + if (null == pair) { + return 0; + } else { + return pair.getCount().get(); + } + } + + public Map getTimingCount() { + return timingCount; + } + + protected void write0(Writer writer) { + TimerMetricsSerializeWrapper wrapper = new TimerMetricsSerializeWrapper(); + wrapper.setTimingCount(timingCount); + wrapper.setDataVersion(dataVersion); + JSON.writeJSONString(writer, wrapper, SerializerFeature.BrowserCompatible); + } + + @Override + public String encode() { + return encode(false); + } + + @Override + public String configFilePath() { + return configPath; + } + + @Override + public void decode(String jsonString) { + if (jsonString != null) { + TimerMetricsSerializeWrapper timerMetricsSerializeWrapper = + TimerMetricsSerializeWrapper.fromJson(jsonString, TimerMetricsSerializeWrapper.class); + if (timerMetricsSerializeWrapper != null) { + this.timingCount.putAll(timerMetricsSerializeWrapper.getTimingCount()); + this.dataVersion.assignNewOne(timerMetricsSerializeWrapper.getDataVersion()); + } + } + } + + @Override + public String encode(boolean prettyFormat) { + TimerMetricsSerializeWrapper metricsSerializeWrapper = new TimerMetricsSerializeWrapper(); + metricsSerializeWrapper.setDataVersion(this.dataVersion); + metricsSerializeWrapper.setTimingCount(this.timingCount); + return metricsSerializeWrapper.toJson(prettyFormat); + } + + public DataVersion getDataVersion() { + return dataVersion; + } + + public void cleanMetrics(Set topics) { + if (topics == null || topics.isEmpty()) { + return; + } + Iterator> iterator = timingCount.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + final String topic = entry.getKey(); + if (topic.startsWith(TopicValidator.SYSTEM_TOPIC_PREFIX)) { + continue; + } + if (topics.contains(topic)) { + continue; + } + + iterator.remove(); + log.info("clean timer metrics, because not in topic config, {}", topic); + } + } + + public static class TimerMetricsSerializeWrapper extends RemotingSerializable { + private ConcurrentMap timingCount = + new ConcurrentHashMap<>(1024); + private DataVersion dataVersion = new DataVersion(); + + public ConcurrentMap getTimingCount() { + return timingCount; + } + + public void setTimingCount( + ConcurrentMap timingCount) { + this.timingCount = timingCount; + } + + public DataVersion getDataVersion() { + return dataVersion; + } + + public void setDataVersion(DataVersion dataVersion) { + this.dataVersion = dataVersion; + } + } + + @Override + public synchronized void persist() { + String config = configFilePath(); + String temp = config + ".tmp"; + String backup = config + ".bak"; + BufferedWriter bufferedWriter = null; + try { + File tmpFile = new File(temp); + File parentDirectory = tmpFile.getParentFile(); + if (!parentDirectory.exists()) { + if (!parentDirectory.mkdirs()) { + log.error("Failed to create directory: {}", parentDirectory.getCanonicalPath()); + return; + } + } + + if (!tmpFile.exists()) { + if (!tmpFile.createNewFile()) { + log.error("Failed to create file: {}", tmpFile.getCanonicalPath()); + return; + } + } + bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(tmpFile, false), + StandardCharsets.UTF_8)); + write0(bufferedWriter); + bufferedWriter.flush(); + bufferedWriter.close(); + log.debug("Finished writing tmp file: {}", temp); + + File configFile = new File(config); + if (configFile.exists()) { + Files.copy(configFile, new File(backup)); + configFile.delete(); + } + + tmpFile.renameTo(configFile); + } catch (IOException e) { + log.error("Failed to persist {}", temp, e); + } finally { + if (null != bufferedWriter) { + try { + bufferedWriter.close(); + } catch (IOException ignore) { + } + } + } + } + + public static class Metric { + private AtomicLong count; + private long timeStamp; + + public Metric() { + count = new AtomicLong(0); + timeStamp = System.currentTimeMillis(); + } + + public AtomicLong getCount() { + return count; + } + + public void setCount(AtomicLong count) { + this.count = count; + } + + public long getTimeStamp() { + return timeStamp; + } + + public void setTimeStamp(long timeStamp) { + this.timeStamp = timeStamp; + } + + @Override + public String toString() { + return String.format("[%d,%d]", count.get(), timeStamp); + } + } + +} diff --git a/store/src/main/java/org/apache/rocketmq/store/timer/TimerRequest.java b/store/src/main/java/org/apache/rocketmq/store/timer/TimerRequest.java new file mode 100644 index 00000000000..1b25d355c65 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/timer/TimerRequest.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.timer; + +import org.apache.rocketmq.common.message.MessageExt; + +import java.util.Set; +import java.util.concurrent.CountDownLatch; + +public class TimerRequest { + + private final long offsetPy; + private final int sizePy; + private final long delayTime; + + private final int magic; + + private long enqueueTime; + private MessageExt msg; + + + //optional would be a good choice, but it relies on JDK 8 + private CountDownLatch latch; + + private boolean released; + + //whether the operation is successful + private boolean succ; + + private Set deleteList; + + public TimerRequest(long offsetPy, int sizePy, long delayTime, long enqueueTime, int magic) { + this(offsetPy, sizePy, delayTime, enqueueTime, magic, null); + } + + public TimerRequest(long offsetPy, int sizePy, long delayTime, long enqueueTime, int magic, MessageExt msg) { + this.offsetPy = offsetPy; + this.sizePy = sizePy; + this.delayTime = delayTime; + this.enqueueTime = enqueueTime; + this.magic = magic; + this.msg = msg; + } + + public long getOffsetPy() { + return offsetPy; + } + + public int getSizePy() { + return sizePy; + } + + public long getDelayTime() { + return delayTime; + } + + public long getEnqueueTime() { + return enqueueTime; + } + + public MessageExt getMsg() { + return msg; + } + + public void setMsg(MessageExt msg) { + this.msg = msg; + } + + public int getMagic() { + return magic; + } + + public Set getDeleteList() { + return deleteList; + } + + public void setDeleteList(Set deleteList) { + this.deleteList = deleteList; + } + + public void setLatch(CountDownLatch latch) { + this.latch = latch; + } + public void setEnqueueTime(long enqueueTime) { + this.enqueueTime = enqueueTime; + } + public void idempotentRelease() { + idempotentRelease(true); + } + + public void idempotentRelease(boolean succ) { + this.succ = succ; + if (!released && latch != null) { + released = true; + latch.countDown(); + } + } + + public boolean isSucc() { + return succ; + } + + @Override + public String toString() { + return "TimerRequest{" + + "offsetPy=" + offsetPy + + ", sizePy=" + sizePy + + ", delayTime=" + delayTime + + ", enqueueTime=" + enqueueTime + + ", magic=" + magic + + ", msg=" + msg + + ", latch=" + latch + + ", released=" + released + + ", succ=" + succ + + ", deleteList=" + deleteList + + '}'; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/timer/TimerWheel.java b/store/src/main/java/org/apache/rocketmq/store/timer/TimerWheel.java new file mode 100644 index 00000000000..70f82998bc9 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/timer/TimerWheel.java @@ -0,0 +1,209 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.timer; + +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.constant.LoggerName; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; + +public class TimerWheel { + + private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME); + public static final int BLANK = -1, IGNORE = -2; + public final int slotsTotal; + public final int precisionMs; + private String fileName; + private final RandomAccessFile randomAccessFile; + private final FileChannel fileChannel; + private final MappedByteBuffer mappedByteBuffer; + private final ByteBuffer byteBuffer; + private final ThreadLocal localBuffer = new ThreadLocal() { + @Override + protected ByteBuffer initialValue() { + return byteBuffer.duplicate(); + } + }; + private final int wheelLength; + + public TimerWheel(String fileName, int slotsTotal, int precisionMs) throws IOException { + this.slotsTotal = slotsTotal; + this.precisionMs = precisionMs; + this.fileName = fileName; + this.wheelLength = this.slotsTotal * 2 * Slot.SIZE; + + File file = new File(fileName); + UtilAll.ensureDirOK(file.getParent()); + + try { + randomAccessFile = new RandomAccessFile(this.fileName, "rw"); + if (file.exists() && randomAccessFile.length() != 0 && + randomAccessFile.length() != wheelLength) { + throw new RuntimeException(String.format("Timer wheel length:%d != expected:%s", + randomAccessFile.length(), wheelLength)); + } + randomAccessFile.setLength(wheelLength); + fileChannel = randomAccessFile.getChannel(); + mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, wheelLength); + assert wheelLength == mappedByteBuffer.remaining(); + this.byteBuffer = ByteBuffer.allocateDirect(wheelLength); + this.byteBuffer.put(mappedByteBuffer); + } catch (FileNotFoundException e) { + log.error("create file channel " + this.fileName + " Failed. ", e); + throw e; + } catch (IOException e) { + log.error("map file " + this.fileName + " Failed. ", e); + throw e; + } + } + + public void shutdown() { + shutdown(true); + } + + public void shutdown(boolean flush) { + if (flush) + this.flush(); + + // unmap mappedByteBuffer + UtilAll.cleanBuffer(this.mappedByteBuffer); + UtilAll.cleanBuffer(this.byteBuffer); + + try { + this.fileChannel.close(); + } catch (IOException e) { + log.error("Shutdown error in timer wheel", e); + } + } + + public void flush() { + ByteBuffer bf = localBuffer.get(); + bf.position(0); + bf.limit(wheelLength); + mappedByteBuffer.position(0); + mappedByteBuffer.limit(wheelLength); + for (int i = 0; i < wheelLength; i++) { + if (bf.get(i) != mappedByteBuffer.get(i)) { + mappedByteBuffer.put(i, bf.get(i)); + } + } + this.mappedByteBuffer.force(); + } + + public Slot getSlot(long timeMs) { + Slot slot = getRawSlot(timeMs); + if (slot.timeMs != timeMs / precisionMs * precisionMs) { + return new Slot(-1, -1, -1); + } + return slot; + } + + //testable + public Slot getRawSlot(long timeMs) { + localBuffer.get().position(getSlotIndex(timeMs) * Slot.SIZE); + return new Slot(localBuffer.get().getLong() * precisionMs, + localBuffer.get().getLong(), localBuffer.get().getLong(), localBuffer.get().getInt(), localBuffer.get().getInt()); + } + + public int getSlotIndex(long timeMs) { + return (int) (timeMs / precisionMs % (slotsTotal * 2)); + } + + public void putSlot(long timeMs, long firstPos, long lastPos) { + localBuffer.get().position(getSlotIndex(timeMs) * Slot.SIZE); + // To be compatible with previous version. + // The previous version's precision is fixed at 1000ms and it store timeMs / 1000 in slot. + localBuffer.get().putLong(timeMs / precisionMs); + localBuffer.get().putLong(firstPos); + localBuffer.get().putLong(lastPos); + } + public void putSlot(long timeMs, long firstPos, long lastPos, int num, int magic) { + localBuffer.get().position(getSlotIndex(timeMs) * Slot.SIZE); + localBuffer.get().putLong(timeMs / precisionMs); + localBuffer.get().putLong(firstPos); + localBuffer.get().putLong(lastPos); + localBuffer.get().putInt(num); + localBuffer.get().putInt(magic); + } + + public void reviseSlot(long timeMs, long firstPos, long lastPos, boolean force) { + localBuffer.get().position(getSlotIndex(timeMs) * Slot.SIZE); + + if (timeMs / precisionMs != localBuffer.get().getLong()) { + if (force) { + putSlot(timeMs, firstPos != IGNORE ? firstPos : lastPos, lastPos); + } + } else { + if (IGNORE != firstPos) { + localBuffer.get().putLong(firstPos); + } else { + localBuffer.get().getLong(); + } + if (IGNORE != lastPos) { + localBuffer.get().putLong(lastPos); + } + } + } + + //check the timerwheel to see if its stored offset > maxOffset in timerlog + public long checkPhyPos(long timeStartMs, long maxOffset) { + long minFirst = Long.MAX_VALUE; + int firstSlotIndex = getSlotIndex(timeStartMs); + for (int i = 0; i < slotsTotal * 2; i++) { + int slotIndex = (firstSlotIndex + i) % (slotsTotal * 2); + localBuffer.get().position(slotIndex * Slot.SIZE); + if ((timeStartMs + i * precisionMs) / precisionMs != localBuffer.get().getLong()) { + continue; + } + long first = localBuffer.get().getLong(); + long last = localBuffer.get().getLong(); + if (last > maxOffset) { + if (first < minFirst) { + minFirst = first; + } + } + } + return minFirst; + } + + public long getNum(long timeMs) { + return getSlot(timeMs).num; + } + + public long getAllNum(long timeStartMs) { + int allNum = 0; + int firstSlotIndex = getSlotIndex(timeStartMs); + for (int i = 0; i < slotsTotal * 2; i++) { + int slotIndex = (firstSlotIndex + i) % (slotsTotal * 2); + localBuffer.get().position(slotIndex * Slot.SIZE); + if ((timeStartMs + i * precisionMs) / precisionMs == localBuffer.get().getLong()) { + localBuffer.get().getLong(); //first pos + localBuffer.get().getLong(); //last pos + allNum = allNum + localBuffer.get().getInt(); + } + } + return allNum; + } +} diff --git a/store/src/main/java/org/apache/rocketmq/store/util/LibC.java b/store/src/main/java/org/apache/rocketmq/store/util/LibC.java index 8eaa44ab461..4c8e7d45385 100644 --- a/store/src/main/java/org/apache/rocketmq/store/util/LibC.java +++ b/store/src/main/java/org/apache/rocketmq/store/util/LibC.java @@ -25,6 +25,8 @@ public interface LibC extends Library { LibC INSTANCE = (LibC) Native.loadLibrary(Platform.isWindows() ? "msvcrt" : "c", LibC.class); + int MADV_NORMAL = 0; + int MADV_RANDOM = 1; int MADV_WILLNEED = 3; int MADV_DONTNEED = 4; @@ -50,4 +52,8 @@ public interface LibC extends Library { int mlockall(int flags); int msync(Pointer p, NativeLong length, int flags); + + int mincore(Pointer p, NativeLong length, byte[] vec); + + int getpagesize(); } diff --git a/store/src/main/java/org/apache/rocketmq/store/util/PerfCounter.java b/store/src/main/java/org/apache/rocketmq/store/util/PerfCounter.java new file mode 100644 index 00000000000..e2a55d63994 --- /dev/null +++ b/store/src/main/java/org/apache/rocketmq/store/util/PerfCounter.java @@ -0,0 +1,370 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.util; + +import org.apache.rocketmq.common.ServiceThread; +import org.apache.rocketmq.logging.org.slf4j.Logger; + +import java.sql.Timestamp; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class PerfCounter { + + private long last = System.currentTimeMillis(); + private float lastTps = 0.0f; + + private final ThreadLocal lastTickMs = new ThreadLocal() { + @Override + protected AtomicLong initialValue() { + return new AtomicLong(System.currentTimeMillis()); + } + }; + + private final Logger logger; + private String prefix = "DEFAULT"; + + public float getLastTps() { + if (System.currentTimeMillis() - last <= maxTimeMsPerCount + 3000) { + return lastTps; + } + return 0.0f; + } + + //1000 * ms, 1000 * 10 ms, then 100ms every slots + private final AtomicInteger[] count; + private final AtomicLong allCount; + private final int maxNumPerCount; + private final int maxTimeMsPerCount; + + + public PerfCounter() { + this(5001, null, null, 1000 * 1000, 10 * 1000); + } + + public PerfCounter(int slots, Logger logger, String prefix, int maxNumPerCount, int maxTimeMsPerCount) { + if (slots < 3000) { + throw new RuntimeException("slots must bigger than 3000, but:%s" + slots); + } + count = new AtomicInteger[slots]; + allCount = new AtomicLong(0); + this.logger = logger; + if (prefix != null) { + this.prefix = prefix; + } + this.maxNumPerCount = maxNumPerCount; + this.maxTimeMsPerCount = maxTimeMsPerCount; + reset(); + } + + public void flow(long cost) { + flow(cost, 1); + } + + public void flow(long cost, int num) { + if (cost < 0) return; + allCount.addAndGet(num); + count[getIndex(cost)].addAndGet(num); + if (allCount.get() >= maxNumPerCount + || System.currentTimeMillis() - last >= maxTimeMsPerCount) { + synchronized (allCount) { + if (allCount.get() < maxNumPerCount + && System.currentTimeMillis() - last < maxTimeMsPerCount) { + return; + } + print(); + this.reset(); + } + } + } + + public void print() { + int min = this.getMin(); + int max = this.getMax(); + int tp50 = this.getTPValue(0.5f); + int tp80 = this.getTPValue(0.8f); + int tp90 = this.getTPValue(0.9f); + int tp99 = this.getTPValue(0.99f); + int tp999 = this.getTPValue(0.999f); + long count0t1 = this.getCount(0, 1); + long count2t5 = this.getCount(2, 5); + long count6t10 = this.getCount(6, 10); + long count11t50 = this.getCount(11, 50); + long count51t100 = this.getCount(51, 100); + long count101t500 = this.getCount(101, 500); + long count501t999 = this.getCount(501, 999); + long count1000t = this.getCount(1000, 100000000); + long elapsed = System.currentTimeMillis() - last; + lastTps = (allCount.get() + 0.1f) * 1000 / elapsed; + String str = String.format("PERF_COUNTER_%s[%s] num:%d cost:%d tps:%.4f min:%d max:%d tp50:%d tp80:%d tp90:%d tp99:%d tp999:%d " + + "0_1:%d 2_5:%d 6_10:%d 11_50:%d 51_100:%d 101_500:%d 501_999:%d 1000_:%d", + prefix, new Timestamp(System.currentTimeMillis()), allCount.get(), elapsed, lastTps, + min, max, tp50, tp80, tp90, tp99, tp999, + count0t1, count2t5, count6t10, count11t50, count51t100, count101t500, count501t999, count1000t); + if (logger != null) { + logger.info(str); + } + } + + private int getIndex(long cost) { + if (cost < 1000) { + return (int) cost; + } + if (cost >= 1000 && cost < 1000 + 1000 * 10) { + int units = (int) ((cost - 1000) / 10); + return 1000 + units; + } + int units = (int) ((cost - 1000 - 1000 * 10) / 100); + units = 2000 + units; + if (units >= count.length) { + units = count.length - 1; + } + return units; + } + + private int convert(int index) { + if (index < 1000) { + return index; + } else if (index >= 1000 && index < 2000) { + return (index - 1000) * 10 + 1000; + } else { + return (index - 2000) * 100 + 1000 * 10 + 1000; + } + } + + public float getRate(int from, int to) { + long tmp = getCount(from, to); + return ((tmp + 0.0f) * 100) / (allCount.get() + 1); + } + + public long getCount(int from, int to) { + from = getIndex(from); + to = getIndex(to); + long tmp = 0; + for (int i = from; i <= to && i < count.length; i++) { + tmp = tmp + count[i].get(); + } + return tmp; + } + + public int getTPValue(float ratio) { + if (ratio <= 0 || ratio >= 1) { + ratio = 0.99f; + } + long num = (long) (allCount.get() * (1 - ratio)); + int tmp = 0; + for (int i = count.length - 1; i > 0; i--) { + tmp += count[i].get(); + if (tmp > num) { + return convert(i); + } + } + return 0; + } + + public int getMin() { + for (int i = 0; i < count.length; i++) { + if (count[i].get() > 0) { + return convert(i); + } + } + return 0; + } + + public int getMax() { + for (int i = count.length - 1; i > 0; i--) { + if (count[i].get() > 0) { + return convert(i); + } + } + return 99999999; + } + + public void reset() { + for (int i = 0; i < count.length; i++) { + if (count[i] == null) { + count[i] = new AtomicInteger(0); + } else { + count[i].set(0); + } + } + allCount.set(0); + last = System.currentTimeMillis(); + } + + public void startTick() { + lastTickMs.get().set(System.currentTimeMillis()); + } + + public void endTick() { + flow(System.currentTimeMillis() - lastTickMs.get().get()); + } + + public static class Ticks extends ServiceThread { + private final Logger logger; + private final Map perfs = new ConcurrentHashMap<>(); + private final Map keyFreqs = new ConcurrentHashMap<>(); + private final PerfCounter defaultPerf; + private final AtomicLong defaultTime = new AtomicLong(System.currentTimeMillis()); + + private final int maxKeyNumPerf; + private final int maxKeyNumDebug; + + private final int maxNumPerCount; + private final int maxTimeMsPerCount; + + public Ticks() { + this(null, 1000 * 1000, 10 * 1000, 20 * 1000, 100 * 1000); + } + + public Ticks(Logger logger) { + this(logger, 1000 * 1000, 10 * 1000, 20 * 1000, 100 * 1000); + } + + @Override + public String getServiceName() { + return this.getClass().getName(); + } + + public Ticks(Logger logger, int maxNumPerCount, int maxTimeMsPerCount, int maxKeyNumPerf, int maxKeyNumDebug) { + this.logger = logger; + this.maxNumPerCount = maxNumPerCount; + this.maxTimeMsPerCount = maxTimeMsPerCount; + this.maxKeyNumPerf = maxKeyNumPerf; + this.maxKeyNumDebug = maxKeyNumDebug; + this.defaultPerf = new PerfCounter(3001, logger, null, maxNumPerCount, maxTimeMsPerCount); + + } + + private PerfCounter makeSureExists(String key) { + if (perfs.get(key) == null) { + if (perfs.size() >= maxKeyNumPerf + 100) { + return defaultPerf; + } + perfs.put(key, new PerfCounter(3001, logger, key, maxNumPerCount, maxTimeMsPerCount)); + } + return perfs.getOrDefault(key, defaultPerf); + } + + public void startTick(String key) { + try { + makeSureExists(key).startTick(); + } catch (Throwable ignored) { + + } + } + + public void endTick(String key) { + try { + makeSureExists(key).endTick(); + } catch (Throwable ignored) { + + } + } + + public void flowOnce(String key, int cost) { + try { + makeSureExists(key).flow(cost); + } catch (Throwable ignored) { + + } + } + + public PerfCounter getCounter(String key) { + try { + return makeSureExists(key); + } catch (Throwable ignored) { + return defaultPerf; + } + } + + private AtomicLong makeSureDebugKeyExists(String key) { + AtomicLong lastTimeMs = keyFreqs.get(key); + if (null == lastTimeMs) { + if (keyFreqs.size() >= maxKeyNumDebug + 100) { + return defaultTime; + } + lastTimeMs = new AtomicLong(0); + keyFreqs.put(key, lastTimeMs); + } + return keyFreqs.getOrDefault(key, defaultTime); + } + public boolean shouldDebugKeyAndTimeMs(String key, int intervalMs) { + try { + AtomicLong lastTimeMs = makeSureDebugKeyExists(key); + if (System.currentTimeMillis() - lastTimeMs.get() > intervalMs) { + lastTimeMs.set(System.currentTimeMillis()); + return true; + } + return false; + } catch (Throwable ignored) { + + } + return false; + } + + @Override + public void run() { + logger.info("{} get started", getServiceName()); + while (!this.isStopped()) { + try { + long maxLiveTimeMs = maxTimeMsPerCount * 2 + 1000; + this.waitForRunning(maxLiveTimeMs); + if (perfs.size() >= maxKeyNumPerf + || keyFreqs.size() >= maxKeyNumDebug) { + logger.warn("The key is full {}-{} {}-{}", perfs.size(), maxKeyNumPerf, keyFreqs.size(), maxKeyNumDebug); + } + { + Iterator> it = perfs.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + PerfCounter value = entry.getValue(); + // May have concurrency problem, but it has no effect, we can ignore it. + if (System.currentTimeMillis() - value.last > maxLiveTimeMs) { + it.remove(); + } + } + } + + { + Iterator> it = keyFreqs.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + AtomicLong value = entry.getValue(); + // May have concurrency problem, but it has no effect, we can ignore it. + if (System.currentTimeMillis() - value.get() > maxLiveTimeMs) { + it.remove(); + } + } + } + + } catch (Exception e) { + logger.error("{} get unknown errror", getServiceName(), e); + try { + Thread.sleep(1000); + } catch (Throwable ignored) { + + } + } + } + logger.info("{} get stopped", getServiceName()); + } + } +} diff --git a/store/src/test/java/org/apache/rocketmq/store/AppendCallbackTest.java b/store/src/test/java/org/apache/rocketmq/store/AppendCallbackTest.java index 7f88d36e9a7..3748571496d 100644 --- a/store/src/test/java/org/apache/rocketmq/store/AppendCallbackTest.java +++ b/store/src/test/java/org/apache/rocketmq/store/AppendCallbackTest.java @@ -25,6 +25,8 @@ import java.util.List; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.common.BrokerConfig; import org.apache.rocketmq.common.UtilAll; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageDecoder; @@ -42,26 +44,28 @@ public class AppendCallbackTest { AppendMessageCallback callback; - CommitLog.MessageExtBatchEncoder batchEncoder = new CommitLog.MessageExtBatchEncoder(10 * 1024 * 1024); + MessageExtEncoder batchEncoder; @Before public void init() throws Exception { MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); - messageStoreConfig.setMapedFileSizeCommitLog(1024 * 8); - messageStoreConfig.setMapedFileSizeConsumeQueue(1024 * 4); + messageStoreConfig.setMappedFileSizeCommitLog(1024 * 8); + messageStoreConfig.setMappedFileSizeConsumeQueue(1024 * 4); messageStoreConfig.setMaxHashSlotNum(100); messageStoreConfig.setMaxIndexNum(100 * 10); - messageStoreConfig.setStorePathRootDir(System.getProperty("user.home") + File.separator + "unitteststore"); - messageStoreConfig.setStorePathCommitLog(System.getProperty("user.home") + File.separator + "unitteststore" + File.separator + "commitlog"); + messageStoreConfig.setMaxMessageSize(10 * 1024 * 1024); + messageStoreConfig.setStorePathRootDir(System.getProperty("java.io.tmpdir") + File.separator + "unitteststore"); + messageStoreConfig.setStorePathCommitLog(System.getProperty("java.io.tmpdir") + File.separator + "unitteststore" + File.separator + "commitlog"); //too much reference - DefaultMessageStore messageStore = new DefaultMessageStore(messageStoreConfig, null, null, null); + DefaultMessageStore messageStore = new DefaultMessageStore(messageStoreConfig, null, null, new BrokerConfig(), new ConcurrentHashMap<>()); CommitLog commitLog = new CommitLog(messageStore); - callback = commitLog.new DefaultAppendMessageCallback(1024); + callback = commitLog.new DefaultAppendMessageCallback(messageStoreConfig); + batchEncoder = new MessageExtEncoder(messageStoreConfig); } @After public void destroy() { - UtilAll.deleteFile(new File(System.getProperty("user.home") + File.separator + "unitteststore")); + UtilAll.deleteFile(new File(System.getProperty("java.io.tmpdir") + File.separator + "unitteststore")); } @Test @@ -84,10 +88,51 @@ public void testAppendMessageBatchEndOfFile() throws Exception { messageExtBatch.setStoreHost(new InetSocketAddress("127.0.0.1", 124)); messageExtBatch.setBody(MessageDecoder.encodeMessages(messages)); - messageExtBatch.setEncodedBuff(batchEncoder.encode(messageExtBatch)); + PutMessageContext putMessageContext = new PutMessageContext(topic + "-" + queue); + messageExtBatch.setEncodedBuff(batchEncoder.encode(messageExtBatch, putMessageContext)); ByteBuffer buff = ByteBuffer.allocate(1024 * 10); //encounter end of file when append half of the data - AppendMessageResult result = callback.doAppend(0, buff, 1000, messageExtBatch); + AppendMessageResult result = + callback.doAppend(0, buff, 1000, messageExtBatch, putMessageContext); + assertEquals(AppendMessageStatus.END_OF_FILE, result.getStatus()); + assertEquals(0, result.getWroteOffset()); + assertEquals(0, result.getLogicsOffset()); + assertEquals(1000, result.getWroteBytes()); + assertEquals(8, buff.position()); //write blank size and magic value + + assertTrue(result.getMsgId().length() > 0); //should have already constructed some message ids + } + + @Test + public void testAppendIPv6HostMessageBatchEndOfFile() throws Exception { + List messages = new ArrayList<>(); + String topic = "test-topic"; + int queue = 0; + for (int i = 0; i < 10; i++) { + Message msg = new Message(); + msg.setBody("body".getBytes()); + msg.setTopic(topic); + msg.setTags("abc"); + messages.add(msg); + } + MessageExtBatch messageExtBatch = new MessageExtBatch(); + messageExtBatch.setTopic(topic); + messageExtBatch.setQueueId(queue); + messageExtBatch.setBornTimestamp(System.currentTimeMillis()); + messageExtBatch.setMsgId("24084004018081003FAA1DDE2B3F898A00002A9F0000000000000CA0"); + messageExtBatch.setSysFlag(0); + messageExtBatch.setBornHostV6Flag(); + messageExtBatch.setStoreHostAddressV6Flag(); + messageExtBatch.setBornHost(new InetSocketAddress("1050:0000:0000:0000:0005:0600:300c:326b", 123)); + messageExtBatch.setStoreHost(new InetSocketAddress("::1", 124)); + messageExtBatch.setBody(MessageDecoder.encodeMessages(messages)); + + PutMessageContext putMessageContext = new PutMessageContext(topic + "-" + queue); + messageExtBatch.setEncodedBuff(batchEncoder.encode(messageExtBatch, putMessageContext)); + ByteBuffer buff = ByteBuffer.allocate(1024 * 10); + //encounter end of file when append half of the data + AppendMessageResult result = + callback.doAppend(0, buff, 1000, messageExtBatch, putMessageContext); assertEquals(AppendMessageStatus.END_OF_FILE, result.getStatus()); assertEquals(0, result.getWroteOffset()); assertEquals(0, result.getLogicsOffset()); @@ -117,9 +162,11 @@ public void testAppendMessageBatchSucc() throws Exception { messageExtBatch.setStoreHost(new InetSocketAddress("127.0.0.1", 124)); messageExtBatch.setBody(MessageDecoder.encodeMessages(messages)); - messageExtBatch.setEncodedBuff(batchEncoder.encode(messageExtBatch)); + PutMessageContext putMessageContext = new PutMessageContext(topic + "-" + queue); + messageExtBatch.setEncodedBuff(batchEncoder.encode(messageExtBatch, putMessageContext)); ByteBuffer buff = ByteBuffer.allocate(1024 * 10); - AppendMessageResult allresult = callback.doAppend(0, buff, 1024 * 10, messageExtBatch); + AppendMessageResult allresult = + callback.doAppend(0, buff, 1024 * 10, messageExtBatch, putMessageContext); assertEquals(AppendMessageStatus.PUT_OK, allresult.getStatus()); assertEquals(0, allresult.getWroteOffset()); @@ -153,4 +200,66 @@ public void testAppendMessageBatchSucc() throws Exception { } + @Test + public void testAppendIPv6HostMessageBatchSucc() throws Exception { + List messages = new ArrayList<>(); + String topic = "test-topic"; + int queue = 0; + for (int i = 0; i < 10; i++) { + Message msg = new Message(); + msg.setBody("body".getBytes()); + msg.setTopic(topic); + msg.setTags("abc"); + messages.add(msg); + } + MessageExtBatch messageExtBatch = new MessageExtBatch(); + messageExtBatch.setTopic(topic); + messageExtBatch.setQueueId(queue); + messageExtBatch.setBornTimestamp(System.currentTimeMillis()); + messageExtBatch.setMsgId("24084004018081003FAA1DDE2B3F898A00002A9F0000000000000CA0"); + messageExtBatch.setSysFlag(0); + messageExtBatch.setBornHostV6Flag(); + messageExtBatch.setStoreHostAddressV6Flag(); + messageExtBatch.setBornHost(new InetSocketAddress("1050:0000:0000:0000:0005:0600:300c:326b", 123)); + messageExtBatch.setStoreHost(new InetSocketAddress("::1", 124)); + messageExtBatch.setBody(MessageDecoder.encodeMessages(messages)); + + PutMessageContext putMessageContext = new PutMessageContext(topic + "-" + queue); + messageExtBatch.setEncodedBuff(batchEncoder.encode(messageExtBatch, putMessageContext)); + ByteBuffer buff = ByteBuffer.allocate(1024 * 10); + AppendMessageResult allresult = + callback.doAppend(0, buff, 1024 * 10, messageExtBatch, putMessageContext); + + assertEquals(AppendMessageStatus.PUT_OK, allresult.getStatus()); + assertEquals(0, allresult.getWroteOffset()); + assertEquals(0, allresult.getLogicsOffset()); + assertEquals(buff.position(), allresult.getWroteBytes()); + + assertEquals(messages.size(), allresult.getMsgNum()); + + Set msgIds = new HashSet<>(); + for (String msgId : allresult.getMsgId().split(",")) { + assertEquals(56, msgId.length()); + msgIds.add(msgId); + } + assertEquals(messages.size(), msgIds.size()); + + List decodeMsgs = MessageDecoder.decodes((ByteBuffer) buff.flip()); + assertEquals(decodeMsgs.size(), decodeMsgs.size()); + long queueOffset = decodeMsgs.get(0).getQueueOffset(); + long storeTimeStamp = decodeMsgs.get(0).getStoreTimestamp(); + for (int i = 0; i < messages.size(); i++) { + assertEquals(messages.get(i).getTopic(), decodeMsgs.get(i).getTopic()); + assertEquals(new String(messages.get(i).getBody()), new String(decodeMsgs.get(i).getBody())); + assertEquals(messages.get(i).getTags(), decodeMsgs.get(i).getTags()); + + assertEquals(messageExtBatch.getBornHostNameString(), decodeMsgs.get(i).getBornHostNameString()); + + assertEquals(messageExtBatch.getBornTimestamp(), decodeMsgs.get(i).getBornTimestamp()); + assertEquals(storeTimeStamp, decodeMsgs.get(i).getStoreTimestamp()); + assertEquals(queueOffset++, decodeMsgs.get(i).getQueueOffset()); + } + + } + } diff --git a/store/src/test/java/org/apache/rocketmq/store/AppendPropCRCTest.java b/store/src/test/java/org/apache/rocketmq/store/AppendPropCRCTest.java new file mode 100644 index 00000000000..d882fc9d9ba --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/AppendPropCRCTest.java @@ -0,0 +1,201 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store; + +import java.io.File; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBatch; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class AppendPropCRCTest { + + AppendMessageCallback callback; + + MessageExtEncoder encoder; + + CommitLog commitLog; + + @Before + public void init() throws Exception { + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setMappedFileSizeCommitLog(1024 * 8); + messageStoreConfig.setMappedFileSizeConsumeQueue(1024 * 4); + messageStoreConfig.setMaxHashSlotNum(100); + messageStoreConfig.setMaxIndexNum(100 * 10); + messageStoreConfig.setMaxMessageSize(10 * 1024 * 1024); + messageStoreConfig.setStorePathRootDir(System.getProperty("java.io.tmpdir") + File.separator + "unitteststore"); + messageStoreConfig.setStorePathCommitLog(System.getProperty("java.io.tmpdir") + File.separator + "unitteststore" + File.separator + "commitlog"); + messageStoreConfig.setForceVerifyPropCRC(true); + messageStoreConfig.setEnabledAppendPropCRC(true); + //too much reference + DefaultMessageStore messageStore = new DefaultMessageStore(messageStoreConfig, null, null, new BrokerConfig(), new ConcurrentHashMap<>()); + commitLog = new CommitLog(messageStore); + encoder = new MessageExtEncoder(messageStoreConfig); + callback = commitLog.new DefaultAppendMessageCallback(messageStoreConfig); + } + + @After + public void destroy() { + UtilAll.deleteFile(new File(System.getProperty("user.home") + File.separator + "unitteststore")); + } + + @Test + public void testAppendMessageSucc() throws Exception { + String topic = "test-topic"; + int queue = 0; + int msgNum = 10; + int propertiesLen = 0; + Message msg = new Message(); + msg.setBody("body".getBytes()); + msg.setTopic(topic); + msg.setTags("abc"); + msg.putUserProperty("a", "aaaaaaaa"); + msg.putUserProperty("b", "bbbbbbbb"); + msg.putUserProperty("c", "cccccccc"); + msg.putUserProperty("d", "dddddddd"); + msg.putUserProperty("e", "eeeeeeee"); + msg.putUserProperty("f", "ffffffff"); + + MessageExtBrokerInner messageExtBrokerInner = new MessageExtBrokerInner(); + messageExtBrokerInner.setTopic(topic); + messageExtBrokerInner.setQueueId(queue); + messageExtBrokerInner.setBornTimestamp(System.currentTimeMillis()); + messageExtBrokerInner.setBornHost(new InetSocketAddress("127.0.0.1", 123)); + messageExtBrokerInner.setStoreHost(new InetSocketAddress("127.0.0.1", 124)); + messageExtBrokerInner.setBody(msg.getBody()); + messageExtBrokerInner.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); + propertiesLen = messageExtBrokerInner.getPropertiesString().length(); + + ByteBuffer buff = ByteBuffer.allocate(1024 * 10); + for (int i = 0; i < msgNum; i++) { + encoder.encode(messageExtBrokerInner); + messageExtBrokerInner.setEncodedBuff(encoder.getEncoderBuffer()); + AppendMessageResult allresult = callback.doAppend(0, buff, 1024 * 10, messageExtBrokerInner, null); + assertEquals(AppendMessageStatus.PUT_OK, allresult.getStatus()); + } + // Expected to pass when message is not modified + buff.flip(); + for (int i = 0; i < msgNum - 1; i++) { + DispatchRequest request = commitLog.checkMessageAndReturnSize(buff, true, false); + assertTrue(request.isSuccess()); + } + // Modify the properties of the last message and expect the verification to fail. + int idx = buff.limit() - (propertiesLen / 2); + buff.put(idx, (byte) (buff.get(idx) + 1)); + DispatchRequest request = commitLog.checkMessageAndReturnSize(buff, true, false); + assertFalse(request.isSuccess()); + } + + @Test + public void testAppendMessageBatchSucc() throws Exception { + List messages = new ArrayList<>(); + String topic = "test-topic"; + int queue = 0; + int propertiesLen = 0; + for (int i = 0; i < 10; i++) { + Message msg = new Message(); + msg.setBody("body".getBytes()); + msg.setTopic(topic); + msg.setTags("abc"); + msg.putUserProperty("a", "aaaaaaaa"); + msg.putUserProperty("b", "bbbbbbbb"); + msg.putUserProperty("c", "cccccccc"); + msg.putUserProperty("d", "dddddddd"); + msg.putUserProperty("e", "eeeeeeee"); + msg.putUserProperty("f", "ffffffff"); + String propertiesString = MessageDecoder.messageProperties2String(msg.getProperties()); + propertiesLen = propertiesString.length(); + messages.add(msg); + } + MessageExtBatch messageExtBatch = new MessageExtBatch(); + messageExtBatch.setTopic(topic); + messageExtBatch.setQueueId(queue); + messageExtBatch.setBornTimestamp(System.currentTimeMillis()); + messageExtBatch.setBornHost(new InetSocketAddress("127.0.0.1", 123)); + messageExtBatch.setStoreHost(new InetSocketAddress("127.0.0.1", 124)); + messageExtBatch.setBody(MessageDecoder.encodeMessages(messages)); + + PutMessageContext putMessageContext = new PutMessageContext(topic + "-" + queue); + messageExtBatch.setEncodedBuff(encoder.encode(messageExtBatch, putMessageContext)); + ByteBuffer buff = ByteBuffer.allocate(1024 * 10); + //encounter end of file when append half of the data + AppendMessageResult allresult = + callback.doAppend(0, buff, 1024 * 10, messageExtBatch, putMessageContext); + + assertEquals(AppendMessageStatus.PUT_OK, allresult.getStatus()); + assertEquals(0, allresult.getWroteOffset()); + assertEquals(0, allresult.getLogicsOffset()); + assertEquals(buff.position(), allresult.getWroteBytes()); + + assertEquals(messages.size(), allresult.getMsgNum()); + + Set msgIds = new HashSet<>(); + for (String msgId : allresult.getMsgId().split(",")) { + assertEquals(32, msgId.length()); + msgIds.add(msgId); + } + assertEquals(messages.size(), msgIds.size()); + + List decodeMsgs = MessageDecoder.decodes((ByteBuffer) buff.flip()); + assertEquals(decodeMsgs.size(), decodeMsgs.size()); + long queueOffset = decodeMsgs.get(0).getQueueOffset(); + long storeTimeStamp = decodeMsgs.get(0).getStoreTimestamp(); + for (int i = 0; i < messages.size(); i++) { + assertEquals(messages.get(i).getTopic(), decodeMsgs.get(i).getTopic()); + assertEquals(new String(messages.get(i).getBody()), new String(decodeMsgs.get(i).getBody())); + assertEquals(messages.get(i).getTags(), decodeMsgs.get(i).getTags()); + + assertEquals(messageExtBatch.getBornHostNameString(), decodeMsgs.get(i).getBornHostNameString()); + + assertEquals(messageExtBatch.getBornTimestamp(), decodeMsgs.get(i).getBornTimestamp()); + assertEquals(storeTimeStamp, decodeMsgs.get(i).getStoreTimestamp()); + assertEquals(queueOffset++, decodeMsgs.get(i).getQueueOffset()); + } + + // Expected to pass when message is not modified + buff.flip(); + for (int i = 0; i < messages.size() - 1; i++) { + DispatchRequest request = commitLog.checkMessageAndReturnSize(buff, true, false); + assertTrue(request.isSuccess()); + } + // Modify the properties of the last message and expect the verification to fail. + int idx = buff.limit() - (propertiesLen / 2); + buff.put(idx, (byte) (buff.get(idx) + 1)); + DispatchRequest request = commitLog.checkMessageAndReturnSize(buff, true, false); + assertFalse(request.isSuccess()); + } +} diff --git a/store/src/test/java/org/apache/rocketmq/store/BatchPutMessageTest.java b/store/src/test/java/org/apache/rocketmq/store/BatchPutMessageTest.java new file mode 100644 index 00000000000..768029ca1af --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/BatchPutMessageTest.java @@ -0,0 +1,264 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store; + +import java.io.File; +import java.net.InetSocketAddress; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBatch; +import org.apache.rocketmq.store.config.FlushDiskType; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.stats.BrokerStatsManager; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.apache.rocketmq.common.message.MessageDecoder.messageProperties2String; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertTrue; + +public class BatchPutMessageTest { + + private MessageStore messageStore; + + public final static Charset CHARSET_UTF8 = StandardCharsets.UTF_8; + + @Before + public void init() throws Exception { + messageStore = buildMessageStore(); + boolean load = messageStore.load(); + assertTrue(load); + messageStore.start(); + } + + @After + public void destroy() { + messageStore.shutdown(); + messageStore.destroy(); + + UtilAll.deleteFile(new File(System.getProperty("java.io.tmpdir") + File.separator + "putmessagesteststore")); + } + + private MessageStore buildMessageStore() throws Exception { + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setMappedFileSizeCommitLog(1024 * 8); + messageStoreConfig.setMappedFileSizeConsumeQueue(1024 * 4); + messageStoreConfig.setMaxHashSlotNum(100); + messageStoreConfig.setMaxIndexNum(100 * 10); + messageStoreConfig.setFlushDiskType(FlushDiskType.SYNC_FLUSH); + messageStoreConfig.setFlushIntervalConsumeQueue(1); + messageStoreConfig.setStorePathRootDir(System.getProperty("java.io.tmpdir") + File.separator + "putmessagesteststore"); + messageStoreConfig.setStorePathCommitLog(System.getProperty("java.io.tmpdir") + File.separator + + "putmessagesteststore" + File.separator + "commitlog"); + messageStoreConfig.setHaListenPort(0); + return new DefaultMessageStore(messageStoreConfig, new BrokerStatsManager("simpleTest", true), new MyMessageArrivingListener(), new BrokerConfig(), new ConcurrentHashMap<>()); + } + + @Test + public void testPutMessages() throws Exception { + String batchPropK = "extraKey"; + String batchPropV = "extraValue"; + Map batchProp = new HashMap<>(1); + batchProp.put(batchPropK, batchPropV); + short batchPropLen = (short) messageProperties2String(batchProp).getBytes(MessageDecoder.CHARSET_UTF8).length; + + List messages = new ArrayList<>(); + String topic = "batch-write-topic"; + int queue = 0; + int[] msgLengthArr = new int[11]; + msgLengthArr[0] = 0; + int j = 1; + for (int i = 0; i < 10; i++) { + Message msg = new Message(); + msg.setBody(("body" + i).getBytes()); + msg.setTopic(topic); + msg.setTags("TAG1"); + msg.setKeys(String.valueOf(System.currentTimeMillis())); + messages.add(msg); + String properties = messageProperties2String(msg.getProperties()); + byte[] propertiesBytes = properties.getBytes(CHARSET_UTF8); + short propertiesLength = (short) propertiesBytes.length; + final byte[] topicData = msg.getTopic().getBytes(MessageDecoder.CHARSET_UTF8); + final int topicLength = topicData.length; + msgLengthArr[j] = calMsgLength(msg.getBody().length, topicLength, propertiesLength + batchPropLen) + msgLengthArr[j - 1]; + j++; + } + byte[] batchMessageBody = MessageDecoder.encodeMessages(messages); + MessageExtBatch messageExtBatch = new MessageExtBatch(); + messageExtBatch.setTopic(topic); + messageExtBatch.setQueueId(queue); + messageExtBatch.setBody(batchMessageBody); + messageExtBatch.putUserProperty(batchPropK, batchPropV); + messageExtBatch.setBornTimestamp(System.currentTimeMillis()); + messageExtBatch.setStoreHost(new InetSocketAddress("127.0.0.1", 125)); + messageExtBatch.setBornHost(new InetSocketAddress("127.0.0.1", 126)); + + PutMessageResult putMessageResult = messageStore.putMessages(messageExtBatch); + assertThat(putMessageResult.isOk()).isTrue(); + + for (long i = 0; i < 10; i++) { + final long index = i; + Boolean exist = await().atMost(3, SECONDS).until(() -> { + MessageExt messageExt = messageStore.lookMessageByOffset(msgLengthArr[(int) index]); + if (messageExt == null) { + return false; + } + GetMessageResult result = messageStore.getMessage("batch_write_group", topic, queue, index, 1024 * 1024, null); + if (result == null) { + return false; + } + boolean equals = GetMessageStatus.FOUND.equals(result.getStatus()); + result.release(); + return equals; + }, item -> item); + assertTrue(exist); + } + + } + + @Test + public void testPutIPv6HostMessages() throws Exception { + List messages = new ArrayList<>(); + String topic = "batch-write-topic"; + int queue = 0; + int[] msgLengthArr = new int[11]; + msgLengthArr[0] = 0; + int j = 1; + for (int i = 0; i < 10; i++) { + Message msg = new Message(); + msg.setBody(("body" + i).getBytes()); + msg.setTopic(topic); + msg.setTags("TAG1"); + msg.setKeys(String.valueOf(System.currentTimeMillis())); + messages.add(msg); + String properties = messageProperties2String(msg.getProperties()); + byte[] propertiesBytes = properties.getBytes(CHARSET_UTF8); + short propertiesLength = (short) propertiesBytes.length; + final byte[] topicData = msg.getTopic().getBytes(MessageDecoder.CHARSET_UTF8); + final int topicLength = topicData.length; + msgLengthArr[j] = calIPv6HostMsgLength(msg.getBody().length, topicLength, propertiesLength) + msgLengthArr[j - 1]; + j++; + } + byte[] batchMessageBody = MessageDecoder.encodeMessages(messages); + MessageExtBatch messageExtBatch = new MessageExtBatch(); + messageExtBatch.setTopic(topic); + messageExtBatch.setQueueId(queue); + messageExtBatch.setBody(batchMessageBody); + messageExtBatch.setMsgId("24084004018081003FAA1DDE2B3F898A00002A9F0000000000000CA0"); + messageExtBatch.setBornTimestamp(System.currentTimeMillis()); + messageExtBatch.setSysFlag(0); + messageExtBatch.setBornHostV6Flag(); + messageExtBatch.setStoreHostAddressV6Flag(); + messageExtBatch.setStoreHost(new InetSocketAddress("1050:0000:0000:0000:0005:0600:300c:326b", 125)); + messageExtBatch.setBornHost(new InetSocketAddress("::1", 126)); + + PutMessageResult putMessageResult = messageStore.putMessages(messageExtBatch); + assertThat(putMessageResult.isOk()).isTrue(); + + for (long i = 0; i < 10; i++) { + final long index = i; + Boolean exist = await().atMost(3, SECONDS).until(() -> { + MessageExt messageExt = messageStore.lookMessageByOffset(msgLengthArr[(int) index]); + if (messageExt == null) { + return false; + } + GetMessageResult result = messageStore.getMessage("batch_write_group", topic, queue, index, 1024 * 1024, null); + if (result == null) { + return false; + } + boolean equals = GetMessageStatus.FOUND.equals(result.getStatus()); + result.release(); + return equals; + }, item -> item); + assertTrue(exist); + } + + } + + private String generateKey(StringBuilder keyBuilder, MessageExt messageExt) { + keyBuilder.setLength(0); + keyBuilder.append(messageExt.getTopic()); + keyBuilder.append('-'); + keyBuilder.append(messageExt.getQueueId()); + return keyBuilder.toString(); + } + + private int calMsgLength(int bodyLength, int topicLength, int propertiesLength) { + final int msgLen = 4 //TOTALSIZE + + 4 //MAGICCODE + + 4 //BODYCRC + + 4 //QUEUEID + + 4 //FLAG + + 8 //QUEUEOFFSET + + 8 //PHYSICALOFFSET + + 4 //SYSFLAG + + 8 //BORNTIMESTAMP + + 8 //BORNHOST + + 8 //STORETIMESTAMP + + 8 //STOREHOSTADDRESS + + 4 //RECONSUMETIMES + + 8 //Prepared Transaction Offset + + 4 + (bodyLength > 0 ? bodyLength : 0) //BODY + + 1 + topicLength //TOPIC + + 2 + (propertiesLength > 0 ? propertiesLength : 0) //propertiesLength + + 0; + return msgLen; + } + + private int calIPv6HostMsgLength(int bodyLength, int topicLength, int propertiesLength) { + final int msgLen = 4 //TOTALSIZE + + 4 //MAGICCODE + + 4 //BODYCRC + + 4 //QUEUEID + + 4 //FLAG + + 8 //QUEUEOFFSET + + 8 //PHYSICALOFFSET + + 4 //SYSFLAG + + 8 //BORNTIMESTAMP + + 20 //BORNHOST + + 8 //STORETIMESTAMP + + 20 //STOREHOSTADDRESS + + 4 //RECONSUMETIMES + + 8 //Prepared Transaction Offset + + 4 + (bodyLength > 0 ? bodyLength : 0) //BODY + + 1 + topicLength //TOPIC + + 2 + (propertiesLength > 0 ? propertiesLength : 0) //propertiesLength + + 0; + return msgLen; + } + + private class MyMessageArrivingListener implements MessageArrivingListener { + @Override + public void arriving(String topic, int queueId, long logicOffset, long tagsCode, long msgStoreTime, + byte[] filterBitMap, Map properties) { + } + } +} diff --git a/store/src/test/java/org/apache/rocketmq/store/ConsumeQueueExtTest.java b/store/src/test/java/org/apache/rocketmq/store/ConsumeQueueExtTest.java index e213a025c84..b1ec617ecfc 100644 --- a/store/src/test/java/org/apache/rocketmq/store/ConsumeQueueExtTest.java +++ b/store/src/test/java/org/apache/rocketmq/store/ConsumeQueueExtTest.java @@ -17,28 +17,27 @@ package org.apache.rocketmq.store; +import java.io.File; +import java.util.Random; import org.apache.rocketmq.common.UtilAll; import org.junit.After; import org.junit.Test; -import java.io.File; -import java.util.Random; - import static org.assertj.core.api.Assertions.assertThat; public class ConsumeQueueExtTest { - private static final String topic = "abc"; - private static final int queueId = 0; - private static final String storePath = "." + File.separator + "unit_test_store"; - private static final int bitMapLength = 64; - private static final int unitSizeWithBitMap = ConsumeQueueExt.CqExtUnit.MIN_EXT_UNIT_SIZE + bitMapLength / Byte.SIZE; - private static final int cqExtFileSize = 10 * unitSizeWithBitMap; - private static final int unitCount = 20; + private static final String TOPIC = "abc"; + private static final int QUEUE_ID = 0; + private static final String STORE_PATH = System.getProperty("java.io.tmpdir") + File.separator + "unit_test_store"; + private static final int BIT_MAP_LENGTH = 64; + private static final int UNIT_SIZE_WITH_BIT_MAP = ConsumeQueueExt.CqExtUnit.MIN_EXT_UNIT_SIZE + BIT_MAP_LENGTH / Byte.SIZE; + private static final int CQ_EXT_FILE_SIZE = 10 * UNIT_SIZE_WITH_BIT_MAP; + private static final int UNIT_COUNT = 20; protected ConsumeQueueExt genExt() { return new ConsumeQueueExt( - topic, queueId, storePath, cqExtFileSize, bitMapLength + TOPIC, QUEUE_ID, STORE_PATH, CQ_EXT_FILE_SIZE, BIT_MAP_LENGTH ); } @@ -57,7 +56,7 @@ protected ConsumeQueueExt.CqExtUnit genUnit(boolean hasBitMap) { cqExtUnit.setTagsCode(Math.abs((new Random(System.currentTimeMillis())).nextInt())); cqExtUnit.setMsgStoreTime(System.currentTimeMillis()); if (hasBitMap) { - cqExtUnit.setFilterBitMap(genBitMap(bitMapLength)); + cqExtUnit.setFilterBitMap(genBitMap(BIT_MAP_LENGTH)); } return cqExtUnit; @@ -93,10 +92,10 @@ public void testPut() { ConsumeQueueExt consumeQueueExt = genExt(); try { - putSth(consumeQueueExt, true, false, unitCount); + putSth(consumeQueueExt, true, false, UNIT_COUNT); } finally { consumeQueueExt.destroy(); - UtilAll.deleteFile(new File(storePath)); + UtilAll.deleteFile(new File(STORE_PATH)); } } @@ -104,7 +103,7 @@ public void testPut() { public void testGet() { ConsumeQueueExt consumeQueueExt = genExt(); - putSth(consumeQueueExt, false, false, unitCount); + putSth(consumeQueueExt, false, false, UNIT_COUNT); try { // from start. @@ -124,7 +123,7 @@ public void testGet() { } } finally { consumeQueueExt.destroy(); - UtilAll.deleteFile(new File(storePath)); + UtilAll.deleteFile(new File(STORE_PATH)); } } @@ -132,21 +131,21 @@ public void testGet() { public void testGet_invalidAddress() { ConsumeQueueExt consumeQueueExt = genExt(); - putSth(consumeQueueExt, false, true, unitCount); + putSth(consumeQueueExt, false, true, UNIT_COUNT); try { ConsumeQueueExt.CqExtUnit unit = consumeQueueExt.get(0); assertThat(unit).isNull(); - long addr = (cqExtFileSize / unitSizeWithBitMap) * unitSizeWithBitMap; - addr += unitSizeWithBitMap; + long addr = (CQ_EXT_FILE_SIZE / UNIT_SIZE_WITH_BIT_MAP) * UNIT_SIZE_WITH_BIT_MAP; + addr += UNIT_SIZE_WITH_BIT_MAP; unit = consumeQueueExt.get(addr); assertThat(unit).isNull(); } finally { consumeQueueExt.destroy(); - UtilAll.deleteFile(new File(storePath)); + UtilAll.deleteFile(new File(STORE_PATH)); } } @@ -154,7 +153,7 @@ public void testGet_invalidAddress() { public void testRecovery() { ConsumeQueueExt putCqExt = genExt(); - putSth(putCqExt, false, true, unitCount); + putSth(putCqExt, false, true, UNIT_COUNT); ConsumeQueueExt loadCqExt = genExt(); @@ -166,25 +165,25 @@ public void testRecovery() { assertThat(loadCqExt.getMinAddress()).isEqualTo(Long.MIN_VALUE); // same unit size. - int countPerFile = (cqExtFileSize - ConsumeQueueExt.END_BLANK_DATA_LENGTH) / unitSizeWithBitMap; + int countPerFile = (CQ_EXT_FILE_SIZE - ConsumeQueueExt.END_BLANK_DATA_LENGTH) / UNIT_SIZE_WITH_BIT_MAP; - int lastFileUnitCount = unitCount % countPerFile; + int lastFileUnitCount = UNIT_COUNT % countPerFile; - int fileCount = unitCount / countPerFile + 1; + int fileCount = UNIT_COUNT / countPerFile + 1; if (lastFileUnitCount == 0) { fileCount -= 1; } if (lastFileUnitCount == 0) { - assertThat(loadCqExt.unDecorate(loadCqExt.getMaxAddress()) % cqExtFileSize).isEqualTo(0); + assertThat(loadCqExt.unDecorate(loadCqExt.getMaxAddress()) % CQ_EXT_FILE_SIZE).isEqualTo(0); } else { assertThat(loadCqExt.unDecorate(loadCqExt.getMaxAddress())) - .isEqualTo(lastFileUnitCount * unitSizeWithBitMap + (fileCount - 1) * cqExtFileSize); + .isEqualTo(lastFileUnitCount * UNIT_SIZE_WITH_BIT_MAP + (fileCount - 1) * CQ_EXT_FILE_SIZE); } } finally { putCqExt.destroy(); loadCqExt.destroy(); - UtilAll.deleteFile(new File(storePath)); + UtilAll.deleteFile(new File(STORE_PATH)); } } @@ -192,13 +191,13 @@ public void testRecovery() { public void testTruncateByMinOffset() { ConsumeQueueExt consumeQueueExt = genExt(); - putSth(consumeQueueExt, false, true, unitCount * 2); + putSth(consumeQueueExt, false, true, UNIT_COUNT * 2); try { // truncate first one file. - long address = consumeQueueExt.decorate((long) (cqExtFileSize * 1.5)); + long address = consumeQueueExt.decorate((long) (CQ_EXT_FILE_SIZE * 1.5)); - long expectMinAddress = consumeQueueExt.decorate(cqExtFileSize); + long expectMinAddress = consumeQueueExt.decorate(CQ_EXT_FILE_SIZE); consumeQueueExt.truncateByMinAddress(address); @@ -207,7 +206,7 @@ public void testTruncateByMinOffset() { assertThat(expectMinAddress).isEqualTo(minAddress); } finally { consumeQueueExt.destroy(); - UtilAll.deleteFile(new File(storePath)); + UtilAll.deleteFile(new File(STORE_PATH)); } } @@ -215,13 +214,13 @@ public void testTruncateByMinOffset() { public void testTruncateByMaxOffset() { ConsumeQueueExt consumeQueueExt = genExt(); - putSth(consumeQueueExt, false, true, unitCount * 2); + putSth(consumeQueueExt, false, true, UNIT_COUNT * 2); try { // truncate, only first 3 files exist. - long address = consumeQueueExt.decorate(cqExtFileSize * 2 + unitSizeWithBitMap); + long address = consumeQueueExt.decorate(CQ_EXT_FILE_SIZE * 2 + UNIT_SIZE_WITH_BIT_MAP); - long expectMaxAddress = address + unitSizeWithBitMap; + long expectMaxAddress = address + UNIT_SIZE_WITH_BIT_MAP; consumeQueueExt.truncateByMaxAddress(address); @@ -230,12 +229,12 @@ public void testTruncateByMaxOffset() { assertThat(expectMaxAddress).isEqualTo(maxAddress); } finally { consumeQueueExt.destroy(); - UtilAll.deleteFile(new File(storePath)); + UtilAll.deleteFile(new File(STORE_PATH)); } } @After public void destroy() { - UtilAll.deleteFile(new File(storePath)); + UtilAll.deleteFile(new File(STORE_PATH)); } } diff --git a/store/src/test/java/org/apache/rocketmq/store/ConsumeQueueTest.java b/store/src/test/java/org/apache/rocketmq/store/ConsumeQueueTest.java index b7d38f8c78f..2e08369bde9 100644 --- a/store/src/test/java/org/apache/rocketmq/store/ConsumeQueueTest.java +++ b/store/src/test/java/org/apache/rocketmq/store/ConsumeQueueTest.java @@ -23,40 +23,56 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.UnknownHostException; -import java.nio.ByteBuffer; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; + import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageConst; import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.queue.ConsumeQueueInterface; +import org.apache.rocketmq.store.queue.CqUnit; +import org.apache.rocketmq.store.queue.ReferredIterator; import org.apache.rocketmq.store.stats.BrokerStatsManager; -import static org.assertj.core.api.Assertions.assertThat; +import org.awaitility.Awaitility; +import org.junit.Assert; import org.junit.Test; +import org.mockito.Mockito; +import org.junit.Assume; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; public class ConsumeQueueTest { - private static final String msg = "Once, there was a chance for me!"; - private static final byte[] msgBody = msg.getBytes(); + private static final String MSG = "Once, there was a chance for me!"; + private static final byte[] MSG_BODY = MSG.getBytes(); - private static final String topic = "abc"; - private static final int queueId = 0; - private static final String storePath = "." + File.separator + "unit_test_store"; - private static final int commitLogFileSize = 1024 * 8; - private static final int cqFileSize = 10 * 20; - private static final int cqExtFileSize = 10 * (ConsumeQueueExt.CqExtUnit.MIN_EXT_UNIT_SIZE + 64); + private static final String TOPIC = "abc"; + private static final int QUEUE_ID = 0; + private static final String STORE_PATH = System.getProperty("java.io.tmpdir") + File.separator + "unit_test_store"; + private static final int COMMIT_LOG_FILE_SIZE = 1024 * 8; + private static final int CQ_FILE_SIZE = 10 * 20; + private static final int CQ_EXT_FILE_SIZE = 10 * (ConsumeQueueExt.CqExtUnit.MIN_EXT_UNIT_SIZE + 64); - private static SocketAddress BornHost; + private static SocketAddress bornHost; - private static SocketAddress StoreHost; + private static SocketAddress storeHost; static { try { - StoreHost = new InetSocketAddress(InetAddress.getLocalHost(), 8123); + storeHost = new InetSocketAddress(InetAddress.getLocalHost(), 8123); } catch (UnknownHostException e) { e.printStackTrace(); } try { - BornHost = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0); + bornHost = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0); } catch (UnknownHostException e) { e.printStackTrace(); } @@ -64,16 +80,39 @@ public class ConsumeQueueTest { public MessageExtBrokerInner buildMessage() { MessageExtBrokerInner msg = new MessageExtBrokerInner(); - msg.setTopic(topic); + msg.setTopic(TOPIC); + msg.setTags("TAG1"); + msg.setKeys("Hello"); + msg.setBody(MSG_BODY); + msg.setKeys(String.valueOf(System.currentTimeMillis())); + msg.setQueueId(QUEUE_ID); + msg.setSysFlag(0); + msg.setBornTimestamp(System.currentTimeMillis()); + msg.setStoreHost(storeHost); + msg.setBornHost(bornHost); + for (int i = 0; i < 1; i++) { + msg.putUserProperty(String.valueOf(i), "imagoodperson" + i); + } + msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); + + return msg; + } + + public MessageExtBrokerInner buildIPv6HostMessage() { + MessageExtBrokerInner msg = new MessageExtBrokerInner(); + msg.setTopic(TOPIC); msg.setTags("TAG1"); msg.setKeys("Hello"); - msg.setBody(msgBody); + msg.setBody(MSG_BODY); + msg.setMsgId("24084004018081003FAA1DDE2B3F898A00002A9F0000000000000CA0"); msg.setKeys(String.valueOf(System.currentTimeMillis())); - msg.setQueueId(queueId); + msg.setQueueId(QUEUE_ID); msg.setSysFlag(0); + msg.setBornHostV6Flag(); + msg.setStoreHostAddressV6Flag(); msg.setBornTimestamp(System.currentTimeMillis()); - msg.setStoreHost(StoreHost); - msg.setBornHost(BornHost); + msg.setBornHost(new InetSocketAddress("1050:0000:0000:0000:0005:0600:300c:326b", 123)); + msg.setStoreHost(new InetSocketAddress("::1", 124)); for (int i = 0; i < 1; i++) { msg.putUserProperty(String.valueOf(i), "imagoodperson" + i); } @@ -85,35 +124,35 @@ public MessageExtBrokerInner buildMessage() { public MessageStoreConfig buildStoreConfig(int commitLogFileSize, int cqFileSize, boolean enableCqExt, int cqExtFileSize) { MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); - messageStoreConfig.setMapedFileSizeCommitLog(commitLogFileSize); - messageStoreConfig.setMapedFileSizeConsumeQueue(cqFileSize); + messageStoreConfig.setMappedFileSizeCommitLog(commitLogFileSize); + messageStoreConfig.setMappedFileSizeConsumeQueue(cqFileSize); messageStoreConfig.setMappedFileSizeConsumeQueueExt(cqExtFileSize); messageStoreConfig.setMessageIndexEnable(false); messageStoreConfig.setEnableConsumeQueueExt(enableCqExt); - - messageStoreConfig.setStorePathRootDir(storePath); - messageStoreConfig.setStorePathCommitLog(storePath + File.separator + "commitlog"); + messageStoreConfig.setHaListenPort(0); + messageStoreConfig.setStorePathRootDir(STORE_PATH); + messageStoreConfig.setStorePathCommitLog(STORE_PATH + File.separator + "commitlog"); return messageStoreConfig; } protected DefaultMessageStore gen() throws Exception { MessageStoreConfig messageStoreConfig = buildStoreConfig( - commitLogFileSize, cqFileSize, true, cqExtFileSize + COMMIT_LOG_FILE_SIZE, CQ_FILE_SIZE, true, CQ_EXT_FILE_SIZE ); BrokerConfig brokerConfig = new BrokerConfig(); DefaultMessageStore master = new DefaultMessageStore( messageStoreConfig, - new BrokerStatsManager(brokerConfig.getBrokerClusterName()), + new BrokerStatsManager(brokerConfig.getBrokerClusterName(), brokerConfig.isEnableDetailStat()), new MessageArrivingListener() { @Override public void arriving(String topic, int queueId, long logicOffset, long tagsCode, long msgStoreTime, byte[] filterBitMap, Map properties) { } } - , brokerConfig); + , brokerConfig, new ConcurrentHashMap<>()); assertThat(master.load()).isTrue(); @@ -122,14 +161,73 @@ public void arriving(String topic, int queueId, long logicOffset, long tagsCode, return master; } - protected void putMsg(DefaultMessageStore master) throws Exception { + protected DefaultMessageStore genForMultiQueue() throws Exception { + MessageStoreConfig messageStoreConfig = buildStoreConfig( + COMMIT_LOG_FILE_SIZE, CQ_FILE_SIZE, true, CQ_EXT_FILE_SIZE + ); + + messageStoreConfig.setEnableLmq(true); + messageStoreConfig.setEnableMultiDispatch(true); + + BrokerConfig brokerConfig = new BrokerConfig(); + + DefaultMessageStore master = new DefaultMessageStore( + messageStoreConfig, + new BrokerStatsManager(brokerConfig.getBrokerClusterName(), brokerConfig.isEnableDetailStat()), + new MessageArrivingListener() { + @Override + public void arriving(String topic, int queueId, long logicOffset, long tagsCode, + long msgStoreTime, byte[] filterBitMap, Map properties) { + } + } + , brokerConfig, new ConcurrentHashMap<>()); + + assertThat(master.load()).isTrue(); + + master.start(); + + return master; + } + + protected void putMsg(DefaultMessageStore master) { long totalMsgs = 200; for (long i = 0; i < totalMsgs; i++) { - master.putMessage(buildMessage()); + if (i < totalMsgs / 2) { + master.putMessage(buildMessage()); + } else { + master.putMessage(buildIPv6HostMessage()); + } + } + } + + protected void putMsgMultiQueue(DefaultMessageStore master) { + for (long i = 0; i < 1; i++) { + master.putMessage(buildMessageMultiQueue()); } } + private MessageExtBrokerInner buildMessageMultiQueue() { + MessageExtBrokerInner msg = new MessageExtBrokerInner(); + msg.setTopic(TOPIC); + msg.setTags("TAG1"); + msg.setKeys("Hello"); + msg.setBody(MSG_BODY); + msg.setKeys(String.valueOf(System.currentTimeMillis())); + msg.setQueueId(QUEUE_ID); + msg.setSysFlag(0); + msg.setBornTimestamp(System.currentTimeMillis()); + msg.setStoreHost(storeHost); + msg.setBornHost(bornHost); + for (int i = 0; i < 1; i++) { + msg.putUserProperty(MessageConst.PROPERTY_INNER_MULTI_DISPATCH, "%LMQ%123,%LMQ%456"); + msg.putUserProperty(String.valueOf(i), "imagoodperson" + i); + } + msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); + + return msg; + } + protected void deleteDirectory(String rootPath) { File file = new File(rootPath); deleteFile(file); @@ -158,9 +256,15 @@ public void testPutMessagePositionInfo_buildCQRepeatedly() throws Exception { for (int i = 0; i < totalMessages; i++) { putMsg(messageStore); } - Thread.sleep(5); - ConsumeQueue cq = messageStore.getConsumeQueueTable().get(topic).get(queueId); + + // Wait consume queue build finish. + final MessageStore store = messageStore; + Awaitility.with().pollInterval(100, TimeUnit.MILLISECONDS).await().timeout(1, TimeUnit.MINUTES).until(() -> { + return store.dispatchBehindBytes() == 0; + }); + + ConsumeQueueInterface cq = messageStore.getConsumeQueueTable().get(TOPIC).get(QUEUE_ID); Method method = cq.getClass().getDeclaredMethod("putMessagePositionInfo", long.class, int.class, long.class, long.class); assertThat(method).isNotNull(); @@ -184,11 +288,104 @@ public void testPutMessagePositionInfo_buildCQRepeatedly() throws Exception { messageStore.shutdown(); messageStore.destroy(); } - deleteDirectory(storePath); + deleteDirectory(STORE_PATH); } } + @Test + public void testPutMessagePositionInfoWrapper_MultiQueue() throws Exception { + Assume.assumeFalse(MixAll.isWindows()); + DefaultMessageStore messageStore = null; + try { + messageStore = genForMultiQueue(); + + int totalMessages = 10; + + for (int i = 0; i < totalMessages; i++) { + putMsgMultiQueue(messageStore); + } + + // Wait consume queue build finish. + final MessageStore store = messageStore; + Awaitility.with().pollInterval(100, TimeUnit.MILLISECONDS).await().timeout(1, TimeUnit.MINUTES).until(() -> { + return store.dispatchBehindBytes() == 0; + }); + + ConsumeQueueInterface cq = messageStore.getConsumeQueueTable().get(TOPIC).get(QUEUE_ID); + Method method = ((ConsumeQueue) cq).getClass().getDeclaredMethod("putMessagePositionInfoWrapper", DispatchRequest.class); + + assertThat(method).isNotNull(); + + method.setAccessible(true); + + SelectMappedBufferResult result = messageStore.getCommitLog().getData(0); + assertThat(result != null).isTrue(); + + DispatchRequest dispatchRequest = messageStore.getCommitLog().checkMessageAndReturnSize(result.getByteBuffer(), false, false); + + assertThat(cq).isNotNull(); + + Object dispatchResult = method.invoke(cq, dispatchRequest); + + ConsumeQueueInterface lmqCq1 = messageStore.getConsumeQueueTable().get("%LMQ%123").get(0); + + ConsumeQueueInterface lmqCq2 = messageStore.getConsumeQueueTable().get("%LMQ%456").get(0); + + assertThat(lmqCq1).isNotNull(); + + assertThat(lmqCq2).isNotNull(); + + } finally { + if (messageStore != null) { + messageStore.shutdown(); + messageStore.destroy(); + } + deleteDirectory(STORE_PATH); + } + + } + + @Test + public void testPutMessagePositionInfoMultiQueue() throws Exception { + DefaultMessageStore messageStore = null; + try { + + messageStore = genForMultiQueue(); + + int totalMessages = 10; + + for (int i = 0; i < totalMessages; i++) { + putMsgMultiQueue(messageStore); + } + + // Wait consume queue build finish. + final MessageStore store = messageStore; + Awaitility.with().pollInterval(100, TimeUnit.MILLISECONDS).await().timeout(1, TimeUnit.MINUTES).until(() -> { + return store.dispatchBehindBytes() == 0; + }); + + ConsumeQueueInterface cq = messageStore.getConsumeQueueTable().get(TOPIC).get(QUEUE_ID); + + ConsumeQueueInterface lmqCq1 = messageStore.getConsumeQueueTable().get("%LMQ%123").get(0); + + ConsumeQueueInterface lmqCq2 = messageStore.getConsumeQueueTable().get("%LMQ%456").get(0); + + assertThat(cq).isNotNull(); + + assertThat(lmqCq1).isNotNull(); + + assertThat(lmqCq2).isNotNull(); + + } finally { + if (messageStore != null) { + messageStore.shutdown(); + messageStore.destroy(); + } + deleteDirectory(STORE_PATH); + } + } + @Test public void testConsumeQueueWithExtendData() { DefaultMessageStore master = null; @@ -210,59 +407,100 @@ public void dispatch(DispatchRequest request) { }); try { - try { - putMsg(master); - // wait build consume queue - Thread.sleep(1000); - } catch (Exception e) { - e.printStackTrace(); - assertThat(Boolean.FALSE).isTrue(); - } - ConsumeQueue cq = master.getConsumeQueueTable().get(topic).get(queueId); + putMsg(master); + final DefaultMessageStore master1 = master; + ConsumeQueueInterface cq = await().atMost(3, SECONDS).until(() -> { + ConcurrentMap map = master1.getConsumeQueueTable().get(TOPIC); + if (map == null) { + return null; + } + ConsumeQueueInterface anInterface = map.get(QUEUE_ID); + return anInterface; + }, item -> null != item); assertThat(cq).isNotNull(); - long index = 0; - - while (index < cq.getMaxOffsetInQueue()) { - SelectMappedBufferResult bufferResult = cq.getIndexBuffer(index); - - assertThat(bufferResult).isNotNull(); - - ByteBuffer buffer = bufferResult.getByteBuffer(); + ReferredIterator bufferResult = cq.iterateFrom(0); - assertThat(buffer).isNotNull(); - try { - ConsumeQueueExt.CqExtUnit cqExtUnit = new ConsumeQueueExt.CqExtUnit(); - for (int i = 0; i < bufferResult.getSize(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE) { - long phyOffset = buffer.getLong(); - int size = buffer.getInt(); - long tagsCode = buffer.getLong(); + assertThat(bufferResult).isNotNull(); - assertThat(phyOffset).isGreaterThanOrEqualTo(0); - assertThat(size).isGreaterThan(0); - assertThat(tagsCode).isLessThan(0); + Assert.assertTrue(bufferResult.hasNext()); - boolean ret = cq.getExt(tagsCode, cqExtUnit); - - assertThat(ret).isTrue(); - assertThat(cqExtUnit).isNotNull(); - assertThat(cqExtUnit.getSize()).isGreaterThan((short) 0); - assertThat(cqExtUnit.getMsgStoreTime()).isGreaterThan(0); - assertThat(cqExtUnit.getTagsCode()).isGreaterThan(0); - } - - } finally { - bufferResult.release(); + try { + while (bufferResult.hasNext()) { + CqUnit cqUnit = bufferResult.next(); + Assert.assertNotNull(cqUnit); + long phyOffset = cqUnit.getPos(); + int size = cqUnit.getSize(); + long tagsCode = cqUnit.getTagsCode(); + + assertThat(phyOffset).isGreaterThanOrEqualTo(0); + assertThat(size).isGreaterThan(0); + assertThat(tagsCode).isGreaterThan(0); + + ConsumeQueueExt.CqExtUnit cqExtUnit = cqUnit.getCqExtUnit(); + assertThat(cqExtUnit).isNotNull(); + assertThat(tagsCode).isEqualTo(cqExtUnit.getTagsCode()); + assertThat(cqExtUnit.getSize()).isGreaterThan((short) 0); + assertThat(cqExtUnit.getMsgStoreTime()).isGreaterThan(0); + assertThat(cqExtUnit.getTagsCode()).isGreaterThan(0); } - index += cqFileSize / ConsumeQueue.CQ_STORE_UNIT_SIZE; + } finally { + bufferResult.release(); } + } finally { master.shutdown(); master.destroy(); - UtilAll.deleteFile(new File(storePath)); + UtilAll.deleteFile(new File(STORE_PATH)); } } + + @Test + public void testCorrectMinOffset() { + String topic = "T1"; + int queueId = 0; + MessageStoreConfig storeConfig = new MessageStoreConfig(); + File tmpDir = new File(System.getProperty("java.io.tmpdir"), "test_correct_min_offset"); + tmpDir.deleteOnExit(); + storeConfig.setStorePathRootDir(tmpDir.getAbsolutePath()); + storeConfig.setEnableConsumeQueueExt(false); + DefaultMessageStore messageStore = Mockito.mock(DefaultMessageStore.class); + Mockito.when(messageStore.getMessageStoreConfig()).thenReturn(storeConfig); + + RunningFlags runningFlags = new RunningFlags(); + Mockito.when(messageStore.getRunningFlags()).thenReturn(runningFlags); + + StoreCheckpoint storeCheckpoint = Mockito.mock(StoreCheckpoint.class); + Mockito.when(messageStore.getStoreCheckpoint()).thenReturn(storeCheckpoint); + + ConsumeQueue consumeQueue = new ConsumeQueue(topic, queueId, storeConfig.getStorePathRootDir(), + storeConfig.getMappedFileSizeConsumeQueue(), messageStore); + + int max = 10000; + int messageSize = 100; + for (int i = 0; i < max; ++i) { + DispatchRequest dispatchRequest = new DispatchRequest(topic, queueId, messageSize * i, messageSize, 0, 0, i, null, null, 0, 0, null); + consumeQueue.putMessagePositionInfoWrapper(dispatchRequest); + } + + consumeQueue.setMinLogicOffset(0L); + consumeQueue.correctMinOffset(0L); + Assert.assertEquals(0, consumeQueue.getMinOffsetInQueue()); + + consumeQueue.setMinLogicOffset(100); + consumeQueue.correctMinOffset(2000); + Assert.assertEquals(20, consumeQueue.getMinOffsetInQueue()); + + consumeQueue.setMinLogicOffset((max - 1) * ConsumeQueue.CQ_STORE_UNIT_SIZE); + consumeQueue.correctMinOffset(max * messageSize); + Assert.assertEquals(max * ConsumeQueue.CQ_STORE_UNIT_SIZE, consumeQueue.getMinLogicOffset()); + + consumeQueue.setMinLogicOffset(max * ConsumeQueue.CQ_STORE_UNIT_SIZE); + consumeQueue.correctMinOffset(max * messageSize); + Assert.assertEquals(max * ConsumeQueue.CQ_STORE_UNIT_SIZE, consumeQueue.getMinLogicOffset()); + consumeQueue.destroy(); + } } diff --git a/store/src/test/java/org/apache/rocketmq/store/DefaultMessageStoreCleanFilesTest.java b/store/src/test/java/org/apache/rocketmq/store/DefaultMessageStoreCleanFilesTest.java new file mode 100644 index 00000000000..083aabc48b3 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/DefaultMessageStoreCleanFilesTest.java @@ -0,0 +1,541 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store; + +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.store.config.FlushDiskType; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.index.IndexFile; +import org.apache.rocketmq.store.index.IndexService; +import org.apache.rocketmq.store.logfile.MappedFile; +import org.apache.rocketmq.store.queue.ConsumeQueueInterface; +import org.apache.rocketmq.store.stats.BrokerStatsManager; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.lang.reflect.Field; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import static org.apache.rocketmq.common.message.MessageDecoder.CHARSET_UTF8; +import static org.apache.rocketmq.store.ConsumeQueue.CQ_STORE_UNIT_SIZE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +/** + * Test case for DefaultMessageStore.CleanCommitLogService and DefaultMessageStore.CleanConsumeQueueService + */ +public class DefaultMessageStoreCleanFilesTest { + private DefaultMessageStore messageStore; + private DefaultMessageStore.CleanCommitLogService cleanCommitLogService; + private DefaultMessageStore.CleanConsumeQueueService cleanConsumeQueueService; + + private SocketAddress bornHost; + private SocketAddress storeHost; + + private String topic = "test"; + private String keys = "hello"; + private int queueId = 0; + private int fileCountCommitLog = 55; + // exactly one message per CommitLog file. + private int msgCount = fileCountCommitLog; + private int mappedFileSize = 128; + private int fileReservedTime = 1; + + @Before + public void init() throws Exception { + storeHost = new InetSocketAddress(InetAddress.getLocalHost(), 8123); + bornHost = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0); + } + + @Test + public void testIsSpaceFullFunctionEmpty2Full() throws Exception { + String deleteWhen = "04"; + // the min value of diskMaxUsedSpaceRatio. + int diskMaxUsedSpaceRatio = 1; + // used to set disk-full flag + double diskSpaceCleanForciblyRatio = 0.01D; + initMessageStore(deleteWhen, diskMaxUsedSpaceRatio, diskSpaceCleanForciblyRatio); + // build and put 55 messages, exactly one message per CommitLog file. + buildAndPutMessagesToMessageStore(msgCount); + MappedFileQueue commitLogQueue = getMappedFileQueueCommitLog(); + assertEquals(fileCountCommitLog, commitLogQueue.getMappedFiles().size()); + int fileCountConsumeQueue = getFileCountConsumeQueue(); + MappedFileQueue consumeQueue = getMappedFileQueueConsumeQueue(); + assertEquals(fileCountConsumeQueue, consumeQueue.getMappedFiles().size()); + cleanCommitLogService.isSpaceFull(); + assertEquals(1 << 4, messageStore.getRunningFlags().getFlagBits() & (1 << 4)); + messageStore.shutdown(); + messageStore.destroy(); + + } + + @Test + public void testIsSpaceFullMultiCommitLogStorePath() throws Exception { + String deleteWhen = "04"; + // the min value of diskMaxUsedSpaceRatio. + int diskMaxUsedSpaceRatio = 1; + // used to set disk-full flag + double diskSpaceCleanForciblyRatio = 0.01D; + MessageStoreConfig config = genMessageStoreConfig(deleteWhen, diskMaxUsedSpaceRatio); + String storePath = config.getStorePathCommitLog(); + StringBuilder storePathBuilder = new StringBuilder(); + for (int i = 0; i < 3; i++) { + storePathBuilder.append(storePath).append(i).append(MixAll.MULTI_PATH_SPLITTER); + } + config.setStorePathCommitLog(storePathBuilder.toString()); + String[] paths = config.getStorePathCommitLog().trim().split(MixAll.MULTI_PATH_SPLITTER); + assertEquals(3, paths.length); + initMessageStore(config, diskSpaceCleanForciblyRatio); + + + + // build and put 55 messages, exactly one message per CommitLog file. + buildAndPutMessagesToMessageStore(msgCount); + MappedFileQueue commitLogQueue = getMappedFileQueueCommitLog(); + assertEquals(fileCountCommitLog, commitLogQueue.getMappedFiles().size()); + int fileCountConsumeQueue = getFileCountConsumeQueue(); + MappedFileQueue consumeQueue = getMappedFileQueueConsumeQueue(); + assertEquals(fileCountConsumeQueue, consumeQueue.getMappedFiles().size()); + cleanCommitLogService.isSpaceFull(); + + assertEquals(1 << 4, messageStore.getRunningFlags().getFlagBits() & (1 << 4)); + messageStore.shutdown(); + messageStore.destroy(); + + } + + @Test + public void testIsSpaceFullFunctionFull2Empty() throws Exception { + String deleteWhen = "04"; + // the min value of diskMaxUsedSpaceRatio. + int diskMaxUsedSpaceRatio = 1; + //use to reset disk-full flag + double diskSpaceCleanForciblyRatio = 0.999D; + initMessageStore(deleteWhen, diskMaxUsedSpaceRatio, diskSpaceCleanForciblyRatio); + //set disk full + messageStore.getRunningFlags().getAndMakeDiskFull(); + + cleanCommitLogService.isSpaceFull(); + assertEquals(0, messageStore.getRunningFlags().getFlagBits() & (1 << 4)); + } + + @Test + public void testDeleteExpiredFilesByTimeUp() throws Exception { + String deleteWhen = Calendar.getInstance().get(Calendar.HOUR_OF_DAY) + ""; + // the max value of diskMaxUsedSpaceRatio + int diskMaxUsedSpaceRatio = 99; + // used to ensure that automatic file deletion is not triggered + double diskSpaceCleanForciblyRatio = 0.999D; + initMessageStore(deleteWhen, diskMaxUsedSpaceRatio, diskSpaceCleanForciblyRatio); + + // build and put 55 messages, exactly one message per CommitLog file. + buildAndPutMessagesToMessageStore(msgCount); + + // undo comment out the code below, if want to debug this case rather than just run it. + // Thread.sleep(1000 * 60 + 100); + + MappedFileQueue commitLogQueue = getMappedFileQueueCommitLog(); + assertEquals(fileCountCommitLog, commitLogQueue.getMappedFiles().size()); + + int fileCountConsumeQueue = getFileCountConsumeQueue(); + MappedFileQueue consumeQueue = getMappedFileQueueConsumeQueue(); + assertEquals(fileCountConsumeQueue, consumeQueue.getMappedFiles().size()); + + int fileCountIndexFile = getFileCountIndexFile(); + assertEquals(fileCountIndexFile, getIndexFileList().size()); + + int expireFileCount = 15; + expireFiles(commitLogQueue, expireFileCount); + + // magic code 10 reference to MappedFileQueue#DELETE_FILES_BATCH_MAX + for (int a = 1, fileCount = expireFileCount; a <= (int) Math.ceil((double) expireFileCount / 10); a++, fileCount -= 10) { + cleanCommitLogService.run(); + cleanConsumeQueueService.run(); + + int expectDeletedCount = fileCount >= 10 ? a * 10 : ((a - 1) * 10 + fileCount); + assertEquals(fileCountCommitLog - expectDeletedCount, commitLogQueue.getMappedFiles().size()); + + int msgCountPerFile = getMsgCountPerConsumeQueueMappedFile(); + int expectDeleteCountConsumeQueue = (int) Math.floor((double) expectDeletedCount / msgCountPerFile); + assertEquals(fileCountConsumeQueue - expectDeleteCountConsumeQueue, consumeQueue.getMappedFiles().size()); + + int msgCountPerIndexFile = getMsgCountPerIndexFile(); + int expectDeleteCountIndexFile = (int) Math.floor((double) expectDeletedCount / msgCountPerIndexFile); + assertEquals(fileCountIndexFile - expectDeleteCountIndexFile, getIndexFileList().size()); + } + } + + @Test + public void testDeleteExpiredFilesBySpaceFull() throws Exception { + String deleteWhen = "04"; + // the min value of diskMaxUsedSpaceRatio. + int diskMaxUsedSpaceRatio = 1; + // used to ensure that automatic file deletion is not triggered + double diskSpaceCleanForciblyRatio = 0.999D; + initMessageStore(deleteWhen, diskMaxUsedSpaceRatio, diskSpaceCleanForciblyRatio); + + // build and put 55 messages, exactly one message per CommitLog file. + buildAndPutMessagesToMessageStore(msgCount); + + // undo comment out the code below, if want to debug this case rather than just run it. + // Thread.sleep(1000 * 60 + 100); + + MappedFileQueue commitLogQueue = getMappedFileQueueCommitLog(); + assertEquals(fileCountCommitLog, commitLogQueue.getMappedFiles().size()); + + int fileCountConsumeQueue = getFileCountConsumeQueue(); + MappedFileQueue consumeQueue = getMappedFileQueueConsumeQueue(); + assertEquals(fileCountConsumeQueue, consumeQueue.getMappedFiles().size()); + + int fileCountIndexFile = getFileCountIndexFile(); + assertEquals(fileCountIndexFile, getIndexFileList().size()); + + int expireFileCount = 15; + expireFiles(commitLogQueue, expireFileCount); + + // magic code 10 reference to MappedFileQueue#DELETE_FILES_BATCH_MAX + for (int a = 1, fileCount = expireFileCount; a <= (int) Math.ceil((double) expireFileCount / 10); a++, fileCount -= 10) { + cleanCommitLogService.run(); + cleanConsumeQueueService.run(); + + int expectDeletedCount = fileCount >= 10 ? a * 10 : ((a - 1) * 10 + fileCount); + assertEquals(fileCountCommitLog - expectDeletedCount, commitLogQueue.getMappedFiles().size()); + + int msgCountPerFile = getMsgCountPerConsumeQueueMappedFile(); + int expectDeleteCountConsumeQueue = (int) Math.floor((double) expectDeletedCount / msgCountPerFile); + assertEquals(fileCountConsumeQueue - expectDeleteCountConsumeQueue, consumeQueue.getMappedFiles().size()); + + int msgCountPerIndexFile = getMsgCountPerIndexFile(); + int expectDeleteCountIndexFile = (int) Math.floor((double) expectDeletedCount / msgCountPerIndexFile); + assertEquals(fileCountIndexFile - expectDeleteCountIndexFile, getIndexFileList().size()); + } + } + + @Test + public void testDeleteFilesImmediatelyBySpaceFull() throws Exception { + String deleteWhen = "04"; + // the min value of diskMaxUsedSpaceRatio. + int diskMaxUsedSpaceRatio = 1; + // make sure to trigger the automatic file deletion feature + double diskSpaceCleanForciblyRatio = 0.01D; + initMessageStore(deleteWhen, diskMaxUsedSpaceRatio, diskSpaceCleanForciblyRatio); + + // build and put 55 messages, exactly one message per CommitLog file. + buildAndPutMessagesToMessageStore(msgCount); + + // undo comment out the code below, if want to debug this case rather than just run it. + // Thread.sleep(1000 * 60 + 100); + + MappedFileQueue commitLogQueue = getMappedFileQueueCommitLog(); + assertEquals(fileCountCommitLog, commitLogQueue.getMappedFiles().size()); + + int fileCountConsumeQueue = getFileCountConsumeQueue(); + MappedFileQueue consumeQueue = getMappedFileQueueConsumeQueue(); + assertEquals(fileCountConsumeQueue, consumeQueue.getMappedFiles().size()); + + int fileCountIndexFile = getFileCountIndexFile(); + assertEquals(fileCountIndexFile, getIndexFileList().size()); + + // In this case, there is no need to expire the files. + // int expireFileCount = 15; + // expireFiles(commitLogQueue, expireFileCount); + + // magic code 10 reference to MappedFileQueue#DELETE_FILES_BATCH_MAX + for (int a = 1, fileCount = fileCountCommitLog; + a <= (int) Math.ceil((double) fileCountCommitLog / 10) && fileCount >= 10; + a++, fileCount -= 10) { + cleanCommitLogService.run(); + cleanConsumeQueueService.run(); + + assertEquals(fileCountCommitLog - 10 * a, commitLogQueue.getMappedFiles().size()); + + int msgCountPerFile = getMsgCountPerConsumeQueueMappedFile(); + int expectDeleteCountConsumeQueue = (int) Math.floor((double) (a * 10) / msgCountPerFile); + assertEquals(fileCountConsumeQueue - expectDeleteCountConsumeQueue, consumeQueue.getMappedFiles().size()); + + int msgCountPerIndexFile = getMsgCountPerIndexFile(); + int expectDeleteCountIndexFile = (int) Math.floor((double) (a * 10) / msgCountPerIndexFile); + assertEquals(fileCountIndexFile - expectDeleteCountIndexFile, getIndexFileList().size()); + } + } + + @Test + public void testDeleteExpiredFilesManually() throws Exception { + String deleteWhen = "04"; + // the max value of diskMaxUsedSpaceRatio + int diskMaxUsedSpaceRatio = 99; + // used to ensure that automatic file deletion is not triggered + double diskSpaceCleanForciblyRatio = 0.999D; + initMessageStore(deleteWhen, diskMaxUsedSpaceRatio, diskSpaceCleanForciblyRatio); + + messageStore.executeDeleteFilesManually(); + + // build and put 55 messages, exactly one message per CommitLog file. + buildAndPutMessagesToMessageStore(msgCount); + + // undo comment out the code below, if want to debug this case rather than just run it. + // Thread.sleep(1000 * 60 + 100); + + MappedFileQueue commitLogQueue = getMappedFileQueueCommitLog(); + assertEquals(fileCountCommitLog, commitLogQueue.getMappedFiles().size()); + + int fileCountConsumeQueue = getFileCountConsumeQueue(); + MappedFileQueue consumeQueue = getMappedFileQueueConsumeQueue(); + assertEquals(fileCountConsumeQueue, consumeQueue.getMappedFiles().size()); + + int fileCountIndexFile = getFileCountIndexFile(); + assertEquals(fileCountIndexFile, getIndexFileList().size()); + + int expireFileCount = 15; + expireFiles(commitLogQueue, expireFileCount); + + // magic code 10 reference to MappedFileQueue#DELETE_FILES_BATCH_MAX + for (int a = 1, fileCount = expireFileCount; a <= (int) Math.ceil((double) expireFileCount / 10); a++, fileCount -= 10) { + cleanCommitLogService.run(); + cleanConsumeQueueService.run(); + + int expectDeletedCount = fileCount >= 10 ? a * 10 : ((a - 1) * 10 + fileCount); + assertEquals(fileCountCommitLog - expectDeletedCount, commitLogQueue.getMappedFiles().size()); + + int msgCountPerFile = getMsgCountPerConsumeQueueMappedFile(); + int expectDeleteCountConsumeQueue = (int) Math.floor((double) expectDeletedCount / msgCountPerFile); + assertEquals(fileCountConsumeQueue - expectDeleteCountConsumeQueue, consumeQueue.getMappedFiles().size()); + + int msgCountPerIndexFile = getMsgCountPerIndexFile(); + int expectDeleteCountIndexFile = (int) Math.floor((double) (a * 10) / msgCountPerIndexFile); + assertEquals(fileCountIndexFile - expectDeleteCountIndexFile, getIndexFileList().size()); + } + } + + private DefaultMessageStore.CleanCommitLogService getCleanCommitLogService() + throws Exception { + Field serviceField = messageStore.getClass().getDeclaredField("cleanCommitLogService"); + serviceField.setAccessible(true); + DefaultMessageStore.CleanCommitLogService cleanCommitLogService = + (DefaultMessageStore.CleanCommitLogService) serviceField.get(messageStore); + serviceField.setAccessible(false); + + return cleanCommitLogService; + } + + private DefaultMessageStore.CleanConsumeQueueService getCleanConsumeQueueService() + throws Exception { + Field serviceField = messageStore.getClass().getDeclaredField("cleanConsumeQueueService"); + serviceField.setAccessible(true); + DefaultMessageStore.CleanConsumeQueueService cleanConsumeQueueService = + (DefaultMessageStore.CleanConsumeQueueService) serviceField.get(messageStore); + serviceField.setAccessible(false); + return cleanConsumeQueueService; + } + + private MappedFileQueue getMappedFileQueueConsumeQueue() + throws Exception { + ConsumeQueueInterface consumeQueue = messageStore.getConsumeQueueTable().get(topic).get(queueId); + Field queueField = consumeQueue.getClass().getDeclaredField("mappedFileQueue"); + queueField.setAccessible(true); + MappedFileQueue fileQueue = (MappedFileQueue) queueField.get(consumeQueue); + queueField.setAccessible(false); + return fileQueue; + } + + private MappedFileQueue getMappedFileQueueCommitLog() throws Exception { + CommitLog commitLog = messageStore.getCommitLog(); + Field queueField = commitLog.getClass().getDeclaredField("mappedFileQueue"); + queueField.setAccessible(true); + MappedFileQueue fileQueue = (MappedFileQueue) queueField.get(commitLog); + queueField.setAccessible(false); + return fileQueue; + } + + private ArrayList getIndexFileList() throws Exception { + Field indexServiceField = messageStore.getClass().getDeclaredField("indexService"); + indexServiceField.setAccessible(true); + IndexService indexService = (IndexService) indexServiceField.get(messageStore); + + Field indexFileListField = indexService.getClass().getDeclaredField("indexFileList"); + indexFileListField.setAccessible(true); + ArrayList indexFileList = (ArrayList) indexFileListField.get(indexService); + + return indexFileList; + } + + private int getFileCountConsumeQueue() { + int countPerFile = getMsgCountPerConsumeQueueMappedFile(); + double fileCount = (double) msgCount / countPerFile; + return (int) Math.ceil(fileCount); + } + + private int getFileCountIndexFile() { + int countPerFile = getMsgCountPerIndexFile(); + double fileCount = (double) msgCount / countPerFile; + return (int) Math.ceil(fileCount); + } + + private int getMsgCountPerConsumeQueueMappedFile() { + int size = messageStore.getMessageStoreConfig().getMappedFileSizeConsumeQueue(); + return size / CQ_STORE_UNIT_SIZE;// 7 in this case + } + + private int getMsgCountPerIndexFile() { + // 7 in this case + return messageStore.getMessageStoreConfig().getMaxIndexNum() - 1; + } + + private void buildAndPutMessagesToMessageStore(int msgCount) throws Exception { + int msgLen = topic.getBytes(CHARSET_UTF8).length + 91; + Map properties = new HashMap<>(4); + properties.put(MessageConst.PROPERTY_KEYS, keys); + String s = MessageDecoder.messageProperties2String(properties); + int propertiesLen = s.getBytes(CHARSET_UTF8).length; + int commitLogEndFileMinBlankLength = 4 + 4; + int singleMsgBodyLen = mappedFileSize - msgLen - propertiesLen - commitLogEndFileMinBlankLength; + + for (int i = 0; i < msgCount; i++) { + MessageExtBrokerInner msg = new MessageExtBrokerInner(); + msg.setTopic(topic); + msg.setBody(new byte[singleMsgBodyLen]); + msg.setKeys(keys); + msg.setQueueId(queueId); + msg.setSysFlag(0); + msg.setBornTimestamp(System.currentTimeMillis()); + msg.setStoreHost(storeHost); + msg.setBornHost(bornHost); + msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); + PutMessageResult result = messageStore.putMessage(msg); + assertTrue(result != null && result.isOk()); + } + + StoreTestUtil.waitCommitLogReput(messageStore); + StoreTestUtil.flushConsumeQueue(messageStore); + StoreTestUtil.flushConsumeIndex(messageStore); + } + + private void expireFiles(MappedFileQueue commitLogQueue, int expireCount) { + for (int i = 0; i < commitLogQueue.getMappedFiles().size(); i++) { + MappedFile mappedFile = commitLogQueue.getMappedFiles().get(i); + int reservedTime = fileReservedTime * 60 * 60 * 1000; + if (i < expireCount) { + boolean modified = mappedFile.getFile().setLastModified(System.currentTimeMillis() - reservedTime * 2); + assertTrue(modified); + } + } + } + + private void initMessageStore(String deleteWhen, int diskMaxUsedSpaceRatio, double diskSpaceCleanForciblyRatio) throws Exception { + initMessageStore(genMessageStoreConfig(deleteWhen,diskMaxUsedSpaceRatio), diskSpaceCleanForciblyRatio); + } + + private MessageStoreConfig genMessageStoreConfig(String deleteWhen, int diskMaxUsedSpaceRatio) { + MessageStoreConfig messageStoreConfig = new MessageStoreConfigForTest(); + messageStoreConfig.setMappedFileSizeCommitLog(mappedFileSize); + messageStoreConfig.setMappedFileSizeConsumeQueue(mappedFileSize); + messageStoreConfig.setMaxHashSlotNum(100); + messageStoreConfig.setMaxIndexNum(8); + messageStoreConfig.setFlushDiskType(FlushDiskType.SYNC_FLUSH); + messageStoreConfig.setFlushIntervalConsumeQueue(1); + + // Invalidate DefaultMessageStore`s scheduled task of cleaning expired files. + // work with the code 'Thread.sleep(1000 * 60 + 100)' behind. + messageStoreConfig.setCleanResourceInterval(Integer.MAX_VALUE); + + messageStoreConfig.setFileReservedTime(fileReservedTime); + messageStoreConfig.setDeleteWhen(deleteWhen); + messageStoreConfig.setDiskMaxUsedSpaceRatio(diskMaxUsedSpaceRatio); + + String storePathRootDir = System.getProperty("java.io.tmpdir") + File.separator + + "DefaultMessageStoreCleanFilesTest-" + UUID.randomUUID(); + String storePathCommitLog = storePathRootDir + File.separator + "commitlog"; + messageStoreConfig.setStorePathRootDir(storePathRootDir); + messageStoreConfig.setStorePathCommitLog(storePathCommitLog); + return messageStoreConfig; + } + + private void initMessageStore(MessageStoreConfig messageStoreConfig, double diskSpaceCleanForciblyRatio) throws Exception { + messageStore = new DefaultMessageStore(messageStoreConfig, + new BrokerStatsManager("test", true), new MyMessageArrivingListener(), new BrokerConfig(), new ConcurrentHashMap<>()); + + cleanCommitLogService = getCleanCommitLogService(); + cleanConsumeQueueService = getCleanConsumeQueueService(); + + assertTrue(messageStore.load()); + messageStore.start(); + + // partially mock a real obj + cleanCommitLogService = spy(cleanCommitLogService); + when(cleanCommitLogService.getDiskSpaceWarningLevelRatio()).thenReturn(diskSpaceCleanForciblyRatio); + when(cleanCommitLogService.getDiskSpaceCleanForciblyRatio()).thenReturn(diskSpaceCleanForciblyRatio); + + putFiledBackToMessageStore(cleanCommitLogService); + } + + private void putFiledBackToMessageStore(DefaultMessageStore.CleanCommitLogService cleanCommitLogService) throws Exception { + Field cleanCommitLogServiceField = DefaultMessageStore.class.getDeclaredField("cleanCommitLogService"); + cleanCommitLogServiceField.setAccessible(true); + cleanCommitLogServiceField.set(messageStore, cleanCommitLogService); + cleanCommitLogServiceField.setAccessible(false); + } + + private class MyMessageArrivingListener implements MessageArrivingListener { + @Override + public void arriving(String topic, int queueId, long logicOffset, long tagsCode, long msgStoreTime, + byte[] filterBitMap, Map properties) { + } + } + + @After + public void destroy() { + messageStore.shutdown(); + messageStore.destroy(); + + MessageStoreConfig messageStoreConfig = messageStore.getMessageStoreConfig(); + File file = new File(messageStoreConfig.getStorePathRootDir()); + UtilAll.deleteFile(file); + } + + private class MessageStoreConfigForTest extends MessageStoreConfig { + @Override + public int getDiskMaxUsedSpaceRatio() { + try { + Field diskMaxUsedSpaceRatioField = this.getClass().getSuperclass().getDeclaredField("diskMaxUsedSpaceRatio"); + diskMaxUsedSpaceRatioField.setAccessible(true); + int ratio = (int) diskMaxUsedSpaceRatioField.get(this); + diskMaxUsedSpaceRatioField.setAccessible(false); + return ratio; + } catch (Exception ignored) { + } + return super.getDiskMaxUsedSpaceRatio(); + } + } +} diff --git a/store/src/test/java/org/apache/rocketmq/store/DefaultMessageStoreShutDownTest.java b/store/src/test/java/org/apache/rocketmq/store/DefaultMessageStoreShutDownTest.java new file mode 100644 index 00000000000..515a4845a4e --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/DefaultMessageStoreShutDownTest.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store; + +import java.io.File; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.store.config.FlushDiskType; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.config.StorePathConfigHelper; +import org.apache.rocketmq.store.stats.BrokerStatsManager; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class DefaultMessageStoreShutDownTest { + private DefaultMessageStore messageStore; + + @Before + public void init() throws Exception { + DefaultMessageStore store = buildMessageStore(); + boolean load = store.load(); + assertTrue(load); + store.start(); + messageStore = spy(store); + when(messageStore.dispatchBehindBytes()).thenReturn(100L); + } + + @Test + public void testDispatchBehindWhenShutdown() { + messageStore.shutdown(); + assertTrue(!messageStore.shutDownNormal); + File file = new File(StorePathConfigHelper.getAbortFile(messageStore.getMessageStoreConfig().getStorePathRootDir())); + assertTrue(file.exists()); + } + + @After + public void destroy() { + messageStore.destroy(); + File file = new File(messageStore.getMessageStoreConfig().getStorePathRootDir()); + UtilAll.deleteFile(file); + } + + public DefaultMessageStore buildMessageStore() throws Exception { + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setMappedFileSizeCommitLog(1024 * 1024 * 10); + messageStoreConfig.setMappedFileSizeConsumeQueue(1024 * 1024 * 10); + messageStoreConfig.setMaxHashSlotNum(10000); + messageStoreConfig.setMaxIndexNum(100 * 100); + messageStoreConfig.setFlushDiskType(FlushDiskType.SYNC_FLUSH); + messageStoreConfig.setHaListenPort(0); + String storeRootPath = System.getProperty("java.io.tmpdir") + File.separator + "store"; + messageStoreConfig.setStorePathRootDir(storeRootPath); + messageStoreConfig.setHaListenPort(0); + return new DefaultMessageStore(messageStoreConfig, new BrokerStatsManager("simpleTest", true), null, new BrokerConfig(), new ConcurrentHashMap<>()); + } + +} diff --git a/store/src/test/java/org/apache/rocketmq/store/DefaultMessageStoreShuwDownTest.java b/store/src/test/java/org/apache/rocketmq/store/DefaultMessageStoreShuwDownTest.java deleted file mode 100644 index ac85d59f1c7..00000000000 --- a/store/src/test/java/org/apache/rocketmq/store/DefaultMessageStoreShuwDownTest.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.rocketmq.store; - -import java.io.File; -import org.apache.rocketmq.common.BrokerConfig; -import org.apache.rocketmq.common.UtilAll; -import org.apache.rocketmq.store.config.FlushDiskType; -import org.apache.rocketmq.store.config.MessageStoreConfig; -import org.apache.rocketmq.store.config.StorePathConfigHelper; -import org.apache.rocketmq.store.stats.BrokerStatsManager; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; - -@RunWith(MockitoJUnitRunner.class) -public class DefaultMessageStoreShuwDownTest { - private DefaultMessageStore messageStore; - - @Before - public void init() throws Exception { - messageStore = spy(buildMessageStore()); - boolean load = messageStore.load(); - when(messageStore.dispatchBehindBytes()).thenReturn(100L); - assertTrue(load); - messageStore.start(); - } - - @Test - public void testDispatchBehindWhenShutDown() { - messageStore.shutdown(); - assertTrue(!messageStore.shutDownNormal); - File file = new File(StorePathConfigHelper.getAbortFile(messageStore.getMessageStoreConfig().getStorePathRootDir())); - assertTrue(file.exists()); - } - - @After - public void destory() { - messageStore.destroy(); - File file = new File(messageStore.getMessageStoreConfig().getStorePathRootDir()); - UtilAll.deleteFile(file); - } - - public DefaultMessageStore buildMessageStore() throws Exception { - MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); - messageStoreConfig.setMapedFileSizeCommitLog(1024 * 1024 * 10); - messageStoreConfig.setMapedFileSizeConsumeQueue(1024 * 1024 * 10); - messageStoreConfig.setMaxHashSlotNum(10000); - messageStoreConfig.setMaxIndexNum(100 * 100); - messageStoreConfig.setFlushDiskType(FlushDiskType.SYNC_FLUSH); - return new DefaultMessageStore(messageStoreConfig, new BrokerStatsManager("simpleTest"), null, new BrokerConfig()); - } - - -} diff --git a/store/src/test/java/org/apache/rocketmq/store/DefaultMessageStoreTest.java b/store/src/test/java/org/apache/rocketmq/store/DefaultMessageStoreTest.java index 9269cdfa772..1d09ca86ecb 100644 --- a/store/src/test/java/org/apache/rocketmq/store/DefaultMessageStoreTest.java +++ b/store/src/test/java/org/apache/rocketmq/store/DefaultMessageStoreTest.java @@ -17,39 +17,73 @@ package org.apache.rocketmq.store; +import com.google.common.collect.Sets; import java.io.File; +import java.io.RandomAccessFile; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; import java.nio.channels.OverlappingFileLockException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; - import org.apache.rocketmq.common.BrokerConfig; import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageBatch; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBatch; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.store.config.BrokerRole; import org.apache.rocketmq.store.config.FlushDiskType; import org.apache.rocketmq.store.config.MessageStoreConfig; -import org.junit.After; +import org.apache.rocketmq.store.config.StorePathConfigHelper; +import org.apache.rocketmq.store.queue.ConsumeQueueInterface; +import org.apache.rocketmq.store.queue.CqUnit; import org.apache.rocketmq.store.stats.BrokerStatsManager; +import org.assertj.core.util.Strings; +import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +@RunWith(MockitoJUnitRunner.class) public class DefaultMessageStoreTest { - private final String StoreMessage = "Once, there was a chance for me!"; - private int QUEUE_TOTAL = 100; - private AtomicInteger QueueId = new AtomicInteger(0); - private SocketAddress BornHost; - private SocketAddress StoreHost; - private byte[] MessageBody; + private final String storeMessage = "Once, there was a chance for me!"; + private final String messageTopic = "FooBar"; + private int queueTotal = 100; + private AtomicInteger queueId = new AtomicInteger(0); + private SocketAddress bornHost; + private SocketAddress storeHost; + private byte[] messageBody; private MessageStore messageStore; @Before public void init() throws Exception { - StoreHost = new InetSocketAddress(InetAddress.getLocalHost(), 8123); - BornHost = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0); + storeHost = new InetSocketAddress(InetAddress.getLocalHost(), 8123); + bornHost = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0); messageStore = buildMessageStore(); boolean load = messageStore.load(); @@ -58,17 +92,18 @@ public void init() throws Exception { } @Test(expected = OverlappingFileLockException.class) - public void test_repate_restart() throws Exception { - long totalMsgs = 100; - QUEUE_TOTAL = 1; - MessageBody = StoreMessage.getBytes(); + public void test_repeat_restart() throws Exception { + queueTotal = 1; + messageBody = storeMessage.getBytes(); MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); - messageStoreConfig.setMapedFileSizeCommitLog(1024 * 8); - messageStoreConfig.setMapedFileSizeConsumeQueue(1024 * 4); + messageStoreConfig.setMappedFileSizeCommitLog(1024 * 8); + messageStoreConfig.setMappedFileSizeConsumeQueue(1024 * 4); messageStoreConfig.setMaxHashSlotNum(100); messageStoreConfig.setMaxIndexNum(100 * 10); - MessageStore master = new DefaultMessageStore(messageStoreConfig, null, new MyMessageArrivingListener(), new BrokerConfig()); + messageStoreConfig.setStorePathRootDir(System.getProperty("java.io.tmpdir") + File.separator + "store"); + messageStoreConfig.setHaListenPort(0); + MessageStore master = new DefaultMessageStore(messageStoreConfig, null, new MyMessageArrivingListener(), new BrokerConfig(), new ConcurrentHashMap<>()); boolean load = master.load(); assertTrue(load); @@ -83,7 +118,7 @@ public void test_repate_restart() throws Exception { } @After - public void destory() { + public void destroy() { messageStore.shutdown(); messageStore.destroy(); @@ -92,53 +127,405 @@ public void destory() { UtilAll.deleteFile(file); } - public MessageStore buildMessageStore() throws Exception { + private MessageStore buildMessageStore() throws Exception { + return buildMessageStore(null); + } + + private MessageStore buildMessageStore(String storePathRootDir) throws Exception { MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); - messageStoreConfig.setMapedFileSizeCommitLog(1024 * 1024 * 10); - messageStoreConfig.setMapedFileSizeConsumeQueue(1024 * 1024 * 10); + messageStoreConfig.setMappedFileSizeCommitLog(1024 * 1024 * 10); + messageStoreConfig.setMappedFileSizeConsumeQueue(1024 * 1024 * 10); messageStoreConfig.setMaxHashSlotNum(10000); messageStoreConfig.setMaxIndexNum(100 * 100); messageStoreConfig.setFlushDiskType(FlushDiskType.SYNC_FLUSH); - return new DefaultMessageStore(messageStoreConfig, new BrokerStatsManager("simpleTest"), new MyMessageArrivingListener(), new BrokerConfig()); + messageStoreConfig.setFlushIntervalConsumeQueue(1); + messageStoreConfig.setHaListenPort(0); + if (Strings.isNullOrEmpty(storePathRootDir)) { + UUID uuid = UUID.randomUUID(); + storePathRootDir = System.getProperty("java.io.tmpdir") + File.separator + "store-" + uuid.toString(); + } + messageStoreConfig.setStorePathRootDir(storePathRootDir); + return new DefaultMessageStore(messageStoreConfig, + new BrokerStatsManager("simpleTest", true), + new MyMessageArrivingListener(), + new BrokerConfig(), new ConcurrentHashMap<>()); } @Test - public void testWriteAndRead() throws Exception { - long totalMsgs = 100; - QUEUE_TOTAL = 1; - MessageBody = StoreMessage.getBytes(); - for (long i = 0; i < totalMsgs; i++) { + public void testWriteAndRead() { + long ipv4HostMsgs = 10; + long ipv6HostMsgs = 10; + long totalMsgs = ipv4HostMsgs + ipv6HostMsgs; + queueTotal = 1; + messageBody = storeMessage.getBytes(); + for (long i = 0; i < ipv4HostMsgs; i++) { messageStore.putMessage(buildMessage()); } + for (long i = 0; i < ipv6HostMsgs; i++) { + messageStore.putMessage(buildIPv6HostMessage()); + } + + StoreTestUtil.waitCommitLogReput((DefaultMessageStore) messageStore); + for (long i = 0; i < totalMsgs; i++) { - GetMessageResult result = messageStore.getMessage("GROUP_A", "TOPIC_A", 0, i, 1024 * 1024, null); + GetMessageResult result = messageStore.getMessage("GROUP_A", "FooBar", 0, i, 1024 * 1024, null); assertThat(result).isNotNull(); result.release(); } verifyThatMasterIsFunctional(totalMsgs, messageStore); } - public MessageExtBrokerInner buildMessage() { + @Test + public void testLookMessageByOffset_OffsetIsFirst() { + final int totalCount = 10; + int queueId = new Random().nextInt(10); + String topic = "FooBar"; + int firstOffset = 0; + AppendMessageResult[] appendMessageResultArray = putMessages(totalCount, topic, queueId); + AppendMessageResult firstResult = appendMessageResultArray[0]; + + MessageExt messageExt = messageStore.lookMessageByOffset(firstResult.getWroteOffset()); + MessageExt messageExt1 = getDefaultMessageStore().lookMessageByOffset(firstResult.getWroteOffset(), firstResult.getWroteBytes()); + + assertThat(new String(messageExt.getBody())).isEqualTo(buildMessageBodyByOffset(storeMessage, firstOffset)); + assertThat(new String(messageExt1.getBody())).isEqualTo(buildMessageBodyByOffset(storeMessage, firstOffset)); + } + + @Test + public void testLookMessageByOffset_OffsetIsLast() { + final int totalCount = 10; + int queueId = new Random().nextInt(10); + String topic = "FooBar"; + AppendMessageResult[] appendMessageResultArray = putMessages(totalCount, topic, queueId); + int lastIndex = totalCount - 1; + AppendMessageResult lastResult = appendMessageResultArray[lastIndex]; + + MessageExt messageExt = getDefaultMessageStore().lookMessageByOffset(lastResult.getWroteOffset(), lastResult.getWroteBytes()); + + assertThat(new String(messageExt.getBody())).isEqualTo(buildMessageBodyByOffset(storeMessage, lastIndex)); + } + + @Test + public void testLookMessageByOffset_OffsetIsOutOfBound() { + final int totalCount = 10; + int queueId = new Random().nextInt(10); + String topic = "FooBar"; + AppendMessageResult[] appendMessageResultArray = putMessages(totalCount, topic, queueId); + long lastOffset = getMaxOffset(appendMessageResultArray); + + MessageExt messageExt = getDefaultMessageStore().lookMessageByOffset(lastOffset); + + assertThat(messageExt).isNull(); + } + + @Test + public void testGetOffsetInQueueByTime() { + final int totalCount = 10; + int queueId = 0; + String topic = "FooBar"; + AppendMessageResult[] appendMessageResults = putMessages(totalCount, topic, queueId, true); + //Thread.sleep(10); + StoreTestUtil.waitCommitLogReput((DefaultMessageStore) messageStore); + + ConsumeQueueInterface consumeQueue = getDefaultMessageStore().findConsumeQueue(topic, queueId); + for (AppendMessageResult appendMessageResult : appendMessageResults) { + long offset = messageStore.getOffsetInQueueByTime(topic, queueId, appendMessageResult.getStoreTimestamp()); + CqUnit cqUnit = consumeQueue.get(offset); + assertThat(cqUnit.getPos()).isEqualTo(appendMessageResult.getWroteOffset()); + assertThat(cqUnit.getSize()).isEqualTo(appendMessageResult.getWroteBytes()); + } + } + + @Test + public void testGetOffsetInQueueByTime_TimestampIsSkewing() { + final int totalCount = 10; + int queueId = 0; + String topic = "FooBar"; + AppendMessageResult[] appendMessageResults = putMessages(totalCount, topic, queueId, true); + //Thread.sleep(10); + StoreTestUtil.waitCommitLogReput((DefaultMessageStore) messageStore); + int skewing = 2; + + ConsumeQueueInterface consumeQueue = getDefaultMessageStore().findConsumeQueue(topic, queueId); + for (AppendMessageResult appendMessageResult : appendMessageResults) { + long offset = messageStore.getOffsetInQueueByTime(topic, queueId, appendMessageResult.getStoreTimestamp() - skewing); + CqUnit cqUnit = consumeQueue.get(offset); + assertThat(cqUnit.getPos()).isEqualTo(appendMessageResult.getWroteOffset()); + assertThat(cqUnit.getSize()).isEqualTo(appendMessageResult.getWroteBytes()); + } + } + + @Test + public void testGetOffsetInQueueByTime_TimestampSkewingIsLarge() { + final int totalCount = 10; + int queueId = 0; + String topic = "FooBar"; + AppendMessageResult[] appendMessageResults = putMessages(totalCount, topic, queueId, true); + //Thread.sleep(10); + StoreTestUtil.waitCommitLogReput((DefaultMessageStore) messageStore); + int skewing = 20000; + + ConsumeQueueInterface consumeQueue = getDefaultMessageStore().findConsumeQueue(topic, queueId); + for (AppendMessageResult appendMessageResult : appendMessageResults) { + long offset = messageStore.getOffsetInQueueByTime(topic, queueId, appendMessageResult.getStoreTimestamp() - skewing); + CqUnit cqUnit = consumeQueue.get(offset); + assertThat(cqUnit.getPos()).isEqualTo(appendMessageResults[0].getWroteOffset()); + assertThat(cqUnit.getSize()).isEqualTo(appendMessageResults[0].getWroteBytes()); + } + } + + @Test + public void testGetOffsetInQueueByTime_ConsumeQueueNotFound1() { + final int totalCount = 10; + int queueId = 0; + int wrongQueueId = 1; + String topic = "FooBar"; + AppendMessageResult[] appendMessageResults = putMessages(totalCount, topic, queueId, false); + //Thread.sleep(10); + + StoreTestUtil.waitCommitLogReput((DefaultMessageStore) messageStore); + + long offset = messageStore.getOffsetInQueueByTime(topic, wrongQueueId, appendMessageResults[0].getStoreTimestamp()); + + assertThat(offset).isEqualTo(0); + } + + @Test + public void testGetOffsetInQueueByTime_ConsumeQueueNotFound2() { + final int totalCount = 10; + int queueId = 0; + int wrongQueueId = 1; + String topic = "FooBar"; + putMessages(totalCount, topic, queueId, false); + //Thread.sleep(10); + StoreTestUtil.waitCommitLogReput((DefaultMessageStore) messageStore); + + long messageStoreTimeStamp = messageStore.getMessageStoreTimeStamp(topic, wrongQueueId, 0); + + assertThat(messageStoreTimeStamp).isEqualTo(-1); + } + + @Test + public void testGetOffsetInQueueByTime_ConsumeQueueOffsetNotExist() { + final int totalCount = 10; + int queueId = 0; + int wrongQueueId = 1; + String topic = "FooBar"; + putMessages(totalCount, topic, queueId, true); + //Thread.sleep(10); + + StoreTestUtil.waitCommitLogReput((DefaultMessageStore) messageStore); + + long messageStoreTimeStamp = messageStore.getMessageStoreTimeStamp(topic, wrongQueueId, -1); + + assertThat(messageStoreTimeStamp).isEqualTo(-1); + } + + @Test + public void testGetMessageStoreTimeStamp() { + final int totalCount = 10; + int queueId = 0; + String topic = "FooBar"; + AppendMessageResult[] appendMessageResults = putMessages(totalCount, topic, queueId, false); + //Thread.sleep(10); + StoreTestUtil.waitCommitLogReput((DefaultMessageStore) messageStore); + + ConsumeQueueInterface consumeQueue = getDefaultMessageStore().findConsumeQueue(topic, queueId); + int minOffsetInQueue = (int) consumeQueue.getMinOffsetInQueue(); + for (int i = minOffsetInQueue; i < consumeQueue.getMaxOffsetInQueue(); i++) { + long messageStoreTimeStamp = messageStore.getMessageStoreTimeStamp(topic, queueId, i); + assertThat(messageStoreTimeStamp).isEqualTo(appendMessageResults[i].getStoreTimestamp()); + } + } + + @Test + public void testGetStoreTime_ParamIsNull() { + long storeTime = getStoreTime(null); + + assertThat(storeTime).isEqualTo(-1); + } + + @Test + public void testGetStoreTime_EverythingIsOk() { + final int totalCount = 10; + int queueId = 0; + String topic = "FooBar"; + AppendMessageResult[] appendMessageResults = putMessages(totalCount, topic, queueId, false); + //Thread.sleep(10); + StoreTestUtil.waitCommitLogReput((DefaultMessageStore) messageStore); + ConsumeQueueInterface consumeQueue = messageStore.getConsumeQueue(topic, queueId); + + for (int i = 0; i < totalCount; i++) { + CqUnit cqUnit = consumeQueue.get(i); + long storeTime = getStoreTime(cqUnit); + assertThat(storeTime).isEqualTo(appendMessageResults[i].getStoreTimestamp()); + } + } + + @Test + public void testGetStoreTime_PhyOffsetIsLessThanCommitLogMinOffset() { + long phyOffset = -10; + int size = 138; + CqUnit cqUnit = new CqUnit(0, phyOffset, size, 0); + long storeTime = getStoreTime(cqUnit); + + assertThat(storeTime).isEqualTo(-1); + } + + @Test + public void testPutMessage_whenMessagePropertyIsTooLong() { + String topicName = "messagePropertyIsTooLongTest"; + MessageExtBrokerInner illegalMessage = buildSpecifyLengthPropertyMessage("123".getBytes(StandardCharsets.UTF_8), topicName, Short.MAX_VALUE + 1); + assertEquals(messageStore.putMessage(illegalMessage).getPutMessageStatus(), PutMessageStatus.PROPERTIES_SIZE_EXCEEDED); + assertEquals(0L, messageStore.getQueueStore().getMaxOffset(topicName, 0).longValue()); + MessageExtBrokerInner normalMessage = buildSpecifyLengthPropertyMessage("123".getBytes(StandardCharsets.UTF_8), topicName, 100); + assertEquals(messageStore.putMessage(normalMessage).getPutMessageStatus(), PutMessageStatus.PUT_OK); + assertEquals(1L, messageStore.getQueueStore().getMaxOffset(topicName, 0).longValue()); + } + + private DefaultMessageStore getDefaultMessageStore() { + return (DefaultMessageStore) this.messageStore; + } + + private AppendMessageResult[] putMessages(int totalCount, String topic, int queueId) { + return putMessages(totalCount, topic, queueId, false); + } + + private AppendMessageResult[] putMessages(int totalCount, String topic, int queueId, boolean interval) { + AppendMessageResult[] appendMessageResultArray = new AppendMessageResult[totalCount]; + for (int i = 0; i < totalCount; i++) { + String messageBody = buildMessageBodyByOffset(storeMessage, i); + + MessageExtBrokerInner msgInner = + i < totalCount / 2 ? buildMessage(messageBody.getBytes(), topic) : buildIPv6HostMessage(messageBody.getBytes(), topic); + msgInner.setQueueId(queueId); + PutMessageResult result = messageStore.putMessage(msgInner); + appendMessageResultArray[i] = result.getAppendMessageResult(); + assertThat(result.getPutMessageStatus()).isEqualTo(PutMessageStatus.PUT_OK); + if (interval) { + try { + Thread.sleep(10); + } catch (InterruptedException e) { + throw new RuntimeException("Thread sleep ERROR"); + } + } + } + return appendMessageResultArray; + } + + private long getMaxOffset(AppendMessageResult[] appendMessageResultArray) { + if (appendMessageResultArray == null) { + return 0; + } + AppendMessageResult last = appendMessageResultArray[appendMessageResultArray.length - 1]; + return last.getWroteOffset() + last.getWroteBytes(); + } + + private String buildMessageBodyByOffset(String message, long i) { + return String.format("%s offset %d", message, i); + } + + private long getStoreTime(CqUnit cqUnit) { + try { + Class abstractConsumeQueueStore = getDefaultMessageStore().getQueueStore().getClass().getSuperclass(); + Method getStoreTime = abstractConsumeQueueStore.getDeclaredMethod("getStoreTime", CqUnit.class); + getStoreTime.setAccessible(true); + return (long) getStoreTime.invoke(getDefaultMessageStore().getQueueStore(), cqUnit); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + private MessageExtBrokerInner buildMessage(byte[] messageBody, String topic) { MessageExtBrokerInner msg = new MessageExtBrokerInner(); - msg.setTopic("FooBar"); + msg.setTopic(topic); msg.setTags("TAG1"); msg.setKeys("Hello"); - msg.setBody(MessageBody); + msg.setBody(messageBody); msg.setKeys(String.valueOf(System.currentTimeMillis())); - msg.setQueueId(Math.abs(QueueId.getAndIncrement()) % QUEUE_TOTAL); + msg.setQueueId(Math.abs(queueId.getAndIncrement()) % queueTotal); msg.setSysFlag(0); msg.setBornTimestamp(System.currentTimeMillis()); - msg.setStoreHost(StoreHost); - msg.setBornHost(BornHost); + msg.setStoreHost(storeHost); + msg.setBornHost(bornHost); + msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); + return msg; + } + + private MessageExtBrokerInner buildSpecifyLengthPropertyMessage(byte[] messageBody, String topic, int length) { + StringBuilder stringBuilder = new StringBuilder(); + Random random = new Random(); + for (int i = 0; i < length; i++) { + stringBuilder.append(random.nextInt(10)); + } + MessageExtBrokerInner msg = new MessageExtBrokerInner(); + msg.putUserProperty("test", stringBuilder.toString()); + msg.setTopic(topic); + msg.setTags("TAG1"); + msg.setKeys("Hello"); + msg.setBody(messageBody); + msg.setQueueId(0); + msg.setBornTimestamp(System.currentTimeMillis()); + msg.setStoreHost(storeHost); + msg.setBornHost(bornHost); + msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); return msg; } + private MessageExtBrokerInner buildIPv6HostMessage(byte[] messageBody, String topic) { + MessageExtBrokerInner msg = new MessageExtBrokerInner(); + msg.setTopic(topic); + msg.setTags("TAG1"); + msg.setKeys("Hello"); + msg.setBody(messageBody); + msg.setMsgId("24084004018081003FAA1DDE2B3F898A00002A9F0000000000000CA0"); + msg.setKeys(String.valueOf(System.currentTimeMillis())); + msg.setQueueId(Math.abs(queueId.getAndIncrement()) % queueTotal); + msg.setSysFlag(0); + msg.setBornHostV6Flag(); + msg.setStoreHostAddressV6Flag(); + msg.setBornTimestamp(System.currentTimeMillis()); + try { + msg.setBornHost(new InetSocketAddress(InetAddress.getByName("1050:0000:0000:0000:0005:0600:300c:326b"), 0)); + } catch (UnknownHostException e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + + try { + msg.setStoreHost(new InetSocketAddress(InetAddress.getByName("::1"), 0)); + } catch (UnknownHostException e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + return msg; + } + + private MessageExtBrokerInner buildMessage() { + return buildMessage(messageBody, messageTopic); + } + + public MessageExtBatch buildMessageBatch(MessageBatch msgBatch) { + MessageExtBatch msgExtBatch = new MessageExtBatch(); + msgExtBatch.setTopic(messageTopic); + msgExtBatch.setTags("TAG1"); + msgExtBatch.setKeys("Hello"); + msgExtBatch.setBody(msgBatch.getBody()); + msgExtBatch.setKeys(String.valueOf(System.currentTimeMillis())); + msgExtBatch.setQueueId(Math.abs(queueId.getAndIncrement()) % queueTotal); + msgExtBatch.setSysFlag(0); + msgExtBatch.setBornTimestamp(System.currentTimeMillis()); + msgExtBatch.setStoreHost(storeHost); + msgExtBatch.setBornHost(bornHost); + return msgExtBatch; + } + @Test public void testGroupCommit() throws Exception { long totalMsgs = 10; - QUEUE_TOTAL = 1; - MessageBody = StoreMessage.getBytes(); + queueTotal = 1; + messageBody = storeMessage.getBytes(); for (long i = 0; i < totalMsgs; i++) { messageStore.putMessage(buildMessage()); } @@ -151,13 +538,55 @@ public void testGroupCommit() throws Exception { verifyThatMasterIsFunctional(totalMsgs, messageStore); } + @Test + public void testMaxOffset() throws InterruptedException { + int firstBatchMessages = 3; + int queueId = 0; + messageBody = storeMessage.getBytes(); + + assertThat(messageStore.getMaxOffsetInQueue(messageTopic, queueId)).isEqualTo(0); + + for (int i = 0; i < firstBatchMessages; i++) { + final MessageExtBrokerInner msg = buildMessage(); + msg.setQueueId(queueId); + messageStore.putMessage(msg); + } + + while (messageStore.dispatchBehindBytes() != 0) { + TimeUnit.MILLISECONDS.sleep(1); + } + + assertThat(messageStore.getMaxOffsetInQueue(messageTopic, queueId)).isEqualTo(firstBatchMessages); + + // Disable the dispatcher + messageStore.getDispatcherList().clear(); + + int secondBatchMessages = 2; + + for (int i = 0; i < secondBatchMessages; i++) { + final MessageExtBrokerInner msg = buildMessage(); + msg.setQueueId(queueId); + messageStore.putMessage(msg); + } + + assertThat(messageStore.getMaxOffsetInQueue(messageTopic, queueId)).isEqualTo(firstBatchMessages); + assertThat(messageStore.getMaxOffsetInQueue(messageTopic, queueId, true)).isEqualTo(firstBatchMessages); + assertThat(messageStore.getMaxOffsetInQueue(messageTopic, queueId, false)).isEqualTo(firstBatchMessages + secondBatchMessages); + } + + private MessageExtBrokerInner buildIPv6HostMessage() { + return buildIPv6HostMessage(messageBody, "FooBar"); + } + private void verifyThatMasterIsFunctional(long totalMsgs, MessageStore master) { for (long i = 0; i < totalMsgs; i++) { master.putMessage(buildMessage()); } + StoreTestUtil.waitCommitLogReput((DefaultMessageStore) messageStore); + for (long i = 0; i < totalMsgs; i++) { - GetMessageResult result = master.getMessage("GROUP_A", "TOPIC_A", 0, i, 1024 * 1024, null); + GetMessageResult result = master.getMessage("GROUP_A", "FooBar", 0, i, 1024 * 1024, null); assertThat(result).isNotNull(); result.release(); @@ -174,23 +603,360 @@ public void testPullSize() throws Exception { messageExtBrokerInner.setQueueId(0); messageStore.putMessage(messageExtBrokerInner); } - //wait for consume queue build - Thread.sleep(10); + // wait for consume queue build + // the sleep time should be great than consume queue flush interval + //Thread.sleep(100); + StoreTestUtil.waitCommitLogReput((DefaultMessageStore) messageStore); String group = "simple"; GetMessageResult getMessageResult32 = messageStore.getMessage(group, topic, 0, 0, 32, null); assertThat(getMessageResult32.getMessageBufferList().size()).isEqualTo(32); + getMessageResult32.release(); GetMessageResult getMessageResult20 = messageStore.getMessage(group, topic, 0, 0, 20, null); assertThat(getMessageResult20.getMessageBufferList().size()).isEqualTo(20); + getMessageResult20.release(); GetMessageResult getMessageResult45 = messageStore.getMessage(group, topic, 0, 0, 10, null); assertThat(getMessageResult45.getMessageBufferList().size()).isEqualTo(10); + getMessageResult45.release(); + + } + + @Test + public void testRecover() throws Exception { + String topic = "recoverTopic"; + messageBody = storeMessage.getBytes(); + for (int i = 0; i < 100; i++) { + MessageExtBrokerInner messageExtBrokerInner = buildMessage(); + messageExtBrokerInner.setTopic(topic); + messageExtBrokerInner.setQueueId(0); + messageStore.putMessage(messageExtBrokerInner); + } + + // Thread.sleep(100);//wait for build consumer queue + StoreTestUtil.waitCommitLogReput((DefaultMessageStore) messageStore); + + long maxPhyOffset = messageStore.getMaxPhyOffset(); + long maxCqOffset = messageStore.getMaxOffsetInQueue(topic, 0); + + //1.just reboot + messageStore.shutdown(); + String storeRootDir = ((DefaultMessageStore) messageStore).getMessageStoreConfig().getStorePathRootDir(); + messageStore = buildMessageStore(storeRootDir); + boolean load = messageStore.load(); + assertTrue(load); + messageStore.start(); + assertTrue(maxPhyOffset == messageStore.getMaxPhyOffset()); + assertTrue(maxCqOffset == messageStore.getMaxOffsetInQueue(topic, 0)); + + //2.damage commit-log and reboot normal + for (int i = 0; i < 100; i++) { + MessageExtBrokerInner messageExtBrokerInner = buildMessage(); + messageExtBrokerInner.setTopic(topic); + messageExtBrokerInner.setQueueId(0); + messageStore.putMessage(messageExtBrokerInner); + } + //Thread.sleep(100); + StoreTestUtil.waitCommitLogReput((DefaultMessageStore) messageStore); + long secondLastPhyOffset = messageStore.getMaxPhyOffset(); + long secondLastCqOffset = messageStore.getMaxOffsetInQueue(topic, 0); + + MessageExtBrokerInner messageExtBrokerInner = buildMessage(); + messageExtBrokerInner.setTopic(topic); + messageExtBrokerInner.setQueueId(0); + messageStore.putMessage(messageExtBrokerInner); + + messageStore.shutdown(); + + //damage last message + damageCommitLog((DefaultMessageStore) messageStore, secondLastPhyOffset); + + //reboot + messageStore = buildMessageStore(storeRootDir); + load = messageStore.load(); + assertTrue(load); + messageStore.start(); + assertTrue(secondLastPhyOffset == messageStore.getMaxPhyOffset()); + assertTrue(secondLastCqOffset == messageStore.getMaxOffsetInQueue(topic, 0)); + + //3.damage commitlog and reboot abnormal + for (int i = 0; i < 100; i++) { + messageExtBrokerInner = buildMessage(); + messageExtBrokerInner.setTopic(topic); + messageExtBrokerInner.setQueueId(0); + messageStore.putMessage(messageExtBrokerInner); + } + //Thread.sleep(100); + StoreTestUtil.waitCommitLogReput((DefaultMessageStore) messageStore); + secondLastPhyOffset = messageStore.getMaxPhyOffset(); + secondLastCqOffset = messageStore.getMaxOffsetInQueue(topic, 0); + + messageExtBrokerInner = buildMessage(); + messageExtBrokerInner.setTopic(topic); + messageExtBrokerInner.setQueueId(0); + messageStore.putMessage(messageExtBrokerInner); + messageStore.shutdown(); + + //damage last message + damageCommitLog((DefaultMessageStore) messageStore, secondLastPhyOffset); + //add abort file + String fileName = StorePathConfigHelper.getAbortFile(((DefaultMessageStore) messageStore).getMessageStoreConfig().getStorePathRootDir()); + File file = new File(fileName); + UtilAll.ensureDirOK(file.getParent()); + file.createNewFile(); + + messageStore = buildMessageStore(storeRootDir); + load = messageStore.load(); + assertTrue(load); + messageStore.start(); + assertTrue(secondLastPhyOffset == messageStore.getMaxPhyOffset()); + assertTrue(secondLastCqOffset == messageStore.getMaxOffsetInQueue(topic, 0)); + + //message write again + for (int i = 0; i < 100; i++) { + messageExtBrokerInner = buildMessage(); + messageExtBrokerInner.setTopic(topic); + messageExtBrokerInner.setQueueId(0); + messageStore.putMessage(messageExtBrokerInner); + } + } + + @Test + public void testStorePathOK() { + if (messageStore instanceof DefaultMessageStore) { + assertTrue(fileExists(((DefaultMessageStore) messageStore).getStorePathPhysic())); + assertTrue(fileExists(((DefaultMessageStore) messageStore).getStorePathLogic())); + } + } + + private boolean fileExists(String path) { + if (path != null) { + File f = new File(path); + return f.exists(); + } + return false; + } + + private void damageCommitLog(DefaultMessageStore store, long offset) throws Exception { + assertThat(store).isNotNull(); + MessageStoreConfig messageStoreConfig = store.getMessageStoreConfig(); + File file = new File(messageStoreConfig.getStorePathCommitLog() + File.separator + "00000000000000000000"); + try (RandomAccessFile raf = new RandomAccessFile(file, "rw"); + FileChannel fileChannel = raf.getChannel()) { + MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 1024 * 1024 * 10); + int bodyLen = mappedByteBuffer.getInt((int) offset + 84); + int topicLenIndex = (int) offset + 84 + bodyLen + 4; + mappedByteBuffer.position(topicLenIndex); + mappedByteBuffer.putInt(0); + mappedByteBuffer.putInt(0); + mappedByteBuffer.putInt(0); + mappedByteBuffer.putInt(0); + mappedByteBuffer.force(); + fileChannel.force(true); + } + } + + @Test + public void testPutMsgExceedsMaxLength() { + messageBody = new byte[4 * 1024 * 1024 + 1]; + MessageExtBrokerInner msg = buildMessage(); + + PutMessageResult result = messageStore.putMessage(msg); + assertThat(result.getPutMessageStatus()).isEqualTo(PutMessageStatus.MESSAGE_ILLEGAL); + } + + @Test + public void testPutMsgBatchExceedsMaxLength() { + messageBody = new byte[4 * 1024 * 1024 + 1]; + MessageExtBrokerInner msg1 = buildMessage(); + MessageExtBrokerInner msg2 = buildMessage(); + MessageExtBrokerInner msg3 = buildMessage(); + + MessageBatch msgBatch = MessageBatch.generateFromList(Arrays.asList(msg1, msg2, msg3)); + msgBatch.setBody(msgBatch.encode()); + + MessageExtBatch msgExtBatch = buildMessageBatch(msgBatch); + + try { + PutMessageResult result = this.messageStore.putMessages(msgExtBatch); + } catch (Exception e) { + assertThat(e.getMessage()).contains("message body size exceeded"); + } + } + + @Test + public void testPutMsgWhenReplicasNotEnough() { + MessageStoreConfig messageStoreConfig = ((DefaultMessageStore) this.messageStore).getMessageStoreConfig(); + messageStoreConfig.setBrokerRole(BrokerRole.SYNC_MASTER); + messageStoreConfig.setTotalReplicas(2); + messageStoreConfig.setInSyncReplicas(2); + messageStoreConfig.setEnableAutoInSyncReplicas(false); + ((DefaultMessageStore) this.messageStore).getBrokerConfig().setEnableSlaveActingMaster(true); + this.messageStore.setAliveReplicaNumInGroup(1); + + MessageExtBrokerInner msg = buildMessage(); + PutMessageResult result = this.messageStore.putMessage(msg); + assertThat(result.getPutMessageStatus()).isEqualTo(PutMessageStatus.IN_SYNC_REPLICAS_NOT_ENOUGH); + ((DefaultMessageStore) this.messageStore).getBrokerConfig().setEnableSlaveActingMaster(false); + } + + @Test + public void testPutMsgWhenAdaptiveDegradation() { + MessageStoreConfig messageStoreConfig = ((DefaultMessageStore) this.messageStore).getMessageStoreConfig(); + messageStoreConfig.setBrokerRole(BrokerRole.SYNC_MASTER); + messageStoreConfig.setTotalReplicas(2); + messageStoreConfig.setInSyncReplicas(2); + messageStoreConfig.setEnableAutoInSyncReplicas(true); + ((DefaultMessageStore) this.messageStore).getBrokerConfig().setEnableSlaveActingMaster(true); + this.messageStore.setAliveReplicaNumInGroup(1); + + MessageExtBrokerInner msg = buildMessage(); + PutMessageResult result = this.messageStore.putMessage(msg); + assertThat(result.getPutMessageStatus()).isEqualTo(PutMessageStatus.PUT_OK); + ((DefaultMessageStore) this.messageStore).getBrokerConfig().setEnableSlaveActingMaster(false); + messageStoreConfig.setEnableAutoInSyncReplicas(false); + } + + @Test + public void testGetBulkCommitLogData() { + DefaultMessageStore defaultMessageStore = (DefaultMessageStore) messageStore; + + messageBody = new byte[2 * 1024 * 1024]; + + for (int i = 0; i < 10; i++) { + MessageExtBrokerInner msg1 = buildMessage(); + messageStore.putMessage(msg1); + } + + System.out.printf("%d%n", defaultMessageStore.getMaxPhyOffset()); + + List bufferResultList = defaultMessageStore.getBulkCommitLogData(0, (int) defaultMessageStore.getMaxPhyOffset()); + List msgList = new ArrayList<>(); + for (SelectMappedBufferResult bufferResult : bufferResultList) { + msgList.addAll(MessageDecoder.decodesBatch(bufferResult.getByteBuffer(), true, false, false)); + bufferResult.release(); + } + + assertThat(msgList.size()).isEqualTo(10); + } + + @Test + public void testPutLongMessage() throws Exception { + MessageExtBrokerInner messageExtBrokerInner = buildMessage(); + CommitLog commitLog = ((DefaultMessageStore) messageStore).getCommitLog(); + MessageStoreConfig messageStoreConfig = ((DefaultMessageStore) messageStore).getMessageStoreConfig(); + MessageExtEncoder.PutMessageThreadLocal putMessageThreadLocal = commitLog.getPutMessageThreadLocal().get(); + + //body size, topic size, properties size exactly equal to max size + messageExtBrokerInner.setBody(new byte[messageStoreConfig.getMaxMessageSize()]); + messageExtBrokerInner.setTopic(new String(new byte[127])); + messageExtBrokerInner.setPropertiesString(new String(new byte[Short.MAX_VALUE])); + PutMessageResult encodeResult1 = putMessageThreadLocal.getEncoder().encode(messageExtBrokerInner); + assertTrue(encodeResult1 == null); + + //body size exactly more than max message body size + messageExtBrokerInner.setBody(new byte[messageStoreConfig.getMaxMessageSize() + 1]); + PutMessageResult encodeResult2 = putMessageThreadLocal.getEncoder().encode(messageExtBrokerInner); + assertTrue(encodeResult2.getPutMessageStatus() == PutMessageStatus.MESSAGE_ILLEGAL); + + //body size exactly equal to max message size + messageExtBrokerInner.setBody(new byte[messageStoreConfig.getMaxMessageSize() + 64 * 1024]); + PutMessageResult encodeResult3 = putMessageThreadLocal.getEncoder().encode(messageExtBrokerInner); + assertTrue(encodeResult3.getPutMessageStatus() == PutMessageStatus.MESSAGE_ILLEGAL); + + //message properties length more than properties maxSize + messageExtBrokerInner.setBody(new byte[messageStoreConfig.getMaxMessageSize()]); + messageExtBrokerInner.setPropertiesString(new String(new byte[Short.MAX_VALUE + 1])); + PutMessageResult encodeResult4 = putMessageThreadLocal.getEncoder().encode(messageExtBrokerInner); + assertTrue(encodeResult4.getPutMessageStatus() == PutMessageStatus.PROPERTIES_SIZE_EXCEEDED); + + //message length more than buffer length capacity + messageExtBrokerInner.setBody(new byte[messageStoreConfig.getMaxMessageSize()]); + messageExtBrokerInner.setTopic(new String(new byte[Short.MAX_VALUE])); + messageExtBrokerInner.setPropertiesString(new String(new byte[Short.MAX_VALUE])); + PutMessageResult encodeResult5 = putMessageThreadLocal.getEncoder().encode(messageExtBrokerInner); + assertTrue(encodeResult5.getPutMessageStatus() == PutMessageStatus.MESSAGE_ILLEGAL); + } + + @Test + public void testDynamicMaxMessageSize() { + MessageExtBrokerInner messageExtBrokerInner = buildMessage(); + MessageStoreConfig messageStoreConfig = ((DefaultMessageStore) messageStore).getMessageStoreConfig(); + int originMaxMessageSize = messageStoreConfig.getMaxMessageSize(); + + messageExtBrokerInner.setBody(new byte[originMaxMessageSize + 10]); + PutMessageResult putMessageResult = messageStore.putMessage(messageExtBrokerInner); + assertTrue(putMessageResult.getPutMessageStatus() == PutMessageStatus.MESSAGE_ILLEGAL); + + int newMaxMessageSize = originMaxMessageSize + 10; + messageStoreConfig.setMaxMessageSize(newMaxMessageSize); + putMessageResult = messageStore.putMessage(messageExtBrokerInner); + assertTrue(putMessageResult.getPutMessageStatus() == PutMessageStatus.PUT_OK); + + messageStoreConfig.setMaxMessageSize(10); + putMessageResult = messageStore.putMessage(messageExtBrokerInner); + assertTrue(putMessageResult.getPutMessageStatus() == PutMessageStatus.MESSAGE_ILLEGAL); + + messageStoreConfig.setMaxMessageSize(originMaxMessageSize); + } + + @Test + public void testDeleteTopics() { + MessageStoreConfig messageStoreConfig = messageStore.getMessageStoreConfig(); + ConcurrentMap> consumeQueueTable = + ((DefaultMessageStore) messageStore).getConsumeQueueTable(); + for (int i = 0; i < 10; i++) { + ConcurrentMap cqTable = new ConcurrentHashMap<>(); + String topicName = "topic-" + i; + for (int j = 0; j < 4; j++) { + ConsumeQueue consumeQueue = new ConsumeQueue(topicName, j, messageStoreConfig.getStorePathRootDir(), + messageStoreConfig.getMappedFileSizeConsumeQueue(), messageStore); + cqTable.put(j, consumeQueue); + } + consumeQueueTable.put(topicName, cqTable); + } + Assert.assertEquals(consumeQueueTable.size(), 10); + HashSet resultSet = Sets.newHashSet("topic-3", "topic-5"); + messageStore.deleteTopics(Sets.difference(consumeQueueTable.keySet(), resultSet)); + Assert.assertEquals(consumeQueueTable.size(), 2); + Assert.assertEquals(resultSet, consumeQueueTable.keySet()); + } + + @Test + public void testCleanUnusedTopic() { + MessageStoreConfig messageStoreConfig = messageStore.getMessageStoreConfig(); + ConcurrentMap> consumeQueueTable = + ((DefaultMessageStore) messageStore).getConsumeQueueTable(); + for (int i = 0; i < 10; i++) { + ConcurrentMap cqTable = new ConcurrentHashMap<>(); + String topicName = "topic-" + i; + for (int j = 0; j < 4; j++) { + ConsumeQueue consumeQueue = new ConsumeQueue(topicName, j, messageStoreConfig.getStorePathRootDir(), + messageStoreConfig.getMappedFileSizeConsumeQueue(), messageStore); + cqTable.put(j, consumeQueue); + } + consumeQueueTable.put(topicName, cqTable); + } + Assert.assertEquals(consumeQueueTable.size(), 10); + HashSet resultSet = Sets.newHashSet("topic-3", "topic-5"); + messageStore.cleanUnusedTopic(resultSet); + Assert.assertEquals(consumeQueueTable.size(), 2); + Assert.assertEquals(resultSet, consumeQueueTable.keySet()); + } + + @Test + public void testChangeStoreConfig() { + Properties properties = new Properties(); + properties.setProperty("enableBatchPush", "true"); + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + MixAll.properties2Object(properties, messageStoreConfig); + assertThat(messageStoreConfig.isEnableBatchPush()).isTrue(); } private class MyMessageArrivingListener implements MessageArrivingListener { @Override public void arriving(String topic, int queueId, long logicOffset, long tagsCode, long msgStoreTime, - byte[] filterBitMap, Map properties) { + byte[] filterBitMap, Map properties) { } } } diff --git a/store/src/test/java/org/apache/rocketmq/store/FlushDiskWatcherTest.java b/store/src/test/java/org/apache/rocketmq/store/FlushDiskWatcherTest.java new file mode 100644 index 00000000000..97968f5938a --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/FlushDiskWatcherTest.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store; + +import org.apache.rocketmq.store.CommitLog.GroupCommitRequest; +import org.junit.Assert; +import org.junit.Test; + +import java.util.LinkedList; +import java.util.List; + +public class FlushDiskWatcherTest { + + private final long timeoutMill = 5000; + + @Test + public void testTimeout() throws Exception { + FlushDiskWatcher flushDiskWatcher = new FlushDiskWatcher(); + flushDiskWatcher.setDaemon(true); + flushDiskWatcher.start(); + + int count = 100; + List requestList = new LinkedList<>(); + for (int i = 0; i < count; i++) { + GroupCommitRequest groupCommitRequest = + new GroupCommitRequest(0, timeoutMill); + requestList.add(groupCommitRequest); + flushDiskWatcher.add(groupCommitRequest); + } + Thread.sleep(2 * timeoutMill); + + for (GroupCommitRequest request : requestList) { + request.wakeupCustomer(PutMessageStatus.PUT_OK); + } + + for (GroupCommitRequest request : requestList) { + Assert.assertTrue(request.future().isDone()); + Assert.assertEquals(request.future().get(), PutMessageStatus.FLUSH_DISK_TIMEOUT); + } + Assert.assertEquals(flushDiskWatcher.queueSize(), 0); + flushDiskWatcher.shutdown(); + } + + @Test + public void testWatcher() throws Exception { + FlushDiskWatcher flushDiskWatcher = new FlushDiskWatcher(); + flushDiskWatcher.setDaemon(true); + flushDiskWatcher.start(); + + int count = 100; + List requestList = new LinkedList<>(); + for (int i = 0; i < count; i++) { + GroupCommitRequest groupCommitRequest = + new GroupCommitRequest(0, timeoutMill); + requestList.add(groupCommitRequest); + flushDiskWatcher.add(groupCommitRequest); + groupCommitRequest.wakeupCustomer(PutMessageStatus.PUT_OK); + } + Thread.sleep((timeoutMill << 20) / 1000000); + for (GroupCommitRequest request : requestList) { + Assert.assertTrue(request.future().isDone()); + Assert.assertEquals(request.future().get(), PutMessageStatus.PUT_OK); + } + Assert.assertEquals(flushDiskWatcher.queueSize(), 0); + flushDiskWatcher.shutdown(); + } + + +} diff --git a/store/src/test/java/org/apache/rocketmq/store/GetMessageResultTest.java b/store/src/test/java/org/apache/rocketmq/store/GetMessageResultTest.java new file mode 100644 index 00000000000..98129c26dfe --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/GetMessageResultTest.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store; + +import org.junit.Assert; +import org.junit.Test; + +public class GetMessageResultTest { + + @Test + public void testAddMessage() { + GetMessageResult getMessageResult = new GetMessageResult(); + SelectMappedBufferResult mappedBufferResult1 = new SelectMappedBufferResult(0, null, 4 * 1024, null); + getMessageResult.addMessage(mappedBufferResult1); + + SelectMappedBufferResult mappedBufferResult2 = new SelectMappedBufferResult(0, null, 2 * 4 * 1024, null); + getMessageResult.addMessage(mappedBufferResult2, 0); + + SelectMappedBufferResult mappedBufferResult3 = new SelectMappedBufferResult(0, null, 4 * 4 * 1024, null); + getMessageResult.addMessage(mappedBufferResult3, 0, 2); + + Assert.assertEquals(getMessageResult.getMessageQueueOffset().size(), 2); + Assert.assertEquals(getMessageResult.getMessageBufferList().size(), 3); + Assert.assertEquals(getMessageResult.getMessageMapedList().size(), 3); + Assert.assertEquals(getMessageResult.getMessageCount(), 4); + Assert.assertEquals(getMessageResult.getMsgCount4Commercial(), 1 + 2 + 4); + Assert.assertEquals(getMessageResult.getBufferTotalSize(), (1 + 2 + 4) * 4 * 1024); + } +} diff --git a/store/src/test/java/org/apache/rocketmq/store/HATest.java b/store/src/test/java/org/apache/rocketmq/store/HATest.java new file mode 100644 index 00000000000..5623adb64fa --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/HATest.java @@ -0,0 +1,299 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.time.Duration; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.store.config.BrokerRole; +import org.apache.rocketmq.store.config.FlushDiskType; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.ha.HAConnectionState; +import org.apache.rocketmq.store.stats.BrokerStatsManager; +import org.junit.After; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class HATest { + private final String storeMessage = "Once, there was a chance for me!"; + private int queueTotal = 100; + private AtomicInteger queueId = new AtomicInteger(0); + private SocketAddress bornHost; + private SocketAddress storeHost; + private byte[] messageBody; + + private MessageStore messageStore; + private MessageStore slaveMessageStore; + private MessageStoreConfig masterMessageStoreConfig; + private MessageStoreConfig slaveStoreConfig; + private BrokerStatsManager brokerStatsManager = new BrokerStatsManager("simpleTest", true); + private String storePathRootParentDir = System.getProperty("java.io.tmpdir") + File.separator + UUID.randomUUID(); + private String storePathRootDir = storePathRootParentDir + File.separator + "store"; + + @Before + public void init() throws Exception { + storeHost = new InetSocketAddress(InetAddress.getLocalHost(), 8123); + bornHost = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0); + masterMessageStoreConfig = new MessageStoreConfig(); + masterMessageStoreConfig.setBrokerRole(BrokerRole.SYNC_MASTER); + masterMessageStoreConfig.setStorePathRootDir(storePathRootDir + File.separator + "master"); + masterMessageStoreConfig.setStorePathCommitLog(storePathRootDir + File.separator + "master" + File.separator + "commitlog"); + masterMessageStoreConfig.setHaListenPort(0); + masterMessageStoreConfig.setTotalReplicas(2); + masterMessageStoreConfig.setInSyncReplicas(2); + masterMessageStoreConfig.setHaHousekeepingInterval(2 * 1000); + masterMessageStoreConfig.setHaSendHeartbeatInterval(1000); + buildMessageStoreConfig(masterMessageStoreConfig); + slaveStoreConfig = new MessageStoreConfig(); + slaveStoreConfig.setBrokerRole(BrokerRole.SLAVE); + slaveStoreConfig.setStorePathRootDir(storePathRootDir + File.separator + "slave"); + slaveStoreConfig.setStorePathCommitLog(storePathRootDir + File.separator + "slave" + File.separator + "commitlog"); + slaveStoreConfig.setHaListenPort(0); + slaveStoreConfig.setTotalReplicas(2); + slaveStoreConfig.setInSyncReplicas(2); + slaveStoreConfig.setHaHousekeepingInterval(2 * 1000); + slaveStoreConfig.setHaSendHeartbeatInterval(1000); + buildMessageStoreConfig(slaveStoreConfig); + messageStore = buildMessageStore(masterMessageStoreConfig, 0L); + slaveMessageStore = buildMessageStore(slaveStoreConfig, 1L); + boolean load = messageStore.load(); + boolean slaveLoad = slaveMessageStore.load(); + assertTrue(load); + assertTrue(slaveLoad); + messageStore.start(); + + slaveMessageStore.updateHaMasterAddress("127.0.0.1:" + masterMessageStoreConfig.getHaListenPort()); + slaveMessageStore.start(); + slaveMessageStore.updateHaMasterAddress("127.0.0.1:" + masterMessageStoreConfig.getHaListenPort()); + await().atMost(6, SECONDS).until(() -> slaveMessageStore.getHaService().getHAClient().getCurrentState() == HAConnectionState.TRANSFER); + } + + @Test + public void testHandleHA() { + long totalMsgs = 10; + queueTotal = 1; + messageBody = storeMessage.getBytes(); + for (long i = 0; i < totalMsgs; i++) { + messageStore.putMessage(buildMessage()); + } + for (long i = 0; i < totalMsgs; i++) { + final long index = i; + Boolean exist = await().atMost(Duration.ofSeconds(5)).until(() -> { + GetMessageResult result = slaveMessageStore.getMessage("GROUP_A", "FooBar", 0, index, 1024 * 1024, null); + if (result == null) { + return false; + } + boolean flag = GetMessageStatus.FOUND == result.getStatus(); + result.release(); + return flag; + + }, item -> item); + assertTrue(exist); + } + } + + @Test + public void testSemiSyncReplica() throws Exception { + long totalMsgs = 5; + queueTotal = 1; + messageBody = storeMessage.getBytes(); + for (long i = 0; i < totalMsgs; i++) { + MessageExtBrokerInner msg = buildMessage(); + CompletableFuture putResultFuture = messageStore.asyncPutMessage(msg); + PutMessageResult result = putResultFuture.get(); + assertEquals(PutMessageStatus.PUT_OK, result.getPutMessageStatus()); + //message has been replicated to slave's commitLog, but maybe not dispatch to ConsumeQueue yet + //so direct read from commitLog by physical offset + MessageExt slaveMsg = slaveMessageStore.lookMessageByOffset(result.getAppendMessageResult().getWroteOffset()); + assertNotNull(slaveMsg); + assertArrayEquals(msg.getBody(), slaveMsg.getBody()); + assertEquals(msg.getTopic(), slaveMsg.getTopic()); + assertEquals(msg.getTags(), slaveMsg.getTags()); + assertEquals(msg.getKeys(), slaveMsg.getKeys()); + } + //shutdown slave, putMessage should return FLUSH_SLAVE_TIMEOUT + slaveMessageStore.shutdown(); + + //wait to let master clean the slave's connection + await().atMost(Duration.ofSeconds(3)).until(() -> messageStore.getHaService().getConnectionCount().get() == 0); + for (long i = 0; i < totalMsgs; i++) { + CompletableFuture putResultFuture = messageStore.asyncPutMessage(buildMessage()); + PutMessageResult result = putResultFuture.get(); + assertEquals(PutMessageStatus.FLUSH_SLAVE_TIMEOUT, result.getPutMessageStatus()); + } + } + + @Test + public void testSemiSyncReplicaWhenSlaveActingMaster() throws Exception { + // SKip MacOS + Assume.assumeFalse(MixAll.isMac()); + long totalMsgs = 5; + queueTotal = 1; + messageBody = storeMessage.getBytes(); + ((DefaultMessageStore) messageStore).getBrokerConfig().setEnableSlaveActingMaster(true); + for (long i = 0; i < totalMsgs; i++) { + MessageExtBrokerInner msg = buildMessage(); + CompletableFuture putResultFuture = messageStore.asyncPutMessage(msg); + PutMessageResult result = putResultFuture.get(); + assertEquals(PutMessageStatus.PUT_OK, result.getPutMessageStatus()); + //message has been replicated to slave's commitLog, but maybe not dispatch to ConsumeQueue yet + //so direct read from commitLog by physical offset + MessageExt slaveMsg = slaveMessageStore.lookMessageByOffset(result.getAppendMessageResult().getWroteOffset()); + assertNotNull(slaveMsg); + assertArrayEquals(msg.getBody(), slaveMsg.getBody()); + assertEquals(msg.getTopic(), slaveMsg.getTopic()); + assertEquals(msg.getTags(), slaveMsg.getTags()); + assertEquals(msg.getKeys(), slaveMsg.getKeys()); + } + + //shutdown slave, putMessage should return IN_SYNC_REPLICAS_NOT_ENOUGH + slaveMessageStore.shutdown(); + messageStore.setAliveReplicaNumInGroup(1); + + //wait to let master clean the slave's connection + Thread.sleep(masterMessageStoreConfig.getHaHousekeepingInterval() + 500); + for (long i = 0; i < totalMsgs; i++) { + CompletableFuture putResultFuture = messageStore.asyncPutMessage(buildMessage()); + PutMessageResult result = putResultFuture.get(); + assertEquals(PutMessageStatus.IN_SYNC_REPLICAS_NOT_ENOUGH, result.getPutMessageStatus()); + } + + ((DefaultMessageStore) messageStore).getBrokerConfig().setEnableSlaveActingMaster(false); + } + + @Test + public void testSemiSyncReplicaWhenAdaptiveDegradation() throws Exception { + long totalMsgs = 5; + queueTotal = 1; + messageBody = storeMessage.getBytes(); + ((DefaultMessageStore) messageStore).getBrokerConfig().setEnableSlaveActingMaster(true); + messageStore.getMessageStoreConfig().setEnableAutoInSyncReplicas(true); + for (long i = 0; i < totalMsgs; i++) { + MessageExtBrokerInner msg = buildMessage(); + CompletableFuture putResultFuture = messageStore.asyncPutMessage(msg); + PutMessageResult result = putResultFuture.get(); + assertEquals(PutMessageStatus.PUT_OK, result.getPutMessageStatus()); + //message has been replicated to slave's commitLog, but maybe not dispatch to ConsumeQueue yet + //so direct read from commitLog by physical offset + final MessageExt[] slaveMsg = {null}; + await().atMost(Duration.ofSeconds(3)).until(() -> { + slaveMsg[0] = slaveMessageStore.lookMessageByOffset(result.getAppendMessageResult().getWroteOffset()); + return slaveMsg[0] != null; + }); + assertArrayEquals(msg.getBody(), slaveMsg[0].getBody()); + assertEquals(msg.getTopic(), slaveMsg[0].getTopic()); + assertEquals(msg.getTags(), slaveMsg[0].getTags()); + assertEquals(msg.getKeys(), slaveMsg[0].getKeys()); + } + + //shutdown slave, putMessage should return IN_SYNC_REPLICAS_NOT_ENOUGH + slaveMessageStore.shutdown(); + messageStore.setAliveReplicaNumInGroup(1); + + //wait to let master clean the slave's connection + await().atMost(Duration.ofSeconds(3)).until(() -> messageStore.getHaService().getConnectionCount().get() == 0); + for (long i = 0; i < totalMsgs; i++) { + CompletableFuture putResultFuture = messageStore.asyncPutMessage(buildMessage()); + PutMessageResult result = putResultFuture.get(); + assertEquals(PutMessageStatus.PUT_OK, result.getPutMessageStatus()); + } + + ((DefaultMessageStore) messageStore).getBrokerConfig().setEnableSlaveActingMaster(false); + messageStore.getMessageStoreConfig().setEnableAutoInSyncReplicas(false); + } + + @After + public void destroy() throws Exception { + + slaveMessageStore.shutdown(); + slaveMessageStore.destroy(); + messageStore.shutdown(); + messageStore.destroy(); + File file = new File(storePathRootParentDir); + UtilAll.deleteFile(file); + } + + private MessageStore buildMessageStore(MessageStoreConfig messageStoreConfig, long brokerId) throws Exception { + BrokerConfig brokerConfig = new BrokerConfig(); + brokerConfig.setBrokerId(brokerId); + return new DefaultMessageStore(messageStoreConfig, brokerStatsManager, null, brokerConfig, new ConcurrentHashMap<>()); + } + + private void buildMessageStoreConfig(MessageStoreConfig messageStoreConfig) { + messageStoreConfig.setMappedFileSizeCommitLog(1024 * 1024 * 10); + messageStoreConfig.setMappedFileSizeConsumeQueue(1024 * 1024 * 10); + messageStoreConfig.setMaxHashSlotNum(10000); + messageStoreConfig.setMaxIndexNum(100 * 100); + messageStoreConfig.setFlushDiskType(FlushDiskType.SYNC_FLUSH); + messageStoreConfig.setFlushIntervalConsumeQueue(1); + } + + private MessageExtBrokerInner buildMessage() { + MessageExtBrokerInner msg = new MessageExtBrokerInner(); + msg.setTopic("FooBar"); + msg.setTags("TAG1"); + msg.setBody(messageBody); + msg.setKeys(String.valueOf(System.currentTimeMillis())); + msg.setQueueId(Math.abs(queueId.getAndIncrement()) % queueTotal); + msg.setSysFlag(0); + msg.setBornTimestamp(System.currentTimeMillis()); + msg.setStoreHost(storeHost); + msg.setBornHost(bornHost); + msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); + return msg; + } + + private boolean isCommitLogAvailable(DefaultMessageStore store) { + try { + Field serviceField = store.getClass().getDeclaredField("reputMessageService"); + serviceField.setAccessible(true); + DefaultMessageStore.ReputMessageService reputService = + (DefaultMessageStore.ReputMessageService) serviceField.get(store); + + Method method = DefaultMessageStore.ReputMessageService.class.getDeclaredMethod("isCommitLogAvailable"); + method.setAccessible(true); + return (boolean) method.invoke(reputService); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/store/src/test/java/org/apache/rocketmq/store/MappedFileQueueTest.java b/store/src/test/java/org/apache/rocketmq/store/MappedFileQueueTest.java index 92f1876b2f0..3cc17c659b9 100644 --- a/store/src/test/java/org/apache/rocketmq/store/MappedFileQueueTest.java +++ b/store/src/test/java/org/apache/rocketmq/store/MappedFileQueueTest.java @@ -17,22 +17,43 @@ package org.apache.rocketmq.store; -import java.io.File; -import java.nio.ByteBuffer; -import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.ThreadFactoryImpl; import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.store.logfile.DefaultMappedFile; +import org.apache.rocketmq.store.logfile.MappedFile; +import org.assertj.core.util.Lists; import org.junit.After; +import org.junit.Assume; import org.junit.Test; +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + import static org.assertj.core.api.Assertions.assertThat; public class MappedFileQueueTest { + + private String storePath = System.getProperty("java.io.tmpdir") + File.separator + "unit_test_store"; + @Test public void testGetLastMappedFile() { final String fixedMsg = "0123456789abcdef"; MappedFileQueue mappedFileQueue = - new MappedFileQueue("target/unit_test_store/a/", 1024, null); + new MappedFileQueue(storePath + File.separator + "a/", 1024, null); for (int i = 0; i < 1024; i++) { MappedFile mappedFile = mappedFileQueue.getLastMappedFile(0); @@ -50,7 +71,7 @@ public void testFindMappedFileByOffset() { final String fixedMsg = "abcd"; MappedFileQueue mappedFileQueue = - new MappedFileQueue("target/unit_test_store/b/", 1024, null); + new MappedFileQueue(storePath + File.separator + "b/", 1024, null); for (int i = 0; i < 1024; i++) { MappedFile mappedFile = mappedFileQueue.getLastMappedFile(0); @@ -98,7 +119,7 @@ public void testFindMappedFileByOffset() { @Test public void testFindMappedFileByOffset_StartOffsetIsNonZero() { MappedFileQueue mappedFileQueue = - new MappedFileQueue("target/unit_test_store/b/", 1024, null); + new MappedFileQueue(storePath + File.separator + "b/", 1024, null); //Start from a non-zero offset MappedFile mappedFile = mappedFileQueue.getLastMappedFile(1024); @@ -122,7 +143,7 @@ public void testAppendMessage() { final String fixedMsg = "0123456789abcdef"; MappedFileQueue mappedFileQueue = - new MappedFileQueue("target/unit_test_store/c/", 1024, null); + new MappedFileQueue(storePath + File.separator + "c/", 1024, null); for (int i = 0; i < 1024; i++) { MappedFile mappedFile = mappedFileQueue.getLastMappedFile(0); @@ -157,7 +178,7 @@ public void testGetMappedMemorySize() { final String fixedMsg = "abcd"; MappedFileQueue mappedFileQueue = - new MappedFileQueue("target/unit_test_store/d/", 1024, null); + new MappedFileQueue(storePath + File.separator + "d/", 1024, null); for (int i = 0; i < 1024; i++) { MappedFile mappedFile = mappedFileQueue.getLastMappedFile(0); @@ -173,7 +194,7 @@ public void testGetMappedMemorySize() { @Test public void testDeleteExpiredFileByOffset() { MappedFileQueue mappedFileQueue = - new MappedFileQueue("target/unit_test_store/e", 5120, null); + new MappedFileQueue(storePath + File.separator + "e/", 5120, null); for (int i = 0; i < 2048; i++) { MappedFile mappedFile = mappedFileQueue.getLastMappedFile(0); @@ -205,7 +226,7 @@ public void testDeleteExpiredFileByOffset() { @Test public void testDeleteExpiredFileByTime() throws Exception { MappedFileQueue mappedFileQueue = - new MappedFileQueue("target/unit_test_store/f/", 1024, null); + new MappedFileQueue(storePath + File.separator + "f/", 1024, null); for (int i = 0; i < 100; i++) { MappedFile mappedFile = mappedFileQueue.getLastMappedFile(0); @@ -215,23 +236,265 @@ public void testDeleteExpiredFileByTime() throws Exception { } assertThat(mappedFileQueue.getMappedFiles().size()).isEqualTo(50); - long expiredTime = 100 * 1000; + long expiredTime = 100 * 1000; for (int i = 0; i < mappedFileQueue.getMappedFiles().size(); i++) { - MappedFile mappedFile = mappedFileQueue.getMappedFiles().get(i); - if (i < 5) { - mappedFile.getFile().setLastModified(System.currentTimeMillis() - expiredTime * 2); - } - if (i > 20) { - mappedFile.getFile().setLastModified(System.currentTimeMillis() - expiredTime * 2); - } - } - mappedFileQueue.deleteExpiredFileByTime(expiredTime, 0, 0, false); + DefaultMappedFile mappedFile = (DefaultMappedFile) mappedFileQueue.getMappedFiles().get(i); + if (i < 5) { + mappedFile.getFile().setLastModified(System.currentTimeMillis() - expiredTime * 2); + } + if (i > 20) { + mappedFile.getFile().setLastModified(System.currentTimeMillis() - expiredTime * 2); + } + } + int maxBatchDeleteFilesNum = 50; + mappedFileQueue.deleteExpiredFileByTime(expiredTime, 0, 0, false, maxBatchDeleteFilesNum); assertThat(mappedFileQueue.getMappedFiles().size()).isEqualTo(45); } + @Test + public void testFindMappedFile_ByIteration() { + MappedFileQueue mappedFileQueue = + new MappedFileQueue(storePath + File.separator + "g/", 1024, null); + for (int i = 0; i < 3; i++) { + MappedFile mappedFile = mappedFileQueue.getLastMappedFile(1024 * i); + mappedFile.setWrotePosition(1024); + } + + assertThat(mappedFileQueue.findMappedFileByOffset(1028).getFileFromOffset()).isEqualTo(1024); + + // Switch two MappedFiles and verify findMappedFileByOffset method + MappedFile tmpFile = mappedFileQueue.getMappedFiles().get(1); + mappedFileQueue.getMappedFiles().set(1, mappedFileQueue.getMappedFiles().get(2)); + mappedFileQueue.getMappedFiles().set(2, tmpFile); + assertThat(mappedFileQueue.findMappedFileByOffset(1028).getFileFromOffset()).isEqualTo(1024); + } + + @Test + public void testMappedFile_SwapMap() { + // four-byte string. + final String fixedMsg = "abcdefgh"; + final int mappedFileSize = 102400; + + MappedFileQueue mappedFileQueue = + new MappedFileQueue(storePath + File.separator + "b/", mappedFileSize, null); + + ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 3, 1000 * 60, + TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(), + new ThreadFactoryImpl("testThreadPool")); + + for (int i = 0; i < mappedFileSize; i++) { + MappedFile mappedFile = mappedFileQueue.getLastMappedFile(0); + assertThat(mappedFile).isNotNull(); + assertThat(mappedFile.appendMessage(fixedMsg.getBytes())).isTrue(); + } + assertThat(mappedFileQueue.getMappedMemorySize()).isEqualTo(fixedMsg.getBytes().length * mappedFileSize); + + AtomicBoolean readOver = new AtomicBoolean(false); + AtomicBoolean hasException = new AtomicBoolean(false); + + executor.submit(() -> { + try { + while (!readOver.get()) { + for (MappedFile mappedFile : mappedFileQueue.getMappedFiles()) { + mappedFile.swapMap(); + Thread.sleep(10); + mappedFile.cleanSwapedMap(true); + } + } + } catch (Throwable t) { + hasException.set(true); + } + } + ); + long start = System.currentTimeMillis(); + long maxReadTimeMs = 60 * 1000; + try { + while (System.currentTimeMillis() - start <= maxReadTimeMs) { + for (int i = 0; i < mappedFileSize && !readOver.get(); i++) { + MappedFile mappedFile = null; + int retryTime = 0; + while (mappedFile == null && retryTime < 10000) { + mappedFile = mappedFileQueue.findMappedFileByOffset(i * fixedMsg.getBytes().length); + retryTime++; + if (mappedFile == null) { + Thread.sleep(1); + } + } + assertThat(mappedFile != null).isTrue(); + retryTime = 0; + int pos = (i * fixedMsg.getBytes().length) % mappedFileSize; + while ((pos + fixedMsg.getBytes().length) > mappedFile.getReadPosition() && retryTime < 10000) { + retryTime++; + if ((pos + fixedMsg.getBytes().length) > mappedFile.getReadPosition()) { + Thread.sleep(1); + } + } + assertThat((pos + fixedMsg.getBytes().length) <= mappedFile.getReadPosition()).isTrue(); + SelectMappedBufferResult ret = mappedFile.selectMappedBuffer(pos, fixedMsg.getBytes().length); + byte[] readRes = new byte[fixedMsg.getBytes().length]; + ret.getByteBuffer().get(readRes); + String readStr = new String(readRes, StandardCharsets.UTF_8); + assertThat(readStr.equals(fixedMsg)).isTrue(); + } + } + readOver.set(true); + } catch (Throwable e) { + hasException.set(true); + readOver.set(true); + } + assertThat(readOver.get()).isTrue(); + assertThat(hasException.get()).isFalse(); + + } + + @Test + public void testMappedFile_CleanSwapedMap() throws InterruptedException { + // four-byte string. + final String fixedMsg = "abcd"; + final int mappedFileSize = 1024000; + + MappedFileQueue mappedFileQueue = + new MappedFileQueue(storePath + File.separator + "b/", mappedFileSize, null); + + ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 1000 * 60, + TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(), + new ThreadFactoryImpl("testThreadPool")); + for (int i = 0; i < mappedFileSize; i++) { + MappedFile mappedFile = mappedFileQueue.getLastMappedFile(0); + assertThat(mappedFile).isNotNull(); + assertThat(mappedFile.appendMessage(fixedMsg.getBytes())).isTrue(); + } + + for (MappedFile mappedFile : mappedFileQueue.getMappedFiles()) { + mappedFile.swapMap(); + } + AtomicBoolean hasException = new AtomicBoolean(false); + CountDownLatch downLatch = new CountDownLatch(5); + for (int i = 0; i < 5; i++) { + executor.submit(() -> { + try { + for (MappedFile mappedFile : mappedFileQueue.getMappedFiles()) { + mappedFile.cleanSwapedMap(true); + mappedFile.cleanSwapedMap(true); + } + } catch (Exception e) { + hasException.set(true); + } finally { + downLatch.countDown(); + } + }); + } + + downLatch.await(10, TimeUnit.SECONDS); + assertThat(hasException.get()).isFalse(); + } + + @Test + public void testMappedFile_Rename() throws IOException, InterruptedException { + Assume.assumeFalse(MixAll.isWindows()); + final String fixedMsg = RandomStringUtils.randomAlphanumeric(128); + final byte[] msgByteArr = fixedMsg.getBytes(StandardCharsets.UTF_8); + final int mappedFileSize = 5 * 1024 * 1024; + + MappedFileQueue mappedFileQueue = + new MappedFileQueue("target/unit_test_store", mappedFileSize, null); + + int currentSize = 0; + while (currentSize <= 2 * mappedFileSize) { + MappedFile mappedFile = mappedFileQueue.getLastMappedFile(0); + mappedFile.appendMessage(msgByteArr); + currentSize += fixedMsg.length(); + } + + assertThat(mappedFileQueue.getMappedFiles().size()).isEqualTo(3); + + ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor(); + ses.scheduleWithFixedDelay(() -> { + MappedFile mappedFile = mappedFileQueue.getLastMappedFile(0); + mappedFile.appendMessage(msgByteArr); + }, 1,1, TimeUnit.MILLISECONDS); + + List mappedFileList = Lists.newArrayList(mappedFileQueue.getMappedFiles()); + mappedFileList.remove(mappedFileList.size() - 1); + + MappedFileQueue compactingMappedFileQueue = + new MappedFileQueue("target/unit_test_store/compacting", mappedFileSize, null); + + currentSize = 0; + while (currentSize < (2 * mappedFileSize - mappedFileSize / 2)) { + MappedFile mappedFile = compactingMappedFileQueue.getLastMappedFile(0); + mappedFile.appendMessage(msgByteArr); + currentSize += fixedMsg.length(); + } + + + mappedFileList.forEach(MappedFile::renameToDelete); + assertThat(mappedFileQueue.getFirstMappedFile().getFileName()).endsWith(".delete"); + assertThat(mappedFileQueue.findMappedFileByOffset(mappedFileSize + fixedMsg.length()).getFileName()).endsWith(".delete"); + + SelectMappedBufferResult sbr = mappedFileList.get(mappedFileList.size() - 1).selectMappedBuffer(0, msgByteArr.length); + assertThat(sbr).isNotNull(); + try { + assertThat(sbr.getMappedFile().getFileName().endsWith(".delete")).isTrue(); + if (sbr.getByteBuffer().hasArray()) { + assertThat(sbr.getByteBuffer().array()).isEqualTo(msgByteArr); + } else { + for (int i = 0; i < msgByteArr.length; i++) { + assertThat(sbr.getByteBuffer().get(i)).isEqualTo(msgByteArr[i]); + } + } + } finally { + sbr.release(); + } + + + compactingMappedFileQueue.getMappedFiles().forEach(mappedFile -> { + try { + mappedFile.moveToParent(); + } catch (IOException e) { + e.printStackTrace(); + } + }); + + mappedFileQueue.getMappedFiles().stream() + .filter(m -> !mappedFileList.contains(m)) + .forEach(m -> compactingMappedFileQueue.getMappedFiles().add(m)); + + int wrotePosition = mappedFileQueue.getLastMappedFile().getWrotePosition(); + + mappedFileList.forEach(mappedFile -> { + mappedFile.destroy(1000); + }); + + TimeUnit.SECONDS.sleep(3); + ses.shutdown(); + + mappedFileQueue.getMappedFiles().clear(); + mappedFileQueue.getMappedFiles().addAll(compactingMappedFileQueue.getMappedFiles()); + + TimeUnit.SECONDS.sleep(3); + } + + @Test + public void testReset() { + final String fixedMsg = "0123456789abcdef"; + MappedFileQueue mappedFileQueue = + new MappedFileQueue(storePath + File.separator + "a/", 64, null); + for (int i = 0; i < 8; i++) { + MappedFile mappedFile = mappedFileQueue.getLastMappedFile(0); + assertThat(mappedFile).isNotNull(); + assertThat(mappedFile.appendMessage(fixedMsg.getBytes())).isTrue(); + } + assertThat(mappedFileQueue.getMappedFiles().size()).isEqualTo(2); + assertThat(mappedFileQueue.resetOffset(0)).isTrue(); + assertThat(mappedFileQueue.getMappedFiles().size()).isEqualTo(1); + } + @After - public void destory() { - File file = new File("target/unit_test_store"); + public void destroy() { + File file = new File(storePath); UtilAll.deleteFile(file); } } diff --git a/store/src/test/java/org/apache/rocketmq/store/MappedFileTest.java b/store/src/test/java/org/apache/rocketmq/store/MappedFileTest.java index 50d0ae47f03..a506d44584d 100644 --- a/store/src/test/java/org/apache/rocketmq/store/MappedFileTest.java +++ b/store/src/test/java/org/apache/rocketmq/store/MappedFileTest.java @@ -22,8 +22,8 @@ import java.io.File; import java.io.IOException; - import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.store.logfile.DefaultMappedFile; import org.junit.After; import org.junit.Test; @@ -34,7 +34,7 @@ public class MappedFileTest { @Test public void testSelectMappedBuffer() throws IOException { - MappedFile mappedFile = new MappedFile("target/unit_test_store/MappedFileTest/000", 1024 * 64); + DefaultMappedFile mappedFile = new DefaultMappedFile("target/unit_test_store/MappedFileTest/000", 1024 * 64); boolean result = mappedFile.appendMessage(storeMessage.getBytes()); assertThat(result).isTrue(); @@ -53,7 +53,7 @@ public void testSelectMappedBuffer() throws IOException { } @After - public void destory() { + public void destroy() { File file = new File("target/unit_test_store"); UtilAll.deleteFile(file); } diff --git a/store/src/test/java/org/apache/rocketmq/store/MessageExtBrokerInnerTest.java b/store/src/test/java/org/apache/rocketmq/store/MessageExtBrokerInnerTest.java new file mode 100644 index 00000000000..415dc381175 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/MessageExtBrokerInnerTest.java @@ -0,0 +1,105 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store; + +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MessageExtBrokerInnerTest { + @Test + public void testDeleteProperty() { + MessageExtBrokerInner messageExtBrokerInner = new MessageExtBrokerInner(); + String propertiesString = ""; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo(""); + + propertiesString = "KeyA\u0001ValueA"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo(""); + + propertiesString = "KeyA\u0001ValueA\u0002"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo(""); + + propertiesString = "KeyA\u0001ValueA\u0002KeyA\u0001ValueA"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo(""); + + propertiesString = "KeyA\u0001ValueA\u0002KeyA\u0001ValueA\u0002"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo(""); + + propertiesString = "KeyB\u0001ValueB\u0002KeyA\u0001ValueA"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo("KeyB\u0001ValueB\u0002"); + + propertiesString = "KeyB\u0001ValueB\u0002KeyA\u0001ValueA\u0002"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo("KeyB\u0001ValueB\u0002"); + + propertiesString = "KeyB\u0001ValueB\u0002KeyA\u0001ValueA\u0002KeyB\u0001ValueB\u0002"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo("KeyB\u0001ValueB\u0002KeyB\u0001ValueB\u0002"); + + propertiesString = "KeyA\u0001ValueA\u0002KeyB\u0001ValueB\u0002"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo("KeyB\u0001ValueB\u0002"); + + propertiesString = "KeyA\u0001ValueA\u0002KeyB\u0001ValueB"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo("KeyB\u0001ValueB"); + + propertiesString = "KeyA\u0001ValueA\u0002KeyB\u0001ValueBKeyA\u0001ValueA\u0002"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo("KeyB\u0001ValueBKeyA\u0001ValueA\u0002"); + + propertiesString = "KeyA\u0001ValueA\u0002KeyB\u0001ValueBKeyA\u0001"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo("KeyB\u0001ValueBKeyA\u0001"); + + propertiesString = "KeyA\u0001ValueA\u0002KeyB\u0001ValueBKeyA"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("KeyA"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo("KeyB\u0001ValueBKeyA"); + + propertiesString = "__CRC32#\u0001"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("__CRC32#"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEmpty(); + + propertiesString = "__CRC32#"; + messageExtBrokerInner.setPropertiesString(propertiesString); + messageExtBrokerInner.deleteProperty("__CRC32#"); + assertThat(messageExtBrokerInner.getPropertiesString()).isEqualTo(propertiesString); + } + +} diff --git a/store/src/test/java/org/apache/rocketmq/store/MultiDispatchTest.java b/store/src/test/java/org/apache/rocketmq/store/MultiDispatchTest.java new file mode 100644 index 00000000000..eae5eaa07a2 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/MultiDispatchTest.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store; + +import java.io.File; +import java.net.InetSocketAddress; +import java.nio.charset.Charset; + +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.queue.MultiDispatchUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.rocksdb.RocksDBException; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MultiDispatchTest { + + private MultiDispatch multiDispatch; + + private DefaultMessageStore messageStore; + + @Before + public void init() throws Exception { + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setMappedFileSizeCommitLog(1024 * 8); + messageStoreConfig.setMappedFileSizeConsumeQueue(1024 * 4); + messageStoreConfig.setMaxHashSlotNum(100); + messageStoreConfig.setMaxIndexNum(100 * 10); + messageStoreConfig.setStorePathRootDir(System.getProperty("java.io.tmpdir") + File.separator + "unitteststore1"); + messageStoreConfig.setStorePathCommitLog( + System.getProperty("java.io.tmpdir") + File.separator + "unitteststore1" + File.separator + "commitlog"); + + messageStoreConfig.setEnableLmq(true); + messageStoreConfig.setEnableMultiDispatch(true); + BrokerConfig brokerConfig = new BrokerConfig(); + //too much reference + messageStore = new DefaultMessageStore(messageStoreConfig, null, null, brokerConfig, new ConcurrentHashMap<>()); + multiDispatch = new MultiDispatch(messageStore); + } + + @After + public void destroy() { + UtilAll.deleteFile(new File(System.getProperty("java.io.tmpdir") + File.separator + "unitteststore1")); + } + + @Test + public void lmqQueueKey() { + MessageExtBrokerInner messageExtBrokerInner = mock(MessageExtBrokerInner.class); + when(messageExtBrokerInner.getQueueId()).thenReturn(2); + String ret = MultiDispatchUtils.lmqQueueKey("%LMQ%lmq123"); + assertEquals(ret, "%LMQ%lmq123-0"); + } + + @Test + public void wrapMultiDispatch() throws RocksDBException { + MessageExtBrokerInner messageExtBrokerInner = buildMessageMultiQueue(); + multiDispatch.wrapMultiDispatch(messageExtBrokerInner); + assertEquals(messageExtBrokerInner.getProperty(MessageConst.PROPERTY_INNER_MULTI_QUEUE_OFFSET), "0,0"); + } + + private MessageExtBrokerInner buildMessageMultiQueue() { + MessageExtBrokerInner msg = new MessageExtBrokerInner(); + msg.setTopic("test"); + msg.setTags("TAG1"); + msg.setKeys("Hello"); + msg.setBody("aaa".getBytes(Charset.forName("UTF-8"))); + msg.setKeys(String.valueOf(System.currentTimeMillis())); + msg.setQueueId(0); + msg.setSysFlag(0); + msg.setBornTimestamp(System.currentTimeMillis()); + msg.setStoreHost(new InetSocketAddress("127.0.0.1", 54270)); + msg.setBornHost(new InetSocketAddress("127.0.0.1", 10911)); + for (int i = 0; i < 1; i++) { + msg.putUserProperty(MessageConst.PROPERTY_INNER_MULTI_DISPATCH, "%LMQ%123,%LMQ%456"); + } + msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); + + return msg; + } +} \ No newline at end of file diff --git a/store/src/test/java/org/apache/rocketmq/store/MultiPathMappedFileQueueTest.java b/store/src/test/java/org/apache/rocketmq/store/MultiPathMappedFileQueueTest.java new file mode 100644 index 00000000000..07037aa03c8 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/MultiPathMappedFileQueueTest.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store; + +import static org.assertj.core.api.Assertions.assertThat; +import java.util.HashSet; +import java.util.Set; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.logfile.MappedFile; +import org.junit.Test; + + +public class MultiPathMappedFileQueueTest { + + @Test + public void testGetLastMappedFile() { + final byte[] fixedMsg = new byte[1024]; + + MessageStoreConfig config = new MessageStoreConfig(); + config.setStorePathCommitLog("target/unit_test_store/a/" + MixAll.MULTI_PATH_SPLITTER + + "target/unit_test_store/b/" + MixAll.MULTI_PATH_SPLITTER + + "target/unit_test_store/c/"); + MappedFileQueue mappedFileQueue = new MultiPathMappedFileQueue(config, 1024, null, null); + String[] storePaths = config.getStorePathCommitLog().trim().split(MixAll.MULTI_PATH_SPLITTER); + for (int i = 0; i < 1024; i++) { + MappedFile mappedFile = mappedFileQueue.getLastMappedFile(fixedMsg.length * i); + assertThat(mappedFile).isNotNull(); + assertThat(mappedFile.appendMessage(fixedMsg)).isTrue(); + int idx = i % storePaths.length; + assertThat(mappedFile.getFileName().startsWith(storePaths[idx])).isTrue(); + } + mappedFileQueue.shutdown(1000); + mappedFileQueue.destroy(); + } + + @Test + public void testLoadReadOnlyMappedFiles() { + { + //create old mapped files + final byte[] fixedMsg = new byte[1024]; + MessageStoreConfig config = new MessageStoreConfig(); + config.setStorePathCommitLog("target/unit_test_store/a/" + MixAll.MULTI_PATH_SPLITTER + + "target/unit_test_store/b/" + MixAll.MULTI_PATH_SPLITTER + + "target/unit_test_store/c/"); + MappedFileQueue mappedFileQueue = new MultiPathMappedFileQueue(config, 1024, null, null); + String[] storePaths = config.getStorePathCommitLog().trim().split(MixAll.MULTI_PATH_SPLITTER); + for (int i = 0; i < 1024; i++) { + MappedFile mappedFile = mappedFileQueue.getLastMappedFile(fixedMsg.length * i); + assertThat(mappedFile).isNotNull(); + assertThat(mappedFile.appendMessage(fixedMsg)).isTrue(); + int idx = i % storePaths.length; + assertThat(mappedFile.getFileName().startsWith(storePaths[idx])).isTrue(); + } + mappedFileQueue.shutdown(1000); + } + + // test load and readonly + MessageStoreConfig config = new MessageStoreConfig(); + config.setStorePathCommitLog("target/unit_test_store/b/"); + config.setReadOnlyCommitLogStorePaths("target/unit_test_store/a" + MixAll.MULTI_PATH_SPLITTER + + "target/unit_test_store/c"); + MultiPathMappedFileQueue mappedFileQueue = new MultiPathMappedFileQueue(config, 1024, null, null); + + mappedFileQueue.load(); + + assertThat(mappedFileQueue.mappedFiles.size()).isEqualTo(1024); + for (int i = 0; i < 1024; i++) { + assertThat(mappedFileQueue.mappedFiles.get(i).getFile().getName()) + .isEqualTo(UtilAll.offset2FileName(1024 * i)); + } + mappedFileQueue.destroy(); + + } + + @Test + public void testUpdatePathsOnline() { + final byte[] fixedMsg = new byte[1024]; + + MessageStoreConfig config = new MessageStoreConfig(); + config.setStorePathCommitLog("target/unit_test_store/a/" + MixAll.MULTI_PATH_SPLITTER + + "target/unit_test_store/b/" + MixAll.MULTI_PATH_SPLITTER + + "target/unit_test_store/c/"); + MappedFileQueue mappedFileQueue = new MultiPathMappedFileQueue(config, 1024, null, null); + String[] storePaths = config.getStorePathCommitLog().trim().split(MixAll.MULTI_PATH_SPLITTER); + for (int i = 0; i < 1024; i++) { + MappedFile mappedFile = mappedFileQueue.getLastMappedFile(fixedMsg.length * i); + assertThat(mappedFile).isNotNull(); + assertThat(mappedFile.appendMessage(fixedMsg)).isTrue(); + int idx = i % storePaths.length; + assertThat(mappedFile.getFileName().startsWith(storePaths[idx])).isTrue(); + + if (i == 500) { + config.setStorePathCommitLog("target/unit_test_store/a/" + MixAll.MULTI_PATH_SPLITTER + + "target/unit_test_store/b/"); + storePaths = config.getStorePathCommitLog().trim().split(MixAll.MULTI_PATH_SPLITTER); + } + } + mappedFileQueue.shutdown(1000); + mappedFileQueue.destroy(); + } + + @Test + public void testFullStorePath() { + final byte[] fixedMsg = new byte[1024]; + + Set fullStorePath = new HashSet<>(); + MessageStoreConfig config = new MessageStoreConfig(); + config.setStorePathCommitLog("target/unit_test_store/a/" + MixAll.MULTI_PATH_SPLITTER + + "target/unit_test_store/b/" + MixAll.MULTI_PATH_SPLITTER + + "target/unit_test_store/c/"); + MappedFileQueue mappedFileQueue = new MultiPathMappedFileQueue(config, 1024, null, () -> fullStorePath); + String[] storePaths = config.getStorePathCommitLog().trim().split(MixAll.MULTI_PATH_SPLITTER); + assertThat(storePaths.length).isEqualTo(3); + + MappedFile mappedFile = mappedFileQueue.getLastMappedFile(0); + assertThat(mappedFile).isNotNull(); + assertThat(mappedFile.appendMessage(fixedMsg)).isTrue(); + assertThat(mappedFile.getFileName().startsWith(storePaths[0])).isTrue(); + + mappedFile = mappedFileQueue.getLastMappedFile(fixedMsg.length); + assertThat(mappedFile.getFileName().startsWith(storePaths[1])).isTrue(); + assertThat(mappedFile.appendMessage(fixedMsg)).isTrue(); + mappedFile = mappedFileQueue.getLastMappedFile(fixedMsg.length * 2); + assertThat(mappedFile.appendMessage(fixedMsg)).isTrue(); + assertThat(mappedFile.getFileName().startsWith(storePaths[2])).isTrue(); + + fullStorePath.add("target/unit_test_store/b/"); + mappedFile = mappedFileQueue.getLastMappedFile(fixedMsg.length * 3); + assertThat(mappedFile.appendMessage(fixedMsg)).isTrue(); + assertThat(mappedFile.getFileName().startsWith(storePaths[2])).isTrue(); + + mappedFile = mappedFileQueue.getLastMappedFile(fixedMsg.length * 4); + assertThat(mappedFile.appendMessage(fixedMsg)).isTrue(); + assertThat(mappedFile.getFileName().startsWith(storePaths[0])).isTrue(); + + mappedFileQueue.shutdown(1000); + mappedFileQueue.destroy(); + } +} \ No newline at end of file diff --git a/store/src/test/java/org/apache/rocketmq/store/RocksDBMessageStoreTest.java b/store/src/test/java/org/apache/rocketmq/store/RocksDBMessageStoreTest.java new file mode 100644 index 00000000000..2af07197a50 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/RocksDBMessageStoreTest.java @@ -0,0 +1,1078 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store; + +import java.io.File; +import java.io.RandomAccessFile; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.OverlappingFileLockException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import com.google.common.collect.Sets; + +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageBatch; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBatch; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.store.config.BrokerRole; +import org.apache.rocketmq.store.config.FlushDiskType; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.config.StorePathConfigHelper; +import org.apache.rocketmq.store.queue.ConsumeQueueInterface; +import org.apache.rocketmq.store.queue.CqUnit; +import org.apache.rocketmq.store.stats.BrokerStatsManager; +import org.assertj.core.util.Strings; +import org.awaitility.Awaitility; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +@RunWith(MockitoJUnitRunner.class) +public class RocksDBMessageStoreTest { + private final String storeMessage = "Once, there was a chance for me!"; + private final String messageTopic = "FooBar"; + private final String storeType = StoreType.DEFAULT_ROCKSDB.getStoreType(); + private int queueTotal = 100; + private final AtomicInteger queueId = new AtomicInteger(0); + private SocketAddress bornHost; + private SocketAddress storeHost; + private byte[] messageBody; + private MessageStore messageStore; + + @Before + public void init() throws Exception { + if (notExecuted()) { + return; + } + storeHost = new InetSocketAddress(InetAddress.getLocalHost(), 8123); + bornHost = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0); + + messageStore = buildMessageStore(); + boolean load = messageStore.load(); + assertTrue(load); + messageStore.start(); + } + + @Test(expected = OverlappingFileLockException.class) + public void test_repeat_restart() throws Exception { + if (notExecuted()) { + throw new OverlappingFileLockException(); + } + queueTotal = 1; + messageBody = storeMessage.getBytes(); + + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setMappedFileSizeCommitLog(1024 * 8); + messageStoreConfig.setMappedFileSizeConsumeQueue(1024 * 4); + messageStoreConfig.setMaxHashSlotNum(100); + messageStoreConfig.setMaxIndexNum(100 * 10); + messageStoreConfig.setStorePathRootDir(System.getProperty("java.io.tmpdir") + File.separator + "store"); + messageStoreConfig.setHaListenPort(0); + MessageStore master = new RocksDBMessageStore(messageStoreConfig, null, new MyMessageArrivingListener(), new BrokerConfig(), new ConcurrentHashMap<>()); + + boolean load = master.load(); + assertTrue(load); + + try { + master.start(); + master.start(); + } finally { + master.shutdown(); + master.destroy(); + } + } + + @After + public void destroy() { + if (notExecuted()) { + return; + } + messageStore.shutdown(); + messageStore.destroy(); + + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + File file = new File(messageStoreConfig.getStorePathRootDir()); + UtilAll.deleteFile(file); + } + + private MessageStore buildMessageStore() throws Exception { + return buildMessageStore(null, ""); + } + + private MessageStore buildMessageStore(String storePathRootDir, String topic) throws Exception { + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setMappedFileSizeCommitLog(1024 * 1024 * 10); + messageStoreConfig.setMappedFileSizeConsumeQueue(1024 * 1024 * 10); + messageStoreConfig.setMaxHashSlotNum(10000); + messageStoreConfig.setMaxIndexNum(100 * 100); + messageStoreConfig.setFlushDiskType(FlushDiskType.SYNC_FLUSH); + messageStoreConfig.setFlushIntervalConsumeQueue(1); + messageStoreConfig.setStoreType(storeType); + messageStoreConfig.setHaListenPort(0); + if (Strings.isNullOrEmpty(storePathRootDir)) { + UUID uuid = UUID.randomUUID(); + storePathRootDir = System.getProperty("java.io.tmpdir") + File.separator + "store-" + uuid.toString(); + } + messageStoreConfig.setStorePathRootDir(storePathRootDir); + ConcurrentMap topicConfigTable = new ConcurrentHashMap<>(); + topicConfigTable.put(topic, new TopicConfig(topic, 1, 1)); + return new RocksDBMessageStore(messageStoreConfig, + new BrokerStatsManager("simpleTest", true), + new MyMessageArrivingListener(), + new BrokerConfig(), topicConfigTable); + } + + @Test + public void testWriteAndRead() { + if (notExecuted()) { + return; + } + long ipv4HostMessages = 10; + long ipv6HostMessages = 10; + long totalMessages = ipv4HostMessages + ipv6HostMessages; + queueTotal = 1; + messageBody = storeMessage.getBytes(); + for (long i = 0; i < ipv4HostMessages; i++) { + messageStore.putMessage(buildMessage()); + } + + for (long i = 0; i < ipv6HostMessages; i++) { + messageStore.putMessage(buildIPv6HostMessage()); + } + + StoreTestUtil.waitCommitLogReput((RocksDBMessageStore) messageStore); + + for (long i = 0; i < totalMessages; i++) { + GetMessageResult result = messageStore.getMessage("GROUP_A", "FooBar", 0, i, 1024 * 1024, null); + assertThat(result).isNotNull(); + result.release(); + } + verifyThatMasterIsFunctional(totalMessages, messageStore); + } + + @Test + public void testLookMessageByOffset_OffsetIsFirst() { + if (notExecuted()) { + return; + } + final int totalCount = 10; + int queueId = new Random().nextInt(10); + String topic = "FooBar"; + int firstOffset = 0; + AppendMessageResult[] appendMessageResultArray = putMessages(totalCount, topic, queueId); + AppendMessageResult firstResult = appendMessageResultArray[0]; + + MessageExt messageExt = messageStore.lookMessageByOffset(firstResult.getWroteOffset()); + MessageExt messageExt1 = getDefaultMessageStore().lookMessageByOffset(firstResult.getWroteOffset(), firstResult.getWroteBytes()); + + assertThat(new String(messageExt.getBody())).isEqualTo(buildMessageBodyByOffset(storeMessage, firstOffset)); + assertThat(new String(messageExt1.getBody())).isEqualTo(buildMessageBodyByOffset(storeMessage, firstOffset)); + } + + @Test + public void testLookMessageByOffset_OffsetIsLast() { + if (notExecuted()) { + return; + } + final int totalCount = 10; + int queueId = new Random().nextInt(10); + String topic = "FooBar"; + AppendMessageResult[] appendMessageResultArray = putMessages(totalCount, topic, queueId); + int lastIndex = totalCount - 1; + AppendMessageResult lastResult = appendMessageResultArray[lastIndex]; + + MessageExt messageExt = getDefaultMessageStore().lookMessageByOffset(lastResult.getWroteOffset(), lastResult.getWroteBytes()); + + assertThat(new String(messageExt.getBody())).isEqualTo(buildMessageBodyByOffset(storeMessage, lastIndex)); + } + + @Test + public void testLookMessageByOffset_OffsetIsOutOfBound() { + if (notExecuted()) { + return; + } + final int totalCount = 10; + int queueId = new Random().nextInt(10); + String topic = "FooBar"; + AppendMessageResult[] appendMessageResultArray = putMessages(totalCount, topic, queueId); + long lastOffset = getMaxOffset(appendMessageResultArray); + + MessageExt messageExt = getDefaultMessageStore().lookMessageByOffset(lastOffset); + + assertThat(messageExt).isNull(); + } + + @Test + public void testGetOffsetInQueueByTime() { + if (notExecuted()) { + return; + } + final int totalCount = 10; + int queueId = 0; + String topic = "FooBar"; + AppendMessageResult[] appendMessageResults = putMessages(totalCount, topic, queueId, true); + //Thread.sleep(10); + StoreTestUtil.waitCommitLogReput((RocksDBMessageStore) messageStore); + + ConsumeQueueInterface consumeQueue = getDefaultMessageStore().findConsumeQueue(topic, queueId); + for (AppendMessageResult appendMessageResult : appendMessageResults) { + long offset = messageStore.getOffsetInQueueByTime(topic, queueId, appendMessageResult.getStoreTimestamp()); + CqUnit cqUnit = consumeQueue.get(offset); + assertThat(cqUnit.getPos()).isEqualTo(appendMessageResult.getWroteOffset()); + assertThat(cqUnit.getSize()).isEqualTo(appendMessageResult.getWroteBytes()); + } + } + + @Test + public void testGetOffsetInQueueByTime_TimestampIsSkewing() { + if (notExecuted()) { + return; + } + final int totalCount = 10; + int queueId = 0; + String topic = "FooBar"; + AppendMessageResult[] appendMessageResults = putMessages(totalCount, topic, queueId, true); + //Thread.sleep(10); + StoreTestUtil.waitCommitLogReput((RocksDBMessageStore) messageStore); + int skewing = 2; + + ConsumeQueueInterface consumeQueue = getDefaultMessageStore().findConsumeQueue(topic, queueId); + for (AppendMessageResult appendMessageResult : appendMessageResults) { + long offset = messageStore.getOffsetInQueueByTime(topic, queueId, appendMessageResult.getStoreTimestamp() - skewing); + CqUnit cqUnit = consumeQueue.get(offset); + assertThat(cqUnit.getPos()).isEqualTo(appendMessageResult.getWroteOffset()); + assertThat(cqUnit.getSize()).isEqualTo(appendMessageResult.getWroteBytes()); + } + } + + @Test + public void testGetOffsetInQueueByTime_TimestampSkewingIsLarge() { + if (notExecuted()) { + return; + } + final int totalCount = 10; + int queueId = 0; + String topic = "FooBar"; + AppendMessageResult[] appendMessageResults = putMessages(totalCount, topic, queueId, true); + //Thread.sleep(10); + StoreTestUtil.waitCommitLogReput((RocksDBMessageStore) messageStore); + int skewing = 20000; + + ConsumeQueueInterface consumeQueue = getDefaultMessageStore().findConsumeQueue(topic, queueId); + for (AppendMessageResult appendMessageResult : appendMessageResults) { + long offset = messageStore.getOffsetInQueueByTime(topic, queueId, appendMessageResult.getStoreTimestamp() - skewing); + CqUnit cqUnit = consumeQueue.get(offset); + assertThat(cqUnit.getPos()).isEqualTo(appendMessageResults[0].getWroteOffset()); + assertThat(cqUnit.getSize()).isEqualTo(appendMessageResults[0].getWroteBytes()); + } + } + + @Test + public void testGetOffsetInQueueByTime_ConsumeQueueNotFound1() { + if (notExecuted()) { + return; + } + final int totalCount = 10; + int queueId = 0; + int wrongQueueId = 1; + String topic = "FooBar"; + AppendMessageResult[] appendMessageResults = putMessages(totalCount, topic, queueId, false); + //Thread.sleep(10); + + StoreTestUtil.waitCommitLogReput((RocksDBMessageStore) messageStore); + + long offset = messageStore.getOffsetInQueueByTime(topic, wrongQueueId, appendMessageResults[0].getStoreTimestamp()); + + assertThat(offset).isEqualTo(0); + } + + @Test + public void testGetOffsetInQueueByTime_ConsumeQueueNotFound2() { + if (notExecuted()) { + return; + } + final int totalCount = 10; + int queueId = 0; + int wrongQueueId = 1; + String topic = "FooBar"; + putMessages(totalCount, topic, queueId, false); + //Thread.sleep(10); + StoreTestUtil.waitCommitLogReput((RocksDBMessageStore) messageStore); + + long messageStoreTimeStamp = messageStore.getMessageStoreTimeStamp(topic, wrongQueueId, 0); + + assertThat(messageStoreTimeStamp).isEqualTo(-1); + } + + @Test + public void testGetOffsetInQueueByTime_ConsumeQueueOffsetNotExist() { + if (notExecuted()) { + return; + } + final int totalCount = 10; + int queueId = 0; + int wrongQueueId = 1; + String topic = "FooBar"; + putMessages(totalCount, topic, queueId, true); + //Thread.sleep(10); + + StoreTestUtil.waitCommitLogReput((RocksDBMessageStore) messageStore); + + long messageStoreTimeStamp = messageStore.getMessageStoreTimeStamp(topic, wrongQueueId, -1); + + assertThat(messageStoreTimeStamp).isEqualTo(-1); + } + + @Test + public void testGetMessageStoreTimeStamp() { + if (notExecuted()) { + return; + } + final int totalCount = 10; + int queueId = 0; + String topic = "FooBar"; + AppendMessageResult[] appendMessageResults = putMessages(totalCount, topic, queueId, false); + //Thread.sleep(10); + StoreTestUtil.waitCommitLogReput((RocksDBMessageStore) messageStore); + + ConsumeQueueInterface consumeQueue = getDefaultMessageStore().findConsumeQueue(topic, queueId); + int minOffsetInQueue = (int) consumeQueue.getMinOffsetInQueue(); + for (int i = minOffsetInQueue; i < consumeQueue.getMaxOffsetInQueue(); i++) { + long messageStoreTimeStamp = messageStore.getMessageStoreTimeStamp(topic, queueId, i); + assertThat(messageStoreTimeStamp).isEqualTo(appendMessageResults[i].getStoreTimestamp()); + } + } + + @Test + public void testGetStoreTime_ParamIsNull() { + if (notExecuted()) { + return; + } + long storeTime = getStoreTime(null); + + assertThat(storeTime).isEqualTo(-1); + } + + @Test + public void testGetStoreTime_EverythingIsOk() { + if (notExecuted()) { + return; + } + final int totalCount = 10; + int queueId = 0; + String topic = "FooBar"; + AppendMessageResult[] appendMessageResults = putMessages(totalCount, topic, queueId, false); + //Thread.sleep(10); + StoreTestUtil.waitCommitLogReput((RocksDBMessageStore) messageStore); + ConsumeQueueInterface consumeQueue = messageStore.getConsumeQueue(topic, queueId); + + for (int i = 0; i < totalCount; i++) { + CqUnit cqUnit = consumeQueue.get(i); + long storeTime = getStoreTime(cqUnit); + assertThat(storeTime).isEqualTo(appendMessageResults[i].getStoreTimestamp()); + } + } + + @Test + public void testGetStoreTime_PhyOffsetIsLessThanCommitLogMinOffset() { + if (notExecuted()) { + return; + } + long phyOffset = -10; + int size = 138; + CqUnit cqUnit = new CqUnit(0, phyOffset, size, 0); + long storeTime = getStoreTime(cqUnit); + + assertThat(storeTime).isEqualTo(-1); + } + + @Test + public void testPutMessage_whenMessagePropertyIsTooLong() { + if (notExecuted()) { + return; + } + String topicName = "messagePropertyIsTooLongTest"; + MessageExtBrokerInner illegalMessage = buildSpecifyLengthPropertyMessage("123".getBytes(StandardCharsets.UTF_8), topicName, Short.MAX_VALUE + 1); + assertEquals(messageStore.putMessage(illegalMessage).getPutMessageStatus(), PutMessageStatus.PROPERTIES_SIZE_EXCEEDED); + assertEquals(0L, messageStore.getQueueStore().getMaxOffset(topicName, 0).longValue()); + MessageExtBrokerInner normalMessage = buildSpecifyLengthPropertyMessage("123".getBytes(StandardCharsets.UTF_8), topicName, 100); + assertEquals(messageStore.putMessage(normalMessage).getPutMessageStatus(), PutMessageStatus.PUT_OK); + assertEquals(1L, messageStore.getQueueStore().getMaxOffset(topicName, 0).longValue()); + } + + private RocksDBMessageStore getDefaultMessageStore() { + return (RocksDBMessageStore) this.messageStore; + } + + private AppendMessageResult[] putMessages(int totalCount, String topic, int queueId) { + return putMessages(totalCount, topic, queueId, false); + } + + private AppendMessageResult[] putMessages(int totalCount, String topic, int queueId, boolean interval) { + AppendMessageResult[] appendMessageResultArray = new AppendMessageResult[totalCount]; + for (int i = 0; i < totalCount; i++) { + String messageBody = buildMessageBodyByOffset(storeMessage, i); + + MessageExtBrokerInner msgInner = + i < totalCount / 2 ? buildMessage(messageBody.getBytes(), topic) : buildIPv6HostMessage(messageBody.getBytes(), topic); + msgInner.setQueueId(queueId); + PutMessageResult result = messageStore.putMessage(msgInner); + appendMessageResultArray[i] = result.getAppendMessageResult(); + assertThat(result.getPutMessageStatus()).isEqualTo(PutMessageStatus.PUT_OK); + if (interval) { + try { + Thread.sleep(10); + } catch (InterruptedException e) { + throw new RuntimeException("Thread sleep ERROR"); + } + } + } + return appendMessageResultArray; + } + + private long getMaxOffset(AppendMessageResult[] appendMessageResultArray) { + if (appendMessageResultArray == null) { + return 0; + } + AppendMessageResult last = appendMessageResultArray[appendMessageResultArray.length - 1]; + return last.getWroteOffset() + last.getWroteBytes(); + } + + private String buildMessageBodyByOffset(String message, long i) { + return String.format("%s offset %d", message, i); + } + + private long getStoreTime(CqUnit cqUnit) { + try { + Class abstractConsumeQueueStore = getDefaultMessageStore().getQueueStore().getClass().getSuperclass(); + Method getStoreTime = abstractConsumeQueueStore.getDeclaredMethod("getStoreTime", CqUnit.class); + getStoreTime.setAccessible(true); + return (long) getStoreTime.invoke(getDefaultMessageStore().getQueueStore(), cqUnit); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + private MessageExtBrokerInner buildMessage(byte[] messageBody, String topic) { + MessageExtBrokerInner msg = new MessageExtBrokerInner(); + msg.setTopic(topic); + msg.setTags("TAG1"); + msg.setKeys("Hello"); + msg.setBody(messageBody); + msg.setKeys(String.valueOf(System.currentTimeMillis())); + msg.setQueueId(Math.abs(queueId.getAndIncrement()) % queueTotal); + msg.setSysFlag(0); + msg.setBornTimestamp(System.currentTimeMillis()); + msg.setStoreHost(storeHost); + msg.setBornHost(bornHost); + msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); + return msg; + } + + private MessageExtBrokerInner buildSpecifyLengthPropertyMessage(byte[] messageBody, String topic, int length) { + StringBuilder stringBuilder = new StringBuilder(); + Random random = new Random(); + for (int i = 0; i < length; i++) { + stringBuilder.append(random.nextInt(10)); + } + MessageExtBrokerInner msg = new MessageExtBrokerInner(); + msg.putUserProperty("test", stringBuilder.toString()); + msg.setTopic(topic); + msg.setTags("TAG1"); + msg.setKeys("Hello"); + msg.setBody(messageBody); + msg.setQueueId(0); + msg.setBornTimestamp(System.currentTimeMillis()); + msg.setStoreHost(storeHost); + msg.setBornHost(bornHost); + msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); + return msg; + } + + private MessageExtBrokerInner buildIPv6HostMessage(byte[] messageBody, String topic) { + MessageExtBrokerInner msg = new MessageExtBrokerInner(); + msg.setTopic(topic); + msg.setTags("TAG1"); + msg.setKeys("Hello"); + msg.setBody(messageBody); + msg.setMsgId("24084004018081003FAA1DDE2B3F898A00002A9F0000000000000CA0"); + msg.setKeys(String.valueOf(System.currentTimeMillis())); + msg.setQueueId(Math.abs(queueId.getAndIncrement()) % queueTotal); + msg.setSysFlag(0); + msg.setBornHostV6Flag(); + msg.setStoreHostAddressV6Flag(); + msg.setBornTimestamp(System.currentTimeMillis()); + try { + msg.setBornHost(new InetSocketAddress(InetAddress.getByName("1050:0000:0000:0000:0005:0600:300c:326b"), 0)); + } catch (UnknownHostException e) { + fail("build IPv6 host message error", e); + } + + try { + msg.setStoreHost(new InetSocketAddress(InetAddress.getByName("::1"), 0)); + } catch (UnknownHostException e) { + fail("build IPv6 host message error", e); + } + return msg; + } + + private MessageExtBrokerInner buildMessage() { + return buildMessage(messageBody, messageTopic); + } + + public MessageExtBatch buildMessageBatch(MessageBatch msgBatch) { + MessageExtBatch msgExtBatch = new MessageExtBatch(); + msgExtBatch.setTopic(messageTopic); + msgExtBatch.setTags("TAG1"); + msgExtBatch.setKeys("Hello"); + msgExtBatch.setBody(msgBatch.getBody()); + msgExtBatch.setKeys(String.valueOf(System.currentTimeMillis())); + msgExtBatch.setQueueId(Math.abs(queueId.getAndIncrement()) % queueTotal); + msgExtBatch.setSysFlag(0); + msgExtBatch.setBornTimestamp(System.currentTimeMillis()); + msgExtBatch.setStoreHost(storeHost); + msgExtBatch.setBornHost(bornHost); + return msgExtBatch; + } + + @Test + public void testGroupCommit() { + if (notExecuted()) { + return; + } + long totalMessages = 10; + queueTotal = 1; + messageBody = storeMessage.getBytes(); + for (long i = 0; i < totalMessages; i++) { + messageStore.putMessage(buildMessage()); + } + + for (long i = 0; i < totalMessages; i++) { + GetMessageResult result = messageStore.getMessage("GROUP_A", "TOPIC_A", 0, i, 1024 * 1024, null); + assertThat(result).isNotNull(); + result.release(); + } + verifyThatMasterIsFunctional(totalMessages, messageStore); + } + + @Test + public void testMaxOffset() { + if (notExecuted()) { + return; + } + int firstBatchMessages = 3; + int queueId = 0; + messageBody = storeMessage.getBytes(); + + assertThat(messageStore.getMaxOffsetInQueue(messageTopic, queueId)).isEqualTo(0); + + for (int i = 0; i < firstBatchMessages; i++) { + final MessageExtBrokerInner msg = buildMessage(); + msg.setQueueId(queueId); + messageStore.putMessage(msg); + } + + Awaitility.await() + .with() + .atMost(3, TimeUnit.SECONDS) + .pollInterval(1, TimeUnit.MILLISECONDS) + .until(() -> messageStore.getMaxOffsetInQueue(messageTopic, queueId) == firstBatchMessages); + + // Disable the dispatcher + messageStore.getDispatcherList().clear(); + + int secondBatchMessages = 2; + + for (int i = 0; i < secondBatchMessages; i++) { + final MessageExtBrokerInner msg = buildMessage(); + msg.setQueueId(queueId); + messageStore.putMessage(msg); + } + + assertThat(messageStore.getMaxOffsetInQueue(messageTopic, queueId)).isEqualTo(firstBatchMessages); + assertThat(messageStore.getMaxOffsetInQueue(messageTopic, queueId, true)).isEqualTo(firstBatchMessages); + assertThat(messageStore.getMaxOffsetInQueue(messageTopic, queueId, false)).isEqualTo(firstBatchMessages + secondBatchMessages); + } + + private MessageExtBrokerInner buildIPv6HostMessage() { + return buildIPv6HostMessage(messageBody, "FooBar"); + } + + private void verifyThatMasterIsFunctional(long totalMessages, MessageStore master) { + for (long i = 0; i < totalMessages; i++) { + master.putMessage(buildMessage()); + } + + StoreTestUtil.waitCommitLogReput((RocksDBMessageStore) messageStore); + + for (long i = 0; i < totalMessages; i++) { + GetMessageResult result = master.getMessage("GROUP_A", "FooBar", 0, i, 1024 * 1024, null); + assertThat(result).isNotNull(); + result.release(); + + } + } + + @Test + public void testPullSize() { + if (notExecuted()) { + return; + } + String topic = "pullSizeTopic"; + + for (int i = 0; i < 32; i++) { + MessageExtBrokerInner messageExtBrokerInner = buildMessage(); + messageExtBrokerInner.setTopic(topic); + messageExtBrokerInner.setQueueId(0); + messageStore.putMessage(messageExtBrokerInner); + } + // wait for consume queue build + Awaitility.await().atMost(10, TimeUnit.SECONDS) + .with() + .pollInterval(10, TimeUnit.MILLISECONDS) + .until(() -> messageStore.getMaxOffsetInQueue(topic, 0) >= 32); + + String group = "simple"; + GetMessageResult getMessageResult32 = messageStore.getMessage(group, topic, 0, 0, 32, null); + assertThat(getMessageResult32.getMessageBufferList().size()).isEqualTo(32); + getMessageResult32.release(); + + GetMessageResult getMessageResult20 = messageStore.getMessage(group, topic, 0, 0, 20, null); + assertThat(getMessageResult20.getMessageBufferList().size()).isEqualTo(20); + + getMessageResult20.release(); + GetMessageResult getMessageResult45 = messageStore.getMessage(group, topic, 0, 0, 10, null); + assertThat(getMessageResult45.getMessageBufferList().size()).isEqualTo(10); + getMessageResult45.release(); + + } + + @Test + public void testRecover() throws Exception { + if (notExecuted()) { + return; + } + String topic = "recoverTopic"; + messageBody = storeMessage.getBytes(); + for (int i = 0; i < 100; i++) { + MessageExtBrokerInner messageExtBrokerInner = buildMessage(); + messageExtBrokerInner.setTopic(topic); + messageExtBrokerInner.setQueueId(0); + messageStore.putMessage(messageExtBrokerInner); + } + + // wait for build consumer queue + Awaitility.await() + .with() + .pollInterval(100, TimeUnit.MILLISECONDS) + .atMost(10, TimeUnit.SECONDS) + .until(() -> messageStore.getMaxOffsetInQueue(topic, 0) >= 100); + + long maxPhyOffset = messageStore.getMaxPhyOffset(); + long maxCqOffset = messageStore.getMaxOffsetInQueue(topic, 0); + + //1.just reboot + messageStore.shutdown(); + String storeRootDir = messageStore.getMessageStoreConfig().getStorePathRootDir(); + messageStore = buildMessageStore(storeRootDir, topic); + boolean load = messageStore.load(); + assertTrue(load); + messageStore.start(); + assertEquals(maxPhyOffset, messageStore.getMaxPhyOffset()); + assertEquals(maxCqOffset, messageStore.getMaxOffsetInQueue(topic, 0)); + + //2.damage commit-log and reboot normal + for (int i = 0; i < 100; i++) { + MessageExtBrokerInner messageExtBrokerInner = buildMessage(); + messageExtBrokerInner.setTopic(topic); + messageExtBrokerInner.setQueueId(0); + messageStore.putMessage(messageExtBrokerInner); + } + + Awaitility.await() + .with() + .pollInterval(100, TimeUnit.MILLISECONDS) + .atMost(10, TimeUnit.SECONDS) + .until(() -> messageStore.getMaxOffsetInQueue(topic, 0) >= 200); + + long secondLastPhyOffset = messageStore.getMaxPhyOffset(); + long secondLastCqOffset = messageStore.getMaxOffsetInQueue(topic, 0); + + // Append a message to corrupt + MessageExtBrokerInner messageExtBrokerInner = buildMessage(); + messageExtBrokerInner.setTopic(topic); + messageExtBrokerInner.setQueueId(0); + messageStore.putMessage(messageExtBrokerInner); + + messageStore.shutdown(); + + // Corrupt the last message + damageCommitLog((RocksDBMessageStore) messageStore, secondLastPhyOffset); + + //reboot + messageStore = buildMessageStore(storeRootDir, topic); + load = messageStore.load(); + assertTrue(load); + messageStore.start(); + assertEquals(secondLastPhyOffset, messageStore.getMaxPhyOffset()); + assertEquals(secondLastCqOffset, messageStore.getMaxOffsetInQueue(topic, 0)); + + //3.Corrupt commit-log and reboot abnormal + for (int i = 0; i < 100; i++) { + messageExtBrokerInner = buildMessage(); + messageExtBrokerInner.setTopic(topic); + messageExtBrokerInner.setQueueId(0); + messageStore.putMessage(messageExtBrokerInner); + } + + Awaitility.await() + .with() + .pollInterval(100, TimeUnit.MILLISECONDS) + .atMost(10, TimeUnit.SECONDS) + .until(() -> messageStore.getMaxOffsetInQueue(topic, 0) >= 300); + + secondLastPhyOffset = messageStore.getMaxPhyOffset(); + secondLastCqOffset = messageStore.getMaxOffsetInQueue(topic, 0); + + messageExtBrokerInner = buildMessage(); + messageExtBrokerInner.setTopic(topic); + messageExtBrokerInner.setQueueId(0); + messageStore.putMessage(messageExtBrokerInner); + messageStore.shutdown(); + + //Corrupt the last message + damageCommitLog((RocksDBMessageStore) messageStore, secondLastPhyOffset); + //add abort file + String fileName = StorePathConfigHelper.getAbortFile(messageStore.getMessageStoreConfig().getStorePathRootDir()); + File file = new File(fileName); + UtilAll.ensureDirOK(file.getParent()); + assertTrue(file.createNewFile()); + + messageStore = buildMessageStore(storeRootDir, topic); + load = messageStore.load(); + assertTrue(load); + messageStore.start(); + assertEquals(secondLastPhyOffset, messageStore.getMaxPhyOffset()); + assertEquals(secondLastCqOffset, messageStore.getMaxOffsetInQueue(topic, 0)); + + //message write again + for (int i = 0; i < 100; i++) { + messageExtBrokerInner = buildMessage(); + messageExtBrokerInner.setTopic(topic); + messageExtBrokerInner.setQueueId(0); + messageStore.putMessage(messageExtBrokerInner); + } + } + + @Test + public void testStorePathOK() { + if (notExecuted()) { + return; + } + if (messageStore instanceof RocksDBMessageStore) { + assertTrue(fileExists(((RocksDBMessageStore) messageStore).getStorePathPhysic())); + assertTrue(fileExists(((RocksDBMessageStore) messageStore).getStorePathLogic())); + } + } + + private boolean fileExists(String path) { + if (path != null) { + File f = new File(path); + return f.exists(); + } + return false; + } + + private void damageCommitLog(RocksDBMessageStore store, long offset) throws Exception { + assertThat(store).isNotNull(); + MessageStoreConfig messageStoreConfig = store.getMessageStoreConfig(); + File file = new File(messageStoreConfig.getStorePathCommitLog() + File.separator + "00000000000000000000"); + try (RandomAccessFile raf = new RandomAccessFile(file, "rw"); + FileChannel fileChannel = raf.getChannel()) { + MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 1024 * 1024 * 10); + int bodyLen = mappedByteBuffer.getInt((int) offset + 84); + int topicLenIndex = (int) offset + 84 + bodyLen + 4; + mappedByteBuffer.position(topicLenIndex); + mappedByteBuffer.putInt(0); + mappedByteBuffer.putInt(0); + mappedByteBuffer.putInt(0); + mappedByteBuffer.putInt(0); + mappedByteBuffer.force(); + fileChannel.force(true); + } + } + + @Test + public void testPutMsgExceedsMaxLength() { + if (notExecuted()) { + return; + } + messageBody = new byte[4 * 1024 * 1024 + 1]; + MessageExtBrokerInner msg = buildMessage(); + + PutMessageResult result = messageStore.putMessage(msg); + assertThat(result.getPutMessageStatus()).isEqualTo(PutMessageStatus.MESSAGE_ILLEGAL); + } + + @Test + public void testPutMsgBatchExceedsMaxLength() { + if (notExecuted()) { + return; + } + messageBody = new byte[4 * 1024 * 1024 + 1]; + MessageExtBrokerInner msg1 = buildMessage(); + MessageExtBrokerInner msg2 = buildMessage(); + MessageExtBrokerInner msg3 = buildMessage(); + + MessageBatch msgBatch = MessageBatch.generateFromList(Arrays.asList(msg1, msg2, msg3)); + msgBatch.setBody(msgBatch.encode()); + + MessageExtBatch msgExtBatch = buildMessageBatch(msgBatch); + + try { + this.messageStore.putMessages(msgExtBatch); + fail("Should have raised an exception"); + } catch (Exception e) { + assertThat(e.getMessage()).contains("message body size exceeded"); + } + } + + @Test + public void testPutMsgWhenReplicasNotEnough() { + if (notExecuted()) { + return; + } + MessageStoreConfig messageStoreConfig = this.messageStore.getMessageStoreConfig(); + messageStoreConfig.setBrokerRole(BrokerRole.SYNC_MASTER); + messageStoreConfig.setTotalReplicas(2); + messageStoreConfig.setInSyncReplicas(2); + messageStoreConfig.setEnableAutoInSyncReplicas(false); + ((RocksDBMessageStore) this.messageStore).getBrokerConfig().setEnableSlaveActingMaster(true); + this.messageStore.setAliveReplicaNumInGroup(1); + + MessageExtBrokerInner msg = buildMessage(); + PutMessageResult result = this.messageStore.putMessage(msg); + assertThat(result.getPutMessageStatus()).isEqualTo(PutMessageStatus.IN_SYNC_REPLICAS_NOT_ENOUGH); + ((RocksDBMessageStore) this.messageStore).getBrokerConfig().setEnableSlaveActingMaster(false); + } + + @Test + public void testPutMsgWhenAdaptiveDegradation() { + if (notExecuted()) { + return; + } + MessageStoreConfig messageStoreConfig = this.messageStore.getMessageStoreConfig(); + messageStoreConfig.setBrokerRole(BrokerRole.SYNC_MASTER); + messageStoreConfig.setTotalReplicas(2); + messageStoreConfig.setInSyncReplicas(2); + messageStoreConfig.setEnableAutoInSyncReplicas(true); + ((RocksDBMessageStore) this.messageStore).getBrokerConfig().setEnableSlaveActingMaster(true); + this.messageStore.setAliveReplicaNumInGroup(1); + + MessageExtBrokerInner msg = buildMessage(); + PutMessageResult result = this.messageStore.putMessage(msg); + assertThat(result.getPutMessageStatus()).isEqualTo(PutMessageStatus.PUT_OK); + ((RocksDBMessageStore) this.messageStore).getBrokerConfig().setEnableSlaveActingMaster(false); + messageStoreConfig.setEnableAutoInSyncReplicas(false); + } + + @Test + public void testGetBulkCommitLogData() { + if (notExecuted()) { + return; + } + RocksDBMessageStore defaultMessageStore = (RocksDBMessageStore) messageStore; + + messageBody = new byte[2 * 1024 * 1024]; + + for (int i = 0; i < 10; i++) { + MessageExtBrokerInner msg1 = buildMessage(); + messageStore.putMessage(msg1); + } + + List bufferResultList = defaultMessageStore.getBulkCommitLogData(0, (int) defaultMessageStore.getMaxPhyOffset()); + List msgList = new ArrayList<>(); + for (SelectMappedBufferResult bufferResult : bufferResultList) { + msgList.addAll(MessageDecoder.decodesBatch(bufferResult.getByteBuffer(), true, false, false)); + bufferResult.release(); + } + + assertThat(msgList.size()).isEqualTo(10); + } + + @Test + public void testPutLongMessage() { + if (notExecuted()) { + return; + } + MessageExtBrokerInner messageExtBrokerInner = buildMessage(); + CommitLog commitLog = messageStore.getCommitLog(); + MessageStoreConfig messageStoreConfig = messageStore.getMessageStoreConfig(); + MessageExtEncoder.PutMessageThreadLocal putMessageThreadLocal = commitLog.getPutMessageThreadLocal().get(); + + //body size, topic size, properties size exactly equal to max size + messageExtBrokerInner.setBody(new byte[messageStoreConfig.getMaxMessageSize()]); + messageExtBrokerInner.setTopic(new String(new byte[127])); + messageExtBrokerInner.setPropertiesString(new String(new byte[Short.MAX_VALUE])); + PutMessageResult encodeResult1 = putMessageThreadLocal.getEncoder().encode(messageExtBrokerInner); + assertNull(encodeResult1); + + //body size exactly more than max message body size + messageExtBrokerInner.setBody(new byte[messageStoreConfig.getMaxMessageSize() + 1]); + PutMessageResult encodeResult2 = putMessageThreadLocal.getEncoder().encode(messageExtBrokerInner); + assertSame(encodeResult2.getPutMessageStatus(), PutMessageStatus.MESSAGE_ILLEGAL); + + //body size exactly equal to max message size + messageExtBrokerInner.setBody(new byte[messageStoreConfig.getMaxMessageSize() + 64 * 1024]); + PutMessageResult encodeResult3 = putMessageThreadLocal.getEncoder().encode(messageExtBrokerInner); + assertSame(encodeResult3.getPutMessageStatus(), PutMessageStatus.MESSAGE_ILLEGAL); + + //message properties length more than properties maxSize + messageExtBrokerInner.setBody(new byte[messageStoreConfig.getMaxMessageSize()]); + messageExtBrokerInner.setPropertiesString(new String(new byte[Short.MAX_VALUE + 1])); + PutMessageResult encodeResult4 = putMessageThreadLocal.getEncoder().encode(messageExtBrokerInner); + assertSame(encodeResult4.getPutMessageStatus(), PutMessageStatus.PROPERTIES_SIZE_EXCEEDED); + + //message length more than buffer length capacity + messageExtBrokerInner.setBody(new byte[messageStoreConfig.getMaxMessageSize()]); + messageExtBrokerInner.setTopic(new String(new byte[Short.MAX_VALUE])); + messageExtBrokerInner.setPropertiesString(new String(new byte[Short.MAX_VALUE])); + PutMessageResult encodeResult5 = putMessageThreadLocal.getEncoder().encode(messageExtBrokerInner); + assertSame(encodeResult5.getPutMessageStatus(), PutMessageStatus.MESSAGE_ILLEGAL); + } + + @Test + public void testDynamicMaxMessageSize() { + if (notExecuted()) { + return; + } + MessageExtBrokerInner messageExtBrokerInner = buildMessage(); + MessageStoreConfig messageStoreConfig = messageStore.getMessageStoreConfig(); + int originMaxMessageSize = messageStoreConfig.getMaxMessageSize(); + + messageExtBrokerInner.setBody(new byte[originMaxMessageSize + 10]); + PutMessageResult putMessageResult = messageStore.putMessage(messageExtBrokerInner); + assertSame(putMessageResult.getPutMessageStatus(), PutMessageStatus.MESSAGE_ILLEGAL); + + int newMaxMessageSize = originMaxMessageSize + 10; + messageStoreConfig.setMaxMessageSize(newMaxMessageSize); + putMessageResult = messageStore.putMessage(messageExtBrokerInner); + assertSame(putMessageResult.getPutMessageStatus(), PutMessageStatus.PUT_OK); + + messageStoreConfig.setMaxMessageSize(10); + putMessageResult = messageStore.putMessage(messageExtBrokerInner); + assertSame(putMessageResult.getPutMessageStatus(), PutMessageStatus.MESSAGE_ILLEGAL); + + messageStoreConfig.setMaxMessageSize(originMaxMessageSize); + } + + @Test + public void testDeleteTopics() { + if (notExecuted()) { + return; + } + MessageStoreConfig messageStoreConfig = messageStore.getMessageStoreConfig(); + ConcurrentMap> consumeQueueTable = + ((RocksDBMessageStore) messageStore).getConsumeQueueTable(); + for (int i = 0; i < 10; i++) { + ConcurrentMap cqTable = new ConcurrentHashMap<>(); + String topicName = "topic-" + i; + for (int j = 0; j < 4; j++) { + ConsumeQueue consumeQueue = new ConsumeQueue(topicName, j, messageStoreConfig.getStorePathRootDir(), + messageStoreConfig.getMappedFileSizeConsumeQueue(), messageStore); + cqTable.put(j, consumeQueue); + } + consumeQueueTable.put(topicName, cqTable); + } + assertEquals(consumeQueueTable.size(), 10); + HashSet resultSet = Sets.newHashSet("topic-3", "topic-5"); + messageStore.deleteTopics(Sets.difference(consumeQueueTable.keySet(), resultSet)); + assertEquals(consumeQueueTable.size(), 2); + assertEquals(resultSet, consumeQueueTable.keySet()); + } + + @Test + public void testCleanUnusedTopic() { + if (notExecuted()) { + return; + } + MessageStoreConfig messageStoreConfig = messageStore.getMessageStoreConfig(); + ConcurrentMap> consumeQueueTable = + ((RocksDBMessageStore) messageStore).getConsumeQueueTable(); + for (int i = 0; i < 10; i++) { + ConcurrentMap cqTable = new ConcurrentHashMap<>(); + String topicName = "topic-" + i; + for (int j = 0; j < 4; j++) { + ConsumeQueue consumeQueue = new ConsumeQueue(topicName, j, messageStoreConfig.getStorePathRootDir(), + messageStoreConfig.getMappedFileSizeConsumeQueue(), messageStore); + cqTable.put(j, consumeQueue); + } + consumeQueueTable.put(topicName, cqTable); + } + assertEquals(consumeQueueTable.size(), 10); + HashSet resultSet = Sets.newHashSet("topic-3", "topic-5"); + messageStore.cleanUnusedTopic(resultSet); + assertEquals(consumeQueueTable.size(), 2); + assertEquals(resultSet, consumeQueueTable.keySet()); + } + + private static class MyMessageArrivingListener implements MessageArrivingListener { + @Override + public void arriving(String topic, int queueId, long logicOffset, long tagsCode, long msgStoreTime, + byte[] filterBitMap, Map properties) { + } + } + + private boolean notExecuted() { + return MixAll.isMac(); + } +} + + diff --git a/store/src/test/java/org/apache/rocketmq/store/StoreCheckpointTest.java b/store/src/test/java/org/apache/rocketmq/store/StoreCheckpointTest.java index 3c0d9253afa..9137254798b 100644 --- a/store/src/test/java/org/apache/rocketmq/store/StoreCheckpointTest.java +++ b/store/src/test/java/org/apache/rocketmq/store/StoreCheckpointTest.java @@ -48,7 +48,7 @@ public void testWriteAndRead() throws IOException { } @After - public void destory() { + public void destroy() { File file = new File("target/checkpoint_test"); UtilAll.deleteFile(file); } diff --git a/store/src/test/java/org/apache/rocketmq/store/StoreStatsServiceTest.java b/store/src/test/java/org/apache/rocketmq/store/StoreStatsServiceTest.java new file mode 100644 index 00000000000..afecbb2430d --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/StoreStatsServiceTest.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.concurrent.BrokenBarrierException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.LongAdder; + +import org.junit.Test; + +public class StoreStatsServiceTest { + + @Test + public void getSinglePutMessageTopicSizeTotal() throws Exception { + final StoreStatsService storeStatsService = new StoreStatsService(); + int num = Runtime.getRuntime().availableProcessors() * 2; + for (int j = 0; j < 100; j++) { + final AtomicReference reference = new AtomicReference<>(null); + final CountDownLatch latch = new CountDownLatch(num); + final CyclicBarrier barrier = new CyclicBarrier(num); + for (int i = 0; i < num; i++) { + new Thread(new Runnable() { + @Override + public void run() { + try { + barrier.await(); + LongAdder longAdder = storeStatsService.getSinglePutMessageTopicSizeTotal("test"); + if (reference.compareAndSet(null, longAdder)) { + } else if (reference.get() != longAdder) { + throw new RuntimeException("Reference should be same!"); + } + } catch (InterruptedException | BrokenBarrierException e) { + e.printStackTrace(); + } finally { + latch.countDown(); + } + } + }).start(); + } + latch.await(); + } + } + + @Test + public void getSinglePutMessageTopicTimesTotal() throws Exception { + final StoreStatsService storeStatsService = new StoreStatsService(); + int num = Runtime.getRuntime().availableProcessors() * 2; + for (int j = 0; j < 100; j++) { + final AtomicReference reference = new AtomicReference<>(null); + final CountDownLatch latch = new CountDownLatch(num); + final CyclicBarrier barrier = new CyclicBarrier(num); + for (int i = 0; i < num; i++) { + new Thread(new Runnable() { + @Override + public void run() { + try { + barrier.await(); + LongAdder longAdder = storeStatsService.getSinglePutMessageTopicTimesTotal("test"); + if (reference.compareAndSet(null, longAdder)) { + } else if (reference.get() != longAdder) { + throw new RuntimeException("Reference should be same!"); + } + } catch (InterruptedException | BrokenBarrierException e) { + e.printStackTrace(); + } finally { + latch.countDown(); + } + } + }).start(); + } + latch.await(); + } + } + + @Test + public void findPutMessageEntireTimePXTest() throws InvocationTargetException, NoSuchMethodException, IllegalAccessException { + final StoreStatsService storeStatsService = new StoreStatsService(); + for (int i = 1; i <= 1000; i++) { + for (int j = 0; j < i; j++) { + storeStatsService.incPutMessageEntireTime(i); + } + } + Method method = StoreStatsService.class.getDeclaredMethod("resetPutMessageTimeBuckets"); + method.setAccessible(true); + method.invoke(storeStatsService); + } + +} \ No newline at end of file diff --git a/store/src/test/java/org/apache/rocketmq/store/StoreTestBase.java b/store/src/test/java/org/apache/rocketmq/store/StoreTestBase.java new file mode 100644 index 00000000000..ae0841b8b8e --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/StoreTestBase.java @@ -0,0 +1,182 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store; + +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExtBatch; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.junit.After; + +import java.io.File; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +public class StoreTestBase { + + private static final int QUEUE_TOTAL = 100; + private AtomicInteger queueId = new AtomicInteger(0); + protected SocketAddress bornHost = new InetSocketAddress("127.0.0.1", 8123); + protected SocketAddress storeHost = bornHost; + private byte[] messageBody = new byte[1024]; + + protected Set baseDirs = new HashSet<>(); + + private static AtomicInteger port = new AtomicInteger(30000); + + public static synchronized int nextPort() { + return port.addAndGet(5); + } + + protected MessageExtBatch buildBatchMessage(int size) { + MessageExtBatch messageExtBatch = new MessageExtBatch(); + messageExtBatch.setTopic("StoreTest"); + messageExtBatch.setTags("TAG1"); + messageExtBatch.setKeys("Hello"); + messageExtBatch.setQueueId(Math.abs(queueId.getAndIncrement()) % QUEUE_TOTAL); + messageExtBatch.setSysFlag(0); + + messageExtBatch.setBornTimestamp(System.currentTimeMillis()); + messageExtBatch.setBornHost(bornHost); + messageExtBatch.setStoreHost(storeHost); + + List messageList = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + messageList.add(buildMessage()); + } + + messageExtBatch.setBody(MessageDecoder.encodeMessages(messageList)); + + return messageExtBatch; + } + + protected MessageExtBrokerInner buildMessage() { + MessageExtBrokerInner msg = new MessageExtBrokerInner(); + msg.setTopic("StoreTest"); + msg.setTags("TAG1"); + msg.setKeys("Hello"); + msg.setBody(messageBody); + msg.setKeys(String.valueOf(System.currentTimeMillis())); + msg.setQueueId(Math.abs(queueId.getAndIncrement()) % QUEUE_TOTAL); + msg.setSysFlag(0); + msg.setBornTimestamp(System.currentTimeMillis()); + msg.setStoreHost(storeHost); + msg.setBornHost(bornHost); + return msg; + } + + protected MessageExtBatch buildIPv6HostBatchMessage(int size) { + MessageExtBatch messageExtBatch = new MessageExtBatch(); + messageExtBatch.setTopic("StoreTest"); + messageExtBatch.setTags("TAG1"); + messageExtBatch.setKeys("Hello"); + messageExtBatch.setBody(messageBody); + messageExtBatch.setMsgId("24084004018081003FAA1DDE2B3F898A00002A9F0000000000000CA0"); + messageExtBatch.setKeys(String.valueOf(System.currentTimeMillis())); + messageExtBatch.setQueueId(Math.abs(queueId.getAndIncrement()) % QUEUE_TOTAL); + messageExtBatch.setSysFlag(0); + messageExtBatch.setBornHostV6Flag(); + messageExtBatch.setStoreHostAddressV6Flag(); + messageExtBatch.setBornTimestamp(System.currentTimeMillis()); + try { + messageExtBatch.setBornHost(new InetSocketAddress(InetAddress.getByName("1050:0000:0000:0000:0005:0600:300c:326b"), 8123)); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + + try { + messageExtBatch.setStoreHost(new InetSocketAddress(InetAddress.getByName("::1"), 8123)); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + + List messageList = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + messageList.add(buildIPv6HostMessage()); + } + + messageExtBatch.setBody(MessageDecoder.encodeMessages(messageList)); + return messageExtBatch; + } + + protected MessageExtBrokerInner buildIPv6HostMessage() { + MessageExtBrokerInner msg = new MessageExtBrokerInner(); + msg.setTopic("StoreTest"); + msg.setTags("TAG1"); + msg.setKeys("Hello"); + msg.setBody(messageBody); + msg.setMsgId("24084004018081003FAA1DDE2B3F898A00002A9F0000000000000CA0"); + msg.setKeys(String.valueOf(System.currentTimeMillis())); + msg.setQueueId(Math.abs(queueId.getAndIncrement()) % QUEUE_TOTAL); + msg.setSysFlag(0); + msg.setBornHostV6Flag(); + msg.setStoreHostAddressV6Flag(); + msg.setBornTimestamp(System.currentTimeMillis()); + try { + msg.setBornHost(new InetSocketAddress(InetAddress.getByName("1050:0000:0000:0000:0005:0600:300c:326b"), 8123)); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + + try { + msg.setStoreHost(new InetSocketAddress(InetAddress.getByName("::1"), 8123)); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + return msg; + } + + public static String createBaseDir() { + String baseDir = System.getProperty("java.io.tmpdir") + File.separator + "unitteststore" + File.separator + UUID.randomUUID(); + final File file = new File(baseDir); + if (file.exists()) { + System.exit(1); + } + return baseDir; + } + + public static boolean makeSureFileExists(String fileName) throws Exception { + File file = new File(fileName); + UtilAll.ensureDirOK(file.getParent()); + return file.createNewFile(); + } + + public static void deleteFile(String fileName) { + deleteFile(new File(fileName)); + } + + public static void deleteFile(File file) { + UtilAll.deleteFile(file); + } + + @After + public void clear() { + for (String baseDir : baseDirs) { + deleteFile(baseDir); + } + } + +} diff --git a/store/src/test/java/org/apache/rocketmq/store/StoreTestUtil.java b/store/src/test/java/org/apache/rocketmq/store/StoreTestUtil.java new file mode 100644 index 00000000000..17a2b5e19d7 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/StoreTestUtil.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store; + +import io.openmessaging.storage.dledger.store.file.DefaultMmapFile; +import io.openmessaging.storage.dledger.store.file.MmapFile; +import java.io.IOException; +import java.util.List; +import org.apache.commons.lang3.SystemUtils; +import org.apache.rocketmq.logging.org.slf4j.Logger; +import org.apache.rocketmq.logging.org.slf4j.LoggerFactory; +import org.apache.rocketmq.store.index.IndexFile; +import org.apache.rocketmq.store.index.IndexService; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; + + +public class StoreTestUtil { + + private static final Logger log = LoggerFactory.getLogger(StoreTestUtil.class); + + public static boolean isCommitLogAvailable(DefaultMessageStore store) { + try { + Field serviceField = null; + if (store instanceof RocksDBMessageStore) { + serviceField = store.getClass().getSuperclass().getDeclaredField("reputMessageService"); + } else { + serviceField = store.getClass().getDeclaredField("reputMessageService"); + } + + serviceField.setAccessible(true); + DefaultMessageStore.ReputMessageService reputService = + (DefaultMessageStore.ReputMessageService) serviceField.get(store); + + Method method = DefaultMessageStore.ReputMessageService.class.getDeclaredMethod("isCommitLogAvailable"); + method.setAccessible(true); + return (boolean) method.invoke(reputService); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + + public static void flushConsumeQueue(DefaultMessageStore store) throws Exception { + Field field = store.getClass().getDeclaredField("flushConsumeQueueService"); + field.setAccessible(true); + DefaultMessageStore.FlushConsumeQueueService flushService = (DefaultMessageStore.FlushConsumeQueueService) field.get(store); + + final int retryTimesOver = 3; + Method method = DefaultMessageStore.FlushConsumeQueueService.class.getDeclaredMethod("doFlush", int.class); + method.setAccessible(true); + method.invoke(flushService, retryTimesOver); + } + + + public static void waitCommitLogReput(DefaultMessageStore store) { + for (int i = 0; i < 500 && isCommitLogAvailable(store); i++) { + try { + Thread.sleep(100); + } catch (InterruptedException ignored) { + } + } + + if (isCommitLogAvailable(store)) { + log.warn("isCommitLogAvailable expected false ,but true"); + } + } + + + public static void flushConsumeIndex(DefaultMessageStore store) throws NoSuchFieldException, Exception { + Field field = store.getClass().getDeclaredField("indexService"); + field.setAccessible(true); + IndexService indexService = (IndexService) field.get(store); + + Field field2 = indexService.getClass().getDeclaredField("indexFileList"); + field2.setAccessible(true); + ArrayList indexFileList = (ArrayList) field2.get(indexService); + + for (IndexFile f : indexFileList) { + indexService.flush(f); + } + } + + public static void releaseMmapFilesOnWindows(List mappedFiles) throws IOException { + if (!SystemUtils.IS_OS_WINDOWS) { + return; + } + for (final MmapFile mappedFile : mappedFiles) { + DefaultMmapFile.clean(mappedFile.getMappedByteBuffer()); + mappedFile.getFileChannel().close(); + } + } +} diff --git a/store/src/test/java/org/apache/rocketmq/store/dledger/DLedgerCommitlogTest.java b/store/src/test/java/org/apache/rocketmq/store/dledger/DLedgerCommitlogTest.java new file mode 100644 index 00000000000..1e4bbf21bd2 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/dledger/DLedgerCommitlogTest.java @@ -0,0 +1,435 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.dledger; + +import io.openmessaging.storage.dledger.DLedgerServer; +import io.openmessaging.storage.dledger.store.file.DLedgerMmapFileStore; +import io.openmessaging.storage.dledger.store.file.MmapFileList; + +import java.io.File; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBatch; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.GetMessageStatus; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.StoreCheckpoint; +import org.apache.rocketmq.store.config.StorePathConfigHelper; +import org.junit.Assert; +import org.junit.Test; +import org.junit.Assume; +import org.apache.rocketmq.common.MixAll; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.apache.rocketmq.store.StoreTestUtil.releaseMmapFilesOnWindows; +import static org.awaitility.Awaitility.await; + +public class DLedgerCommitlogTest extends MessageStoreTestBase { + + @Test + public void testTruncateCQ() throws Exception { + String base = createBaseDir(); + String peers = String.format("n0-localhost:%d", nextPort()); + String group = UUID.randomUUID().toString(); + String topic = UUID.randomUUID().toString(); + { + DefaultMessageStore messageStore = createDledgerMessageStore(base, group, "n0", peers, null, false, 0); + DLedgerCommitLog dLedgerCommitLog = (DLedgerCommitLog) messageStore.getCommitLog(); + DLedgerServer dLedgerServer = dLedgerCommitLog.getdLedgerServer(); + DLedgerMmapFileStore dLedgerMmapFileStore = (DLedgerMmapFileStore) dLedgerServer.getdLedgerStore(); + MmapFileList mmapFileList = dLedgerMmapFileStore.getDataFileList(); + Boolean success = await().atMost(Duration.ofSeconds(4)).until(() -> dLedgerCommitLog.getdLedgerServer().getMemberState().isLeader(), item -> item); + Assert.assertTrue(success); + doPutMessages(messageStore, topic, 0, 2000, 0); + await().atMost(Duration.ofSeconds(10)).until(() -> 2000 == messageStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(24, mmapFileList.getMappedFiles().size()); + Assert.assertEquals(0, messageStore.getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(2000, messageStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, messageStore.dispatchBehindBytes()); + doGetMessages(messageStore, topic, 0, 2000, 0); + messageStore.shutdown(); + releaseMmapFilesOnWindows(dLedgerMmapFileStore.getDataFileList().getMappedFiles()); + } + + { + //Abnormal recover, left some commitlogs + DefaultMessageStore messageStore = createDledgerMessageStore(base, group, "n0", peers, null, true, 4); + DLedgerCommitLog dLedgerCommitLog = (DLedgerCommitLog) messageStore.getCommitLog(); + DLedgerServer dLedgerServer = dLedgerCommitLog.getdLedgerServer(); + DLedgerMmapFileStore dLedgerMmapFileStore = (DLedgerMmapFileStore) dLedgerServer.getdLedgerStore(); + MmapFileList mmapFileList = dLedgerMmapFileStore.getDataFileList(); + Boolean success = await().atMost(Duration.ofSeconds(4)).until(() -> dLedgerCommitLog.getdLedgerServer().getMemberState().isLeader(), item -> item); + Assert.assertTrue(success); + Assert.assertEquals(20, mmapFileList.getMappedFiles().size()); + Assert.assertEquals(0, messageStore.getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(1700, messageStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, messageStore.dispatchBehindBytes()); + doGetMessages(messageStore, topic, 0, 1700, 0); + messageStore.shutdown(); + releaseMmapFilesOnWindows(dLedgerMmapFileStore.getDataFileList().getMappedFiles()); + } + { + //Abnormal recover, left none commitlogs + DefaultMessageStore messageStore = createDledgerMessageStore(base, group, "n0", peers, null, true, 20); + DLedgerCommitLog dLedgerCommitLog = (DLedgerCommitLog) messageStore.getCommitLog(); + DLedgerServer dLedgerServer = dLedgerCommitLog.getdLedgerServer(); + DLedgerMmapFileStore dLedgerMmapFileStore = (DLedgerMmapFileStore) dLedgerServer.getdLedgerStore(); + MmapFileList mmapFileList = dLedgerMmapFileStore.getDataFileList(); + Boolean success = await().atMost(Duration.ofSeconds(4)).until(() -> dLedgerCommitLog.getdLedgerServer().getMemberState().isLeader(), item -> item); + Assert.assertTrue(success); + Assert.assertEquals(0, mmapFileList.getMappedFiles().size()); + Assert.assertEquals(0, messageStore.getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(0, messageStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, messageStore.dispatchBehindBytes()); + messageStore.shutdown(); + } + } + + @Test + public void testRecover() throws Exception { + String base = createBaseDir(); + String peers = String.format("n0-localhost:%d", nextPort()); + String group = UUID.randomUUID().toString(); + String topic = UUID.randomUUID().toString(); + { + DefaultMessageStore messageStore = createDledgerMessageStore(base, group, "n0", peers, null, false, 0); + DLedgerCommitLog dLedgerCommitLog = (DLedgerCommitLog) messageStore.getCommitLog(); + Boolean success = await().atMost(Duration.ofSeconds(4)).until(() -> dLedgerCommitLog.getdLedgerServer().getMemberState().isLeader(), item -> item); + Assert.assertTrue(success); + doPutMessages(messageStore, topic, 0, 1000, 0); + await().atMost(Duration.ofSeconds(10)).until(() -> 1000 == messageStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, messageStore.getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(1000, messageStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, messageStore.dispatchBehindBytes()); + doGetMessages(messageStore, topic, 0, 1000, 0); + messageStore.shutdown(); + } + + { + //normal recover + DefaultMessageStore messageStore = createDledgerMessageStore(base, group, "n0", peers, null, false, 0); + Assert.assertEquals(0, messageStore.getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(1000, messageStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, messageStore.dispatchBehindBytes()); + doGetMessages(messageStore, topic, 0, 1000, 0); + messageStore.shutdown(); + } + + { + //abnormal recover + DefaultMessageStore messageStore = createDledgerMessageStore(base, group, "n0", peers, null, true, 0); + Assert.assertEquals(0, messageStore.getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(1000, messageStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, messageStore.dispatchBehindBytes()); + doGetMessages(messageStore, topic, 0, 1000, 0); + messageStore.shutdown(); + } + } + @Test + public void testDLedgerAbnormallyRecover() throws Exception { + String base = createBaseDir(); + String peers = String.format("n0-localhost:%d", nextPort()); + String group = UUID.randomUUID().toString(); + String topic = UUID.randomUUID().toString(); + + int messageNumPerQueue = 100; + + DefaultMessageStore messageStore = createDledgerMessageStore(base, group, "n0", peers, null, false, 0); + Thread.sleep(1000); + doPutMessages(messageStore, topic, 0, messageNumPerQueue, 0); + doPutMessages(messageStore, topic, 1, messageNumPerQueue, 0); + Thread.sleep(1000); + Assert.assertEquals(0, messageStore.getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(messageNumPerQueue, messageStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, messageStore.dispatchBehindBytes()); + doGetMessages(messageStore, topic, 0, messageNumPerQueue, 0); + StoreCheckpoint storeCheckpoint = messageStore.getStoreCheckpoint(); + storeCheckpoint.setPhysicMsgTimestamp(0); + storeCheckpoint.setLogicsMsgTimestamp(0); + messageStore.shutdown(); + + String fileName = StorePathConfigHelper.getAbortFile(base); + makeSureFileExists(fileName); + + File file = new File(base + File.separator + "consumequeue" + File.separator + topic + File.separator + "0" + File.separator + "00000000000000001040"); + file.delete(); +// truncateAllConsumeQueue(base + File.separator + "consumequeue" + File.separator + topic + File.separator); + messageStore = createDledgerMessageStore(base, group, "n0", peers, null, false, 0); + Thread.sleep(1000); + doGetMessages(messageStore, topic, 0, messageNumPerQueue, 0); + doGetMessages(messageStore, topic, 1, messageNumPerQueue, 0); + messageStore.shutdown(); + + } + + @Test + public void testPutAndGetMessage() throws Exception { + String base = createBaseDir(); + String peers = String.format("n0-localhost:%d", nextPort()); + String group = UUID.randomUUID().toString(); + DefaultMessageStore messageStore = createDledgerMessageStore(base, group, "n0", peers, null, false, 0); + DLedgerCommitLog dLedgerCommitLog = (DLedgerCommitLog) messageStore.getCommitLog(); + Boolean success = await().atMost(Duration.ofSeconds(4)).until(() -> dLedgerCommitLog.getdLedgerServer().getMemberState().isLeader(), item -> item); + Assert.assertTrue(success); + String topic = UUID.randomUUID().toString(); + + List results = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + MessageExtBrokerInner msgInner = + i < 5 ? buildMessage() : buildIPv6HostMessage(); + msgInner.setTopic(topic); + msgInner.setQueueId(0); + PutMessageResult putMessageResult = messageStore.putMessage(msgInner); + results.add(putMessageResult); + Assert.assertEquals(PutMessageStatus.PUT_OK, putMessageResult.getPutMessageStatus()); + Assert.assertEquals(i, putMessageResult.getAppendMessageResult().getLogicsOffset()); + } + await().atMost(Duration.ofSeconds(10)).until(() -> 10 == messageStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, messageStore.getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(10, messageStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, messageStore.dispatchBehindBytes()); + GetMessageResult getMessageResult = messageStore.getMessage("group", topic, 0, 0, 32, null); + Assert.assertEquals(GetMessageStatus.FOUND, getMessageResult.getStatus()); + + Assert.assertEquals(10, getMessageResult.getMessageBufferList().size()); + Assert.assertEquals(10, getMessageResult.getMessageMapedList().size()); + + for (int i = 0; i < results.size(); i++) { + ByteBuffer buffer = getMessageResult.getMessageBufferList().get(i); + MessageExt messageExt = MessageDecoder.decode(buffer); + Assert.assertEquals(i, messageExt.getQueueOffset()); + Assert.assertEquals(results.get(i).getAppendMessageResult().getMsgId(), messageExt.getMsgId()); + Assert.assertEquals(results.get(i).getAppendMessageResult().getWroteOffset(), messageExt.getCommitLogOffset()); + } + messageStore.destroy(); + messageStore.shutdown(); + } + + @Test + public void testBatchPutAndGetMessage() throws Exception { + String base = createBaseDir(); + String peers = String.format("n0-localhost:%d", nextPort()); + String group = UUID.randomUUID().toString(); + DefaultMessageStore messageStore = createDledgerMessageStore(base, group, "n0", peers, null, false, 0); + DLedgerCommitLog dLedgerCommitLog = (DLedgerCommitLog) messageStore.getCommitLog(); + Boolean success = await().atMost(Duration.ofSeconds(4)).until(() -> dLedgerCommitLog.getdLedgerServer().getMemberState().isLeader(), item -> item); + Assert.assertTrue(success); + String topic = UUID.randomUUID().toString(); + // should be less than 4 + int batchMessageSize = 2; + int repeat = 10; + List results = new ArrayList<>(); + for (int i = 0; i < repeat; i++) { + MessageExtBatch messageExtBatch = + i < repeat / 10 ? buildBatchMessage(batchMessageSize) : buildIPv6HostBatchMessage(batchMessageSize); + messageExtBatch.setTopic(topic); + messageExtBatch.setQueueId(0); + PutMessageResult putMessageResult = messageStore.putMessages(messageExtBatch); + results.add(putMessageResult); + Assert.assertEquals(PutMessageStatus.PUT_OK, putMessageResult.getPutMessageStatus()); + Assert.assertEquals(i * batchMessageSize, putMessageResult.getAppendMessageResult().getLogicsOffset()); + } + await().atMost(Duration.ofSeconds(10)).until(() -> repeat * batchMessageSize == messageStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, messageStore.getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(repeat * batchMessageSize, messageStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, messageStore.dispatchBehindBytes()); + GetMessageResult getMessageResult = messageStore.getMessage("group", topic, 0, 0, 100, null); + Assert.assertEquals(GetMessageStatus.FOUND, getMessageResult.getStatus()); + + Assert.assertEquals(repeat * batchMessageSize > 32 ? 32 : repeat * batchMessageSize, getMessageResult.getMessageBufferList().size()); + Assert.assertEquals(repeat * batchMessageSize > 32 ? 32 : repeat * batchMessageSize, getMessageResult.getMessageMapedList().size()); + Assert.assertEquals(repeat * batchMessageSize, getMessageResult.getMaxOffset()); + + for (int i = 0; i < results.size(); i++) { + ByteBuffer buffer = getMessageResult.getMessageBufferList().get(i * batchMessageSize); + MessageExt messageExt = MessageDecoder.decode(buffer); + Assert.assertEquals(i * batchMessageSize, messageExt.getQueueOffset()); + Assert.assertEquals(results.get(i).getAppendMessageResult().getMsgId().split(",").length, batchMessageSize); + Assert.assertEquals(results.get(i).getAppendMessageResult().getWroteOffset(), messageExt.getCommitLogOffset()); + } + messageStore.destroy(); + messageStore.shutdown(); + } + + @Test + public void testAsyncPutAndGetMessage() throws Exception { + Assume.assumeFalse(MixAll.isWindows()); + String base = createBaseDir(); + String peers = String.format("n0-localhost:%d", nextPort()); + String group = UUID.randomUUID().toString(); + DefaultMessageStore messageStore = createDledgerMessageStore(base, group, "n0", peers, null, false, 0); + DLedgerCommitLog dLedgerCommitLog = (DLedgerCommitLog) messageStore.getCommitLog(); + Boolean success = await().atMost(Duration.ofSeconds(4)).until(() -> dLedgerCommitLog.getdLedgerServer().getMemberState().isLeader(), item -> item); + Assert.assertTrue(success); + String topic = UUID.randomUUID().toString(); + + List results = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + MessageExtBrokerInner msgInner = + i < 5 ? buildMessage() : buildIPv6HostMessage(); + msgInner.setTopic(topic); + msgInner.setQueueId(0); + CompletableFuture futureResult = messageStore.asyncPutMessage(msgInner); + PutMessageResult putMessageResult = futureResult.get(3000, TimeUnit.MILLISECONDS); + results.add(putMessageResult); + Assert.assertEquals(PutMessageStatus.PUT_OK, putMessageResult.getPutMessageStatus()); + Assert.assertEquals(i, putMessageResult.getAppendMessageResult().getLogicsOffset()); + } + await().atMost(Duration.ofSeconds(10)).until(() -> 10 == messageStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, messageStore.getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(10, messageStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, messageStore.dispatchBehindBytes()); + GetMessageResult getMessageResult = messageStore.getMessage("group", topic, 0, 0, 32, null); + Assert.assertEquals(GetMessageStatus.FOUND, getMessageResult.getStatus()); + + Assert.assertEquals(10, getMessageResult.getMessageBufferList().size()); + Assert.assertEquals(10, getMessageResult.getMessageMapedList().size()); + + for (int i = 0; i < results.size(); i++) { + ByteBuffer buffer = getMessageResult.getMessageBufferList().get(i); + MessageExt messageExt = MessageDecoder.decode(buffer); + Assert.assertEquals(i, messageExt.getQueueOffset()); + Assert.assertEquals(results.get(i).getAppendMessageResult().getMsgId(), messageExt.getMsgId()); + Assert.assertEquals(results.get(i).getAppendMessageResult().getWroteOffset(), messageExt.getCommitLogOffset()); + } + messageStore.destroy(); + messageStore.shutdown(); + } + + @Test + public void testAsyncBatchPutAndGetMessage() throws Exception { + String base = createBaseDir(); + String peers = String.format("n0-localhost:%d", nextPort()); + String group = UUID.randomUUID().toString(); + DefaultMessageStore messageStore = createDledgerMessageStore(base, group, "n0", peers, null, false, 0); + DLedgerCommitLog dLedgerCommitLog = (DLedgerCommitLog) messageStore.getCommitLog(); + Boolean success = await().atMost(Duration.ofSeconds(4)).until(() -> dLedgerCommitLog.getdLedgerServer().getMemberState().isLeader(), item -> item); + Assert.assertTrue(success); + String topic = UUID.randomUUID().toString(); + // should be less than 4 + int batchMessageSize = 2; + int repeat = 10; + + List results = new ArrayList<>(); + for (int i = 0; i < repeat; i++) { + MessageExtBatch messageExtBatch = + i < 5 ? buildBatchMessage(batchMessageSize) : buildIPv6HostBatchMessage(batchMessageSize); + messageExtBatch.setTopic(topic); + messageExtBatch.setQueueId(0); + CompletableFuture futureResult = messageStore.asyncPutMessages(messageExtBatch); + PutMessageResult putMessageResult = futureResult.get(3000, TimeUnit.MILLISECONDS); + results.add(putMessageResult); + Assert.assertEquals(PutMessageStatus.PUT_OK, putMessageResult.getPutMessageStatus()); + Assert.assertEquals(i * batchMessageSize, putMessageResult.getAppendMessageResult().getLogicsOffset()); + } + await().atMost(Duration.ofSeconds(10)).until(() -> repeat * batchMessageSize == messageStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, messageStore.getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(repeat * batchMessageSize, messageStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, messageStore.dispatchBehindBytes()); + GetMessageResult getMessageResult = messageStore.getMessage("group", topic, 0, 0, 32, null); + Assert.assertEquals(GetMessageStatus.FOUND, getMessageResult.getStatus()); + + Assert.assertEquals(repeat * batchMessageSize > 32 ? 32 : repeat * batchMessageSize, getMessageResult.getMessageBufferList().size()); + Assert.assertEquals(repeat * batchMessageSize > 32 ? 32 : repeat * batchMessageSize, getMessageResult.getMessageMapedList().size()); + Assert.assertEquals(repeat * batchMessageSize, getMessageResult.getMaxOffset()); + + for (int i = 0; i < results.size(); i++) { + ByteBuffer buffer = getMessageResult.getMessageBufferList().get(i * batchMessageSize); + MessageExt messageExt = MessageDecoder.decode(buffer); + Assert.assertEquals(i * batchMessageSize, messageExt.getQueueOffset()); + Assert.assertEquals(results.get(i).getAppendMessageResult().getMsgId().split(",").length, batchMessageSize); + Assert.assertEquals(results.get(i).getAppendMessageResult().getWroteOffset(), messageExt.getCommitLogOffset()); + } + messageStore.destroy(); + messageStore.shutdown(); + } + + @Test + public void testCommittedPos() throws Exception { + String peers = String.format("n0-localhost:%d;n1-localhost:%d", nextPort(), nextPort()); + String group = UUID.randomUUID().toString(); + DefaultMessageStore leaderStore = createDledgerMessageStore(createBaseDir(), group, "n0", peers, "n0", false, 0); + + String topic = UUID.randomUUID().toString(); + MessageExtBrokerInner msgInner = buildMessage(); + msgInner.setTopic(topic); + msgInner.setQueueId(0); + PutMessageResult putMessageResult = leaderStore.putMessage(msgInner); + Assert.assertEquals(PutMessageStatus.IN_SYNC_REPLICAS_NOT_ENOUGH, putMessageResult.getPutMessageStatus()); + + Assert.assertEquals(0, leaderStore.getCommitLog().getMaxOffset()); + Assert.assertEquals(0, leaderStore.getMaxOffsetInQueue(topic, 0)); + + DefaultMessageStore followerStore = createDledgerMessageStore(createBaseDir(), group, "n1", peers, "n0", false, 0); + await().atMost(10, SECONDS).until(followerCatchesUp(followerStore, topic)); + + Assert.assertEquals(1, leaderStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertTrue(leaderStore.getCommitLog().getMaxOffset() > 0); + + leaderStore.destroy(); + followerStore.destroy(); + + leaderStore.shutdown(); + followerStore.shutdown(); + } + + @Test + public void testIPv6HostMsgCommittedPos() throws Exception { + String peers = String.format("n0-localhost:%d;n1-localhost:%d", nextPort(), nextPort()); + String group = UUID.randomUUID().toString(); + DefaultMessageStore leaderStore = createDledgerMessageStore(createBaseDir(), group, "n0", peers, "n0", false, 0); + + String topic = UUID.randomUUID().toString(); + MessageExtBrokerInner msgInner = buildIPv6HostMessage(); + msgInner.setTopic(topic); + msgInner.setQueueId(0); + PutMessageResult putMessageResult = leaderStore.putMessage(msgInner); + Assert.assertEquals(PutMessageStatus.IN_SYNC_REPLICAS_NOT_ENOUGH, putMessageResult.getPutMessageStatus()); + + //Thread.sleep(1000); + + Assert.assertEquals(0, leaderStore.getCommitLog().getMaxOffset()); + Assert.assertEquals(0, leaderStore.getMaxOffsetInQueue(topic, 0)); + + DefaultMessageStore followerStore = createDledgerMessageStore(createBaseDir(), group, "n1", peers, "n0", false, 0); + await().atMost(10, SECONDS).until(followerCatchesUp(followerStore, topic)); + + Assert.assertEquals(1, leaderStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertTrue(leaderStore.getCommitLog().getMaxOffset() > 0); + + leaderStore.destroy(); + followerStore.destroy(); + + leaderStore.shutdown(); + followerStore.shutdown(); + } + + private Callable followerCatchesUp(DefaultMessageStore followerStore, String topic) { + return () -> followerStore.getMaxOffsetInQueue(topic, 0) == 1; + } +} diff --git a/store/src/test/java/org/apache/rocketmq/store/dledger/DLedgerMultiPathTest.java b/store/src/test/java/org/apache/rocketmq/store/dledger/DLedgerMultiPathTest.java new file mode 100644 index 00000000000..5eb83207322 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/dledger/DLedgerMultiPathTest.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.dledger; + +import java.io.File; +import java.time.Duration; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.config.FlushDiskType; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.stats.BrokerStatsManager; +import org.junit.Assert; +import org.junit.Test; +import org.junit.Assume; + +import static org.awaitility.Awaitility.await; + +public class DLedgerMultiPathTest extends MessageStoreTestBase { + + + @Test + public void multiDirsStorageTest() throws Exception { + Assume.assumeFalse(MixAll.isWindows()); + String base = createBaseDir(); + String topic = UUID.randomUUID().toString(); + String peers = String.format("n0-localhost:%d", nextPort()); + String group = UUID.randomUUID().toString(); + String multiStorePath = + base + "/multi/a/" + MessageStoreConfig.MULTI_PATH_SPLITTER + + base + "/multi/b/" + MessageStoreConfig.MULTI_PATH_SPLITTER + + base + "/multi/c/" + MessageStoreConfig.MULTI_PATH_SPLITTER; + { + + DefaultMessageStore dLedgerStore = createDLedgerMessageStore(base, group, "n0", peers, multiStorePath, null); + DLedgerCommitLog dLedgerCommitLog = (DLedgerCommitLog) dLedgerStore.getCommitLog(); + Boolean success = await().atMost(Duration.ofSeconds(4)).until(() -> dLedgerCommitLog.getdLedgerServer().getMemberState().isLeader(), item -> item); + Assert.assertTrue(success); + doPutMessages(dLedgerStore, topic, 0, 1000, 0); + await().atMost(Duration.ofSeconds(10)).until(() -> 1000 == dLedgerStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(11, dLedgerStore.getMaxPhyOffset() / dLedgerStore.getMessageStoreConfig().getMappedFileSizeCommitLog()); + Assert.assertEquals(0, dLedgerStore.getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(1000, dLedgerStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, dLedgerStore.dispatchBehindBytes()); + doGetMessages(dLedgerStore, topic, 0, 1000, 0); + dLedgerStore.shutdown(); + } + { + String readOnlyPath = + base + "/multi/a/" + MessageStoreConfig.MULTI_PATH_SPLITTER + + base + "/multi/b/" + MessageStoreConfig.MULTI_PATH_SPLITTER; + multiStorePath = + base + "/multi/c/" + MessageStoreConfig.MULTI_PATH_SPLITTER + + base + "/multi/d/" + MessageStoreConfig.MULTI_PATH_SPLITTER; + + DefaultMessageStore dLedgerStore = createDLedgerMessageStore(base, group, "n0", peers, multiStorePath, readOnlyPath); + DLedgerCommitLog dLedgerCommitLog = (DLedgerCommitLog) dLedgerStore.getCommitLog(); + Boolean success = await().atMost(Duration.ofSeconds(4)).until(() -> dLedgerCommitLog.getdLedgerServer().getMemberState().isLeader(), item -> item); + Assert.assertTrue(success); + doGetMessages(dLedgerStore, topic, 0, 1000, 0); + long beforeSize = Objects.requireNonNull(new File(base + "/multi/a/").listFiles()).length; + doPutMessages(dLedgerStore, topic, 0, 1000, 1000); + await().atMost(Duration.ofSeconds(10)).until(() -> 2000 == dLedgerStore.getMaxOffsetInQueue(topic, 0)); + long afterSize = Objects.requireNonNull(new File(base + "/multi/a/").listFiles()).length; + Assert.assertEquals(beforeSize, afterSize); + Assert.assertEquals(0, dLedgerStore.getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(2000, dLedgerStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, dLedgerStore.dispatchBehindBytes()); + + dLedgerStore.shutdown(); + } + + } + + protected DefaultMessageStore createDLedgerMessageStore(String base, String group, String selfId, String peers, + String dLedgerCommitLogPath, String readOnlyPath) throws Exception { + MessageStoreConfig storeConfig = new MessageStoreConfig(); + storeConfig.setMappedFileSizeCommitLog(1024 * 100); + storeConfig.setMappedFileSizeConsumeQueue(1024); + storeConfig.setMaxHashSlotNum(100); + storeConfig.setMaxIndexNum(100 * 10); + storeConfig.setStorePathRootDir(base); + storeConfig.setStorePathDLedgerCommitLog(dLedgerCommitLogPath); + storeConfig.setReadOnlyCommitLogStorePaths(readOnlyPath); + storeConfig.setFlushDiskType(FlushDiskType.ASYNC_FLUSH); + + storeConfig.setEnableDLegerCommitLog(true); + storeConfig.setdLegerGroup(group); + storeConfig.setdLegerPeers(peers); + storeConfig.setdLegerSelfId(selfId); + DefaultMessageStore defaultMessageStore = new DefaultMessageStore(storeConfig, new BrokerStatsManager("DLedgerCommitLogTest", true), (topic, queueId, logicOffset, tagsCode, msgStoreTime, filterBitMap, properties) -> { + + }, new BrokerConfig(), new ConcurrentHashMap<>()); + Assert.assertTrue(defaultMessageStore.load()); + defaultMessageStore.start(); + return defaultMessageStore; + } +} diff --git a/store/src/test/java/org/apache/rocketmq/store/dledger/MessageStoreTestBase.java b/store/src/test/java/org/apache/rocketmq/store/dledger/MessageStoreTestBase.java new file mode 100644 index 00000000000..a21806ffcf6 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/dledger/MessageStoreTestBase.java @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.dledger; + +import io.openmessaging.storage.dledger.DLedgerConfig; +import io.openmessaging.storage.dledger.DLedgerServer; +import java.io.File; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.StoreTestBase; +import org.apache.rocketmq.store.config.FlushDiskType; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.config.StorePathConfigHelper; +import org.apache.rocketmq.store.stats.BrokerStatsManager; +import org.junit.Assert; + +public class MessageStoreTestBase extends StoreTestBase { + + protected DefaultMessageStore createDledgerMessageStore(String base, String group, String selfId, String peers, String leaderId, boolean createAbort, int deleteFileNum) throws Exception { + System.setProperty("dledger.disk.ratio.check", "0.95"); + System.setProperty("dledger.disk.ratio.clean", "0.95"); + baseDirs.add(base); + MessageStoreConfig storeConfig = new MessageStoreConfig(); + storeConfig.setMappedFileSizeCommitLog(1024 * 100); + storeConfig.setMappedFileSizeConsumeQueue(1024); + storeConfig.setMaxHashSlotNum(100); + storeConfig.setMaxIndexNum(100 * 10); + storeConfig.setStorePathRootDir(base); + storeConfig.setStorePathCommitLog(base + File.separator + "commitlog"); + storeConfig.setFlushDiskType(FlushDiskType.ASYNC_FLUSH); + + storeConfig.setEnableDLegerCommitLog(true); + storeConfig.setdLegerGroup(group); + storeConfig.setdLegerPeers(peers); + storeConfig.setdLegerSelfId(selfId); + + storeConfig.setRecheckReputOffsetFromCq(true); + DefaultMessageStore defaultMessageStore = new DefaultMessageStore(storeConfig, new BrokerStatsManager("DLedgerCommitlogTest", true), (topic, queueId, logicOffset, tagsCode, msgStoreTime, filterBitMap, properties) -> { + + }, new BrokerConfig(), new ConcurrentHashMap<>()); + DLedgerServer dLegerServer = ((DLedgerCommitLog) defaultMessageStore.getCommitLog()).getdLedgerServer(); + if (leaderId != null) { + dLegerServer.getdLedgerConfig().setEnableLeaderElector(false); + if (selfId.equals(leaderId)) { + dLegerServer.getMemberState().changeToLeader(0); + } else { + dLegerServer.getMemberState().changeToFollower(0, leaderId); + } + } + if (createAbort) { + String fileName = StorePathConfigHelper.getAbortFile(storeConfig.getStorePathRootDir()); + makeSureFileExists(fileName); + } + if (deleteFileNum > 0) { + DLedgerConfig config = dLegerServer.getdLedgerConfig(); + if (deleteFileNum > 0) { + File dir = new File(config.getDataStorePath()); + File[] files = dir.listFiles(); + if (files != null) { + Arrays.sort(files); + for (int i = files.length - 1; i >= 0; i--) { + File file = files[i]; + file.delete(); + if (files.length - i >= deleteFileNum) { + break; + } + } + } + } + } + Assert.assertTrue(defaultMessageStore.load()); + defaultMessageStore.start(); + return defaultMessageStore; + } + + + protected DefaultMessageStore createMessageStore(String base, boolean createAbort) throws Exception { + baseDirs.add(base); + MessageStoreConfig storeConfig = new MessageStoreConfig(); + storeConfig.setMappedFileSizeCommitLog(1024 * 100); + storeConfig.setMappedFileSizeConsumeQueue(1024); + storeConfig.setMaxHashSlotNum(100); + storeConfig.setMaxIndexNum(100 * 10); + storeConfig.setStorePathRootDir(base); + storeConfig.setStorePathCommitLog(base + File.separator + "commitlog"); + storeConfig.setFlushDiskType(FlushDiskType.ASYNC_FLUSH); + DefaultMessageStore defaultMessageStore = new DefaultMessageStore(storeConfig, new BrokerStatsManager("CommitlogTest", true), (topic, queueId, logicOffset, tagsCode, msgStoreTime, filterBitMap, properties) -> { + + }, new BrokerConfig(), new ConcurrentHashMap<>()); + + if (createAbort) { + String fileName = StorePathConfigHelper.getAbortFile(storeConfig.getStorePathRootDir()); + makeSureFileExists(fileName); + } + Assert.assertTrue(defaultMessageStore.load()); + defaultMessageStore.start(); + return defaultMessageStore; + } + + protected void doPutMessages(MessageStore messageStore, String topic, int queueId, int num, long beginLogicsOffset) throws UnknownHostException { + for (int i = 0; i < num; i++) { + MessageExtBrokerInner msgInner = buildMessage(); + msgInner.setTopic(topic); + msgInner.setQueueId(queueId); + PutMessageResult putMessageResult = messageStore.putMessage(msgInner); + Assert.assertEquals(PutMessageStatus.PUT_OK, putMessageResult.getPutMessageStatus()); + Assert.assertEquals(beginLogicsOffset + i, putMessageResult.getAppendMessageResult().getLogicsOffset()); + } + } + + protected void doGetMessages(MessageStore messageStore, String topic, int queueId, int num, long beginLogicsOffset) { + for (int i = 0; i < num; i++) { + GetMessageResult getMessageResult = messageStore.getMessage("group", topic, queueId, beginLogicsOffset + i, 3, null); + Assert.assertNotNull(getMessageResult); + Assert.assertTrue(!getMessageResult.getMessageBufferList().isEmpty()); + MessageExt messageExt = MessageDecoder.decode(getMessageResult.getMessageBufferList().get(0)); + Assert.assertEquals(beginLogicsOffset + i, messageExt.getQueueOffset()); + getMessageResult.release(); + } + } + +} diff --git a/store/src/test/java/org/apache/rocketmq/store/dledger/MixCommitlogTest.java b/store/src/test/java/org/apache/rocketmq/store/dledger/MixCommitlogTest.java new file mode 100644 index 00000000000..db7b594a73b --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/dledger/MixCommitlogTest.java @@ -0,0 +1,202 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.dledger; + +import java.time.Duration; +import java.util.UUID; + +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.StoreTestBase; +import org.apache.rocketmq.store.config.StorePathConfigHelper; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; + +import static org.awaitility.Awaitility.await; + +public class MixCommitlogTest extends MessageStoreTestBase { + + @Test + public void testFallBehindCQ() throws Exception { + Assume.assumeFalse(MixAll.isWindows()); + String base = createBaseDir(); + String topic = UUID.randomUUID().toString(); + String peers = String.format("n0-localhost:%d", nextPort()); + String group = UUID.randomUUID().toString(); + { + DefaultMessageStore originalStore = createMessageStore(base, false); + doPutMessages(originalStore, topic, 0, 1000, 0); + await().atMost(Duration.ofSeconds(10)).until(() -> 1000 == originalStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(11, originalStore.getMaxPhyOffset() / originalStore.getMessageStoreConfig().getMappedFileSizeCommitLog()); + Assert.assertEquals(0, originalStore.getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(1000, originalStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, originalStore.dispatchBehindBytes()); + doGetMessages(originalStore, topic, 0, 1000, 0); + originalStore.shutdown(); + } + //delete the cq files + { + StoreTestBase.deleteFile(StorePathConfigHelper.getStorePathConsumeQueue(base)); + } + { + DefaultMessageStore dledgerStore = createDledgerMessageStore(base, group, "n0", peers, null, true, 0); + DLedgerCommitLog dLedgerCommitLog = (DLedgerCommitLog) dledgerStore.getCommitLog(); + Boolean success = await().atMost(Duration.ofSeconds(4)).until(() -> dLedgerCommitLog.getdLedgerServer().getMemberState().isLeader(), item -> item); + Assert.assertTrue(success); + await().atMost(Duration.ofSeconds(10)).until(() -> 1000 == dledgerStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, dledgerStore.getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(1000, dledgerStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, dledgerStore.dispatchBehindBytes()); + doGetMessages(dledgerStore, topic, 0, 1000, 0); + doPutMessages(dledgerStore, topic, 0, 1000, 1000); + await().atMost(Duration.ofSeconds(10)).until(() -> 2000 == dledgerStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, dledgerStore.getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(2000, dledgerStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, dledgerStore.dispatchBehindBytes()); + doGetMessages(dledgerStore, topic, 0, 2000, 0); + dledgerStore.shutdown(); + } + } + + @Test + public void testPutAndGet() throws Exception { + String base = createBaseDir(); + String topic = UUID.randomUUID().toString(); + String peers = String.format("n0-localhost:%d", nextPort()); + String group = UUID.randomUUID().toString(); + + long dividedOffset; + { + DefaultMessageStore originalStore = createMessageStore(base, false); + doPutMessages(originalStore, topic, 0, 1000, 0); + await().atMost(Duration.ofSeconds(10)).until(() -> 1000 == originalStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, originalStore.getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(1000, originalStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, originalStore.dispatchBehindBytes()); + dividedOffset = originalStore.getCommitLog().getMaxOffset(); + dividedOffset = dividedOffset - dividedOffset % originalStore.getMessageStoreConfig().getMappedFileSizeCommitLog() + originalStore.getMessageStoreConfig().getMappedFileSizeCommitLog(); + doGetMessages(originalStore, topic, 0, 1000, 0); + originalStore.shutdown(); + } + { + DefaultMessageStore recoverOriginalStore = createMessageStore(base, true); + await().atMost(Duration.ofSeconds(10)).until(() -> 1000 == recoverOriginalStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, recoverOriginalStore.getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(1000, recoverOriginalStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, recoverOriginalStore.dispatchBehindBytes()); + doGetMessages(recoverOriginalStore, topic, 0, 1000, 0); + recoverOriginalStore.shutdown(); + } + { + DefaultMessageStore dledgerStore = createDledgerMessageStore(base, group, "n0", peers, null, true, 0); + DLedgerCommitLog dLedgerCommitLog = (DLedgerCommitLog) dledgerStore.getCommitLog(); + Assert.assertFalse(dLedgerCommitLog.getdLedgerServer().getdLedgerConfig().isEnableDiskForceClean()); + Assert.assertEquals(dividedOffset, dLedgerCommitLog.getDividedCommitlogOffset()); + Assert.assertEquals(0, dledgerStore.dispatchBehindBytes()); + Assert.assertEquals(dividedOffset, dLedgerCommitLog.getMaxOffset()); + Boolean success = await().atMost(Duration.ofSeconds(4)).until(() -> dLedgerCommitLog.getdLedgerServer().getMemberState().isLeader(), item -> item); + Assert.assertTrue(success); + doPutMessages(dledgerStore, topic, 0, 1000, 1000); + await().atMost(Duration.ofSeconds(10)).until(() -> 2000 == dledgerStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, dledgerStore.getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(2000, dledgerStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, dledgerStore.dispatchBehindBytes()); + doGetMessages(dledgerStore, topic, 0, 2000, 0); + dledgerStore.shutdown(); + } + { + DefaultMessageStore recoverDledgerStore = createDledgerMessageStore(base, group, "n0", peers, null, true, 0); + DLedgerCommitLog dLedgerCommitLog = (DLedgerCommitLog) recoverDledgerStore.getCommitLog(); + Assert.assertFalse(dLedgerCommitLog.getdLedgerServer().getdLedgerConfig().isEnableDiskForceClean()); + Assert.assertEquals(dividedOffset, dLedgerCommitLog.getDividedCommitlogOffset()); + Boolean success = await().atMost(Duration.ofSeconds(4)).until(() -> dLedgerCommitLog.getdLedgerServer().getMemberState().isLeader(), item -> item); + Assert.assertTrue(success); + doPutMessages(recoverDledgerStore, topic, 0, 1000, 2000); + await().atMost(Duration.ofSeconds(10)).until(() -> 3000 == recoverDledgerStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, recoverDledgerStore.getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(3000, recoverDledgerStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, recoverDledgerStore.dispatchBehindBytes()); + doGetMessages(recoverDledgerStore, topic, 0, 3000, 0); + recoverDledgerStore.shutdown(); + } + } + + @Test + public void testDeleteExpiredFiles() throws Exception { + String base = createBaseDir(); + String topic = UUID.randomUUID().toString(); + String peers = String.format("n0-localhost:%d", nextPort()); + String group = UUID.randomUUID().toString(); + + long dividedOffset; + { + DefaultMessageStore originalStore = createMessageStore(base, false); + doPutMessages(originalStore, topic, 0, 1000, 0); + await().atMost(Duration.ofSeconds(10)).until(() -> 1000 == originalStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, originalStore.getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(1000, originalStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, originalStore.dispatchBehindBytes()); + dividedOffset = originalStore.getCommitLog().getMaxOffset(); + dividedOffset = dividedOffset - dividedOffset % originalStore.getMessageStoreConfig().getMappedFileSizeCommitLog() + originalStore.getMessageStoreConfig().getMappedFileSizeCommitLog(); + originalStore.shutdown(); + } + long maxPhysicalOffset; + { + DefaultMessageStore dledgerStore = createDledgerMessageStore(base, group, "n0", peers, null, true, 0); + DLedgerCommitLog dLedgerCommitLog = (DLedgerCommitLog) dledgerStore.getCommitLog(); + Assert.assertTrue(dledgerStore.getMessageStoreConfig().isCleanFileForciblyEnable()); + Assert.assertFalse(dLedgerCommitLog.getdLedgerServer().getdLedgerConfig().isEnableDiskForceClean()); + Assert.assertEquals(dividedOffset, dLedgerCommitLog.getDividedCommitlogOffset()); + Boolean success = await().atMost(Duration.ofSeconds(4)).until(() -> dLedgerCommitLog.getdLedgerServer().getMemberState().isLeader(), item -> item); + Assert.assertTrue(success); + doPutMessages(dledgerStore, topic, 0, 1000, 1000); + await().atMost(Duration.ofSeconds(10)).until(() -> 2000 == dledgerStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, dledgerStore.getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(2000, dledgerStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, dledgerStore.dispatchBehindBytes()); + Assert.assertEquals(0, dledgerStore.getMinPhyOffset()); + maxPhysicalOffset = dledgerStore.getMaxPhyOffset(); + Assert.assertTrue(maxPhysicalOffset > 0); + + doGetMessages(dledgerStore, topic, 0, 2000, 0); + + for (int i = 0; i < 100; i++) { + dledgerStore.getCommitLog().deleteExpiredFile(System.currentTimeMillis(), 0, 0, true); + } + Assert.assertEquals(dividedOffset, dledgerStore.getMinPhyOffset()); + Assert.assertEquals(maxPhysicalOffset, dledgerStore.getMaxPhyOffset()); + for (int i = 0; i < 100; i++) { + Assert.assertEquals(Integer.MAX_VALUE, dledgerStore.getCommitLog().deleteExpiredFile(System.currentTimeMillis(), 0, 0, true)); + } + Assert.assertEquals(dividedOffset, dledgerStore.getMinPhyOffset()); + Assert.assertEquals(maxPhysicalOffset, dledgerStore.getMaxPhyOffset()); + + Assert.assertTrue(dledgerStore.getMessageStoreConfig().isCleanFileForciblyEnable()); + Assert.assertTrue(dLedgerCommitLog.getdLedgerServer().getdLedgerConfig().isEnableDiskForceClean()); + + //Test fresh + dledgerStore.getMessageStoreConfig().setCleanFileForciblyEnable(false); + for (int i = 0; i < 100; i++) { + Assert.assertEquals(Integer.MAX_VALUE, dledgerStore.getCommitLog().deleteExpiredFile(System.currentTimeMillis(), 0, 0, true)); + } + Assert.assertFalse(dLedgerCommitLog.getdLedgerServer().getdLedgerConfig().isEnableDiskForceClean()); + doGetMessages(dledgerStore, topic, 0, 1000, 1000); + dledgerStore.shutdown(); + } + } +} diff --git a/store/src/test/java/org/apache/rocketmq/store/ha/FlowMonitorTest.java b/store/src/test/java/org/apache/rocketmq/store/ha/FlowMonitorTest.java new file mode 100644 index 00000000000..32fe495a73f --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/ha/FlowMonitorTest.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.ha; + +import java.time.Duration; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.Assert; +import org.junit.Test; + +import static org.awaitility.Awaitility.await; + +public class FlowMonitorTest { + + @Test + public void testLimit() throws Exception { + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setHaFlowControlEnable(true); + messageStoreConfig.setMaxHaTransferByteInSecond(10); + + FlowMonitor flowMonitor = new FlowMonitor(messageStoreConfig); + flowMonitor.start(); + + flowMonitor.addByteCountTransferred(3); + Boolean flag = await().atMost(Duration.ofSeconds(2)).until(() -> 7 == flowMonitor.canTransferMaxByteNum(), item -> item); + flag &= await().atMost(Duration.ofSeconds(2)).until(() -> 10 == flowMonitor.canTransferMaxByteNum(), item -> item); + Assert.assertTrue(flag); + + flowMonitor.shutdown(); + } + + @Test + public void testSpeed() throws Exception { + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setHaFlowControlEnable(true); + messageStoreConfig.setMaxHaTransferByteInSecond(10); + + FlowMonitor flowMonitor = new FlowMonitor(messageStoreConfig); + + flowMonitor.addByteCountTransferred(3); + flowMonitor.calculateSpeed(); + Assert.assertEquals(3, flowMonitor.getTransferredByteInSecond()); + + flowMonitor.addByteCountTransferred(5); + flowMonitor.calculateSpeed(); + Assert.assertEquals(5, flowMonitor.getTransferredByteInSecond()); + } +} diff --git a/store/src/test/java/org/apache/rocketmq/store/ha/HAClientTest.java b/store/src/test/java/org/apache/rocketmq/store/ha/HAClientTest.java new file mode 100644 index 00000000000..33b3c541d8b --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/ha/HAClientTest.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.ha; + +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class HAClientTest { + private HAClient haClient; + + @Mock + private DefaultMessageStore messageStore; + + @Before + public void setUp() throws Exception { +// when(messageStore.getMessageStoreConfig()).thenReturn(new MessageStoreConfig()); + when(messageStore.getBrokerConfig()).thenReturn(new BrokerConfig()); + this.haClient = new DefaultHAClient(this.messageStore); + } + + @After + public void tearDown() throws Exception { + this.haClient.shutdown(); + } + + @Test + public void updateMasterAddress() { + assertThat(this.haClient.getMasterAddress()).isNull(); + this.haClient.updateMasterAddress("127.0.0.1:10911"); + assertThat(this.haClient.getMasterAddress()).isEqualTo("127.0.0.1:10911"); + + this.haClient.updateMasterAddress("127.0.0.1:10912"); + assertThat(this.haClient.getMasterAddress()).isEqualTo("127.0.0.1:10912"); + } + + @Test + public void updateHaMasterAddress() { + assertThat(this.haClient.getHaMasterAddress()).isNull(); + this.haClient.updateHaMasterAddress("127.0.0.1:10911"); + assertThat(this.haClient.getHaMasterAddress()).isEqualTo("127.0.0.1:10911"); + + this.haClient.updateHaMasterAddress("127.0.0.1:10912"); + assertThat(this.haClient.getHaMasterAddress()).isEqualTo("127.0.0.1:10912"); + } +} diff --git a/store/src/test/java/org/apache/rocketmq/store/ha/HAServerTest.java b/store/src/test/java/org/apache/rocketmq/store/ha/HAServerTest.java new file mode 100644 index 00000000000..fa8f41dbf84 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/ha/HAServerTest.java @@ -0,0 +1,296 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.ha; + +import java.io.IOException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.SystemClock; +import org.apache.rocketmq.store.CommitLog; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import org.rocksdb.RocksDBException; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +@RunWith(MockitoJUnitRunner.Silent.class) +public class HAServerTest { + private DefaultMessageStore defaultMessageStore; + private MessageStoreConfig storeConfig; + private HAService haService; + private Random random = new Random(); + private List haClientList = new ArrayList<>(); + + @Before + public void setUp() throws Exception { + this.storeConfig = new MessageStoreConfig(); + this.storeConfig.setHaListenPort(9000 + random.nextInt(1000)); + this.storeConfig.setHaSendHeartbeatInterval(10); + + this.defaultMessageStore = mockMessageStore(); + this.haService = new DefaultHAService(); + this.haService.init(defaultMessageStore); + this.haService.start(); + } + + @After + public void tearDown() { + tearDownAllHAClient(); + + await().atMost(Duration.ofMinutes(1)).until(new Callable() { + @Override + public Boolean call() throws Exception { + return HAServerTest.this.haService.getConnectionCount().get() == 0; + } + }); + + this.haService.shutdown(); + } + + @Test + public void testConnectionList_OneHAClient() throws IOException { + setUpOneHAClient(); + + await().atMost(Duration.ofMinutes(1)).until(new Callable() { + @Override + public Boolean call() { + return HAServerTest.this.haService.getConnectionCount().get() == 1; + } + }); + } + + @Test + public void testConnectionList_MultipleHAClient() throws IOException { + setUpOneHAClient(); + setUpOneHAClient(); + setUpOneHAClient(); + + await().atMost(Duration.ofMinutes(1)).until(new Callable() { + @Override + public Boolean call() { + return HAServerTest.this.haService.getConnectionCount().get() == 3; + } + }); + + tearDownOneHAClient(); + + await().atMost(Duration.ofMinutes(1)).until(new Callable() { + @Override + public Boolean call() { + return HAServerTest.this.haService.getConnectionCount().get() == 2; + } + }); + } + + @Test + public void inSyncReplicasNums() throws IOException, RocksDBException { + DefaultMessageStore messageStore = mockMessageStore(); + doReturn(123L).when(messageStore).getMaxPhyOffset(); + doReturn(123L).when(messageStore).getMasterFlushedOffset(); + setUpOneHAClient(messageStore); + + messageStore = mockMessageStore(); + doReturn(124L).when(messageStore).getMaxPhyOffset(); + doReturn(124L).when(messageStore).getMasterFlushedOffset(); + setUpOneHAClient(messageStore); + + messageStore = mockMessageStore(); + doReturn(123L).when(messageStore).getMaxPhyOffset(); + doReturn(123L).when(messageStore).getMasterFlushedOffset(); + setUpOneHAClient(messageStore); + + messageStore = mockMessageStore(); + doReturn(125L).when(messageStore).getMaxPhyOffset(); + doReturn(125L).when(messageStore).getMasterFlushedOffset(); + setUpOneHAClient(messageStore); + + final int haSlaveFallbehindMax = this.defaultMessageStore.getMessageStoreConfig().getHaMaxGapNotInSync(); + + await().atMost(Duration.ofMinutes(1)).until(new Callable() { + @Override + public Boolean call() throws Exception { + return HAServerTest.this.haService.inSyncReplicasNums(haSlaveFallbehindMax) == 5; + } + }); + + assertThat(HAServerTest.this.haService.inSyncReplicasNums(123L + haSlaveFallbehindMax)).isEqualTo(3); + assertThat(HAServerTest.this.haService.inSyncReplicasNums(124L + haSlaveFallbehindMax)).isEqualTo(2); + assertThat(HAServerTest.this.haService.inSyncReplicasNums(125L + haSlaveFallbehindMax)).isEqualTo(1); + } + + @Test + public void isSlaveOK() throws IOException, RocksDBException { + DefaultMessageStore messageStore = mockMessageStore(); + doReturn(123L).when(messageStore).getMaxPhyOffset(); + doReturn(123L).when(messageStore).getMasterFlushedOffset(); + setUpOneHAClient(messageStore); + + messageStore = mockMessageStore(); + doReturn(124L).when(messageStore).getMaxPhyOffset(); + doReturn(124L).when(messageStore).getMasterFlushedOffset(); + setUpOneHAClient(messageStore); + + final int haSlaveFallbehindMax = this.defaultMessageStore.getMessageStoreConfig().getHaMaxGapNotInSync(); + + await().atMost(Duration.ofMinutes(1)).until(new Callable() { + @Override + public Boolean call() throws Exception { + return HAServerTest.this.haService.isSlaveOK(haSlaveFallbehindMax + 123); + } + }); + + assertThat(HAServerTest.this.haService.isSlaveOK(122L + haSlaveFallbehindMax)).isTrue(); + assertThat(HAServerTest.this.haService.isSlaveOK(124L + haSlaveFallbehindMax)).isFalse(); + } + + @Test + public void putRequest_SingleAck() + throws IOException, ExecutionException, InterruptedException, TimeoutException, RocksDBException { + CommitLog.GroupCommitRequest request = new CommitLog.GroupCommitRequest(124, 4000, 1); + this.haService.putRequest(request); + + assertThat(request.future().get()).isEqualTo(PutMessageStatus.FLUSH_SLAVE_TIMEOUT); + + DefaultMessageStore messageStore = mockMessageStore(); + doReturn(124L).when(messageStore).getMaxPhyOffset(); + doReturn(124L).when(messageStore).getMasterFlushedOffset(); + setUpOneHAClient(messageStore); + + request = new CommitLog.GroupCommitRequest(124, 4000, 1); + this.haService.putRequest(request); + assertThat(request.future().get()).isEqualTo(PutMessageStatus.PUT_OK); + } + + @Test + public void putRequest_MultipleAckAndRequests() + throws IOException, ExecutionException, InterruptedException, RocksDBException { + CommitLog.GroupCommitRequest oneAck = new CommitLog.GroupCommitRequest(124, 4000, 2); + this.haService.putRequest(oneAck); + + CommitLog.GroupCommitRequest twoAck = new CommitLog.GroupCommitRequest(124, 4000, 3); + this.haService.putRequest(twoAck); + + DefaultMessageStore messageStore = mockMessageStore(); + doReturn(125L).when(messageStore).getMaxPhyOffset(); + doReturn(125L).when(messageStore).getMasterFlushedOffset(); + setUpOneHAClient(messageStore); + + assertThat(oneAck.future().get()).isEqualTo(PutMessageStatus.PUT_OK); + assertThat(twoAck.future().get()).isEqualTo(PutMessageStatus.FLUSH_SLAVE_TIMEOUT); + + messageStore = mockMessageStore(); + doReturn(128L).when(messageStore).getMaxPhyOffset(); + doReturn(128L).when(messageStore).getMasterFlushedOffset(); + setUpOneHAClient(messageStore); + + twoAck = new CommitLog.GroupCommitRequest(124, 4000, 3); + this.haService.putRequest(twoAck); + assertThat(twoAck.future().get()).isEqualTo(PutMessageStatus.PUT_OK); + } + + @Test + public void getPush2SlaveMaxOffset() throws IOException, RocksDBException { + DefaultMessageStore messageStore = mockMessageStore(); + doReturn(123L).when(messageStore).getMaxPhyOffset(); + doReturn(123L).when(messageStore).getMasterFlushedOffset(); + setUpOneHAClient(messageStore); + + messageStore = mockMessageStore(); + doReturn(124L).when(messageStore).getMaxPhyOffset(); + doReturn(124L).when(messageStore).getMasterFlushedOffset(); + setUpOneHAClient(messageStore); + + messageStore = mockMessageStore(); + doReturn(125L).when(messageStore).getMaxPhyOffset(); + doReturn(125L).when(messageStore).getMasterFlushedOffset(); + setUpOneHAClient(messageStore); + + await().atMost(Duration.ofMinutes(1)).until(new Callable() { + @Override + public Boolean call() throws Exception { + return HAServerTest.this.haService.getPush2SlaveMaxOffset().get() == 125L; + } + }); + } + + private void setUpOneHAClient(DefaultMessageStore defaultMessageStore) throws IOException { + HAClient haClient = new DefaultHAClient(defaultMessageStore); + haClient.updateHaMasterAddress("127.0.0.1:" + this.storeConfig.getHaListenPort()); + haClient.start(); + this.haClientList.add(haClient); + } + + private void setUpOneHAClient() throws IOException { + HAClient haClient = new DefaultHAClient(this.defaultMessageStore); + haClient.updateHaMasterAddress("127.0.0.1:" + this.storeConfig.getHaListenPort()); + haClient.start(); + this.haClientList.add(haClient); + } + + private DefaultMessageStore mockMessageStore() throws IOException, RocksDBException { + DefaultMessageStore messageStore = mock(DefaultMessageStore.class); + BrokerConfig brokerConfig = mock(BrokerConfig.class); + + doReturn(true).when(brokerConfig).isInBrokerContainer(); + doReturn("mock").when(brokerConfig).getIdentifier(); + doReturn(brokerConfig).when(messageStore).getBrokerConfig(); + doReturn(new SystemClock()).when(messageStore).getSystemClock(); + doAnswer(invocation -> System.currentTimeMillis()).when(messageStore).now(); + doReturn(this.storeConfig).when(messageStore).getMessageStoreConfig(); + doReturn(new BrokerConfig()).when(messageStore).getBrokerConfig(); + doReturn(true).when(messageStore).isOffsetAligned(anyLong()); +// doReturn(new PutMessageResult(PutMessageStatus.PUT_OK, new AppendMessageResult(AppendMessageStatus.PUT_OK))).when(messageStore).sendMsgBack(anyLong()); + doReturn(true).when(messageStore).truncateFiles(anyLong()); + + DefaultMessageStore masterStore = mock(DefaultMessageStore.class); + doReturn(Long.MAX_VALUE).when(masterStore).getFlushedWhere(); + doReturn(masterStore).when(messageStore).getMasterStoreInProcess(); + + CommitLog commitLog = new CommitLog(messageStore); + doReturn(commitLog).when(messageStore).getCommitLog(); + return messageStore; + } + + private void tearDownOneHAClient() { + final HAClient haClient = this.haClientList.remove(0); + haClient.shutdown(); + } + + private void tearDownAllHAClient() { + for (final HAClient client : this.haClientList) { + client.shutdown(); + } + this.haClientList.clear(); + } +} diff --git a/store/src/test/java/org/apache/rocketmq/store/ha/WaitNotifyObjectTest.java b/store/src/test/java/org/apache/rocketmq/store/ha/WaitNotifyObjectTest.java new file mode 100644 index 00000000000..35584fd4ed7 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/ha/WaitNotifyObjectTest.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.ha; + +import org.junit.Assert; +import org.junit.Test; + +public class WaitNotifyObjectTest { + @Test + public void removeFromWaitingThreadTable() throws Exception { + final WaitNotifyObject waitNotifyObject = new WaitNotifyObject(); + for (int i = 0; i < 5; i++) { + Thread t = new Thread(new Runnable() { + @Override + public void run() { + waitNotifyObject.allWaitForRunning(100); + waitNotifyObject.removeFromWaitingThreadTable(); + } + }); + t.start(); + t.join(); + } + Assert.assertEquals(0, waitNotifyObject.waitingThreadTable.size()); + } + +} diff --git a/store/src/test/java/org/apache/rocketmq/store/ha/autoswitch/AutoSwitchHATest.java b/store/src/test/java/org/apache/rocketmq/store/ha/autoswitch/AutoSwitchHATest.java new file mode 100644 index 00000000000..7d659d2f6ae --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/ha/autoswitch/AutoSwitchHATest.java @@ -0,0 +1,554 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.ha.autoswitch; + +import java.util.concurrent.ConcurrentHashMap; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.GetMessageStatus; +import org.apache.rocketmq.store.MappedFileQueue; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.StoreCheckpoint; +import org.apache.rocketmq.store.config.BrokerRole; +import org.apache.rocketmq.store.config.FlushDiskType; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.logfile.MappedFile; +import org.apache.rocketmq.store.stats.BrokerStatsManager; +import org.junit.After; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Ignore; +import org.junit.Test; +import org.rocksdb.RocksDBException; + +import java.io.File; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Random; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class AutoSwitchHATest { + private final String storeMessage = "Once, there was a chance for me!"; + private final int defaultMappedFileSize = 1024 * 1024; + private int queueTotal = 100; + private AtomicInteger queueId = new AtomicInteger(0); + private SocketAddress bornHost; + private SocketAddress storeHost; + private byte[] messageBody; + + private DefaultMessageStore messageStore1; + private DefaultMessageStore messageStore2; + private DefaultMessageStore messageStore3; + private MessageStoreConfig storeConfig1; + private MessageStoreConfig storeConfig2; + private MessageStoreConfig storeConfig3; + private String store1HaAddress; + private String store2HaAddress; + + private BrokerStatsManager brokerStatsManager = new BrokerStatsManager("simpleTest", true); + private String tmpdir = System.getProperty("java.io.tmpdir"); + private String storePathRootParentDir = (StringUtils.endsWith(tmpdir, File.separator) ? tmpdir : tmpdir + File.separator) + UUID.randomUUID(); + private String storePathRootDir = storePathRootParentDir + File.separator + "store"; + private Random random = new Random(); + + public void init(int mappedFileSize) throws Exception { + String brokerName = "AutoSwitchHATest_" + random.nextInt(65535); + queueTotal = 1; + messageBody = storeMessage.getBytes(); + storeHost = new InetSocketAddress(InetAddress.getLocalHost(), 8123); + bornHost = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0); + storeConfig1 = new MessageStoreConfig(); + storeConfig1.setBrokerRole(BrokerRole.SYNC_MASTER); + storeConfig1.setHaSendHeartbeatInterval(1000); + storeConfig1.setStorePathRootDir(storePathRootDir + File.separator + brokerName + "#1"); + storeConfig1.setStorePathCommitLog(storePathRootDir + File.separator + brokerName + "#1" + File.separator + "commitlog"); + storeConfig1.setStorePathEpochFile(storePathRootDir + File.separator + brokerName + "#1" + File.separator + "EpochFileCache"); + storeConfig1.setTotalReplicas(3); + storeConfig1.setInSyncReplicas(2); + buildMessageStoreConfig(storeConfig1, mappedFileSize); + this.store1HaAddress = "127.0.0.1:10912"; + + storeConfig2 = new MessageStoreConfig(); + storeConfig2.setBrokerRole(BrokerRole.SLAVE); + storeConfig2.setHaSendHeartbeatInterval(1000); + storeConfig2.setStorePathRootDir(storePathRootDir + File.separator + brokerName + "#2"); + storeConfig2.setStorePathCommitLog(storePathRootDir + File.separator + brokerName + "#2" + File.separator + "commitlog"); + storeConfig2.setStorePathEpochFile(storePathRootDir + File.separator + brokerName + "#2" + File.separator + "EpochFileCache"); + storeConfig2.setHaListenPort(10943); + storeConfig2.setTotalReplicas(3); + storeConfig2.setInSyncReplicas(2); + buildMessageStoreConfig(storeConfig2, mappedFileSize); + this.store2HaAddress = "127.0.0.1:10943"; + + messageStore1 = buildMessageStore(storeConfig1, 1L); + messageStore2 = buildMessageStore(storeConfig2, 2L); + + storeConfig3 = new MessageStoreConfig(); + storeConfig3.setBrokerRole(BrokerRole.SLAVE); + storeConfig3.setHaSendHeartbeatInterval(1000); + storeConfig3.setStorePathRootDir(storePathRootDir + File.separator + brokerName + "#3"); + storeConfig3.setStorePathCommitLog(storePathRootDir + File.separator + brokerName + "#3" + File.separator + "commitlog"); + storeConfig3.setStorePathEpochFile(storePathRootDir + File.separator + brokerName + "#3" + File.separator + "EpochFileCache"); + storeConfig3.setHaListenPort(10980); + storeConfig3.setTotalReplicas(3); + storeConfig3.setInSyncReplicas(2); + buildMessageStoreConfig(storeConfig3, mappedFileSize); + messageStore3 = buildMessageStore(storeConfig3, 3L); + + assertTrue(messageStore1.load()); + assertTrue(messageStore2.load()); + assertTrue(messageStore3.load()); + messageStore1.start(); + messageStore2.start(); + messageStore3.start(); + +// ((AutoSwitchHAService) this.messageStore1.getHaService()).("127.0.0.1:8000"); +// ((AutoSwitchHAService) this.messageStore2.getHaService()).setLocalAddress("127.0.0.1:8001"); +// ((AutoSwitchHAService) this.messageStore3.getHaService()).setLocalAddress("127.0.0.1:8002"); + } + + public void init(int mappedFileSize, boolean allAckInSyncStateSet) throws Exception { + String brokerName = "AutoSwitchHATest_" + random.nextInt(65535); + queueTotal = 1; + messageBody = storeMessage.getBytes(); + storeHost = new InetSocketAddress(InetAddress.getLocalHost(), 8123); + bornHost = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0); + storeConfig1 = new MessageStoreConfig(); + storeConfig1.setBrokerRole(BrokerRole.SYNC_MASTER); + storeConfig1.setStorePathRootDir(storePathRootDir + File.separator + brokerName + "#1"); + storeConfig1.setStorePathCommitLog(storePathRootDir + File.separator + brokerName + "#1" + File.separator + "commitlog"); + storeConfig1.setStorePathEpochFile(storePathRootDir + File.separator + brokerName + "#1" + File.separator + "EpochFileCache"); + storeConfig1.setAllAckInSyncStateSet(allAckInSyncStateSet); + buildMessageStoreConfig(storeConfig1, mappedFileSize); + this.store1HaAddress = "127.0.0.1:10912"; + + storeConfig2 = new MessageStoreConfig(); + storeConfig2.setBrokerRole(BrokerRole.SLAVE); + storeConfig2.setStorePathRootDir(storePathRootDir + File.separator + brokerName + "#2"); + storeConfig2.setStorePathCommitLog(storePathRootDir + File.separator + brokerName + "#2" + File.separator + "commitlog"); + storeConfig2.setStorePathEpochFile(storePathRootDir + File.separator + brokerName + "#2" + File.separator + "EpochFileCache"); + storeConfig2.setHaListenPort(10943); + storeConfig2.setAllAckInSyncStateSet(allAckInSyncStateSet); + buildMessageStoreConfig(storeConfig2, mappedFileSize); + this.store2HaAddress = "127.0.0.1:10943"; + + messageStore1 = buildMessageStore(storeConfig1, 1L); + messageStore2 = buildMessageStore(storeConfig2, 2L); + + assertTrue(messageStore1.load()); + assertTrue(messageStore2.load()); + messageStore1.start(); + messageStore2.start(); + +// ((AutoSwitchHAService) this.messageStore1.getHaService()).setLocalAddress("127.0.0.1:8000"); +// ((AutoSwitchHAService) this.messageStore2.getHaService()).setLocalAddress("127.0.0.1:8001"); + } + + private boolean changeMasterAndPutMessage(DefaultMessageStore master, MessageStoreConfig masterConfig, + DefaultMessageStore slave, long slaveId, MessageStoreConfig slaveConfig, int epoch, String masterHaAddress, + int totalPutMessageNums) throws RocksDBException { + + boolean flag = true; + // Change role + slaveConfig.setBrokerRole(BrokerRole.SLAVE); + masterConfig.setBrokerRole(BrokerRole.SYNC_MASTER); + flag &= slave.getHaService().changeToSlave("", epoch, slaveId); + slave.getHaService().updateHaMasterAddress(masterHaAddress); + flag &= master.getHaService().changeToMaster(epoch); + // Put message on master + for (int i = 0; i < totalPutMessageNums; i++) { + PutMessageResult result = master.putMessage(buildMessage()); + flag &= result.isOk(); + } + return flag; + } + + private void checkMessage(final DefaultMessageStore messageStore, int totalNums, int startOffset) { + await().atMost(30, TimeUnit.SECONDS) + .until(() -> { + GetMessageResult result = messageStore.getMessage("GROUP_A", "FooBar", 0, startOffset, 1024, null); +// System.out.printf(result + "%n"); + return result != null && result.getStatus() == GetMessageStatus.FOUND && result.getMessageCount() >= totalNums; + }); + } + + @Test + public void testConfirmOffset() throws Exception { + init(defaultMappedFileSize, true); + // Step1, set syncStateSet, if both broker1 and broker2 are in syncStateSet, the confirmOffset will be computed as the min slaveAckOffset(broker2's ack) + ((AutoSwitchHAService) this.messageStore1.getHaService()).setSyncStateSet(new HashSet<>(Arrays.asList(1L, 2L))); + boolean masterAndPutMessage = changeMasterAndPutMessage(this.messageStore1, this.storeConfig1, this.messageStore2, 2, this.storeConfig2, 1, store1HaAddress, 10); + assertTrue(masterAndPutMessage); + checkMessage(this.messageStore2, 10, 0); + + final long confirmOffset = this.messageStore1.getConfirmOffset(); + + // Step2, shutdown store2 + this.messageStore2.shutdown(); + + // Put message, which should put failed. + final PutMessageResult putMessageResult = this.messageStore1.putMessage(buildMessage()); + assertEquals(putMessageResult.getPutMessageStatus(), PutMessageStatus.FLUSH_SLAVE_TIMEOUT); + + // The confirmOffset still don't change, because syncStateSet contains broker2, but broker2 shutdown + assertEquals(confirmOffset, this.messageStore1.getConfirmOffset()); + + // Step3, shutdown store1, start store2, change store2 to master, epoch = 2 + this.messageStore1.shutdown(); + + storeConfig2.setBrokerRole(BrokerRole.SYNC_MASTER); + messageStore2 = buildMessageStore(storeConfig2, 2L); + messageStore2.getRunningFlags().makeFenced(true); + assertTrue(messageStore2.load()); + messageStore2.start(); + messageStore2.getHaService().changeToMaster(2); + messageStore2.getRunningFlags().makeFenced(false); + ((AutoSwitchHAService) messageStore2.getHaService()).setSyncStateSet(new HashSet<>(Collections.singletonList(2L))); + + // Put message on master + for (int i = 0; i < 10; i++) { + messageStore2.putMessage(buildMessage()); + } + + // Step4, start store1, it should truncate dirty logs and syncLog from store2 + storeConfig1.setBrokerRole(BrokerRole.SLAVE); + messageStore1 = buildMessageStore(storeConfig1, 1L); + assertTrue(messageStore1.load()); + messageStore1.start(); + messageStore1.getHaService().changeToSlave("", 2, 1L); + messageStore1.getHaService().updateHaMasterAddress(this.store2HaAddress); + + checkMessage(this.messageStore1, 20, 0); + } + + @Test + public void testAsyncLearnerBrokerRole() throws Exception { + init(defaultMappedFileSize); + ((AutoSwitchHAService) this.messageStore1.getHaService()).setSyncStateSet(new HashSet<>(Collections.singletonList(1L))); + + storeConfig1.setBrokerRole(BrokerRole.SYNC_MASTER); + storeConfig2.setBrokerRole(BrokerRole.SLAVE); + storeConfig2.setAsyncLearner(true); + messageStore1.getHaService().changeToMaster(1); + messageStore2.getHaService().changeToSlave("", 1, 2L); + messageStore2.getHaService().updateHaMasterAddress(store1HaAddress); + // Put message on master + for (int i = 0; i < 10; i++) { + messageStore1.putMessage(buildMessage()); + } + checkMessage(messageStore2, 10, 0); + final Set syncStateSet = ((AutoSwitchHAService) this.messageStore1.getHaService()).getSyncStateSet(); + assertFalse(syncStateSet.contains(2L)); + } + + @Test + public void testOptionAllAckInSyncStateSet() throws Exception { + init(defaultMappedFileSize, true); + AtomicReference> syncStateSet = new AtomicReference<>(); + ((AutoSwitchHAService) this.messageStore1.getHaService()).setSyncStateSet(new HashSet<>(Collections.singletonList(1L))); + ((AutoSwitchHAService) this.messageStore1.getHaService()).registerSyncStateSetChangedListener(newSyncStateSet -> { + syncStateSet.set(newSyncStateSet); + }); + + changeMasterAndPutMessage(this.messageStore1, this.storeConfig1, this.messageStore2, 2, this.storeConfig2, 1, store1HaAddress, 10); + checkMessage(this.messageStore2, 10, 0); + // Check syncStateSet + final Set result = syncStateSet.get(); + assertTrue(result.contains(1L)); + assertTrue(result.contains(2L)); + + // Now, shutdown store2 + this.messageStore2.shutdown(); + this.messageStore2.destroy(); + + ((AutoSwitchHAService) this.messageStore1.getHaService()).setSyncStateSet(result); + + final PutMessageResult putMessageResult = this.messageStore1.putMessage(buildMessage()); + assertEquals(putMessageResult.getPutMessageStatus(), PutMessageStatus.FLUSH_SLAVE_TIMEOUT); + } + + @Ignore + @Test + public void testChangeRoleManyTimes() throws Exception { + + // Skip MacOSX platform for now as this test case is not stable on it. + Assume.assumeFalse(MixAll.isMac()); + + // Step1, change store1 to master, store2 to follower + init(defaultMappedFileSize); + ((AutoSwitchHAService) this.messageStore1.getHaService()).setSyncStateSet(new HashSet<>(Collections.singletonList(1L))); + changeMasterAndPutMessage(this.messageStore1, this.storeConfig1, this.messageStore2, 2, this.storeConfig2, 1, store1HaAddress, 10); + checkMessage(this.messageStore2, 10, 0); + + // Step2, change store1 to follower, store2 to master, epoch = 2 + ((AutoSwitchHAService) this.messageStore2.getHaService()).setSyncStateSet(new HashSet<>(Collections.singletonList(2L))); + changeMasterAndPutMessage(this.messageStore2, this.storeConfig2, this.messageStore1, 1, this.storeConfig1, 2, store2HaAddress, 10); + checkMessage(this.messageStore1, 20, 0); + + // Step3, change store2 to follower, store1 to master, epoch = 3 + changeMasterAndPutMessage(this.messageStore1, this.storeConfig1, this.messageStore2, 2, this.storeConfig2, 3, store1HaAddress, 10); + checkMessage(this.messageStore2, 30, 0); + } + + @Test + public void testAddBroker() throws Exception { + // Step1: broker1 as leader, broker2 as follower + init(defaultMappedFileSize); + ((AutoSwitchHAService) this.messageStore1.getHaService()).setSyncStateSet(new HashSet<>(Collections.singletonList(1L))); + + changeMasterAndPutMessage(this.messageStore1, this.storeConfig1, this.messageStore2, 2, this.storeConfig2, 1, store1HaAddress, 10); + checkMessage(this.messageStore2, 10, 0); + + // Step2: add new broker3, link to broker1 + messageStore3.getHaService().changeToSlave("", 1, 3L); + messageStore3.getHaService().updateHaMasterAddress(store1HaAddress); + checkMessage(messageStore3, 10, 0); + } + + @Test + public void testTruncateEpochLogAndAddBroker() throws Exception { + // Noted that 10 msg 's total size = 1570, and if init the mappedFileSize = 1700, one file only be used to store 10 msg. + init(1700); + + // Step1: broker1 as leader, broker2 as follower, append 2 epoch, each epoch will be stored on one file(Because fileSize = 1700, which only can hold 10 msgs); + // Master: + + ((AutoSwitchHAService) this.messageStore1.getHaService()).setSyncStateSet(new HashSet<>(Collections.singletonList(1L))); + changeMasterAndPutMessage(this.messageStore1, this.storeConfig1, this.messageStore2, 2, this.storeConfig2, 1, store1HaAddress, 10); + checkMessage(this.messageStore2, 10, 0); + + changeMasterAndPutMessage(this.messageStore1, this.storeConfig1, this.messageStore2, 2, this.storeConfig2, 2, store1HaAddress, 10); + checkMessage(this.messageStore2, 20, 0); + + // Step2: Check file position, each epoch will be stored on one file(Because fileSize = 1700, which equal to 10 msg size); + // So epoch1 was stored in firstFile, epoch2 was stored in second file, the lastFile was empty. + final MappedFileQueue fileQueue = this.messageStore1.getCommitLog().getMappedFileQueue(); + assertEquals(2, fileQueue.getTotalFileSize() / 1700); + + // Step3: truncate epoch1's log (truncateEndOffset = 1570), which means we should delete the first file directly. + final MappedFile firstFile = this.messageStore1.getCommitLog().getMappedFileQueue().getFirstMappedFile(); + firstFile.shutdown(1000); + fileQueue.retryDeleteFirstFile(1000); + assertEquals(this.messageStore1.getCommitLog().getMinOffset(), 1700); + checkMessage(this.messageStore1, 10, 10); + + final AutoSwitchHAService haService = (AutoSwitchHAService) this.messageStore1.getHaService(); + haService.truncateEpochFilePrefix(1570); + + // Step4: add broker3 as slave, only have 10 msg from offset 10; + messageStore3.getHaService().changeToSlave("", 2, 3L); + messageStore3.getHaService().updateHaMasterAddress(store1HaAddress); + + checkMessage(messageStore3, 10, 10); + } + + @Test + public void testTruncateEpochLogAndChangeMaster() throws Exception { + // Noted that 10 msg 's total size = 1570, and if init the mappedFileSize = 1700, one file only be used to store 10 msg. + init(1700); + + // Step1: broker1 as leader, broker2 as follower, append 2 epoch, each epoch will be stored on one file(Because fileSize = 1700, which only can hold 10 msgs); + // Master: + + ((AutoSwitchHAService) this.messageStore1.getHaService()).setSyncStateSet(new HashSet<>(Collections.singletonList(1L))); + changeMasterAndPutMessage(this.messageStore1, this.storeConfig1, this.messageStore2, 2, this.storeConfig2, 1, store1HaAddress, 10); + checkMessage(this.messageStore2, 10, 0); + changeMasterAndPutMessage(this.messageStore1, this.storeConfig1, this.messageStore2, 2, this.storeConfig2, 2, store1HaAddress, 10); + checkMessage(this.messageStore2, 20, 0); + + // Step2: Check file position, each epoch will be stored on one file(Because fileSize = 1700, which equal to 10 msg size); + // So epoch1 was stored in firstFile, epoch2 was stored in second file, the lastFile was empty. + final MappedFileQueue fileQueue = this.messageStore1.getCommitLog().getMappedFileQueue(); + assertEquals(2, fileQueue.getTotalFileSize() / 1700); + + // Step3: truncate epoch1's log (truncateEndOffset = 1570), which means we should delete the first file directly. + final MappedFile firstFile = this.messageStore1.getCommitLog().getMappedFileQueue().getFirstMappedFile(); + firstFile.shutdown(1000); + fileQueue.retryDeleteFirstFile(1000); + assertEquals(this.messageStore1.getCommitLog().getMinOffset(), 1700); + + final AutoSwitchHAService haService = (AutoSwitchHAService) this.messageStore1.getHaService(); + haService.truncateEpochFilePrefix(1570); + checkMessage(this.messageStore1, 10, 10); + + // Step4: add broker3 as slave + messageStore3.getHaService().changeToSlave("", 2, 3L); + messageStore3.getHaService().updateHaMasterAddress(store1HaAddress); + + checkMessage(messageStore3, 10, 10); + + // Step5: change broker2 as leader, broker3 as follower + ((AutoSwitchHAService) this.messageStore2.getHaService()).setSyncStateSet(new HashSet<>(Collections.singletonList(2L))); + changeMasterAndPutMessage(this.messageStore2, this.storeConfig2, this.messageStore3, 3, this.storeConfig3, 3, this.store2HaAddress, 10); + checkMessage(messageStore3, 20, 10); + + // Step6, let broker1 link to broker2, it should sync log from epoch3. + this.storeConfig1.setBrokerRole(BrokerRole.SLAVE); + this.messageStore1.getHaService().changeToSlave("", 3, 1L); + this.messageStore1.getHaService().updateHaMasterAddress(this.store2HaAddress); + + checkMessage(messageStore1, 20, 0); + } + + @Test + public void testAddBrokerAndSyncFromLastFile() throws Exception { + init(1700); + + // Step1: broker1 as leader, broker2 as follower, append 2 epoch, each epoch will be stored on one file(Because fileSize = 1700, which only can hold 10 msgs); + // Master: + ((AutoSwitchHAService) this.messageStore1.getHaService()).setSyncStateSet(new HashSet<>(Collections.singletonList(1L))); + changeMasterAndPutMessage(this.messageStore1, this.storeConfig1, this.messageStore2, 2, this.storeConfig2, 1, store1HaAddress, 10); + checkMessage(this.messageStore2, 10, 0); + changeMasterAndPutMessage(this.messageStore1, this.storeConfig1, this.messageStore2, 2, this.storeConfig2, 2, store1HaAddress, 10); + checkMessage(this.messageStore2, 20, 0); + + // Step2: restart broker3 + messageStore3.shutdown(); + messageStore3.destroy(); + + storeConfig3.setSyncFromLastFile(true); + messageStore3 = buildMessageStore(storeConfig3, 3L); + assertTrue(messageStore3.load()); + messageStore3.start(); + + // Step2: add new broker3, link to broker1. because broker3 request sync from lastFile, so it only synced 10 msg from offset 10; + messageStore3.getHaService().changeToSlave("", 2, 3L); + messageStore3.getHaService().updateHaMasterAddress("127.0.0.1:10912"); + + checkMessage(messageStore3, 10, 10); + } + + @Test + public void testCheckSynchronizingSyncStateSetFlag() throws Exception { + // Step1: broker1 as leader, broker2 as follower + init(defaultMappedFileSize); + ((AutoSwitchHAService) this.messageStore1.getHaService()).setSyncStateSet(new HashSet<>(Collections.singletonList(1L))); + + changeMasterAndPutMessage(this.messageStore1, this.storeConfig1, this.messageStore2, 2, this.storeConfig2, 1, store1HaAddress, 10); + checkMessage(this.messageStore2, 10, 0); + AutoSwitchHAService masterHAService = (AutoSwitchHAService) this.messageStore1.getHaService(); + + // Step2: check flag SynchronizingSyncStateSet + Assert.assertTrue(masterHAService.isSynchronizingSyncStateSet()); + Assert.assertEquals(this.messageStore1.getConfirmOffset(), 1580); + Set syncStateSet = masterHAService.getSyncStateSet(); + Assert.assertEquals(syncStateSet.size(), 2); + Assert.assertTrue(syncStateSet.contains(1L)); + + // Step3: set new syncStateSet + HashSet newSyncStateSet = new HashSet() {{ + add(1L); + add(2L); + }}; + masterHAService.setSyncStateSet(newSyncStateSet); + Assert.assertFalse(masterHAService.isSynchronizingSyncStateSet()); + } + + @Test + public void testBuildConsumeQueueNotExceedConfirmOffset() throws Exception { + init(defaultMappedFileSize); + ((AutoSwitchHAService) this.messageStore1.getHaService()).setSyncStateSet(new HashSet<>(Collections.singletonList(1L))); + changeMasterAndPutMessage(this.messageStore1, this.storeConfig1, this.messageStore2, 2, this.storeConfig2, 1, store1HaAddress, 10); + checkMessage(this.messageStore2, 10, 0); + + long tmpConfirmOffset = this.messageStore2.getConfirmOffset(); + long setConfirmOffset = this.messageStore2.getConfirmOffset() - this.messageStore2.getConfirmOffset() / 2; + messageStore2.shutdown(); + StoreCheckpoint storeCheckpoint = new StoreCheckpoint(storeConfig2.getStorePathRootDir() + File.separator + "checkpoint"); + assertEquals(tmpConfirmOffset, storeCheckpoint.getConfirmPhyOffset()); + storeCheckpoint.setConfirmPhyOffset(setConfirmOffset); + storeCheckpoint.shutdown(); + messageStore2 = buildMessageStore(storeConfig2, 2L); + messageStore2.getRunningFlags().makeFenced(true); + assertTrue(messageStore2.load()); + messageStore2.start(); + messageStore2.getRunningFlags().makeFenced(false); + assertEquals(setConfirmOffset, messageStore2.getConfirmOffset()); + checkMessage(this.messageStore2, 5, 0); + } + + @After + public void destroy() throws Exception { + if (this.messageStore2 != null) { + messageStore2.shutdown(); + messageStore2.destroy(); + } + if (this.messageStore1 != null) { + messageStore1.shutdown(); + messageStore1.destroy(); + } + if (this.messageStore3 != null) { + messageStore3.shutdown(); + messageStore3.destroy(); + } + File file = new File(storePathRootParentDir); + UtilAll.deleteFile(file); + } + + private DefaultMessageStore buildMessageStore(MessageStoreConfig messageStoreConfig, + long brokerId) throws Exception { + BrokerConfig brokerConfig = new BrokerConfig(); + brokerConfig.setBrokerId(brokerId); + brokerConfig.setEnableControllerMode(true); + return new DefaultMessageStore(messageStoreConfig, brokerStatsManager, null, brokerConfig, new ConcurrentHashMap<>()); + } + + private void buildMessageStoreConfig(MessageStoreConfig messageStoreConfig, int mappedFileSize) { + messageStoreConfig.setMappedFileSizeCommitLog(mappedFileSize); + messageStoreConfig.setMappedFileSizeConsumeQueue(1024 * 1024); + messageStoreConfig.setMaxHashSlotNum(10000); + messageStoreConfig.setMaxIndexNum(100 * 100); + messageStoreConfig.setFlushDiskType(FlushDiskType.SYNC_FLUSH); + messageStoreConfig.setFlushIntervalConsumeQueue(1); + } + + private MessageExtBrokerInner buildMessage() { + MessageExtBrokerInner msg = new MessageExtBrokerInner(); + msg.setTopic("FooBar"); + msg.setTags("TAG1"); + msg.setBody(messageBody); + msg.setKeys(String.valueOf(System.currentTimeMillis())); + msg.setQueueId(Math.abs(queueId.getAndIncrement()) % queueTotal); + msg.setSysFlag(0); + msg.setBornTimestamp(System.currentTimeMillis()); + msg.setStoreHost(storeHost); + msg.setBornHost(bornHost); + msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); + return msg; + } +} diff --git a/store/src/test/java/org/apache/rocketmq/store/ha/autoswitch/EpochFileCacheTest.java b/store/src/test/java/org/apache/rocketmq/store/ha/autoswitch/EpochFileCacheTest.java new file mode 100644 index 00000000000..aef83e934a1 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/ha/autoswitch/EpochFileCacheTest.java @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.ha.autoswitch; + +import java.io.File; +import java.nio.file.Paths; +import org.apache.rocketmq.remoting.protocol.EpochEntry; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class EpochFileCacheTest { + private EpochFileCache epochCache; + private EpochFileCache epochCache2; + private String path; + private String path2; + + @Before + public void setup() { + this.path = Paths.get(File.separator + "tmp", "EpochCheckpoint").toString(); + this.epochCache = new EpochFileCache(path); + assertTrue(this.epochCache.appendEntry(new EpochEntry(1, 100))); + assertTrue(this.epochCache.appendEntry(new EpochEntry(2, 300))); + assertTrue(this.epochCache.appendEntry(new EpochEntry(3, 500))); + final EpochEntry entry = this.epochCache.getEntry(2); + assertEquals(entry.getEpoch(), 2); + assertEquals(entry.getStartOffset(), 300); + assertEquals(entry.getEndOffset(), 500); + } + + @After + public void shutdown() { + new File(this.path).delete(); + if (this.path2 != null) { + new File(this.path2).delete(); + } + } + + @Test + public void testInitFromFile() { + // Remove entries, init from file + assertTrue(this.epochCache.initCacheFromFile()); + final EpochEntry entry = this.epochCache.getEntry(2); + assertEquals(entry.getEpoch(), 2); + assertEquals(entry.getStartOffset(), 300); + assertEquals(entry.getEndOffset(), 500); + } + + @Test + public void testTruncate() { + this.epochCache.truncateSuffixByOffset(150); + assertNotNull(this.epochCache.getEntry(1)); + assertNull(this.epochCache.getEntry(2)); + } + + @Test + public void testFindEpochEntryByOffset() { + final EpochEntry entry = this.epochCache.findEpochEntryByOffset(350); + assertEquals(entry.getEpoch(), 2); + assertEquals(entry.getStartOffset(), 300); + assertEquals(entry.getEndOffset(), 500); + } + + @Test + public void testFindConsistentPointSample1() { + this.path2 = Paths.get(File.separator + "tmp", "EpochCheckpoint2").toString(); + this.epochCache2 = new EpochFileCache(path2); + assertTrue(this.epochCache2.appendEntry(new EpochEntry(1, 100))); + assertTrue(this.epochCache2.appendEntry(new EpochEntry(2, 300))); + assertTrue(this.epochCache2.appendEntry(new EpochEntry(3, 450))); + /** + * cache1: , , + * cache2: , , + * The consistent point should be 450 + */ + final long consistentPoint = this.epochCache.findConsistentPoint(this.epochCache2); + assertEquals(consistentPoint, 450); + } + + @Test + public void testFindConsistentPointSample2() { + this.path2 = Paths.get(File.separator + "tmp", "EpochCheckpoint2").toString(); + this.epochCache2 = new EpochFileCache(path2); + assertTrue(this.epochCache2.appendEntry(new EpochEntry(1, 100))); + assertTrue(this.epochCache2.appendEntry(new EpochEntry(2, 300))); + assertTrue(this.epochCache2.appendEntry(new EpochEntry(3, 500))); + /** + * cache1: , , + * cache2: , , + * The consistent point should be 600 + */ + this.epochCache.setLastEpochEntryEndOffset(700); + this.epochCache2.setLastEpochEntryEndOffset(600); + final long consistentPoint = this.epochCache.findConsistentPoint(this.epochCache2); + assertEquals(consistentPoint, 600); + } + + @Test + public void testFindConsistentPointSample3() { + this.path2 = Paths.get(File.separator + "tmp", "EpochCheckpoint2").toString(); + this.epochCache2 = new EpochFileCache(path2); + assertTrue(this.epochCache2.appendEntry(new EpochEntry(1, 200))); + assertTrue(this.epochCache2.appendEntry(new EpochEntry(2, 500))); + /** + * cache1: , , + * cache2: , + * The consistent point should be -1 + */ + final long consistentPoint = this.epochCache.findConsistentPoint(this.epochCache2); + assertEquals(consistentPoint, -1); + } + + @Test + public void testFindConsistentPointSample4() { + this.path2 = Paths.get(File.separator + "tmp", "EpochCheckpoint2").toString(); + this.epochCache2 = new EpochFileCache(path2); + assertTrue(this.epochCache2.appendEntry(new EpochEntry(1, 100))); + assertTrue(this.epochCache2.appendEntry(new EpochEntry(2, 300))); + assertTrue(this.epochCache2.appendEntry(new EpochEntry(3, 500))); + assertTrue(this.epochCache2.appendEntry(new EpochEntry(4, 800))); + /** + * cache1: , , + * cache2: , , , + * The consistent point should be 700 + */ + this.epochCache.setLastEpochEntryEndOffset(700); + final long consistentPoint = this.epochCache2.findConsistentPoint(this.epochCache); + assertEquals(consistentPoint, 700); + } +} diff --git a/store/src/test/java/org/apache/rocketmq/store/index/IndexFileTest.java b/store/src/test/java/org/apache/rocketmq/store/index/IndexFileTest.java index 7ad5b38db1d..7b35951b877 100644 --- a/store/src/test/java/org/apache/rocketmq/store/index/IndexFileTest.java +++ b/store/src/test/java/org/apache/rocketmq/store/index/IndexFileTest.java @@ -30,8 +30,8 @@ import static org.assertj.core.api.Assertions.assertThat; public class IndexFileTest { - private final int HASH_SLOT_NUM = 100; - private final int INDEX_NUM = 400; + private static final int HASH_SLOT_NUM = 100; + private static final int INDEX_NUM = 400; @Test public void testPutKey() throws Exception { @@ -62,8 +62,8 @@ public void testSelectPhyOffset() throws Exception { boolean putResult = indexFile.putKey(Long.toString(400), 400, System.currentTimeMillis()); assertThat(putResult).isFalse(); - final List phyOffsets = new ArrayList(); - indexFile.selectPhyOffset(phyOffsets, "60", 10, 0, Long.MAX_VALUE, true); + final List phyOffsets = new ArrayList<>(); + indexFile.selectPhyOffset(phyOffsets, "60", 10, 0, Long.MAX_VALUE); assertThat(phyOffsets).isNotEmpty(); assertThat(phyOffsets.size()).isEqualTo(1); indexFile.destroy(0); diff --git a/store/src/test/java/org/apache/rocketmq/store/kv/CompactionLogTest.java b/store/src/test/java/org/apache/rocketmq/store/kv/CompactionLogTest.java new file mode 100644 index 00000000000..e113b18f1e5 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/kv/CompactionLogTest.java @@ -0,0 +1,267 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.kv; + +import com.google.common.collect.Lists; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.store.AppendMessageResult; +import org.apache.rocketmq.store.AppendMessageStatus; +import org.apache.rocketmq.store.CommitLog; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.MappedFileQueue; +import org.apache.rocketmq.store.MessageExtEncoder; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageSpinLock; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.logfile.DefaultMappedFile; +import org.apache.rocketmq.store.logfile.MappedFile; +import org.apache.rocketmq.store.queue.SparseConsumeQueue; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.stubbing.Answer; + +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.security.DigestException; +import java.security.NoSuchAlgorithmException; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import static org.apache.rocketmq.store.kv.CompactionLog.COMPACTING_SUB_FOLDER; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class CompactionLogTest { + CompactionLog clog; + MessageStoreConfig storeConfig; + MessageStore defaultMessageStore; + CompactionPositionMgr positionMgr; + String topic = "ctopic"; + int queueId = 0; + int offsetMemorySize = 1024; + int compactionFileSize = 10240; + int compactionCqFileSize = 1024; + + + private static MessageExtEncoder encoder = new MessageExtEncoder(1024, new MessageStoreConfig()); + private static SocketAddress storeHost; + private static SocketAddress bornHost; + + @Rule + public TemporaryFolder tmpFolder = new TemporaryFolder(); + String logPath; + String cqPath; + + static { + try { + storeHost = new InetSocketAddress(InetAddress.getLocalHost(), 8123); + } catch (UnknownHostException e) { + } + try { + bornHost = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0); + } catch (UnknownHostException e) { + } + } + + @Before + public void setUp() throws IOException { + File file = tmpFolder.newFolder("compaction"); + logPath = Paths.get(file.getAbsolutePath(), "compactionLog").toString(); + cqPath = Paths.get(file.getAbsolutePath(), "compactionCq").toString(); + + storeConfig = mock(MessageStoreConfig.class); + doReturn(compactionFileSize).when(storeConfig).getCompactionMappedFileSize(); + doReturn(compactionCqFileSize).when(storeConfig).getCompactionCqMappedFileSize(); + defaultMessageStore = mock(DefaultMessageStore.class); + doReturn(storeConfig).when(defaultMessageStore).getMessageStoreConfig(); + positionMgr = mock(CompactionPositionMgr.class); + doReturn(-1L).when(positionMgr).getOffset(topic, queueId); + } + + static int queueOffset = 0; + static int keyCount = 10; + public static ByteBuffer buildMessage() { + MessageExtBrokerInner msg = new MessageExtBrokerInner(); + msg.setTopic("ctopic"); + msg.setTags(System.currentTimeMillis() + "TAG"); + msg.setKeys(String.valueOf(queueOffset % keyCount)); + msg.setBody(RandomStringUtils.randomAlphabetic(100).getBytes(StandardCharsets.UTF_8)); + msg.setQueueId(0); + msg.setSysFlag(0); + msg.setBornTimestamp(System.currentTimeMillis()); + msg.setStoreHost(storeHost); + msg.setBornHost(bornHost); + msg.setQueueOffset(queueOffset); + queueOffset++; + for (int i = 1; i < 3; i++) { + msg.putUserProperty(String.valueOf(i), "xxx" + i); + } + msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); + encoder.encode(msg); + return encoder.getEncoderBuffer(); + } + + + @Test + public void testCheck() throws IllegalAccessException { + MappedFileQueue mfq = mock(MappedFileQueue.class); + MappedFileQueue smfq = mock(MappedFileQueue.class); + SparseConsumeQueue scq = mock(SparseConsumeQueue.class); + doReturn(smfq).when(scq).getMappedFileQueue(); + CompactionLog.TopicPartitionLog tpLog = mock(CompactionLog.TopicPartitionLog.class); + FieldUtils.writeField(tpLog, "mappedFileQueue", mfq, true); + FieldUtils.writeField(tpLog, "consumeQueue", scq, true); + + doReturn(Lists.newArrayList()).when(mfq).getMappedFiles(); + doReturn(Lists.newArrayList()).when(smfq).getMappedFiles(); + + doCallRealMethod().when(tpLog).sanityCheck(); + tpLog.sanityCheck(); + } + + @Test(expected = RuntimeException.class) + public void testCheckWithException() throws IllegalAccessException, IOException { + MappedFileQueue mfq = mock(MappedFileQueue.class); + MappedFileQueue smfq = mock(MappedFileQueue.class); + SparseConsumeQueue scq = mock(SparseConsumeQueue.class); + doReturn(smfq).when(scq).getMappedFileQueue(); + CompactionLog.TopicPartitionLog tpLog = mock(CompactionLog.TopicPartitionLog.class); + FieldUtils.writeField(tpLog, "mappedFileQueue", mfq, true); + FieldUtils.writeField(tpLog, "consumeQueue", scq, true); + + Files.createDirectories(Paths.get(logPath, topic, String.valueOf(queueId))); + Files.write(Paths.get(logPath, topic, String.valueOf(queueId), "102400"), + RandomStringUtils.randomAlphanumeric(compactionFileSize).getBytes(StandardCharsets.UTF_8), + StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE); + MappedFile mappedFile = new DefaultMappedFile( + Paths.get(logPath, topic, String.valueOf(queueId), "102400").toFile().getAbsolutePath(), + compactionFileSize); + doReturn(Lists.newArrayList(mappedFile)).when(mfq).getMappedFiles(); + doReturn(Lists.newArrayList()).when(smfq).getMappedFiles(); + + doCallRealMethod().when(tpLog).sanityCheck(); + tpLog.sanityCheck(); + } + + @Test + public void testCompaction() throws DigestException, NoSuchAlgorithmException, IllegalAccessException { + Iterator iterator = mock(Iterator.class); + SelectMappedBufferResult smb = mock(SelectMappedBufferResult.class); + when(iterator.hasNext()).thenAnswer((Answer)invocationOnMock -> queueOffset < 1024); + when(iterator.next()).thenAnswer((Answer)invocation -> + new SelectMappedBufferResult(0, buildMessage(), 0, null)); + + MappedFile mf = mock(MappedFile.class); + List mappedFileList = Lists.newArrayList(mf); + doReturn(iterator).when(mf).iterator(0); + + MessageStore messageStore = mock(DefaultMessageStore.class); + CommitLog commitLog = mock(CommitLog.class); + when(messageStore.getCommitLog()).thenReturn(commitLog); + when(commitLog.getCommitLogSize()).thenReturn(1024 * 1024); + CompactionLog clog = mock(CompactionLog.class); + FieldUtils.writeField(clog, "defaultMessageStore", messageStore, true); + doCallRealMethod().when(clog).getOffsetMap(any()); + FieldUtils.writeField(clog, "positionMgr", positionMgr, true); + + queueOffset = 0; + CompactionLog.OffsetMap offsetMap = clog.getOffsetMap(mappedFileList); + assertEquals(1023, offsetMap.getLastOffset()); + + doCallRealMethod().when(clog).compaction(any(List.class), any(CompactionLog.OffsetMap.class)); + doNothing().when(clog).putEndMessage(any(MappedFileQueue.class)); + doCallRealMethod().when(clog).checkAndPutMessage(any(SelectMappedBufferResult.class), + any(MessageExt.class), any(CompactionLog.OffsetMap.class), any(CompactionLog.TopicPartitionLog.class)); + doCallRealMethod().when(clog).shouldRetainMsg(any(MessageExt.class), any(CompactionLog.OffsetMap.class)); + List compactResult = Lists.newArrayList(); + when(clog.asyncPutMessage(any(ByteBuffer.class), any(MessageExt.class), + any(CompactionLog.TopicPartitionLog.class))) + .thenAnswer((Answer>)invocation -> { + compactResult.add(invocation.getArgument(1)); + return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.PUT_OK, + new AppendMessageResult(AppendMessageStatus.PUT_OK))); + }); + queueOffset = 0; + clog.compaction(mappedFileList, offsetMap); + assertEquals(keyCount, compactResult.size()); + assertEquals(1014, compactResult.stream().mapToLong(MessageExt::getQueueOffset).min().orElse(1024)); + assertEquals(1023, compactResult.stream().mapToLong(MessageExt::getQueueOffset).max().orElse(0)); + } + + @Test + public void testReplaceFiles() throws IOException, IllegalAccessException { + Assume.assumeFalse(MixAll.isWindows()); + CompactionLog clog = mock(CompactionLog.class); + doCallRealMethod().when(clog).replaceFiles(anyList(), any(CompactionLog.TopicPartitionLog.class), + any(CompactionLog.TopicPartitionLog.class)); + doCallRealMethod().when(clog).replaceCqFiles(any(SparseConsumeQueue.class), + any(SparseConsumeQueue.class), anyList()); + + CompactionLog.TopicPartitionLog dest = mock(CompactionLog.TopicPartitionLog.class); + MappedFileQueue destMFQ = mock(MappedFileQueue.class); + when(dest.getLog()).thenReturn(destMFQ); + List destFiles = Lists.newArrayList(); + when(destMFQ.getMappedFiles()).thenReturn(destFiles); + + List srcFiles = Lists.newArrayList(); + String fileName = logPath + File.separator + COMPACTING_SUB_FOLDER + File.separator + String.format("%010d", 0); + MappedFile mf = new DefaultMappedFile(fileName, 1024); + srcFiles.add(mf); + MappedFileQueue srcMFQ = mock(MappedFileQueue.class); + when(srcMFQ.getMappedFiles()).thenReturn(srcFiles); + CompactionLog.TopicPartitionLog src = mock(CompactionLog.TopicPartitionLog.class); + when(src.getLog()).thenReturn(srcMFQ); + + FieldUtils.writeField(clog, "readMessageLock", new PutMessageSpinLock(), true); + + clog.replaceFiles(Lists.newArrayList(), dest, src); + assertEquals(destFiles.size(), 1); + destFiles.forEach(f -> { + assertFalse(f.getFileName().contains(COMPACTING_SUB_FOLDER)); + }); + } + +} \ No newline at end of file diff --git a/store/src/test/java/org/apache/rocketmq/store/kv/CompactionPositionMgrTest.java b/store/src/test/java/org/apache/rocketmq/store/kv/CompactionPositionMgrTest.java new file mode 100644 index 00000000000..9206fcc4520 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/kv/CompactionPositionMgrTest.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.kv; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.IOException; + +import static org.junit.Assert.assertEquals; + +public class CompactionPositionMgrTest { + + @Rule + public TemporaryFolder tmpFolder = new TemporaryFolder(); + + File file; + + @Before + public void setUp() throws IOException { + file = tmpFolder.newFolder("compaction"); + } + + @Test + public void testGetAndSet() { + CompactionPositionMgr mgr = new CompactionPositionMgr(file.getAbsolutePath()); + mgr.setOffset("topic1", 1, 1); + assertEquals(1, mgr.getOffset("topic1", 1)); + mgr.setOffset("topic1", 1, 2); + assertEquals(2, mgr.getOffset("topic1", 1)); + mgr.setOffset("topic1", 2, 1); + assertEquals(1, mgr.getOffset("topic1", 2)); + } + + @Test + public void testLoadAndPersist() throws IOException { + CompactionPositionMgr mgr = new CompactionPositionMgr(file.getAbsolutePath()); + mgr.setOffset("topic1", 1, 2); + mgr.setOffset("topic1", 2, 1); + mgr.persist(); + mgr = null; + + CompactionPositionMgr mgr2 = new CompactionPositionMgr(file.getAbsolutePath()); + mgr2.load(); + assertEquals(2, mgr2.getOffset("topic1", 1)); + assertEquals(1, mgr2.getOffset("topic1", 2)); + } +} \ No newline at end of file diff --git a/store/src/test/java/org/apache/rocketmq/store/kv/OffsetMapTest.java b/store/src/test/java/org/apache/rocketmq/store/kv/OffsetMapTest.java new file mode 100644 index 00000000000..e520c6a3bb4 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/kv/OffsetMapTest.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.kv; + +import org.apache.rocketmq.store.kv.CompactionLog.OffsetMap; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThrows; + +public class OffsetMapTest { + + @Test + public void testPutAndGet() throws Exception { + OffsetMap offsetMap = new OffsetMap(0); //min 100 entry + offsetMap.put("abcde", 1); + offsetMap.put("abc", 3); + offsetMap.put("cde", 4); + offsetMap.put("abcde", 9); + assertEquals(offsetMap.get("abcde"), 9); + assertEquals(offsetMap.get("cde"), 4); + assertEquals(offsetMap.get("not_exist"), -1); + assertEquals(offsetMap.getLastOffset(), 9); + } + + @Test + public void testFull() throws Exception { + OffsetMap offsetMap = new OffsetMap(0); //min 100 entry + for (int i = 0; i < 100; i++) { + offsetMap.put(String.valueOf(i), i); + } + + assertEquals(offsetMap.get("66"), 66); + assertNotEquals(offsetMap.get("55"), 56); + assertEquals(offsetMap.getLastOffset(), 99); + assertThrows(IllegalArgumentException.class, () -> offsetMap.put(String.valueOf(100), 100)); + } +} \ No newline at end of file diff --git a/store/src/test/java/org/apache/rocketmq/store/logfile/DefaultMappedFileTest.java b/store/src/test/java/org/apache/rocketmq/store/logfile/DefaultMappedFileTest.java new file mode 100644 index 00000000000..c150aae6f0f --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/logfile/DefaultMappedFileTest.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.logfile; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class DefaultMappedFileTest { + + @Rule + public TemporaryFolder tmpFolder = new TemporaryFolder(); + + String path; + + @Before + public void setUp() throws IOException { + path = tmpFolder.newFolder("compaction").getAbsolutePath(); + } + + @Test + public void testWriteFile() throws IOException { + Files.write(Paths.get(path,"test.file"), "111".getBytes(StandardCharsets.UTF_8), + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + + Files.write(Paths.get(path,"test.file"), "111".getBytes(StandardCharsets.UTF_8), + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + + List positions = Files.readAllLines(Paths.get(path, "test.file"), StandardCharsets.UTF_8); + int p = Integer.parseInt(positions.stream().findFirst().orElse("0")); + assertEquals(111, p); + + Files.write(Paths.get(path,"test.file"), "222".getBytes(StandardCharsets.UTF_8), + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + positions = Files.readAllLines(Paths.get(path,"test.file"), StandardCharsets.UTF_8); + p = Integer.parseInt(positions.stream().findFirst().orElse("0")); + assertEquals(222, p); + } + +} \ No newline at end of file diff --git a/store/src/test/java/org/apache/rocketmq/store/pop/AckMsgTest.java b/store/src/test/java/org/apache/rocketmq/store/pop/AckMsgTest.java new file mode 100644 index 00000000000..b5a3ff6381a --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/pop/AckMsgTest.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.pop; + +import com.alibaba.fastjson.JSON; +import org.junit.Assert; +import org.junit.Test; + +public class AckMsgTest { + + @Test + public void testSerializeAndDeSerialize() { + String longString = "{\"ackOffset\":100,\"brokerName\":\"brokerName\",\"consumerGroup\":\"group\"," + + "\"popTime\":1670212915531,\"queueId\":3,\"startOffset\":200,\"topic\":\"topic\"}"; + + AckMsg ackMsg = new AckMsg(); + ackMsg.setBrokerName("brokerName"); + ackMsg.setTopic("topic"); + ackMsg.setConsumerGroup("group"); + ackMsg.setQueueId(3); + ackMsg.setStartOffset(200L); + ackMsg.setAckOffset(100L); + ackMsg.setPopTime(1670212915531L); + String jsonString = JSON.toJSONString(ackMsg); + AckMsg ackMsg1 = JSON.parseObject(jsonString, AckMsg.class); + AckMsg ackMsg2 = JSON.parseObject(longString, AckMsg.class); + + Assert.assertEquals(ackMsg1.getBrokerName(), ackMsg2.getBrokerName()); + Assert.assertEquals(ackMsg1.getTopic(), ackMsg2.getTopic()); + Assert.assertEquals(ackMsg1.getConsumerGroup(), ackMsg2.getConsumerGroup()); + Assert.assertEquals(ackMsg1.getQueueId(), ackMsg2.getQueueId()); + Assert.assertEquals(ackMsg1.getStartOffset(), ackMsg2.getStartOffset()); + Assert.assertEquals(ackMsg1.getAckOffset(), ackMsg2.getAckOffset()); + Assert.assertEquals(ackMsg1.getPopTime(), ackMsg2.getPopTime()); + } +} \ No newline at end of file diff --git a/store/src/test/java/org/apache/rocketmq/store/pop/BatchAckMsgTest.java b/store/src/test/java/org/apache/rocketmq/store/pop/BatchAckMsgTest.java new file mode 100644 index 00000000000..4bcfcf18be6 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/pop/BatchAckMsgTest.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.pop; + +import com.alibaba.fastjson.JSON; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +public class BatchAckMsgTest { + + @Test + public void testSerializeAndDeSerialize() { + String longString = "{\"ackOffsetList\":[100, 101],\"consumerGroup\":\"group\"," + + "\"popTime\":1679454922000,\"queueId\":3,\"startOffset\":200,\"topic\":\"topic\"}"; + + BatchAckMsg batchAckMsg = new BatchAckMsg(); + List aol = new ArrayList<>(32); + aol.add(100L); + aol.add(101L); + + batchAckMsg.setAckOffsetList(aol); + batchAckMsg.setStartOffset(200L); + batchAckMsg.setConsumerGroup("group"); + batchAckMsg.setTopic("topic"); + batchAckMsg.setQueueId(3); + batchAckMsg.setPopTime(1679454922000L); + + String jsonString = JSON.toJSONString(batchAckMsg); + BatchAckMsg batchAckMsg1 = JSON.parseObject(jsonString, BatchAckMsg.class); + BatchAckMsg batchAckMsg2 = JSON.parseObject(longString, BatchAckMsg.class); + + Assert.assertEquals(batchAckMsg1.getAckOffsetList(), batchAckMsg2.getAckOffsetList()); + Assert.assertEquals(batchAckMsg1.getTopic(), batchAckMsg2.getTopic()); + Assert.assertEquals(batchAckMsg1.getConsumerGroup(), batchAckMsg2.getConsumerGroup()); + Assert.assertEquals(batchAckMsg1.getQueueId(), batchAckMsg2.getQueueId()); + Assert.assertEquals(batchAckMsg1.getStartOffset(), batchAckMsg2.getStartOffset()); + Assert.assertEquals(batchAckMsg1.getPopTime(), batchAckMsg2.getPopTime()); + } +} diff --git a/store/src/test/java/org/apache/rocketmq/store/queue/BatchConsumeMessageTest.java b/store/src/test/java/org/apache/rocketmq/store/queue/BatchConsumeMessageTest.java new file mode 100644 index 00000000000..e3ac1b6bdac --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/queue/BatchConsumeMessageTest.java @@ -0,0 +1,577 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.queue; + +import java.io.File; +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Queue; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.attribute.CQType; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.common.utils.QueueTypeUtils; +import org.apache.rocketmq.store.ConsumeQueueExt; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.GetMessageStatus; +import org.apache.rocketmq.store.MessageFilter; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; + +public class BatchConsumeMessageTest extends QueueTestBase { + private static final int BATCH_NUM = 10; + private static final int TOTAL_MSGS = 200; + private DefaultMessageStore messageStore; + private ConcurrentMap topicConfigTableMap; + + @Before + public void init() throws Exception { + this.topicConfigTableMap = new ConcurrentHashMap<>(); + messageStore = (DefaultMessageStore) createMessageStore(null, true, this.topicConfigTableMap); + messageStore.load(); + messageStore.start(); + } + + @After + public void destroy() { + messageStore.shutdown(); + messageStore.destroy(); + + File file = new File(messageStore.getMessageStoreConfig().getStorePathRootDir()); + UtilAll.deleteFile(file); + } + + @Test + public void testSendMessagesToCqTopic() { + String topic = UUID.randomUUID().toString(); + ConcurrentMap topicConfigTable = createTopicConfigTable(topic, CQType.SimpleCQ); + this.topicConfigTableMap.putAll(topicConfigTable); + +// int batchNum = 10; + + // case 1 has PROPERTY_INNER_NUM but has no INNER_BATCH_FLAG +// MessageExtBrokerInner messageExtBrokerInner = buildMessage(topic, batchNum); +// messageExtBrokerInner.setSysFlag(0); +// PutMessageResult putMessageResult = messageStore.putMessage(messageExtBrokerInner); +// Assert.assertEquals(PutMessageStatus.MESSAGE_ILLEGAL, putMessageResult.getPutMessageStatus()); + + // case 2 has PROPERTY_INNER_NUM and has INNER_BATCH_FLAG, but is not a batchCq +// MessageExtBrokerInner messageExtBrokerInner = buildMessage(topic, 1); +// PutMessageResult putMessageResult = messageStore.putMessage(messageExtBrokerInner); +// Assert.assertEquals(PutMessageStatus.MESSAGE_ILLEGAL, putMessageResult.getPutMessageStatus()); + + // case 3 has neither PROPERTY_INNER_NUM nor INNER_BATCH_FLAG. + MessageExtBrokerInner messageExtBrokerInner = buildMessage(topic, -1); + PutMessageResult putMessageResult = messageStore.putMessage(messageExtBrokerInner); + Assert.assertEquals(PutMessageStatus.PUT_OK, putMessageResult.getPutMessageStatus()); + } + + @Test + public void testSendMessagesToBcqTopic() { + String topic = UUID.randomUUID().toString(); + ConcurrentMap topicConfigTable = createTopicConfigTable(topic, CQType.BatchCQ); + this.topicConfigTableMap.putAll(topicConfigTable); + + // case 1 has PROPERTY_INNER_NUM but has no INNER_BATCH_FLAG +// MessageExtBrokerInner messageExtBrokerInner = buildMessage(topic, 1); +// PutMessageResult putMessageResult = messageStore.putMessage(messageExtBrokerInner); +// Assert.assertEquals(PutMessageStatus.MESSAGE_ILLEGAL, putMessageResult.getPutMessageStatus()); + + // case 2 has neither PROPERTY_INNER_NUM nor INNER_BATCH_FLAG. + MessageExtBrokerInner messageExtBrokerInner = buildMessage(topic, -1); + PutMessageResult putMessageResult = messageStore.putMessage(messageExtBrokerInner); + Assert.assertEquals(PutMessageStatus.PUT_OK, putMessageResult.getPutMessageStatus()); + + // case 3 has INNER_BATCH_FLAG but has no PROPERTY_INNER_NUM. + messageExtBrokerInner = buildMessage(topic, 1); + MessageAccessor.clearProperty(messageExtBrokerInner, MessageConst.PROPERTY_INNER_NUM); + messageExtBrokerInner.setSysFlag(MessageSysFlag.INNER_BATCH_FLAG); + putMessageResult = messageStore.putMessage(messageExtBrokerInner); + Assert.assertEquals(PutMessageStatus.PUT_OK, putMessageResult.getPutMessageStatus()); + } + + @Test + public void testConsumeBatchMessage() { + String topic = UUID.randomUUID().toString(); + ConcurrentMap topicConfigTable = createTopicConfigTable(topic, CQType.BatchCQ); + this.topicConfigTableMap.putAll(topicConfigTable); + int batchNum = 10; + + MessageExtBrokerInner messageExtBrokerInner = buildMessage(topic, batchNum); + PutMessageResult putMessageResult = messageStore.putMessage(messageExtBrokerInner); + Assert.assertEquals(PutMessageStatus.PUT_OK, putMessageResult.getPutMessageStatus()); + + await().atMost(5, SECONDS).until(fullyDispatched(messageStore)); + + List results = new ArrayList<>(); + for (int i = 0; i < batchNum; i++) { + GetMessageResult result = messageStore.getMessage("whatever", topic, 0, i, Integer.MAX_VALUE, Integer.MAX_VALUE, null); + try { + Assert.assertEquals(GetMessageStatus.FOUND, result.getStatus()); + results.add(result); + } finally { + result.release(); + } + } + + for (GetMessageResult result : results) { + Assert.assertEquals(0, result.getMinOffset()); + Assert.assertEquals(batchNum, result.getMaxOffset()); + } + + } + + @Test + public void testNextBeginOffsetConsumeBatchMessage() { + String topic = UUID.randomUUID().toString(); + ConcurrentMap topicConfigTable = createTopicConfigTable(topic, CQType.BatchCQ); + this.topicConfigTableMap.putAll(topicConfigTable); + Random random = new Random(); + int putMessageCount = 1000; + + Queue queue = new ArrayDeque<>(); + for (int i = 0; i < putMessageCount; i++) { + int batchNum = random.nextInt(1000) + 2; + MessageExtBrokerInner messageExtBrokerInner = buildMessage(topic, batchNum); + PutMessageResult putMessageResult = messageStore.putMessage(messageExtBrokerInner); + Assert.assertEquals(PutMessageStatus.PUT_OK, putMessageResult.getPutMessageStatus()); + queue.add(batchNum); + } + + await().atMost(5, SECONDS).until(fullyDispatched(messageStore)); + + long pullOffset = 0L; + int getMessageCount = 0; + int atMostMsgNum = 1; + while (true) { + GetMessageResult getMessageResult = messageStore.getMessage("group", topic, 0, pullOffset, atMostMsgNum, null); + if (Objects.equals(getMessageResult.getStatus(), GetMessageStatus.OFFSET_OVERFLOW_ONE)) { + break; + } + Assert.assertEquals(1, getMessageResult.getMessageQueueOffset().size()); + Long baseOffset = getMessageResult.getMessageQueueOffset().get(0); + Integer batchNum = queue.poll(); + Assert.assertNotNull(batchNum); + Assert.assertEquals(baseOffset + batchNum, getMessageResult.getNextBeginOffset()); + pullOffset = getMessageResult.getNextBeginOffset(); + getMessageCount++; + } + Assert.assertEquals(putMessageCount, getMessageCount); + } + + @Test + public void testGetOffsetInQueueByTime() throws Exception { + String topic = "testGetOffsetInQueueByTime"; + + ConcurrentMap topicConfigTable = createTopicConfigTable(topic, CQType.BatchCQ); + this.topicConfigTableMap.putAll(topicConfigTable); + Assert.assertTrue(QueueTypeUtils.isBatchCq(messageStore.getTopicConfig(topic))); + + // The initial min max offset, before and after the creation of consume queue + Assert.assertEquals(0, messageStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(-1, messageStore.getMinOffsetInQueue(topic, 0)); + + int batchNum = 10; + long timeMid = -1; + for (int i = 0; i < 19; i++) { + PutMessageResult putMessageResult = messageStore.putMessage(buildMessage(topic, batchNum)); + Assert.assertEquals(PutMessageStatus.PUT_OK, putMessageResult.getPutMessageStatus()); + Thread.sleep(2); + if (i == 7) + timeMid = System.currentTimeMillis(); + } + + await().atMost(5, SECONDS).until(fullyDispatched(messageStore)); + + Assert.assertEquals(80, messageStore.getOffsetInQueueByTime(topic, 0, timeMid)); + Assert.assertEquals(0, messageStore.getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(190, messageStore.getMaxOffsetInQueue(topic, 0)); + + int maxBatchDeleteFilesNum = messageStore.getMessageStoreConfig().getMaxBatchDeleteFilesNum(); + messageStore.getCommitLog().deleteExpiredFile(1L, 100, 12000, true, maxBatchDeleteFilesNum); + Assert.assertEquals(80, messageStore.getOffsetInQueueByTime(topic, 0, timeMid)); + + // can set periodic interval for executing DefaultMessageStore.this.cleanFilesPeriodically() method, we can execute following code. + // default periodic interval is 60s, This code snippet will take 60 seconds. + /*final long a = timeMid; + await().atMost(Duration.ofMinutes(2)).until(()->{ + long time = messageStore.getOffsetInQueueByTime(topic, 0, a); + return 180 ==time; + }); + Assert.assertEquals(180, messageStore.getOffsetInQueueByTime(topic, 0, timeMid));*/ + } + + @Test + public void testDispatchNormalConsumeQueue() throws Exception { + String topic = "TestDispatchBuildConsumeQueue"; + ConcurrentMap topicConfigTable = createTopicConfigTable(topic, CQType.SimpleCQ); + this.topicConfigTableMap.putAll(topicConfigTable); + + long timeStart = -1; + long timeMid = -1; + long commitLogMid = -1; + + for (int i = 0; i < 100; i++) { + MessageExtBrokerInner messageExtBrokerInner = buildMessage(topic, -1); + PutMessageResult putMessageResult = messageStore.putMessage(messageExtBrokerInner); + Assert.assertEquals(PutMessageStatus.PUT_OK, putMessageResult.getPutMessageStatus()); + + Thread.sleep(2); + if (i == 0) { + timeStart = putMessageResult.getAppendMessageResult().getStoreTimestamp(); + } + if (i == 50) { + timeMid = putMessageResult.getAppendMessageResult().getStoreTimestamp(); + commitLogMid = putMessageResult.getAppendMessageResult().getWroteOffset(); + } + + } + + await().atMost(5, SECONDS).until(fullyDispatched(messageStore)); + + ConsumeQueueInterface consumeQueue = messageStore.getConsumeQueue(topic, 0); + Assert.assertEquals(CQType.SimpleCQ, consumeQueue.getCQType()); + //check the consume queue + Assert.assertEquals(0, consumeQueue.getMinOffsetInQueue()); + Assert.assertEquals(0, consumeQueue.getOffsetInQueueByTime(0)); + Assert.assertEquals(50, consumeQueue.getOffsetInQueueByTime(timeMid)); + Assert.assertEquals(100, consumeQueue.getOffsetInQueueByTime(timeMid + Integer.MAX_VALUE)); + Assert.assertEquals(100, consumeQueue.getMaxOffsetInQueue()); + //check the messagestore + Assert.assertEquals(100, messageStore.getMessageTotalInQueue(topic, 0)); + Assert.assertEquals(consumeQueue.getMinOffsetInQueue(), messageStore.getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(consumeQueue.getMaxOffsetInQueue(), messageStore.getMaxOffsetInQueue(topic, 0)); + for (int i = -100; i < 100; i += 20) { + Assert.assertEquals(consumeQueue.getOffsetInQueueByTime(timeMid + i), messageStore.getOffsetInQueueByTime(topic, 0, timeMid + i)); + } + + //check the message time + long earliestMessageTime = messageStore.getEarliestMessageTime(topic, 0); + Assert.assertEquals(timeStart, earliestMessageTime); + long messageStoreTime = messageStore.getMessageStoreTimeStamp(topic, 0, 50); + Assert.assertEquals(timeMid, messageStoreTime); + long commitLogOffset = messageStore.getCommitLogOffsetInQueue(topic, 0, 50); + Assert.assertTrue(commitLogOffset >= messageStore.getMinPhyOffset()); + Assert.assertTrue(commitLogOffset <= messageStore.getMaxPhyOffset()); + Assert.assertEquals(commitLogMid, commitLogOffset); + + Assert.assertTrue(messageStore.checkInMemByConsumeOffset(topic, 0, 50, 1)); + } + + @Test + public void testDispatchBuildBatchConsumeQueue() throws Exception { + String topic = "testDispatchBuildBatchConsumeQueue"; + int batchNum = 10; + long timeStart = -1; + long timeMid = -1; + + ConcurrentMap topicConfigTable = createTopicConfigTable(topic, CQType.BatchCQ); + this.topicConfigTableMap.putAll(topicConfigTable); + + for (int i = 0; i < 100; i++) { + PutMessageResult putMessageResult = messageStore.putMessage(buildMessage(topic, batchNum)); + Assert.assertEquals(PutMessageStatus.PUT_OK, putMessageResult.getPutMessageStatus()); + Thread.sleep(2); + if (i == 0) { + timeStart = putMessageResult.getAppendMessageResult().getStoreTimestamp(); + } + if (i == 30) { + timeMid = putMessageResult.getAppendMessageResult().getStoreTimestamp(); + } + + } + + await().atMost(5, SECONDS).until(fullyDispatched(messageStore)); + + ConsumeQueueInterface consumeQueue = messageStore.getConsumeQueue(topic, 0); + Assert.assertEquals(CQType.BatchCQ, consumeQueue.getCQType()); + + Assert.assertEquals(0, consumeQueue.getMinOffsetInQueue()); + Assert.assertEquals(1000, consumeQueue.getMaxOffsetInQueue()); + + //check the message store + Assert.assertEquals(1000, messageStore.getMessageTotalInQueue(topic, 0)); + Assert.assertEquals(consumeQueue.getMinOffsetInQueue(), messageStore.getMinOffsetInQueue(topic, 0)); + Assert.assertEquals(consumeQueue.getMaxOffsetInQueue(), messageStore.getMaxOffsetInQueue(topic, 0)); + for (int i = -100; i < 100; i += 20) { + Assert.assertEquals(consumeQueue.getOffsetInQueueByTime(timeMid + i), messageStore.getOffsetInQueueByTime(topic, 0, timeMid + i)); + } + + //check the message time + long earliestMessageTime = messageStore.getEarliestMessageTime(topic, 0); + Assert.assertEquals(earliestMessageTime, timeStart); + long messageStoreTime = messageStore.getMessageStoreTimeStamp(topic, 0, 300); + Assert.assertEquals(messageStoreTime, timeMid); + long commitLogOffset = messageStore.getCommitLogOffsetInQueue(topic, 0, 300); + Assert.assertTrue(commitLogOffset >= messageStore.getMinPhyOffset()); + Assert.assertTrue(commitLogOffset <= messageStore.getMaxPhyOffset()); + + Assert.assertTrue(messageStore.checkInMemByConsumeOffset(topic, 0, 300, 1)); + + //get the message Normally + GetMessageResult getMessageResult = messageStore.getMessage("group", topic, 0, 0, 10 * batchNum, null); + Assert.assertEquals(10, getMessageResult.getMessageMapedList().size()); + for (int i = 0; i < 10; i++) { + SelectMappedBufferResult sbr = getMessageResult.getMessageMapedList().get(i); + MessageExt messageExt = MessageDecoder.decode(sbr.getByteBuffer()); + short tmpBatchNum = Short.parseShort(messageExt.getProperty(MessageConst.PROPERTY_INNER_NUM)); + Assert.assertEquals(i * batchNum, Long.parseLong(messageExt.getProperty(MessageConst.PROPERTY_INNER_BASE))); + Assert.assertEquals(batchNum, tmpBatchNum); + } + } + + @Test + public void testGetBatchMessageWithinNumber() { + String topic = UUID.randomUUID().toString(); + + ConcurrentMap topicConfigTable = createTopicConfigTable(topic, CQType.BatchCQ); + this.topicConfigTableMap.putAll(topicConfigTable); + + int batchNum = 20; + for (int i = 0; i < 200; i++) { + PutMessageResult putMessageResult = messageStore.putMessage(buildMessage(topic, batchNum)); + Assert.assertEquals(PutMessageStatus.PUT_OK, putMessageResult.getPutMessageStatus()); + Assert.assertEquals(i * batchNum, putMessageResult.getAppendMessageResult().getLogicsOffset()); + Assert.assertEquals(batchNum, putMessageResult.getAppendMessageResult().getMsgNum()); + } + + await().atMost(5, SECONDS).until(fullyDispatched(messageStore)); + + ConsumeQueueInterface consumeQueue = messageStore.getConsumeQueue(topic, 0); + Assert.assertEquals(CQType.BatchCQ, consumeQueue.getCQType()); + Assert.assertEquals(0, consumeQueue.getMinOffsetInQueue()); + Assert.assertEquals(200 * batchNum, consumeQueue.getMaxOffsetInQueue()); + + { + GetMessageResult getMessageResult = messageStore.getMessage("group", topic, 0, 5, 1, Integer.MAX_VALUE, null); + Assert.assertEquals(GetMessageStatus.FOUND, getMessageResult.getStatus()); + Assert.assertEquals(batchNum, getMessageResult.getNextBeginOffset()); + Assert.assertEquals(1, getMessageResult.getMessageMapedList().size()); + Assert.assertEquals(batchNum, getMessageResult.getMessageCount()); + SelectMappedBufferResult sbr = getMessageResult.getMessageMapedList().get(0); + MessageExt messageExt = MessageDecoder.decode(sbr.getByteBuffer()); + short tmpBatchNum = Short.parseShort(messageExt.getProperty(MessageConst.PROPERTY_INNER_NUM)); + Assert.assertEquals(0, messageExt.getQueueOffset()); + Assert.assertEquals(batchNum, tmpBatchNum); + } + { + GetMessageResult getMessageResult = messageStore.getMessage("group", topic, 0, 5, 39, Integer.MAX_VALUE, null); + Assert.assertEquals(GetMessageStatus.FOUND, getMessageResult.getStatus()); + Assert.assertEquals(1, getMessageResult.getMessageMapedList().size()); + Assert.assertEquals(batchNum, getMessageResult.getNextBeginOffset()); + Assert.assertEquals(batchNum, getMessageResult.getMessageCount()); + + } + + { + GetMessageResult getMessageResult = messageStore.getMessage("group", topic, 0, 5, 60, Integer.MAX_VALUE, null); + Assert.assertEquals(GetMessageStatus.FOUND, getMessageResult.getStatus()); + Assert.assertEquals(3, getMessageResult.getMessageMapedList().size()); + Assert.assertEquals(3 * batchNum, getMessageResult.getNextBeginOffset()); + Assert.assertEquals(3 * batchNum, getMessageResult.getMessageCount()); + for (int i = 0; i < getMessageResult.getMessageBufferList().size(); i++) { + Assert.assertFalse(getMessageResult.getMessageMapedList().get(i).hasReleased()); + SelectMappedBufferResult sbr = getMessageResult.getMessageMapedList().get(i); + MessageExt messageExt = MessageDecoder.decode(sbr.getByteBuffer()); + Assert.assertNotNull(messageExt); + short innerBatchNum = Short.parseShort(messageExt.getProperty(MessageConst.PROPERTY_INNER_NUM)); + Assert.assertEquals(i * batchNum, Long.parseLong(messageExt.getProperty(MessageConst.PROPERTY_INNER_BASE))); + Assert.assertEquals(batchNum, innerBatchNum); + + } + } + } + + @Test + public void testGetBatchMessageWithinSize() { + String topic = UUID.randomUUID().toString(); + ConcurrentMap topicConfigTable = createTopicConfigTable(topic, CQType.BatchCQ); + this.topicConfigTableMap.putAll(topicConfigTable); + + int batchNum = 10; + for (int i = 0; i < 100; i++) { + PutMessageResult putMessageResult = messageStore.putMessage(buildMessage(topic, batchNum)); + Assert.assertEquals(PutMessageStatus.PUT_OK, putMessageResult.getPutMessageStatus()); + Assert.assertEquals(i * 10, putMessageResult.getAppendMessageResult().getLogicsOffset()); + Assert.assertEquals(batchNum, putMessageResult.getAppendMessageResult().getMsgNum()); + } + + await().atMost(5, SECONDS).until(fullyDispatched(messageStore)); + + ConsumeQueueInterface consumeQueue = messageStore.getConsumeQueue(topic, 0); + Assert.assertEquals(CQType.BatchCQ, consumeQueue.getCQType()); + Assert.assertEquals(0, consumeQueue.getMinOffsetInQueue()); + Assert.assertEquals(1000, consumeQueue.getMaxOffsetInQueue()); + + { + GetMessageResult getMessageResult = messageStore.getMessage("group", topic, 0, 5, Integer.MAX_VALUE, 100, null); + Assert.assertEquals(GetMessageStatus.FOUND, getMessageResult.getStatus()); + Assert.assertEquals(10, getMessageResult.getNextBeginOffset()); + Assert.assertEquals(1, getMessageResult.getMessageMapedList().size()); + SelectMappedBufferResult sbr = getMessageResult.getMessageMapedList().get(0); + MessageExt messageExt = MessageDecoder.decode(sbr.getByteBuffer()); + short tmpBatchNum = Short.parseShort(messageExt.getProperty(MessageConst.PROPERTY_INNER_NUM)); + Assert.assertEquals(0, messageExt.getQueueOffset()); + Assert.assertEquals(batchNum, tmpBatchNum); + } + { + GetMessageResult getMessageResult = messageStore.getMessage("group", topic, 0, 5, Integer.MAX_VALUE, 2048, null); + Assert.assertEquals(GetMessageStatus.FOUND, getMessageResult.getStatus()); + Assert.assertEquals(1, getMessageResult.getMessageMapedList().size()); + Assert.assertEquals(10, getMessageResult.getNextBeginOffset()); + + } + + { + GetMessageResult getMessageResult = messageStore.getMessage("group", topic, 0, 5, Integer.MAX_VALUE, 4096, null); + Assert.assertEquals(GetMessageStatus.FOUND, getMessageResult.getStatus()); + Assert.assertEquals(3, getMessageResult.getMessageMapedList().size()); + Assert.assertEquals(30, getMessageResult.getNextBeginOffset()); + for (int i = 0; i < getMessageResult.getMessageBufferList().size(); i++) { + Assert.assertFalse(getMessageResult.getMessageMapedList().get(i).hasReleased()); + SelectMappedBufferResult sbr = getMessageResult.getMessageMapedList().get(i); + MessageExt messageExt = MessageDecoder.decode(sbr.getByteBuffer()); + short tmpBatchNum = Short.parseShort(messageExt.getProperty(MessageConst.PROPERTY_INNER_NUM)); + Assert.assertEquals(i * batchNum, Long.parseLong(messageExt.getProperty(MessageConst.PROPERTY_INNER_BASE))); + Assert.assertEquals(batchNum, tmpBatchNum); + + } + } + } + + protected void putMsg(String topic) { + ConcurrentMap topicConfigTable = createTopicConfigTable(topic, CQType.BatchCQ); + this.topicConfigTableMap.putAll(topicConfigTable); + + for (int i = 0; i < TOTAL_MSGS; i++) { + MessageExtBrokerInner message = buildMessage(topic, BATCH_NUM * (i % 2 + 1)); + switch (i % 3) { + case 0: + message.setTags("TagA"); + break; + + case 1: + message.setTags("TagB"); + break; + } + message.setTagsCode(message.getTags().hashCode()); + message.setPropertiesString(MessageDecoder.messageProperties2String(message.getProperties())); + PutMessageResult putMessageResult = messageStore.putMessage(message); + Assert.assertEquals(PutMessageStatus.PUT_OK, putMessageResult.getPutMessageStatus()); + } + + await().atMost(5, SECONDS).until(fullyDispatched(messageStore)); + } + + @Test + public void testEstimateMessageCountInEmptyConsumeQueue() { + String topic = UUID.randomUUID().toString(); + ConsumeQueueInterface consumeQueue = messageStore.findConsumeQueue(topic, 0); + MessageFilter filter = new MessageFilter() { + @Override + public boolean isMatchedByConsumeQueue(Long tagsCode, ConsumeQueueExt.CqExtUnit cqExtUnit) { + return tagsCode == "TagA".hashCode(); + } + + @Override + public boolean isMatchedByCommitLog(ByteBuffer msgBuffer, Map properties) { + return false; + } + }; + long estimation = consumeQueue.estimateMessageCount(0, 0, filter); + Assert.assertEquals(-1, estimation); + + // test for illegal offset + estimation = consumeQueue.estimateMessageCount(0, 100, filter); + Assert.assertEquals(-1, estimation); + estimation = consumeQueue.estimateMessageCount(100, 1000, filter); + Assert.assertEquals(-1, estimation); + } + + @Test + public void testEstimateMessageCount() { + String topic = UUID.randomUUID().toString(); + putMsg(topic); + ConsumeQueueInterface cq = messageStore.findConsumeQueue(topic, 0); + MessageFilter filter = new MessageFilter() { + @Override + public boolean isMatchedByConsumeQueue(Long tagsCode, ConsumeQueueExt.CqExtUnit cqExtUnit) { + return tagsCode == "TagA".hashCode(); + } + + @Override + public boolean isMatchedByCommitLog(ByteBuffer msgBuffer, Map properties) { + return false; + } + }; + long estimation = cq.estimateMessageCount(0, 2999, filter); + Assert.assertEquals(1000, estimation); + + // test for illegal offset + estimation = cq.estimateMessageCount(0, Long.MAX_VALUE, filter); + Assert.assertEquals(-1, estimation); + estimation = cq.estimateMessageCount(100000, 1000000, filter); + Assert.assertEquals(-1, estimation); + estimation = cq.estimateMessageCount(100, 0, filter); + Assert.assertEquals(-1, estimation); + } + + @Test + public void testEstimateMessageCountSample() { + String topic = UUID.randomUUID().toString(); + putMsg(topic); + messageStore.getMessageStoreConfig().setSampleCountThreshold(10); + messageStore.getMessageStoreConfig().setMaxConsumeQueueScan(20); + ConsumeQueueInterface cq = messageStore.findConsumeQueue(topic, 0); + MessageFilter filter = new MessageFilter() { + @Override + public boolean isMatchedByConsumeQueue(Long tagsCode, ConsumeQueueExt.CqExtUnit cqExtUnit) { + return tagsCode == "TagA".hashCode(); + } + + @Override + public boolean isMatchedByCommitLog(ByteBuffer msgBuffer, Map properties) { + return false; + } + }; + long estimation = cq.estimateMessageCount(1000, 2000, filter); + Assert.assertEquals(300, estimation); + } +} diff --git a/store/src/test/java/org/apache/rocketmq/store/queue/BatchConsumeQueueTest.java b/store/src/test/java/org/apache/rocketmq/store/queue/BatchConsumeQueueTest.java new file mode 100644 index 00000000000..c6525bd8365 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/queue/BatchConsumeQueueTest.java @@ -0,0 +1,306 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.queue; + +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.store.ConsumeQueue; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.apache.rocketmq.store.StoreTestBase; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.stats.BrokerStatsManager; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import static java.lang.String.format; + +public class BatchConsumeQueueTest extends StoreTestBase { + + List batchConsumeQueues = new ArrayList<>(); + + private BatchConsumeQueue createBatchConsume(String path) { + if (path == null) { + path = createBaseDir(); + } + baseDirs.add(path); + MessageStore messageStore = null; + try { + messageStore = createMessageStore(null); + } catch (Exception e) { + Assert.fail(); + } + BatchConsumeQueue batchConsumeQueue = new BatchConsumeQueue("topic", 0, path, fileSize, messageStore); + batchConsumeQueues.add(batchConsumeQueue); + return batchConsumeQueue; + } + + private int fileSize = BatchConsumeQueue.CQ_STORE_UNIT_SIZE * 20; + + @Test(timeout = 20000) + public void testBuildAndIterateBatchConsumeQueue() { + BatchConsumeQueue batchConsumeQueue = createBatchConsume(null); + batchConsumeQueue.load(); + short batchNum = 10; + int unitNum = 10000; + int initialMsgOffset = 1000; + for (int i = 0; i < unitNum; i++) { + batchConsumeQueue.putBatchMessagePositionInfo(i, 1024, 111, i * batchNum, i * batchNum + initialMsgOffset, batchNum); + } + Assert.assertEquals(500, getBcqFileSize(batchConsumeQueue)); + Assert.assertEquals(initialMsgOffset + batchNum * unitNum, batchConsumeQueue.getMaxOffsetInQueue()); + Assert.assertEquals(initialMsgOffset, batchConsumeQueue.getMinOffsetInQueue()); + + { + CqUnit first = batchConsumeQueue.getEarliestUnit(); + Assert.assertNotNull(first); + Assert.assertEquals(initialMsgOffset, first.getQueueOffset()); + Assert.assertEquals(batchNum, first.getBatchNum()); + } + + { + CqUnit last = batchConsumeQueue.getLatestUnit(); + Assert.assertNotNull(last); + Assert.assertEquals(initialMsgOffset + batchNum * unitNum - batchNum, last.getQueueOffset()); + Assert.assertEquals(batchNum, last.getBatchNum()); + } + + for (int i = 0; i < initialMsgOffset + batchNum * unitNum + 10; i++) { + ReferredIterator it = batchConsumeQueue.iterateFrom(i); + if (i < initialMsgOffset || i >= initialMsgOffset + batchNum * unitNum) { + Assert.assertNull(it); + continue; + } + Assert.assertNotNull(it); + CqUnit cqUnit = it.nextAndRelease(); + Assert.assertNotNull(cqUnit); + + long baseOffset = (i / batchNum) * batchNum; + Assert.assertEquals(baseOffset, cqUnit.getQueueOffset()); + Assert.assertEquals(batchNum, cqUnit.getBatchNum()); + + Assert.assertEquals((i - initialMsgOffset) / batchNum, cqUnit.getPos()); + Assert.assertEquals(1024, cqUnit.getSize()); + Assert.assertEquals(111, cqUnit.getTagsCode()); + Assert.assertNull(cqUnit.getCqExtUnit()); + } + batchConsumeQueue.destroy(); + } + + @Test(timeout = 20000) + public void testBuildAndSearchBatchConsumeQueue() { + // Preparing the data may take some time + BatchConsumeQueue batchConsumeQueue = createBatchConsume(null); + batchConsumeQueue.load(); + short batchSize = 10; + int unitNum = 20000; + for (int i = 0; i < unitNum; i++) { + batchConsumeQueue.putBatchMessagePositionInfo(i, 100, 0, i * batchSize, i * batchSize + 1, batchSize); + } + Assert.assertEquals(1000, getBcqFileSize(batchConsumeQueue)); + Assert.assertEquals(unitNum * batchSize + 1, batchConsumeQueue.getMaxOffsetInQueue()); + Assert.assertEquals(1, batchConsumeQueue.getMinOffsetInQueue()); + batchConsumeQueue.reviseMaxAndMinOffsetInQueue(); + Assert.assertEquals(unitNum * batchSize + 1, batchConsumeQueue.getMaxOffsetInQueue()); + Assert.assertEquals(1, batchConsumeQueue.getMinOffsetInQueue()); + + // test search the offset + // lower bounds + Assert.assertFalse(ableToFindResult(batchConsumeQueue, 0)); + Assert.assertTrue(ableToFindResult(batchConsumeQueue, 1)); + // upper bounds + Assert.assertFalse(ableToFindResult(batchConsumeQueue, unitNum * batchSize + 1)); + Assert.assertTrue(ableToFindResult(batchConsumeQueue, unitNum * batchSize)); + // iterate every possible batch-msg offset + for (int i = 1; i <= unitNum * batchSize; i++) { + int expectedValue = ((i - 1) / batchSize) * batchSize + 1; + SelectMappedBufferResult batchMsgIndexBuffer = batchConsumeQueue.getBatchMsgIndexBuffer(i); + Assert.assertEquals(expectedValue, batchMsgIndexBuffer.getByteBuffer().getLong(BatchConsumeQueue.MSG_BASE_OFFSET_INDEX)); + batchMsgIndexBuffer.release(); + } + SelectMappedBufferResult sbr = batchConsumeQueue.getBatchMsgIndexBuffer(501); + Assert.assertEquals(501, sbr.getByteBuffer().getLong(BatchConsumeQueue.MSG_BASE_OFFSET_INDEX)); + Assert.assertEquals(10, sbr.getByteBuffer().getShort(BatchConsumeQueue.MSG_BASE_OFFSET_INDEX + 8)); + sbr.release(); + + // test search the storeTime + Assert.assertEquals(1, batchConsumeQueue.getOffsetInQueueByTime(-100)); + Assert.assertEquals(1, batchConsumeQueue.getOffsetInQueueByTime(0)); + Assert.assertEquals(11, batchConsumeQueue.getOffsetInQueueByTime(1)); + for (int i = 0; i < unitNum; i++) { + int storeTime = i * batchSize; + int expectedOffset = storeTime + 1; + long offset = batchConsumeQueue.getOffsetInQueueByTime(storeTime); + Assert.assertEquals(expectedOffset, offset); + } + Assert.assertEquals(199991, batchConsumeQueue.getOffsetInQueueByTime(System.currentTimeMillis())); + batchConsumeQueue.destroy(); + } + + @Test(timeout = 20000) + public void testBuildAndRecoverBatchConsumeQueue() { + String tmpPath = createBaseDir(); + short batchSize = 10; + { + BatchConsumeQueue batchConsumeQueue = createBatchConsume(tmpPath); + batchConsumeQueue.load(); + for (int i = 0; i < 100; i++) { + batchConsumeQueue.putBatchMessagePositionInfo(i, 100, 0, i * batchSize, i * batchSize + 1, batchSize); + } + Assert.assertEquals(5, getBcqFileSize(batchConsumeQueue)); + Assert.assertEquals(1001, batchConsumeQueue.getMaxOffsetInQueue()); + Assert.assertEquals(1, batchConsumeQueue.getMinOffsetInQueue()); + + for (int i = 0; i < 10; i++) { + batchConsumeQueue.flush(0); + } + } + { + BatchConsumeQueue recover = createBatchConsume(tmpPath); + recover.load(); + recover.recover(); + Assert.assertEquals(5, getBcqFileSize(recover)); + Assert.assertEquals(1001, recover.getMaxOffsetInQueue()); + Assert.assertEquals(1, recover.getMinOffsetInQueue()); + for (int i = 1; i <= 1000; i++) { + int expectedValue = ((i - 1) / batchSize) * batchSize + 1; + SelectMappedBufferResult batchMsgIndexBuffer = recover.getBatchMsgIndexBuffer(i); + Assert.assertEquals(expectedValue, batchMsgIndexBuffer.getByteBuffer().getLong(BatchConsumeQueue.MSG_BASE_OFFSET_INDEX)); + batchMsgIndexBuffer.release(); + } + } + } + + @Test(timeout = 20000) + public void testTruncateBatchConsumeQueue() { + String tmpPath = createBaseDir(); + BatchConsumeQueue batchConsumeQueue = createBatchConsume(tmpPath); + batchConsumeQueue.load(); + short batchSize = 10; + int unitNum = 20000; + for (int i = 0; i < unitNum; i++) { + batchConsumeQueue.putBatchMessagePositionInfo(i, 100, 0, i * batchSize, i * batchSize + 1, batchSize); + } + Assert.assertEquals(1000, getBcqFileSize(batchConsumeQueue)); + Assert.assertEquals(unitNum * batchSize + 1, batchConsumeQueue.getMaxOffsetInQueue()); + Assert.assertEquals(1, batchConsumeQueue.getMinOffsetInQueue()); + + int truncatePhyOffset = new Random().nextInt(unitNum); + batchConsumeQueue.truncateDirtyLogicFiles(truncatePhyOffset); + + for (int i = 1; i < unitNum; i++) { + long msgOffset = i * batchSize + 1; + if (i < truncatePhyOffset) { + int expectedValue = ((i - 1) / batchSize) * batchSize + 1; + SelectMappedBufferResult batchMsgIndexBuffer = batchConsumeQueue.getBatchMsgIndexBuffer(i); + Assert.assertEquals(expectedValue, batchMsgIndexBuffer.getByteBuffer().getLong(BatchConsumeQueue.MSG_BASE_OFFSET_INDEX)); + batchMsgIndexBuffer.release(); + } else { + Assert.assertNull(format("i: %d, truncatePhyOffset: %d", i, truncatePhyOffset), batchConsumeQueue.getBatchMsgIndexBuffer(msgOffset)); + } + } + } + + @Test + public void testTruncateAndDeleteBatchConsumeQueue() { + String tmpPath = createBaseDir(); + BatchConsumeQueue batchConsumeQueue = createBatchConsume(tmpPath); + batchConsumeQueue.load(); + short batchSize = 10; + for (int i = 0; i < 100; i++) { + batchConsumeQueue.putBatchMessagePositionInfo(i, 100, 0, i * batchSize, i * batchSize + 1, batchSize); + } + Assert.assertEquals(5, batchConsumeQueue.mappedFileQueue.getMappedFiles().size()); + Assert.assertEquals(1001, batchConsumeQueue.getMaxOffsetInQueue()); + Assert.assertEquals(1, batchConsumeQueue.getMinOffsetInQueue()); + + batchConsumeQueue.truncateDirtyLogicFiles(80); + + Assert.assertEquals(4, batchConsumeQueue.mappedFileQueue.getMappedFiles().size()); + Assert.assertEquals(801, batchConsumeQueue.getMaxOffsetInQueue()); + Assert.assertEquals(1, batchConsumeQueue.getMinOffsetInQueue()); + + //test + batchConsumeQueue.deleteExpiredFile(30); + Assert.assertEquals(3, batchConsumeQueue.mappedFileQueue.getMappedFiles().size()); + Assert.assertEquals(801, batchConsumeQueue.getMaxOffsetInQueue()); + Assert.assertEquals(301, batchConsumeQueue.getMinOffsetInQueue()); + + } + + @After + @Override + public void clear() { + super.clear(); + for (BatchConsumeQueue batchConsumeQueue : batchConsumeQueues) { + batchConsumeQueue.destroy(); + } + } + + private int getBcqFileSize(BatchConsumeQueue batchConsumeQueue) { + return batchConsumeQueue.mappedFileQueue.getMappedFiles().size(); + } + + private boolean ableToFindResult(BatchConsumeQueue batchConsumeQueue, long msgOffset) { + SelectMappedBufferResult batchMsgIndexBuffer = batchConsumeQueue.getBatchMsgIndexBuffer(msgOffset); + try { + return batchMsgIndexBuffer != null; + } finally { + if (batchMsgIndexBuffer != null) { + batchMsgIndexBuffer.release(); + } + } + } + + protected MessageStore createMessageStore(String baseDir) throws Exception { + if (baseDir == null) { + baseDir = createBaseDir(); + } + baseDirs.add(baseDir); + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setMappedFileSizeCommitLog(1024 * 8); + messageStoreConfig.setMappedFileSizeConsumeQueue(100 * ConsumeQueue.CQ_STORE_UNIT_SIZE); + messageStoreConfig.setMapperFileSizeBatchConsumeQueue(20 * BatchConsumeQueue.CQ_STORE_UNIT_SIZE); + messageStoreConfig.setMappedFileSizeConsumeQueueExt(1024); + messageStoreConfig.setMaxIndexNum(100 * 10); + messageStoreConfig.setEnableConsumeQueueExt(false); + messageStoreConfig.setStorePathRootDir(baseDir); + messageStoreConfig.setStorePathCommitLog(baseDir + File.separator + "commitlog"); + messageStoreConfig.setHaListenPort(nextPort()); + messageStoreConfig.setMaxTransferBytesOnMessageInDisk(1024 * 1024); + messageStoreConfig.setMaxTransferBytesOnMessageInMemory(1024 * 1024); + messageStoreConfig.setMaxTransferCountOnMessageInDisk(1024); + messageStoreConfig.setMaxTransferCountOnMessageInMemory(1024); + messageStoreConfig.setSearchBcqByCacheEnable(true); + + return new DefaultMessageStore( + messageStoreConfig, + new BrokerStatsManager("simpleTest", true), + (topic, queueId, logicOffset, tagsCode, msgStoreTime, filterBitMap, properties) -> { + }, + new BrokerConfig(), new ConcurrentHashMap<>()); + } + +} diff --git a/store/src/test/java/org/apache/rocketmq/store/queue/ConsumeQueueStoreTest.java b/store/src/test/java/org/apache/rocketmq/store/queue/ConsumeQueueStoreTest.java new file mode 100644 index 00000000000..59e1d08791f --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/queue/ConsumeQueueStoreTest.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.queue; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.attribute.CQType; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.util.UUID; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; + +public class ConsumeQueueStoreTest extends QueueTestBase { + private MessageStore messageStore; + private ConcurrentMap topicConfigTableMap; + + + + @Before + public void init() throws Exception { + this.topicConfigTableMap = new ConcurrentHashMap<>(); + messageStore = createMessageStore(null, true, topicConfigTableMap); + messageStore.load(); + messageStore.start(); + } + + @After + public void destroy() { + messageStore.shutdown(); + messageStore.destroy(); + + File file = new File(messageStore.getMessageStoreConfig().getStorePathRootDir()); + UtilAll.deleteFile(file); + } + + @Test + public void testLoadConsumeQueuesWithWrongAttribute() { + String normalTopic = UUID.randomUUID().toString(); + ConcurrentMap topicConfigTable = createTopicConfigTable(normalTopic, CQType.SimpleCQ); + this.topicConfigTableMap.putAll(topicConfigTable); + + for (int i = 0; i < 10; i++) { + PutMessageResult putMessageResult = messageStore.putMessage(buildMessage(normalTopic, -1)); + assertEquals(PutMessageStatus.PUT_OK, putMessageResult.getPutMessageStatus()); + } + + await().atMost(5, SECONDS).until(fullyDispatched(messageStore)); + + // simulate delete topic but with files left. + this.topicConfigTableMap.clear(); + + topicConfigTable = createTopicConfigTable(normalTopic, CQType.BatchCQ); + this.topicConfigTableMap.putAll(topicConfigTable); + + RuntimeException runtimeException = Assert.assertThrows(RuntimeException.class, () -> messageStore.getQueueStore().load()); + Assert.assertTrue(runtimeException.getMessage().endsWith("should be SimpleCQ, but is BatchCQ")); + } + + @Test + public void testLoadBatchConsumeQueuesWithWrongAttribute() { + String batchTopic = UUID.randomUUID().toString(); + ConcurrentMap topicConfigTable = createTopicConfigTable(batchTopic, CQType.BatchCQ); + this.topicConfigTableMap.putAll(topicConfigTable); + + for (int i = 0; i < 10; i++) { + PutMessageResult putMessageResult = messageStore.putMessage(buildMessage(batchTopic, 10)); + assertEquals(PutMessageStatus.PUT_OK, putMessageResult.getPutMessageStatus()); + } + + await().atMost(5, SECONDS).until(fullyDispatched(messageStore)); + + // simulate delete topic but with files left. + this.topicConfigTableMap.clear(); + + topicConfigTable = createTopicConfigTable(batchTopic, CQType.SimpleCQ); + this.topicConfigTableMap.putAll(topicConfigTable); + messageStore.shutdown(); + + RuntimeException runtimeException = Assert.assertThrows(RuntimeException.class, () -> messageStore.getQueueStore().load()); + Assert.assertTrue(runtimeException.getMessage().endsWith("should be BatchCQ, but is SimpleCQ")); + } + +} diff --git a/store/src/test/java/org/apache/rocketmq/store/queue/ConsumeQueueTest.java b/store/src/test/java/org/apache/rocketmq/store/queue/ConsumeQueueTest.java new file mode 100644 index 00000000000..c3c8be52ddd --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/queue/ConsumeQueueTest.java @@ -0,0 +1,306 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.queue; + +import java.io.File; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.common.attribute.CQType; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.store.ConsumeQueueExt; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.DispatchRequest; +import org.apache.rocketmq.store.MessageFilter; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.stats.BrokerStatsManager; +import org.junit.Assert; +import org.junit.Test; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.awaitility.Awaitility.await; + +public class ConsumeQueueTest extends QueueTestBase { + + private static final String TOPIC = "StoreTest"; + private static final int QUEUE_ID = 0; + private static final String STORE_PATH = "." + File.separator + "unit_test_store"; + private static final int COMMIT_LOG_FILE_SIZE = 1024 * 8; + private static final int CQ_FILE_SIZE = 10 * 20; + private static final int CQ_EXT_FILE_SIZE = 10 * (ConsumeQueueExt.CqExtUnit.MIN_EXT_UNIT_SIZE + 64); + + public MessageStoreConfig buildStoreConfig(int commitLogFileSize, int cqFileSize, + boolean enableCqExt, int cqExtFileSize) { + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setMappedFileSizeCommitLog(commitLogFileSize); + messageStoreConfig.setMappedFileSizeConsumeQueue(cqFileSize); + messageStoreConfig.setMappedFileSizeConsumeQueueExt(cqExtFileSize); + messageStoreConfig.setMessageIndexEnable(false); + messageStoreConfig.setEnableConsumeQueueExt(enableCqExt); + + messageStoreConfig.setStorePathRootDir(STORE_PATH); + messageStoreConfig.setStorePathCommitLog(STORE_PATH + File.separator + "commitlog"); + + return messageStoreConfig; + } + + protected DefaultMessageStore gen() throws Exception { + MessageStoreConfig messageStoreConfig = buildStoreConfig( + COMMIT_LOG_FILE_SIZE, CQ_FILE_SIZE, true, CQ_EXT_FILE_SIZE + ); + + BrokerConfig brokerConfig = new BrokerConfig(); + + DefaultMessageStore master = new DefaultMessageStore( + messageStoreConfig, new BrokerStatsManager(brokerConfig), + (topic, queueId, logicOffset, tagsCode, msgStoreTime, filterBitMap, properties) -> { + }, brokerConfig, new ConcurrentHashMap<>()); + + assertThat(master.load()).isTrue(); + + master.start(); + + return master; + } + + protected void putMsg(DefaultMessageStore messageStore) throws Exception { + int totalMsgs = 200; + for (int i = 0; i < totalMsgs; i++) { + MessageExtBrokerInner message = buildMessage(); + message.setQueueId(0); + switch (i % 3) { + case 0: + message.setTags("TagA"); + break; + + case 1: + message.setTags("TagB"); + break; + + case 2: + message.setTags("TagC"); + break; + } + message.setTagsCode(message.getTags().hashCode()); + message.setPropertiesString(MessageDecoder.messageProperties2String(message.getProperties())); + messageStore.putMessage(message); + } + await().atMost(5, SECONDS).until(fullyDispatched(messageStore)); + } + + @Test + public void testIterator() throws Exception { + final int msgNum = 100; + final int msgSize = 1000; + MessageStore messageStore = createMessageStore(null, true, null); + messageStore.load(); + String topic = UUID.randomUUID().toString(); + //The initial min max offset, before and after the creation of consume queue + Assert.assertEquals(0, messageStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, messageStore.getMinOffsetInQueue(topic, 0)); + + ConsumeQueueInterface consumeQueue = messageStore.getConsumeQueue(topic, 0); + Assert.assertEquals(CQType.SimpleCQ, consumeQueue.getCQType()); + Assert.assertEquals(0, consumeQueue.getMaxOffsetInQueue()); + Assert.assertEquals(0, consumeQueue.getMinOffsetInQueue()); + Assert.assertEquals(0, messageStore.getMaxOffsetInQueue(topic, 0)); + Assert.assertEquals(0, messageStore.getMinOffsetInQueue(topic, 0)); + for (int i = 0; i < msgNum; i++) { + DispatchRequest request = new DispatchRequest(consumeQueue.getTopic(), consumeQueue.getQueueId(), i * msgSize, msgSize, i, + System.currentTimeMillis(), i, null, null, 0, 0, null); + request.setBitMap(new byte[10]); + messageStore.getQueueStore().putMessagePositionInfoWrapper(consumeQueue, request); + } + Assert.assertEquals(0, consumeQueue.getMinLogicOffset()); + Assert.assertEquals(0, consumeQueue.getMinOffsetInQueue()); + Assert.assertEquals(msgNum, consumeQueue.getMaxOffsetInQueue()); + Assert.assertEquals(msgNum, consumeQueue.getMessageTotalInQueue()); + //TO DO Should test it + //Assert.assertEquals(100 * 100, consumeQueue.getMaxPhysicOffset()); + + + Assert.assertNull(consumeQueue.iterateFrom(-1)); + Assert.assertNull(consumeQueue.iterateFrom(msgNum)); + + { + CqUnit first = consumeQueue.getEarliestUnit(); + Assert.assertNotNull(first); + Assert.assertEquals(0, first.getQueueOffset()); + Assert.assertEquals(msgSize, first.getSize()); + Assert.assertTrue(first.isTagsCodeValid()); + } + { + CqUnit last = consumeQueue.getLatestUnit(); + Assert.assertNotNull(last); + Assert.assertEquals(msgNum - 1, last.getQueueOffset()); + Assert.assertEquals(msgSize, last.getSize()); + Assert.assertTrue(last.isTagsCodeValid()); + } + + for (int i = 0; i < msgNum; i++) { + ReferredIterator iterator = consumeQueue.iterateFrom(i); + Assert.assertNotNull(iterator); + long queueOffset = i; + while (iterator.hasNext()) { + CqUnit cqUnit = iterator.next(); + Assert.assertEquals(queueOffset, cqUnit.getQueueOffset()); + Assert.assertEquals(queueOffset * msgSize, cqUnit.getPos()); + Assert.assertEquals(msgSize, cqUnit.getSize()); + Assert.assertTrue(cqUnit.isTagsCodeValid()); + Assert.assertEquals(queueOffset, cqUnit.getTagsCode()); + Assert.assertEquals(queueOffset, cqUnit.getValidTagsCodeAsLong().longValue()); + Assert.assertEquals(1, cqUnit.getBatchNum()); + Assert.assertNotNull(cqUnit.getCqExtUnit()); + ConsumeQueueExt.CqExtUnit cqExtUnit = cqUnit.getCqExtUnit(); + Assert.assertEquals(queueOffset, cqExtUnit.getTagsCode()); + Assert.assertArrayEquals(new byte[10], cqExtUnit.getFilterBitMap()); + queueOffset++; + } + Assert.assertEquals(msgNum, queueOffset); + } + messageStore.getQueueStore().destroy(consumeQueue); + } + + @Test + public void testEstimateMessageCountInEmptyConsumeQueue() { + DefaultMessageStore master = null; + try { + master = gen(); + ConsumeQueueInterface consumeQueue = master.findConsumeQueue(TOPIC, QUEUE_ID); + MessageFilter filter = new MessageFilter() { + @Override + public boolean isMatchedByConsumeQueue(Long tagsCode, ConsumeQueueExt.CqExtUnit cqExtUnit) { + return tagsCode == "TagA".hashCode(); + } + + @Override + public boolean isMatchedByCommitLog(ByteBuffer msgBuffer, Map properties) { + return false; + } + }; + long estimation = consumeQueue.estimateMessageCount(0, 0, filter); + Assert.assertEquals(-1, estimation); + + // test for illegal offset + estimation = consumeQueue.estimateMessageCount(0, 100, filter); + Assert.assertEquals(-1, estimation); + estimation = consumeQueue.estimateMessageCount(100, 1000, filter); + Assert.assertEquals(-1, estimation); + } catch (Exception e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } finally { + if (master != null) { + master.shutdown(); + master.destroy(); + } + UtilAll.deleteFile(new File(STORE_PATH)); + } + } + + @Test + public void testEstimateMessageCount() { + DefaultMessageStore messageStore = null; + try { + messageStore = gen(); + } catch (Exception e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + + try { + try { + putMsg(messageStore); + } catch (Exception e) { + fail("Failed to put message", e); + } + + ConsumeQueueInterface cq = messageStore.findConsumeQueue(TOPIC, QUEUE_ID); + MessageFilter filter = new MessageFilter() { + @Override + public boolean isMatchedByConsumeQueue(Long tagsCode, ConsumeQueueExt.CqExtUnit cqExtUnit) { + return tagsCode == "TagA".hashCode(); + } + + @Override + public boolean isMatchedByCommitLog(ByteBuffer msgBuffer, Map properties) { + return false; + } + }; + long estimation = cq.estimateMessageCount(0, 199, filter); + Assert.assertEquals(67, estimation); + + // test for illegal offset + estimation = cq.estimateMessageCount(0, 1000, filter); + Assert.assertEquals(67, estimation); + estimation = cq.estimateMessageCount(1000, 10000, filter); + Assert.assertEquals(-1, estimation); + estimation = cq.estimateMessageCount(100, 0, filter); + Assert.assertEquals(-1, estimation); + } finally { + messageStore.shutdown(); + messageStore.destroy(); + UtilAll.deleteFile(new File(STORE_PATH)); + } + } + + @Test + public void testEstimateMessageCountSample() { + DefaultMessageStore messageStore = null; + try { + messageStore = gen(); + } catch (Exception e) { + e.printStackTrace(); + assertThat(Boolean.FALSE).isTrue(); + } + + try { + try { + putMsg(messageStore); + } catch (Exception e) { + fail("Failed to put message", e); + } + messageStore.getMessageStoreConfig().setSampleCountThreshold(10); + messageStore.getMessageStoreConfig().setMaxConsumeQueueScan(20); + ConsumeQueueInterface cq = messageStore.findConsumeQueue(TOPIC, QUEUE_ID); + MessageFilter filter = new MessageFilter() { + @Override + public boolean isMatchedByConsumeQueue(Long tagsCode, ConsumeQueueExt.CqExtUnit cqExtUnit) { + return tagsCode == "TagA".hashCode(); + } + + @Override + public boolean isMatchedByCommitLog(ByteBuffer msgBuffer, Map properties) { + return false; + } + }; + long estimation = cq.estimateMessageCount(100, 150, filter); + Assert.assertEquals(15, estimation); + } finally { + messageStore.shutdown(); + messageStore.destroy(); + UtilAll.deleteFile(new File(STORE_PATH)); + } + } +} diff --git a/store/src/test/java/org/apache/rocketmq/store/queue/QueueTestBase.java b/store/src/test/java/org/apache/rocketmq/store/queue/QueueTestBase.java new file mode 100644 index 00000000000..81dc158db53 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/queue/QueueTestBase.java @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.queue; + +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.TopicAttributes; +import org.apache.rocketmq.common.TopicConfig; +import org.apache.rocketmq.common.attribute.CQType; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.sysflag.MessageSysFlag; +import org.apache.rocketmq.store.ConsumeQueue; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.StoreTestBase; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.stats.BrokerStatsManager; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public class QueueTestBase extends StoreTestBase { + + protected ConcurrentMap createTopicConfigTable(String topic, CQType cqType) { + ConcurrentMap topicConfigTable = new ConcurrentHashMap<>(); + TopicConfig topicConfigToBeAdded = new TopicConfig(); + + Map attributes = new HashMap<>(); + attributes.put(TopicAttributes.QUEUE_TYPE_ATTRIBUTE.getName(), cqType.toString()); + topicConfigToBeAdded.setTopicName(topic); + topicConfigToBeAdded.setAttributes(attributes); + + topicConfigTable.put(topic, topicConfigToBeAdded); + return topicConfigTable; + } + + protected Callable fullyDispatched(MessageStore messageStore) { + return () -> messageStore.dispatchBehindBytes() == 0; + } + + protected MessageStore createMessageStore(String baseDir, boolean extent, ConcurrentMap topicConfigTable) throws Exception { + if (baseDir == null) { + baseDir = createBaseDir(); + } + baseDirs.add(baseDir); + MessageStoreConfig messageStoreConfig = new MessageStoreConfig(); + messageStoreConfig.setMappedFileSizeCommitLog(1024 * 8); + messageStoreConfig.setMappedFileSizeConsumeQueue(100 * ConsumeQueue.CQ_STORE_UNIT_SIZE); + messageStoreConfig.setMapperFileSizeBatchConsumeQueue(20 * BatchConsumeQueue.CQ_STORE_UNIT_SIZE); + messageStoreConfig.setMappedFileSizeConsumeQueueExt(1024); + messageStoreConfig.setMaxIndexNum(100 * 10); + messageStoreConfig.setEnableConsumeQueueExt(extent); + messageStoreConfig.setStorePathRootDir(baseDir); + messageStoreConfig.setStorePathCommitLog(baseDir + File.separator + "commitlog"); + messageStoreConfig.setHaListenPort(nextPort()); + messageStoreConfig.setMaxTransferBytesOnMessageInDisk(1024 * 1024); + messageStoreConfig.setMaxTransferBytesOnMessageInMemory(1024 * 1024); + messageStoreConfig.setMaxTransferCountOnMessageInDisk(1024); + messageStoreConfig.setMaxTransferCountOnMessageInMemory(1024); + + messageStoreConfig.setFlushIntervalCommitLog(1); + messageStoreConfig.setFlushCommitLogThoroughInterval(2); + + return new DefaultMessageStore( + messageStoreConfig, + new BrokerStatsManager("simpleTest", true), + (topic, queueId, logicOffset, tagsCode, msgStoreTime, filterBitMap, properties) -> { + }, + new BrokerConfig(), topicConfigTable); + } + + public MessageExtBrokerInner buildMessage(String topic, int batchNum) { + MessageExtBrokerInner msg = new MessageExtBrokerInner(); + msg.setTopic(topic); + msg.setTags("TAG1"); + msg.setKeys("Hello"); + msg.setBody(new byte[1024]); + msg.setKeys(String.valueOf(System.currentTimeMillis())); + msg.setQueueId(0); + msg.setSysFlag(0); + msg.setBornTimestamp(System.currentTimeMillis()); + msg.setStoreHost(storeHost); + msg.setBornHost(storeHost); + MessageAccessor.putProperty(msg, MessageConst.PROPERTY_INNER_NUM, String.valueOf(batchNum)); + msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); + if (batchNum > 1) { + msg.setSysFlag(MessageSysFlag.INNER_BATCH_FLAG); + } + if (batchNum == -1) { + MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_INNER_NUM); + } + return msg; + } +} diff --git a/store/src/test/java/org/apache/rocketmq/store/queue/SparseConsumeQueueTest.java b/store/src/test/java/org/apache/rocketmq/store/queue/SparseConsumeQueueTest.java new file mode 100644 index 00000000000..c9e290b5db5 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/queue/SparseConsumeQueueTest.java @@ -0,0 +1,168 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.queue; + +import org.apache.rocketmq.common.UtilAll; +import org.apache.rocketmq.store.CommitLog; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.concurrent.ThreadLocalRandom; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class SparseConsumeQueueTest { + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + String path; + + MessageStore defaultMessageStore; + SparseConsumeQueue scq; + + String topic = "topic1"; + int queueId = 1; + + @Before + public void setUp() throws IOException { + path = tempFolder.newFolder("scq").getAbsolutePath(); + defaultMessageStore = mock(DefaultMessageStore.class); + CommitLog commitLog = mock(CommitLog.class); + when(defaultMessageStore.getCommitLog()).thenReturn(commitLog); + when(commitLog.getCommitLogSize()).thenReturn(10 * 1024 * 1024); + MessageStoreConfig config = mock(MessageStoreConfig.class); + doReturn(config).when(defaultMessageStore).getMessageStoreConfig(); + doReturn(true).when(config).isSearchBcqByCacheEnable(); + } + + private void fillByteBuf(ByteBuffer bb, long phyOffset, long queueOffset) { + bb.putLong(phyOffset); + bb.putInt("size".length()); + bb.putLong("tagsCode".length()); + bb.putLong(System.currentTimeMillis()); + bb.putLong(queueOffset); + bb.putShort((short)1); + bb.putInt(0); + bb.putInt(0); // 4 bytes reserved + } + + @Test + public void testLoad() throws IOException { + scq = new SparseConsumeQueue(topic, queueId, path, BatchConsumeQueue.CQ_STORE_UNIT_SIZE, defaultMessageStore); + + String file1 = UtilAll.offset2FileName(111111); + String file2 = UtilAll.offset2FileName(222222); + + long phyOffset = 10; + long queueOffset = 1; + ByteBuffer bb = ByteBuffer.allocate(BatchConsumeQueue.CQ_STORE_UNIT_SIZE); + fillByteBuf(bb, phyOffset, queueOffset); + Files.createDirectories(Paths.get(path, topic, String.valueOf(queueId))); + Files.write(Paths.get(path, topic, String.valueOf(queueId), file1), bb.array(), + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + bb.clear(); + fillByteBuf(bb, phyOffset + 1, queueOffset + 1); + Files.write(Paths.get(path, topic, String.valueOf(queueId), file2), bb.array(), + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + + scq.load(); + scq.recover(); + assertEquals(scq.get(queueOffset + 1).getPos(), phyOffset + 1); + } + + private void fillByteBufSeq(ByteBuffer bb, int circle, long basePhyOffset, long baseQueueOffset) { + long phyOffset = basePhyOffset; + long queueOffset = baseQueueOffset; + + for (int i = 0; i < circle; i++) { + fillByteBuf(bb, phyOffset, queueOffset); + phyOffset++; + queueOffset++; + } + } + + @Test + public void testSearch() throws IOException { + int fileSize = 10 * BatchConsumeQueue.CQ_STORE_UNIT_SIZE; + scq = new SparseConsumeQueue(topic, queueId, path, fileSize, defaultMessageStore); + + ByteBuffer bb = ByteBuffer.allocate(fileSize); + long basePhyOffset = 101; + long baseQueueOffset = 101; + + /* 101 -> 101 ... 110 -> 110 + 201 -> 201 ... 210 -> 210 + 301 -> 301 ... 310 -> 310 + ... + */ + for (int i = 0; i < 5; i++) { + String fileName = UtilAll.offset2FileName(i * fileSize); + fillByteBufSeq(bb, 10, basePhyOffset, baseQueueOffset); + Files.createDirectories(Paths.get(path, topic, String.valueOf(queueId))); + Files.write(Paths.get(path, topic, String.valueOf(queueId), fileName), bb.array(), + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + basePhyOffset = i * 100 + 1; + baseQueueOffset = i * 100 + 1; + bb.clear(); + } + + scq.load(); + scq.recover(); + + ReferredIterator bufferConsumeQueue = scq.iterateFromOrNext(105); //in the file + assertNotNull(bufferConsumeQueue); + assertTrue(bufferConsumeQueue.hasNext()); + assertEquals(bufferConsumeQueue.next().getQueueOffset(), 105); + bufferConsumeQueue.release(); + + bufferConsumeQueue = scq.iterateFromOrNext(120); // in the next file + assertNotNull(bufferConsumeQueue); + assertTrue(bufferConsumeQueue.hasNext()); + assertEquals(bufferConsumeQueue.next().getQueueOffset(), 201); + bufferConsumeQueue.release(); + + bufferConsumeQueue = scq.iterateFromOrNext(600); // not in the file + assertNull(bufferConsumeQueue); + } + + @Test + public void testCreateFile() throws IOException { + scq = new SparseConsumeQueue(topic, queueId, path, BatchConsumeQueue.CQ_STORE_UNIT_SIZE, defaultMessageStore); + long physicalOffset = Math.abs(ThreadLocalRandom.current().nextLong()); + String formatName = UtilAll.offset2FileName(physicalOffset); + scq.createFile(physicalOffset); + + assertTrue(Files.exists(Paths.get(path, topic, String.valueOf(queueId), formatName))); + scq.putBatchMessagePositionInfo(5,4,3,2,1,(short)1); + assertEquals(4, scq.get(1).getSize()); + } +} \ No newline at end of file diff --git a/store/src/test/java/org/apache/rocketmq/store/stats/BrokerStatsManagerTest.java b/store/src/test/java/org/apache/rocketmq/store/stats/BrokerStatsManagerTest.java new file mode 100644 index 00000000000..a602da09395 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/stats/BrokerStatsManagerTest.java @@ -0,0 +1,215 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.stats; + +import org.apache.rocketmq.common.topic.TopicValidator; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static org.apache.rocketmq.common.stats.Stats.BROKER_PUT_NUMS; +import static org.apache.rocketmq.common.stats.Stats.GROUP_GET_FALL_SIZE; +import static org.apache.rocketmq.common.stats.Stats.GROUP_GET_FALL_TIME; +import static org.apache.rocketmq.common.stats.Stats.GROUP_GET_LATENCY; +import static org.apache.rocketmq.common.stats.Stats.GROUP_GET_NUMS; +import static org.apache.rocketmq.common.stats.Stats.GROUP_GET_SIZE; +import static org.apache.rocketmq.common.stats.Stats.QUEUE_GET_NUMS; +import static org.apache.rocketmq.common.stats.Stats.QUEUE_GET_SIZE; +import static org.apache.rocketmq.common.stats.Stats.QUEUE_PUT_NUMS; +import static org.apache.rocketmq.common.stats.Stats.QUEUE_PUT_SIZE; +import static org.apache.rocketmq.common.stats.Stats.SNDBCK_PUT_NUMS; +import static org.apache.rocketmq.common.stats.Stats.TOPIC_PUT_NUMS; +import static org.apache.rocketmq.common.stats.Stats.TOPIC_PUT_SIZE; +import static org.assertj.core.api.Assertions.assertThat; + +public class BrokerStatsManagerTest { + private BrokerStatsManager brokerStatsManager; + + private static final String TOPIC = "TOPIC_TEST"; + private static final Integer QUEUE_ID = 0; + private static final String GROUP_NAME = "GROUP_TEST"; + private static final String CLUSTER_NAME = "DefaultCluster"; + + @Before + public void init() { + brokerStatsManager = new BrokerStatsManager(CLUSTER_NAME, true); + brokerStatsManager.start(); + } + + @After + public void destroy() { + brokerStatsManager.shutdown(); + } + + @Test + public void testGetStatsItem() { + assertThat(brokerStatsManager.getStatsItem("TEST", "TEST")).isNull(); + } + + @Test + public void testIncQueuePutNums() { + brokerStatsManager.incQueuePutNums(TOPIC, QUEUE_ID); + String statsKey = brokerStatsManager.buildStatsKey(TOPIC, String.valueOf(QUEUE_ID)); + assertThat(brokerStatsManager.getStatsItem(QUEUE_PUT_NUMS, statsKey).getTimes().doubleValue()).isEqualTo(1L); + brokerStatsManager.incQueuePutNums(TOPIC, QUEUE_ID, 2, 2); + assertThat(brokerStatsManager.getStatsItem(QUEUE_PUT_NUMS, statsKey).getValue().doubleValue()).isEqualTo(3L); + } + + @Test + public void testIncQueuePutSize() { + brokerStatsManager.incQueuePutSize(TOPIC, QUEUE_ID, 2); + String statsKey = brokerStatsManager.buildStatsKey(TOPIC, String.valueOf(QUEUE_ID)); + assertThat(brokerStatsManager.getStatsItem(QUEUE_PUT_SIZE, statsKey).getValue().doubleValue()).isEqualTo(2L); + } + + @Test + public void testIncQueueGetNums() { + brokerStatsManager.incQueueGetNums(GROUP_NAME, TOPIC, QUEUE_ID, 1); + final String statsKey = brokerStatsManager.buildStatsKey(brokerStatsManager.buildStatsKey(TOPIC, String.valueOf(QUEUE_ID)), GROUP_NAME); + assertThat(brokerStatsManager.getStatsItem(QUEUE_GET_NUMS, statsKey).getValue().doubleValue()).isEqualTo(1L); + } + + @Test + public void testIncQueueGetSize() { + brokerStatsManager.incQueueGetSize(GROUP_NAME, TOPIC, QUEUE_ID, 1); + final String statsKey = brokerStatsManager.buildStatsKey(brokerStatsManager.buildStatsKey(TOPIC, String.valueOf(QUEUE_ID)), GROUP_NAME); + assertThat(brokerStatsManager.getStatsItem(QUEUE_GET_SIZE, statsKey).getValue().doubleValue()).isEqualTo(1L); + } + + @Test + public void testIncTopicPutNums() { + brokerStatsManager.incTopicPutNums(TOPIC); + assertThat(brokerStatsManager.getStatsItem(TOPIC_PUT_NUMS, TOPIC).getTimes().doubleValue()).isEqualTo(1L); + brokerStatsManager.incTopicPutNums(TOPIC, 2, 2); + assertThat(brokerStatsManager.getStatsItem(TOPIC_PUT_NUMS, TOPIC).getValue().doubleValue()).isEqualTo(3L); + } + + @Test + public void testIncTopicPutSize() { + brokerStatsManager.incTopicPutSize(TOPIC, 2); + assertThat(brokerStatsManager.getStatsItem(TOPIC_PUT_SIZE, TOPIC).getValue().doubleValue()).isEqualTo(2L); + } + + @Test + public void testIncGroupGetNums() { + brokerStatsManager.incGroupGetNums(GROUP_NAME, TOPIC, 1); + String statsKey = brokerStatsManager.buildStatsKey(TOPIC, GROUP_NAME); + assertThat(brokerStatsManager.getStatsItem(GROUP_GET_NUMS, statsKey).getValue().doubleValue()).isEqualTo(1L); + } + + @Test + public void testIncGroupGetSize() { + brokerStatsManager.incGroupGetSize(GROUP_NAME, TOPIC, 1); + String statsKey = brokerStatsManager.buildStatsKey(TOPIC, GROUP_NAME); + assertThat(brokerStatsManager.getStatsItem(GROUP_GET_SIZE, statsKey).getValue().doubleValue()).isEqualTo(1L); + } + + @Test + public void testIncGroupGetLatency() { + brokerStatsManager.incGroupGetLatency(GROUP_NAME, TOPIC, 1, 1); + String statsKey = String.format("%d@%s@%s", 1, TOPIC, GROUP_NAME); + assertThat(brokerStatsManager.getStatsItem(GROUP_GET_LATENCY, statsKey).getValue().doubleValue()).isEqualTo(1L); + } + + @Test + public void testIncBrokerPutNums() { + brokerStatsManager.incBrokerPutNums(); + assertThat(brokerStatsManager.getStatsItem(BROKER_PUT_NUMS, CLUSTER_NAME).getValue().doubleValue()).isEqualTo(1L); + } + + @Test + public void testOnTopicDeleted() { + brokerStatsManager.incTopicPutNums(TOPIC); + brokerStatsManager.incTopicPutSize(TOPIC, 100); + brokerStatsManager.incQueuePutNums(TOPIC, QUEUE_ID); + brokerStatsManager.incQueuePutSize(TOPIC, QUEUE_ID, 100); + brokerStatsManager.incGroupGetNums(GROUP_NAME, TOPIC, 1); + brokerStatsManager.incGroupGetSize(GROUP_NAME, TOPIC, 100); + brokerStatsManager.incQueueGetNums(GROUP_NAME, TOPIC, QUEUE_ID, 1); + brokerStatsManager.incQueueGetSize(GROUP_NAME, TOPIC, QUEUE_ID, 100); + brokerStatsManager.incSendBackNums(GROUP_NAME, TOPIC); + brokerStatsManager.incGroupGetLatency(GROUP_NAME, TOPIC, 1, 1); + brokerStatsManager.recordDiskFallBehindTime(GROUP_NAME, TOPIC, 1, 11L); + brokerStatsManager.recordDiskFallBehindSize(GROUP_NAME, TOPIC, 1, 11L); + + brokerStatsManager.onTopicDeleted(TOPIC); + + Assert.assertNull(brokerStatsManager.getStatsItem(TOPIC_PUT_NUMS, TOPIC)); + Assert.assertNull(brokerStatsManager.getStatsItem(TOPIC_PUT_SIZE, TOPIC)); + Assert.assertNull(brokerStatsManager.getStatsItem(QUEUE_PUT_NUMS, TOPIC + "@" + QUEUE_ID)); + Assert.assertNull(brokerStatsManager.getStatsItem(QUEUE_PUT_SIZE, TOPIC + "@" + QUEUE_ID)); + Assert.assertNull(brokerStatsManager.getStatsItem(GROUP_GET_SIZE, TOPIC + "@" + GROUP_NAME)); + Assert.assertNull(brokerStatsManager.getStatsItem(GROUP_GET_NUMS, TOPIC + "@" + GROUP_NAME)); + Assert.assertNull(brokerStatsManager.getStatsItem(QUEUE_GET_SIZE, TOPIC + "@" + QUEUE_ID + "@" + GROUP_NAME)); + Assert.assertNull(brokerStatsManager.getStatsItem(QUEUE_GET_NUMS, TOPIC + "@" + QUEUE_ID + "@" + GROUP_NAME)); + Assert.assertNull(brokerStatsManager.getStatsItem(SNDBCK_PUT_NUMS, TOPIC + "@" + GROUP_NAME)); + Assert.assertNull(brokerStatsManager.getStatsItem(GROUP_GET_LATENCY, "1@" + TOPIC + "@" + GROUP_NAME)); + Assert.assertNull(brokerStatsManager.getStatsItem(GROUP_GET_FALL_SIZE, "1@" + TOPIC + "@" + GROUP_NAME)); + Assert.assertNull(brokerStatsManager.getStatsItem(GROUP_GET_FALL_TIME, "1@" + TOPIC + "@" + GROUP_NAME)); + } + + @Test + public void testOnGroupDeleted() { + brokerStatsManager.incGroupGetNums(GROUP_NAME, TOPIC, 1); + brokerStatsManager.incGroupGetSize(GROUP_NAME, TOPIC, 100); + brokerStatsManager.incQueueGetNums(GROUP_NAME, TOPIC, QUEUE_ID, 1); + brokerStatsManager.incQueueGetSize(GROUP_NAME, TOPIC, QUEUE_ID, 100); + brokerStatsManager.incSendBackNums(GROUP_NAME, TOPIC); + brokerStatsManager.incGroupGetLatency(GROUP_NAME, TOPIC, 1, 1); + brokerStatsManager.recordDiskFallBehindTime(GROUP_NAME, TOPIC, 1, 11L); + brokerStatsManager.recordDiskFallBehindSize(GROUP_NAME, TOPIC, 1, 11L); + + brokerStatsManager.onGroupDeleted(GROUP_NAME); + + Assert.assertNull(brokerStatsManager.getStatsItem(GROUP_GET_SIZE, TOPIC + "@" + GROUP_NAME)); + Assert.assertNull(brokerStatsManager.getStatsItem(GROUP_GET_NUMS, TOPIC + "@" + GROUP_NAME)); + Assert.assertNull(brokerStatsManager.getStatsItem(QUEUE_GET_SIZE, TOPIC + "@" + QUEUE_ID + "@" + GROUP_NAME)); + Assert.assertNull(brokerStatsManager.getStatsItem(QUEUE_GET_NUMS, TOPIC + "@" + QUEUE_ID + "@" + GROUP_NAME)); + Assert.assertNull(brokerStatsManager.getStatsItem(SNDBCK_PUT_NUMS, TOPIC + "@" + GROUP_NAME)); + Assert.assertNull(brokerStatsManager.getStatsItem(GROUP_GET_LATENCY, "1@" + TOPIC + "@" + GROUP_NAME)); + Assert.assertNull(brokerStatsManager.getStatsItem(GROUP_GET_FALL_SIZE, "1@" + TOPIC + "@" + GROUP_NAME)); + Assert.assertNull(brokerStatsManager.getStatsItem(GROUP_GET_FALL_TIME, "1@" + TOPIC + "@" + GROUP_NAME)); + } + + @Test + public void testIncBrokerGetNumsWithoutSystemTopic() { + brokerStatsManager.incBrokerGetNumsWithoutSystemTopic(TOPIC, 1); + assertThat(brokerStatsManager.getStatsItem(BrokerStatsManager.BROKER_GET_NUMS_WITHOUT_SYSTEM_TOPIC, CLUSTER_NAME) + .getValue().doubleValue()).isEqualTo(1L); + assertThat(brokerStatsManager.getBrokerGetNumsWithoutSystemTopic()).isEqualTo(1L); + + brokerStatsManager.incBrokerGetNumsWithoutSystemTopic(TopicValidator.RMQ_SYS_TRACE_TOPIC, 1); + assertThat(brokerStatsManager.getStatsItem(BrokerStatsManager.BROKER_GET_NUMS_WITHOUT_SYSTEM_TOPIC, CLUSTER_NAME) + .getValue().doubleValue()).isEqualTo(1L); + assertThat(brokerStatsManager.getBrokerGetNumsWithoutSystemTopic()).isEqualTo(1L); + } + + @Test + public void testIncBrokerPutNumsWithoutSystemTopic() { + brokerStatsManager.incBrokerPutNumsWithoutSystemTopic(TOPIC, 1); + assertThat(brokerStatsManager.getStatsItem(BrokerStatsManager.BROKER_PUT_NUMS_WITHOUT_SYSTEM_TOPIC, CLUSTER_NAME) + .getValue().doubleValue()).isEqualTo(1L); + assertThat(brokerStatsManager.getBrokerPutNumsWithoutSystemTopic()).isEqualTo(1L); + + brokerStatsManager.incBrokerPutNumsWithoutSystemTopic(TopicValidator.RMQ_SYS_TRACE_TOPIC, 1); + assertThat(brokerStatsManager.getStatsItem(BrokerStatsManager.BROKER_PUT_NUMS_WITHOUT_SYSTEM_TOPIC, CLUSTER_NAME) + .getValue().doubleValue()).isEqualTo(1L); + assertThat(brokerStatsManager.getBrokerPutNumsWithoutSystemTopic()).isEqualTo(1L); + } +} diff --git a/store/src/test/java/org/apache/rocketmq/store/timer/StoreTestUtils.java b/store/src/test/java/org/apache/rocketmq/store/timer/StoreTestUtils.java new file mode 100644 index 00000000000..2a04392cbda --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/timer/StoreTestUtils.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.timer; + +import java.io.File; +import java.util.UUID; + +public class StoreTestUtils { + public static String createBaseDir() { + String baseDir = System.getProperty("java.io.tmpdir") + File.separator + "unitteststore-" + UUID.randomUUID(); + final File file = new File(baseDir); + if (file.exists()) { + System.exit(1); + } + return baseDir; + } + + public static void deleteFile(String fileName) { + deleteFile(new File(fileName)); + } + + public static void deleteFile(File file) { + if (!file.exists()) { + return; + } + if (file.isFile()) { + file.delete(); + } else if (file.isDirectory()) { + File[] files = file.listFiles(); + for (File file1 : files) { + deleteFile(file1); + } + file.delete(); + } + } + +} diff --git a/store/src/test/java/org/apache/rocketmq/store/timer/TimerCheckPointTest.java b/store/src/test/java/org/apache/rocketmq/store/timer/TimerCheckPointTest.java new file mode 100644 index 00000000000..f72874af2dc --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/timer/TimerCheckPointTest.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.timer; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class TimerCheckPointTest { + + private String baseDir; + + @Before + public void init() throws IOException { + baseDir = StoreTestUtils.createBaseDir(); + } + + @Test + public void testCheckPoint() throws IOException { + String baseSrc = baseDir + File.separator + "timercheck"; + TimerCheckpoint first = new TimerCheckpoint(baseSrc); + assertEquals(0, first.getLastReadTimeMs()); + assertEquals(0, first.getLastTimerLogFlushPos()); + assertEquals(0, first.getLastTimerQueueOffset()); + assertEquals(0, first.getMasterTimerQueueOffset()); + first.setLastReadTimeMs(1000); + first.setLastTimerLogFlushPos(1100); + first.setLastTimerQueueOffset(1200); + first.setMasterTimerQueueOffset(1300); + first.shutdown(); + TimerCheckpoint second = new TimerCheckpoint(baseSrc); + assertEquals(1000, second.getLastReadTimeMs()); + assertEquals(1100, second.getLastTimerLogFlushPos()); + assertEquals(1200, second.getLastTimerQueueOffset()); + assertEquals(1300, second.getMasterTimerQueueOffset()); + } + + @Test + public void testNewCheckPoint() throws IOException { + String baseSrc = baseDir + File.separator + "timercheck2"; + TimerCheckpoint first = new TimerCheckpoint(baseSrc); + assertEquals(0, first.getLastReadTimeMs()); + assertEquals(0, first.getLastTimerLogFlushPos()); + assertEquals(0, first.getLastTimerQueueOffset()); + assertEquals(0, first.getMasterTimerQueueOffset()); + assertEquals(0, first.getDataVersion().getStateVersion()); + assertEquals(0, first.getDataVersion().getCounter().get()); + first.setLastReadTimeMs(1000); + first.setLastTimerLogFlushPos(1100); + first.setLastTimerQueueOffset(1200); + first.setMasterTimerQueueOffset(1300); + first.getDataVersion().setStateVersion(1400); + first.getDataVersion().setTimestamp(1500); + first.getDataVersion().setCounter(new AtomicLong(1600)); + first.shutdown(); + TimerCheckpoint second = new TimerCheckpoint(baseSrc); + assertEquals(1000, second.getLastReadTimeMs()); + assertEquals(1100, second.getLastTimerLogFlushPos()); + assertEquals(1200, second.getLastTimerQueueOffset()); + assertEquals(1300, second.getMasterTimerQueueOffset()); + assertEquals(1400, second.getDataVersion().getStateVersion()); + assertEquals(1500, second.getDataVersion().getTimestamp()); + assertEquals(1600, second.getDataVersion().getCounter().get()); + } + + @Test + public void testEncodeDecode() throws IOException { + TimerCheckpoint first = new TimerCheckpoint(); + first.setLastReadTimeMs(1000); + first.setLastTimerLogFlushPos(1100); + first.setLastTimerQueueOffset(1200); + first.setMasterTimerQueueOffset(1300); + + TimerCheckpoint second = TimerCheckpoint.decode(TimerCheckpoint.encode(first)); + assertEquals(first.getLastReadTimeMs(), second.getLastReadTimeMs()); + assertEquals(first.getLastTimerLogFlushPos(), second.getLastTimerLogFlushPos()); + assertEquals(first.getLastTimerQueueOffset(), second.getLastTimerQueueOffset()); + assertEquals(first.getMasterTimerQueueOffset(), second.getMasterTimerQueueOffset()); + } + + @Test + public void testNewEncodeDecode() throws IOException { + TimerCheckpoint first = new TimerCheckpoint(); + first.setLastReadTimeMs(1000); + first.setLastTimerLogFlushPos(1100); + first.setLastTimerQueueOffset(1200); + first.setMasterTimerQueueOffset(1300); + first.getDataVersion().setStateVersion(1400); + first.getDataVersion().setTimestamp(1500); + first.getDataVersion().setCounter(new AtomicLong(1600)); + TimerCheckpoint second = TimerCheckpoint.decode(TimerCheckpoint.encode(first)); + assertEquals(first.getLastReadTimeMs(), second.getLastReadTimeMs()); + assertEquals(first.getLastTimerLogFlushPos(), second.getLastTimerLogFlushPos()); + assertEquals(first.getLastTimerQueueOffset(), second.getLastTimerQueueOffset()); + assertEquals(first.getMasterTimerQueueOffset(), second.getMasterTimerQueueOffset()); + assertEquals(first.getDataVersion().getStateVersion(), 1400); + assertEquals(first.getDataVersion().getTimestamp(), 1500); + assertEquals(first.getDataVersion().getCounter().get(), 1600); + } + + @After + public void shutdown() { + if (null != baseDir) { + StoreTestUtils.deleteFile(baseDir); + } + } +} diff --git a/store/src/test/java/org/apache/rocketmq/store/timer/TimerLogTest.java b/store/src/test/java/org/apache/rocketmq/store/timer/TimerLogTest.java new file mode 100644 index 00000000000..112c3ad46b2 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/timer/TimerLogTest.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.timer; + +import org.apache.rocketmq.store.SelectMappedBufferResult; +import org.junit.After; +import org.junit.Test; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertArrayEquals; + +public class TimerLogTest { + + private final Set baseDirs = new HashSet<>(); + private final List timerLogs = new ArrayList<>(); + + public TimerLog createTimerLog(String baseDir) { + if (null == baseDir) { + baseDir = StoreTestUtils.createBaseDir(); + } + TimerLog timerLog = new TimerLog(baseDir, 1024); + timerLogs.add(timerLog); + baseDirs.add(baseDir); + timerLog.load(); + return timerLog; + } + + @Test + public void testAppendRollSelectDelete() throws Exception { + TimerLog timerLog = createTimerLog(null); + ByteBuffer byteBuffer = ByteBuffer.allocate(TimerLog.UNIT_SIZE); + byteBuffer.putInt(TimerLog.UNIT_SIZE); + byteBuffer.putLong(Long.MAX_VALUE); + byteBuffer.putInt(0); + byteBuffer.putLong(Long.MAX_VALUE); + byteBuffer.putInt(0); + byteBuffer.putLong(1000); + byteBuffer.putInt(10); + byteBuffer.putInt(123); + byteBuffer.putInt(0); + long ret = -1; + for (int i = 0; i < 10; i++) { + ret = timerLog.append(byteBuffer.array(), 0, TimerLog.UNIT_SIZE); + assertEquals(i * TimerLog.UNIT_SIZE, ret); + } + for (int i = 0; i < 100; i++) { + timerLog.append(byteBuffer.array()); + } + assertEquals(6, timerLog.getMappedFileQueue().getMappedFiles().size()); + SelectMappedBufferResult sbr = timerLog.getTimerMessage(ret); + assertNotNull(sbr); + assertEquals(TimerLog.UNIT_SIZE, sbr.getByteBuffer().getInt()); + sbr.release(); + SelectMappedBufferResult wholeSbr = timerLog.getWholeBuffer(ret); + assertEquals(0, wholeSbr.getStartOffset()); + wholeSbr.release(); + timerLog.getMappedFileQueue().deleteExpiredFileByOffsetForTimerLog(1024, timerLog.getOffsetForLastUnit(), TimerLog.UNIT_SIZE); + assertEquals(1, timerLog.getMappedFileQueue().getMappedFiles().size()); + } + + @Test + public void testRecovery() throws Exception { + String basedir = StoreTestUtils.createBaseDir(); + TimerLog first = createTimerLog(basedir); + first.append(new byte[512]); + first.append(new byte[510]); + byte[] data = "Hello Recovery".getBytes(); + first.append(data); + first.shutdown(); + TimerLog second = createTimerLog(basedir); + assertEquals(2, second.getMappedFileQueue().getMappedFiles().size()); + second.getMappedFileQueue().truncateDirtyFiles(1204 + 1000); + SelectMappedBufferResult sbr = second.getTimerMessage(1024 + 510); + byte[] expect = new byte[data.length]; + sbr.getByteBuffer().get(expect); + assertArrayEquals(expect, data); + } + + @After + public void shutdown() { + for (TimerLog timerLog : timerLogs) { + timerLog.shutdown(); + timerLog.getMappedFileQueue().destroy(); + } + for (String baseDir : baseDirs) { + StoreTestUtils.deleteFile(baseDir); + } + } +} diff --git a/store/src/test/java/org/apache/rocketmq/store/timer/TimerMessageStoreTest.java b/store/src/test/java/org/apache/rocketmq/store/timer/TimerMessageStoreTest.java new file mode 100644 index 00000000000..4ce3985f6c9 --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/timer/TimerMessageStoreTest.java @@ -0,0 +1,547 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.store.timer; + +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.rocketmq.common.BrokerConfig; +import org.apache.rocketmq.common.MixAll; +import org.apache.rocketmq.common.TopicFilterType; +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageClientIDSetter; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageDecoder; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.store.ConsumeQueue; +import org.apache.rocketmq.store.DefaultMessageStore; +import org.apache.rocketmq.store.GetMessageResult; +import org.apache.rocketmq.store.GetMessageStatus; +import org.apache.rocketmq.store.MessageArrivingListener; +import org.apache.rocketmq.common.message.MessageExtBrokerInner; +import org.apache.rocketmq.store.MessageStore; +import org.apache.rocketmq.store.PutMessageResult; +import org.apache.rocketmq.store.PutMessageStatus; +import org.apache.rocketmq.store.config.FlushDiskType; +import org.apache.rocketmq.store.config.MessageStoreConfig; +import org.apache.rocketmq.store.stats.BrokerStatsManager; +import org.junit.After; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class TimerMessageStoreTest { + private final byte[] msgBody = new byte[1024]; + private static MessageStore messageStore; + private SocketAddress bornHost; + private SocketAddress storeHost; + + private final int precisionMs = 500; + + private final Set baseDirs = new HashSet<>(); + private final List timerStores = new ArrayList<>(); + private final AtomicInteger counter = new AtomicInteger(0); + + public static MessageStoreConfig storeConfig; + + @Before + public void init() throws Exception { + String baseDir = StoreTestUtils.createBaseDir(); + baseDirs.add(baseDir); + + storeHost = new InetSocketAddress(InetAddress.getLocalHost(), 8123); + bornHost = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0); + + storeConfig = new MessageStoreConfig(); + storeConfig.setMappedFileSizeCommitLog(1024 * 1024 * 1024); + storeConfig.setMappedFileSizeTimerLog(1024 * 1024 * 1024); + storeConfig.setMappedFileSizeConsumeQueue(10240); + storeConfig.setMaxHashSlotNum(10000); + storeConfig.setMaxIndexNum(100 * 1000); + storeConfig.setStorePathRootDir(baseDir); + storeConfig.setStorePathCommitLog(baseDir + File.separator + "commitlog"); + storeConfig.setFlushDiskType(FlushDiskType.ASYNC_FLUSH); + storeConfig.setTimerInterceptDelayLevel(true); + storeConfig.setTimerPrecisionMs(precisionMs); + + messageStore = new DefaultMessageStore(storeConfig, new BrokerStatsManager("TimerTest",false), new MyMessageArrivingListener(), new BrokerConfig(), new ConcurrentHashMap<>()); + boolean load = messageStore.load(); + assertTrue(load); + messageStore.start(); + } + + public TimerMessageStore createTimerMessageStore(String rootDir) throws IOException { + if (null == rootDir) { + rootDir = StoreTestUtils.createBaseDir(); + } + + TimerCheckpoint timerCheckpoint = new TimerCheckpoint(rootDir + File.separator + "config" + File.separator + "timercheck"); + TimerMetrics timerMetrics = new TimerMetrics(rootDir + File.separator + "config" + File.separator + "timermetrics"); + TimerMessageStore timerMessageStore = new TimerMessageStore(messageStore, storeConfig, timerCheckpoint, timerMetrics, null); + messageStore.setTimerMessageStore(timerMessageStore); + + baseDirs.add(rootDir); + timerStores.add(timerMessageStore); + + return timerMessageStore; + } + + private static PutMessageResult transformTimerMessage(TimerMessageStore timerMessageStore, MessageExtBrokerInner msg) { + //do transform + int delayLevel = msg.getDelayTimeLevel(); + long deliverMs; + + try { + if (msg.getProperty(MessageConst.PROPERTY_TIMER_DELAY_SEC) != null) { + deliverMs = System.currentTimeMillis() + Integer.parseInt(msg.getProperty(MessageConst.PROPERTY_TIMER_DELAY_SEC)) * 1000; + } else if (msg.getProperty(MessageConst.PROPERTY_TIMER_DELAY_MS) != null) { + deliverMs = System.currentTimeMillis() + Integer.parseInt(msg.getProperty(MessageConst.PROPERTY_TIMER_DELAY_MS)); + } else { + deliverMs = Long.parseLong(msg.getProperty(MessageConst.PROPERTY_TIMER_DELIVER_MS)); + } + } catch (Exception e) { + return new PutMessageResult(PutMessageStatus.WHEEL_TIMER_MSG_ILLEGAL, null); + } + if (deliverMs > System.currentTimeMillis()) { + if (delayLevel <= 0 && deliverMs - System.currentTimeMillis() > storeConfig.getTimerMaxDelaySec() * 1000L) { + return new PutMessageResult(PutMessageStatus.WHEEL_TIMER_MSG_ILLEGAL, null); + } + + int timerPrecisionMs = storeConfig.getTimerPrecisionMs(); + if (deliverMs % timerPrecisionMs == 0) { + deliverMs -= timerPrecisionMs; + } else { + deliverMs = deliverMs / timerPrecisionMs * timerPrecisionMs; + } + + if (timerMessageStore.isReject(deliverMs)) { + return new PutMessageResult(PutMessageStatus.WHEEL_TIMER_FLOW_CONTROL, null); + } + MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TIMER_OUT_MS, deliverMs + ""); + MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic()); + MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId())); + msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); + msg.setTopic(TimerMessageStore.TIMER_TOPIC); + msg.setQueueId(0); + } else if (null != msg.getProperty(MessageConst.PROPERTY_TIMER_DEL_UNIQKEY)) { + return new PutMessageResult(PutMessageStatus.WHEEL_TIMER_MSG_ILLEGAL, null); + } + return null; + } + + @Test + public void testPutTimerMessage() throws Exception { + Assume.assumeFalse(MixAll.isWindows()); + String topic = "TimerTest_testPutTimerMessage"; + + final TimerMessageStore timerMessageStore = createTimerMessageStore(null); + timerMessageStore.load(); + timerMessageStore.start(true); + + long curr = System.currentTimeMillis() / precisionMs * precisionMs; + long delayMs = curr + 3000; + for (int i = 0; i < 10; i++) { + for (int j = 0; j < 5; j++) { + MessageExtBrokerInner inner = buildMessage((i % 2 == 0) ? 3000 : delayMs, topic + i, i % 2 == 0); + transformTimerMessage(timerMessageStore,inner); + PutMessageResult putMessageResult = messageStore.putMessage(inner); + assertEquals(PutMessageStatus.PUT_OK, putMessageResult.getPutMessageStatus()); + } + } + + // Wait until messages have been wrote to TimerLog but the slot (delayMs) hasn't expired. + await().atMost(2000, TimeUnit.MILLISECONDS).until(new Callable() { + @Override + public Boolean call() { + return timerMessageStore.getCommitQueueOffset() == 10 * 5; + } + }); + + for (int i = 0; i < 10; i++) { + Assert.assertEquals(5, timerMessageStore.getTimerMetrics().getTimingCount(topic + i)); + } + + for (int i = 0; i < 10; i++) { + for (int j = 0; j < 5; j++) { + ByteBuffer msgBuff = getOneMessage(topic + i, 0, j, 4000); + assertNotNull(msgBuff); + MessageExt msgExt = MessageDecoder.decode(msgBuff); + assertNotNull(msgExt); + assertEquals(topic + i, msgExt.getTopic()); + // assertThat(System.currentTimeMillis()).isLessThan(delayMs + precisionMs * 2); + } + } + for (int i = 0; i < 10; i++) { + Assert.assertEquals(0, timerMessageStore.getTimerMetrics().getTimingCount(topic + i)); + } + } + + @Test + public void testTimerFlowControl() throws Exception { + String topic = "TimerTest_testTimerFlowControl"; + + storeConfig.setTimerCongestNumEachSlot(100); + TimerMessageStore timerMessageStore = createTimerMessageStore(null); + timerMessageStore.load(); + timerMessageStore.start(true); + + long curr = System.currentTimeMillis() / precisionMs * precisionMs; + // Make sure delayMs won't be over. + long delayMs = curr + 100000; + + int passFlowControlNum = 0; + for (int i = 0; i < 500; i++) { + MessageExtBrokerInner inner = buildMessage(delayMs, topic, false); + + PutMessageResult putMessageResult = transformTimerMessage(timerMessageStore,inner); + if (putMessageResult == null || !putMessageResult.getPutMessageStatus().equals(PutMessageStatus.WHEEL_TIMER_FLOW_CONTROL)) { + putMessageResult = messageStore.putMessage(inner); + } + else { + putMessageResult = new PutMessageResult(PutMessageStatus.WHEEL_TIMER_FLOW_CONTROL,null); + } + + // Message with delayMs in getSlotIndex(delayMs - precisionMs). + long congestNum = timerMessageStore.getCongestNum(delayMs - precisionMs); + assertTrue(congestNum <= 220); + if (congestNum < 100) { + assertEquals(PutMessageStatus.PUT_OK, putMessageResult.getPutMessageStatus()); + } else { + Assert.assertTrue(PutMessageStatus.PUT_OK == putMessageResult.getPutMessageStatus() + || PutMessageStatus.WHEEL_TIMER_FLOW_CONTROL == putMessageResult.getPutMessageStatus()); + if (PutMessageStatus.PUT_OK == putMessageResult.getPutMessageStatus()) { + passFlowControlNum++; + } + } + //wait reput + Thread.sleep(5); + } + assertThat(passFlowControlNum).isGreaterThan(0).isLessThan(120); + } + + + @Test + public void testPutExpiredTimerMessage() throws Exception { + // Skip on Mac to make CI pass + Assume.assumeFalse(MixAll.isMac()); + Assume.assumeFalse(MixAll.isWindows()); + + String topic = "TimerTest_testPutExpiredTimerMessage"; + + TimerMessageStore timerMessageStore = createTimerMessageStore(null); + timerMessageStore.load(); + timerMessageStore.start(true); + + long delayMs = System.currentTimeMillis() - 2 * precisionMs; + for (int i = 0; i < 10; i++) { + MessageExtBrokerInner inner = buildMessage(delayMs, topic, false); + transformTimerMessage(timerMessageStore,inner); + PutMessageResult putMessageResult = messageStore.putMessage(inner); + assertEquals(PutMessageStatus.PUT_OK, putMessageResult.getPutMessageStatus()); + } + + long curr = System.currentTimeMillis(); + for (int i = 0; i < 10; i++) { + ByteBuffer msgBuff = getOneMessage(topic, 0, i, 1000); + assertNotNull(msgBuff); + assertTrue(System.currentTimeMillis() - curr < 200); + } + } + + @Test + public void testDeleteTimerMessage() throws Exception { + String topic = "TimerTest_testDeleteTimerMessage"; + + TimerMessageStore timerMessageStore = createTimerMessageStore(null); + timerMessageStore.load(); + timerMessageStore.start(true); + + long curr = System.currentTimeMillis() / precisionMs * precisionMs; + long delayMs = curr + 1000; + String uniqKey = null; + for (int i = 0; i < 5; i++) { + MessageExtBrokerInner inner = buildMessage(delayMs, topic, false); + transformTimerMessage(timerMessageStore,inner); + if (null == uniqKey) { + uniqKey = MessageClientIDSetter.getUniqID(inner); + } + assertEquals(PutMessageStatus.PUT_OK, messageStore.putMessage(inner).getPutMessageStatus()); + } + + MessageExtBrokerInner delMsg = buildMessage(delayMs, topic, false); + transformTimerMessage(timerMessageStore,delMsg); + MessageAccessor.putProperty(delMsg, TimerMessageStore.TIMER_DELETE_UNIQUE_KEY, uniqKey); + delMsg.setPropertiesString(MessageDecoder.messageProperties2String(delMsg.getProperties())); + assertEquals(PutMessageStatus.PUT_OK, messageStore.putMessage(delMsg).getPutMessageStatus()); + + // The first one should have been deleted. + ByteBuffer msgBuff = getOneMessage(topic, 0, 0, 3000); + assertNotNull(msgBuff); + MessageExt msgExt = MessageDecoder.decode(msgBuff); + assertNotNull(msgExt); + assertNotEquals(uniqKey, MessageClientIDSetter.getUniqID(msgExt)); + + // The last one should be null. + assertNull(getOneMessage(topic, 0, 4, 500)); + } + + @Test + public void testPutDeleteTimerMessage() throws Exception { + String topic = "TimerTest_testPutDeleteTimerMessage"; + + final TimerMessageStore timerMessageStore = createTimerMessageStore(null); + timerMessageStore.load(); + timerMessageStore.start(true); + + long curr = System.currentTimeMillis() / precisionMs * precisionMs; + final long delayMs = curr + 1000; + for (int i = 0; i < 5; i++) { + MessageExtBrokerInner inner = buildMessage(delayMs, topic, false); + transformTimerMessage(timerMessageStore,inner); + assertEquals(PutMessageStatus.PUT_OK, messageStore.putMessage(inner).getPutMessageStatus()); + } + + MessageExtBrokerInner delMsg = buildMessage(delayMs, topic, false); + transformTimerMessage(timerMessageStore,delMsg); + MessageAccessor.putProperty(delMsg, TimerMessageStore.TIMER_DELETE_UNIQUE_KEY, "XXX"); + delMsg.setPropertiesString(MessageDecoder.messageProperties2String(delMsg.getProperties())); + assertEquals(PutMessageStatus.PUT_OK, messageStore.putMessage(delMsg).getPutMessageStatus()); + + // Wait until currReadTimeMs catches up current time and delayMs is over. + await().atMost(5000, TimeUnit.MILLISECONDS).until(new Callable() { + @Override + public Boolean call() { + long curr = System.currentTimeMillis() / precisionMs * precisionMs; + return curr >= delayMs + && (timerMessageStore.getCurrReadTimeMs() == curr || timerMessageStore.getCurrReadTimeMs() == curr + precisionMs); + } + }); + + for (int i = 0; i < 5; i++) { + ByteBuffer msgBuff = getOneMessage(topic, 0, i, 1000); + assertNotNull(msgBuff); + // assertThat(System.currentTimeMillis()).isLessThan(delayMs + precisionMs); + } + assertNull(getOneMessage(topic, 0, 5, 1000)); + + // Test put expired delete msg. + MessageExtBrokerInner expiredInner = buildMessage(System.currentTimeMillis() - 100, topic, false); + MessageAccessor.putProperty(expiredInner, TimerMessageStore.TIMER_DELETE_UNIQUE_KEY, "XXX"); + PutMessageResult putMessageResult = transformTimerMessage(timerMessageStore,expiredInner); + assertEquals(PutMessageStatus.WHEEL_TIMER_MSG_ILLEGAL, putMessageResult.getPutMessageStatus()); + } + + @Test + public void testStateAndRecover() throws Exception { + final String topic = "TimerTest_testStateAndRecover"; + + String base = StoreTestUtils.createBaseDir(); + final TimerMessageStore first = createTimerMessageStore(base); + first.load(); + first.start(true); + + final int msgNum = 250; + long curr = System.currentTimeMillis() / precisionMs * precisionMs; + final long delayMs = curr + 5000; + for (int i = 0; i < msgNum; i++) { + MessageExtBrokerInner inner = buildMessage((i % 2 == 0) ? 5000 : delayMs, topic, i % 2 == 0); + transformTimerMessage(first,inner); + PutMessageResult putMessageResult = messageStore.putMessage(inner); + long cqOffset = first.getCommitQueueOffset(); + assertEquals(PutMessageStatus.PUT_OK, putMessageResult.getPutMessageStatus()); + } + + // Wait until messages have written to TimerLog and currReadTimeMs catches up current time. + await().atMost(5000, TimeUnit.MILLISECONDS).until(new Callable() { + @Override + public Boolean call() { + long curr = System.currentTimeMillis() / precisionMs * precisionMs; + long cqOffset = first.getCommitQueueOffset(); + return first.getCommitQueueOffset() == msgNum + && (first.getCurrReadTimeMs() == curr || first.getCurrReadTimeMs() == curr + precisionMs); + } + }); + assertThat(first.getTimerLog().getMappedFileQueue().getMappedFiles().size()) + .isGreaterThanOrEqualTo(msgNum / (storeConfig.getMappedFileSizeTimerLog() / TimerLog.UNIT_SIZE)); + assertThat(first.getQueueOffset()).isEqualTo(msgNum); + assertThat(first.getCommitQueueOffset()).isEqualTo(first.getQueueOffset()); + assertThat(first.getCommitReadTimeMs()).isEqualTo(first.getCurrReadTimeMs()); + curr = System.currentTimeMillis() / precisionMs * precisionMs; + assertThat(first.getCurrReadTimeMs()).isLessThanOrEqualTo(curr + precisionMs); + + for (int i = 0; i <= first.getTimerLog().getMappedFileQueue().getMappedFiles().size() + 10; i++) { + first.getTimerLog().getMappedFileQueue().flush(0); + Thread.sleep(10); + } + + // Damage the timer wheel, trigger the check physical pos. + Slot slot = first.getTimerWheel().getSlot(delayMs - precisionMs); + assertNotEquals(-1, slot.timeMs); + first.getTimerWheel().putSlot(slot.timeMs, -1, Long.MAX_VALUE, slot.num, slot.magic); + first.getTimerWheel().flush(); + first.shutdown(); + + final TimerMessageStore second = createTimerMessageStore(base); + second.debug = true; + assertTrue(second.load()); + assertEquals(msgNum, second.getQueueOffset()); + assertEquals(second.getCommitQueueOffset(), second.getQueueOffset()); + assertEquals(second.getCurrReadTimeMs(), second.getCommitReadTimeMs()); + assertEquals(first.getCommitReadTimeMs(), second.getCommitReadTimeMs()); + second.start(true); + + // Wait until all messages have been written back to commitLog and consumeQueue. + await().atMost(30000, TimeUnit.MILLISECONDS).until(new Callable() { + @Override + public Boolean call() { + ConsumeQueue cq = (ConsumeQueue) messageStore.getConsumeQueue(topic, 0); + return cq != null && cq.getMaxOffsetInQueue() >= msgNum - 1; + } + }); + + for (int i = 0; i < msgNum; i++) { + ByteBuffer msgBuff = getOneMessage(topic, 0, i, 2000); + assertThat(msgBuff).isNotNull(); + } + second.shutdown(); + } + + @Test + public void testMaxDelaySec() throws Exception { + String topic = "TimerTest_testMaxDelaySec"; + + TimerMessageStore first = createTimerMessageStore(null); + first.load(); + first.start(true); + + long curr = System.currentTimeMillis() / precisionMs * precisionMs; + long delaySec = storeConfig.getTimerMaxDelaySec() + 20; + + MessageExtBrokerInner absolute = buildMessage(curr + delaySec * 1000, topic, false); + PutMessageResult putMessageResult = transformTimerMessage(first,absolute); + assertEquals(PutMessageStatus.WHEEL_TIMER_MSG_ILLEGAL, putMessageResult.getPutMessageStatus()); + + MessageExtBrokerInner relative = buildMessage(delaySec * 1000, topic, true); + putMessageResult = transformTimerMessage(first,relative); + assertEquals(PutMessageStatus.WHEEL_TIMER_MSG_ILLEGAL, putMessageResult.getPutMessageStatus()); + } + + + @Test + public void testRollMessage() throws Exception { + storeConfig.setTimerRollWindowSlot(2); + String topic = "TimerTest_testRollMessage"; + + TimerMessageStore timerMessageStore = createTimerMessageStore(null); + timerMessageStore.load(); + timerMessageStore.start(true); + + long curr = System.currentTimeMillis() / precisionMs * precisionMs; + long delayMs = curr + 4 * precisionMs; + MessageExtBrokerInner inner = buildMessage(delayMs, topic, false); + transformTimerMessage(timerMessageStore,inner); + assertEquals(PutMessageStatus.PUT_OK, messageStore.putMessage(inner).getPutMessageStatus()); + + ByteBuffer msgBuff = getOneMessage(topic, 0, 0, 5000); + assertNotNull(msgBuff); + MessageExt msgExt = MessageDecoder.decode(msgBuff); + assertNotNull(msgExt); + assertEquals(1, Integer.valueOf(msgExt.getProperty(MessageConst.PROPERTY_TIMER_ROLL_TIMES)).intValue()); + storeConfig.setTimerRollWindowSlot(Integer.MAX_VALUE); + } + + public ByteBuffer getOneMessage(String topic, int queue, long offset, int timeout) throws Exception { + int retry = timeout / 100; + while (retry-- > 0) { + GetMessageResult getMessageResult = messageStore.getMessage("TimerGroup", topic, queue, offset, 1, null); + if (null != getMessageResult && GetMessageStatus.FOUND == getMessageResult.getStatus()) { + return getMessageResult.getMessageBufferList().get(0); + } + Thread.sleep(100); + } + return null; + } + + public MessageExtBrokerInner buildMessage(long delayedMs, String topic, boolean relative) { + MessageExtBrokerInner msg = new MessageExtBrokerInner(); + msg.setTopic(topic); + msg.setQueueId(0); + msg.setTags(counter.incrementAndGet() + ""); + msg.setKeys("timer"); + if (relative) { + MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TIMER_DELAY_SEC, delayedMs / 1000 + ""); + } else { + MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TIMER_DELIVER_MS, delayedMs + ""); + } + msg.setBody(msgBody); + msg.setKeys(String.valueOf(System.currentTimeMillis())); + msg.setQueueId(0); + msg.setBornTimestamp(System.currentTimeMillis()); + msg.setBornHost(bornHost); + msg.setStoreHost(storeHost); + MessageClientIDSetter.setUniqID(msg); + TopicFilterType topicFilterType = MessageExt.parseTopicFilterType(msg.getSysFlag()); + long tagsCodeValue = + MessageExtBrokerInner.tagsString2tagsCode(topicFilterType, msg.getTags()); + msg.setTagsCode(tagsCodeValue); + msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); + return msg; + } + + private class MyMessageArrivingListener implements MessageArrivingListener { + @Override + public void arriving(String topic, int queueId, long logicOffset, long tagsCode, long msgStoreTime, + byte[] filterBitMap, Map properties) { + } + } + + @After + public void clear() { + for (TimerMessageStore store : timerStores) { + store.shutdown(); + } + for (String baseDir : baseDirs) { + StoreTestUtils.deleteFile(baseDir); + } + if (null != messageStore) { + messageStore.shutdown(); + messageStore.destroy(); + } + } +} diff --git a/store/src/test/java/org/apache/rocketmq/store/timer/TimerMetricsTest.java b/store/src/test/java/org/apache/rocketmq/store/timer/TimerMetricsTest.java new file mode 100644 index 00000000000..3c7b9b67fba --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/timer/TimerMetricsTest.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.timer; + +import org.apache.rocketmq.common.message.MessageAccessor; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageExt; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +public class TimerMetricsTest { + + + @Test + public void testTimingCount() { + String baseDir = StoreTestUtils.createBaseDir(); + + TimerMetrics first = new TimerMetrics(baseDir); + Assert.assertTrue(first.load()); + MessageExt msg = new MessageExt(); + MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, "AAA"); + first.addAndGet(msg, 1000); + MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, "BBB"); + first.addAndGet(msg, 2000); + Assert.assertEquals(1000, first.getTimingCount("AAA")); + Assert.assertEquals(2000, first.getTimingCount("BBB")); + long curr = System.currentTimeMillis(); + Assert.assertTrue(first.getTopicPair("AAA").getTimeStamp() > curr - 10); + Assert.assertTrue(first.getTopicPair("AAA").getTimeStamp() <= curr); + first.persist(); + + TimerMetrics second = new TimerMetrics(baseDir); + Assert.assertTrue(second.load()); + Assert.assertEquals(1000, second.getTimingCount("AAA")); + Assert.assertEquals(2000, second.getTimingCount("BBB")); + Assert.assertTrue(second.getTopicPair("BBB").getTimeStamp() > curr - 100); + Assert.assertTrue(second.getTopicPair("BBB").getTimeStamp() <= curr); + second.persist(); + StoreTestUtils.deleteFile(baseDir); + } + + @Test + public void testTimingDistribution() { + String baseDir = StoreTestUtils.createBaseDir(); + TimerMetrics first = new TimerMetrics(baseDir); + List timerDist = new ArrayList() {{ + add(5); + add(60); + add(300); // 5s, 1min, 5min + add(900); + add(3600); + add(14400); // 15min, 1h, 4h + add(28800); + add(86400); // 8h, 24h + }}; + for (int period : timerDist) { + first.updateDistPair(period, period); + } + + int temp = 0; + + for (int j = 0; j < 50; j++) { + for (int period : timerDist) { + Assert.assertEquals(first.getDistPair(period).getCount().get(),period + temp); + first.updateDistPair(period, j); + } + temp += j; + } + + StoreTestUtils.deleteFile(baseDir); + } +} diff --git a/store/src/test/java/org/apache/rocketmq/store/timer/TimerWheelTest.java b/store/src/test/java/org/apache/rocketmq/store/timer/TimerWheelTest.java new file mode 100644 index 00000000000..10d8cecc1fd --- /dev/null +++ b/store/src/test/java/org/apache/rocketmq/store/timer/TimerWheelTest.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.rocketmq.store.timer; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; + +import static org.junit.Assert.assertEquals; + +public class TimerWheelTest { + + private String baseDir; + + private final int slotsTotal = 30; + private final int precisionMs = 500; + private TimerWheel timerWheel; + + private final long defaultDelay = System.currentTimeMillis() / precisionMs * precisionMs; + + @Before + public void init() throws IOException { + baseDir = StoreTestUtils.createBaseDir(); + timerWheel = new TimerWheel(baseDir, slotsTotal, precisionMs); + } + + @Test + public void testPutGet() { + long delayedTime = defaultDelay + precisionMs; + + Slot first = timerWheel.getSlot(delayedTime); + assertEquals(-1, first.timeMs); + assertEquals(-1, first.firstPos); + assertEquals(-1, first.lastPos); + + timerWheel.putSlot(delayedTime, 1, 2, 3, 4); + Slot second = timerWheel.getSlot(delayedTime); + assertEquals(delayedTime, second.timeMs); + assertEquals(1, second.firstPos); + assertEquals(2, second.lastPos); + assertEquals(3, second.num); + assertEquals(4, second.magic); + } + + @Test + public void testGetNum() { + long delayedTime = defaultDelay + precisionMs; + + timerWheel.putSlot(delayedTime, 1, 2, 3, 4); + assertEquals(3, timerWheel.getNum(delayedTime)); + assertEquals(3, timerWheel.getAllNum(delayedTime)); + + timerWheel.putSlot(delayedTime + 5 * precisionMs, 5, 6, 7, 8); + assertEquals(7, timerWheel.getNum(delayedTime + 5 * precisionMs)); + assertEquals(10, timerWheel.getAllNum(delayedTime)); + } + + @Test + public void testCheckPhyPos() { + long delayedTime = defaultDelay + precisionMs; + timerWheel.putSlot(delayedTime, 1, 100, 1, 0); + timerWheel.putSlot(delayedTime + 5 * precisionMs, 2, 200, 2, 0); + timerWheel.putSlot(delayedTime + 10 * precisionMs, 3, 300, 3, 0); + + assertEquals(1, timerWheel.checkPhyPos(delayedTime, 50)); + assertEquals(2, timerWheel.checkPhyPos(delayedTime, 100)); + assertEquals(3, timerWheel.checkPhyPos(delayedTime, 200)); + assertEquals(Long.MAX_VALUE, timerWheel.checkPhyPos(delayedTime, 300)); + assertEquals(Long.MAX_VALUE, timerWheel.checkPhyPos(delayedTime, 400)); + + assertEquals(2, timerWheel.checkPhyPos(delayedTime + 5 * precisionMs, 50)); + assertEquals(2, timerWheel.checkPhyPos(delayedTime + 5 * precisionMs, 100)); + assertEquals(3, timerWheel.checkPhyPos(delayedTime + 5 * precisionMs, 200)); + assertEquals(Long.MAX_VALUE, timerWheel.checkPhyPos(delayedTime + 5 * precisionMs, 300)); + assertEquals(Long.MAX_VALUE, timerWheel.checkPhyPos(delayedTime + 5 * precisionMs, 400)); + } + + @Test + public void testPutRevise() { + long delayedTime = System.currentTimeMillis() / precisionMs * precisionMs + 3 * precisionMs; + timerWheel.putSlot(delayedTime, 1, 2); + + timerWheel.reviseSlot(delayedTime + 5 * precisionMs, 3, 4, false); + Slot second = timerWheel.getSlot(delayedTime); + assertEquals(delayedTime, second.timeMs); + assertEquals(1, second.firstPos); + assertEquals(2, second.lastPos); + + timerWheel.reviseSlot(delayedTime, TimerWheel.IGNORE, 4, false); + Slot three = timerWheel.getSlot(delayedTime); + assertEquals(1, three.firstPos); + assertEquals(4, three.lastPos); + + timerWheel.reviseSlot(delayedTime, 3, TimerWheel.IGNORE, false); + Slot four = timerWheel.getSlot(delayedTime); + assertEquals(3, four.firstPos); + assertEquals(4, four.lastPos); + + timerWheel.reviseSlot(delayedTime + 2 * slotsTotal * precisionMs, TimerWheel.IGNORE, 5, true); + Slot five = timerWheel.getRawSlot(delayedTime); + assertEquals(delayedTime + 2 * slotsTotal * precisionMs, five.timeMs); + assertEquals(5, five.firstPos); + assertEquals(5, five.lastPos); + } + + @Test + public void testRecoveryData() throws Exception { + long delayedTime = System.currentTimeMillis() / precisionMs * precisionMs + 5 * precisionMs; + timerWheel.putSlot(delayedTime, 1, 2, 3, 4); + timerWheel.flush(); + + TimerWheel tmpWheel = new TimerWheel(baseDir, slotsTotal, precisionMs); + Slot slot = tmpWheel.getSlot(delayedTime); + assertEquals(delayedTime, slot.timeMs); + assertEquals(1, slot.firstPos); + assertEquals(2, slot.lastPos); + assertEquals(3, slot.num); + assertEquals(4, slot.magic); + + tmpWheel.shutdown(); + } + + @Test(expected = RuntimeException.class) + public void testRecoveryFixedTTL() throws Exception { + timerWheel.flush(); + TimerWheel tmpWheel = new TimerWheel(baseDir, slotsTotal + 1, precisionMs); + } + + @After + public void shutdown() { + if (null != timerWheel) { + timerWheel.shutdown(); + } + if (null != baseDir) { + StoreTestUtils.deleteFile(baseDir); + } + } + + +} diff --git a/store/src/test/resources/logback-test.xml b/store/src/test/resources/logback-test.xml deleted file mode 100644 index 875b6715ac9..00000000000 --- a/store/src/test/resources/logback-test.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{5} - %msg%n - UTF-8 - - - - - - - - - - - - diff --git a/store/src/test/resources/rmq.logback-test.xml b/store/src/test/resources/rmq.logback-test.xml new file mode 100644 index 00000000000..8695d52d57c --- /dev/null +++ b/store/src/test/resources/rmq.logback-test.xml @@ -0,0 +1,36 @@ + + + + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + \ No newline at end of file diff --git a/style/rmq_checkstyle.xml b/style/rmq_checkstyle.xml index e93b353aaa1..0fb549b707d 100644 --- a/style/rmq_checkstyle.xml +++ b/style/rmq_checkstyle.xml @@ -73,7 +73,7 @@ - + @@ -101,7 +101,7 @@ - + diff --git a/style/rmq_codeStyle.xml b/style/rmq_codeStyle.xml index 7c7ce54b9c9..1cae9e196bd 100644 --- a/style/rmq_codeStyle.xml +++ b/style/rmq_codeStyle.xml @@ -105,7 +105,7 @@